2022 年 5 月 22 日

物件方法,"this"

物件通常用來表示真實世界的實體,例如使用者、訂單等

let user = {
  name: "John",
  age: 30
};

而在真實世界中,使用者可以執行動作:從購物車中選擇商品、登入、登出等。

在 JavaScript 中,動作以屬性中的函式表示。

方法範例

首先,我們來教導 使用者 打招呼

let user = {
  name: "John",
  age: 30
};

user.sayHi = function() {
  alert("Hello!");
};

user.sayHi(); // Hello!

在這裡,我們只使用函式表達式來建立一個函式,並將其指定給物件的屬性 user.sayHi

然後我們可以將其呼叫為 user.sayHi()。使用者現在可以說話了!

屬於物件的函式稱為其方法

因此,這裡我們有物件 user 的方法 sayHi

當然,我們可以使用預先宣告的函式作為方法,如下所示

let user = {
  // ...
};

// first, declare
function sayHi() {
  alert("Hello!");
}

// then add as a method
user.sayHi = sayHi;

user.sayHi(); // Hello!
物件導向程式設計

當我們使用物件來表示實體來撰寫程式碼時,這稱為物件導向程式設計,簡稱「OOP」。

OOP 是一項龐大的事物,本身就是一門有趣的科學。如何選擇正確的實體?如何組織它們之間的互動?那是架構,而且有許多關於該主題的精彩書籍,例如 E. Gamma、R. Helm、R. Johnson、J. Vissides 所著的「設計模式:可重複使用物件導向軟體的要素」,或 G. Booch 所著的「物件導向分析與設計與應用」等。

方法簡寫

在物件文字中,方法有更簡短的語法

// these objects do the same

user = {
  sayHi: function() {
    alert("Hello");
  }
};

// method shorthand looks better, right?
user = {
  sayHi() { // same as "sayHi: function(){...}"
    alert("Hello");
  }
};

如示範所示,我們可以省略 "function",只寫 sayHi()

說實話,這些符號並非完全相同。與物件繼承相關的細微差異(稍後會介紹),但目前並不重要。在幾乎所有情況下,較簡短的語法較為優先。

方法中的「this」

物件方法通常需要存取儲存在物件中的資訊才能執行其工作。

例如,user.sayHi() 內的程式碼可能需要 user 的名稱。

為了存取物件,方法可以使用 this 關鍵字。

this 的值是「點之前」的物件,用於呼叫方法的物件。

例如

let user = {
  name: "John",
  age: 30,

  sayHi() {
    // "this" is the "current object"
    alert(this.name);
  }

};

user.sayHi(); // John

在執行 user.sayHi() 期間,this 的值將為 user

技術上,也可以不透過 this 存取物件,而是透過外部變數來參照物件

let user = {
  name: "John",
  age: 30,

  sayHi() {
    alert(user.name); // "user" instead of "this"
  }

};

…但這種程式碼不可靠。如果我們決定將 user 複製到另一個變數,例如 admin = user,並用其他內容覆寫 user,那麼它將存取錯誤的物件。

以下示範

let user = {
  name: "John",
  age: 30,

  sayHi() {
    alert( user.name ); // leads to an error
  }

};


let admin = user;
user = null; // overwrite to make things obvious

admin.sayHi(); // TypeError: Cannot read property 'name' of null

如果我們在 alert 內部使用 this.name 而不是 user.name,那麼程式碼將會執行。

「this」未繫結

在 JavaScript 中,關鍵字 this 的行為與大多數其他程式語言不同。它可以在任何函式中使用,即使它不是物件的方法。

以下範例沒有語法錯誤

function sayHi() {
  alert( this.name );
}

this 的值是在執行期間評估的,具體取決於上下文。

例如,這裡將同一個函式指定給兩個不同的物件,且在呼叫時有不同的「this」

let user = { name: "John" };
let admin = { name: "Admin" };

function sayHi() {
  alert( this.name );
}

// use the same function in two objects
user.f = sayHi;
admin.f = sayHi;

// these calls have different this
// "this" inside the function is the object "before the dot"
user.f(); // John  (this == user)
admin.f(); // Admin  (this == admin)

admin['f'](); // Admin (dot or square brackets access the method – doesn't matter)

規則很簡單:如果呼叫 obj.f(),則在呼叫 f 時,thisobj。因此在上方的範例中,thisuseradmin

不使用物件呼叫:this == undefined

我們甚至可以在完全沒有物件的情況下呼叫函式

function sayHi() {
  alert(this);
}

sayHi(); // undefined

在這種情況下,this 在嚴格模式中為 undefined。如果我們嘗試存取 this.name,將會產生錯誤。

在非嚴格模式中,此情況下 this 的值將為全域物件(瀏覽器中的 window,我們將在本章節稍後 全域物件 中說明)。這是一種歷史行為,"use strict" 已修正此行為。

通常此類呼叫為程式設計錯誤。如果函式內有 this,則表示預期在物件內容中呼叫它。

未繫結 this 的後果

如果您來自另一種程式設計語言,則您可能習慣「繫結 this」的概念,其中在物件中定義的方法總是讓 this 參照該物件。

在 JavaScript 中,this 為「自由」的,其值會在呼叫時評估,且不取決於方法宣告的位置,而是取決於「點之前」的物件為何。

在執行階段評估 this 的概念有優點也有缺點。一方面,函式可以重複使用於不同的物件。另一方面,更大的彈性會產生更多錯誤的可能性。

我們的立場並非判斷此語言設計決策的好壞。我們將了解如何使用它,如何獲得好處並避免問題。

箭頭函式沒有「this」

箭頭函式很特別:它們沒有「自己的」this。如果我們從此類函式參照 this,則會從外部「一般」函式取得。

例如,這裡的 arrow() 使用來自外部 user.sayHi() 方法的 this

let user = {
  firstName: "Ilya",
  sayHi() {
    let arrow = () => alert(this.firstName);
    arrow();
  }
};

user.sayHi(); // Ilya

這是箭頭函式的特殊功能,當我們實際上不想要有獨立的 this,而是想要從外部內容取得 this 時,這項功能會很有用。稍後在本章節 重新檢視箭頭函式 中,我們將更深入探討箭頭函式。

摘要

  • 儲存在物件屬性中的函式稱為「方法」。
  • 方法允許物件「作用」類似於 object.doSomething()
  • 方法可以將物件參照為 this

this 的值在執行階段定義。

  • 宣告函式時,它可以使用 this,但 this 在函式呼叫之前沒有值。
  • 函式可以在物件之間複製。
  • 在「方法」語法中呼叫函式時:object.method(),呼叫期間 this 的值為 object

請注意,箭頭函式很特別:它們沒有 this。在箭頭函式內存取 this 時,會從外部取得。

作業

重要性:5

這裡的函式 makeUser 傳回一個物件。

存取其 ref 的結果是什麼?為什麼?

function makeUser() {
  return {
    name: "John",
    ref: this
  };
}

let user = makeUser();

alert( user.ref.name ); // What's the result?

答案:一個錯誤。

試試看

function makeUser() {
  return {
    name: "John",
    ref: this
  };
}

let user = makeUser();

alert( user.ref.name ); // Error: Cannot read property 'name' of undefined

這是因為設定 this 的規則不會查看物件定義。只有呼叫的時刻才有關係。

makeUser() 內部,this 的值為 undefined,因為它被呼叫為一個函式,而不是使用「點」語法作為一個方法。

this 的值對整個函式來說都是一個,程式碼區塊和物件文字不會影響它。

因此,ref: this 實際上會取得函式的目前 this,其值為 undefined

我們可以改寫函式,並傳回相同的 this,其值為 undefined

function makeUser(){
  return this; // this time there's no object literal
}

alert( makeUser().name ); // Error: Cannot read property 'name' of undefined

正如你所見,alert( makeUser().name ) 的結果與前一個範例中 alert( user.ref.name ) 的結果相同。

以下是相反的情況

function makeUser() {
  return {
    name: "John",
    ref() {
      return this;
    }
  };
}

let user = makeUser();

alert( user.ref().name ); // John

現在它可以運作,因為 user.ref() 是方法。而且 this 的值會設定為點 . 之前的物件。

重要性:5

建立一個物件 calculator,包含三個方法

  • read() 提示輸入兩個值,並將它們分別儲存為物件屬性,名稱為 ab
  • sum() 傳回已儲存值的總和。
  • mul() 將已儲存的值相乘,並傳回結果。
let calculator = {
  // ... your code ...
};

calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );

執行示範

開啟一個包含測試的沙盒。

let calculator = {
  sum() {
    return this.a + this.b;
  },

  mul() {
    return this.a * this.b;
  },

  read() {
    this.a = +prompt('a?', 0);
    this.b = +prompt('b?', 0);
  }
};

calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );

在沙盒中開啟包含測試的解答。

重要性:2

有一個 ladder 物件,允許上上下下

let ladder = {
  step: 0,
  up() {
    this.step++;
  },
  down() {
    this.step--;
  },
  showStep: function() { // shows the current step
    alert( this.step );
  }
};

現在,如果我們需要按順序進行多個呼叫,可以這樣做

ladder.up();
ladder.up();
ladder.down();
ladder.showStep(); // 1
ladder.down();
ladder.showStep(); // 0

修改 updownshowStep 的程式碼,讓呼叫可以串連,如下所示

ladder.up().up().down().showStep().down().showStep(); // shows 1 then 0

這種方法在 JavaScript 函式庫中廣泛使用。

開啟一個包含測試的沙盒。

解決方案是從每個呼叫中傳回物件本身。

let ladder = {
  step: 0,
  up() {
    this.step++;
    return this;
  },
  down() {
    this.step--;
    return this;
  },
  showStep() {
    alert( this.step );
    return this;
  }
};

ladder.up().up().down().showStep().down().showStep(); // shows 1 then 0

我們也可以每行寫一個呼叫。對於長鏈來說,這樣更易於閱讀

ladder
  .up()
  .up()
  .down()
  .showStep() // 1
  .down()
  .showStep(); // 0

在沙盒中開啟包含測試的解答。

教學課程地圖

留言

留言前請先閱讀此內容…
  • 如果您有建議要如何改進,請 提交 GitHub 問題 或提出拉取請求,而不是留言。
  • 如果您無法理解文章中的某些內容,請詳細說明。
  • 若要插入幾行程式碼,請使用 <code> 標籤,若要插入多行,請將它們包覆在 <pre> 標籤中,若要插入超過 10 行,請使用沙盒 (plnkrjsbincodepen…)