詳解 JavaScript 建構函式和 "new" 操作符

技術漫談發表於2020-11-12

構造器和操作符 "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 = {};(隱式建立)

  // 新增屬性到 this
  this.name = name;
  this.isAdmin = false;

  // return this;(隱式返回)
}

所以 new User("Jack") 的結果是相同的物件:

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

現在,如果我們想建立其他使用者,我們可以呼叫 new User("Ann")new User("Alice") 等。比每次都使用字面量建立要短得多,而且更易於閱讀。

這是構造器的主要目的 —— 實現可重用的物件建立程式碼。

讓我們再強調一遍 —— 從技術上講,任何函式都可以用作構造器。即:任何函式都可以通過 new 來執行,它會執行上面的演算法。“首字母大寫”是一個共同的約定,以明確表示一個函式將被使用 new 來執行。

new function() { ... }

如果我們有許多行用於建立單個複雜物件的程式碼,我們可以將它們封裝在建構函式中,像這樣:

let user = new function() {
  this.name = "John";
  this.isAdmin = false;

  // ……用於使用者建立的其他程式碼
  // 也許是複雜的邏輯和語句
  // 區域性變數等
};

構造器不能被再次呼叫,因為它不儲存在任何地方,只是被建立和呼叫。因此,這個技巧旨在封裝構建單個物件的程式碼,而無需將來重用。

構造器模式測試:new.target

進階內容:

本節涉及的語法內容很少使用,除非你想了解所有內容,否則你可以直接跳過該語法。

在一個函式內部,我們可以使用 new.target 屬性來檢查它是否被使用 new 進行呼叫了。

對於常規呼叫,它為空,對於使用 new 的呼叫,則等於該函式:

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

// 不帶 "new":
User(); // undefined

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

它可以被用在函式內部,來判斷該函式是被通過 new 呼叫的“構造器模式”,還是沒被通過 new 呼叫的“常規模式”。

我們也可以讓 new 呼叫和常規呼叫做相同的工作,像這樣:

function User(name) {
  if (!new.target) { // 如果你沒有通過 new 執行我
    return new User(name); // ……我會給你新增 new
  }

  this.name = name;
}

let john = User("John"); // 將呼叫重定向到新使用者
alert(john.name); // John

這種方法有時被用在庫中以使語法更加靈活。這樣人們在呼叫函式時,無論是否使用了 new,程式都能工作。

不過,到處都使用它並不是一件好事,因為省略了 new 使得很難觀察到程式碼中正在發生什麼。而通過 new 我們都可以知道這建立了一個新物件。

構造器的 return

通常,構造器沒有 return 語句。它們的任務是將所有必要的東西寫入 this,並自動轉換為結果。

但是,如果這有一個 return 語句,那麼規則就簡單了:

  • 如果 return 返回的是一個物件,則返回這個物件,而不是 this
  • 如果 return 返回的是一個原始型別,則忽略。

換句話說,帶有物件的 return 返回該物件,在所有其他情況下返回 this

例如,這裡 return 通過返回一個物件覆蓋 this

function BigUser() {

  this.name = "John";

  return { name: "Godzilla" };  // <-- 返回這個物件
}

alert( new BigUser().name );  // Godzilla,得到了那個物件

這裡有一個 return 為空的例子(或者我們可以在它之後放置一個原始型別,沒有什麼影響):

function SmallUser() {

  this.name = "John";

  return; // <-- 返回 this
}

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

通常構造器沒有 return 語句。這裡我們主要為了完整性而提及返回物件的特殊行為。

省略括號

順便說一下,如果沒有引數,我們可以省略 new 後的括號:

let user = new User; // <-- 沒有引數
// 等同於
let user = new User();

這裡省略括號不被認為是一種“好風格”,但是規範允許使用該語法。

構造器中的方法

使用建構函式來建立物件會帶來很大的靈活性。建構函式可能有一些引數,這些引數定義瞭如何構造物件以及要放入什麼。

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

例如,下面的 new User(name) 用給定的 name 和方法 sayHi 建立了一個物件:

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 以及其他我們計劃學習的內容。

物件,我們還會回來噠!

在本章中,我們只介紹了關於物件和構造器的基礎知識。它們對於我們在下一章中,學習更多關於資料型別和函式的相關知識非常重要。

在我們學習了那些之後,我們將回到物件,在 info:prototypesinfo:classes 章節中深入介紹它們。

作業題

先自己做題目再看答案。

1. 兩個函式 — 一個物件

重要程度:⭐️⭐️

是否可以建立像 new A()==new B() 這樣的函式 AB

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

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

alert( a == b ); // true

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

2. 建立 new Calculator

重要程度:⭐️⭐️⭐️⭐️⭐️

建立一個建構函式 Calculator,它建立的物件中有三個方法:

  • read() 使用 prompt 請求兩個值並把它們記錄在物件的屬性中。
  • sum() 返回這些屬性的總和。
  • mul() 返回這些屬性的乘積。

例如:

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

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

3. 建立 new Accumulator

重要程度:⭐️⭐️⭐️⭐️⭐️

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

它建立的物件應該:

  • 將“當前 value”儲存在屬性 value 中。起始值被設定到構造器 startingValue 的引數。
  • read() 方法應該使用 prompt 來讀取一個新的數字,並將其新增到 value 中。

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

下面是示例程式碼:

let accumulator = new Accumulator(1); // 初始值 1

accumulator.read(); // 新增使用者輸入的 value
accumulator.read(); // 新增使用者輸入的 value

alert(accumulator.value); // 顯示這些值的總和

答案:

在微信公眾號「技術漫談」後臺回覆 1-4-5 獲取作業答案。


現代 JavaScript 教程:開源的現代 JavaScript 從入門到進階的優質教程。React 官方文件推薦,與 MDN 並列的 JavaScript 學習教程

線上免費閱讀:https://zh.javascript.info


掃描下方二維碼,關注微信公眾號「技術漫談」,訂閱更多精彩內容。

相關文章