本文由尤慕译自Understanding JavaScript Function Invocation and "this".
多年来,我都有看到大家对 js 函数调用的一些困惑。尤其是,许多人都会抱怨函数调用中的this语义含糊不清。
依我的观点,只要理解了核心的函数调用原语(the core function invocation primitive),把其它各种类型的函数调用看作建立在该原语之上的语法糖,
这些困惑就能迎刃而解。实际上,这正是ECMAScript规范思考的方式。本文是对规范的一种简化描述,但基础思想是一致的。
The Core Primitive: 核心原语
首先,我们来看核心的函数调用原语:Function 对象的call方法。该方法比较直观(译者注,call 即调用)。
- 从入参的第1位(从0开始)到最后,构造出一个参数列表(argList)
- 第0个入参是thisValue
- 将函数的this绑定到thisValue,函数的参数绑定到argList,然后调用该函数
例如:
function hello(thing) {
console.log(this + " says hello " + thing);
}
hello.call("Yehuda", "world") //=> Yehuda says hello world
如你所见,调用hello方法时,this被绑定到 "Yehuda" ,入参是"world"。这便是 js 函数调用的核心原语。你可以认为,其它类型的方法调用都会转换成这种原语(即desugar
: 将方便的语法转换成使用原语描述的形式)。
Simple Function Invocation:简单的函数调用
显然,调用函数时总是使用call是很烦人的一件事。js 允许我们直接通过括号语法进行函数调用(如:hello("world")
)。我们看它是如何转换成原语的:
function hello(thing) {
console.log("Hello " + thing);
}
// 我们这样写:
hello("world")
// 会被转换成:
hello.call(window, "world");
在ECMAScript 5中,这种行为在strict mode作了一些变化:
// 我们这样写:
hello("world")
// 会被转换成:
hello.call(undefined, "world");
简而言之就是,像fn(...args)
这样的函数调用和fn.call(window [ES5-strict: undefined], ...args)
是互通的。
注意这同样适用于内联函数: (function() {})()
和(function() {}).call(window [ES5-strict: undefined)
是一样的。
Member Functions:成员函数(方法)
另一种常见的函数调用,是调用的函数是作为对象的成员而存在(person.hello()
)。这种情况下,转换为原语描述:
var person = {
name: "Brendan Eich",
hello: function(thing) {
console.log(this + " says hello " + thing);
}
}
// 我们这样写:
person.hello("world")
// 会转换成:
person.hello.call(person, "world");
注意,我们不用考虑此类调用中hello方法是如何绑定到object对象上的(编译器会帮我们处理)。上一例的hello是被定义为一个独立的函数。接下来我们看看如果将hello动态的绑定到 对象上会发生什么:
function hello(thing) {
console.log(this + " says hello " + thing);
}
person = { name: "Brendan Eich" }
person.hello = hello;
person.hello("world") // 仍然会被转换为 person.hello.call(person, "world")
hello("world") // "[object DOMWindow]world"
注意到没,函数的this并非是个固定不变的值,而是在运行时由调用者所决定。
Using Function.prototype.bind:使用Function.prototype.bind
有时候需要保持函数的this不变,人们很久之前就使用了一种闭包的技巧,来满足这种需求:
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")
,我们其实是绕了个弯,用原始的call方法将this设定为我们需要的值。
我们可以将这种技巧抽象的更具普适性:
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是一个类数组对象,用来装载传递给一个函数的所有参数。其二,apply方法和call方法工作方式几乎相同,不同点是前者的参数列表是一个类数组对象,后者的参数列表是一个参数一个位置。
我们的bind方法只是简单的返回一个新的函数。调用这个返回的函数时,该函数只是简单的调用原来传入的函数,并将第二个传的参数thisValue设为该函数的this。当然,它还会将bind剩余的参数传入给调用bind所返回的那个函数。
由于该技巧在 js 中已经成为习语,ES5引入了一个新方法bind,让所有函数对象都拥有此方法:
var boundHello = person.hello.bind(person);
boundHello("world") // "Brendan Eich says hello world"
当需要将一个函数作为回调传递时,bind尤其有用:
var person = {
name: "Alex Russell",
hello: function() { console.log(this.name + " says hello world"); }
}
$("#some-div").click(person.hello.bind(person));
// when the div is clicked, "Alex Russell says hello world" is printed
总是bind bind bind的,难免显得笨拙。TC39(负责ECMAScript下一版的委员会),正在开发一个更优雅的、向后兼容的解决方案(译者注(2016-11-08): 目前有两种方式解决这种问题,一是es6的arrow functions以及es7的function bind operator)。
On jQuery:说说 jQuery
jQuery大量使用了匿名回调函数,它在内部会使用call来将回调的this绑定到一个更有用的值上。例如,事件处理器的this不是指向window,jQuery会通过call将其设定到事件处理器所绑定的元素。
这是极其有用的,因为匿名回调函数的默认this往往没什么用处,还会给js 初学者这样一种印象,即,this通常是一个怪异的、可变的、难以推理的概念。
如果你掌握了如何将一个普通函数转换成原语描述的形式(func.call(thisValue, ...args)
),你应该能顺利走出 js this 的迷宫了。
PS: I Cheated ==> 附言: 我撒了谎
文中的几处,我对规范中的琐言碎语进行了简化。可能最大的欺骗之处即是我将func.call称作"原语(primitive)"。实际上,规范中是到的原语叫[[Call]]
, 不管是func.call还是 [obj.]func() ,都会使用该原语。
但是,我们看看规范中对func.call的定义(译者注:规范的语言用英文读更容易懂一些,这里不再进行中文翻译):
- If
IsCallable(func)
is false, then throw a TypeError exception. - Let
argList
be an empty List. - If this method was called with more than one argument then in left to right order starting with arg1 append each argument as the last element of
argList
- Return the result of calling the
[[Call]]
internal method of func, providingthisArg
as the this value and argList as the list of arguments.
如上所述,这个定义本质上就是js语言对原语[[Call]]的绑定说明。
如果你看了函数调用的说明,前7步是设定thisValue和argList,最后一步是:
Return the result of calling the [[Call]] internal method on func, providing thisValue as the this value and providing the list argList as the argument values.
(对 func 调用内部的[[Call]]
方法,将this指向thisValue,参数指向argList)"
规范中的语言十分繁琐,主是处理好argList和thisValue。
关于call作为原语,我撒了点谎,但是它和规范的本质是相同的。
注意,对于一些额外的情况(比如with
),本文没有涉及。