JavaScript: 理解函数调用及this

本文由尤慕译自Understanding JavaScript Function Invocation and "this".

多年来,我都有看到大家对 js 函数调用的一些困惑。尤其是,许多人都会抱怨函数调用中的this语义含糊不清。

依我的观点,只要理解了核心的函数调用原语(the core function invocation primitive),把其它各种类型的函数调用看作建立在该原语之上的语法糖,
这些困惑就能迎刃而解。实际上,这正是ECMAScript规范思考的方式。本文是对规范的一种简化描述,但基础思想是一致的。

The Core Primitive: 核心原语

首先,我们来看核心的函数调用原语:Function 对象的call方法。该方法比较直观(译者注,call 即调用)。

  1. 从入参的第1位(从0开始)到最后,构造出一个参数列表(argList)
  2. 第0个入参是thisValue
  3. 将函数的this绑定到thisValue,函数的参数绑定到argList,然后调用该函数

例如:

function hello(thing) {  
  console.log(this + " says hello " + thing);
}

hello.call("Yehuda", "world") //=> Yehuda says hello world  

如你所见,调用hello方法时,this被绑定到 "Yehuda" ,入参是"world"。这便是 js 函数调用的核心原语。你可以认为,其它类型的方法调用都会转换成这种原语(即desugar: 将方便的语法转换成使用原语描述的形式)。

Simple Function Invocation:简单的函数调用

显然,调用函数时总是使用call是很烦人的一件事。js 允许我们直接通过括号语法进行函数调用(如:hello("world"))。我们看它是如何转换成原语的:

function hello(thing) {  
  console.log("Hello " + thing);
}

// 我们这样写:
hello("world")

// 会被转换成:
hello.call(window, "world");  

在ECMAScript 5中,这种行为在strict mode作了一些变化:

// 我们这样写:
hello("world")

// 会被转换成:
hello.call(undefined, "world");  

简而言之就是,像fn(...args)这样的函数调用和fn.call(window [ES5-strict: undefined], ...args)是互通的。

注意这同样适用于内联函数: (function() {})()(function() {}).call(window [ES5-strict: undefined)是一样的。

Member Functions:成员函数(方法)

另一种常见的函数调用,是调用的函数是作为对象的成员而存在(person.hello())。这种情况下,转换为原语描述:

  var person = {  
    name: "Brendan Eich",
    hello: function(thing) {
      console.log(this + " says hello " + thing);
    }
  }

  // 我们这样写:
  person.hello("world")

  // 会转换成:
  person.hello.call(person, "world");  

注意,我们不用考虑此类调用中hello方法是如何绑定到object对象上的(编译器会帮我们处理)。上一例的hello是被定义为一个独立的函数。接下来我们看看如果将hello动态的绑定到 对象上会发生什么:

function hello(thing) {  
  console.log(this + " says hello " + thing);
}

person = { name: "Brendan Eich" }  
person.hello = hello;

person.hello("world") // 仍然会被转换为 person.hello.call(person, "world")

hello("world") // "[object DOMWindow]world"  

注意到没,函数的this并非是个固定不变的值,而是在运行时由调用者所决定。

Using Function.prototype.bind:使用Function.prototype.bind

有时候需要保持函数的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方法只是简单的返回一个新的函数。调用这个返回的函数时,该函数只是简单的调用原来传入的函数,并将第二个传的参数thisValue设为该函数的this。当然,它还会将bind剩余的参数传入给调用bind所返回的那个函数。

由于该技巧在 js 中已经成为习语,ES5引入了一个新方法bind,让所有函数对象都拥有此方法:

var boundHello = person.hello.bind(person);  
boundHello("world") // "Brendan Eich says hello world"  

当需要将一个函数作为回调传递时,bind尤其有用:

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

总是bind bind bind的,难免显得笨拙。TC39(负责ECMAScript下一版的委员会),正在开发一个更优雅的、向后兼容的解决方案(译者注(2016-11-08): 目前有两种方式解决这种问题,一是es6的arrow functions以及es7的function bind operator)。

On jQuery:说说 jQuery

jQuery大量使用了匿名回调函数,它在内部会使用call来将回调的this绑定到一个更有用的值上。例如,事件处理器的this不是指向window,jQuery会通过call将其设定到事件处理器所绑定的元素。

这是极其有用的,因为匿名回调函数的默认this往往没什么用处,还会给js 初学者这样一种印象,即,this通常是一个怪异的、可变的、难以推理的概念。

如果你掌握了如何将一个普通函数转换成原语描述的形式(func.call(thisValue, ...args)),你应该能顺利走出 js this 的迷宫了。

PS: I Cheated ==> 附言: 我撒了谎

文中的几处,我对规范中的琐言碎语进行了简化。可能最大的欺骗之处即是我将func.call称作"原语(primitive)"。实际上,规范中是到的原语叫[[Call]], 不管是func.call还是 [obj.]func() ,都会使用该原语。

但是,我们看看规范中对func.call的定义(译者注:规范的语言用英文读更容易懂一些,这里不再进行中文翻译):

  1. If IsCallable(func) is false, then throw a TypeError exception.
  2. Let argList be an empty List.
  3. If this method was called with more than one argument then in left to right order starting with arg1 append each argument as the last element of argList
  4. Return the result of calling the [[Call]] internal method of func, providing thisArg as the this value and argList as the list of arguments.

如上所述,这个定义本质上就是js语言对原语[[Call]]的绑定说明。

如果你看了函数调用的说明,前7步是设定thisValueargList,最后一步是:

Return the result of calling the [[Call]] internal method on func, providing thisValue as the this value and providing the list argList as the argument values.

(对 func 调用内部的[[Call]]方法,将this指向thisValue,参数指向argList)"

规范中的语言十分繁琐,主是处理好argListthisValue

关于call作为原语,我撒了点谎,但是它和规范的本质是相同的。

注意,对于一些额外的情况(比如with),本文没有涉及。

你可能感兴趣的:(JavaScript: 理解函数调用及this)