================================================================
在 JavaScript 的世界裡,原型(prototype)是一個貫穿始終的核心概念。它不像變數、函式那樣直觀,卻默默支撐著 JS 的繼承機制,讓我們寫出更簡潔、高效的程式碼。我們就結合具體程式碼實例,一點點拆解原型的奧祕,從顯式原型、隱式原型,到 new 的執行過程、原型鏈。
首先要記住一個關鍵結論:所有函式天生就擁有一個 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
這裡的邏輯很清晰👇:
補充一個重要注意點⚠️:實例物件無法直接修改建構函式原型上的屬性值。比如我們嘗試給 car1 的 height 賦值,看看會發生什麼:
car1.height = 5000; // 看似修改,實則是給 car1 自身添加了 height 屬性
console.log(car1.height); // 輸出:5000(存取的是自身屬性)
console.log(Car.prototype.height); // 輸出:1400(原型上的屬性沒變)
如果說 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 關鍵字(比如 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 方法)
補充一個小知識點🌟:建構函式的 prototype 上,預設有一個 constructor 屬性,它指向建構函式本身。所以我們可以透過實例的 proto.constructor,找到它的建構函式:
console.log(car.constructor); // 輸出:function Car() { this.name = 'su7' }
這也是為什麼每個實例都能「知道」自己是由哪個建構函式建立的~
理解了顯式原型、隱式原型和 new 的執行過程,原型鏈就很好懂了。我們先回顧一個核心邏輯:JS 引擎存取物件屬性時,會先找自身,再找 proto(建構函式原型),再找 proto 的 proto,層層往上找,直到找到 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() 的查找過程:
最後,整理幾個新手最容易踩的坑,結合我們的程式碼實例,幫大家避坑:
用一句話總結原型相關的核心邏輯👇:
函式有 prototype(顯式原型),物件有 proto(隱式原型),實例的 proto 指向建構函式的 prototype,層層向上形成原型鏈,支撐 JS 的繼承和屬性查找。