博客 / 詳情

返回

前端工程師復健筆記-JavaScript 核心深度複習-原型與繼承

我們來深入、系統地詳解 JavaScript 的原型與繼承。這是 JavaScript 中最核心、最獨特的特性之一。

第一部分:核心概念 - 為什麼需要原型?

JavaScript 在誕生之初,被設想為一種簡單的腳本語言,並未打算引入類的概念。為了實現對象之間的屬性和方法共享,從而節省內存並建立繼承關係,設計了基於原型的繼承模型。


第二部分:理解 __proto__prototype

這是最容易混淆的兩個概念。請記住它們的核心區別:

  • __proto__ (讀作 "dunder proto"):

    • 它是每個對象實例都有的一個內置屬性
    • 它指向創建該實例的構造函數的 prototype 對象
    • 它是對象查找屬性和方法的實際鏈條,即原型鏈的鏈接點。
    • (注意:__proto__ 是一個歷史遺留的訪問器,現代標準中更推薦使用 Object.getPrototypeOf(obj) 來獲取)
  • prototype

    • 它是只有函數才有的一個屬性(除了 Function.prototype.bind() 創建的函數)。
    • 當這個函數被作為構造函數 (使用 new 關鍵字調用) 時,它創建的所有實例__proto__ 都將指向這個函數的 prototype 對象。
    • 它的主要用途是存儲可以被所有實例共享的屬性和方法

一句話總結

  • prototype 是函數的屬性,是一個藍圖。
  • __proto__ 是對象的屬性,指向這個藍圖。
  • 實例的 __proto__ === 其構造函數的 prototype

第三部分:圖解原型鏈

讓我們通過代碼來理解。

// 1. 創建一個構造函數
function Person(name) {
  this.name = name;
}

// 2. 在構造函數的 prototype 上添加方法
Person.prototype.sayHello = function() {
  console.log(`Hello, I'm ${this.name}`);
};

// 3. 使用 new 創建實例
const alice = new Person('Alice');
const bob = new Person('Bob');

// 4. 訪問屬性和方法
alice.sayHello(); // "Hello, I'm Alice"
bob.sayHello();   // "Hello, I'm Bob"

console.log(alice.sayHello === bob.sayHello); // true! 方法被共享了

屬性查找過程(原型鏈機制)
當訪問 alice.sayHello() 時,JavaScript 引擎會:

  1. 首先檢查 alice 對象本身是否有 sayHello 屬性。沒有。
  2. 然後通過 alice.__proto__ 找到 Person.prototype,檢查它是否有 sayHello。找到了!於是執行它。
  3. 如果 Person.prototype 上也沒有,引擎會繼續通過 Person.prototype.__proto__ 找到 Object.prototype 進行查找。
  4. 如果直到 Object.prototype (其 __proto__null) 都沒找到,則返回 undefined

第四部分:實現繼承的幾種方式

1. 原型鏈繼承 (直接繼承)

function Parent() {
  this.names = ['kevin', 'daisy'];
}
Parent.prototype.getName = function () { return this.names; };

function Child() {}
// 關鍵:讓 Child 的原型指向 Parent 的實例
Child.prototype = new Parent();

const child1 = new Child();
child1.names.push('yayu');
console.log(child1.getName()); // ['kevin', 'daisy', 'yayu']

const child2 = new Child();
console.log(child2.getName()); // ['kevin', 'daisy', 'yayu'] 
// 問題!child2 的 names 也被修改了,因為所有實例共享了同一個 Parent 實例的屬性。

缺點

  • 引用類型的屬性被所有實例共享。
  • 創建子類實例時,無法向父類構造函數傳參。

2. 構造函數繼承 (經典繼承)

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() { console.log(this.name); };

function Child(name, age) {
  // 關鍵:在子類構造函數中"執行"父類構造函數,並綁定子類的this
  Parent.call(this, name); // 相當於 this.Parent(name)
  this.age = age;
}

const child1 = new Child('Alice', 10);
child1.colors.push('green');
console.log(child1.colors); // ['red', 'blue', 'green']
console.log(child1.name);   // 'Alice'

const child2 = new Child('Bob', 12);
console.log(child2.colors); // ['red', 'blue'] // 互不影響

// child1.sayName(); // 報錯!無法繼承父類原型上的方法

優點:解決了引用類型共享問題和傳參問題。
缺點:方法都在構造函數中定義,無法實現函數複用。並且,無法繼承父類原型上的屬性和方法

3. 組合繼承 (最常用)

結合了原型鏈繼承和構造函數繼承的優點。

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() { console.log(this.name); };

function Child(name, age) {
  // 1. 繼承屬性
  Parent.call(this, name); // 第二次調用 Parent
  this.age = age;
}
// 2. 繼承方法
Child.prototype = new Parent(); // 第一次調用 Parent
// 修正 constructor 指向
Child.prototype.constructor = Child;

Child.prototype.sayAge = function() { console.log(this.age); };

const child1 = new Child('Alice', 10);
child1.colors.push('green');
console.log(child1.colors); // ['red', 'blue', 'green']
child1.sayName(); // 'Alice'
child1.sayAge();  // 10

const child2 = new Child('Bob', 12);
console.log(child2.colors); // ['red', 'blue']
child2.sayName(); // 'Bob'
child2.sayAge();  // 12

優點:融合優點,是 JavaScript 中最常用的繼承模式。
缺點:調用了兩次父類構造函數,生成了兩份屬性(一份在實例上,一份在 Child.prototype 上)。

4. 寄生組合式繼承 (最理想)

這是解決組合繼承缺點的最完美方式。

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() { console.log(this.name); };

function Child(name, age) {
  // 只調用一次 Parent 構造函數
  Parent.call(this, name);
  this.age = age;
}

// 關鍵步驟:創建一個空的函數對象,將其原型指向 Parent.prototype
// 避免了調用 new Parent(),從而不會初始化父類的屬性到原型上
function F() {}
F.prototype = Parent.prototype;
// 將 Child 的原型指向這個空函數的實例
Child.prototype = new F();
// 修正 constructor
Child.prototype.constructor = Child;

// 添加子類原型方法
Child.prototype.sayAge = function() { console.log(this.age); };

const child = new Child('Alice', 10);

現代簡化寫法 (使用 Object.create):

// ... Parent 和 Child 構造函數同上 ...
// 替換上面的 F 函數部分
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

// ...

優點

  • 只調用一次父類構造函數。
  • 避免了在子類原型上創建不必要的、多餘的屬性。
  • 原型鏈保持不變。

這是 ES6 的 class extends 繼承在底層實現的原理


第五部分:ES6 的 Class 語法糖

ES6 引入了 class 關鍵字,讓原型繼承的寫法更加清晰、更像傳統面嚮對象語言。

class Parent {
  constructor(name) {
    this.name = name;
    this.colors = ['red', 'blue'];
  }
  // 方法自動被添加到 Parent.prototype 上
  sayName() {
    console.log(this.name);
  }
}

class Child extends Parent { // 使用 extends 實現繼承
  constructor(name, age) {
    super(name); // 相當於 Parent.call(this, name),必須在 this 前調用
    this.age = age;
  }
  sayAge() {
    console.log(this.age);
  }
}

const child = new Child('Alice', 10);
child.sayName(); // 'Alice'
child.sayAge();  // 10

console.log(child instanceof Child);  // true
console.log(child instanceof Parent); // true

重要提示

  • class 本質只是一個語法糖,它的底層實現仍然是基於原型的寄生組合式繼承
  • typeof Parent 的結果是 "function"。類本身就是函數。
  • 類中定義的方法都是不可枚舉的 (Object.keys(Parent.prototype) 拿不到 sayName),這與 ES5 的行為不同。

總結

  1. 原型是 JavaScript 實現繼承和共享特性的根本機制。
  2. 理解 __proto__ (實例的原型鏈鏈接) 和 prototype (構造函數的原型對象) 的區別至關重要。
  3. 原型鏈是屬性/方法查找的路徑,它構成了繼承的基礎。
  4. 繼承方式從有問題的原型鏈繼承,發展到功能完備但效率不高的組合繼承,最終到完美的寄生組合式繼承
  5. ES6 的 Class 是原型繼承的語法糖,它讓代碼更易寫易讀,但其本質並未改變。

掌握原型與繼承,是真正理解 JavaScript 對象模型和麪向對象編程的關鍵。

user avatar zzd41 頭像 guizimo 頭像 tigerandflower 頭像 yaofly 頭像 huishou 頭像 dragonir 頭像 chongdianqishi 頭像 shaochuancs 頭像 zhangxishuo 頭像 buxia97 頭像 jianqiangdepaobuxie 頭像 suporka 頭像
170 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.