[譯] Javascript 中多樣的 this

lsvih發表於2017-10-13

Javascript 中多樣的 this

本文將盡量解釋清楚 JavaScript 中最基礎的部分之一:執行上下文(execution context)。如果你經常使用 JS 框架,那理解 this 更是錦上添花。但如果你想更加認真地對待程式設計的話,理解上下文無疑是非常重要的。

我們可以像平常說話一樣來使用 this。例如:我會說“我媽很不爽,這(this)太糟糕了”,而不會說“我媽很不爽,我媽很不爽這件事太糟糕了”。理解了 this 的上下文,才會理解我們為什麼覺得很糟糕。

現在試著把這個例子與程式語言聯絡起來。在 Javascript 中,我們將 this 作為一個快捷方式,一個引用。它指向其所在上下文的某個物件或變數。

現在這麼說可能會讓人不解,不過很快你就能理解它們了。

全域性上下文

如果你和某人聊天,在剛開始對話、沒有做介紹、沒有任何上下文時,他對你說:“這(this)太糟糕了”,你會怎麼想?大多數情況人們會試圖將“這(this)”與周圍的事物、最近發生的事情聯絡起來。

對於瀏覽器來說也是如此。成千上萬的開發者在沒有上下文的情況下使用了 this。我們可憐的瀏覽器只能將 this 指向一個全域性物件(大多數情況下是 window)。

var a = 15;
console.log(this.a);
// => 15
console.log(window.a);
// => 15複製程式碼

[以上程式碼需在瀏覽器中執行]

函式外部的任何地方都為全域性上下文,this 始終指向全域性上下文(window 物件)。

函式上下文

以真實世界來類比,函式上下文可以看成句子的上下文。“我媽很不爽,這(this)很不妙。”我們都知道這句話中的 this 是什麼意思。其它句子中同樣可以使用 this,但是由於其處於所處上下文不同因而意思全然不同。例如,“風暴來襲,這(this)太糟糕了。”

JavaScript 的上下文與物件有關,它取決於函式被執行時所在的物件。因此 this 會指向被執行函式所在的物件。

var a = 20;

function gx () {
    return this;
}

function fx () {
    return this.a;
}

function fy () {
    return window.a;
}

console.log(gx() === window);
// => True
console.log(fx());
// => 20
console.log(fy());
// => 20複製程式碼

this 由函式被呼叫的方式決定。如你所見,上面的所有函式都是在全域性上下文中被呼叫。

var o = {
  prop: 37,
  f: function() {
    return this.prop;
  }
};

console.log(o.f());
// => 37複製程式碼

當一個函式是作為某個物件的方法被呼叫時,它的 this 指向的就是這個方法所在的物件。

function fx () {
    return this;
}

var obj = {
    method: function () {
        return this;
    }
};

var x_obj = {
    y_obj: {
        method: function () {
            return this;
        }
    }
};

console.log(fx() === window);
// => True — 我們仍處於全域性上下文中。
console.log(obj.method() === window);
// => False — 函式作為一個物件的方法被呼叫。
console.log(obj.method() === obj);
// => True — 函式作為一個物件的方法被呼叫。
console.log(x_obj.y_obj.method() === x_obj)
// => False — 函式作為 y_obj 物件的方法被呼叫,因此 `this` 指向的是 y_obj 的上下文。複製程式碼

例 4

function f2 () {
  'use strict'; 
  return this;
}

console.log(f2() === undefined);
// => True複製程式碼

在嚴格模式下,全域性作用域的函式在全域性作用域被呼叫時,thisundefined

例 5

function fx () {
    return this;
}

var obj = {
    method: fx
};

console.log(obj.method() === window);
// => False
console.log(obj.method() === obj);
// => True複製程式碼

與前面的例子一樣,無論函式是如何被定義的,在這兒它都是作為一個物件方法被呼叫。

例 6

var obj = {
    method: function () {
        return this;
    }
};

var sec_obj = {
    method: obj.method
};

console.log(sec_obj.method() === obj);
// => False
console.log(sec_obj.method() === sec_obj);
// => True複製程式碼

this 是動態的,它可以由一個物件指向另一個物件。

例 7

var shop = {
  fruit: "Apple",
  sellMe: function() {
    console.log("this ", this.fruit);
// => this Apple
    console.log("shop ", shop.fruit);
// => shop Apple
  }
}

shop.sellMe()複製程式碼

我們既能通過 shop 物件也能通過 this 來訪問 fruit 屬性。

例 8

var Foo = function () {
    this.bar = "baz"; 
};

var foo = new Foo();

console.log(foo.bar); 
// => baz
console.log(window.bar);
// => undefined複製程式碼

現在情況不同了。new 操作符建立了一個物件的例項。因此函式的上下文設定為這個被建立的物件例項。

Call、apply、bind

依舊以真實世界舉例:“這(this)太糟糕了,因為我媽開始不爽了。”

這三個方法可以讓我們在任何期許的上下文中執行函式。讓我們舉幾個例子看看它們的用法:

例 1

var bar = "xo xo";

var foo = {
    bar: "lorem ipsum"
};

function test () {
    return this.bar;
}

console.log(test());
// => xo xo — 我們在全域性上下文中呼叫了 test 函式。
console.log(test.call(foo)); 
// => lorem ipsum — 通過使用 `call`,我們在 foo 物件的上下文中呼叫了 test 函式。
console.log(test.apply(foo));
// => lorem ipsum — 通過使用 `apply`,我們在 foo 物件的上下文中呼叫了 test 函式。複製程式碼

這兩種方法都能讓你在任何需要的上下文中執行函式。

apply 可以讓你在呼叫函式時將引數以不定長陣列的形式傳入,而 call 則需要你明確引數。

例 2

var a = 5;

function test () {
    return this.a;
}

var bound = test.bind(document);

console.log(bound()); 
// => undefined — 在 document 物件中沒有 a 這個變數。
console.log(bound.call(window)); 
// => undefined — 在 document 物件中沒有 a 這個變數。在這個情況中,call 不能改變上下文。

var sec_bound = test.bind({a: 15})

console.log(sec_bound())
// => 15 — 我們建立了一個新物件 {a:15},並在此上下文中呼叫了 test 函式。複製程式碼

bind 方法返回的函式的下上文會被永久改變。
在使用 bind 之後,其上下文就固定了,無論你再使用 call、apply 或者 bind 都無法再改變其上下文。

箭頭函式(ES6)

箭頭函式是 ES6 中的一個新語法。它是一個非常方便的工具,不過你需要知道,在箭頭函式中的上下文與普通函式中的上下文的定義是不同的。讓我們舉例看看。

例 1

var foo = (() => this);
console.log(foo() === window); 
// => True複製程式碼

當我們使用箭頭函式時,this 會保留其封閉範圍的上下文。

例 2

var obj = {method: () => this};

var sec_obj = {
  method: function() {
    return this;
  }
};

console.log(obj.method() === obj);
// => False
console.log(obj.method() === window);
// => True
console.log(sec_obj.method() === sec_obj);
// => True複製程式碼

請注意箭頭函式與普通函式的不同點。在這個例子中使用箭頭函式時,我們仍然處於 window 上下文中。
我們可以這麼看:

x => this.y equals function (x) { return this.y }.bind(this)

可以將箭頭函式看做其始終 bind 了函式外層上下文的 this,因此不能將它作為建構函式使用。下面的例子也說明了其不同之處。

例 3

var a = "global";

var obj = {
 method: function () {
   return {
     a: "inside method",
     normal: function() {
       return this.a;
     },
     arrowFunction: () => this.a
   };
 },
 a: "inside obj"
};

console.log(obj.method().normal());
// => inside method
console.log(obj.method().arrowFunction());
// => inside obj複製程式碼

當你瞭解了函式中動態(dynamic) this 與詞法(lexical)this ,在定義新函式的時候請三思。如果函式將作為一個方法被呼叫,那麼使用動態 this;如果它作為一個子程式(subroutine)被呼叫,則使用詞法 this

譯註:瞭解動態作用域與詞法作用域可閱讀此文章

相關閱讀


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章