`__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 → 是一個物件,上面掛著 drink 和 addTopping(「我做出來的實例都能用這些方法」) 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 引擎的查找流程是:
pearlTea 自己身上有 drink 嗎?→ 沒有pearlTea.__proto__(也就是 MilkTea.prototype)上找 → 找到了!執行它__proto__ 往上找 Object.prototypenull → 沒有就回傳 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 ✅
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() 時:
rex 自己沒有 speak → 去 Dog.prototype 找Dog.prototype 有 speak → 找到了,執行!如果呼叫 rex.hasOwnProperty('name'):
rex 沒有 → Dog.prototype 沒有 → Animal.prototype 沒有Object.prototype 有 → 找到了!instanceof 的本質console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
instanceof 的原理:沿著 rex.__proto__ 這條鏈,看 Dog.prototype 和 Animal.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;
}
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 自己身上新建一個同名屬性,把原型上的屬性「遮住」了。
Function.__proto__ === Function.prototypeconsole.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
三條核心規則:
__proto__,它指向建立該物件的建構函式的 prototypeprototype,它是一個物件,用於存放實例共用的屬性和方法__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
試著畫出 Object 和 Function 之間互相糾纏的原型鏈圖吧——如果你能畫對,說明你真的懂了。
記住:JavaScript 沒有類,只有物件。原型鏈就是物件之間的「關係網」,
__proto__是網線,prototype是網線另一頭的插座。