【譯】理解 JavaScript 中函數調用和 this

原文 Understanding JavaScript Function Invocation and "this"
github 的地址 歡迎 star!
javascript

前言

過去幾年,我常常聽到不少人對 JavaScript 函數調用的談論,尤爲是對其中 this 指向是困惑的。html

在我看來,經過深刻了解函數調用的核心概念,這些困惑都是能夠消除的,其餘形式的調用都是其核心的語法糖。事實上,ECMAScript 規範就是這樣認爲的。這篇博客在不少地方其實就是簡單的規範而已,不過基本概念都是相通的。java

The Core Primitive(核心原始地調用)

首先,來看一下核心原始地調用:函數 call 的方法[1]。call 的方法相對直接明瞭。來看一下過程:git

  1. call 括號後面的集合就是參數 list(從第一個參數到最後一個):arguments
  2. 第一個參數就是 thisValue
  3. 函數調用的時候,將函數的 this 指向這個 thisValue,除了 thisValue 之外的 arguments 當作函數的參數

例如:github

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

hello.call("Yehuda", "world") //=> Yehuda says hello world
複製代碼

如你所見,調用 hello 方法是把 this 指向 "Yehuda",同時給它傳遞了一個簡單 "world" 的參數。這就是核心原始的函數調用的形式。你能夠認爲其它全部的函數調用其原理都是經過 call 的形式來實現的(其它形式都是 call 的語法糖,語法糖是指用一個更方便語法和一個更基本的核心原生術語描述它)面試

[1] 在 ECMAScript 5規範中,call 方法用另一種更加底層的原生的方法描述。可是它真是一個很是輕的包裝, 因此我在這裏簡化了一點。想了解更多信息請看文章末尾。設計模式

簡單的函數調用

明顯地,每次都用 call 方法調用函數有點煩人。全部 JavaScript 容許咱們經過這種模式的語法 hello("world") 調用函數。當咱們這樣調用時,它內部語法仍是用 call 的形式數組

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

// this:
hello("world")

// desugars to:  上面的內部實現
hello.call(window, "world");
複製代碼

固然上面的形式在 ECMAScript 5的嚴格模式下是不一樣的[2]bash

// this:
hello("world")

// desugars to:
hello.call(undefined, "world");
複製代碼

通用公式: fn(...args) <==等價於==> fn.call(window [ES5-strict: undefined], ...args)閉包

注意,對於當即執行的匿名函數也是如此:

(function() {})()
// 等價於
(function() {}).call(window [ES5-strict: undefined)
複製代碼

[2]實際上,我撒了點謊。 ECMAScript 5 規範說了應該所有都是 undefined 傳遞的,但在非嚴格模式須要把 this 指向全局對象。這樣作是爲了不在嚴格模式下調用了非嚴格模式的三方庫致使異常的狀況

對象方法進行調用

接下來常見的方法,調用一個對象的方法如 person.hello()。在這種狀況下,調用語法糖以下:

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

// this:
person.hello("world")

// desugars to this: 內部實現,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.call(window, "world")這樣原始的調用形式,咱們也沒法達到改變 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"
複製代碼

爲了理解 this,你只須要知道這兩個點。首先 arguments 是一個類數組的對象,它表明了傳遞給函數的全部參數。第二,apply 的方法準確地說像 call 的底層實現,只是把那個類數組對象用一個接一個參數代替。

咱們 bind 的方法簡單地返回了一個新的函數。當它被調用的時候,新的函數又調用原始函數,並把 this 指向原始值,它也能夠傳遞參數。

由於這個方法是經常使用的,故 ES5 給全部 Function 對象定義了一個新的方法 bind,實現了以下調用:

var boundHello = person.hello.bind(person);
boundHello("world") // "Brendan Eich says hello world"  this都是指向person的
複製代碼

這是很是實用的,當你把一個原始函數做爲一個回調的時候:

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 的全部事件操做中(假設你沒有進行特殊的操做),DOM 調用回調函數,this 老是指向那個 DOM 元素。

這是很是有用的,由於在匿名函數中默認的 this 是沒有什麼特殊做用的。但這使得那些剛剛學習 JavaScript 的人將難以理解 this。

若是你明白將函數調用改寫爲 func.call(thisValue, ...args) 這樣簡單的方法(去除語法糖),將不會在肯定JavaScript中this值的過程當中迷失。

PS(附言): 我撒謊了

在不少地方,我稍微簡化了規範裏面一些確切的點。最明顯的地方就是我將 func.call 的方法看成一個原生函數調用方法。事實上,規範裏面明確了 func.call[obj.]func() 都是經過一個叫 [[Call]] 的原始方法實現的。

固然,看看 fun.call(thisArg, arg1, arg2, ...) 的定義:

  1. 若是不能當作函數調用,則拋出類型錯誤。
  2. 參數列表爲空
  3. 若是傳了不止一個參數,則按從左到右的 arg1, arg2 的順序把這些值傳遞給函數做爲參數列表(除了 this )
  4. 使用調用者提供的this值和參數調用該函數的返回值。this 指向 thisArg,arguments 指向後面的參數組成的 list

如上所述,這個定義本質就是一個簡單的 JavaScript 綁定原始 [[Call]] 的操做。

你能夠回顧一下這個函數調用的定義,首先幾步是創建 thisValueargList,把 this 指向 thisValue, 把 arguments 指向了 argList,最後一步,調用內部函數,返回結果。這和那個原始的最初的調用是基本相同的。

我撒謊稱 call 是最原始的函數調用方式,但它的內部調用實現基本和規範裏面說的原始調用形式是相同的。

另外還有一些額外的 this 指向的例子沒有說明,像 with。

額外的總結

看到了《JavaScript 設計模式與開發實踐的總結

除去不經常使用的 with 和 eval 的狀況,具體到實際應用中,this 的指向大體能夠分爲如下4種:

  1. 做爲對象的方法調用:指向函數直接所在的那個對象
  2. 做爲普通函數調用(this 就是改寫爲 call 的第一個參數)
  3. 構造器調用( new Fn() ):指向新生成的那個對象實例(固然書中說了,構造函數裏面要避免顯式的返回一個對象,否則 this 指向的是返回的那個對象!)
  4. Function.prototype.call 或 Function.prototype.apply 調用

此外,還有箭頭函數中,this 就是箭頭函數相鄰外面的那個this(也是一個參數)

固然你能夠看一下 call 的實現,你就能夠理解一下函數核心原始的底層調用是什麼樣子:

Function.protoType.call2 = function(context){
    var context = context | window;// 可能沒有傳參數
    context.fn = this;
    var args = [];
    for(var i = 1; i < arguments.length; i++) {
        args.push("arguments[" + i +"]"); // 不這麼作的話 字符串的引號會被自動去掉 變成了變量 致使報錯
    }
    args = args.join(",");
    
    var result = eval("context.fn(" + args +")");//至關於執行了context.fn(arguments[1], arguments[2]);
    
    delete context.fn;
    return result;
}
複製代碼

另外強烈推薦你們能夠看一下如何編寫高質量的函數 -- 敲山震虎篇 ---詳細介紹了函數中底層知識: 總結以下:

  1. 建立函數,開闢堆內存,以字符串存入函數體,將函數名(變量)的值變爲函數體堆內存中地址。
  2. 執行函數,將存儲的字符串函數體複製一份到新開闢的棧內存中,使其變爲真正的 JS 代碼

反正我就記住了,this 是一個參數,是一個參數!

若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝!

參考

  1. www.ruanyifeng.com/blog/2018/0…
  2. Understanding JavaScript Function Invocation and "this"
  3. 【週刊-1】三年大廠面試官-面試題精選及答案--call 實現
  4. 如何編寫高質量的函數 -- 敲山震虎篇
相關文章
相關標籤/搜索