[JS] this (동적 바인딩)

초코침·2023년 3월 3일
0

JavaScript

목록 보기
11/26

객체는 프로퍼티메서드를 묶은 자료구조다.

동작을 나타내는 메서드는 자신이 속한 객체의 상태를 나타내는 프로퍼티를 당연히 참조 및 변경할 수 있어야 할 것이다. 자신이 포함된 객체의 상태에 접근하기 위해선 우선적으로 자신이 속한 객체를 가리킬 수 있어야 하는데, 어떻게 가리킬 수 있을까?

this의 탄생 배경

  1. 객체 리터럴로 객체를 생성하는 경우

    메서드 내부에서 메서드 자신이 속한 객체를 가리키는 식별자를 재귀적으로 참조할 수 있다.

    객체 리터럴은 변수에 할당되기 직전에 평가된다. 즉, 메서드가 호출되는 시점은 이미 객체 리터럴 평가가 완료되어 객체가 생성된 상태이며 식별자(변수)에 생성된 객체가 할당된 이후다. 따라서 메서드 내부에서 참조할 수 있게 된다.

    const circle = {
      radius: 5,
      getDiameter() {
        return 2 * circle.radius;
      },
    };

    식별자(변수명)를 정한 상태에서 객체를 생성하기 때문에, 객체 내부에서 참조하려면 할당할 변수 이름(circle)과 동일하게 써 주면 참조할 수 있게 된다.

    즉, 객체를 생성하면서 이를 가리키는 식별자까지 한 번에 알 수 있는 상황이 된다.

    (하지만 재귀적으로 참조하는 방식은 바람직하지 않다!)

  1. 생성자 함수 방식으로 객체를 생성하는 경우

    생성자 함수로 객체를 생성하려면 먼저 생성자 함수를 정의한 다음, new 연산자로 생성자 함수를 호출해야만 한다.

    function Circle(radius) {
    	????.radius=radius;
    	// 생성할 객체의 radius에 매개변수 radius를 넣어줘야 하는데 생성할 객체를 뭐라 부르지...
    }

    객체 리터럴의 경우 변수명 쓰면 되니까 접근할 수 있었지만.. 생성자 함수를 작성할 때는 변수명을 모르니까.. 뭐라하지..?

    Circle.prototype.getDiameter = function () {
    	return 2 * ????.radius;
    };

    생성할 객체의 radius에 참조해야 하는데.. 생성할 객체 이름이 뭔 줄 알고 접근하는 코드를 쓰지..?

    즉, 생성자 함수를 정의하는 시점에는 아직 인스턴스를 생성(생성자 함수를 호출)하기 이전이므로 생성자 함수가 생성할 인스턴스를 가리키는 식별자를 알 수 없다.

    따라서 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 특수한 식별자가 필요하게 된다.

What is ‘this’?

자바스크립트는 위와 같은 상황을 해결하기 위해 this라는 특수한 식별자를 제공한다.

this자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수(Self-Referencing Variable)다.

즉, this를 통해 자신이 속한 객체 또는 자신이 생성할 인스턴스의 프로퍼티나 메서드를 참조할 수 있다.

객체 리터럴로 만든 메서드 내부의 this는 메서드를 호출한 객체를 가리킨다.

const circle = {
  radius: 5,
  getDiameter() {
    return 2 * this.radius; // 식별자 circle 대신에 this 쓰면 간단!
  },
};

console.log(circle.getDiameter()) // 10

생성자 함수 내부의 this는 미래에 생성자 함수를 호출해 생성하게 될 인스턴스를 가리킨다.

function Circle(radius) {
	this.radius=radius;
	// 미래 생성할 인스턴스(this)의 radius!
}

this는 자바스크립트 엔진에 의해 암묵적으로 생성되고 코드 어디서든 참조할 수 있다. 전역에서도 함수 내부에서도 참조할 수 있다.

  • 전역 전역에서 this는 전역 객체를 가리킨다.
    • 브라우저 환경
      this는 전역 객체 window를 가리킨다.
    • node 환경
      this는 {}를 가리킨다. {}module.exports이다. window는 브라우저 런타임에서 넣는 객체기 때문에 node에는 없는 객체다.
  • 일반 함수
    일반 함수 내부에서 this는 전역과 동일하다.
    • 브라우저 환경
      마찬가지로 전역 객체인 window를 가리킨다.
    • node 환경
      일반 함수 내부에서 this는 전역과 동일하다고 했으나, node 환경에서 일반 함수 내부의 this는 node의 전역 객체인 global 객체를 가리킨다는 예외가 있다.
  • 메서드
    메서드 내부에서 this는 메서드를 호출한 객체를 가리킨다. (브라우저 환경과 node 환경 동일)
  • 생성자 함수
    생성자 함수 내부에서 this는 생성자 함수가 생성할 인스턴스를 가리킨다. (브라우저 환경과 node 환경 동일)

또한, 함수를 호출하면 arguments 객체this암묵적으로 함수 내부에 전달된다. 함수 내부에서 arguments 객체를 지역 변수같이 사용하는 것처럼 this도 지역 변수처럼 사용할 수 있다.

단, this가 가리키는 값, 즉 this 바인딩은 함수 호출 방식에 의해 동적으로 결정된다.

동적 this 바인딩

this 바인딩은 함수 호출 방식 즉, 함수가 어떻게 호출되었는지에 따라 동적으로 결정된다.

함수를 호출하는 방식은 크게 4가지가 있다.

  1. 일반 함수 호출
  2. 메서드 호출
  3. 생성자 함수 호출
  4. Function.prototype.apply, Function.prototype.call, Function.prototype.bind 메서드에 의한 간접 호출

일반 함수 호출

일반 함수로 호출된 모든 함수(중첩 함수, 콜백 함수) 내부의 this는 전역 객체가 바인딩된다.

전역 함수, 중첩 함수 등 일반 함수로 호출하기만 하면 호출 당시 this는 전역 객체를 가리킨다.

function outer() {
  console.log(this); // 브라우저: window 객체, node: global 객체

  function inner() {
    console.log(this); // 브라우저: window 객체, node: global 객체
  }
  
  inner();
}

outer();

콜백 함수가 일반 함수로 호출된다면 콜백 함수 내부의 this에도 전역 객체가 바인딩된다. 어떠한 함수라도 일반 함수로 호출되면 this는 전역 객체다.

이때, 메서드의 this와 메서드 내부의 중첩 함수 또는 콜백 함수의 this가 일치하지 않는 것은 중첩 함수 또는 콜백 함수가 헬퍼 함수로 동작하기 어렵게 만들기 때문에 명시적으로 this를 일치시켜 주는 것이 좋다.

var value = 'globalValue'; // var로 선언한 변수는 전역 객체 window의 프로퍼티가 된다.

const obj = {
  value: 1,
  func() {
    console.log(this); // 자신을 호출한 obj를 가리킨다. -> {value: 1, func: f}
    console.log(this.value); // 자신을 호출한 obj의 value 프로퍼티 값을 읽어온다. -> 1

		const that=this; // 메서드의 this(메서드를 호출한 객체 obj)를 담고 있음

    setTimeout(function () { // 메서드 내부에서 일반 함수로 호출되었다.
      console.log(this); // 일반 함수로 호출된 함수 내부의 this는 전역 객체 -> window
      console.log(this.value); // 전역 객체 window의 value -> 'globalValue'

			console.log(that.value); // that은 메서드를 호출한 객체 obj -> 1
    });
  },
};

obj.func();

이외에도 apply, call, bind, 화살표 함수로 this 바인딩을 일치시킬 수 있다.

참고: 화살표 함수 내부의 this는 상위 스코프의 this를 가리킨다!

다만, 객체를 생성하지 않는 일반 함수에서 this는 의미가 없다. 따라서 strict mode가 적용된 일반 함수 내부의 thisundefined가 바인딩된다.

'use strict';
function outer() {
  console.log(this); // undefined

  function inner() {
    console.log(this); // undefined
  }
  
  inner();
}

outer();

메서드 호출

메서드 내부의 this는 메서드를 호출한 객체가 바인딩된다.

단, 메서드를 소유한 객체가 아닌 메서드를 호출한 객체에 바인딩된다. 바인딩은 호출 기준!

다음과 같이 객체를 만들었다고 했을 때, getName 메서드는 person 객체에 포함된 것이 아닌 독립적으로 존재하는 별도의 객체다.

const person = {
	name: 'choco',
	getName() {
		return this.name;
	}
}

독립적인 객체기 때문에 메서드를 다른 객체의 프로퍼티에 할당할 수도 있고, 일반 변수에 할당해서 일반 함수로 호출할 수도 있다.

그렇다면 다른 객체의 프로퍼티에 할당했을 때 어떻게 될까?

메서드 호출에서의 this는 소유한 객체가 아닌 메서드를 호출한 객체를 기준으로 하기 때문에 호출한 객체 즉, getName이라는 메서드 내부의 this는 anotherPerson이다.

const anotherPerson = {
	name: 'cheeeeeem'
}

anotherPerson.getName = person.getName;

console.log(anotherPerson.getName()); // 'cheeeeem'

person과 anotherPerson은 같은 getName을 참조하고 있다.

또한, 메서드를 일반 변수에 할당해서 일반 함수처럼 호출한다면, 역시나 전역 객체를 가리키게 된다.

const general = person.getName;

console.log(general());
// this.name에서 this는 window를 가리켜 window의 name 프로퍼티를 참조 -> '' (브라우저 환경)
// node 환경에서 this.name은 undefined

생성자 함수 호출

생성자 함수 내부의 this에는 생성자 함수가 미래에 생성할 인스턴스가 바인딩된다.

function Person(name) {
	this.name = name;
	this.getName = function () {
		return this.name;
	}
}

const choco = new Person('choco');
console.log(choco.getName()); // this는 생성자 함수로 생성된 인스턴스인 choco -> 'choco'

마찬가지로 생성자 함수를 new 연산자와 함께 사용하지 않고 일반 함수처럼 호출한다면 this는 전역 객체다.

function Person(name) {
	this.name = name;
	this.getName = function () {
		return this.name;
	}
}

console.log(Person('choco')); // Person 함수는 반환값이 없음 -> undefined
console.log(name); // this.name으로 전역의 name에 'choco'를 할당 -> 'choco'

메서드에 의한 간접 호출

Function.prototype.apply

this로 사용할 객체와 인수 리스트를 배열로 전달받아 함수를 호출한다.

함수를 호출하면서 첫 번째 인수로 전달한 특정 객체를 호출한 함수의 this에 바인딩하며 call과 똑같이 동작한다.

/**
 * @param thisArg - this로 사용할 객체
 * @param argsArray - 함수에게 전달할 인수 리스트의 배열 또는 유사 배열 객체
 * @returns 호출된 함수의 반환값
 */
Function.prototype.apply(thisArg[, argsArray]);

Function.prototype.call

this로 사용할 객체와 인수 리스트를 쉼표로 구분해 전달받아 함수를 호출한다.

함수를 호출하면서 첫 번째 인수로 전달한 특정 객체를 호출한 함수의 this에 바인딩하며 apply와 똑같이 동작한다.

/**
 * @param thisArg - this로 사용할 객체
 * @param arg1, arg2, ... - 함수에게 전달할 인수 리스트
 * @returns 호출된 함수의 반환값
 */
Function.prototype.apply(thisArg[, arg1[, arg2[, ...]);

즉, applycall은 인수를 전달하는 방식이 다를 뿐 동일하게 동작한다.

getThisBinding 함수에 thisArg라는 객체를 바인딩해줬기 때문에 getThisBinding 함수의 리턴은 thisArg가 된다.

const thisArg = { a: 1 };

function getThisBinding() {
  console.log(arguments);
  return this;
}

console.log(getThisBinding.apply(thisArg, [1, 2, 3])); // { a: 1 }
console.log(getThisBinding.call(thisArg, 1, 2, 3)); // { a: 1 }

Function.prototype.bind

첫 번째 인수로 전달한 값으로 this 바인딩이 교체된 함수를 새롭게 생성해 반환한다.

apply, call과 다르게 함수를 호출하지 않는다.

const thisArg = { a: 1 };

function getThisBinding() {
  return this;
}

console.log(getThisBinding.bind(thisArg)); // this가 변경된 getThisBinding 함수를 리턴
console.log(getThisBinding.bind(thisArg)()); // this를 변경하고 함수 실행 -> { a: 1 }
profile
블로그 이사중 🚚 (https://sungjihyun.vercel.app)

0개의 댓글