译:理解 JavaScript 函数调用和“this”关键字


译:理解 JavaScript 函数调用和“this”关键字

原文标题:Understanding JavaScript Function Invocation and "this"

原文地址:https://yehudakatz.com/2011/08/11/understanding-javascript-function-invocation-and-this/

原作者:Yehuda Katz (https://yehudakatz.com/author/wycats/) 

译者:punkginger(http://www.punkginger.top)

这些年来,我发现很多人觉得 JavaScript 的函数调用很难理解。 其中很多人抱怨到函数调用中 this 的语义特别令人困惑。

在我看来,只要理解了核心、基础的函数调用方式[1],然后将所有其他调用函数的方法看做对该原始方式的锦上添花[2],这类困惑就迎刃而解。 事实上,这正是 ECMAScript 规范想实现的。 在某些方面,这篇文章是该对规范的简化,但基本思想是相同的。


[1]译者注:目前流传的译文中将 the core function invocation primitive 译为 函数调用的核心原语 ,本文中将其作为专有名词或许更合理,不过此处为了便于初读本文的读者理解,我将其意译。事实上,Primitive是JavaScript的专有名词,MDN将其译为原始数据或者基本类型,此处应该是作者类比基本类型,给基础的函数调用方法起的概括性的名称,我将其译为原始方法

[2]译者注:原文sugar on top of that primitive,现有译文译为语法糖,我认为不太妥当。后文中的“简化”对应原文"desugar",也是作者针对这里"sugar"的描述造出来的词语,因此,这种简化并不是语法上的简化,而是概念上,抽象层级上的简化。

The Core Primitive 核心原始方法

首先,让我们来看看函数调用的核心原始方法:一个函数(Function)对象的call方法[3]。这是一个非常直观的方法。

  1. 从参数的第1位(从0开始)到最后,构造出一个参数列表argList
  2. 第1个入参是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"。这就是JavaScript最核心、基础的函数调用方式,核心原始方法。你可以认为其他所有的函数都可以简化成这种原始方法(“简化”就是将一种方便的语法用更加基础的核心原始方法描述)


[3]原注:在ES5规范中,call方法通过另一个更底层的原始方法来描述,但它只是该原始方法之上相当薄的一层封装,所以我在这里稍微简化一下。 有关更多信息,请参阅本文末尾。

Simple Function Invocation 基础函数调用

显然,每次调用函数时都使用call方法是一件相当恼人的事。JavaScript允许我们直接使用括号语法调用函数(hello("world")),当我们那样做时,函数调用被简化为:

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

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

// 简化为:
hello.call(window, "world");

在ECMAScript5的严格模式下,这种行为有些许变化[4]:

// 这样:
hello("world")

// 简化为:
hello.call(undefined, "world");

简单来说:一个函数可以这样fn(...args)调用,而fn.call(window [ES5-strict: undefined], ...args)是一样的。

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


[4]原注:事实上,我撒了一个小谎。ECMAScript 5规范认为undefined(几乎)总是被省略,而这种情况下,在非严格模式时,被调用的函数应该将其thisValue变为全局对象。这使得现有的非严格模式库不会在严格模式下被调用时崩溃。

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 方法如何以这种形式绑定到对象上并不重要。 上例中,我们将 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 Window]world"

请注意,函数并没有一个持久的this的概念,函数的this并非是个固定值,而是在运行时由调用者所决定。

Using 使用 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是一个类似数组的对象,它代表了所有传入函数的参数。其次,apply方法与原始方法call很像,只是在接收一列参数时,它接收一个类似数组的对象,而不是每次接收列表中的一个参数。

我们的bind方法返回一个新的函数。当bind被调用时,我们的新函数调用了被传入的初始函数,并将初始函数的this设为传入的值。它也会将arguments对象完整传递。

因为这种写法成了某种习语,所以ES5在所有Function对象上都引入了一个实现这种行为的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));

// 当这个div被点击时, "Alex Russell says hello world" 会被打印在屏幕上

当然,这种写法总有些笨拙,因此TC39(制定未来版本的ECMAScript的委员会)仍在寻找一种更加优雅且向后兼容的解决方案[5]


[5]译者注:感谢尤慕在简书的文章,目前有两种解决方法,es6的arrow functions以及es7的function bind operator

On jQuery 在jQuery中

由于jQuery使用了大量的匿名回调函数,它会在内部使用call方法将那些回调的this设为更有用的值。举个例子,jQuery中所有的事件处理器的this都不接受window,jQuery在回调中调用call方法,将事件处理器设为其第一个参数,即thisValue

这非常有用,因为匿名回调的默认this的值没什么用处,而这会让JavaScript初学者认为this是一个怪异、难以理解且多变的概念

如果你掌握了将一个修饰过的函数转变为简化过的函数func.call(thisValue, ...args)的基本方法,你应该就能在平静的JavaScript this的海域中畅通无阻地航行。

PS: I Cheated 附:我作弊了

我在上文中的几处将规范里的确切措辞简化了。或许其中最大的欺骗在于我将func.call称为原始方法(primitive)。事实上,规范中有一个func.call[obj.]func()两者都使用的原始方法(内部称为[[Call]]

但是,让我们来看看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.

译:

  1. 如果IsCallable(func)结果为false,抛出一个TypeError异常
  2. argList成为空列表
  3. 如果调用此方法时使用多个参数,则从 arg1 开始按从左到右的顺序将每个参数附加到argList 后作为最后一个元素
  4. 返回调用 func 的 [[Call]] 内部方法的结果,提供 thisArg 作为 this 值,提供 argList 作为参数列表。

你会发现,这个定义本质上是JavaScript对[[Call]]方法的绑定操作的简单说明

如果你看了函数调用的说明,你会发现前7步设定了thisValueargList,而最后一步是:返回调用 func 上的 [[Call]] 内部方法的结果,提供 thisValue 作为 this 值并提供列表 argList 作为参数值

而在设定好thisValueargList后,规范中的语言十分繁琐。

我确实在将call称为原始方法上撒了个小谎,但我所表达的含义与本文所引用的规范和文章是一致的。

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

声明:punkginger's blog|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 译:理解 JavaScript 函数调用和“this”关键字


曾有言“将两件不相干的事物的名称组合在一起就是一个摇滚乐队名”,我也许有这种潜质...?