================================================================

前言

在 JavaScript 的世界裡,原型(prototype)是一個貫穿始終的核心概念。它不像變數、函式那樣直觀,卻默默支撐著 JS 的繼承機制,讓我們寫出更簡潔、高效的程式碼。我們就結合具體程式碼實例,一點點拆解原型的奧祕,從顯式原型、隱式原型,到 new 的執行過程、原型鏈。

一、先搞懂:什麼是顯式原型(prototype)?

首先要記住一個關鍵結論:所有函式天生就擁有一個 prototype 屬性,它是一個物件,我們稱之為「顯式原型」。這個物件的作用很簡單——存放建構函式的共用屬性和方法,讓所有實例物件都能共享,避免重複建立,節省記憶體。

就像我們給出的程式碼範例,先看最基礎的陣列擴充:

// 給 Array 的原型添加一個 abc 方法
Array.prototype.abc = function() {
  return 'abc'
}

const arr = [] // 建立陣列實例(等同於 new Array())
console.log(arr.abc()); // 輸出:abc

這裡有個小細節✨:我們並沒有給 arr 這個具體的陣列實例添加 abc 方法,但是它卻能直接呼叫 abc(),這就是原型的功勞!因為 arr 是 Array 建構函式建立的實例,Array.prototype 上的方法,會被所有 Array 實例共享。

再看更直觀的建構函式範例,比如我們定義的 Car 建構函式:

// 給 Car 的原型掛載共用屬性
Car.prototype.name = 'su7-Ultra'
Car.prototype.height = 1400
Car.prototype.weight = 1.5

// 建構函式,只定義獨有的屬性(color)
function Car(color) {
  this.color = color // 每個實例的 color 可能不同,單獨定義
}

const car1 = new Car('pink') // 建立 Car 實例
console.log(car1.name); // 輸出:su7-Ultra

這裡的邏輯很清晰👇:

  • Car 是一個建構函式,它的 prototype 上掛載了 name、height 等所有 Car 實例都共用的屬性;
  • 我們建立 car1 實例時,只需要傳入獨有的 color 屬性,不需要重複定義 name、height 等共用屬性;
  • car1 能直接存取到 name,就是因為它「繼承」了 Car.prototype 上的屬性。

補充一個重要注意點⚠️:實例物件無法直接修改建構函式原型上的屬性值。比如我們嘗試給 car1 的 height 賦值,看看會發生什麼:

car1.height = 5000; // 看似修改,實則是給 car1 自身添加了 height 屬性
console.log(car1.height); // 輸出:5000(存取的是自身屬性)
console.log(Car.prototype.height); // 輸出:1400(原型上的屬性沒變)

二、隱式原型(proto):實例與原型的「橋樑」

如果說 prototype 是建構函式的「專屬屬性」,那 proto 就是每個物件的「專屬屬性」——每一個物件(包括實例物件)都擁有一個 proto 屬性,它也是一個物件,我們稱之為「隱式原型」

它的核心作用是:建立實例和建構函式原型之間的連線,讓實例能夠找到原型上的屬性和方法。這裡有一個重中之重的等式,一定要記牢📝:

實例物件的隱式原型(proto) === 其建構函式的顯式原型(prototype)

我們用程式碼驗證一下,還是用上面的 Car 實例:

function Car(color) {
  this.color = color
}
const car1 = new Car('pink')

// 驗證等式
console.log(car1.__proto__ === Car.prototype); // 輸出:true

這就解釋了為什麼 car1 能存取到 Car.prototype 上的 height 屬性:當 JS 引擎(比如 v8)存取物件的一個屬性時,會先找物件自身的「顯示屬性」(比如 car1 的 color);如果找不到,就會透過 proto,去它的建構函式原型(Car.prototype)裡找;如果還找不到,就繼續往上找——這就是我們後面要講的原型鏈啦!

再看一個小範例,幫大家加深理解👇:

Animal.prototype.say = function() {
  console.log('好可愛呀');
}

function Animal() {
  this.name = '跳跳虎' // 自身屬性
}

const a = new Animal()
a.say = 'hello' // 給 a 自身添加 say 屬性

console.log(a); // 輸出:Animal { name: '跳跳虎', say: 'hello' }
// 此時存取 a.say,優先找自身的 say,所以輸出 'hello',不會去原型上找 say 方法

三、new 關鍵字:背後藏著 5 步「魔法」

我們建立實例時,通常會用到 new 關鍵字(比如 new Car()、new Animal()),但你知道它在背後悄悄做了什麼嗎?其實它的執行過程有五步,結合程式碼一看就懂:

我們以 const car = new Car() 為例,拆解 new 的 5 步操作:

// 給 Car 原型添加 run 方法
Car.prototype.run = function() {
  console.log('running');
}

function Car() { // 本質是 new Function() 建立的函式
  // const obj = {}  // 第1步:建立空物件(註解模擬 new 的底層操作)
  // Car.call(obj)  // 第2步:改變 this 指向,讓 Car 的 this 指向空物件 obj
  this.name = 'su7' // 第3步:執行建構函式程式碼,給 obj 添加屬性
  // obj.__proto__ = Car.prototype  // 第4步:建立原型連接
  // return obj  // 第5步:回傳加工後的 obj
}

const car = new Car() // 執行 new 操作,建立實例,實例的 __proto__ === Car.prototype

car.run() // 輸出:running(透過原型鏈找到 Car.prototype 上的 run 方法)
  1. 建立一個空物件:相當於 const obj = {};
  2. 改變 this 指向:讓建構函式 Car 裡的 this,指向這個新建的空物件 obj。Car.call(obj),call 方法將建構函式 Car 裡的 this 強制指向新建的空物件 obj;
  3. 執行建構函式程式碼:把建構函式裡的屬性/方法,添加到空物件 obj 上(這裡就是給 obj 添加 name: 'su7');
  4. 建立原型連接:把 obj 的隱式原型(proto),指向 Car 的顯式原型(prototype),也就是 obj.proto = Car.prototype;
  5. 回傳這個物件:把 obj 回傳,賦值給 car,所以 car 其實就是這個被加工後的 obj。

補充一個小知識點🌟:建構函式的 prototype 上,預設有一個 constructor 屬性,它指向建構函式本身。所以我們可以透過實例的 proto.constructor,找到它的建構函式:

console.log(car.constructor); // 輸出:function Car() { this.name = 'su7' }

這也是為什麼每個實例都能「知道」自己是由哪個建構函式建立的~

四、原型鏈:JS 繼承的「底層邏輯」

理解了顯式原型、隱式原型和 new 的執行過程,原型鏈就很好懂了。我們先回顧一個核心邏輯:JS 引擎存取物件屬性時,會先找自身,再找 proto(建構函式原型),再找 protoproto,層層往上找,直到找到 null 為止——這種層層查找的關係,就是原型鏈

最經典的例子,就是我們給出的「祖孫繼承」程式碼,完美體現了原型鏈的查找過程:

// 祖父建構函式
Grand.prototype.house = function() {
  console.log('四合院');
}
function Grand() {
  this.card = 10000
}

// 父親建構函式,繼承 Grand
Parent.prototype = new Grand() // 讓 Parent 的原型指向 Grand 的實例
function Parent() {
  this.lastName = '張'
}

// 孩子建構函式,繼承 Parent
Child.prototype = new Parent() // 讓 Child 的原型指向 Parent 的實例
function Child() {
  this.age = 18
}

const c = new Child() // 建立 Child 實例
console.log(c.toString()); // 輸出:[object Object]

我們來拆解 c.toString() 的查找過程:

  1. 先找 c 自身:c 只有 age 屬性,沒有 toString() 方法;
  2. 找 c.proto(即 Child.prototype,也就是 Parent 的實例):Parent 的實例有 lastName 屬性,沒有 toString();
  3. 找 c.proto.proto(即 Parent.prototype.proto,也就是 Grand 的實例):Grand 的實例有 card 屬性,沒有 toString();
  4. 找 c.proto.proto.proto(即 Grand.prototype.proto,也就是 Object.prototype):Object.prototype 上有 toString() 方法,所以執行這個方法,輸出 [object Object];
  5. 如果 Object.prototype 上還找不到,就找它的 proto,而 Object.prototype.proto === null,查找結束。

五、常見誤區

最後,整理幾個新手最容易踩的坑,結合我們的程式碼實例,幫大家避坑:

  1. 誤區:prototype 是物件的屬性?正解:prototype 是函式的屬性,物件只有 proto
  2. 誤區:實例能修改原型上的屬性?正解:實例只能給自身添加屬性,無法直接修改原型上的屬性,只會「覆蓋」自身的存取優先權;
  3. 誤區:原型鏈是無限的?正解:原型鏈的盡頭是 null,Object.prototype.proto === null,查找至此結束。

用一句話總結原型相關的核心邏輯👇:

函式有 prototype(顯式原型),物件有 proto(隱式原型),實例的 proto 指向建構函式的 prototype,層層向上形成原型鏈,支撐 JS 的繼承和屬性查找。


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


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

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝14   💬10   ❤️2
390
🥈
我愛JS
📝2   💬9   ❤️2
87
🥉
💬1  
4
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付