2011.8.11
多年以来,我看到大量关于javascript函数调用的困惑。尤其,许多人抱怨函数调用中“this”的语意是混乱的。
在我看来,大量这样的混乱可以通过理解核心函数调用原语被清理,然后再看所有其他在原语之上进行包装的调用函数的方法。实际上,这正好是ECMAScript规格对这个问题的考虑。在某些领域,这个是一个规格的简化,但基本思想是一样的。
核心原语
首先,我们来看核心函数调用原语,一个函数的调用方法[1]。这个调用方法是相对直线向前的(The call method is relatively straightforward.)。
1. 构造参数列表(argList)从参数1到最后
2. 第一个参数是thisValue
3. 把this 赋值给thisValue 并用argList 作为参数列表调用函数
例如:
function hello(thing) { console.log(this + " says hello " + thing); } hello.call("Yehuda", "world") //=> Yehuda says hello world
正如你看到的,我们通过把this赋值给 "Yehuda"和一个单一参数来调用hello 方法。这就是javascript函数调用核心原语。你能想象所有其他的函数调用都是对这个原语包装。(包装是使用一个便利的语法和按照更基本的核心原语描述它)
[1] In the ES5 spec, the call method isdescribed in terms of another, more low level primitive, but it’s a
very thinwrapper on top of that primitive, so I’m simplifying a bit here. See the end ofthis post for more information.
简单函数调用
很明显,任何时候使用call 调用函数都是相当的烦人的。Javascript允许我们使用括弧直接调用函数(hello("world"))。我们这样做的时候,调用包装为:
function hello(thing) { console.log("Hello " + thing); } // this: hello("world") // desugars to: hello.call(window, "world");
这个行为在ECMAScript中只有当使用严格模式时改变了:
// 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 为全局对象。这允许严格模式调用者避免破坏现存的非严格模式库。
成员函数
下一个非常常见的方法是调用作为对象的成员方法(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"),我们转了一圈然然后使用我们原语调用方法来修改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。它也通过参数传递。
因为这是一个比较常见的习语,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中
因为jQuery大量使用匿名回调函数,它内部使用call方法设置那些回调的this 值为更有用的值。例如, 在所有的事件处理中替代接收window 作为this(如你没有特别的处理),jQuery在回调中使用建立事件处理器的元素作为它的第一个参数调用call 。
这非常有用,因为匿名调用中this 的默认值不是特别的有用,但它可以给JavaScipt初学者的印象,this 通常是奇怪的,常常改变,很难确定的概念。 (原文:but it can give beginners to JavaScriptthe impression that this is, ingeneral a strange, often mutated concept that is hard to reason about.)
如果你理解了基本规则,转换一个包装的函数调用为一个非包装的func.call(thisValue,...args),你应该能导航不那么变化莫测的Javascript this 值水域。(原文:you should be able to navigate the not sotreacherous waters of the JavaScript this value.)
附录:我行骗了
在一些地方,我从规格精确的语法简化了现实一点。可能最重要欺骗是我称func.call为“原语”的习惯。实际上,该规格有一个原语(内部引用为[[Call]])是func.call 和[obj.]func()使用。
不管怎样,一起来看看func.call定义:
1. 如果IsCallable(func)是false,则抛出一个TypeError异常。
2. 置argList为空列表。
3. 如果方法被调用时使用超过一个参数,那么以从左到右顺序从arg1开始附加每个参数作为argList最后的元素。
4. 返回调用func的[[Call]]内部方法的结果,提供thisArg作为this值,和argList作为参数列表。
正如你看到的,这个定义是本质非常简单的对原语[[Call]] 操作的JavaScript语言绑定。
如果你看一下调用函数的定义,七步的第一步是建立thisValue和argList,最后一步是:“返回在func中调用[[Call]]内部方法的结果,提供thisValue作为this值并提供参数列表作为argument的值。”
它的本质是相同的语法,一旦argList 和thisValue 被确定。
我撒了一点谎,在把call 叫做原语,但它的意思的本质是相同的,所以在本文的开始和引用的章节与段落我拉出规格。
这也有一些附加的情况(尤其包括)在这里没有提到情况。
这个条目在2011年8月11日星期四上午2:54被张贴,并在JavaScript下存档。你可以通过RSS2.0反馈跟随任意响应到这个入口。你可以跳到最后离开响应。Pinging当前不允许(原文:Pinging is currently not allowed.没有理解是什么意思)。
原文地址:http://yehudakatz.com/2011/08/11/understanding-javascript-function-invocation-and-this/
译注:sugary 我译作了“包装”;primitive译作“原语”。
限于本人水平,如有不当,欢迎批评指正。