JavaScript: 理解函數(shù)調(diào)用及this

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

多年來腥光,我都有看到大家對 js 函數(shù)調(diào)用的一些困惑雅采。尤其是克蚂,許多人都會抱怨函數(shù)調(diào)用中的this語義含糊不清。

依我的觀點(diǎn)旋廷,只要理解了核心的函數(shù)調(diào)用原語(the core function invocation primitive),把其它各種類型的函數(shù)調(diào)用看作建立在該原語之上的語法糖急前,
這些困惑就能迎刃而解。實(shí)際上宅广,這正是ECMAScript規(guī)范思考的方式葫掉。本文是對規(guī)范的一種簡化描述,但基礎(chǔ)思想是一致的跟狱。

The Core Primitive: 核心原語

首先俭厚,我們來看核心的函數(shù)調(diào)用原語:Function 對象的call方法。該方法比較直觀(譯者注,call 即調(diào)用)驶臊。

  1. 從入?yún)⒌牡?位(從0開始)到最后挪挤,構(gòu)造出一個參數(shù)列表(argList)
  2. 第0個入?yún)⑹?em>thisValue
  3. 將函數(shù)的this綁定到thisValue,函數(shù)的參數(shù)綁定到argList,然后調(diào)用該函數(shù)

例如:

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

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

如你所見叼丑,調(diào)用hello方法時,this被綁定到 "Yehuda" ,入?yún)⑹?/em>"world"扛门。這便是 js 函數(shù)調(diào)用的核心原語鸠信。你可以認(rèn)為,其它類型的方法調(diào)用都會轉(zhuǎn)換成這種原語(即desugar: 將方便的語法轉(zhuǎn)換成使用原語描述的形式)论寨。

Simple Function Invocation:簡單的函數(shù)調(diào)用

顯然星立,調(diào)用函數(shù)時總是使用call是很煩人的一件事。js 允許我們直接通過括號語法進(jìn)行函數(shù)調(diào)用(如:hello("world"))葬凳。我們看它是如何轉(zhuǎn)換成原語的:

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

// 我們這樣寫:
hello("world")

// 會被轉(zhuǎn)換成:
hello.call(window, "world");  

在ECMAScript 5中绰垂,這種行為在strict mode作了一些變化:

// 我們這樣寫:
hello("world")

// 會被轉(zhuǎn)換成:
hello.call(undefined, "world");  

簡而言之就是,像fn(...args)這樣的函數(shù)調(diào)用和fn.call(window [ES5-strict: undefined], ...args)是互通的火焰。

注意這同樣適用于內(nèi)聯(lián)函數(shù): (function() {})()(function() {}).call(window [ES5-strict: undefined)是一樣的劲装。

Member Functions:成員函數(shù)(方法)

另一種常見的函數(shù)調(diào)用,是調(diào)用的函數(shù)是作為對象的成員而存在(person.hello())荐健。這種情況下酱畅,轉(zhuǎn)換為原語描述:

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

  // 我們這樣寫:
  person.hello("world")

  // 會轉(zhuǎn)換成:
  person.hello.call(person, "world");  

注意,我們不用考慮此類調(diào)用中hello方法是如何綁定到object對象上的(編譯器會幫我們處理)江场。上一例的hello是被定義為一個獨(dú)立的函數(shù)纺酸。接下來我們看看如果將hello動態(tài)的綁定到 對象上會發(fā)生什么:

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

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

person.hello("world") // 仍然會被轉(zhuǎn)換為 person.hello.call(person, "world")

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

注意到?jīng)],函數(shù)的this并非是個固定不變的值址否,而是在運(yùn)行時由調(diào)用者所決定餐蔬。

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

有時候需要保持函數(shù)的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的調(diào)用仍然會轉(zhuǎn)換為boundHello.call(window, "world"),我們其實(shí)是繞了個彎樊诺,用原始的call方法將this設(shè)定為我們需要的值。

我們可以將這種技巧抽象的更具普適性:

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"

要理解這段代碼音同,需要另外知道兩點(diǎn)信息词爬。其一,arguments是一個類數(shù)組對象权均,用來裝載傳遞給一個函數(shù)的所有參數(shù)顿膨。其二,apply方法和call方法工作方式幾乎相同叽赊,不同點(diǎn)是前者的參數(shù)列表是一個類數(shù)組對象恋沃,后者的參數(shù)列表是一個參數(shù)一個位置。

我們的bind方法只是簡單的返回一個新的函數(shù)必指。調(diào)用這個返回的函數(shù)時囊咏,該函數(shù)只是簡單的調(diào)用原來傳入的函數(shù),并將第二個傳的參數(shù)thisValue設(shè)為該函數(shù)的this。當(dāng)然梅割,它還會將bind剩余的參數(shù)傳入給調(diào)用bind所返回的那個函數(shù)霜第。

由于該技巧在 js 中已經(jīng)成為習(xí)語,ES5引入了一個新方法bind,讓所有函數(shù)對象都擁有此方法:

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

當(dāng)需要將一個函數(shù)作為回調(diào)傳遞時户辞,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(負(fù)責(zé)ECMAScript下一版的委員會)咆课,正在開發(fā)一個更優(yōu)雅的、向后兼容的解決方案(譯者注(2016-11-08): 目前有兩種方式解決這種問題扯俱,一是es6的arrow functions以及es7的function bind operator)书蚪。

On jQuery:說說 jQuery

jQuery大量使用了匿名回調(diào)函數(shù),它在內(nèi)部會使用call來將回調(diào)的this綁定到一個更有用的值上迅栅。例如殊校,事件處理器的this不是指向window,jQuery會通過call將其設(shè)定到事件處理器所綁定的元素。

這是極其有用的读存,因為匿名回調(diào)函數(shù)的默認(rèn)this往往沒什么用處为流,還會給js 初學(xué)者這樣一種印象,即让簿,this通常是一個怪異的敬察、可變的、難以推理的概念尔当。

如果你掌握了如何將一個普通函數(shù)轉(zhuǎn)換成原語描述的形式(func.call(thisValue, ...args)),你應(yīng)該能順利走出 js this 的迷宮了莲祸。

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

文中的幾處,我對規(guī)范中的瑣言碎語進(jìn)行了簡化椭迎∪裰模可能最大的欺騙之處即是我將func.call稱作"原語(primitive)"。實(shí)際上畜号,規(guī)范中是到的原語叫[[Call]], 不管是func.call還是 [obj.]func() ,都會使用該原語缴阎。

但是,我們看看規(guī)范中對func.call的定義(譯者注:規(guī)范的語言用英文讀更容易懂一些简软,這里不再進(jìn)行中文翻譯):

  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.

如上所述蛮拔,這個定義本質(zhì)上就是js語言對原語[[Call]]的綁定說明。

如果你看了函數(shù)調(diào)用的說明替饿,前7步是設(shè)定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 調(diào)用內(nèi)部的[[Call]]方法语泽,將this指向thisValue,參數(shù)指向argList)"

規(guī)范中的語言十分繁瑣,主是處理好argListthisValue视卢。

關(guān)于call作為原語踱卵,我撒了點(diǎn)謊,但是它和規(guī)范的本質(zhì)是相同的。

注意,對于一些額外的情況(比如with),本文沒有涉及惋砂。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末妒挎,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子西饵,更是在濱河造成了極大的恐慌酝掩,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件眷柔,死亡現(xiàn)場離奇詭異期虾,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)驯嘱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門镶苞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人鞠评,你說我怎么就攤上這事茂蚓。” “怎么了剃幌?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵聋涨,是天一觀的道長。 經(jīng)常有香客問我负乡,道長牍白,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任抖棘,我火速辦了婚禮淹朋,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘钉答。我一直安慰自己础芍,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布数尿。 她就那樣靜靜地躺著仑性,像睡著了一般。 火紅的嫁衣襯著肌膚如雪右蹦。 梳的紋絲不亂的頭發(fā)上诊杆,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機(jī)與錄音何陆,去河邊找鬼晨汹。 笑死,一個胖子當(dāng)著我的面吹牛贷盲,可吹牛的內(nèi)容都是我干的淘这。 我是一名探鬼主播剥扣,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼铝穷!你這毒婦竟也來了钠怯?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤曙聂,失蹤者是張志新(化名)和其女友劉穎晦炊,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宁脊,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡断国,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了榆苞。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片并思。...
    茶點(diǎn)故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖语稠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情弄砍,我是刑警寧澤仙畦,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站音婶,受9級特大地震影響慨畸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜衣式,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一寸士、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧碴卧,春花似錦弱卡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至荧飞,卻和暖如春凡人,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背叹阔。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工挠轴, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人耳幢。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓岸晦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子委煤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容