Stories

Detail Return Return

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

我們來深入、系統地詳解 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 tianmiaogongzuoshi_5ca47d59bef41 Avatar cyzf Avatar zaotalk Avatar linlinma Avatar front_yue Avatar dirackeeko Avatar zourongle Avatar razyliang Avatar inslog Avatar anchen_5c17815319fb5 Avatar Dream-new Avatar u_17443142 Avatar
Favorites 85 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.