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),本文没有涉及。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容