在过去的几年,我看到了很多对Javascript Function调用的困惑。特别的,很多人都抱怨在函数调用中this的语义让人感到困扰。
在我看来,通过简单的核心函数调用,然后研究在这个简单的函数调用之上作为糖调用函数的所有其他方式,可以消除许多这种混淆。事实上,这正是ECMAScript规范所考虑的。在某些方面,这篇文章是对规范的简化,但基本思想是相同的。
核心原语:
首先,让我们看一下原生的核心函数调用,即函数的调用方法[1]。call方法相对简单。
1.从参数1到末尾创建一个参数列表(argList)
2.第一个参数是 thisValue
3.给this赋值thisValue,并将argList作为参数,调用次函数。
例如:
function hello(thing) { console.log(this + " says hello " + thing); } hello.call("Yehuda", "world") //=> Yehuda says hello world
正如你所见,我们通过将hello的this指向字符串"Yehda",并且传参"world",调用此函数。这是一个Javascript核心原语调用。你可以基于此函数调用去发散思维,采用一种方便的语法,并以更基本的简单语句进行描述。
[1] 在ES5规范中,该call
方法是根据另一个更底层的原语进行描述的,但是它是该原语之上非常薄的包装,因此在此我进行了一些简化。有关更多信息,请参见本文结尾。
简单函数调用
显然,一直调用函数call
会很烦人。JavaScript允许我们使用括号语法(hello("world")
直接调用函数。
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
在非严格模式下,被调用的函数应将其更改为全局对象。这允许严格模式调用者避免破坏现有的非严格模式库。
成员函数(Member Functions)
译者注:成员函数 ( member function )是由类定义的函数,有时称为类方法(method) ,当调用成员函数时,(通常)指定函数要操作的对象。
调用方法的下一个非常常见的方法是作为对象(person.hello()
)的成员。在这种情况下,调用将终止
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
是一个类似数组的对象,它表示传递给函数的所有参数。其次,该apply
方法的工作方式与call
原始方法完全相同,不同之处在于它采用了一个类似于Array的对象,而不是一次列出一个参数。
我们的bind
方法只是返回一个新函数。调用它时,我们的新函数将简单地调用传入的原始函数,将原始值设置为this
。它还通过参数传递。
由于这是一个有点普遍的习惯用法,因此ES5在所有的函数上实现了一个bind方法,以此来实现上述功能:
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指向(不需要特殊干预),jQuery使用将事件处理程序设置为其第一个参数的元素对回调调进行调用。
这非常有用,因为匿名回调中的默认值不是特别有用,但它会给JavaScript初学者留下这样的印象:这通常是一个奇怪的、经常变化的概念,很难理解。
如果你已经理解了如何将一个使用this的函数在另一个没有明确this指向的fun.call(thisValue,...args),您应该能够在不那么危险的地方使用JavaScript中导航这个值。
PS:我被骗了:
在几个地方,我从规范的确切措辞中稍微简化了实现。可能最重要的欺骗就是我所说func.call是个
“原始”方式。实际上,规范中有一个原语(内部称为[[Call]]
)。它同时是 func.call 和 [obj.]func() 使用。
但是,请看一下func.call 的定义
:
- 如果IsCallable(func)为false,则抛出TypeError异常。
- 令argList为空列表。
- 如果使用多个参数调用此方法,则从arg1开始以从左到右的顺序将每个参数附加为argList的最后一个元素
- 返回调用func的[[Call]]内部方法的结果,并提供thisArg作为this值,并提供argList作为参数列表。
如您所见,此定义实质上是原始 call 的最简单的javascript语法实现。
如果你看了函数调用的定义,前7步设定了this的值和参数列表,并且,最后一步是:“返回调用函数内置的call方法提供了thisValue作为this值,并提过了一个 argList 作为参数值”。
一旦argList和this值被确定,它本质上是相同的语句。
在调用call原语时,我有点作弊,但其含义本质上与我在本文开头拿出规范并引用章节和韵文时的含义相同。
还有一些我在这里没有提到的其他情况(最明显的是涉及到)。
原文出处:翻译自Yehuda Katz https://yehudakatz.com/2011/08/11/understanding-javascript-function-invocation-and-this/