当前位置: 代码迷 >> JavaScript >> 懂得javascript函数调用和“this”
  详细解决方案

懂得javascript函数调用和“this”

热度:327   发布时间:2012-11-25 11:44:31.0
理解javascript函数调用和“this”

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译作“原语”。

限于本人水平,如有不当,欢迎批评指正。


  相关解决方案