책 코어자바스크립트 (위키북스 출판) 을 참고해 작성한 포스팅입니다.
this
자바스크립트에서 this는 실행 컨텍스트가 생성될 때 결정된다. 실행 컨텍스트는 함수를 호출할 때 생성되므로, this는 함수를 호출할 때 결정된다고 할 수 있다.
전역 공간에서의 this
전역 공간에서 this는 전역 컨텍스트를 생성하는 주체인 전역 객체를 가리킨다. 브라우저 환경에서는 window
이고 Node.js환경에서는 global
이 된다.
1
2
3
4
var a = 1;
console.log(a);
console.log(window.a);
console.log(this.a);
전역 공간에서 선언한 변수 a에 1을 할당하면 window.a와 this.a 모두 1이 출력된다. 전역 공간에서의 this는 전역 객체를 의미하고 자바스크립트의 모든 변수는 특정 객체의 프로퍼티로서 동작하기 때문이다.
여기에서 말하는 특정 객체란 실행 컨텍스트의 LexicalEnvironment를 의미한다. 실행 컨텍스트는 변수를 수집해 LexicalEnvironment에 프로퍼티로 저장하고 이후에 변수를 호출할 때 이 LexicalEnvironment를 조회해 일치하는 프로퍼티가 있을 경우 그 값을 반환한다.
window.a나 this.a가 아닌 a를 호출했을 때도 1이 출력되는데 이것은 변수 a에 접근하고자 하면 스코프 체인에서 a를 검색하다가 가장 마지막에 도달하는 전역 스코프의 LexicalEnvironment, 즉 전역 객체에서 해당 프로퍼티 a를 발견해 그 값을 반환하기 때문이다. 단순하게 (window.)이 생략된 것이라고 여겨도 된다.
함수 vs 메서드
어떤 함수를 실행하는 방법 중 가장 일반적인 것으로 함수로서 호출하는 경우와 메서드로서 호출하는 경우가 있다. 이 둘을 구분하는 유일한 차이는 독립성에 있다. 함수는 그 자체로 독립적인 기능을 수행하는 반면에 메서드는 자신을 호출한 대상 객체에 관한 동작을 수행한다.
1
2
3
4
5
6
7
8
9
var func = function (x) {
console.log(this, x);
};
func(1); // Window {...} 1
var obj = {
method: func
};
obj.method(2); // { method: f } 2
먼저, func
라는 변수에 익명함수를 할당했다. func
를 호출하니 this로 전역 객체 Window가 출력된다.
그리고, obj
라는 변수에 객체를 할당하고 그 객체의 method
프로퍼티에 func
함수를 할당했다. obj
의 method
를 호출하니 this로 obj
가 출력된다.
원래의 익명함수는 변함없는데 이를 변수에 담아 호출한 경우와 obj 객체의 프로퍼티에 할당해서 호출한 경우에 this가 달라지는 것을 확인할 수 있다.
메서드 내부에서의 this
this에는 호출한 주체에 대한 정보가 담긴다. 어떤 함수를 메서드로서 호출하는 경우 호출 주체는 프로퍼티명 앞의 객체가 된다.
1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
methodA: function () {
console.log(this);
},
inner: {
methodB: function () {
consolel.log(this);
}
}
};
obj.methodA(); // { methodA: f, inner: {...} } (=== obj)
obj.inner.methodB(); // { methodB: f } === (obj.inner)
함수 내부에서의 this
어떤 함수를 함수로서 호출할 때는 this가 지정되지 않는다. 함수로서 호출하는 것은 호출 주체를 명시하지 않고 개발자가 코드에 직접 관여해서 실행한 것이기 때문에 호출 주체를 알 수 없는 것이다.
this는 단어 자체가 주는 느낌 그대로 코드를 바라보면 예상과 다른 결과가 나온다. 아래 코드에서 (1)~(3)이 무엇을 출력할 지 예상해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var obj1 = {
outer: function () {
console.log(this); //(1)
var innerFunc = function () {
console.log(this); //(2) (3)
};
innerFunc();
var obj2 = {
innerMethod: innerFunc
};
obj2.innerMethod();
}
};
obj1.outer();
- 객체 내부에 outer 라는 프로퍼티가 있고 익명함수가 연결된다. 이 객체를 변수 obj1에 할당한다.
- obj1.outer를 호출한다.
- obj1.outer 함수의 실행 컨텍스트가 생성되면서 호이스팅, 스코프 체인 수집, this 바인딩을 수행한다. 이 함수는 메서드로서 호출된 것이므로 this는 obj1이 바인딩된다.
- (1)에서 ob1 객체가 출력된다.
- 호이스팅된 변수 innerFunc는 outer 스코프 내에서만 접근할 수 있는 지역변수다. 이 지역변수에 익명 함수를 할당한다.
- innerFunc를 호출한다.
- innerFunc 함수의 실행 컨텍스트가 생성되면서 호이스팅, 스코프 체인 수집, this 바인딩을 수행한다. 이 함수는 함수로서 호출된 것이므로 this는 전역객체가 바인딩된다.
- (2)에서 window가 출력된다.
- 호이스팅된 변수 obj2는 outer 스코프 내에서만 접근할 수 있는 지역변수다. 여기에 객체 내부에 innerMethod라는 프로퍼티가 있으며 앞서 정의된 변수 innerFunc와 연결된 익명함수가 연결된 객체를 할당한다
- obj2.innerMethod를 호출한다.
- obj2.innerMethod 함수의 실행 컨텍스트가 생성된다. 이 함수는 메서드로서 호출된 것이므로 this는 obj2가 바인딩된다.
- (3)에서 obj2가 출력된다.
(2)와 (3)은 같은 함수를 호출했음에도 불구하고 바인딩되는 this의 대상이 달라졌다. 오직 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지 없는지가 this 바인딩에서 중요하다.
this를 바인딩하지 않는 함수
ES6에서는 함수 내부에서 this가 전역 객체를 바라보는 문제를 보완하고자 this를 바인딩하지 않는 화살표 함수를 도입했다. 화살표 함수는 실행 컨텍스트를 생성할 때 this 바인딩 과정 자체가 빠져 상위 스코프의 this를 그대로 활용할 수 있다.
1
2
3
4
5
6
7
8
9
10
var obj = {
outer: function () {
console.log(this); // {outer: f}
var innerFunc = () => {
console.log(this); // {outer: f}
};
innerFunc();
}
};
obj.outer();
명시적으로 this를 바인딩하는 방법
call 메서드
메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령이다. 이때 call 메서드의 첫번째 인자를 this로 바인딩하고 이후의 인자들을 호출할 함수의 매개변수로 한다.
1
2
3
4
5
var func = function (a, b) {
console.log(this, a, b);
};
func.call({ x: 1 }, 4, 5); // {x:1} 4 5
call 메서드를 이용해 임의의 객체를 this로 지정할 수 있다.
apply 메서드
call 메서드와 기능적으로 동일하나 두번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다는 차이가 있다.
1
2
3
4
5
var func = function (a, b, c) {
console.log(this, a, b, c);
};
func.apply({ x: 1 }, [4, 5, 6]); // {x:1} 4 5 6
생성자 내부에서 다른 생성자를 호출하기
call 또는 apply를 이용해 다른 생성자를 호출하면 반복을 줄일 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person(name, gender) {
this.name = name;
this.gender = gender;
}
function Student(name, gender, school) {
Person.call(this, name, gender);
this.school = school;
}
function Employee(name, gender, company) {
Person.apply(this, [name, gender]);
this.company = company;
}
var juri = new Student("juri", "female", "가나대");
var jun = new Employee("jun", "male", "다라회사");
생성자 함수가 실행되기 전 빈 객체가 생성되고 이 객체가 this로 바인딩 된다. 즉, 이후 생성자 함수 내부에서 사용되는 this는 이 빈 객체를 가리킨다. 생성자 함수 내부에서 Person 생성자 함수를 호출해 인스턴스의 속성을 정의할 수 있게 된다.
bind 메서드
ES5에서 추가된 기능으로 즉시 호출하지 않고 넘겨받은 this와 인수들을 바탕으로 새로운 함수를 반환한다. 다시 새로운 함수를 호출할 때 인수를 넘기면 그 인수들은 기존 bind 메서드를 호출할 때 전달했던 인수들의 뒤에 이어서 등록된다.
1
2
3
4
5
6
7
8
9
var func = function (a, b, c) {
console.log(this, a, b, c);
};
var bindFunc1 = func.bind({ x: 1 });
bindFunc1(1, 2, 3); // { x : 1 } 1 2 3
var bindFunc2 = func.bind({ x: 1 }, 4, 5);
bindFunc2(6); // { x : 1 } 4 5 6
상위 컨텍스트의 this를 내부함수에 전달하기
call, apply, bind 메서드를 이용하면 메서드의 내부함수가 메서드의 this를 바라보게 우회할 수 있다.
(call)
1
2
3
4
5
6
7
8
9
10
var obj = {
outer: function () {
console.log(this);
var innerFunc = function () {
console.log(this);
};
innerFunc.call(this);
}
};
obj.outer();
(bind)
1
2
3
4
5
6
7
8
9
10
var obj = {
outer: function () {
console.log(this);
var innerFunc = function () {
console.log(this);
}.bind(this);
innerFunc();
}
};
obj.outer();