前言
❝
JavaScript常被描述為一種「基於原型的語言」——每個對象都擁有一個「原型對象」,對象以其原型為模板、從原型繼承屬性和放法。原型對象也可能擁有原型,並從中繼承屬性和方法,一層一層以此類推。這種關系常被稱為「原型鏈」,它解釋了為何一個對象會擁有定義在其他對象中的屬性和方法。
准確的說,這些屬性和方法定義在
Object
的構造函數
的prototype
屬性上,而非對象實例本身。❞
四句話道破原型與原型鏈:
- 每個函數(類)天生自帶一個屬性
prototype
,屬性值是一個對象,里面存儲了當前類供實例
使用的屬性和方法 「(顯示原型)」 - 在瀏覽器默認給原型開辟的堆內存中有一個
constructor
屬性:存儲的是當前類本身(⚠️注意:自己開辟的堆內存中默認沒有constructor
屬性,需要自己手動添加)「(構造函數)」 - 每個對象都有一個
__proto__
屬性,這個屬性指向當前實例所屬類的原型
(不確定所屬類,都指向Object.prototype
)「(隱式原型)」 - 當你試圖獲取一個對象的某個屬性時,如果這個對象本身沒有這個屬性,那么它會去它的隱式原型
__proto__
(也就是它的構造函數的顯示原型prototype
)中查找。「(原型鏈)」
構造函數,原型與實例的關系:
❝
每個
構造函數(constructor)
都有一個原型對象(prototype)
,原型對象(prototype)
都包含一個指向構造函數(constructor)
的指針,而實例(instance)
都包含一個指向原型對象(__proto__)
的內部指針❞
prototype(顯式原型)
每個函數都有一個prototype
屬性
// 構造函數(類)
function Person(name){
this.name = name
}
// new了一個實例 (對象)
var person = new Person('南玖')
console.log(person) //Person { name: '南玖' }
console.log(Person.prototype) //構造函數(類)的原型 ----->對象
Person.prototype.age = 18 // 構造函數原型
console.log(person.age) // 18
上面我們把這個函數Person
的原型打印出來了,它指向的是一個對象,並且這個對象正是調用該構造函數而創建的實例的原型
上面這張圖表示的是構造函數與實例原型之間的關系,所以我們知道了構造函數的prototype
屬性指向的是一個對象。
那實例與實例原型之間的關系又是怎樣的呢?這里就要提到__proto__
屬性了
__proto__(隱式原型)
從上面四句話中我們可以知道這是每一個Javascript對象(除null)
都具有的一個屬性,這個屬性會指向該對象的原型(也就是實例原型)
因為在JavaScript中沒有類的概念,為了實現類似繼承的方式,通過__proto__
將對象和原型聯系起來組成原型鏈,的以讓對象訪問到不屬於自己的屬性。
那么我們就能夠證明實例與實例原型之間的關系
console.log(person.__proto__) //實例(對象)的原型--->對象
console.log(person.__proto__ === Person.prototype) //實例的原型與構造函數的原型相等
從上圖我們可以看出實例對象與構造函數都可以指向原型,那么原型能不能指向構造函數或者是實例呢?
constructor(構造函數)
原型是沒有屬性指向實例的,因為一個構造函數可以創建多個實例對象;
從前面的四句話中我們知道「在瀏覽器默認給原型開辟的堆內存中有一個constructor
屬性」,所以原型也是可以指向構造函數的,這個屬性就是「constructor」
於是我們可以證明一下觀點:
console.log(Person.prototype.constructor) //實例的顯式原型的構造函數ƒ Person(name){this.name = name}
console.log(person.__proto__.constructor) //實例的隱式原型的構造函數 ƒ Person(name){this.name = name}
console.log(person.__proto__.constructor === Person.prototype.constructor)//true 實例原型的構造函數與類的構造函數相等
console.log(Person === Person.prototype.constructor) //true
實例對象的__proto__
是如何產生的?
我們知道當我們使用new 操作符時,生成的實例對象就擁有了__proto__
屬性
function Foo() {}
// 這個函數時Function的實例對象
// function是一個語法糖
// 內部其實調用了new Function()
所以可以說,在new的過程中,新對象被添加了__proto__
屬性並且鏈接到了構造函數的原型上。
new的原理
說簡單點可以分為以下四步:
- 新建一個空對象
- 鏈接原型
- 綁定this,執行構造函數
- 返回新對象
function myNew() {
// 1.新建一個空對象
let obj = {}
// 2.獲得構造函數
let con = arguments.__proto__.constructor
// 3.鏈接原型
obj.__proto__ = con.prototype
// 4.綁定this,執行構造函數
let res = con.apply(obj, arguments)
// 5.返回新對象
return typeof res === 'object' ? res : obj
}
原型鏈
說完了原型,我們再來看看什么是原型鏈?先來看一張圖:
這張圖中,由__proto__
串起來的鏈式關系,我們就稱它為原型鏈
原型鏈的作用
原型鏈決定了JavaScript
中繼承的實現方式,當我們訪問一個屬性時,它的查找機制如下:
- 訪問對象實例屬性,有的話直接返回,沒有則通過
__proto__
去它的原型對象上查找 - 原型對象上能找到的話則返回,找不到繼續通過原型對象的
__proto__
查找 - 一直往下找,直到找到
Object.prototype
,如果能找到則返回,找不到就返回undefined
,不會再往下找了,因為Object.prototype.__proto__
是null,說明了Object
是所有對象的原型鏈頂層了。
❝
從圖中我們可以發現,所有對象都可以通過原型鏈最終找到
Object.prototype
,雖然Object.prototype
也是一個對象,但是這個對象卻不是Object
創造的,而是引擎自己創建了Object.prototype
。所以可以這樣說,所有實例都是對象,但是對象不一定都是實例。❞
構造函數的__proto__
是什么呢?
由上面的原型鏈的解釋,我們應該能夠理解構造函數的__proto__
的,在JavaScript
中所有東西都是對象,那么構造函數肯定也是對象,是對象就有__proto__
。
function Person(){}
console.log(Person.__proto__)
console.log(Function.prototype)
console.log(Person.__proto__===Function.prototype) // true
「這也說明了所有函數都是Function
的實例」
那這么理解的話,Function.__proto__
豈不是等於Function.prototype
。。。。我們不妨來打印一下看看
Function.__proto__ === Function.prototype // true
打印出來確實是這樣的。難道 Function.prototype
也是通過 new Function()
產生的嗎?
❝
答案是否定的,這個函數也是引擎自己創建的。首先引擎創建了
Object.prototype
,然后創建了Function.prototype
,並且通過__proto__
將兩者聯系了起來。這里也很好的解釋了上面的一個問題,為什么let fun = Function.prototype.bind()
沒有prototype
屬性。因為Function.prototype
是引擎創建出來的對象,引擎認為不需要給這個對象添加prototype
屬性。❞
總結
Object
是所有對象的爸爸,所有對象都可以通過__proto__
找到它Function
是所有函數的爸爸,所有函數都可以通過__proto__
找到它Function.prototype
和Object.prototype
是兩個特殊的對象,他們由引擎
來創建- 除了以上兩個特殊對象,其他對象都是通過
構造器
new
出來的 - 函數的
prototype
是一個對象,也就是原型 - 對象的
__proto__
指向原型,__proto__
將對象和原型連接起來組成了原型鏈