理解原型鏈和原型繼承

原型鏈

原型鏈比做用域鏈要好理解的多。程序員

JavaScript中的每一個對象,都有一個內置的_proto_屬性。這個屬性是編程不可見的(雖然ES6標準中開放了這個屬性,然而瀏覽器對這個屬性的可見性的支持不一樣),它其實是對另外一個對象或者null的引用。編程

當一個對象須要引用一個屬性時,JavaScript引擎首先會從這個對象自身的屬性表中尋找這個屬性標識,若是找到則進行相應讀寫操做,若沒有在自身的屬性表中找到,則在_proto_屬性引用的對象的屬性表中查找,如此往復,直到找到這個屬性或者_proto_屬性指向null爲止。瀏覽器

這個_proto_的引用鏈,被稱做原型鏈性能優化

<!--more-->app

注意,此處有一個性能優化的問題:往原型鏈越深處搜索,耗費的時間越多。函數

原型鏈和構造函數

JavaScript是一種面向對象的語言,而且能夠進行原型繼承性能

JavaScript中的函數有一個屬性prototype,這個prototype屬性是一個對象,它的一個屬性constructor引用該函數自身。即:優化

func.prototype.constructor === func; // ==> true

這個屬性有什麼用呢?咱們知道一個,一個函數使用new操做符調用時,會做爲構造函數返回一個新的對象。這個對象的_proto_屬性引用其構造函數的prototype屬性this

所以這個就不難理解了:prototype

var obj = new Func();

obj.constructor == Func; // ==> true

還有這個:

obj instanceof Func; // ==> true

也是經過查找原型鏈上的constructor屬性實現的。

構造函數生成的不一樣實例的_proto_屬性是對同一個prototype對象的引用。因此修改prototype對象會影響全部的實例。

「子類」繼承實現的幾種方式

之因此子類要加引號,是由於這裏說「類」的概念是不嚴謹的。JavaScript是一門面向對象的語言,可是它跟Java等語言不一樣,在ES6標準出爐以前,它是沒有類的定義的。

可是熟悉Java等語言的程序員,也但願使用JavaScript時,跟使用Java類似,經過類生成實例,經過子類複用代碼。那麼在ES6以前,怎麼作到像以下代碼同樣使用相似"類"的方式呢?

var parent = new Parent("Sam");

var child = new Children("Samson");

parent.say(); // ==> "Hello, Sam!"

child.say(); // ==> "Hello, Samson! hoo~~"

child instanceof Parent; // ==> true

咱們看到,這裏咱們把構造函數當作類來用。

如下咱們討論一下實現的幾種方式:

最簡單的方式

結合原型鏈的概念,咱們很容易就能寫出這樣的代碼:

function Parent(name){
    this.name = name;
}

Parent.prototype.say = function(){
    console.log("Hello, " + this.name + "!");
}

function Children(name){
    this.name = name;
}

Children.prototype = new Parent();

Children.prototype.say = function(){
    console.log("Hello, " + this.name + "! hoo~~");
}

這個方式缺點很明顯:做爲子類的構造函數須要依賴一個父類的對象。這個對象中的屬性name根本毫無用處。

第一次改進

// ...
Children.prototype = Parent.prototype;
// ...

這樣就不會產生無用的父類屬性了。

然而,這樣的話子類和父類的原型就引用了同一個對象,修改子類的prototype也會影響父類的原型。

這時候咱們發現:

parent.say(); // ==> "Hello,Sam! hoo~~"

這第一次改進還不如不改。

第二次改進——臨時構造函數/Object.create

function F(){  // empty  
}

F.prototype = Parent.prototype;

Children.prototype = new F();

// ...

parent.say(); // ==> "Hello, Sam!"
child.say();  // ==> "Hello, Samson! hoo~~"

這樣一來,修改子類的原型只是修改了F的一個實例的屬性,並無改變Parent.prototype,從而解決了上面的問題。

在ES5的時代,咱們還能夠直接這樣:

Children.prototype = Object.create(Parent.prototype);

這裏的思路是同樣的,都是讓子類的prototype不直接引用父類prototype。目前的現代瀏覽器幾乎已經添加了對這個方法的支持。(但咱們下面會仍以臨時構造函數爲基礎)

可是細細思考,這個方案仍有須要優化的地方。例如:如何讓父類的構造函數邏輯直接運用到子類中,而不是再從新寫一遍同樣的?這個例子中只有一個name屬性的初始化,那麼假設有不少屬性且邏輯同樣的話,豈不是沒有作到代碼重用?

第三次改進——構造函數方法借用

使用apply,實現「方法重用」的思想。

function Children(name){
    Parent.apply(this, arguments);
    // do other initial things
}

「聖盃」模式

如今完整的代碼以下:

function Parent(name){
    this.name = name;
}

Parent.prototype.say = function(){
    console.log("Hello, " + this.name + "!");
}

function Children(name){
    Parent.apply(this, arguments);
    // do other initial things
}

function F(){  // empty  
}

F.prototype = Parent.prototype;

Child.prototype = new F();

Children.prototype.say = function(){
    console.log("Hello, " + this.name + "! hoo~~");
}

這就是所謂「聖盃」模式,聽着很高大上吧?

以上就是ES3的時代,咱們用來實現原型繼承的一個近似最佳實踐。

「聖盃」模式的問題

「聖盃」模式依然存在一個問題:雖然父類和子類實例的繼承的prototype對象不是同一個實例,可是這兩個prototype對象上面的屬性引用了一樣的對象。

假設咱們有:

Parent.prototype.a = { x: 1};
// ...

那麼即便是「聖盃」模式下,依然會有這樣的問題:

parent.x // ==> 1
child.x  // ==> 1

child.x = 2;
parent.x // ==>2

問題在於,JavaScript的拷貝不是 深拷貝(deepclone)

要解決這個問題,咱們能夠利用屬性遞歸遍歷,本身實現一個深拷貝的方法。這個方法在這裏我就不寫了。

ES6來了

ES6極大的支持了工程化,它的標準讓瀏覽器內部實現類和類的繼承:

class Parent {
    constructor(name) { //構造函數
          this.name = name;
    }
    say() {
          console.log("Hello, " + this.name + "!");
    }
}

class Children extends Parent {
    constructor(name) { //構造函數
        super(name);    //調用父類構造函數
        // ...
    }
    say() {
          console.log("Hello, " + this.name + "! hoo~~");
    }
}

如今瀏覽器對其支持程度還不高。可是這種寫法的確省力很多。讓咱們對將來拭目以待。

相關文章
相關標籤/搜索