原型台夺、原型鏈及繼承關(guān)系

原型:

在講原型關(guān)系之前給我們來看一張圖片:

原型關(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)系被稱為原型鏈夫偶。
如圖:


image.png

為了解釋這個過程,用下面的例子做下說明:

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ù)一舉解決了原型鏈的兩大問題:
  1. 其一, 保證了原型鏈中引用類型值的獨立,不再被所有實例共享;
  2. 其二, 子類型創(chuàng)建時也能夠向父類型傳遞參數(shù).
  • 缺點
  1. 構(gòu)造函數(shù)無法復(fù)用:如果僅僅借用構(gòu)造函數(shù),那么將無法避免構(gòu)造函數(shù)模式存在的問題--方法都在構(gòu)造函數(shù)中定義, 因此函數(shù)復(fù)用也就不可用了
  2. 超類型(如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ā)生以下變化:

  1. 創(chuàng)建一個空對象镊绪,并且 this 變量引用該對象,同時還繼承了該函數(shù)的原型。
  2. 屬性和方法被加入到 this 引用的對象中叉瘩。
  3. 新創(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é)果:


image

可見,原型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é)果:


image

你會發(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末舞丛,一起剝皮案震驚了整個濱河市耘子,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌球切,老刑警劉巖谷誓,帶你破解...
    沈念sama閱讀 222,807評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異吨凑,居然都是意外死亡捍歪,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評論 3 399
  • 文/潘曉璐 我一進(jìn)店門鸵钝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來糙臼,“玉大人,你說我怎么就攤上這事恩商”涮樱” “怎么了?”我有些...
    開封第一講書人閱讀 169,589評論 0 363
  • 文/不壞的土叔 我叫張陵痕届,是天一觀的道長韧献。 經(jīng)常有香客問我末患,道長研叫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,188評論 1 300
  • 正文 為了忘掉前任璧针,我火速辦了婚禮嚷炉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘探橱。我一直安慰自己申屹,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 69,185評論 6 398
  • 文/花漫 我一把揭開白布隧膏。 她就那樣靜靜地躺著哗讥,像睡著了一般。 火紅的嫁衣襯著肌膚如雪胞枕。 梳的紋絲不亂的頭發(fā)上杆煞,一...
    開封第一講書人閱讀 52,785評論 1 314
  • 那天,我揣著相機與錄音,去河邊找鬼决乎。 笑死队询,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的构诚。 我是一名探鬼主播蚌斩,決...
    沈念sama閱讀 41,220評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼范嘱!你這毒婦竟也來了送膳?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,167評論 0 277
  • 序言:老撾萬榮一對情侶失蹤丑蛤,失蹤者是張志新(化名)和其女友劉穎肠缨,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盏阶,經(jīng)...
    沈念sama閱讀 46,698評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡晒奕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,767評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了名斟。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脑慧。...
    茶點故事閱讀 40,912評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖砰盐,靈堂內(nèi)的尸體忽然破棺而出闷袒,到底是詐尸還是另有隱情,我是刑警寧澤岩梳,帶...
    沈念sama閱讀 36,572評論 5 351
  • 正文 年R本政府宣布囊骤,位于F島的核電站,受9級特大地震影響冀值,放射性物質(zhì)發(fā)生泄漏也物。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,254評論 3 336
  • 文/蒙蒙 一列疗、第九天 我趴在偏房一處隱蔽的房頂上張望滑蚯。 院中可真熱鬧,春花似錦抵栈、人聲如沸告材。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽斥赋。三九已至,卻和暖如春产艾,著一層夾襖步出監(jiān)牢的瞬間疤剑,已是汗流浹背洛波。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留骚露,地道東北人蹬挤。 一個月前我還...
    沈念sama閱讀 49,359評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像棘幸,于是被迫代替她去往敵國和親焰扳。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,922評論 2 361

推薦閱讀更多精彩內(nèi)容