多年来,我看到很多关于JavaScript函数调用的混淆。特别是,很多人抱怨this
函数调用的语义令人困惑。
在我看来,通过理解核心函数调用原语,然后查看在该原语之上调用函数作为糖的所有其他方法,可以清除很多这种混淆。事实上,这正是ECMAScript规范对此的看法。在某些领域,这篇文章是规范的简化,但基本思路是一样的。
核心原语
首先,让我们看一下核心函数调用原语,一个函数的call
方法[1]。呼叫方法相对简单。
argList
从参数1到结尾创建参数列表()- 第一个参数是
thisValue
- 使用
this
set tothisValue
和argList
作为其参数列表调用该函数
例如:
function hello(thing) {
console.log(this + " says hello " + thing);
}
hello.call("Yehuda", "world") //=> Yehuda says hello world
如您所见,我们hello
使用this
set to "Yehuda"
和单个参数调用该方法"world"
。这是JavaScript函数调用的核心原语。您可以将所有其他函数调用视为对该原语的贬低。(对“desugar”来说是采用方便的语法并用更基本的核心原语来描述它)。
[1]在ES5规范中,该call
方法是根据另一个更低级的原语来描述的,但它在该原语之上是一个非常薄的包装器,所以我在这里简化了一下。有关更多信息,请参阅本文末尾。
简单的函数调用
显然,一直调用函数call
会非常烦人。JavaScript允许我们使用parens语法直接调用函数(hello("world")
。当我们这样做时,调用desugars:
function hello(thing) {
console.log("Hello " + thing);
}
// this:
hello("world")
// desugars to:
hello.call(window, "world");
仅当使用严格模式 [2] 时,ECMAScript 5中的此行为已更改:
// this:
hello("world")
// desugars to:
hello.call(undefined, "world");
简短版本是:函数调用就像fn(...args)
是一样fn.call(window [ES5-strict: undefined], ...args)
。
请注意,对于内联声明的函数也是如此:(function() {})()
与...相同(function() {}).call(window [ES5-strict: undefined)
。
[2]实际上,我撒谎了一下。ECMAScript 5规范说undefined
(几乎)总是传递,但被调用的函数应该thisValue
在不处于严格模式时将其更改为全局对象。这允许严格模式调用者避免破坏现有的非严格模式库。
会员职能
调用方法的下一个非常常见的方法是作为object(person.hello()
)的成员。在这种情况下,调用desugars:
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
方法如何附加到此表单中的对象并不重要。请记住,我们之前定义hello
为独立功能。让我们看看如果我们动态地附加到对象会发生什么:
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
:
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
是一个类似于Array的对象,它表示传递给函数的所有参数。其次,该apply
方法与call
原语完全相同,只是它采用类似于Array的对象,而不是一次列出一个参数。
我们的bind
方法只返回一个新函数。调用它时,我们的新函数只调用传入的原始函数,将原始值设置为this
。它也通过参数传递。
因为这是一个有点常见的习惯用法,ES5 bind
在Function
实现此行为的所有对象上引入了一个新方法:
var boundHello = person.hello.bind(person);
boundHello("world") // "Brendan Eich says hello world"
当您需要将原始函数作为回调传递时,这非常有用:
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
当然,这有点笨拙,TC39(负责ECMAScript下一版本的委员会)继续研究更优雅,仍然向后兼容的解决方案。
在jQuery上
因为jQuery大量使用匿名回调函数,所以它在call
内部使用该方法将this
这些回调的值设置为更有用的值。例如,jQuery 不是window
像this
在所有事件处理程序中那样接收(就像没有特殊干预一样),而是call
使用将事件处理程序设置为其第一个参数的元素调用回调。
这是非常有用的,因为默认值this
的匿名回调不是特别有用,但它可以给JavaScript能给初学者的印象this
是,一般一个怪,经常发生突变的概念是很难推论。
如果您了解将含糖函数调用转换为desugared的基本规则func.call(thisValue, ...args)
,您应该能够导航JavaScript this
值不那么危险的水域。
PS:我被骗了
在一些地方,我从规范的确切措辞中略微简化了现实。可能最重要的作弊是我称之为func.call
“原始”的方式。在现实中,该规范具有原始(内部称为[[Call]]
),这两个func.call
和[obj.]func()
使用。
但是,看一下定义func.call
:
- 如果IsCallable(func)为false,则抛出TypeError异常。
- 让argList为空List。
- 如果使用多个参数调用此方法,则从arg1开始以从左到右的顺序将每个参数附加为argList的最后一个元素
- 返回调用func的[[Call]]内部方法的结果,提供thisArg作为此值,并将argList作为参数列表。
如您所见,此定义本质上是一种非常简单的JavaScript语言绑定到基本[[Call]]
操作。
如果你看一下调用函数的定义,第一七个步骤设置thisValue
和argList
,最后一步是:“返回调用的结果[电话]]上FUNC内部方法,提供thisValue作为该值,并提供列表argList作为参数值。“
一旦确定argList
并且thisValue
已经确定,它基本上是相同的措辞。
我在调用call
一个原语时作了一些欺骗,但其含义基本上与我在本文开头提取规范并引用章节和诗句时的含义相同。
我还with
没有在这里介绍一些其他案例(最值得注意的是涉及)。