原型:
在講原型關(guān)系之前給我們來看一張圖片:
由圖我們可知幾個關(guān)系:
- 每一個構(gòu)造函數(shù)都有(原型)prototype指向它的原型對象腔剂。
- 原型對象有constructor指向它的構(gòu)造函數(shù)堪伍。
- 構(gòu)造函數(shù)可以通過new 的創(chuàng)建方式創(chuàng)建實例對象
- 實例對象通過proto指向它的原型對象解寝。
- 原型對象也有自己的原型對象扩然,通過proto指向。
原型鏈
如果試圖引用對象(實例instance)的某個屬性,會首先在對象內(nèi)部尋找該屬性,直至找不到,然后才在該對象的原型(instance.prototype)里去找這個屬性.如果還找不到則往原型的原型上找聋伦,這樣一個層層查找形成的一個鏈?zhǔn)降年P(guān)系被稱為原型鏈夫偶。
如圖:
為了解釋這個過程,用下面的例子做下說明:
function Father(){
this.property = true;
}
Father.prototype.getFatherValue = function(){
return this.property;
}
function Son(){
this.sonProperty = false;
}
//繼承 Father
Son.prototype = new Father();//Son.prototype被重寫,導(dǎo)致Son.prototype.constructor也一同被重寫
Son.prototype.getSonVaule = function(){
return this.sonProperty;
}
var instance = new Son();
console.log(instance.getFatherValue());//true
可見son實例對象找不到getFatherValue方法觉增,只能前去Father原型那里去找兵拢,返回值為true。
如果逾礁,對子類son進(jìn)行改造:
function Father(){
this.property = true;
}
Father.prototype.getFatherValue = function(){
return this.property;
}
function Son(){
this.sonProperty = false;
this.getFatherValue = function(){
return this.sonProperty;
}
}
//繼承 Father
Son.prototype = new Father();//Son.prototype被重寫,導(dǎo)致Son.prototype.constructor也一同被重寫
Son.prototype.getSonVaule = function(){
return this.sonProperty;
}
var instance = new Son();
console.log(instance.getFatherValue());//false
你會發(fā)現(xiàn)當(dāng)子類里出現(xiàn)相同的方法時说铃,則執(zhí)行子類中的方法,也就驗證了之前的實例對象查找引用屬性的過程嘹履。
確定原型和實例的關(guān)系
使用原型鏈后, 我們怎么去判斷原型和實例的這種繼承關(guān)系呢? 方法一般有兩種.
第一種是使用 instanceof 操作符, 只要用這個操作符來測試實例(instance)與原型鏈中出現(xiàn)過的構(gòu)造函數(shù),結(jié)果就會返回true. 以下幾行代碼就說明了這點.
console.log(instance instanceof Object);//true
console.log(instance instanceof Father);//true
console.log(instance instanceof Son);//true
由于原型鏈的關(guān)系, 我們可以說instance 是 Object, Father 或 Son中任何一個類型的實例. 因此, 這三個構(gòu)造函數(shù)的結(jié)果都返回了true.
第二種是使用 isPrototypeOf() 方法, 同樣只要是原型鏈中出現(xiàn)過的原型,isPrototypeOf() 方法就會返回true, 如下所示.
console.log(Object.prototype.isPrototypeOf(instance));//true
console.log(Father.prototype.isPrototypeOf(instance));//true
console.log(Son.prototype.isPrototypeOf(instance));//true
原型鏈存在的問題截汪。
原型鏈并非十分完美, 它包含如下兩個問題:
- 問題一: 當(dāng)原型鏈中包含引用類型值的原型時,該引用類型值會被所有實例共享;
- 問題二: 在創(chuàng)建子類型(例如創(chuàng)建Son的實例)時,不能向超類型(例如Father)的構(gòu)造函數(shù)中傳遞參數(shù).
有鑒于此, 實踐中很少會單獨使用原型鏈.
為此,下面將有一些嘗試以彌補原型鏈的不足.
js 繼承
借用構(gòu)造函數(shù)(經(jīng)典繼承)
為解決原型鏈中上述兩個問題, 我們開始使用一種叫做借用構(gòu)造函數(shù)(constructor stealing)的技術(shù)(也叫經(jīng)典繼承).
基本思路:就是在子類的構(gòu)造函數(shù)里調(diào)用父類的構(gòu)造函數(shù)。
function Father(){
this.colors = ["red","blue","green"];
function hello() {
console.log('hello world')
}
}
function Son(){
Father.call(this);//繼承了Father,且向父類型傳遞參數(shù)
}
var instance1 = new Son();
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"
var instance2 = new Son();
console.log(instance2.colors);//"red,blue,green" 可見引用類型值是獨立的
- 優(yōu)點
特別注意的是引用類型的值是獨立的植捎。
很明顯,借用構(gòu)造函數(shù)一舉解決了原型鏈的兩大問題:
- 其一, 保證了原型鏈中引用類型值的獨立,不再被所有實例共享;
- 其二, 子類型創(chuàng)建時也能夠向父類型傳遞參數(shù).
- 缺點
- 構(gòu)造函數(shù)無法復(fù)用:如果僅僅借用構(gòu)造函數(shù),那么將無法避免構(gòu)造函數(shù)模式存在的問題--方法都在構(gòu)造函數(shù)中定義, 因此函數(shù)復(fù)用也就不可用了
- 超類型(如Father)中定義的方法,對子類型而言也是不可見的.(超類里的方法在子類里無法調(diào)用,比如hello方法就無法調(diào)用衙解,親測是這樣的,有興趣可以動手一試)
考慮此,借用構(gòu)造函數(shù)的技術(shù)也很少單獨使用.
原型繼承
該方法最初由道格拉斯·克羅克福德于2006年在一篇題為 《Prototypal Inheritance in JavaScript》(JavaScript中的原型式繼承) 的文章中提出. 他的想法是借助原型可以基于已有的對象創(chuàng)建新對象焰枢, 同時還不必因此創(chuàng)建自定義類型. 大意如下:
基本思路:在create()函數(shù)內(nèi)部, 先創(chuàng)建一個臨時性的構(gòu)造函數(shù), 然后將傳入的對象作為這個構(gòu)造函數(shù)的原型,最后返回了這個臨時類型的一個新實例.
function create(o){
function Fn() {}
Fn.prototype = o;
return new Fn();
}
實質(zhì)上就是對傳入的實例o進(jìn)行了一次淺拷貝蚓峦。
function Father(){
this.colors = ["red","blue","green"];
}
let fa = new Father()
var instance1 =create(fa);
instance1.colors.push("black");
console.log(instance1.colors); // [ 'red', 'blue', 'green', 'black' ]
var instance2 = create(fa);
instance2.colors.push("white");
console.log(instance2.colors); //[ 'red', 'blue', 'green', 'black', 'white' ]
在此例中:instance1與instance的原型是同一個對象,當(dāng)instance1操作原型的引用類型數(shù)值济锄,也會影響到instance2暑椰。此時數(shù)據(jù)是共享的。
再看下面這個例子:
function Father(){
this.colors = ["red","blue","green"];
}
var instance1 = create(new Father());
instance1.colors.push("black");
console.log(instance1.colors); // [ 'red', 'blue', 'green', 'black' ]
var instance2 = create(new Father());
instance2.colors.push("white");
console.log(instance2.colors); // [ 'red', 'blue', 'green', 'white' ]
此時由于原型實例不是同一個荐绝,數(shù)據(jù)不在共享一汽。
在 ECMAScript5 中,通過新增 object.create() 方法規(guī)范化了上面的原型式繼承.
object.create() 接收兩個參數(shù):
- 一個用作新對象原型的對象
- (可選的)一個為新對象定義額外屬性的對象
關(guān)鍵點:原型式繼承中, 包含引用類型值的屬性始終都會共享相應(yīng)的值, 就像使用原型模式一樣.
組合繼承
組合繼承, 有時候也叫做偽經(jīng)典繼承,指的是將原型鏈和借用構(gòu)造函數(shù)的技術(shù)組合到一塊,從而發(fā)揮兩者之長的一種繼承模式。
基本思路:使用原型鏈實現(xiàn)對原型屬性和方法的繼承,通過借用構(gòu)造函數(shù)來實現(xiàn)對實例屬性的繼承.
如下例:
function Father(name){
this.name = name;
this.colors = ["red","blue","green"];
}
Father.prototype.sayName = function(){
alert(this.name);
};
function Son(name,age){
Father.call(this,name);//繼承實例屬性,第一次調(diào)用Father()
this.age = age;
}
Son.prototype = new Father();//繼承父類方法,第二次調(diào)用Father()
Son.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new Son("louis",5);
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"
instance1.sayName();//louis
instance1.sayAge();//5
var instance1 = new Son("zhai",10);
console.log(instance1.colors);//"red,blue,green"
instance1.sayName();//zhai
instance1.sayAge();//10
在這個例子中召夹,類Son通過構(gòu)造函數(shù)繼承可以向父類Father傳參岩喷,同時能夠保證實例數(shù)據(jù)不被共享。同時通過原型繼承可以復(fù)用父類的方法监憎,兩繼承組合起來纱意,各取所需。
組合繼承避免了原型鏈和借用構(gòu)造函數(shù)的缺陷,融合了它們的優(yōu)點,成為 JavaScript 中最常用的繼承模式. 而且, instanceof 和 isPrototypeOf( )也能用于識別基于組合繼承創(chuàng)建的對象.
此處調(diào)用了兩次父類的構(gòu)造函數(shù)鲸阔,后面的寄生式組合繼承將會對這個問題進(jìn)行優(yōu)化偷霉。
寄生式繼承
寄生式繼承是與原型式繼承緊密相關(guān)的一種思路。
基本思路:寄生式繼承的思路與(寄生)構(gòu)造函數(shù)和工廠模式類似, 即創(chuàng)建一個僅用于封裝繼承過程的函數(shù),該函數(shù)在內(nèi)部以某種方式來增強對象,最后再像真的是它做了所有工作一樣返回對象. 如下.
function createAnother(original){
var clone = create(original);//通過調(diào)用create函數(shù)創(chuàng)建一個新對象
clone.sayHi = function(){//以某種方式來增強這個對象
alert("hi");
};
return clone;//返回這個對象
}
直白點褐筛,所謂寄生式繼承也就是在其他繼承方式(構(gòu)造繼承类少、原型繼承等)上增加新的功能,返回新的對象渔扎。
寄生組合式繼承
前面講過,組合繼承是 JavaScript 最常用的繼承模式; 不過, 它也有自己的不足. 組合繼承最大的問題就是無論什么情況下,都會調(diào)用兩次父類構(gòu)造函數(shù): 一次是在創(chuàng)建子類型原型的時候, 另一次是在子類型構(gòu)造函數(shù)內(nèi)部. 寄生組合式繼承就是為了降低調(diào)用父類構(gòu)造函數(shù)的開銷而出現(xiàn)的 .如下例:
function extend(subClass,superClass){
var prototype = create(superClass.prototype);//創(chuàng)建對象
prototype.constructor = subClass;//增強對象
subClass.prototype = prototype;//指定對象
}
下面我們來看下extend的另一種更為有效的擴展.
// 把上面的 create 拆開硫狞,其實差不多。
function extend(subClass, superClass) {
var F = function() {};
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;
subClass.superclass = superClass.prototype;
if(superClass.prototype.constructor == Object.prototype.constructor) {
superClass.prototype.constructor = superClass;
}
}
擴展
屬性查找
- hasOwnProperty:使用了原型鏈后, 當(dāng)查找一個對象的屬性時赞警,JavaScript 會向上遍歷原型鏈妓忍,直到找到給定名稱的屬性為止虏两,到查找到達(dá)原型鏈的頂部 - 也就是 Object.prototype - 但是仍然沒有找到指定的屬性愧旦,就會返回 undefined. 此時若想避免原型鏈查找, 建議使用 hasOwnProperty 方法. 因為 hasOwnProperty 是 JavaScript 中唯一一個處理屬性但是不查找原型鏈的函數(shù).如下:
console.log(instance1.hasOwnProperty('age'));//true
- isPrototypeOf:對比而言isPrototypeOf 則是用來判斷該方法所屬的對象是不是參數(shù)的原型對象,是則返回true定罢,否則返回false笤虫。
console.log(Father.prototype.isPrototypeOf(instance1));//true
instanceof && typeof
instanceof 運算符是用來在運行時指出對象是否是構(gòu)造器的一個實例, 例如漏寫了new運算符去調(diào)用某個構(gòu)造器, 此時構(gòu)造器內(nèi)部可以通過 instanceof 來判斷.(java中功能類似)
function f(){
if(this instanceof arguments.callee)
console.log('此處作為構(gòu)造函數(shù)被調(diào)用');
else
console.log('此處作為普通函數(shù)被調(diào)用');
}
f();//此處作為普通函數(shù)被調(diào)用
new f();//此處作為構(gòu)造函數(shù)被調(diào)用
new運算符
new實質(zhì)上做了三件事;
var obj = {};
obj.__proto__ = F.prototype;
F.call(obj); //執(zhí)行
第一行祖凫,我們創(chuàng)建了一個空對象obj;
第二行琼蚯,我們將這個空對象的proto成員指向了F函數(shù)對象prototype成員對象;
第三行,我們將F函數(shù)對象的this指針替換成obj惠况,然后再調(diào)用F函數(shù).
我們可以這么理解: 以 new 操作符調(diào)用構(gòu)造函數(shù)的時候举户,函數(shù)內(nèi)部實際上發(fā)生以下變化:
- 創(chuàng)建一個空對象镊绪,并且 this 變量引用該對象,同時還繼承了該函數(shù)的原型。
- 屬性和方法被加入到 this 引用的對象中叉瘩。
- 新創(chuàng)建的對象由 this 所引用,并且最后隱式的返回 this殖卑。
關(guān)于原型繼承袍暴、構(gòu)造函數(shù)繼承(經(jīng)典繼承)里對于數(shù)據(jù)的操作
在繼承關(guān)系里,內(nèi)部屬性數(shù)值變不變攘蔽,數(shù)據(jù)共不共享前面也有所介紹龙屉,但是不夠具體。這塊時常令人迷惑满俗,決定單獨拿出來講講:
首先在繼承關(guān)系里转捕,原型繼承與構(gòu)造函數(shù)繼承可以分成兩個比較重要的繼承關(guān)系作岖,其他的繼承都是在這基礎(chǔ)上演變組合出來的,所以搞懂這兩個繼承關(guān)系中的數(shù)據(jù)變化瓜富,就差不多了鳍咱。
在講區(qū)別之前我們先兩個例子:
function kk() {
this.a = 3;
this.k = {l: 5};
}
function j() {
kk.call(this)
}
let m = new j();
m.a = 9;
m.k.l = 9;
let n = new j();
console.log(n.a, n.k.l);
打印結(jié)果:
可見,原型kk的數(shù)據(jù)并沒有改變与柑,再看一個例子:
function kk() {
this.a = 3;
this.k = {l: 5};
}
function j() {
}
j.prototype = new kk();
let m = new j();
m.a = 9;
m.k.l = 9;
let n = new j();
console.log(n.a, n.k.l);
打印結(jié)果:
你會發(fā)現(xiàn):原型里a沒變, k 變了谤辜。
對比上例,a始終沒變价捧,k有所區(qū)別丑念,究竟是什么原因呢?
如果你的眼睛足夠雪亮结蟋,會一眼看出上例是構(gòu)造函數(shù)繼承脯倚,下例是原型繼承,它兩的區(qū)別之前已經(jīng)說過嵌屎,構(gòu)造函數(shù)繼承數(shù)據(jù)不會共享推正,而原型繼承會共享。于是你會說為什么a怎么不變宝惰,你又在忽悠人植榕,哼!哈哈哈尼夺,抱歉尊残,有沒有看見a是基本數(shù)據(jù)類型,k是引用類型(<font color="red">引用類型包括:對象淤堵、數(shù)組寝衫、函數(shù)。</font>)啊拐邪,基本數(shù)據(jù)類型是指針的指向區(qū)別慰毅,引用類型是地址的指向區(qū)別。不了解這塊可以看看這篇文章:https://segmentfault.com/a/1190000008472264扎阶。
使用權(quán)威指南6.2.2繼承那塊的一句話<font color="red">“如果允許屬性賦值操作汹胃,它也總是在原始對象上創(chuàng)造屬性或者對已有屬性賦值,而不會修改原型鏈乘陪,在JavaScript里统台,只有查詢屬性才能感受到繼承的存在,而設(shè)置屬性則與繼承無關(guān)”</font>啡邑。
如何理解這句話贱勃?我想是指繼承關(guān)系中屬性在本身內(nèi)部找不到的時候才會去原型里找,只是借用屬性,但是并不會修改原型本身的屬性值贵扰,這也就解釋了基本數(shù)據(jù)類型始終不變的原因仇穗。而原型繼承中由于使用的同一原型對象,里面的引用類型使用同一個地址戚绕,導(dǎo)致應(yīng)用類型的數(shù)值是可以變化的纹坐。
總結(jié)兩點:
- 原型的基本數(shù)據(jù)類型不會受影響
- 在原型繼承里,引用類型的屬性會發(fā)生改變,在構(gòu)造函數(shù)繼承中不會受影響(地址不同)
參考地址: https://juejin.im/post/58f94c9bb123db411953691b#heading-13