2022 年 10 月 1 日

建構函式,運算子「new」

常規的 {...} 語法允許我們建立一個物件。但我們經常需要建立許多類似的物件,例如多個使用者或選單項目等。

這可以使用建構函式和 "new" 運算子來完成。

建構函式

建構函式在技術上是常規函式。不過有兩個慣例

  1. 它們以大寫字母開頭命名。
  2. 它們應僅使用 "new" 運算子執行。

例如

function User(name) {
  this.name = name;
  this.isAdmin = false;
}

let user = new User("Jack");

alert(user.name); // Jack
alert(user.isAdmin); // false

當函數以 new 執行時,會執行下列步驟

  1. 建立一個新的空物件並指定給 this
  2. 函數主體執行。通常會修改 this,新增新的屬性。
  3. 傳回 this 的值。

換句話說,new User(...) 執行類似下列動作

function User(name) {
  // this = {};  (implicitly)

  // add properties to this
  this.name = name;
  this.isAdmin = false;

  // return this;  (implicitly)
}

因此 let user = new User("Jack") 與下列結果相同

let user = {
  name: "Jack",
  isAdmin: false
};

現在如果我們要建立其他使用者,可以呼叫 new User("Ann")new User("Alice") 等。比每次使用文字簡潔許多,也容易閱讀。

這就是建構函式的用意:實作可重複使用的物件建立程式碼。

再次說明:技術上來說,任何函數(除了箭頭函數,因為它們沒有 this)都可以當作建構函式使用。它可以用 new 執行,並執行上述演算法。首字母大寫是一種慣例,用來清楚說明函數會以 new 執行。

new function() { … }

如果我們有許多程式碼行都與建立單一複雜物件有關,可以使用立即呼叫的建構函式將它們包起來,如下所示

// create a function and immediately call it with new
let user = new function() {
  this.name = "John";
  this.isAdmin = false;

  // ...other code for user creation
  // maybe complex logic and statements
  // local variables etc
};

這個建構函式無法再次呼叫,因為它沒有儲存在任何地方,只會建立並呼叫。因此,這個技巧的目的是封裝建立單一物件的程式碼,而不會重複使用。

建構函式模式測試:new.target

進階內容

本節的語法很少使用,除非你想要了解所有內容,否則可以略過。

在函數內,我們可以使用特殊屬性 new.target 來檢查函數是否以 new 呼叫。

對於一般呼叫,它是不定義的;如果以 new 呼叫,它會等於函數

function User() {
  alert(new.target);
}

// without "new":
User(); // undefined

// with "new":
new User(); // function User { ... }

可以在函數內使用它來判斷函數是否以 new(「建構函式模式」)或一般方式呼叫(「一般模式」)。

我們也可以讓 new 和一般呼叫執行相同的動作,如下所示

function User(name) {
  if (!new.target) { // if you run me without new
    return new User(name); // ...I will add new for you
  }

  this.name = name;
}

let john = User("John"); // redirects call to new User
alert(john.name); // John

這種方法有時會用在函式庫中,以讓語法更靈活。因此,人們可以選擇使用或不使用 new 呼叫函數,函數仍然會運作。

不過,這可能不是一個到處都適用的好方法,因為省略 new 會讓正在執行的動作變得不太明顯。使用 new 時,我們都知道正在建立新的物件。

建構函式的傳回值

通常,建構函式沒有 return 陳述式。它們的工作是將所有必要的資訊寫入 this,而它會自動成為結果。

但如果有 return 陳述式,那麼規則很簡單

  • 如果以物件呼叫 return,則會回傳物件,而不是 this
  • 如果以基本型別呼叫 return,則會略過。

換句話說,以物件呼叫 return 會回傳該物件,在其他所有情況下,則會回傳 this

例如,這裡的 return 會透過回傳物件來覆寫 this

function BigUser() {

  this.name = "John";

  return { name: "Godzilla" };  // <-- returns this object
}

alert( new BigUser().name );  // Godzilla, got that object

以下是一個有空 return 的範例(或我們可以在其後放置一個基本型別,沒關係)

function SmallUser() {

  this.name = "John";

  return; // <-- returns this
}

alert( new SmallUser().name );  // John

建構函式通常沒有 return 陳述式。我們在此提到回傳物件的特殊行為,主要是為了完整性。

省略括號

順帶一提,我們可以在 new 之後省略括號

let user = new User; // <-- no parentheses
// same as
let user = new User();

在此省略括號不算是「良好風格」,但規範允許這種語法。

建構函式中的方法

使用建構函式函式來建立物件能提供很大的彈性。建構函式函式可能有參數,用來定義如何建構物件,以及在其中放入什麼。

當然,我們不僅可以將屬性新增到 this,也可以新增方法。

例如,以下的 new User(name) 會建立一個具有給定 namesayHi 方法的物件

function User(name) {
  this.name = name;

  this.sayHi = function() {
    alert( "My name is: " + this.name );
  };
}

let john = new User("John");

john.sayHi(); // My name is: John

/*
john = {
   name: "John",
   sayHi: function() { ... }
}
*/

若要建立複雜物件,有一個更進階的語法,類別,我們稍後會介紹。

摘要

  • 建構函式函式,或簡稱建構函式,是常規函式,但有一個共識,就是將它們命名為開頭大寫字母。
  • 建構函式函式只能使用 new 呼叫。此類呼叫表示在開始時建立空的 this,並在結束時回傳已填入資料的 this

我們可以使用建構函式函式來建立多個類似的物件。

JavaScript 為許多內建語言物件提供建構函式函式:例如用於日期的 Date、用於集合的 Set,以及我們計畫研究的其他函式。

物件,我們回來了!

在本章中,我們只介紹物件和建構函式的基礎知識。它們對於在下一章中進一步瞭解資料型別和函式至關重要。

在我們瞭解這些之後,我們會回到物件,並在 原型、繼承類別 章節中深入探討它們。

作業

重要性:2

是否可以建立函式 AB,讓 new A() == new B()

function A() { ... }
function B() { ... }

let a = new A();
let b = new B();

alert( a == b ); // true

如果可以,請提供它們的程式碼範例。

可以。

如果函式回傳物件,則 new 會回傳物件,而不是 this

因此,例如,它們可以回傳同一個外部定義的物件 obj

let obj = {};

function A() { return obj; }
function B() { return obj; }

alert( new A() == new B() ); // true
重要性:5

建立一個建構函式 Calculator,建立具有 3 個方法的物件

  • read() 提示輸入兩個值,並分別將它們儲存為物件屬性,其名稱為 ab
  • sum() 傳回這些屬性的總和。
  • mul() 傳回這些屬性的乘積。

例如

let calculator = new Calculator();
calculator.read();

alert( "Sum=" + calculator.sum() );
alert( "Mul=" + calculator.mul() );

執行示範

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

function Calculator() {

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

  this.sum = function() {
    return this.a + this.b;
  };

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

let calculator = new Calculator();
calculator.read();

alert( "Sum=" + calculator.sum() );
alert( "Mul=" + calculator.mul() );

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

重要性:5

建立一個建構函式 Accumulator(startingValue)

它建立的物件應

  • 將「目前值」儲存在屬性 value 中。起始值設定為建構函式 startingValue 的引數。
  • read() 方法應使用 prompt 讀取新的數字,並將其加入 value

換句話說,value 屬性是所有使用者輸入值與初始值 startingValue 的總和。

以下是程式碼的示範

let accumulator = new Accumulator(1); // initial value 1

accumulator.read(); // adds the user-entered value
accumulator.read(); // adds the user-entered value

alert(accumulator.value); // shows the sum of these values

執行示範

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

function Accumulator(startingValue) {
  this.value = startingValue;

  this.read = function() {
    this.value += +prompt('How much to add?', 0);
  };

}

let accumulator = new Accumulator(1);
accumulator.read();
accumulator.read();
alert(accumulator.value);

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

教學課程地圖

留言

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