[3장] this
Table of contents
자바스크립트에서 this
는 혼란스러운 개념 중 하나이다. 다른 객체지향 언어와 달리 자바스크립트에서는 this
가 클래스뿐만 아니라 함수와 객체(메서드)에서도 사용되며, 상황에 따라 this
가 바라보는 대상이 달라질 수 있기 때문이다.
함수와 객체(메서드)의 구분이 느슨한 자바스크립트에서 this
는 실질적으로 이 둘을 구분하는 거의 유일한 기능이다.
상황별로 this
가 어떻게 달라지는지, 왜 그렇게 되는지, 예상과 다른 대상을 바라보고 있을 경우 그 원인을 효과적으로 추적하는 방법 등을 살펴보자.
1. 상황에 따라 달라지는 this
자바스크립트에서 this
는 실행 컨텍스트가 생성될 때(함수를 호출할 때) 결정되며, 함수를 어떤 방식으로 호출하느냐에 따라 값이 달라진다.
1-1 전역 공간에서의 this
전역 공간에서 this
는 전역 객체를 가리키며, 자바스크립트의 런타임 환경에 따라 window
또는 global
이 될 수 있다. 이는 전역 객체가 전역 컨텍스트를 생성하는 주체이기 때문이다.
전역 공간에서의 this(브라우저 환경)
console.log(this === window); // ture
전역공간에서의 this(Node.js 환경)
console.log(this === global); // true
전역 공간에서만 발생하는 특이한 성질 하나를 살펴보자.
전역변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로도 할당한다. 변수이면서 객체의 프로퍼티이기도 한 셈이다.
전역변수와 전역객체(1)
var a = 1;
console.log(a); // 1
console.log(window.a); // 1
console.log(this.a); // 1
전역 공간에서 변수 a
를 var
로 선언하고 1을 할당하면, 이 변수는 전역 객체의 프로퍼티가 된다. 따라서 window.a
와 this.a
는 모두 1을 출력한다. 이는 자바스크립트에서 모든 변수가 실제로 특정 객체의 프로퍼티로 동작하기 때문이다. 이 때, 특정 객체는 실행 컨텍스트의 LexicalEnvironment(L.E
)이며, 전역 컨텍스트의 경우 L.E
는 전역 객체를 참조한다. 따라서 변수를 호출하면 L.E
를 조회하여 해당 프로퍼티의 값을 반환한다.
그런데 a
를 직접 호출할 때도 1이 나오는 이유는 무엇일까?
변수 a
에 접근하고자 하면 스코프 체인에서 a
를 검색하다가 가장 마지막에 도달하는 전역 스코프의 L.E
즉 전역객체에서 해당 프로퍼티 a
를 발견해서 그 값을 반환하기 때문이다. 이 원리는 단순하게 (window.)
이 생략된 것으로 여겨도 무방하다.
그렇다면 우리는 전역 공간에서 변수를 var
로 선언하는 대신 window
의 프로퍼티에 직접 할당하더라도, 결과적으로 var
로 선언한 것과 똑같이 동작할 것이라고 예상할 수 있다.
다음과 같이 대부분의 경우에는 그렇다.
var a = 1;
window.b = 2;
console.log(a, window.a, this.a); // 1 1 1
console.log(b, window.b, this.b); // 2 2 2
window.a = 3;
b = 4;
console.log(a, window.a, this.a); // 3 3 3
console.log(b, window.b, this.b); // 4 4 4
하지만 '삭제' 명령에 대해서는 전혀 다르다.
var a = 1;
delete window.a; // false
console.log(a, window.a, this.a); // 1 1 1
var b = 2;
delete b; // false
console.log(b, window.b, this.b); // 2 2 2
위 예제를 살펴보면 전역변수로 선언한 경우에는 삭제가 되지 않는다.
참고
변수에
delete
연산자를 쓰는 것이 이상해보일 수도 있지만, 앞서 설명한 바와 같이(window.)
을 생략한 것으로 이해하면 된다. 전역변수가 곧 전역객체의 프로퍼티 이므로 문제가 되지 않는다.
반면 다음과 같이 처음부터 전역객체의 프로퍼티로 할당한 경우에는 삭제가 되는 것을 확인할 수 있다.
window.c = 3;
delete window.c; // true
console.log(c, window.c, this.c); // Uncaught ReferenceError: c is not defined
window.d = 4;
delete d; // true
console.log(d, window.d, this.d); // Uncaught ReferenceError: d is not defined
이는 사용자가 의도치 않게 삭제하는 것을 방지하는 차원에서 마련한 나름의 방어 전략이라고 해석된다. 즉, 전역 변수를 선언하면 자바스크립트 엔진이 이를 자동으로 전역 객체의 프로퍼티로 할당하면서 추가적으로 해당 프로퍼티의 configurable
속성(변경 및 삭제 가능성)을 false
로 정의하는 것이다.
이처럼 var
로 선언한 전역 변수와 전역 객체의 프로퍼티는 호이스팅 여부 및 configurable
여부에서 차이를 보인다.
1-2 메서드로서 호출할 때 그 메서드 내부에서의 this
함수 vs 메서드
어떤 함수를 실행하는 가장 일반적인 방법은 함수로서 호출하는 경우와 메서드로서 호출하는 경우이다. 이 둘을 구분하는 차이는 독립성에 있는데 함수는 그 자체로 독립적인 기능을 수행하고, 메서드는 자신을 호출한 대상 객체에 관한 동작을 수행한다. 자바스크립트는 상황별로 this
키워드에 다른 값을 부여하게 함으로써 이를 구현했다.
함수를 객체의 프로퍼티에 할당한다고 해서 그 자체로 메서드가 되는 것은 아니며, 객체의 메서드로서 호출할 경우에만 메서드로 동작한다.
함수를 호출하는 방식에 따라 '함수로서 호출'과 '메서드로서 호출'을 구분할 수 있다. 예제와 함께 살펴보자.
함수로서 호출
함수를 변수에 담아 호출하는 방식으로, 함수 내부에서 사용되는 this
는 전역 객체를 참조한다.
var func = function(x) {
console.log(this, x); // window 객체를 참조
};
func(1); // 함수로서 호출
메서드로서 호출
객체의 프로퍼티에 할당된 함수를 호출하는 방식으로, 함수 내부에서 사용되는 this
는 해당 객체를 참조한다.
var func = function(x) {
console.log(this, x);
};
var obj = {
method: func,
};
obj.method(2); // 메서드로서 호출
따라서, 어떤 함수를 호출할 때 그 함수 이름(프로퍼티명) 앞에 객체가 명시되어 있는 경우에는 메서드로 호출한 것이고, 그렇지 않은 모든 경우에는 함수로 호출한 것으로 볼 수 있다.
var obj = {
method: function(x) {
console.log(this, x);
},
};
obj.method(1); // { method: f } 1
obj['method'](2); // { method: f } 2
메서드 내부에서의 this
this
에는 호출한 주체에 대한 정보가 담긴다. 어떤 함수를 메서드로서 호출하는 경우 호출 주체는 바로 함수명(프로퍼티명) 앞의 객체이다. 점 표기법의 경우 마지막 점 앞에 명시된 객체가 곧 this
가 되는 것이다.
메서드 내부에서의 this
var obj = {
methodA: function() {
console.log(this);
},
inner: {
methodB: function() {
console.log(this);
},
},
};
obj.methodA(); // { methodA: f, inner: {...} } ( === obj)
obj['methodA'](); // { methodA: f, inner: {...} } ( === obj)
obj.inner.methodB(); // { methodB: f } ( === obj.inner)
obj.inner['methodB'](); // { methodB: f } ( === obj.inner)
obj['inner'].methodB(); // { methodB: f } ( === obj.inner)
obj['inner']['methodB'](); // { methodB: f } ( === obj.inner)
1-3 함수로서 호출할 때 그 함수 내부에서의 this
함수 내부에서의 this
함수를 함수로서 호출 시, 함수 내부의 this
는 전역 객체를 참조한다. 이는 this
가 호출한 주체에 대한 정보를 담는데, 함수로서 호출할 때는 개발자가 직접 관여해서 실행한 것이라 호출 주체의 정보를 알 수 없기 때문이다.
실행 컨텍스트를 활성화할 당시에 this
가 지정되지 않은 경우 this
는 전역 객체를 바라본다. 따라서 함수에서의 this
는 전역 객체를 가리키며, 더글라스 크락포드는 이를 명백한 설계상의 오류라고 지적했다.
메서드의 내부함수에서의 this
메서드 내부에서 정의하고 실행한 함수의 this
는 앞서말한 '설계상의 오류'로 인해 예측과 다르게 동작할 수 있다.
다음 예제를 통해 확인해보자.
내부함수에서의 this
01 var obj1 = {
02 outer: function() {
03 console.log(this); // { outer: ƒ } ( === obj1 )
04 var innerFunc = function() {
05 console.log(this);
06 };
07 innerFunc(); // Window { ... } ( === 전역객체)
08
09 var obj2 = {
10 innerMethod: innerFunc,
11 };
12 obj2.innerMethod(); // {innerMethod: ƒ} ( === obj2 )
13 },
14 };
15 obj1.outer();
- 7번째 줄에서는
outer
메서드 내부에 있는innerFunc
를 함수로서 호출했다. - 12번째 줄에서는 같은 함수
innerFunc
를 메서드로서 호출했다.
같은 함수임에도 7번째 줄에 의해 바인딩되는 this
와 12번째 줄에 의해 바인딩되는 this
의 대상이 서로 달라졌다.
위와같이 this
바인딩에 관해서는 함수를 실행하는 당시의 주변 환경(메서드 내부인지, 함수 내부인지 등)은 중요하지 않고, 오직 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지 없는지가 관건이다.
메서드 내부 함수에서의 this를 우회하는 방법
아쉽게도 ES5까지는 자체적으로 내부함수에 this
를 상속할 방법이 없지만, 다행히 이를 우회할 방법이 있다.
대표적인 방법은 바로 변수를 활용하는 것이다.
내부함수에서의 this를 우회하는 방법
var obj = {
outer: function() {
console.log(this); // (1) { outer: f }
var innerFunc1 = function() {
console.log(this); // (2) Window { ... }
};
innerFunc1();
var self = this;
var innerFunc2 = function() {
console.log(self); // (3) { outer: f }
};
innerFunc2();
},
};
obj.outer();
innerFunc1
의 내부에서this
는 전역객체를 가리킨다.outer
스코프에서self
라는 변수에this
를 저장한 상태에서 호출한innerFunc2
의 경우self
에는 객체obj
가 출력된다.
사람마다 _this
, that
, _
등 여러 변수명을 쓰는데 self
가 가장 많이 쓰이는 것으로 보인다.
this를 바인딩하지 않는 함수
ES6에서는 함수 내부에서 this
가 전역객체를 바라보는 문제를 보완하고자 this
를 바인딩하지 않는 화살표 함수를 새로 도입했다. 화살표 함수는 실행 컨텍스트를 생성할 때 this
바인딩 과정 자체가 빠지게 되어, 상위 스코프의 this
를 그대로 활용할 수 있다.
this를 바인딩하지 않는 함수(화살표 함수)
var obj = {
outer: function() {
console.log(this); // (1) { outer: f }
var innerFunc = () => {
console.log(this); // (2) { outer: f }
};
innerFunc();
},
};
obj.outer();
그 밖에도 call, aplly
등의 메서드를 활용해 함수를 호출할 때 명시적으로 this
를 지정하는 방법이 있다.
1-4 콜백 함수 호출 시 그 함수 내부에서의 this
함수 A의 제어권을 다른 함수(또는 메서드) B에게 넘겨주는 경우 함수 A를 콜백함수라 한다. 이때 함수 A는 함수 B의 내부 로직에 따라 실행되며, this
역시 함수 B 내부 로직에서 정한 규칙에 따라 값이 결정된다.
콜백 함수도 함수이기 때문에 기본적으로 this
가 전역객체를 참조하지만, 제어권을 받은 함수에서 콜백 함수에 별도로 this
가 될 대상을 지정한 경우에는 그 대상을 참조하게 된다.
콜백 함수 내부에서의 this
setTimeout(function() { console.log(this); }, 300); // (1)
[1, 2, 3, 4, 5].forEach(function(x) { // (2)
console.log(this, x);
});
document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a')
.addEventListener('click', function(e) { // (3)
console.log(this, e);
});
(1) :
setTimeout
함수는 300ms 만큼 시간 지연을 한 뒤 콜백 함수를 실행하라는 명령이다. 0.3초 뒤 전역객체가 출력된다.(2) :
forEach
메서드는 배열의 각 요소를 앞에서부터 차례로 하나씩 꺼내어 그 값을 콜백 함수의 첫 번째 인자로 삼아 함수를 실행하라는 명령이다. 전역객체와 배열의 각 요소가 총 5회 출력된다.(3) :
addEventListener
는 지정한 HTML 엘리먼트에 'click'이벤트가 발생할 때마다 그 이벤트 정보를 콜백 함수의 첫 번째 인자로 삼아 함수를 실행하라는 명령이다. 버튼을 클릭하면 앞서 지정한 엘리먼트와 클릭 이벤트에 관한 정보가 담긴 객체가 출력된다.
(1)의 setTimeout
함수와 (2)의 forEach
메서드는 그 내부에서 콜백 함수를 호출할 때 대상이 될 this
를 지정하지 않는다. 따라서 콜백 함수 내부에서의 this
는 전역객체를 참조한다.
한편, (3)의 addEventListener
메서드는 콜백 함수를 호출할 때 자신의 this
를 상속하도록 정의되어 있다. 콜백 함수가 addEventListener
에 의해 호출될 때, this
값이 자동으로 이벤트 리스너가 붙어 있는 요소인 버튼을 가리키게 된다는 것이다.
즉, addEventListener
메서드는 콜백 함수를 호출할 때, 그 콜백 함수 안의 this
를 이벤트 리스너가 추가된 요소(여기서는 버튼)로 설정한다. 그래서 콜백 함수 내에서 this
를 사용하면, 그것은 해당 버튼을 참조하게 되는 것이다.
이처럼 콜백 함수에서의 this
는 '무조건 ~이다.'라고 정의할 수 없다. 콜백 함수의 제어권을 가지는 함수(메서드)가 콜백 함수에서의 this
를 무엇으로 할지를 결정하며, 특별히 정의하지 않은 경우에는 기본적으로 함수와 마찬가지로 전역객체를 바라본다.
1-5 생성자 함수 내부에서의 this
생성자 함수는 어떤 공통된 성질을 지니는 객체들을 생성하는 데 사용하는 함수이다. 객체 지향 언어에서는 생성자를 클래스, 클래스를 통해 만든 객체를 인스턴스라고 한다.
생성자는 구체적인 인스턴스를 만들기 위한 일종의 틀이다. 이 틀에는 해당 클래스의 공통 속성들이 미리 준비되어 있고, 여기에 구체적인 인스턴스의 개성을 더해 개별 인스턴스를 만들 수 있는 것이다.
자바스크립트는 함수에 생성자로서의 역할을 함께 부여했다. new
명령어와 함께 함수를 호출하면 해당 함수가 생성자로서 동작하게 된다. 그리고 어떤 함수가 생성자 함수로서 호출된 경우 내부에서의 this
는 곧 새로 만들 구체적인 인스턴스 자신이 된다.
생성자 함수를 호출하면 우선 생성자의 prototype
프로퍼티를 참조하는 __proto__
라는 프로퍼티가 있는 객체(인스턴스)를 만들고, 미리 준비된 공통 속성 및 개성을 해당 객체(this
)에 부여한다. 이렇게 해서 구체적인 인스턴스가 만들어진다.
생성자 함수
01 var Cat = function(name, age) {
02 this.bark = '야옹';
03 this.name = name;
04 this.age = age;
05 };
06 var choco = new Cat('초코', 7);
07 var nabi = new Cat('나비', 5);
08 console.log(choco, nabi);
/* 결과
Cat { bark: '야옹', name: '초코', age: 7 }
Cat { bark: '야옹', name: '나비', age: 5 }
*/
new
명령어와 함께Cat
함수를 호출해 변수choco
,nabi
에 각각 할당- 각각
Cat
클래스의 인스턴스 객체가 출력됨 - 6번째 줄에서 실행한 생성자 함수 내부에서의
this
는choco
인스턴스를 가리킴 - 7번째 줄에서 실행한 생성자 함수 내부에서의
this
는nabi
인스턴스를 가리킴
2. 명시적으로 this를 바인딩하는 방법
this
에 별도의 대상을 바인딩하는 방법도 있다.
2-1 call 메서드
Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])
call
메서드는 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령이다. 이때 call
메서드의 첫 번째 인자를 this
로 바인딩하고, 이후의 인자들을 호출할 함수의 매개변수로 한다.
call 메서드(1)
var func = function(a, b, c) {
console.log(this, a, b, c);
};
func(1, 2, 3); // Window{ ... } 1 2 3
func.call({ x: 1 }, 4, 5, 6); // { x: 1 } 4 5 6
- 함수를 그냥 실행하면
this
는 전역객체를 참조하지만,call
메서드를 이용하면 임의의 객체를this
로 지정할 수 있다.
call 메서드(2)
var obj = {
a: 1,
method: function(x, y) {
console.log(this.a, x, y);
},
};
obj.method(2, 3); // 1 2 3
obj.method.call({ a: 4 }, 5, 6); // 4 5 6
- 메서드에 대해서도 마찬가지로 객체의 메서드를 그냥 호출하면
this
는 객체를 참조하지만call
메서드를 이용하면 임의의 객체를this
로 지정할 수 있다.
2-2 apply 메서드
Function.prototype.apply(thisArg[, argsArray])
apply
메서드는 call
메서드와 기능적으로 완전히 동일하다.
call
메서드는 첫 번째 인자를 제외한 나머지 모든 인자들을 호출할 함수의 매개변수로 지정하는 반면, apply
메서드는 두 번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다는 점에서만 차이가 있다.
apply 메서드
var func = function(a, b, c) {
console.log(this, a, b, c);
};
func.apply({ x: 1 }, [4, 5, 6]); // { x: 1 } 4 5 6
var obj = {
a: 1,
method: function(x, y) {
console.log(this.a, x, y);
},
};
obj.method.apply({ a: 4 }, [5, 6]); // 4 5 6
2-3 call / apply 메서드의 활용
유사배열객체에 배열 메서드를 적용
객체에는 배열 메서드를 직접 적용할 수 없지만, 키가 0 또는 양의 정수인 프로퍼티가 존재하고 length
프로퍼티의 값이 0 또는 양의 정수인 객체, 즉 배열의 구조와 유사한 객체의 경우 call
또는 apply
메서드를 이용해 배열 메서드를 차용할 수 있다.
call/apply 메서드의 활용 1-1) 유사 배열객체에 배열 메서드를 적용
var obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
};
Array.prototype.push.call(obj, 'd');
console.log(obj); // { 0: 'a', 1: 'b', 2: 'c', 3: 'd', length: 4 }
var arr = Array.prototype.slice.call(obj);
console.log(arr); // [ 'a', 'b', 'c', 'd' ]
- 배열 메서드인
push
를 객체obj
에 적용해 프로퍼티 3에 'd'를 추가했다. slice
메서드를 적용해 객체를 배열로 전환했다.slice
에 매개변수를 아무것도 넘기지 않을 경우는 원본 배열의 얕은 복사본을 반환한다.
함수 내부에서 접근할 수 있는 arguments
객체도 유사배열객체이므로 배열로 전환해서 활용할 수 있다. querySelectorAll
, gelElementsByClassName
등의 Node
선택자로 선택한 결과인 NodeList
도 마찬가지이다.
call/apply 메서드의 활용 1-2) arguments, NodeList에 배열 메서드를 적용
function a() {
var argv = Array.prototype.slice.call(arguments);
argv.forEach(function(arg) {
console.log(arg);
});
}
a(1, 2, 3);
document.body.innerHTML = '<div>a</div><div>b</div><div>c</div>';
var nodeList = document.querySelectorAll('div');
var nodeArr = Array.prototype.slice.call(nodeList);
nodeArr.forEach(function(node) {
console.log(node);
});
그 밖에도 유사배열객체에는 call
/apply
메서드를 이용해 모든 배열 베서드를 적용할 수 있다. 배열처럼 인덱스와 length
프로퍼티를 지니는 문자열에 대해서도 마찬가지이다.
call/apply 메서드의 활용 1-3) 문자열에 배열 메서드 적용 예시
var str = 'abc def';
Array.prototype.every.call(str, function(char) {
return char !== ' ';
}); // false
Array.prototype.some.call(str, function(char) {
return char === ' ';
}); // true
var newArr = Array.prototype.map.call(str, function(char) {
return char + '!';
});
console.log(newArr); // ['a!', 'b!', 'c!', ' !', 'd!', 'e!', 'f!']
var newStr = Array.prototype.reduce.apply(str, [
function(string, char, i) {
return string + char + i;
},
'',
]);
console.log(newStr); // "a0b1c2 3d4e5f6"
다만, 아래와 같은 예외적인 상황도 있다.
Array.prototype.push.call(str, ', pushed string');
// Error: Cannot assign to read only property 'length' of object [object String]
- 문자열의 경우
length
프로퍼티가 읽기 전용이기 때문에 원본 문자열에 변경을 가하는 메서드(push, shift, unshift, slice
등)는 에러를 던진다.
Array.prototype.concat.call(str, 'string'); // [String {"abc def"}, "string"]
- 또한,
concat
처럼 대상이 반드시 배열이어야 하는 경우에는 에러는 나지 않지만 제대로 된 결과를 얻을 수 없다.
사실 call
/apply
를 이용해 형변환하는 것은 'this
를 원하는 값으로 지정해서 호출한다'라는 본래의 메서드의 의도와는 다소 동떨어진 활용법이라 할 수 있다. slice
메서드는 오직 배열 형태로 '복사'하기 위해 차용됐을 뿐이니, 코드만 봐서는 어떤 의도인지 파악하기 쉽지 않다.
이에 ES6에서는 유사배열객체 또는 순회 가능한 모든 종류의 데이터 타입을 배열로 전환하는 Array.from
메서드를 새로 도입했다.
call/apply 메서드의 활용 1-4) ES6의 Array.from 메서드
var obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
};
var arr = Array.from(obj);
console.log(arr); // ['a', 'b', 'c']
생성자 내부에서 다른 생성자를 호출
생성자 내부에 다른 생성자와 공통된 내용이 있을 경우 call
또는 apply
를 이용해 다른 생성자를 호출하면 간단하게 반복을 줄일 수 있다.
call/apply 메서드의 활용 2) 생성자 내부에서 다른 생성자를 호출
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 wd = new Student('Woodstock', 'female', '코범대');
var sn = new Employee('Snoopy', 'male', '구글');
Student
,Employee
생성자 함수 내부에서Person
생성자 함수를 호출해서 인스턴스의 속성을 정의하도록 구현
여러 인수를 묶어 하나의 배열로 전달하고 싶을 때 - apply 활용
여러 개의 인수를 받는 메서드에게 하나의 배열로 인수들을 전달하고 싶을 때 apply
메서드를 사용하면 좋다.
예를 들어, 최대/최솟값을 구해야 할 때 다음과 같이 코드를 직접 구현하면, 코드가 불필요하게 길고 가독성도 떨어진다.
call/apply 메서드의 활용 3-1) 최대/최솟값을 구하는 코드를 직접 구현
var numbers = [10, 20, 3, 16, 45];
var max = (min = numbers[0]);
numbers.forEach(function(number) {
if (number > max) {
max = number;
}
if (number < min) {
min = number;
}
});
console.log(max, min); // 45 3
반면, Math.max
/Math.min
를 적용하면 훨씬 간단해진다.
call/apply 메서드의 활용 3-2) 여러 인수를 받는 메서드(Math.max/Math.min)에 apply를 적용
var numbers = [10, 20, 3, 16, 45];
var max = Math.max.apply(null, numbers);
var min = Math.min.apply(null, numbers);
console.log(max, min); // 45 3
ES6에서는 스프레드 연산자를 이용해 더욱 간편하게 작성할 수 있다.
call/apply 메서드의 활용 3-2) ES6의 스프레드 연산자 활용
const numbers = [10, 20, 3, 16, 45];
const max = Math.max(...numbers);
const min = Math.min(...numbers);
console.log(max, min); // 45 3
call
/apply
메서드는 명시적으로 별도의 this
를 바인딩하면서 함수 또는 메서드를 실행하는 훌륭한 방법이지만 오히려 이로 인해 this
를 예측하기 어렵게 만들어 코드 해석을 방해한다는 단점이 있다.
그럼에도 불구하고 ES5 이하의 환경에서는 마땅한 대안이 없기 때문에 실무에서 매우 광범위하게 활용되고 있다.
2-4 bind 메서드
Function.prototype.binf(thisArg[, arg1[, arg2[, ...]]])
bind
메서드는 ES5에서 추가된 기능으로, call
과 비슷하지만 즉시 호출하지는 않고 넘겨받은 this
및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드이다. 다시 새로운 함수를 호출할 때 인수를 넘기면 그 인수들은 기존 bind
메서드를 호출할 때 전달했던 인수들의 뒤에 이어서 등록된다.
즉, bind
메서드는 함수에 this
를 미리 적용하는 것과 부분 적용 함수를 구현하는 두 가지 목적을 모두 지닌다.
bnid 메서드 - this 지정과 부분 적용 함수 구현
var func = function(a, b, c, d) {
console.log(this, a, b, c, d);
};
func(1, 2, 3, 4); // Window{ ... } 1 2 3 4
var bindFunc1 = func.bind({ x: 1 });
bindFunc1(5, 6, 7, 8); // { x: 1 } 5 6 7 8
var bindFunc2 = func.bind({ x: 1 }, 4, 5);
bindFunc2(6, 7); // { x: 1 } 4 5 6 7
bindFunc2(8, 9); // { x: 1 } 4 5 8 9
name 프로퍼티
bind
메서드를 적용해서 새로 만든 함수는 한 가지 독특한 성질이 있다. 바로 name
프로퍼티에 동사 bind
의 수동태인 'bound'라는 접두어가 붙는다는 점이다.
어떤 함수의 name
프로퍼티가 'bound xxx'라면 이는 곧 함수명이 xxx인 원본 함수에 bind
메서드를 적용한 새로운 함수라는 의미가 되므로 기존의 call
이나 apply
보다 코드를 추적하기에 더 수월해진 면이 있다.
bind 메서드 - name 프로퍼티
var func = function(a, b, c, d) {
console.log(this, a, b, c, d);
};
var bindFunc = func.bind({ x: 1 }, 4, 5);
console.log(func.name); // func
console.log(bindFunc.name); // bound func
상위 컨텍스트의 this를 내부함수나 콜백 함수에 전달하기
1-3절에서 메서드의 내부함수에서 메서드의 this
를 그대로 바라보게 하기 위한 방법으로 self
등의 변수를 활용한 우회법을 소개했는데, call
, apply
또는 bind
메서드를 이용하면 더 깔끔하게 처리할 수 있다.
내부함수에 this 전달 - call vs. bind
// call
var obj = {
outer: function() {
console.log(this); // {outer: ƒ}
var innerFunc = function() {
console.log(this); // {outer: ƒ}
};
innerFunc.call(this);
},
};
obj.outer();
// bind
var obj = {
outer: function() {
console.log(this); // {outer: ƒ}
var innerFunc = function() {
console.log(this); // {outer: ƒ}
}.bind(this);
innerFunc();
},
};
obj.outer();
또한, 콜백 함수를 인자로 받는 함수나 메서드 중에서 기본적으로 콜백 함수 내에서의 this
에 관여하는 함수 또는 메서드에 대해서도 bind
메서드를 이용하면 this
값을 사용자의 입맛에 맞게 바꿀 수 있다.
bind 메서드 - 내부함수에 this 전달
var obj = {
logThis: function() {
console.log(this);
},
logThisLater1: function() {
setTimeout(this.logThis, 500);
},
logThisLater2: function() {
setTimeout(this.logThis.bind(this), 1000);
},
};
obj.logThisLater1(); // Window { ... }
obj.logThisLater2(); // obj { logThis: f, ... }
2-5 화살표 함수의 예외사항
ES6에 새롭게 도입된 화살표 함수는 실행 컨텍스트 생성 시 this
를 바인딩하는 과정이 제외됐다. 즉 이 함수 내부에는 this
가 아예 없으며, 접근하고자 하면 스코프체인상 가장 가까운 this
에 접근하게 된다.
화살표 함수 내부에서의 this
var obj = {
outer: function() {
console.log(this); // {outer: ƒ}
var innerFunc = () => {
console.log(this); // {outer: ƒ}
};
innerFunc();
},
};
obj.outer();
- 별도의 변수로
this
를 우회하거나call
/apply
/bind
를 적용할 필요가 없어 더욱 간결하고 편리하다.
2-6 별도의 인자로 this를 받는 경우(콜백 함수 내에서의 this)
콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this
로 지정할 객체(thisArg
)를 인자로 지정할 수 있는 경우가 있다. 이러한 메서드의 thisArg
값을 지정하면 콜백 함수 내부에서 this
값을 원하는 대로 변경할 수 있다.
이런 형태는 여러 내부 요소에 대해 같은 동작을 반복 수행해야 하는 배열 메서드에 많이 포진돼 있으며, 같은 이유로 ES6에서 새로 등장한 Set
, Map
등의 메서드에도 일부 존재한다.
그중 대표적인 배열 메서드인 forEach
의 예를 살펴보자.
thisArg를 받는 경우 예시 - forEach 메서드
01 var report = {
02 sum: 0,
03 count: 0,
04 add: function() {
05 var args = Array.prototype.slice.call(arguments);
06 args.forEach(function(entry) {
07 this.sum += entry;
08 ++this.count;
09 }, this);
10 },
11 average: function() {
12 return this.sum / this.count;
13 },
14 };
15 report.add(60, 85, 95);
16 console.log(report.sum, report.count, report.average()); // 240 3 80
- 5번째 줄에서
add
메서드는arguments
를 배열로 변환해args
변수에 담는다. - 6번째 줄에서는 이 배열을 순회하면서 콜백 함수를 실행한다.
이때 콜백 함수 내부에서의
this
는forEacth
함수의 두 번째인자로 전달해준this
(9번째 줄)가 바인딩 된다. - 11번째 줄의
average
는sum
프로퍼티를count
프로퍼티로 나눈 결과를 반환하는 메서드이다. - 15번째 줄에서 60, 85, 95를 인자로 삼아
add
메서드를 호출하면 이 세 인자를 배열로 만들어forEach
메서드가 실행된다. - 콜백 함수 내부에서의
this
는add
메서드에서의this
가 전달된 상태이므로add
메서드의this(report)
를 그대로 가리키고 있다. - 따라서 배열의 세 요소를 순회하면서
report.sum
값 및report.count
값이 차례로 바뀌고, 순회를 마친 결과report.sum
에는 240이,report.count
에는 3이 담기게 된다.
배열의 forEach
를 예로 들었지만, 이 밖에도 thisArg
를 인자로 받는 메서드는 많이 있다.
콜백 함수와 함께 thisArg를 인자로 받는 메서드
Array.prototype.forEach(callback[, thisArg])
Array.prototype.map(callback[, thisArg])
Array.prototype.filter(callback[, thisArg])
Array.prototype.some(callback[, thisArg])
Array.prototype.every(callback[, thisArg])
Array.prototype.find(callback[, thisArg])
Array.prototype.findIndex(callback[, thisArg])
Array.prototype.flatMap(callback[, thisArg])
Array.prototype.from(arrayLike[, callback[, thisArg]])
Set.prototype.forEach(callback[, thisArg])
Map.prototype.forEach(callback[, thisArg])
정리
명시적 this
바인딩이 없을 때
- 전역공간에서의
this
는 전역객체를 참조한다. - 어떤 함수를 메서드로서 호출한 경우
this
는 메서드 호출 주체를 참조한다. - 어떤 함수를 함수로서 호출한 경우
this
는 전역객체를 참조한다. 메서드의 내부함수에서도 같다. - 콜백 함수 내부에서의
this
는 해당 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않는 경우에는 전역객체를 참조한다. - 생성자 함수에서의
this
는 생성될 인스턴스를 참조한다.
명시적 this
바인딩
call
,apply
메서드는this
를 명시적으로 지정하면서 함수 또는 메서드를 호출한다.bind
메서드는this
및 함수에 넘길 인수를 일부 지정해서 새로운 함수를 만든다.- 요소를 순회하면서 콜백 함수를 반복 호출하는 내용의 일부 메서드는 별도의 인자로
this
를 받기도 한다.
Subscribe to my newsletter
Read articles from woodstock directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
woodstock
woodstock
안녕하세요! 프론트엔드 개발자 woodstock입니다. 저는 매일 조금씩 발전하고자 하는 마음으로 개발공부를 시작했고, 이 블로그는 그 과정에서 배우고 성장하는 이야기를 담고 있습니다. 여러분의 피드백과 조언은 언제나 환영합니다! 함께 배우고 성장하는 과정을 즐길 수 있기를 기대합니다.