原型、原型鏈及繼承關係

原型:

在講原型關係以前給咱們來看一張圖片:java

原型關係圖

由圖咱們可知幾個關係:segmentfault

  • 每個構造函數都有(原型)prototype指向它的原型對象。
  • 原型對象有constructor指向它的構造函數。
  • 構造函數能夠經過new 的建立方式建立實例對象
  • 實例對象經過_proto_指向它的原型對象。
  • 原型對象也有本身的原型對象,經過_proto_指向。

原型鏈

若是試圖引用對象(實例instance)的某個屬性,會首先在對象內部尋找該屬性,直至找不到,而後纔在該對象的原型(instance.prototype)裏去找這個屬性.若是還找不到則往原型的原型上找,這樣一個層層查找造成的一個鏈式的關係被稱爲原型鏈。 如圖: 數組

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被重寫,致使Son.prototype.constructor也一同被重寫
Son.prototype.getSonVaule = function(){
	return this.sonProperty;
}
var instance = new Son();
console.log(instance.getFatherValue());//true
複製代碼

可見son實例對象找不到getFatherValue方法,只能前去Father原型那裏去找,返回值爲true。 若是,對子類son進行改造:bash

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被重寫,致使Son.prototype.constructor也一同被重寫
Son.prototype.getSonVaule = function(){
	return this.sonProperty;
}
var instance = new Son();
console.log(instance.getFatherValue());//false
複製代碼

你會發現當子類裏出現相同的方法時,則執行子類中的方法,也就驗證了以前的實例對象查找引用屬性的過程。函數

肯定原型和實例的關係

使用原型鏈後, 咱們怎麼去判斷原型和實例的這種繼承關係呢? 方法通常有兩種.post

第一種是使用 instanceof 操做符, 只要用這個操做符來測試實例(instance)與原型鏈中出現過的構造函數,結果就會返回true. 如下幾行代碼就說明了這點.測試

console.log(instance instanceof Object);//true
console.log(instance instanceof Father);//true
console.log(instance instanceof Son);//true
複製代碼

因爲原型鏈的關係, 咱們能夠說instance 是 Object, Father 或 Son中任何一個類型的實例. 所以, 這三個構造函數的結果都返回了true.優化

第二種是使用 isPrototypeOf() 方法, 一樣只要是原型鏈中出現過的原型,isPrototypeOf() 方法就會返回true, 以下所示.ui

console.log(Object.prototype.isPrototypeOf(instance));//true
console.log(Father.prototype.isPrototypeOf(instance));//true
console.log(Son.prototype.isPrototypeOf(instance));//true
複製代碼

原型鏈存在的問題。

原型鏈並不是十分完美, 它包含以下兩個問題:this

  • 問題一: 當原型鏈中包含引用類型值的原型時,該引用類型值會被全部實例共享;
  • 問題二: 在建立子類型(例如建立Son的實例)時,不能向超類型(例如Father)的構造函數中傳遞參數. 有鑑於此, 實踐中不多會單獨使用原型鏈.

爲此,下面將有一些嘗試以彌補原型鏈的不足.

js 繼承

借用構造函數(經典繼承)

爲解決原型鏈中上述兩個問題, 咱們開始使用一種叫作借用構造函數(constructor stealing)的技術(也叫經典繼承).

基本思路:就是在子類的構造函數裏調用父類的構造函數。

function Father(){
	this.colors = ["red","blue","green"];
    function hello() {
        console.log('hello world')
    }
}
function Son(){
	Father.call(this);//繼承了Father,且向父類型傳遞參數
}
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" 可見引用類型值是獨立的
複製代碼
  • 優勢 特別注意的是引用類型的值是獨立的。 很明顯,借用構造函數一舉解決了原型鏈的兩大問題:
  1. 其一, 保證了原型鏈中引用類型值的獨立,再也不被全部實例共享;
  2. 其二, 子類型建立時也可以向父類型傳遞參數.
  • 缺點
  1. 構造函數沒法複用:若是僅僅借用構造函數,那麼將沒法避免構造函數模式存在的問題--方法都在構造函數中定義, 所以函數複用也就不可用了
  2. 超類型(如Father)中定義的方法,對子類型而言也是不可見的.(超類裏的方法在子類裏沒法調用,好比hello方法就沒法調用,親測是這樣的,有興趣能夠動手一試)

考慮此,借用構造函數的技術也不多單獨使用.

原型繼承

該方法最初由道格拉斯·克羅克福德於2006年在一篇題爲 《Prototypal Inheritance in JavaScript》(JavaScript中的原型式繼承) 的文章中提出. 他的想法是藉助原型能夠基於已有的對象建立新對象, 同時還沒必要所以建立自定義類型. 大意以下:

基本思路:在create()函數內部, 先建立一個臨時性的構造函數, 而後將傳入的對象做爲這個構造函數的原型,最後返回了這個臨時類型的一個新實例.

function create(o){
    function Fn() {}
    Fn.prototype = o;
    return new Fn();
  }
複製代碼

實質上就是對傳入的實例o進行了一次淺拷貝。

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的原型是同一個對象,當instance1操做原型的引用類型數值,也會影響到instance2。此時數據是共享的。 再看下面這個例子:

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' ]
複製代碼

此時因爲原型實例不是同一個,數據不在共享。

在 ECMAScript5 中,經過新增 object.create() 方法規範化了上面的原型式繼承. object.create() 接收兩個參數:

  • 一個用做新對象原型的對象
  • (可選的)一個爲新對象定義額外屬性的對象

關鍵點:原型式繼承中, 包含引用類型值的屬性始終都會共享相應的值, 就像使用原型模式同樣.

組合繼承

組合繼承, 有時候也叫作僞經典繼承,指的是將原型鏈和借用構造函數的技術組合到一塊,從而發揮二者之長的一種繼承模式。

基本思路:使用原型鏈實現對原型屬性和方法的繼承,經過借用構造函數來實現對實例屬性的繼承.

以下例:

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);//繼承實例屬性,第一次調用Father()
	this.age = age;
}
Son.prototype = new Father();//繼承父類方法,第二次調用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經過構造函數繼承能夠向父類Father傳參,同時可以保證明例數據不被共享。同時經過原型繼承能夠複用父類的方法,兩繼承組合起來,各取所需。

組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優勢,成爲 JavaScript 中最經常使用的繼承模式. 並且, instanceof 和 isPrototypeOf( )也能用於識別基於組合繼承建立的對象.

此處調用了兩次父類的構造函數,後面的寄生式組合繼承將會對這個問題進行優化。

寄生式繼承

寄生式繼承是與原型式繼承緊密相關的一種思路。 基本思路:寄生式繼承的思路與(寄生)構造函數和工廠模式相似, 即建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來加強對象,最後再像真的是它作了全部工做同樣返回對象. 以下.

function createAnother(original){
	var clone = create(original);//經過調用create函數建立一個新對象
	clone.sayHi = function(){//以某種方式來加強這個對象
		alert("hi");
	};
	return clone;//返回這個對象
}
複製代碼

直白點,所謂寄生式繼承也就是在其餘繼承方式(構造繼承、原型繼承等)上增長新的功能,返回新的對象。

寄生組合式繼承

前面講過,組合繼承是 JavaScript 最經常使用的繼承模式; 不過, 它也有本身的不足. 組合繼承最大的問題就是不管什麼狀況下,都會調用兩次父類構造函數: 一次是在建立子類型原型的時候, 另外一次是在子類型構造函數內部. 寄生組合式繼承就是爲了下降調用父類構造函數的開銷而出現的 .以下例:

function extend(subClass,superClass){
	var prototype = create(superClass.prototype);//建立對象
	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:使用了原型鏈後, 當查找一個對象的屬性時,JavaScript 會向上遍歷原型鏈,直到找到給定名稱的屬性爲止,到查找到達原型鏈的頂部 - 也就是 Object.prototype - 可是仍然沒有找到指定的屬性,就會返回 undefined. 此時若想避免原型鏈查找, 建議使用 hasOwnProperty 方法. 由於 hasOwnProperty 是 JavaScript 中惟一一個處理屬性可是不查找原型鏈的函數.以下:
console.log(instance1.hasOwnProperty('age'));//true
複製代碼
  • isPrototypeOf:對比而言isPrototypeOf 則是用來判斷該方法所屬的對象是否是參數的原型對象,是則返回true,不然返回false。
console.log(Father.prototype.isPrototypeOf(instance1));//true
複製代碼

instanceof && typeof

instanceof 運算符是用來在運行時指出對象是不是構造器的一個實例, 例如漏寫了new運算符去調用某個構造器, 此時構造器內部能夠經過 instanceof 來判斷.(java中功能相似)

function f(){
  if(this instanceof arguments.callee)
    console.log('此處做爲構造函數被調用');
  else
    console.log('此處做爲普通函數被調用');
}
f();//此處做爲普通函數被調用
new f();//此處做爲構造函數被調用
複製代碼

new運算符

new實質上作了三件事;

var obj  = {};
obj.__proto__ = F.prototype;
F.call(obj);  //執行
複製代碼

第一行,咱們建立了一個空對象obj; 第二行,咱們將這個空對象的__proto__成員指向了F函數對象prototype成員對象; 第三行,咱們將F函數對象的this指針替換成obj,而後再調用F函數.

咱們能夠這麼理解: 以 new 操做符調用構造函數的時候,函數內部實際上發生如下變化:

  1. 建立一個空對象,而且 this 變量引用該對象,同時還繼承了該函數的原型。
  2. 屬性和方法被加入到 this 引用的對象中。
  3. 新建立的對象由 this 所引用,而且最後隱式的返回 this。

關於原型繼承、構造函數繼承(經典繼承)裏對於數據的操做

在繼承關係裏,內部屬性數值變不變,數據共不共享前面也有所介紹,可是不夠具體。這塊時常使人迷惑,決定單獨拿出來說講:

首先在繼承關係裏,原型繼承與構造函數繼承能夠分紅兩個比較重要的繼承關係,其餘的繼承都是在這基礎上演變組合出來的,因此搞懂這兩個繼承關係中的數據變化,就差很少了。

在講區別以前咱們先兩個例子:

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);
複製代碼

打印結果:

可見,原型kk的數據並無改變,再看一個例子:

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);
複製代碼

打印結果:

你會發現:原型裏a沒變, k 變了。 對比上例,a始終沒變,k有所區別,到底是什麼緣由呢?

若是你的眼睛足夠雪亮,會一眼看出上例是構造函數繼承,下例是原型繼承,它兩的區別以前已經說過,構造函數繼承數據不會共享,而原型繼承會共享。因而你會說爲何a怎麼不變,你又在忽悠人,哼!哈哈哈,抱歉,有沒有看見a是基本數據類型,k是引用類型(引用類型包括:對象、數組、函數。)啊,基本數據類型是指針的指向區別,引用類型是地址的指向區別。不瞭解這塊能夠看看這篇文章:segmentfault.com/a/119000000…

使用權威指南6.2.2繼承那塊的一句話「若是容許屬性賦值操做,它也老是在原始對象上創造屬性或者對已有屬性賦值,而不會修改原型鏈,在JavaScript裏,只有查詢屬性才能感覺到繼承的存在,而設置屬性則與繼承無關」

如何理解這句話?我想是指繼承關係中屬性在自己內部找不到的時候纔會去原型裏找,只是借用屬性,可是並不會修改原型自己的屬性值,這也就解釋了基本數據類型始終不變的緣由。而原型繼承中因爲使用的同一原型對象,裏面的引用類型使用同一個地址,致使應用類型的數值是能夠變化的。

總結兩點:

  • 原型的基本數據類型不會受影響
  • 在原型繼承裏,引用類型的屬性會發生改變,在構造函數繼承中不會受影響(地址不一樣)

參考地址: juejin.im/post/58f94c…

相關文章
相關標籤/搜索