深刻理解JavaScirpt的函数调用和"this"

过去不少年里,我看到过太多关于JavaScript函数调用的混淆。尤为是,不少人抱怨函数调用中this的语义使人困惑。
在我看来,经过理解核心函数调用原语,而后将其余全部调用函数的方法视为在原语之上的语法糖,如此即可澄清不少这类疑惑。事实上,这正是ECMAScript规范对此的见解。在某些方面,这篇文章是规范的简化,但基本思路是同样的。javascript

核心原语

首先,咱们先看一下函数调用的核心原语,Function对象的call方法[1]。调用方法方法相对简单。java

  1. 从参数1到末尾建立一个参数列表(argList)
  2. 第一个参数(参数0)是thisValue
  3. 经过将this的值设为thisValueargList做为其参数列表调用函数

举例:闭包

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

因为jQuery中大量使用匿名回调函数,所以它在内部使用call方法将这些回调的this值设置为更有用的值。举个例子,在全部事件处理程序中(如不进行特殊干预),jQuery不接收window做为其this值,而是经过把设置事件处理程序的元素做为它第一个参数在回调函数上调用call
这很是有用,由于匿名回调函数中的默认this的值并非特别有用,除了它给初学者对javascript的一种印象,this一般是一个奇怪的,常常变更至于难以解释的概念。
若是你理解了将“含糖”函数调用转换为“已去糖”的func.call(thisValue, ...args)的基本规则,那么你应该可以在并非那么危险的JavaScriptthis水域中航行。

PS:我撒谎的部分

在个别地方,我从规范的确切措辞中略微简化了事实。可能最严重的欺骗是我称呼func.call为原语的说法。实际上,规范有一个func.call[obj.]func()都使用的原语(内部称为[[Call]])。
然而,仍是看一下func.call的定义吧:

  1. 若是IsCallable(func)值为false,则抛出TypeError异常
  2. argList为一个空的List
  3. 若是使用多个参数调用此方法,则从arg1开始,从左往右将每一个参数追加为argList的最后一个元素
  4. 提供thisArg做为this的值,并将argList做为参数列表,返回调用func的内部方法[[Call]]的结果

如你所见,此定义本质上是一种很简单的JavaScript语义绑定到原语[[Call]]操做。
若是你看一下调用函数的定义,前七个步骤设置thisValueargList,最后一步是:“提供thisArg做为this的值,并将列表argList做为参数值,返回调用func的内部方法[[Call]]的结果。”
一旦肯定了argListthisValue,它基本上是相同的措辞。
我在称call是一个原语时做了一些欺骗,但其含义基本上与我在文章开头提出的规范和引用的章节是同样的。
还有一些我没有在这里介绍的其余案例(最值得注意的是with)。

原文地址

相关文章
相关标签/搜索