As i wish

[You don't know JS] - this가 이런 거로군! - 2 본문

JavaScript

[You don't know JS] - this가 이런 거로군! - 2

어면태 2019. 5. 11. 17:56

오늘은 지난 번에 이어서 포스팅을 해보겠습니다.

지난 번에는  this 바인딩에 관해서 공부 했었는데 오늘은 그 바인딩에 순서에 대하여 알아보려 합니다.

this가 이런 거로군! - 1

 

[You don't know JS] - this가 이런 거로군! - 1

앞서 포스팅했던 것에 이어서 두 번째 장인 'this가 이런 거로군!'에 대하여 포스팅해보겠습니다. 앞서 포스팅에서 this가 알고 있었던 것과는 달리 호출부에서 함수를 호출할 때 바인딩된다고 포스팅했었습니다...

eomtttttt-develop.tistory.com

일단은 바인딩에 관해서는 4가지 가 있었죠.

1. 기본 바인딩

2. 암시적 바인딩

3. 명시적 바인딩

4. new 바인딩

 

암시적 바인딩 vs 명시적 바인딩

먼저 암시적 바인딩과 명시적 바인딩에 순서에 대해 알아보겠습니다.

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

obj1.foo() 를 하면 암시적 바인딩에 따라 a가 2로 바인딩 되는데 밑에서 obj1.foo.call(obj2) 로 명시적 바인딩을 함으로서 a가 obj2에 a로 바인딩 되는것을 확인 해 볼 수 있습니다.

위와 같이 명시적 바인딩이 암시적 바인딩보다 우선순위가 높음을 알 수 있죠.

명시적 바인딩 > 암시적 바인딩

 

암시적 바인딩 vs new 바인딩

function foo(something) {
    this.a = something;
}

var obj1 = {
    foo: foo
};

obj1.foo(2);
console.log(obj1.a); // 2

var bar = new obj1.foo(4);

console.log(obj1.a); // 2
console.log(bar.a); // 4

obj1.foo(2) 를 하게 되면 암시적 바인딩 규칙에 따라 obj1 이 바인딩이 됩니다. 그러나 new 로 인해 bar가 새롭게 바인딩 되는것을 볼 수 있습니다. obj1.a는 변함 없는데 bar.a는 변함이 있는것이 그 증거입니다. 따라서 new 바인딩이 암시적 바인딩보다 높다는것을 알 수 있죠.

new 바인딩 > 암시적 바인딩

 

명시적 바인딩 vs new 바인딩

명시적 바인딩을 위한 call(), apply() 함수는 new와 테스트하기 조금 힘든감이 있습니다. 대신 bind() 함수는 컨택스트가 바인딩된 새로운 함수를 반환하는 특징을 가지고 있습니다. bind() 함수를 이용해서 확인해 보겠습니다.

function foo(something) {
    this.a = something;
}

var obj1 = {};

var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); // 2

var baz = new bar(3);
console.log(obj1.a); // 2
console.log(baz.a); // 3

foo.bind(obj1) 을 이용해서 obj1 객체를 foo 함수의 컨텍스트로 하드바인딩 처리를 하고 bar 로 만들었죠. bar(2)를 실행하면 하드 바인딩 된 obj1가 this가 되고 obj1.a 가 2 가 되죠.

그러나 하드바인딩 된 bar 를 new 를 통해 호출하면 새로운 객체가 나오고 그 객체가 컨텍스트로 바인딩 됩니다. 따라서 baz.a는 3이 되죠.

따라서 new 바인딩이 명시적 바인딩보다 우선순위가 높다는 것을 알 수 있죠.

new 바인딩 > 명시적 바인딩

 

따라서 앞서 예상했던 데로 순서는

1. new 바인딩 (var bar = new foo())

2. call 과 apply로 함수를 호출, 명시적 바인딩 (var bar = foo.call(obj2))

3. 함수를 컨텍스트, 즉 객체를 소유 또는 포함 하는 형태, 암시적 바인딩 (var bar = obj1.foo())

4. 그 외의 경우, 기본 바인딩 (var bar = foo())

순으로 this 바인딩의 우선순위가 결정 된다고 볼 수 있죠.

new 바인딩 > 명시적 바인딩 > 암시적 바인딩 > 기본 바인딩

허나 늘 예외도 있습니다.

 

예외

1. this 무시

callm, apply, bind의 첫번째 인자로 null 또는 undefined를 넘기면 this 바인딩이 무시 되고 기본 바인딩 규칙이 적용됩니다.

function foo() {
    console.log(this.a);
}

var a = 2;
foo.call(null) // 2

그러나 이러한 방법은 위험한 방법이죠. 어떤 함수를 호출 했을 시에 null을 전달 했는데 마침 그 함수가 내부적으로 this를 레퍼런스로 참조하면 기본 바인딩이 적용되어 전역변수를 참조하거나 예기치 못한 상황이 발생 할 수 있죠. 

그래서 빈 객체를 넣어주기도 합니다.

function foo(a, b) {
    console.log('a:' + a + ', b:' + b);
}

var ø = Object.create(null);

foo.apply(ø, [2, 3]); // a:2, b:3

var bar = foo.bind(ø, 2);
bar(3); // a:2, b:3

ø 는 맥 기준으로 (option key + o) 입니다.

 

2. 간접 레퍼런스

간접 레퍼런스가 생성되는 경우로, 함수를 호출하면 무조건 기본 바인딩 규칙이 적용되어 버립니다.

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

암시적 바인딩을 통했을 때에는 this가 o 객체로 바인딩 되지만, 간접 레퍼런스(p.foo = o.foo) 를 하게 된 후 함수를 call하게 되면 this가 전역 객체로 바인딩 되어(기본 바인딩 규칙) 2가 출력 되는 것을 확인 할 수 있습니다,

 

3. 소프트 바인딩

암시적/명시적 바인딩 기법을 통해 임의로 this 바인딩을 하는 동시에 전역 객체나 undefined가 아닌 다른 기본 바인딩 값을 셋팅할 수 있습니다.

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

소프트 바인딩은 this를 obj2 나 (obj2 = foo.softBind(obj))  obj3 (fooOBJ.call(obj3))로 수동으로 바인딩 할 수도 있고, 기본 바인딩 규칙이 적용 (setTimeout(obj2.foo, 10)) 되어야 할 땐 다시 obj로 되돌립니다.

이로써 예외 적인 경우가 끝났습니다.

마지막으로 어휘적 this에 대해 알아보겠습니다.

어휘적 this

일반적인 함수는 앞에 4가지 바인딩 규칙을 준수하는데 ES6 부터는 이 규칙을 따르지 않는 새로훈 함수가 생깁니다. 이름하여 화살표 함수(Arrow Function) 입니다. 화살표 함수는 4가지 규칙 대신 스코프를 보고 this를 바인딩 합니다.

function foo() {
    return (a) => {
        // this는 어휘적으로 foo()에 상속된다.
        console.log(this.a);
    }
}

var obj1 = {
    a: 2
};

var obj2 = {
    a: 3
};

var bar = foo.call(obj1);
bar.call(obj2); // 2 , 3 이 아니다!!! 

foo() 내부에서 생성된 화살표 함수는 foo() 호출 당시 this를 무조건 어휘적으로 포착합니다. foo() 는 obj1에 this가 바인딩 되므로 bar(반환된 화살표 함수를 가리키는 변수)의 this 역시 obj1으로 바인딩 됩니다. 화살표 함수의 어휘적 바인딩은 절대로 오버라이드 할 수 가 없습니다.

화살표 함수에 대한 참고 자료

마지막으로 지금 까지 배운것들을 정리해 보겠습니다.

- 함수 실행에 있어서 this바인딩은 함수의 직접적인 호출부에 따라 달라집니다. 일단 호출부를 식별한 후 다음 4가지 규칙에 우선순위에 따라 적용 됩니다.

1. new 바인딩

2. 명시적 바인딩

3. 암시적 바인딩

4. 기본 바인딩

- 실수로 기본바인딩 규칙이 적용되지 않도록 ø = Object.create(null) 로 this 바인딩을 안전하게 할 수 도 있습니다.

- ES6 화살표 함수는 표준 바인딩 규칙을 무시하고 렉시컬 스코프로 this를 바인딩 합니다. 즉, 함수 스코프를 보고 this를 바인딩 합니다.

 

이상 긴 this에 대한 포스팅을 마치겠습니다.

참고: 자바스크립스 this 바인딩 우선순위

Comments