proto vs prototype:90% 的人分不清的 JavaScript 核心

`__proto__` vs `prototype`:90% 的人分不清的 JavaScript 核心機制

「每個物件都有一條祕密通道,通往另一個物件。這條通道叫 __proto__,而這條通道串起來的高速公路,就叫做原型鏈。」


一、先搞清楚兩個容易混淆的東西

很多初學者看到 __proto__prototype 就頭大,覺得它們是同一個東西。不是的。 它們的關係就像「鑰匙」和「鎖」——有關聯,但用途完全不同。

__proto__prototype 的差別:

__proto__ prototype
誰擁有它 所有物件都有 只有函式才有
它指向什麼 指向「建立我的那個建構函式的 prototype 指向這個函式作為建構函式時,實例共用的那個物件
一句話概括 「我是誰生出來的?」 「我生出來的東西都能用什麼?」

二、從一個故事開始

假設你開了一家手搖飲店:

// 你設計了一個「奶茶配方」(建構函式)
function MilkTea(flavor) {
  this.flavor = flavor;
}

// 所有奶茶共用的能力,寫在 prototype 上
MilkTea.prototype.drink = function() {
  console.log(`喝了一口${this.flavor}奶茶,真好喝!`);
};

MilkTea.prototype.addTopping = function(topping) {
  console.log(`給${this.flavor}奶茶加了${topping},口感升級!`);
};

// 做出兩杯奶茶(實例)
const pearlTea = new MilkTea('珍珠');
const taroTea = new MilkTea('芋泥');

現在來驗證一下關係:

console.log(pearlTea.__proto__ === MilkTea.prototype);  // true ✅
console.log(taroTea.__proto__ === MilkTea.prototype);   // true ✅
console.log(MilkTea.prototype.constructor === MilkTea);  // true ✅

看懂了嗎?

  • pearlTea.__proto__ → 指向 MilkTea.prototype(「我是 MilkTea 做出來的」)
  • MilkTea.prototype → 是一個物件,上面掛著 drinkaddTopping(「我做出來的實例都能用這些方法」)

三、圖解:物件之間的祕密通道

  pearlTea                          taroTea
  { flavor: '珍珠' }                { flavor: '芋泥' }
       │                                │
       │ __proto__                      │ __proto__
       ▼                                ▼
  ┌─────────────────────────────────────────┐
  │         MilkTea.prototype               │
  │  {                                      │
  │    constructor: MilkTea,                │
  │    drink: function() { ... },           │
  │    addTopping: function() { ... }       │
  │  }                                      │
  └─────────────────────┬───────────────────┘
                        │
                        │ __proto__
                        ▼
  ┌─────────────────────────────────────────┐
  │         Object.prototype                │
  │  {                                      │
  │    toString: function() { ... },        │
  │    hasOwnProperty: function() { ... },  │
  │    valueOf: function() { ... }          │
  │  }                                      │
  └─────────────────────┬───────────────────┘
                        │
                        │ __proto__
                        ▼
                      null          ← 終點站,原型鏈的盡頭

這就是原型鏈! 當你存取 pearlTea.drink() 時,JavaScript 引擎的查找流程是:

  1. pearlTea 自己身上有 drink 嗎?→ 沒有
  2. pearlTea.__proto__(也就是 MilkTea.prototype)上找 → 找到了!執行它
  3. 如果還沒找到,繼續沿著 __proto__ 往上找 Object.prototype
  4. 如果還沒找到,繼續找 null沒有就回傳 undefined

就像你找東西:先翻自己的口袋,再翻家裡的櫃子,再翻儲藏室,都沒有那就是沒有。


四、__proto__ 的「正經寫法」

__proto__ 其實是一個 getter/setter,它在瀏覽器中被實作為 Object.prototype 上的屬性。正式開發中,建議用這兩個方法代替:

// 設定原型
Object.setPrototypeOf(pearlTea, someOtherProto);

// 取得原型
Object.getPrototypeOf(pearlTea);  // 等價於 pearlTea.__proto__

建立物件時直接指定原型,更建議用 Object.create

const baseTea = {
  stir() {
    console.log('攪拌均勻');
  }
};

const greenTea = Object.create(baseTea);
greenTea.flavor = '抹茶';

greenTea.stir();  // "攪拌均勻" —— 沿原型鏈找到的
console.log(Object.getPrototypeOf(greenTea) === baseTea);  // true

五、函式也是物件!所以函式也有 __proto__

這是最容易讓人腦袋打結的部分。記住一個鐵律:

在 JavaScript 中,函式也是物件。

function Foo() {}

// 函式 Foo 也是一個物件,所以它有 __proto__
console.log(Foo.__proto__ === Function.prototype);  // true

// Function.prototype 也是一個物件
console.log(Function.prototype.__proto__ === Object.prototype);  // true

// Object.prototype 是終點
console.log(Object.prototype.__proto__ === null);  // true

來,畫一張函式的原型鏈全景圖:

        Foo(函式)
          │
          │ __proto__
          ▼
  Function.prototype       ← 所有函式共用的方法(call, apply, bind...)
          │
          │ __proto__
          ▼
  Object.prototype         ← 所有物件共用的方法(toString, hasOwnProperty...)
          │
          │ __proto__
          ▼
        null

Foo.prototype 是另一條線:

        Foo(函式)
          │
          │ prototype(只有函式有這個屬性)
          ▼
  Foo.prototype            ← Foo 的實例的 __proto__ 指向這裡
          │
          │ __proto__
          ▼
  Object.prototype
          │
          │ __proto__
          ▼
        null

兩條不同的鏈! Foo.__proto__Foo.prototype 走的是完全不同的路。


六、new 關鍵字到底做了什麼?

理解 new 的流程,原型鏈就徹底通了:

function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {
  console.log(`Hi, I'm ${this.name}`);
};

const alice = new Person('Alice');

new Person('Alice') 背後發生了 4 件事

// 等價於以下流程:
function fakeNew(Constructor, ...args) {
  // 1. 建立一個空物件
  const obj = {};

  // 2. 把這個物件的 __proto__ 指向建構函式的 prototype
  Object.setPrototypeOf(obj, Constructor.prototype);
  // 等價於 obj.__proto__ = Constructor.prototype;

  // 3. 用這個物件作為 this,執行建構函式
  const result = Constructor.apply(obj, args);

  // 4. 如果建構函式回傳了一個物件,就用那個物件;否則回傳 obj
  return result instanceof Object ? result : obj;
}

驗證一下:

const bob = fakeNew(Person, 'Bob');
bob.sayHi();  // "Hi, I'm Bob" ✅
console.log(Object.getPrototypeOf(bob) === Person.prototype);  // true ✅

七、ES6 的 class —— 只是語法糖而已

ES6 的 class 寫法看起來像 Java/C++,但本質上還是原型鏈:

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} 發出聲音`);
  }
}

class Dog extends Animal {
  speak() {
    console.log(`${this.name} 在汪汪叫`);
  }
}

const rex = new Dog('Rex');
rex.speak();  // "Rex 在汪汪叫"

上面的程式碼,底層等價於:

function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function() {
  console.log(this.name + ' 發出聲音');
};

function Dog(name) {
  Animal.call(this, name);  // 呼叫父類別建構函式
}

// 建立原型鏈:Dog.prototype → Animal.prototype → Object.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.speak = function() {
  console.log(this.name + ' 在汪汪叫');
};

const rex = new Dog('Rex');
rex.speak();  // "Rex 在汪汪叫"

原型鏈圖:

  rex(Dog 的實例)
    │
    │ __proto__
    ▼
  Dog.prototype
    │
    │ __proto__  (透過 Object.create(Animal.prototype) 建立)
    ▼
  Animal.prototype
    │
    │ __proto__
    ▼
  Object.prototype
    │
    │ __proto__
    ▼
  null

當呼叫 rex.speak() 時:

  1. rex 自己沒有 speak → 去 Dog.prototype
  2. Dog.prototypespeak找到了,執行!

如果呼叫 rex.hasOwnProperty('name')

  1. rex 沒有 → Dog.prototype 沒有 → Animal.prototype 沒有
  2. Object.prototype 有 → 找到了!

八、幾個經典面試題,來檢驗一下

題目 1:instanceof 的本質

console.log(rex instanceof Dog);    // true
console.log(rex instanceof Animal); // true

instanceof 的原理:沿著 rex.__proto__ 這條鏈,看 Dog.prototypeAnimal.prototype 是否在鏈上。

// 模擬 instanceof
function myInstanceof(obj, Constructor) {
  let proto = Object.getPrototypeOf(obj);
  while (proto !== null) {
    if (proto === Constructor.prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

題目 2:屬性遮蔽(Shadowing)

const obj = { a: 1 };
const child = Object.create(obj);

console.log(child.a);      // 1(從原型鏈上找到的)
console.log('a' in child); // false(child 自己沒有 a)

child.a = 2;               // 在 child 自己身上建立了 a
console.log(child.a);      // 2(自己的屬性優先)
console.log(obj.a);        // 1(原型上的沒被修改)

delete child.a;            // 刪掉自己的 a
console.log(child.a);      // 1(又從原型鏈上找到了)

原型鏈上的屬性不會被實例修改——你寫 child.a = 2 不會改原型,而是在 child 自己身上新建一個同名屬性,把原型上的屬性「遮住」了。

題目 3:Function.__proto__ === Function.prototype

console.log(Function.__proto__ === Function.prototype);  // true 🤯

函式 Function 自己就是由自己建立的。 這是 JavaScript 的一個自舉(bootstrap)設計——Function 是整個型別系統的起點,它打破了「函式由別人建立」的規則,自己指向自己。


九、總結:一張圖記住一切

                    null
                     ▲
                     │ __proto__
             Object.prototype
              ▲             ▲
     __proto__│             │ __proto__
             │              │
    Function.prototype    Foo.prototype
              ▲             ▲
     __proto__│             │ __proto__
             │              │
           Foo ─────────────┘
           (Foo.prototype 指向右邊)

   實例:
   foo.__proto__ → Foo.prototype → Object.prototype → null

三條核心規則:

  1. 所有物件都有 __proto__,它指向建立該物件的建構函式的 prototype
  2. 只有函式有 prototype,它是一個物件,用於存放實例共用的屬性和方法
  3. 查找屬性時沿著 __proto__ 鏈向上搜尋,直到找到或到達 null 為止

理解了這三點,原型鏈就徹底通了。


十、最後的靈魂拷問

console.log(Object.__proto__ === Function.prototype);           // true
console.log(Function.__proto__ === Function.prototype);         // true
console.log(Function.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null);               // true

試著畫出 ObjectFunction 之間互相糾纏的原型鏈圖吧——如果你能畫對,說明你真的懂了。


記住:JavaScript 沒有類,只有物件。原型鏈就是物件之間的「關係網」,__proto__ 是網線,prototype 是網線另一頭的插座。


原文出處:https://juejin.cn/post/7648213611392172051


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝6   💬2  
342
🥈
我愛JS
💬1  
3
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登