在JavaScript
中,函數(shù)原型鏈?zhǔn)亲顝?qiáng)大也是最容易讓人迷惑的特性慈缔。長(zhǎng)期以來(lái)對(duì)于prototype
和__proto__
的一知半解導(dǎo)致在實(shí)際開(kāi)發(fā)中經(jīng)常遇到難以排查的問(wèn)題船逮,所以有必要將JavaScript
中的原型概念理解清楚。
1. __proto__
vs prototype
1.1 __proto__
在JavaScript
中所有對(duì)象都擁有一個(gè)__proto__
用來(lái)表示其原型繼承斋扰,所謂的原型鏈也就是根據(jù)__proto__
一層層向上追溯爷速。JavaScript
中有一個(gè)內(nèi)置屬性[[prototype]]
(注意不是prototype
)來(lái)表征其原型對(duì)象央星,大多數(shù)瀏覽器支持通過(guò)__proto__
來(lái)對(duì)齊進(jìn)行訪問(wèn)。一個(gè)普通對(duì)象的__proto__
為Object.prototype
:
var a = {
'h' : 1
}
// output: true
a.__proto__ === Object.prototype
1.2 prototype
prototype
是只有函數(shù)才有的屬性惫东。
當(dāng)創(chuàng)建函數(shù)時(shí)莉给,JavaScript
會(huì)自動(dòng)給函數(shù)創(chuàng)建一個(gè)prototype
屬性,并指向原型對(duì)象functionname.prototype
廉沮。
JavaScript
可以通過(guò)prototype
和__proto__
在兩個(gè)對(duì)象之間建立一個(gè)原型關(guān)系颓遏,實(shí)現(xiàn)方法和屬性的共享,從而實(shí)現(xiàn)繼承滞时。
1.3 構(gòu)造函數(shù)創(chuàng)建對(duì)象實(shí)例
JavaScript
中的函數(shù)對(duì)象有兩個(gè)不同的內(nèi)部方法:[[Call]]
和Construct
叁幢。
如果不通過(guò)new
關(guān)鍵字來(lái)調(diào)用函數(shù)(比如call,apply等)坪稽,則執(zhí)行[[Call]]
方法曼玩,該種方式只是單純地執(zhí)行函數(shù)體,并不創(chuàng)建函數(shù)對(duì)象窒百。
如果通過(guò)new
關(guān)鍵字來(lái)調(diào)用函數(shù)黍判,執(zhí)行的是[[Constrcut]]
方法,該方法會(huì)創(chuàng)建一個(gè)實(shí)例對(duì)象篙梢,同時(shí)將該對(duì)象的__proto__
屬性執(zhí)行構(gòu)造函數(shù)的prototype
也即functionname.prototype
,從而繼承該構(gòu)造函數(shù)下的所有實(shí)例和方法顷帖。
有了以上概念后,來(lái)看一個(gè)例子:
function Foo(firstName, lastName){
this.firstName = firstName;
this.lastName = lastName;
}
Foo.prototype.logName = function(){
Foo.combineName();
console.log(this.fullName);
}
Foo.prototype.combineName = function(){
this.fullName = `${this.firstName} ${this.lastName}`
}
var foo = new Foo('Sanfeng', 'Zhang');
foo.combineName();
console.log(foo.fullName); // Sanfeng Zhang
foo.logName(); // Uncaught TypeError: Foo.combineName is not a function
明明聲明了Foo.prototype.logName
,但是Foo.combineName
卻出錯(cuò)渤滞,其原因在于原型鏈理解出錯(cuò)贬墩。
首先來(lái)看下foo
的原型鏈:
var foo = new Foo('Sanfeng', 'Zhang')
:
通過(guò)new
創(chuàng)建一個(gè)函數(shù)對(duì)象,此時(shí)JavaScript
會(huì)給創(chuàng)建出來(lái)對(duì)象的__proto__
賦值為functionname.protoye
也即Foo.prototype
,所以foo.combineName
可以正常訪問(wèn)combineName
蔼水。其完整原型鏈為:
foo.__proto__ === Foo.prototype
foo.__proto__.__proto__ === Foo.prototype.__proto__ === Object.prototype
foo.__proto__.__proto__.__proto__ === Foo.prototype.__proto__.__proto__ === Object.prototype.__proto__ === null
[圖片上傳失敗...(image-21e9dd-1513432632315)]
接下來(lái)看下Foo的原型鏈:
直接通過(guò)Foo.combineName
調(diào)用時(shí)震糖,JavaScript
會(huì)從Foo.__proto__
找起录肯,而Foo.__proto__
指向Function.prototype
,所以根本無(wú)法找到掛載在Foo.prototype
上的combineName
方法趴腋。
其完整原型鏈為:
Foo.__proto__ = Function.prototype;
Foo.__proto__.__proto__ = Function.prototype.__proto__;
Foo.__proto__.__proto__.__proto__ = Function.prototype.__proto__.__proto__ = Object.prototype.__proto__ = null;
[圖片上傳失敗...(image-8e415a-1513432632315)]
接下來(lái)做一下變形:
function Foo(firstName, lastName){
this.firstName = firstName;
this.lastName = lastName;
}
Foo.__proto__.combineName = function() {
console.log('combine name');
}
Foo.combineName(); // combine name
Funciton.combineName(); // combine name
var foo = new Foo('Sanfeng', 'Zhang');
foo.combineName(); // foo.combineName is not a function
這次是在Foo.__proto__
上注冊(cè)的combineName
,所以實(shí)例對(duì)象foo無(wú)法訪問(wèn)到,但是Function Foo
可以訪問(wèn)到,另外我們看到因?yàn)?code>Foo.__proto__指向Function.prototype
优炬,所以可以直接通過(guò)Function.combineName
訪問(wèn)颁井。
2 原型繼承
理解清楚了__proto__
與prototype
的聯(lián)系和區(qū)別后,我們來(lái)看下如何利用兩者實(shí)現(xiàn)原型繼承蠢护。首先來(lái)看一個(gè)例子:
function Student(props) {
this.name = props.name || 'unamed';
}
Student.prototype.hello = function () {
console.log('Hello ' + this.name);
}
var xiaoming = new Student({name: 'xiaoming'}); // Hello xiaoming
這個(gè)很好理解:
xiaoming -> Student.prototype -> Object.prototype -> null
接下來(lái)雅宾,我們來(lái)創(chuàng)建一個(gè)PrimaryStudent
:
function PrimaryStudent(props) {
Student.call(this, props);
this.grade = props.grade || 1;
}
其中Student.call(this, props);
僅僅執(zhí)行Student方法,不創(chuàng)建對(duì)象葵硕,參考1.3節(jié)中的[[Call]]
眉抬。
此時(shí)的原型鏈為:
new PrimaryStudent() -> PrimaryStudent.prototype -> Object.prototype -> null
可以看到,目前PrimaryStudent
和Student
并沒(méi)有任何關(guān)聯(lián)懈凹,僅僅是借助Student.call(this, props);
聲明了name
屬性蜀变。
要想繼承Student
必須要實(shí)現(xiàn)如下的原型鏈:
new PrimaryStudent() -> PrimaryStudent.prototype -> Student.prototype -> Object.prototype -> null
當(dāng)然可以直接進(jìn)行如下賦值:
PrimaryStudent.prototype = Student.prototype
但這樣其實(shí)沒(méi)有任何意義,如此一來(lái)介评,所以在PrimaryStudent
上掛載的方法都是直接掛載到Student
的原型上了库北,PrimaryStudent
就顯得可有可無(wú)了。
那如何才能將方法掛載到PrimaryStudent
而不是Student
上呢们陆?其實(shí)很簡(jiǎn)單寒瓦,在PrimaryStudent
和Student
之間插入一個(gè)新的對(duì)象作為兩者之間的橋梁:
function F() {}
F.prototype = Student.prototype;
PrimaryStudent.prototype = new F();
PrimaryStudent.prototype.constructor = PrimaryStudent;
// 此時(shí)就相當(dāng)于在new F()對(duì)象上添加方法
PrimaryStudent.prototype.getGrade = function() {
}
如此一來(lái)就實(shí)現(xiàn)了PrimaryStudent與Student的繼承:
new PrimaryStudent() -> new PrimaryStudent().__proto__ -> PrimaryStudent.prototype -> new F() -> new F().__proto__ -> F.prototype -> Student.prototype -> Object.prototype -> null
3 關(guān)鍵字new
實(shí)際開(kāi)發(fā)中,我們總是通過(guò)一個(gè)new
來(lái)創(chuàng)建對(duì)象坪仇。那么為什么new
可以創(chuàng)建一個(gè)我們需要的對(duì)象杂腰?其與普通的函數(shù)執(zhí)行有什么不同呢?
來(lái)看下下面這段代碼:
function fun() {
console.log('fun');
}
fun();
var f = new fun();
其對(duì)應(yīng)的輸出都是一樣的:
fun
fun
但實(shí)際上椅文,兩者有著本質(zhì)的區(qū)別颈墅,前者是普通的函數(shù)執(zhí)行,也即在當(dāng)前活躍對(duì)象執(zhí)行環(huán)境內(nèi)直接執(zhí)行函數(shù)fun雾袱。
但new fun()
的實(shí)質(zhì)卻是創(chuàng)建了一個(gè)fun
對(duì)象恤筛,其含義等同于下文代碼:
function new(constructor) {
var obj = {}
Object.setPrototypeOf(obj, constructor.prototype);
return constructor.apply(obj, [...arguments].slice(1)) || obj
}
可以看到,當(dāng)我們執(zhí)行new fun()
時(shí)芹橡,實(shí)際執(zhí)行了如下操作:
- 創(chuàng)建了一個(gè)新的對(duì)象毒坛。
- 新對(duì)象的原型繼承自構(gòu)造函數(shù)的原型。
- 以新對(duì)象的 this 執(zhí)行構(gòu)造函數(shù)林说。
- 返回新的對(duì)象煎殷。如果構(gòu)造函數(shù)返回了一個(gè)對(duì)象,那么這個(gè)對(duì)象會(huì)取代整個(gè) new 出來(lái)的結(jié)果
從中也可以看到腿箩,其實(shí)new關(guān)鍵字也利用了原型繼承來(lái)實(shí)現(xiàn)對(duì)象創(chuàng)建豪直。