译:理解 JavaScript 函数调用和“this”关键字
原文标题:Understanding JavaScript Function Invocation and "this"
原文地址:https://yehudakatz.com/2011/08/11/understanding-javascript-function-invocation-and-this/
原作者:Yehuda Katz (https://yehudakatz.com/author/wycats/)
译者:punkginger(http://www.punkginger.top)
这些年来,我发现很多人觉得 JavaScript 的函数调用很难理解。 其中很多人抱怨到函数调用中 this
的语义特别令人困惑。
在我看来,只要理解了核心、基础的函数调用方式[1],然后将所有其他调用函数的方法看做对该原始方式的锦上添花[2],这类困惑就迎刃而解。 事实上,这正是 ECMAScript 规范想实现的。 在某些方面,这篇文章是该对规范的简化,但基本思想是相同的。
[1]译者注:目前流传的译文中将 the core function invocation primitive 译为 函数调用的核心原语 ,本文中将其作为专有名词或许更合理,不过此处为了便于初读本文的读者理解,我将其意译。事实上,Primitive是JavaScript的专有名词,MDN将其译为原始数据或者基本类型,此处应该是作者类比基本类型,给基础的函数调用方法起的概括性的名称,我将其译为原始方法。
[2]译者注:原文sugar on top of that primitive,现有译文译为语法糖,我认为不太妥当。后文中的“简化”对应原文"desugar",也是作者针对这里"sugar"的描述造出来的词语,因此,这种简化并不是语法上的简化,而是概念上,抽象层级上的简化。
The Core Primitive 核心原始方法
首先,让我们来看看函数调用的核心原始方法:一个函数(Function)对象的call
方法[3]。这是一个非常直观的方法。
- 从参数的第1位(从0开始)到最后,构造出一个参数列表
argList
- 第1个入参是
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"
。这就是JavaScript最核心、基础的函数调用方式,核心原始方法。你可以认为其他所有的函数都可以简化成这种原始方法(“简化”就是将一种方便的语法用更加基础的核心原始方法描述)
[3]原注:在ES5规范中,call
方法通过另一个更底层的原始方法来描述,但它只是该原始方法之上相当薄的一层封装,所以我在这里稍微简化一下。 有关更多信息,请参阅本文末尾。
Simple Function Invocation 基础函数调用
显然,每次调用函数时都使用call
方法是一件相当恼人的事。JavaScript允许我们直接使用括号语法调用函数(hello("world")
),当我们那样做时,函数调用被简化为:
function hello(thing) {
console.log("Hello " + thing);
}
// 这样写:
hello("world")
// 简化为:
hello.call(window, "world");
在ECMAScript5的严格模式下,这种行为有些许变化[4]:
// 这样:
hello("world")
// 简化为:
hello.call(undefined, "world");
简单来说:一个函数可以这样fn(...args)
调用,而fn.call(window [ES5-strict: undefined], ...args)
是一样的。
要注意,这同样适用于内联函数 : (function() {})()
和(function() {}).call(window [ES5-strict: undefined)
是一样的。
[4]原注:事实上,我撒了一个小谎。ECMAScript 5规范认为undefined
(几乎)总是被省略,而这种情况下,在非严格模式时,被调用的函数应该将其thisValue
变为全局对象。这使得现有的非严格模式库不会在严格模式下被调用时崩溃。
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
方法如何以这种形式绑定到对象上并不重要。 上例中,我们将 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 Window]world"
请注意,函数并没有一个持久的this
的概念,函数的this
并非是个固定值,而是在运行时由调用者所决定。
Using 使用 Function.prototype.bind
因为有些时候一个固定的this值可以给我们带来便利,所以人们长久以来一种用一种简单的闭包技巧,使一个函数拥有不变的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
方法返回一个新的函数。当bind
被调用时,我们的新函数调用了被传入的初始函数,并将初始函数的this
设为传入的值。它也会将arguments
对象完整传递。
因为这种写法成了某种习语,所以ES5在所有Function
对象上都引入了一个实现这种行为的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));
// 当这个div被点击时, "Alex Russell says hello world" 会被打印在屏幕上
当然,这种写法总有些笨拙,因此TC39(制定未来版本的ECMAScript的委员会)仍在寻找一种更加优雅且向后兼容的解决方案[5]
[5]译者注:感谢尤慕在简书的文章,目前有两种解决方法,es6的arrow functions以及es7的function bind operator
On jQuery 在jQuery中
由于jQuery使用了大量的匿名回调函数,它会在内部使用call
方法将那些回调的this
设为更有用的值。举个例子,jQuery中所有的事件处理器的this
都不接受window
,jQuery在回调中调用call
方法,将事件处理器设为其第一个参数,即thisValue
这非常有用,因为匿名回调的默认this
的值没什么用处,而这会让JavaScript初学者认为this
是一个怪异、难以理解且多变的概念
如果你掌握了将一个修饰过的函数转变为简化过的函数func.call(thisValue, ...args)
的基本方法,你应该就能在平静的JavaScript this的海域中畅通无阻地航行。
PS: I Cheated 附:我作弊了
我在上文中的几处将规范里的确切措辞简化了。或许其中最大的欺骗在于我将func.call
称为原始方法(primitive)。事实上,规范中有一个func.call
和[obj.]func()
两者都使用的原始方法(内部称为[[Call]]
)
但是,让我们来看看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, providing thisArg as the this value and argList as the list of arguments.
译:
- 如果
IsCallable(func)
结果为false,抛出一个TypeError异常 - 让
argList
成为空列表 - 如果调用此方法时使用多个参数,则从 arg1 开始按从左到右的顺序将每个参数附加到
argList
后作为最后一个元素 - 返回调用 func 的 [[Call]] 内部方法的结果,提供
thisArg
作为 this 值,提供argList
作为参数列表。
你会发现,这个定义本质上是JavaScript对[[Call]]方法的绑定操作的简单说明
如果你看了函数调用的说明,你会发现前7步设定了thisValue
和argList
,而最后一步是:返回调用 func 上的 [[Call]]
内部方法的结果,提供 thisValue
作为 this 值并提供列表 argList
作为参数值
而在设定好thisValue
和argList
后,规范中的语言十分繁琐。
我确实在将call
称为原始方法上撒了个小谎,但我所表达的含义与本文所引用的规范和文章是一致的。
注意,对于一些额外的情况(主要是with
),本文没有涉及。
Comments | NOTHING