this 키워드

메서드로 프로퍼티를 참조하고 변경하기 위해서는 우선 자신이 속한 객체를 가리키는 식별자를 참조해야만 가능한 일이다.

1
2
3
4
5
6
7
8
9
10
11
12
const circle = {
// 프로퍼티: 객체 고유의 상태 데이터
radius: 5,
// 메서드: 상태 데이터를 참조하고 조작하는 동작
getDiameter() {
// 이 메서드가 자신이 속한 객체의 프로퍼티나 다른 메서드를 참조하려면
// 자신이 속한 객체인 circle을 참조할 수 있어야 한다.
return 2 * circle.radius;
},
};

console.log(circle.getDiameter()); // 10
  • 객체 리터럴은 circle 변수에 할당되기 직전에 평가된다?
    = 할당 연산자에 의해서 피연산자를 할당해주기 위해서는 우측의 객체 리터럴이 평가된 값으로 존재해야 할당을 해줄 수 있기 때문이다.

하지만 위처럼 재귀적으로 자신이 속한 객체를 참조하는 것은 바람직하지 않다.

그 예시를 생성자 함수를 통해 설명해보자

1
2
3
4
5
6
7
8
9
10
11
12
13
// 생성자 함수
function Circle(radius) {
// 이 시점에는 생성자 함수 자신이 생성할 인스턴스를 가리키는 식별자를 알 수 없다.
????.radius = radius;
}

Circle.prototype.getDiameter = function () {
// 이 시점에는 생성자 함수 자신이 생성할 인스턴스를 가리키는 식별자를 알 수 없다.
return 2 * ????.radius;
};

// 생성자 함수로 인스턴스를 생성하려면 먼저 생성자 함수를 정의해야 한다.
const circle = new Circle(5);
  • 생성자 함수 내부에서 프로퍼티나 메서드를 추가하기 위해서는 자신이 생성할 인스턴스를 참조할 수 있어야 하는데, 인스턴스를 생성하려면 생성자 함수가 존재해야한다.

따라서 자신이 속한 객체, 자신이 생성할 인스턴스를 가리킬 특별한 식별자가 필요하다.

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

  • this는 코드 어디서든 참조할 수 있다. (전역에서도 가능)

this는 객체의 프로퍼티나 메소드를 참조하기 위한 자기 참조 변수이므로 객체의 메서드 내부 또는 생성자 함수 내부에서만 의미가 있다. 따라서 strict mode가 선언된 일반 함수 내부의 this는 undefined가 바인딩된다. (일반함수에선 필요 없다)

함수 호출방식과 this 바인딩

this에 바인딩될 값은 함수 호출 방식에 의해 동적으로 결정된다.

1. 일반 함수 호출

전역 객체에 바인딩된다.

중첩 함수 또한 일반 함수로 호출 시 함수 내부의 this는 전역 객체에 바인딩 된다.

매서드 내에서 정의된 중첩함수도 일반 함수로 호출되면 역시 전역 객체에 바인딩 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// var 키워드로 선언한 전역 변수 value는 전역 객체의 프로퍼티다.
var value = 1;
// const 키워드로 선언한 전역 변수 value는 전역 객체의 프로퍼티가 아니다.
// const value = 1;

const obj = {
value: 100,
foo() {
console.log("foo's this: ", this); // {value: 100, foo: ƒ}
console.log("foo's this.value: ", this.value); // 100

// 메서드 내에서 정의한 중첩 함수
function bar() {
console.log("bar's this: ", this); // window
console.log("bar's this.value: ", this.value); // 1
}

// 메서드 내에서 정의한 중첩 함수도 일반 함수로 호출되면 중첩 함수 내부의 this에는 전역 객체가 바인딩된다.
bar();
},
};

obj.foo();

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var value = 1;

const obj = {
value: 100,
foo() {
console.log("foo's this: ", this); // {value: 100, foo: ƒ}
// 콜백 함수 내부의 this에는 전역 객체가 바인딩된다.
setTimeout(function () {
console.log("callback's this: ", this); // window
console.log("callback's this.value: ", this.value); // 1
}, 100);
},
};

obj.foo();

하지만 메서드 내의 중첩함수와 콜백함수는 외부함수를 돕는 헬퍼 함수의 역할을 하는데 외부함수인 메서드와 중첩함수 또는 콜백함수의 this가 일치하지 않는다는 것은 중첩함수 또는 콜백함수가 헬퍼 함수로 동작하는 것을 어렵게 만든다.

화살표함수 내부에서 this

1
2
3
4
5
6
7
8
9
10
11
var value = 1;

const obj = {
value: 100,
foo() {
// 화살표 함수 내부의 this는 상위 스코프의 this를 가리킨다.
setTimeout(() => console.log(this.value), 100); // 100
},
};

obj.foo();

2. 메서드 호출

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const person = {
name: "Lee",
getName() {
// 메서드 내부의 this는 메서드를 호출한 객체에 바인딩된다.
return this.name;
},
};
const people = {
name: "Kim",
getName() {
return this.name;
},
};

console.log(person.getName()); // Lee
console.log(people.getName()); // Kim
  • person 객체의 getName 프로퍼티가 가리키는 함수 객체는 person 객체에 포함된 것이 아니라 독립적으로 존재하는 별도의 객체이다? 내 생각에는 this가 가리키는 것이 메서드를 소유한 객체라고 생각해도 맞지 않나?
    person 객체에 getName 프로퍼티 키가 가리키는 함수 객체를 소유하고 있는 것이 아니라 참조값을 가지므로 독립적으로 존재하는 객체를 가리키고 있는 것이 맞다.
1
2
3
4
5
6
7
8
const anotherPerson = {
name: "Kim",
};
// getName 메서드를 anotherPerson 객체의 메서드로 할당
anotherPerson.getName = person.getName;

// getName 메서드를 호출한 객체는 anotherPerson이다.
console.log(anotherPerson.getName()); // Kim
  • 새로운 객체의 프로퍼티에 person.getName 프로퍼티를 할당해줄 수 있다.

this는 getName 메서드를 호출한 객체에 바인딩된다.

프로토타입 메서드 내부에서도 마찬가지로 바인딩된다.

3. 생성자 함수 호출

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 생성자 함수
function Circle(radius) {
// 생성자 함수 내부의 this는 생성자 함수가 생성할 인스턴스를 가리킨다.
this.radius = radius;
this.getDiameter = function () {
return 2 * this.radius;
};
}

// 반지름이 5인 Circle 객체를 생성
const circle1 = new Circle(5);
// 반지름이 10인 Circle 객체를 생성
const circle2 = new Circle(10);

console.log(circle1.getDiameter()); // 10
console.log(circle2.getDiameter()); // 20

4. Function.prototype.apply/call/bind 메서드에 의한 간접 호출

apply, call, bind 메서드는 Function.prototype의 메서드이다. 이들 메서드는 모든 함수가 상속받아 사용 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
function getThisBinding() {
return this;
}

// this로 사용할 객체
const thisArg = { a: 1 };

console.log(getThisBinding()); // window

// getThisBinding 함수를 호출하면서 인수로 전달한 객체를 getThisBinding 함수의 this에 바인딩한다.
console.log(getThisBinding.apply(thisArg)); // {a: 1}
console.log(getThisBinding.call(thisArg)); // {a: 1}

call,apply 메서드는 함수를 호출하면서 첫번째 인수로 전달한 객체를 호출한 함수의 this에 바인딩한다.

  • 위 예제에서는 getThisBinding() 함수에 인수를 전달해주지 않는다.
  • call, apply의 반환값은 호출한 함수의 반환값이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function getThisBinding() {
console.log(arguments);
return this;
}

// this로 사용할 객체
const thisArg = { a: 1 };

// getThisBinding 함수를 호출하면서 인수로 전달한 객체를 getThisBinding 함수의 this에 바인딩한다.
// apply 메서드는 호출할 함수의 인수를 배열로 묶어 전달한다.
console.log(getThisBinding.apply(thisArg, [1, 2, 3]));
// Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
// {a: 1}

// call 메서드는 호출할 함수의 인수를 쉼표로 구분한 리스트 형식으로 전달한다.
console.log(getThisBinding.call(thisArg, 1, 2, 3));
// Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
// {a: 1}

call,apply 메서드로 함수를 호출하면서 호출한 함수에 인수를 전달해줄 수 있다.

유사배열 객체에 배열 메서드 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function convertArgsToArray() {
console.log(arguments);

// arguments 객체를 배열로 변환
// Array.prototype.slice를 인수없이 호출하면 배열의 복사본을 생성한다.
const arr = Array.prototype.slice.call(arguments);
// const arr = Array.prototype.slice.apply(arguments);
// const arr = Array.from(arguments)
console.log(arr);

return arr;
}

convertArgsToArray(1, 2, 3); // [1, 2, 3]
  • arguments 객체는 배열이 아니므로 배열 메서드를 사용할 수 없지만 apply, call 메서드를 사용하면 가능하다.

새로 나온 Array.from() 정적 메서드를 사용할 수 있다. 하지만 arguments 객체를 잘 안쓴다.

bind

1
2
3
4
5
6
7
8
9
10
11
12
function getThisBinding() {
return this;
}

// this로 사용할 객체
const thisArg = { a: 1 };

// bind 메서드는 첫 번째 인수로 전달한 thisArg로 this 바인딩이 교체된
// getThisBinding 함수를 새롭게 생성해 반환한다.
console.log(getThisBinding.bind(thisArg)); // getThisBinding
// bind 메서드는 함수를 호출하지는 않으므로 명시적으로 호출해야 한다.
console.log(getThisBinding.bind(thisArg)()); // {a: 1}
  • bind 메서드는 함수를 호출하지 않고 인수로 전달받은 객체로 this 바인딩이 교체된 함수를 새롭게 생성하여 반환한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const person = {
name: "Lee",
foo(callback) {
// ①
setTimeout(callback, 100);
},
};

person.foo(function () {
console.log(`Hi! my name is ${this.name}.`); // ② Hi! my name is .
// 일반 함수로 호출된 콜백 함수 내부의 this.name은 브라우저 환경에서 window.name과 같다.
// 브라우저 환경에서 window.name은 브라우저 창의 이름을 나타내는 빌트인 프로퍼티이며 기본값은 ''이다.
// Node.js 환경에서 this.name은 undefined다.
});
  • person.foo의 콜백함수가 호출되기 전 1의 시점에서 this는 foo 메서드를 호출한 객체(person)를 가리킨다.
  • 그러나 person.foo의 콜백함수가 일반 함수로서 호출된 2의 시점에서 this는 전역객체 windows를 가리킨다.
  • person.foo의 콜백함수는 헬퍼함수로 person.foo를 돕는 역할을 하기 때문에 서로의 this가 같아야한다.

이 때, bind 메서드를 사용하여 this를 일치시킨다.

1
2
3
4
5
6
7
8
9
10
11
const person = {
name: "Lee",
foo(callback) {
// bind 메서드로 callback 함수 내부의 this 바인딩을 전달
setTimeout(callback.bind(this), 100);
},
};

person.foo(function () {
console.log(`Hi! my name is ${this.name}.`); // Hi! my name is Lee.
});
  • callback 함수에 this가 바인딩된 새로운 함수를 반환

코드해설

즉, foo안의 this는 person 객체를가리키는데, 콜백함수 호출하면 this가 window를 가리킨다. 그러므로 bind함수를 사용하여 foo 메서드가 가리키는 this를 callback 함수에 바인딩해줘서 콜백함수가 가리키는 this와 일치 시켜준다.

bind, call, apply, that으로 this 바인딩을 일치 시켜주는 것 보다 화살표 함수를 사용하는 것이 간편하다. 하지만 여러 가지 방식에 대해서도 알아두자.

함수 호출 방식 this 바인딩
일반 함수 호출 전역 객체
메서드 호출 메서드를 호출한 객체
생성자 함수 호출 생성자 함수가 (미래에) 생성할 인스턴스
Function.prototype.apply/call/bind 메서드에 의한 간접호출 Function.prototype.apply/call/bind 메서드에 첫번째로 전달한 객체