아래 내용은 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
에서는 전역 객체가 기본 바인딩 대상에서 제외되므로 this
는 undefined
가 된다.
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
위처럼 obj
의 foo
를 별도로 선언하든 객체 내부에 foo
함수를 직접 작성하든 obj
가 실제로 foo
함수를 가지고 있는것은 아니다. 다만 호출부에서 obj
컨텍스트로 foo
를 호출하므로 해당 시점에서 함수의 레퍼런스를 포함/소유하고 있다고 볼 수 있다.
함수 레퍼런스에 대한 콘텍스트 객체가 존재하면 this
는 해당 콘텍스트 객체에 바인딩된다. foo
를 호출하는 컨텍스트가 obj
이므로 this
는 obj
가 된다.
만일 프로퍼티 참조가 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(); //엥, 전역이네!
여기서 bar
이 obj.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()
는 어떻게 호출해도 foo
에 obj
를 바인딩해서 사용하므로 결과는 변하지 않는다. 이런 바인딩은 명시적이고 강력해서 하드 바인딩이라고 한다.
다음과 같이 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
키워드를 붙여 생성자 호출을 하면 자동으로 다음과 같은 일을 해준다.
- 새 객체가 만들어진다.
- 새로 생성된 객체의 [[prototype]]이 연결된다.
- 새로 생성된 객체는 해당 함수 호출 시 this로 바인딩된다.
- 이 함수가 자신의 또 다른 객체를 반환하지 않으면 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
함수 호출 시 apply
와 bind
는 종종 함수의 인자를 컨트롤 하기 위해 사용된다. 이 때 굳이 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
가 되도록 한다.
'개발' 카테고리의 다른 글
JSConf JP 2019 참석기 DAY2 (0) | 2021.09.06 |
---|---|
JSConf JP 2019 참석기 DAY1 (0) | 2021.09.05 |
브라우저 렌더링 과정 (0) | 2021.08.26 |
정규표현식을 사용하여 문자열에서 이모지 포함 여부를 판별하는 방법 (0) | 2021.08.17 |
리액트 디자인 패턴 (0) | 2021.06.06 |