JavaScript 함수 호출및 this의 이해

지난 몇 년 동안 JavaScript 함수 호출에 대해 많은 혼란이 있었습니다. 특히 많은 사람들이 함수 호출에서 this의 의미가 혼란 스럽다고 불평했습니다.

필자의 견해로는, 이 혼란의 대부분은 Core functionInvocation에 대한 Primitive를 이해하고 그 Primitive에서 함수를 호출하는 방법을 살펴봄으로써 해결됩니다. 실제로 이런 방법이 ECMAScript 스펙을 이해하는데 많은 도움이 됩니다.

Core Primitive

먼저 Core Function Primitive 기본 함수인 call 메서드[1]를 살펴 보겠습니다. call 메서드는 비교적 간단합니다.

  1. 첫번째 파라미터는 thisValue입니다.
  2. 두번째 파라미터부터 그 이후 파라미터들을 이용해 파라미터 목록 (argList)을 만듭니다.
  3. thisthisValue로, argListArgument list로 하여 함수를 Invoke 합니다.
1
2
3
4
5
function hello(thing) {
console.log(this + " says hello " + thing);
}
hello.call("Yehuda", "world") //=> Yehuda says hello world

보시다시피, this는 “Yehuda”로 설정되고 파라미터가 “world” 한개인 hello 메서드를 호출했습니다. 이것이 JavaScript 함수 호출의 Core Primitive입니다. 다른 모든 함수 호출은 이 Primitive에 대해 Desugaring하는 것으로 생각할 수 있습니다.( “desugar”에 이르기까지는 편리한 문법을 사용하여 보다 기본적인 Core primitive로 설명합니다).

Desugaring: 소스 코드를 문법적으로 엄격한 형식에 맞춰 변환합니다.

[1] ES5 스펙에서 call 함수를 다른 것(더 낮은 레벨의 primitive)으로 설명 하지만, 단지 Primitive 위의 아주 얇은 Wrapper이므로 여기서 간단하게 단순화 합니다. 자세한 내용은이 게시물의 끝 부분을 참조하십시오.

간단한 함수 Invocation

call 함수를 항상 호출하는 것은 많은 불편을 초래합니다. JavaScript에서는 Parens 구문(hello ("world"))을 사용하여 함수를 직접 Invoke 할 수있습니다. 그렇게 할 때 Invocation은 다음과 같이 됩니다.

1
2
3
4
5
6
7
8
9
function hello(thing) {
console.log("Hello " + thing);
}
// this:
hello("world")
// desugars to:
hello.call(window, "world");

ECMAScript 5에서 strict mode[2]를 사용 할 때만 아래와 같이 변경됩니다.

1
2
3
4
5
// this:
hello("world")
// desugars to:
hello.call(undefined, "world");

짧은 버전의 fn(… args) 함수 Invocation은 fn.call (window [ES5-strict: undefined], … args)과 동일합니다.

일반적인 (function() {})() 형태의 함수 호출은 (function() {}).call(window [ES5-strict: undefined])와 동일합니다.

[2] 사실, 나는 조금 거짓말을 했습니다. ECMAScript 5 스펙에서는 undefined가 (거의) 항상 전달되지만, 호출되는 함수가 strict 모드가 아니면 thisValue를 전역 객체로 변경해야 한다고 합니다. 이렇게하면 strict 모드 호출자가 기존의 non-strict 모드 라이브러리를 손상시키지 않도록 할 수 있습니다.

Member Function

다음으로 함수를 호출하는 가장 일반적인 방법은 객체의 멤버 (person.hello())입니다. 이 경우, Invocation desugars는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
var person = {
name: "Brendan Eich",
hello: function(thing) {
console.log(this + " says hello " + thing);
}
}
// this:
person.hello("world")
// desugars to this:
person.hello.call(person, "world");

hello 메서드가 어떻게 이런 형태의 객체에 Attach 되는지 중요하지 않습니다. 이전 예제에서 hello를 Standalone 함수로 정의했음을 기억하십시오. 우리가 동적으로 메서드를 객체에 Attach 하면 어떻게되는지 살펴 보겠습니다.

1
2
3
4
5
6
7
8
9
10
function hello(thing) {
console.log(this + " says hello " + thing);
}
person = { name: "Brendan Eich" }
person.hello = hello;
person.hello("world") // still desugars to person.hello.call(person, "world")
hello("world") // "[object DOMWindow]world"

이 함수는 지속적인 this를 가지고 있지 않습니다. 호출자가 호출 한 방식에 따라 호출 시간에 항상 설정됩니다.

Function.prototype.bind 사용하기

지속적인 this 값은 함수에 대한 참조에서 편리 하기 때문에 대부분의 개발자들은 변하지 않는 this를 가진 함수로 변환하기 위해 간단한 Closure 트릭을 사용 해왔습니다.

1
2
3
4
5
6
7
8
9
10
var person = {
name: "Brendan Eich",
hello: function(thing) {
console.log(this.name + " says hello " + thing);
}
}
var boundHello = function(thing) { return person.hello.call(person, thing); }
boundHello("world");

boundHello 함수 호출은 여전히 boundHello.call(window, "world")의 Desugar 임에도 불구하고 우회하여 Primitive call 메서드를 사용하여 this 값을 우리가 원했던 값으로 다시 변경합니다 .

함수를 조금 꼬아서 이 방법을 범용으로 만들 수 있습니다.

1
2
3
4
5
6
7
8
var bind = function(func, thisValue) {
return function() {
return func.apply(thisValue, arguments);
}
}
var boundHello = bind(person.hello, person);
boundHello("world") // "Brendan Eich says hello world"

이것을 이해하기 위해서는 두가지 정보가 더 필요합니다. 첫째, arguments는 함수에 전달된 모든 파라미터를 나타내는 Array와 비슷한 객체입니다. 그리고 둘째는, apply 메서드는 call Primitive와 똑같이 작동한다는 것입니다. 단, 한 번에 하나씩 인자를 나열하는 대신 Array와 비슷한 객체를 사용한다는 점이 call 메서드와 다릅니다.

우리의 bind 메서드는 단순히 새로운 함수를 반환합니다. bind 메서드가 호출 될 때, 우리의 새로운 함수는 전달된 원래의 함수를 단순히 호출하고, 또한 인수를 통해 전달된 thisValuethis로 설정합니다.

이런 방법은 일반적으로 많이 사용되는 관용구 였기 때문에, ES5는 이 동작을 구현하는 모든 Function 객체에 새로운 메서드 bind를 도입했습니다.

1
2
var boundHello = person.hello.bind(person);
boundHello("world") // "Brendan Eich says hello world"

콜백으로 전달할 Raw function이 필요할 때 가장 유용합니다.

1
2
3
4
5
6
7
8
var person = {
name: "Alex Russell",
hello: function() { console.log(this.name + " says hello world"); }
}
$("#some-div").click(person.hello.bind(person));
// div가 클릭되면 "Alex Russell says hello world"라고 인쇄됩니다.

이런 방식은 다소 어수선하기 때문에, TC39(ECMAScript의 다음 버전 작업을 하는 위원회)는 보다 우아하고 하위 버전 호환성을 갖춘 해결책을 만들기 위해 준비중입니다.

On jQuery

jQuery는 익명의 콜백 함수를 많이 사용하기 때문에 내부적으로 call 메서드를 사용하여 콜백의 this 값을 보다 유용한 값으로 설정합니다. 예를 들어 특별한 처리없이 모든 이벤트 핸들러에서 windowthis로 수신하는 대신 jQuery는 첫번째 매개 변수로 이벤트 핸들러를 설정하는 요소로 콜백에 대해 call을 호출합니다.

이것은 익명의 콜백에서 this의 기본값이 특별히 유용하지 않기 때문에 매우 유용 하지만, 초보자에게 this일반적으로 돌연변이 같이 이상한 개념이된 이유입니다.

만약 Sugary Function 호출을 Desugar Function func.call (thisValue, ... args)로 변환하는 기본 규칙을 이해한다면, JavaScript this 값을 잘못 사용하여 위험한 곳에 사용된 것을 찾을수 있습니다.

PS: I Cheated

필자는 여러 곳에서 명세의 정확한 표현을 약간 단순화했습니다. 아마 가장 중요한 속임수는 func.call을 “Primitive”이라고 부르는 방식일 것입니다. 실제로 spec에는 ‘func.call[obj.]func()’둘 다 사용하는 프리미티브 (내부적으로[[Call]])가 있습니다.

func.call의 정의를 살펴 보겠습니다.

  1. IsCallable (func)이 false 인 경우 TypeError 예외를 발생시킵니다.
  2. argList를 빈 목록으로 둡니다.
  3. 이 메서드가 복수의 파라미터로 호출됐을 경우, arg1로 시작되는 왼쪽에서 오른쪽의 순서로 각 파라미터를 argList의 마지막 요소로서 추가합니다.
  4. thisArg를 this 값으로, argList를 파라미터 목록으로 제공하여 [[Call]]func의 내부 메서드 호출 결과를 반환합니다.

보시다시피, 이 정의는 기본적으로 Primitive [[Call]] 연산에 바인딩하는 매우 간단한 JavaScript 언어입니다.

함수를 invoke하는 정의를 살펴보면 처음 7 단계에서 thisValueargList를 설정하고 마지막 단계는 “func에서 [[Call]] 내부 메서드를 호출 한 결과를 반환하고 thisValuethis 값으로 제공하고 argList리스트를 argument 값으로 제공하십시오.” 입니다.

argList와 thisValue가 결정되면 그것은 본질적으로 동일한 문구입니다.

call을 primitive라고 부르는 것에 조금 속임수를 썼지만, 의미는 본질적으로 이 글의 시작 부분에서 스펙을 이용해 설명 했던 것과 같습니다.


이 내용은 나중에 참고하기 위해 제가 공부하며 정리한 내용입니다.
의역, 오역, 직역이 있을 수 있음을 알려드립니다.
This post is a translation of this original article [http://yehudakatz.com/2011/08/11/understanding-javascript-function-invocation-and-this/]

참고

공유하기