在 JavaScript 裡,“原型”這個詞聽起來高大上,實際上就是一個“默認備胎”:當物件自己找不到屬性時,就沿著原型這條暗道去“親戚家”借。沒有類、沒有藍圖,僅靠這條備胎鏈,就能把公共方法層層複用,讓記憶體省一半、程式碼少一半。本文只聊“原型”本身——prototype、__proto__ 這些眼前能用的工具,把“借東西”的流程畫成一張家譜圖,幫你先看清“親戚”是誰、住哪、怎麼串門。至於後面更高階的封裝、多態、模組化,等我們把這條鏈走熟再升級也不遲。
prototype又稱顯示原型,函數天生擁有的一個屬性,將構造函數中的一些固定的屬性和方法掛載到原型上,在創建實例的時候,就不需要重複執行這些屬性和方法了,我們先來創造一個環境,主角依然是我們的小米 su7,su7 的屬性有無數個,但是各個車主只需要選擇並改動的屬性並沒有那麼多,這個時候我們就能用得到原型。
Car.prototype.name = 'su7-Ultra'
Car.prototype.lang = 4800
Car.prototype.height = 1400
Car.prototype.weight = 1.5
function Car(color) {
this.color = color
}
const car1 = new Car('pink')
const car2 = new Car('green')
console.log(car1);
用原型之後我們只需要輸入想要的顏色即可,不需要反反復復創建函數。同時掛載在原型上的屬性是可以直接被實例物件訪問到的(如下圖)

並且實例物件無法修改構造函數原型上的屬性值,
Person.prototype.say = '我太帥了'
function Person() {
this.name = '饒總'
}
const p = new Person()
p.say = 'hello'
const p2 = new Person()
console.log(p2.say);
這個時候同時有兩個 key 都為 say,但 value 不相同,一個被掛在構造函數的原型上,一個被掛在第一個實例物件 p 上,按照上面說法實例物件無法修改構造函數原型上的屬性值,但是打印出來真是這樣嗎,究竟是 '我太帥了' ,還是 'hello',我們來揭曉答案

果然是實例物件無法修改構造函數原型上的屬性值。
__proto__又稱隱式原型,每一個物件都擁有一個 __proto__ 屬性,該屬性值也是一個物件,v8 在訪問物件中的一個屬性時,會先訪問該物件中的顯示屬性,如果找不到,就回去物件的隱式原型中查找,實例物件的隱式原型 === 構造函數的顯示原型,所以如果實例物件的隱式原型找不到那麼就再會去構造函數的顯示原型上找。
這不得不再引出一個概念—— 原型鏈:v8 在訪問物件中的屬性時,會先訪問該物件中的顯示屬性,如果找不到,就去物件的隱式原型上找,如果還找不到,就去__proto__.__proto__ 上找,層層往上,直到找到null為止。這種查找關係被稱為原型鏈。
為了更好的理解它,我們來舉個繼承例子
function Parent() {
this.lastName = '張'
}
Child.prototype = new Parent() // {lastName: '張'}.__proto__ = Parent.prototype
function Child() {
this.age = 18
}
const c = new Child()
console.log(c.lastName);
在實例物件中我們只能找到兒子的年齡屬性,姓氏張是兒子從父親那裡繼承的,我們要查到兒子的姓氏,根據原型鏈原理我們先從實例物件 c 中找有沒有顯示屬性是關於姓氏的,很明顯並沒有,接著就去實例物件的隱式原型上找,也沒有,最後就來到了構造函數的顯示原型上查找,在程式碼的第四行可以看到構造函數的顯示原型被賦值上了 lastName 屬性,所以最終是否可以查找得到姓氏張呢?我們來直接看結果

好你說這個也太簡單了吧,就父子繼承而已。話不多說我再附上一串程式碼和打印結果
Grand.prototype.house = function() {
console.log('四合院');
}
function Grand() {
this.card = 10000
}
Parent.prototype = new Grand() // {card: 10000}.__proto__ = Grand.prototype.__proto__ = Object.prototype.__proto__ = null
function Parent() {
this.lastName = '張'
}
Child.prototype = new Parent() // {lastName: '張'}.__proto__ = Parent.prototype
function Child() {
this.age = 18
}
const c = new Child() // {age: 18}.__proto__ = Child.prototype
console.log(c.card);
c.house()
// console.log(c.toString());

這裡我們要注意一點:如果讓你查找一個整個頁面都沒有的屬性又該會是什麼打印結果呢?我們注意看上面最後一行註解掉的程式碼,他的輸出結果如下

他是直接找到了全局的物件上,經歷了一遍原型鏈查找在 Object.prototype上找到,如果再不找到最終就會停留在null上,下面放一張 js 界中廣為流傳的一張圖,如果你能看懂那麼你就是徹底會了!

new 在幹什麼?這時候你會說什麼?上篇文章不是講了 new 究竟幹了些什麼嗎,怎麼又問,不必驚訝,其實上次沒講全,這次來帶你真正看看 new 究竟究竟都幹了些什麼(這絕對是最終理解)直接一套小連招先上五個步驟
this 指向這個空物件__proto__) 赋值成 構造函數的顯示原型(prototype)再上程式碼(加註解)
Car.prototype.run = function() {
console.log('running');
}
function Car() { // new Function()
// const obj = {} //1
// Car.call(obj) // call 方法將 Car 函數中的 this = obj 2
this.name = 'su7' // 3
// obj.__proto__ = Car.prototype // 4
// return obj 5
}
const car = new Car() // {name: 'su7'}.__proto__ == Car.prototype
car.run()
最後輸出

prototype)是函數自帶的“樣板房”,所有實例都能來蹭住。__proto__)是實例手裡的“門禁卡”,刷卡就能進樣板房找方法。null 到頭。new 的五步曲:空物件→認證→綁卡→執行→返回,一口氣把“樣板房”繼承給新實例。把這四點串成一張地鐵圖,以後看任何“找不到屬性”的問題,先問一句:它刷卡刷到第幾站了?原型鏈通了,繼承就不再是黑魔法。