过去不少年里,我看到过太多关于JavaScript函数调用的混淆。尤为是,不少人抱怨函数调用中this的语义使人困惑。
在我看来,经过理解核心函数调用原语,而后将其余全部调用函数的方法视为在原语之上的语法糖,如此即可澄清不少这类疑惑。事实上,这正是ECMAScript规范对此的见解。在某些方面,这篇文章是规范的简化,但基本思路是同样的。javascript
首先,咱们先看一下函数调用的核心原语,Function对象的call
方法[1]。调用方法方法相对简单。java
argList
)thisValue
this
的值设为thisValue
和argList
做为其参数列表调用函数举例:闭包
function hello(thing) { console.log(this + " says hello " + thing); } hello.call("Yehuda", "world") //=> Yehuda says hello world
如你所见,咱们经过将this
设置为“Yehuda”
和单个参数“world”
来调用hello
方法。这正是JavaScript中函数调用的核心原语。你能够认为全部其余方式的函数调用均可”去糖“获得这个原语。(“去糖”是指采用一种方便的语法并用更基本的核心原语来描述它)。 app
[1]在ES5规范中,call
方法是用另外一个更底层的原语来描述的,但它是在那个原语之上的简单封装,因此我在这里简化了一下。有关更多信息,请参阅本文末尾。函数
显而易见,一直用call
调用函数将会很是烦人。JavaScript容许咱们直接使用括号语法hello("world")
来调用函数。当咱们这样作时,调用“去糖”以下:this
function hello(thing) { console.log("Hello " + thing); } // this: hello("world") // desugars to: hello.call(window, "world");
仅在使用严格模式[2]的ECMAScript 5中,此行为将改变:spa
// this: hello("world") // desugars to: hello.call(undefined, "world");
简短版本的说法是:像fn(...args)
这样的函数调用和fn.call(window [ES5-strict: undefined], ...args)
是如出一辙的。
注意,对于行内声明的函数(function() {})()
也是成立的:(function() {})()
和(function() {}).call(window [ES5-strict: undefined)
是如出一辙的。prototype
[2]事实上,我撒了一点小谎。ECMAScript 5规范说undefined
(几乎)老是被传递,但不在严格模式下时被调用函数应该将其thisValue
更改成全局对象。这容许严格模式下调用者避免破坏现有的非严格模式库。code
调用方法的下一个很是广泛的方式是做为一个对象的一个成员 (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
是一个类Array对象,它表示传递给函数的全部参数。其次,apply
方法的工做原理和call
原语除了它采用类Array对象而不是一次列出一个参数以外彻底同样。
咱们的bind
方法简单地返回一个新函数。当它被调用时,咱们的新函数只是调用传入的原始函数,并将原始值设置为其this
值,固然它也传递参数。
由于这是一个有点常见的习惯用法,ES5在全部Function
对象上引入了一个新方法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中大量使用匿名回调函数,所以它在内部使用call
方法将这些回调的this
值设置为更有用的值。举个例子,在全部事件处理程序中(如不进行特殊干预),jQuery不接收window
做为其this
值,而是经过把设置事件处理程序的元素做为它第一个参数在回调函数上调用call
。
这很是有用,由于匿名回调函数中的默认this
的值并非特别有用,除了它给初学者对javascript的一种印象,this
一般是一个奇怪的,常常变更至于难以解释的概念。
若是你理解了将“含糖”函数调用转换为“已去糖”的func.call(thisValue, ...args)
的基本规则,那么你应该可以在并非那么危险的JavaScriptthis
水域中航行。
在个别地方,我从规范的确切措辞中略微简化了事实。可能最严重的欺骗是我称呼func.call
为原语的说法。实际上,规范有一个func.call
和[obj.]func()
都使用的原语(内部称为[[Call]]
)。
然而,仍是看一下func.call
的定义吧:
IsCallable(func)
值为false
,则抛出TypeError异常argList
为一个空的ListargList
的最后一个元素thisArg
做为this
的值,并将argList
做为参数列表,返回调用func的内部方法[[Call]]
的结果如你所见,此定义本质上是一种很简单的JavaScript语义绑定到原语[[Call]]
操做。
若是你看一下调用函数的定义,前七个步骤设置thisValue
和argList
,最后一步是:“提供thisArg
做为this
的值,并将列表argList
做为参数值,返回调用func的内部方法[[Call]]
的结果。”
一旦肯定了argList
和thisValue
,它基本上是相同的措辞。
我在称call
是一个原语时做了一些欺骗,但其含义基本上与我在文章开头提出的规范和引用的章节是同样的。
还有一些我没有在这里介绍的其余案例(最值得注意的是with
)。