자바스크립트의 THIS
📝 TL;DR - this의 확정 규칙
이전 글에서 this의 기본 규칙에 대해 알아봤다. 그렇다면 동시에 여러개가 쓰였을 때 this의 우선순위는 어떻게 될까. 일단 결론은 아래와 같다.
1. new
로 함수를 호출(new 바인딩) 했는가? - 새로 생성된 객체가 this
var bar = new foo();
2. 명시적 바인딩이나 하드 바인딩으로 호출됐는가? - 명시적으로 지정된 객체가 this
var bar = foo.call(obj2);
3. 암시적 바인딩(함수의 콘텍스트) 형태로 호출했는가? - 콘텍스트가 this
var bar = obj1.foo();
4. 그 외의 경우 this는 기본값으로 적용 - 엄격모드는 undefined, 비엄격 모드는 전역객체
var bar = foo();
☕️ 우선순위를 비교해 보자
1. 명시적 바인딩 > 암시적 바인딩
function foo() {
console.log(this.a);
}
var obj1 = {
a: 2,
foo: foo,
};
var obj2 = {
a: 3,
foo: foo,
};
obj1.foo(); //2
obj2.foo(); //3
obj1.foo.call(obj2); //3
obj2.foo.call(obj1); //2
- 결과를 보면 명시적으로
call
을 사용하여 바인딩 했을 때의 우선순위가 높다.
2. new바인딩 > 암시적 바인딩
function foo(something) {
this.a = something;
}
var obj1 = {
fooJ: foo,
};
var obj2 = {};
obj1.foo(2);
console.log(obj1.a); //2
obj1.foo.call(obj2, 3);
console.log(obj2.a); //3
var bar = new obj1.foo(4);
console.log(obj1.a); //2
console.log(bar.a); //4
3. new바인딩>명시적 바인딩
new 와 call, apply는 동시에 사용할 수 없으므로 하드 바인딩을 이용해 두 규칙 간 우선순위를 테스트 해보자
function foo(something) {
this.a = something;
}
var obj1 = {};
//하드바인딩
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); //2
//new 로 바인딩
var baz = new bar(3);
console.log(obj1.a); //2
console.log(baz.a); //3
bar
는obj1
에 하드 바인딩 되었는데,new bar(3)
실행 후에도obj1.a
의 값은3
으로 변하지 않았다.obj1
에 하드 바인딩 된bar()
호출은new
로 오버라이드 할 수 있다.new
가 적용되어 새로 만들어진 객체가baz
에 할당되고 실제baz.a
의 값은 3이 된다.
- bind의 new 오버라이드를 가능케 하는 코드 (bind의 폴리필)
this instanceof FNOP && oThis ? this : oThis;
FNOP.prototype = this.prototype;
fBound.prototype = new FNOP();
- 하드 바인딩 함수가
new
로 호출되어this
가 새로 생성된 객체로 세팅됐는지 조사해보고 맞으면 하드 바인딩에 의한this
를 버리고 새로 생성된this
를 대신 사용한다. this
하드 바인딩을 무시하는 함수를 생성하여 함수 인자를 전부 또는 일부만 미리 세팅해야 할 때 유용(커링의 일종이다)function foo(p1, p2) { this.val = p1 + p2; } // 'null'을 입력한 건 여기서 'this' 하드 바인딩은 // 어차피 new 호출 시 오버라이드 되므로 신경 쓰지 않겠다는 의미다. var bar = foo.bind(null, 'p1'); var baz = new bar('p2'); baz.val; //p1p2
☕️ 바인딩 예외
1. this 무시
-
call, aply, bind 메서드 첫 번째 인자로 null 또는 undefined 를 넘기면 this 바인딩이 무시되고 기본 바인딩 규칙이 적용된다.
function foo() { console.log(this.a); } var a = 2; foo.call(null); //2
-
apply는 함수 호출 시 다수의 인자를 배열 값으로 죽 펼쳐 보내는 용도로 자주 쓰이고(es6부터는 spread 연산자가 생겨서 그걸로 쓰기도 한다.)
-
bind도 유사한 방법으로 인자들을 커링하는 메서드로 많이 사용된다.
function foo(a, b) { console.log('a:' + a + 'b:' + b); } // 인자들을 배열 형태로 죽 펼친다 foo.apply(null, [2, 3]); // bind 로 커링한다 var bar = foo.bind(null, 2); bar(3); //a:2, b:3
-
null을 넣는 이유는, apply와 bind 모두 첫 인자로 바인딩할 값을 정해야 하는데, 일종의 자리끼움(placeholder) 값으로 null정도의 값을 전달한다.
더 안전한 this
객체 내용이 하나도 없으면서 전혀 위임되지 않은 객체를 바인딩 한다.
function foo(a,b){
console.log("a:"+ a + ", b" +b);
}
//DMZ 객체 생성
var ø = Object.create(null);
//인자들을 배열 형태로 쭉 펼친다.
foo.apply(ø,[2,3]); //a:2, b:3
//bind로 커링한다.
var bar = foo.bind(ø, 2);
bar(3); //a:2, b:3
더 안전하게 가고자 한다면 프로그램에서 부작용 100% 무관한 객체를 this로 바인딩 하는 방법이다.
간접 레퍼런스
function foo() {
console.log(this.a);
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); //3
(p.foo = o.foo)(); //2
할당 표현식 p.foo = o.foo
의 결과값은 원 함수의 레퍼런스이므로, 실제로 호출한 부분은 p.foo()
, o.foo()
가 아니고 foo()
다... 그래서 기본 바인딩 규칙이 적용된다.
소프트 바인딩
호출시점에 this를 체크하는 부분에서 주어진 함수를 래핑하여 전역 객체나 undefined일 경우 미리 준비한 대체 기본객체로 세팅한다.
그 외의 경우 this는 손대지 않는다.
그리고 선택적인 커링(앞의 bind()부분 참고) 기능도 있다.
function foo(){
console.log("name" + this.name);
}
var obj = {name:"obj"},
obj2 = {name:"obj2"},
obj3 = {name:"obj3"};
var fooOBJ = foo.softBind(obj);
fooOBJ(); //name : obj
obj2.foo = foo.softBind(obj);
obj2.foo(); //name: obj2
fooOBJ.call(obj3); //name: obj3
setTimeout(obj2.foo, 10);
//name: obj
소프트 바인딩이 탑재된 foo()함수는 this를 obj2 나 obj3 로 수동 바인딩 할 수 있고 기본 바인딩 규칙이 적용되어야 할 땐 다시 obj로 되돌린다.
어휘적 this
function foo() {
return a => {
// 여기서 this 는 어휘적으로 foo()에서 상속된다
console.lgo(this.a);
};
}
var obj1 = {
a: 2,
};
var obj2 = {
a: 3,
};
var bar = foo.call(obj1);
bar.call(obj1); //2, 3이 아니다
- ES6부터는 화살표 함수를 써서 상위 스코프의 this를 바인딩 할 수 있다.
- 화살표 함수의 어휘적 바인딩은 new로도 오버라이딩 할 수 없다.