
對于Javascript原型鏈,是Javascript中很重要的內(nèi)容,要理解關(guān)鍵有三點:
Javascript中原型鏈作用是為了實現(xiàn)Javascript中的繼承機制。
Javascript中原型鏈?zhǔn)抢?strong>對象關(guān)聯(lián)的方式實現(xiàn)的(不同于一般的類復(fù)制)。
Javascript中之所以能使用原型鏈來實現(xiàn)繼承,關(guān)鍵是Javascript對象中屬性檢索機制。
于是從類基礎(chǔ)開始說起。
1. 面向?qū)ο?/h1>
1.1 類基礎(chǔ)
在面向?qū)ο笳Z言中,經(jīng)常會使用到類。類是一種設(shè)計模式,是某種事物的描述,類一般存在構(gòu)造函數(shù),構(gòu)造函數(shù)是用于構(gòu)建實例,實例是類的具體實現(xiàn),類和實例的關(guān)系就類似于藍(lán)圖和建筑物的關(guān)系一樣。
1.2 繼承和多態(tài)
繼承和多態(tài)是面向?qū)ο蟮膬蓚€重要的特性。對于類似Java的面向?qū)ο笳Z言來說,繼承是通過創(chuàng)建實例化的過程中,復(fù)制父類的屬性和方法來實現(xiàn),并且通過重寫父類的方法(實現(xiàn)了多態(tài))。
2. Javascript中對象屬性
這里先提前引入原型鏈,主要是為了描述,具體原型鏈繼承的方式在 3.2 原型鏈繼承 中描述。
2.1 屬性獲取
Javascript的對象中屬性獲取的時候,會根據(jù)一定的步驟進(jìn)行取值:
首先判斷對象中是否存在該屬性,如果存在該屬性則返回屬性值。
否則訪問該對象的原型對象(__proto__),判斷其是否存在該屬性,如果存在,則返回該屬性值。
否則繼續(xù)遍歷原型對象,直到Object.prototype如果仍然沒有找到該屬性則返回undefined
var o1 = {a: 1};
console.log(o1.a); // 1, obj中存在a,直接返回
function F(){};
F.prototype.b = 2;
var o2 = new F;
console.log(o2.hasOwnProperty('b')); // false
console.log(o2.b); // 2, obj的原型對象中存在屬性b,返回該屬性
var o3 = {};
console.log(o3.b); // undefined , 由于obj.__proto__中并不存在該屬性,所以返回undefined
2.2 屬性設(shè)置
屬性設(shè)置也會遍歷原型鏈,但是根據(jù)屬性存在位置以及原型鏈上屬性描述符的不同,可能會存在不同的設(shè)置結(jié)果:
// 1. 對象中存在該屬性 , 直接修改屬性值
var o1 = {a: 1};
console.log(o1.__proto__.a); // undefined
console.log(o1.a); // 1
o1.a = 2;
console.log(o1.__proto__.a); // undefined ,原型鏈上并沒有增加該屬性
console.log(o1.a); // 2,當(dāng)前對象屬性值被修改
// 2. 對象中不存在該屬性,原型鏈上也不存在該屬性,則對象中增加該屬性
var o2 = {};
console.log(o2.__proto__.a); // undefined
console.log(o2.hasOwnProperty('a')); // false
o2.a = 1;
console.log(o2.__proto__.a); // undefined ,原型鏈上并沒有增加該屬性
console.log(o2.hasOwnProperty('a')); // true
// 3. 對象中不存在該屬性,原型鏈上存在該屬性且不為只讀,則當(dāng)前對象增加該屬性,并屏蔽原型鏈屬性值
function F() {}
F.prototype.a = 1;
var o3 = new F;
console.log(o3.__proto__.a); // 1
console.log(o3.hasOwnProperty('a')); // false
o3.a = 2;
console.log(o3.__proto__.a); // 1, 原型鏈上屬性沒有發(fā)生變化
console.log(o3.hasOwnProperty('a')); // true,對象增加屬性
console.log(o3.a); // 2,屏蔽原型鏈上屬性
// 4. 對象中不存在該屬性,原型鏈上該屬性為只讀,則不會在當(dāng)前對象中增加該屬性,且該屬性值不變
function F() {}
Object.defineProperty(F.prototype, 'a', {
writable: false,
value: 1
})
var o4 = new F;
console.log(o4.__proto__.a); // 1
console.log(o4.hasOwnProperty('a')); // false
o4.a = 2;
console.log(o4.__proto__.a); // 1, 原型鏈上屬性沒有發(fā)生變化
console.log(o4.hasOwnProperty('a')); // false,對象中沒有增加該屬性值
console.log(o4.a); // 1,獲取原型鏈上屬性
// 5. 對象中不存在該屬性,原型鏈上存在該屬性的setter方法,則會調(diào)用該setter方法
function F() {}
Object.defineProperty(F.prototype, 'a', {
set(){
console.log('set a');
}
})
var o5 = new F;
console.log(o5.__proto__.a); // undefined
console.log(o5.hasOwnProperty('a')); // false
o5.a = 2; // 'set a'
console.log(o5.__proto__.a); // undefined, 原型鏈上屬性沒有發(fā)生變化
console.log(o5.hasOwnProperty('a')); // false,對象中沒有增加該屬性值
console.log(o5.a); // unedefined,獲取原型鏈上屬性值并沒有變
3. 繼承
3.1 混入
如 1.2 繼承和多態(tài) 中所說,一般面向?qū)ο缶幊陶Z言的繼承都是在對象實例化的時候,采用復(fù)制的方式將父類的內(nèi)容深度復(fù)制一份到實例中,由于Javascript中并沒有實例化的過程,但是可以通過對象復(fù)制的方式來實現(xiàn)繼承關(guān)系,這樣的方式可以叫做混入。
一般的顯示混入,在混合對象的過程中,會將目標(biāo)對象中不存在的屬性進(jìn)行復(fù)制添加。
// 顯示混入函數(shù)
function mixin(target, source) {
for(let key in source){
if(!target.hasOwnProperty(key)){ // 不存在該屬性,則添加該屬性
target[key] = source[key];
}
}
}
這樣就可以將源對象的屬性添加到目標(biāo)對象屬性中,類似復(fù)制的原理實現(xiàn)繼承關(guān)系
當(dāng)然也可以直接創(chuàng)建一個對象包含所有屬性,用新對象屬性覆蓋掉原對象屬性并返回。
3.2 原型繼承
3.2.1 Function.prototype和Object.__proto__
Javascript中默認(rèn)的繼承機制并沒有使用類似上面的復(fù)制機制實現(xiàn),而是利用Javascript中的對象,通過對象關(guān)聯(lián)的方式進(jìn)行實現(xiàn)繼承,也就是原型繼承。
首先Javascript的Function對象中,默認(rèn)包含一個不可枚舉的prototype屬性,該屬性的值為對應(yīng)的原型對象,其結(jié)果為包含一個不可枚舉屬性constructor的對象
function F() {}
console.log(F.hasOwnProperty('prototype')); // true
console.log(Object.propertyIsEnumerable(F.prototype)); // false
console.log(F.prototoype); // { constructor: f}
注意:這里我們可以使用new F的方式創(chuàng)建對象,這種方式類似Java等面向?qū)ο笳Z言中的實例化,F類似構(gòu)造函數(shù),但是Javascript中不存在類,所以這只能理解為構(gòu)造函數(shù)方法調(diào)用。且這里的constructor并不代表對象的構(gòu)造關(guān)系。
Javascript對象中存在__proto__屬性,指向?qū)ο蟮脑蛯ο?/p>
function F() {};
var f = new F();
console.log(f.__proto__ === F.prototype); // true
3.2.2 Javascript原型繼承實現(xiàn)
原型繼承利用了Object.create()方法實現(xiàn)
function F() {}
F.prototype.a = 1;
function G() {}
G.prototype = Object.create(F.prototype);
var g = new G;
console.log(g.a); // 1,根據(jù)原型鏈機制獲取到原型對象F.protoype中屬性'a'的值
console.log(g.__proto__ === G.prototype); // true
console.log(g.__proto__.__proto__ === F.prototype); // true
其中,Object.create()的實現(xiàn)原理:
function create(o) {
function F(){}
F.prototype = o;
return new F();
}
通過代碼我們可以看到,利用Object.create()關(guān)聯(lián)了對象,使得G和F聯(lián)系了起來,同樣通過這個就可以知道為什么Object.create(null)創(chuàng)建出來的對象對象不在Object.prototype鏈上了
3.3 行為委托
對于Function的對象,利用Function.prototype原型鏈,創(chuàng)建了對象的關(guān)聯(lián),對于兩個對象之間,根據(jù)Object.create()的原理,也可以直接創(chuàng)建關(guān)聯(lián),通過這樣的方法,在屬性獲取不到的時候,當(dāng)前對象會委托關(guān)聯(lián)對象進(jìn)行數(shù)據(jù)獲取。
var o1 = {a: 1};
var o2 = Object.create(o1);
console.log(o2.a); //1 , 獲取o1對象上的值
因為Javascript中繼承本身就是對象之間的關(guān)聯(lián),所以比起使用原型繼承的方式,需要使用new等看起來像構(gòu)造函數(shù)的方式實現(xiàn)繼承關(guān)系,利用行為委托更優(yōu)秀。Javascript繼承實現(xiàn)的本質(zhì)對象關(guān)聯(lián)。
// 原型鏈實現(xiàn)繼承,通常子類含有父類相同方法并進(jìn)行重寫
function F(width, height){
this.width = width;
this.height = height;
}
F.prototype.width = 1
F.prototype.height = 1;
F.prototype.square = function () {
return this.width * this.height
}
function G(width, height){
F.call(this, width, height); // 需要使用這種顯示的方式來調(diào)用父類構(gòu)造器實現(xiàn)初始化
}
G.prototype = Object.create(F.prototype);
G.prototype.square = function () {
return 1/2 * this.width * this.height
}
var g = new G(2, 1);
g.square(); // 1;
// 行為委托實現(xiàn)繼承,子對象和父對象方法名一般不同
var F = {
init(width, height) {
this.width = width;
this.height = height;
},
rectSquare() {
return this.width * this.height
}
}
var G = Object.create(F);
G.build = function(width, height){
this.init(width, height); // 利用this來實例化對象
};
G.angelSquare = function() {
return 1/2 * this.width * this.height;
}
G.build(2, 1);
G.angelSquare(); // 1;
4. 其他
4.1 ES6中class
ES6中引入了class關(guān)鍵字和extends關(guān)鍵字來實現(xiàn)Javascript中的繼承,使得看起來更像一般的面向?qū)ο笳Z言,但是實際上這里的class只是原型繼承的語法糖,本質(zhì)還是對象的關(guān)聯(lián),并非類的復(fù)制,所以當(dāng)改變原型對象的內(nèi)容會影響到對應(yīng)的對象。
class F {
constructor(name) {
this.name = name;
}
log() {
return 'log:' + this.a
}
}
var f = new F('patrick');
F.prototype.log = function () {
return 'new log:' + this.name
}
console.log(f.log()); // new log: patrick 修改了原型對象,影響了實際對象。
4.2 hasOwnProperty
由于我們所說的Javascript對象中屬性獲取和設(shè)置是需要在原型鏈上進(jìn)行查找的,所以使用hasOwnProperty值來判斷是否為當(dāng)前對象屬性,可以阻斷原型鏈上的查找,急速性能
4.3 Object.prototype
對于所有的對象,最終原型對象都指向Object.prototype,而且Object.prototype的原型對象為null
5. 參考:
《你不知道的Javascript(上篇)》
MDN Inheritance and the prototype chain