多年以来,我看到了许多人对于JavaScript函数调用有很多困惑。特别是许多人会抱怨,”this”在函数调用中的语义是令人疑惑的。
在我看来,通过理解核心的函数调用的原始模型,并且去看一下在此基础之上的其他方式的函数调用(对原始调用的思想的抽取)可以消除这些困惑。实际上,ECMAScript 标准也是这么考虑的。在某些地方来看,这篇文章是标准的简化,但是二者的基本思想是一致的。
核心的原始函数调用方法
首先,让我们来看一下核心的函数调用原始模型,一个Function的call方法[1]。call方法相对比较直接。
- 1、取参数的第一个到最后一个组成一个参数列表(argList);
- 2、第一个参数是thisValue;
- 3、把this设置为thisValue同时argList作为它的参数列表来调用函数。
1
2
3
4
5 |
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”))。当我们这么做的时候,调用是这样的:
1
2
3
4
5
6
7
8
9 |
function hello(thing) {
console.log("Hello"+thing);
}
// this:
hello("world")
// desugars to:
hello.call(window,"world"); |
在ECMAScript 5 中,在严格模式下这个行为已经发生了变化[2]:
1
2
3
4
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(几乎)总是被传入,当不在严格模式下时,被调用的函数应该改变this的值为全局对象。这允许严格模式的调用者避免打破已经存在的非严格模式库。
成员函数
下面一种非常常用的函数调用方式是函数作为一个对象的方法成员来调用(person.hello())。这种情况下函数调用像这样:
1
2
3
4
5
6
7
8
9
10
11
12 |
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为一个独立的函数。让我们来看看动态的把函数附加到对象上发生了什么:
1
2
3
4
5
6
7
8
9
10 |
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值:
尽管我们的boundHello 方法仍然可以改写为boundHello
1
2
3
4
5
6
7
8
9
10 |
var person = {
name:"Brendan Eich",
hello:function(thing) {
console.log(this.name +"says hello"+thing);
}
}
var boundHello=function(thing){retur person.hello.call(person,thing); }
boundHello("world"); |
.call(window, “world”) ,我们转换了一个角度,应用我们的基元call方法来改变this为我们期望的值。
我们可以用自制体系来使得这个窍门有一般用途:
1
2
3
4
5
6
7
8 |
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对象来实现下面的行为:
1
2 |
var boundHello = person.hello.bind(person);
boundHello("world")//"Brendan Eich says hello world" |
当你需要一个未加工的函数作为回调函数的时候这是非常有用的:
1
2
3
4
5
6
7
8 |
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 值这一领域。
附:我有所‘欺骗’
在几个地方,对于规范的措辞我有所简化。或许最重要的‘欺骗’是我将func.call称为一个基元(”primitive”)。实际上,这个规范有一个基元(在内部被称为[[Call]])为func.call和obj.]func()所共有。
然而,让我们来看一下func.call的定义:
- 1、如果IsCallable(func) 结果为false,那么就抛出一个类型异常;
- 2、让 argList 为一个空列表;
- 3、如果这个方法被调用的时候参数不止一个,那么从左到右开始将arg1追加每一个参数作为 argList 的最新元素;
- 4、返回调用func的内部方法[[Call]]的执行结果,提供thisArg作为this的值,argList作为参数的列表。
正如你所见,这个定义本质上是一个很简单的JavaScript的语言绑定到基元[[Call]]操作符。
如果你看一下函数调用的定义,前七步是设置thisValue和argList,最后一步是:“返回 调用func的内部方法 [[Call]]的结果值,提供thisArg作为this的值,argList作为参数的列表”。
一旦thisValue和argList的值被确定,func.call的定义和函数调用的定义本质上是相同的字眼。
我在称call为一个基元上做了一点欺骗,但是在本质上他们意思还是一样的,我在文章开头拿出规范且做了引用。
还有很多案例(大多数文章会明显的包含with)我没有在文章中进行讨论。
|