多年以来,我看到了许多人对于JavaScript函数调用方面存在困惑与不解。特别是许多人会抱怨,"this"在函数调用中的语义是令人疑惑的。
在我看来,通过理解核心函数调用的基元,并且去看一下在此基础之上的其他方式的函数调用(对原始调用的思想的抽取)可以消除这些困惑。实际上,这正是ECMAScript 标准对于此的想法所在。在某些领域内来看,这篇文章是标准的简化,但是二者的基本思想是一致的。
核心的原始函数调用
首先,让我们来看一下核心的函数调用原始模型,一个Function
的call
方法[1]。call
方法相对比较直接。
- 取参数的第一个到最后一个组成一个参数列表(
argList
); - 第一个参数是
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函数调用的主要原始形式。你可以把所有其他的函数调用作为这个原始模式的替代来考虑。(要运用原始模型来调用其他函数就要用更便利的语法,并依据一个更基本的主要原始模型)
注:[1] 在ES5标准中,call
方法的描述基于其他的,更底层的基元,但是它是在那个原始模型上的非常简单的包裹,因此我在这里将其简化了。想了解更多可以参考这篇文章的最后的信息。
简单的函数调用
很明显,总是用call
来调用函数将是令人难以忍受的。JavaScript允许我们用括号语法来直接调用函数(hello("world")
)。当我们这么做的时候,调用是这样的:
function hello(thing) {
console.log("Hello " +thing);
}
// this:
hello("world")
// desugars to:
hello.call(window, "world");
在ECMAScript 5 中,在严格模式下这个行为已经发生了变化[2]:
// 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
(几乎)总是被传入,当不在严格模式下时,被调用的函数应该改变this
的值为全局对象。这允许严格模式的调用者避免打破已经存在的非严格模式库。
成员函数
下面一种非常常用的函数调用方式是函数作为一个对象的方法成员来调用(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
是完全一样的,唯一的不同是它采用的一个类数组的对象来作为参数,而不是用参数列表。
我们的 bind
方法简单的返回一个新函数。当它被调用的时候,我们的新函数简单的调用传进来的原始函数,设置原始值为this
。它也遍历参数。
因为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里面的bind
因为jQuery里面大量的应用匿名回调函数,它内部使用call方法来设置那些回调函数的this值为更有用的值。比如,在所有的事件处理器函数中,jQuery没有接收window
作为this
的值(如果你没有特殊的干预),而是对回调函数调用call方法,并将事件处理器函数作为元素的第一个参数。
这极其有用,因为在匿名函数内部的this的默认值并不是特别有用,但是它会给JavaScript初学者一个这样的感觉:this一般是很奇怪的,并且是难以推测的经常变化的一个概念。
如果你理解了从一个有糖分的函数调用到无糖分的函数调用func.call(thisValue, ...args)
的转换规则,你应该就能操纵这个并不是十分阴险的 JavaScript this
值这一领域。
type | this | func(…args) | Window | func(…args) func defined in ES5 Strict Mode | undefined | path.to.obj.func(…ars) | path.to.obj |
---|
附:我有所‘欺骗’
在几个地方,对于规范的措辞我有所简化。或许最重要的‘欺骗’是我将func.call
称为一个基元("primitive")。实际上,这个规范有一个基元(在内部被称为[[Call]]
)为func.call
和obj.]func()
所共有。
然而,让我们来看一下func.call
的定义:
- 如果
IsCallable(func)
结果为false
,那么就抛出一个类型异常; - 让
argList
为一个空列表; - 如果这个方法被调用的时候参数不止一个,那么从左到右开始将
arg1
追加每一个参数作为argList
的最新元素; - 返回调用
func
的内部方法[[Call]]
的执行结果,提供thisArg
作为this
的值,argList
作为参数的列表。
正如你所见,这个定义本质上是一个很简单的JavaScript的语言绑定到基元[[Call]]
操作符。
如果你看一下函数调用的定义,前七步是设置thisValue
和argList
,最后一步是:“返回 调用func
的内部方法 [[Call]]
的结果值,提供thisArg
作为this
的值,argList
作为参数的列表”。
一旦thisValue
和argList
的值被确定,func.call
的定义和函数调用的定义本质上是相同的字眼。
我在称call
为一个基元上做了一点欺骗,但是在本质上他们意思还是一样的,我在文章开头拿出规范且做了引用。
还有很多案例(大多数文章会明显的包含with
)我没有在文章中进行讨论。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!