-
호출부는 현재 실행중인 함수 직전의 호출 코드 내부에 있다.
-
호출 스택은 현재 실행 지점에 오기까지 호출된 함수의 스택을 말한다.
-
호출 스택과 호출부를 다음 예시로 이해해보자.
function baz() { // 호출 스택: baz // 호출부는 전역 스택의 내부다 console.log("baz") bar() -> bar의 호출부 } function bar() { // 호출 스택: baz -> bar // 호출부: baz 내부다 console.log("bar") foo() -> foo의 호출부 } function foo() { // 호출 스택: baz -> bar -> foo // 호출부: bar 내부다 console.log('foo') } baz() -> baz의 호출부
-
브라우저 디버거 툴을 이용하면 코드 실행에 따라 호출 스택이 차례로 쌓이는 모습과
this
바인딩, 변수 스코프 등을 확인할 수 있다.
- 가장 평범한 함수 호출인 단독 함수 실행에 관한 규칙으로, 이 경우
this
는 전역 객체를 참조한다.function foo() { console.log(this.a) } // 키워드 var를 이용하여 전역 스코프에 변수 a 선언 var a = 2 foo() // 2
- 엄격 모드
strict mode
에서는 전역 객체가 기본 바인딩 대상에서 제외된다.function foo() { "use strict" console.log(this.a) } var a = 2 foo() // TypeError: 'this' is 'undefined'
- 단, 호출부의 엄격 모드는 상관없다
function foo() { console.log(this.a) } var a = 2 (function() { "use strict" foo() // 2 })()
- 호출부에 콘텍스트 객체가 있는지, 즉 객체의 소유/포함 여부를 확인하는 것이다.
- 함수 레퍼런스에 대한 콘텍스트 객체가 존재할 때 암시적 바인딩 규칙에 따르면 바로 이 콘텍스트 객체가 함수 호출시
this
에 바인딩 된다.function foo() { console.log(this.a) } var obj = { a: 2, foo: foo } obj.foo() // 2
- 함수 레퍼런스에 대한 콘텍스트 객체가 존재할 때 암시적 바인딩 규칙에 따르면 바로 이 콘텍스트 객체가 함수 호출시
- 암시적으로 바인딩 된 함수에서 바인딩이 소실되는 경우가 있다. 이럴 땐, 엄격 모드 여부에 따라 전역 객체나
undefined
중 한 가지로 기본 바인딩 된다.function foo() { console.log(this.a) } var obj = { a: 2; foo: foo; } var bar = obj.foo var a = "엥, 전역이네!" bar() // 엥, 전역이네!
bar
는obj
의foo
를 참조하는 변수처럼 보이지만, 실은foo
를 직접 가리키는 또 다른 레퍼런스다.
- 어떤 객체를
this
바인딩에 이용하겠다는 의지를 코드에 명확히 밝힐 방도가 없을까?- 이럴 때 모든 자바스크립트 함수에 사용할 수 있는 유틸리티가 바로
apply
와call
메서드다. - 두 메서드는
this
에 바인딩 할 객체를 첫째 인자로 받아 함수 호출 시 이 객체를this
로 세팅한다.this
를 지정한 객체로 직접 바인딩하므로 이를 명시적 바인딩이라 한다.
- 이럴 때 모든 자바스크립트 함수에 사용할 수 있는 유틸리티가 바로
function foo() {
console.log(this.a)
}
var obj = {
a: 2
}
var bar = function() {
foo.call(obj)
}
bar() // 2
bar.call(window) // 2
- 함수
bar
는 내부에서foo.call(obj)
로foo
를 호출하면서obj
를this
에 강제로 바인딩하도록 하드 코딩한다.- 따라서
bar
를 어떻게 호출하든 이 함수는 항상obj
를 바인딩하여foo
를 실행한다. 이런 바인딩은 명시적이고 강력해서 하드 바인딩이라고 한다.
- 따라서
- 하드 바인딩은 매우 자주 쓰는 패턴이어서 ES5 내장 유틸리티
Function.prototype.bind
가 구현되어 있다.function foo(something) { console.log(this.a, something) return this.a + something } var obj = { a: 2 } var bar = foo.bind(obj) var b = bar(3) // 2 3 console.log(b) // 5
- 많은 라이브러리 함수와 자바스크립트 언어 및 호스트 환경에 내장된 여러 새로운 함수는 대개 콘텍스트라 불리는 선택적인 인자를 제공한다. 이는
bind
를 써서 콜백 함수의this
를 지정할 수 없는 경우를 대비한 일종의 예비책이다.function foo(el) { console.log(el, this.id) } var obj = { id: "고구마" } // foo 호출 시 obj를 this로 사용한다 [1, 2, 3].forEach(foo, obj) // 1 고구마 2 고구마 3 고구마
- (먼저 오해 바로잡기): 자바스크립트 생성자는 앞에
new
연산자가 있을 때 호출되는 일반 함수에 불과하다.- 예를 들어, 생성자
Number
함수에 대한 명세는 다음과 같이 쓰여 있다. new 표현식의 일부로 호출 시 Number는 생성자이며 새로 만들어진 객체를 초기화한다.
- 예를 들어, 생성자
- 함수 앞에
new
를 붙여 생성자 호출을 하면 다음과 같은 일들이 저절로 벌어진다.- 새 객체가 툭 만들어진다.
- 새로 생성된 객체의
[[Prototype]]
이 연결된다. - 새로 생성된 객체는 해당 함수 호출 시
this
로 바인딩된다. - 이 함수가 자신의 또 다른 객체를 반환하지 않는 한
new
와 함께 호출된 함수는 자동으로 새로 생성된 객체를 반환한다.
new
는 함수 호출시this
를 새 객체와 바인딩하는 방법이며 이것이new
바인딩이다.
- 지금까지 함수를 호출할 때의 4가지
this
바인딩 규칙을 알아봤다. 만약에 여러 개의 규칙이 중복으로 해당할 땐 어떻게 할까?new
로 함수를 호출했는가? 👉 그렇다면 새로 생성된 객체가this
다.var bar = new foo()
call
과apply
로 함수를 호출(명시적 바인딩), 이를테면bind
하드 바인딩 내부에 숨겨진 형태로 호출됐는가? 👉 그렇다면 명시적으로 지정된 객체가this
다.var bar = foo.call(obj2)
- 함수를 콘텍스트(암시적 바인딩), 즉 객체를 소유 또는 포함하는 형태로 호출했는가? 👉 그렇다면 바로 이 콘텍스트 객체가
this
다.var bar = obj1.foo()
- 그 외의 경우에
this
는 기본값으로 세팅된다(기본 바인딩).var bar = foo()
- 특정 바인딩을 의도했는데 실제로는 기본 바인딩 규칙이 예외되는 사례들이 있다.
function foo() {
console.log(this.a)
}
var a = 2
foo.call(null) // 2
- 그런데
null
같은 값으로this
바인딩을 할 이유가 뭘까?apply
는 함수 호출 시 다수의 인자를 배열 값으로 쭉 펼쳐 보내는 용도로 자주 쓰인다.bind
도 비슷한 방법으로 인자들을 커링하는 메서드로 많이 쓰인다.
function foo(a, b) {
console.log("a: " + a + ", b: " + b)
}
foo.apply(null, [2, 3]) // a: 2, b: 3
var bar = foo.bind(null, 2)
bar(3) // a: 2, b: 3
- 그러나 호출 시
null
을 전달했는데 마침 그 함수가 내부적으로this
를 레퍼런스로 참조하면 기본 바인딩이 적용되어 전역 변수를 참조하거나 최악으로는 예기치 못한 일이 발생할 수 있다.
- 더 안전하게 가고자 한다면 100% 빈 객체를
null
대신 전달할 수 있다.- 빈 객체는
Object.create(null)
로 만들 수 있다.{}
과 비슷하지만Object.prototype
으로 위임하지 않으므로{}
보다 더 텅 빈 객체라고 볼 수 있다.function foo(a, b) { console.log("a: " + a + ", b: " + b) } var pi = Object.create(null) // 인자들을 배열 형태로 쭉 펼친다 foo.apply(pi, [2, 3]) // a: 2, b: 3 // bind로 커링한다 var bar = foo.bind(pi, 2) bar(3) // a: 2, b: 3
- 빈 객체는
- 기능적으로 더 안전하다는 의미 외에도 pi로 표기하면
this
는 텅빈 객체로 하겠다는 의도를null
보다 더 확실하게 밝히는 효과가 있다.
- 의도적이든 아니든 유의할 점은 간접 레퍼런스가 생성되는 경우로, 함수를 호출하면 무조건 기본 바인딩 규칙이 적용되어 버린다.
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
를 암시적 바인딩하거나 나중에 다시 명시적 바인딩하는 식으로 수동으로 오버라이딩하는 것이 불가능하다. - 암시적/명시적 바인딩 기법을 통해 임의로
this
바인딩을 하는 동시에 전역 객체나undefined
가 아닌 다른 기본 바인딩 값을 세팅할 수 있는 유틸리티가 있다. 이른바 소프트 바인딩Soft binding
이 그것이다. softBind()
유틸리티는 호출 시점에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 👉 소프트 바인딩이 적용됐다
- 소프트 바인딩이 탑재된
foo
함수는this
를obj2
나obj3
으로 수동 바인딩할 수 있고, 기본 바인딩 규칙이 적용되어야 할 땐 다시obj
으로 되돌린다.
- 소프트 바인딩이 탑재된
- 일반적인 함수는 지금까지 살펴본 4가지 규칙을 준수한다. 하지만! 화살표 함수는 4가지 표준 규칙 대신 에두른 스코프를 보고
this
를 알아서 바인딘한다. - 이는
bind()
대신 사용가능한 해결책이지만this
에서 도망치려는 꼼수다(라고 저자는 말한다)