아래 내용은 You Don't Know JS의 this 관련 내용을 정리한 것임.

 

this는 작성 시점이 아닌 런타임 시점, 함수가 호출되는 상황에 따라 콘텍스트가 결정된다.
this 바인딩이 일어날 때 함수 호출부를 확인하여 this가 무엇을 가리키는지 알아야 한다.

아래 코드는 호출부와 호출스택을 이해하기 위한 코드이다.

function baz() {
  // 호출 스택: baz
  // 호출부: 전역스코프 내부
  console.log("baz");
  bar();
}

function bar() {
  // 호출 스택: baz -> bar
  // 호출부: baz 내부
  console.log("bar");
  foo();
}

function foo() {
  //호출 스택: baz -> bar -> foo
  // 호출부: bar 내부
  console.log("foo")
}

baz(); // baz의 호출부

위 코드에서 최종적인 호출부는 전역 스코프가 된다.

참조 규칙

1. 기본 바인딩

어떤 규칙에도 해당하지 않을때 적용되는 기본 규칙이다.

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

var a = 2;
foo(); // 2

this는 전역 객체에 바인딩되고, foo에서는 a라는 프로퍼티를 참조한다.
여기서 다른 규칙 없이 foo의 호출부인 전역 스코프에 바운딩 되었음을 알 수 있다.strict mode에서는 전역 객체가 기본 바인딩 대상에서 제외되므로 thisundefined가 된다.

function foo() {
  "use strict"

  console.log(this.a);
}

var a = 2;
foo(); // Uncaught TypeError: Cannot read property 'a' of undefined

다만 foo() 함수 본문를 none-strict mode에서 실행할 시, 호출부의 strict 여부와는 상관 없이 전역 객체만이 기본 바인딩의 대상이 된다.

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

var a = 2;
(function() {
  "use strict"

  foo(); // 2
})();

2. 암시적 바인딩

호출부에 컨텍스트 객체가 있으면, 즉 객체가 포함(Owning)/소유(Containing)하고있으면 this는 암시적으로 바인딩된다.

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

var obj = {
  a: 2,
  foo: foo
};

obj.foo() // 2

위처럼 objfoo를 별도로 선언하든 객체 내부에 foo 함수를 직접 작성하든 obj가 실제로 foo 함수를 가지고 있는것은 아니다. 다만 호출부에서 obj 컨텍스트로 foo를 호출하므로 해당 시점에서 함수의 레퍼런스를 포함/소유하고 있다고 볼 수 있다.
함수 레퍼런스에 대한 콘텍스트 객체가 존재하면 this는 해당 콘텍스트 객체에 바인딩된다. foo를 호출하는 컨텍스트가 obj이므로 thisobj가 된다.

만일 프로퍼티 참조가 chaining 되어있다면 최상위/최하위 수준의 정보만 호출부와 연관된다.
아래 코드에서 중간 단계인 obj1.a는 무시된다.

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

var obj2 = {
  a: 2,
  foo: foo
}

var obj1 = {
  a: 1,
  obj2: obj2
};
obj1.obj2.foo() // 2

암시적 소실

암시적 바인딩이 되었던 this가 기본 바인딩으로 인해 전역 객체나 undefined로 바인딩 되는 경우가 있다.

function foo() {
  console.log(this.a);
}
var obj = {
  a: 2,
  foo: foo
}
var bar = obj.foo;
var a = "엥, 전역이네!";
bar(); //엥, 전역이네!

여기서 barobj.foo를 참조하는 것 처럼 보이나 실제로는 foo를 직접 참조하는 변수가 된다. 때문에 bar()을 실행한 전역 객체로 this가 기본 바인딩 된다.

콜백 함수를 전달할 시에도 같은 문제가 일어난다.

function foo() {
  console.log(this.a);
}
function bar(callback) {
  callback(); //호출부
}
var obj = {
  a: 2,
  foo: foo
}
var a = "엥, 전역이네!";
bar(obj.foo); //엥, 전역이네!

인자로 전달하는 행위 자체가 암시적인 레퍼런스 할당이 되어 this가 전역 객체로 바인딩 되는 문제가 일어난다.

이런 문제를 해결하기 위해 this를 고정하는 명시적 바인딩을 사용할 수 있다.

3. 명시적 바인딩

암시적 바인딩에서는 함수 레퍼런스를 객체에 넣기 위해 객체 자신에 함수를 참조할 속성을 추가해야했다. 이를 따로 이용하지 않고 코드에 this를 명확히 밝히기 위해 명시적 바인딩을 사용한다

.모든 자바스크립트 함수는 prototype을 통해 call()apply() 매소드를 사용할 수 있다. 두 메소드는 인자로 객체를 받아 이를 this로 바인딩하며, 이를 명시적 바인딩이라고 한다.

function foo() {
  console.log(this.a);
}
var obj = {
  a: 2
}

// foo.call()에서 명시적으로 obj를 바인딩한다.
foo.call(obj); // 2

객체 대신 primitive type을 인자로 전달하면 해당 type에 대응되는 객체로 wrapping 해주며, 이를 boxing이라고 한다.

하드 바인딩

그러나 명시적 바인딩을 사용해도 this의 바인딩 대상이 덮어씌워지는 문제는 해결할 수 없다. 이를 해결하기 위해 다음과 같은 패턴을 사용한다.

function foo() {
  console.log(this.a);
}
var obj = {
  a: 2
};
var bar = function() {
  foo.call(obj);
}

bar(); // 2
setTimeout(bar, 100); // 2
bar.call(window); // 2, 재정의된 this는 의미가 없다.

함수 bar()는 어떻게 호출해도 fooobj를 바인딩해서 사용하므로 결과는 변하지 않는다. 이런 바인딩은 명시적이고 강력해서 하드 바인딩이라고 한다.

다음과 같이 bind 헬퍼를 구현할 수 있다.

function foo(something) {
  console.log(this.a, something);
  return this.a + something;
}
// bind 헬퍼
function bind(fn, obj) {
  return function() {
    return fn.apply(obj, arguments);
  }
}

var obj = {
  a: 2
}
var bar = bind(foo, obj);

var b = var(3); // 2 3
console.log(b) // 5

ES5에서는 Function.prototype.bind이라는 이름으로 구현되었으며, foo.bind(obj)와 같은 형식으로 사용할 수 있다.

4. new 바인딩

일반적으로 new연산자는 클래스 지향 언어에서 생성자를 호출하여 클래스 인스턴스를 만들어낸다.

something = new MyClass();
그러나 자바스크립트에서 생성자는 클래스와 상관없이 new 연산자로 실행하였을때 함께 실행되는 일반 함수이다. 생성자 자체는 클래스 내부에 있는 함수도 아니며 클래스 인스턴스화 기능도 없다.

ES5에서는 대부분의 함수를 new 연산자로 실행할 수 있었으나 ES6에서 함수 선언 방식이 달라지면서 constructor이 아닌 함수는 부를 수 없게 되었다.function 키워드를 붙여 생성한 함수는 자동으로 constructor 역할을 함께 하며, arrow function으로 생성한 함수는 constructor이 되지 않는다.

함수 앞에 new 키워드를 붙여 생성자 호출을 하면 자동으로 다음과 같은 일을 해준다.

  1. 새 객체가 만들어진다.
  2. 새로 생성된 객체의 [[prototype]]이 연결된다.
  3. 새로 생성된 객체는 해당 함수 호출 시 this로 바인딩된다.
  4. 이 함수가 자신의 또 다른 객체를 반환하지 않으면 new와 함께 호출된 함수는 자동으로 새로 생성된 객체를 반환한다.

this 확정 규칙

바인딩 예외

1. this 무시

call, apply, bind 메서드에 첫 번째 인자로 null 또는 undefined를 넘기면 this 바인딩이 무시되고 기본 바인딩 규칙이 적용된다.

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

var a = 2;
foo.apply(null, [2, 3]); // a:2, b:3

foo.bind(null, 2);
foo(3); // a:2, b:3

함수 호출 시 applybind는 종종 함수의 인자를 컨트롤 하기 위해 사용된다. 이 때 굳이 this를 지정해줄 필요가 없으면 함수의 첫번째 인자를 null로 주는것이다.
이 경우 주의할 점은, 자신이 다루지 않는 함수(서브파티 라이브러리 함수 등)가 내부적으로 this를 참조하고 있다면 window 객체를 참조하는 등의 영향을 끼칠 우려가 있다는 것이다.

이를 방지하기 위해 부작용과 무관한 객체를 바인딩하는 방법이 있다. 이러한 객체는 내용이 없고, 아무것도 위임되지 않는다.

var blank = Object.create(null); //{}와 달리 Object.prototype으로 위임하지 않음

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

p.foo = o.foo의 반환 값은 원 함수(Underlying Function) 객체의 레퍼런스이므로, 실제 호출부는 foo()가 된다. 때문에 기본 바인딩 규칙이 적용된다.

3. 소프트 바인딩

앞서 함수 바인딩이 예상과 달라지는 것을 막아주는 하드 바인딩에 대해 언급하였다. 그러나 이후에 this를 다시 암시적 혹은 명시적 바인딩하기가 어려워진다.

이 문제를 해결한 위해 임의로 this를 바인딩 할 수 있는 동시에 기본 바인딩 값을 세팅할 수 있는 Soft Binding 유틸리티가 있다.
softBind()의 로직은 bind()와 거의 동일하지만, this가 존재하지 않거나 전역 변수인 경우에는 초기에 바인딩한 this가 되도록 한다.

+ Recent posts