js原型和繼承

對js原型和繼承的理解一直處於「不懂-懂-不懂-懂-不懂。。。」的無限循環之中,原本打算只是簡單總結下js繼承方式,可看了些網上的資料後,發現又不懂繼承了。。。這篇文章只是一個閱讀筆記,總結了我所看到的js文章,參考見文末。javascript

1、原型與構造函數

一、prototype屬性

       Js全部的函數都有一個prototype屬性,這個屬性引用了一個對象,即原型對象,也簡稱原型。這個函數包括構造函數和普通函數,咱們講的更可能是構造函數的原型,可是也不可否定普通函數也有原型。譬如普通函數:html

function F(){};
alert(F.prototype instanceof Object);//true

默認狀況下,原型對象也會得到一個constructor屬性,該屬性包含一個指針,指向prototype屬性所在的函數java

Person.prototype.constructor === Person

在面向對象的語言中,咱們使用類來建立一個自定義對象。然而js中全部事物都是對象,那麼用什麼辦法來建立自定義對象呢?這就須要用到js的原型:
咱們能夠簡單的把prototype看作是一個模版,新建立的自定義對象都是這個模版(prototype)的一個拷貝 (實際上不是拷貝而是連接,只不過這種連接是不可見,新實例化的對象內部有一個看不見的__Proto__指針,指向原型對象)。es6

關於_proto_

_proto_是對[[propertyName]]屬性的實現(是隻能對象能夠擁有的屬性,而且是不可訪問的內部屬性),它指向對象的構造函數的原型對象。以下:segmentfault

function Person(name) {
    this.name = name;
}
var p1 = new Person();
p1._proto_ === Person.prototype;//true

__proto__只是瀏覽器的私有實現,目前ECMAScript標準實現方法是Object.getPrototypeOf(object),以下:瀏覽器

function Person(name) {
    this.name = name;
}
var p1 = new Person();
Object.getPrototypeOf(p1) === Person.prototype;//true

二、經過構造函數實例化對象過程

       構造函數,也即構造對象。首先了解下經過構造函數實例化對象的過程app

function A(x){
     this.x=x;
}
var obj = new A(1);

實例化obj對象有三步:函數

(1).建立obj對象:obj=new Object();
(2).將obj的內部__proto__指向構造他的函數A的prototype,同時obj.constructor===A.prototype.constructor(這個是永遠成立的,即便A.prototype再也不指向原來的A原型,也就是說:類的實例對象的constructor屬性永遠指向"構造函數"的prototype.constructor),從而使得obj.constructor.prototype指向A.prototype(obj.constructor.prototype===A.prototype,當A.prototype改變時則不成立,下文有遇到)obj.constructor.prototype與的內部_proto_是兩碼事,實例化對象時用的是_proto_,obj是沒有prototype屬性的,可是有內部的__proto__,經過__proto__來取得原型鏈上的原型屬性和原型方法,FireFox公開了__proto__,能夠在FireFox中alert(obj.__proto__);
(3).將obj做爲this去調用構造函數A,從而設置成員(即對象屬性和對象方法)並初始化。this

當這3步完成,這個obj對象就與構造函數A再無聯繫,這個時候即便構造函數A再加任何成員,都再也不影響已經實例化的obj對象了。此時,obj對象具備了x屬性,同時具備了構造函數A的原型對象的全部成員,固然,此時該原型對象是沒有成員的。prototype

三、原型對象

原型對象初始是空的,也就是沒有一個成員(即原型屬性和原型方法)。能夠經過以下方法驗證原型對象具備多少成員。

function A(x){
    this.x=x;
}

var num = 0;
for(o in A.prototype){
    alert(o);// alert 原型屬性名字
    num++;
} 
alert(num);//0

可是,一旦定義了原型屬性或原型方法,則全部經過該構造函數實例化出來的全部對象,都繼承了這些原型屬性和原型方法,這是經過內部的_proto_鏈來實現的。

例如:

A.prototype.say=function(){alert('haha');}

那全部的A的對象都具備了say方法,這個原型對象的say方法是惟一的副本給你們共享的,而不是每個對象都有關於say方法的一個副本。

2、原型與繼承

因爲js不像java那樣是真正面向對象的語言,js是基於對象的,它沒有類的概念。因此,要想實現繼承,能夠經過構造函數和原型的方式模擬實現類的功能。

js裏經常使用的以下兩種繼承方式:

原型鏈繼承(對象間的繼承)
    類式繼承(構造函數間的繼承)

如下經過實例詳細介紹這兩種繼承方式的原理

一、類式繼承詳解

類式繼承是在子類型構造函數的內部調用超類型的構造函數。

1    function A(x){
2        this.x = x;
3    }
4    function B(x,y){
5        this.tempObj = A;
6        this.tempObj(x);
7        delete this.tempObj;
8        this.y=y;
9    }

解釋
       第五、六、7行:建立臨時屬性tmpObj引用構造函數A,而後在B內部執行(注意,這裏執行函數的時候並無用new),執行完後刪除。當在B內部執行了this.x=x後(這裏的this是B的對象),B固然就擁有了x屬性,固然B的x屬性和A的x屬性二者是獨立,因此並不能算嚴格的繼承。第五、六、7行有更簡單的實現,就是經過call(apply)方法:A.call(this,x);
       這兩種方法都有將this傳遞到A的執行裏,this指向的是B的對象,這就是爲何不直接A(x)的緣由。這種繼承方式便是類繼承(js沒有類,這裏只是指構造函數),雖然繼承了A構造對象的全部屬性方法,可是不能繼承A的原型對象的成員。而要實現這個目的,就是在此基礎上再添加原型繼承。

二、原型鏈繼承詳解

原型式繼承是藉助已有的對象建立新的對象,將子類的原型指向父類,就至關於加入了父類這條原型鏈。

1    function A(x){
2        this.x = x;
3    }
4    A.prototype.a = 'a';
5    function B(x,y){
6        A.call(this,x);
7        this.y = y;
8    }
9    B.prototype.b1 = function(){
10        alert('b1');
11    }
12   B.prototype = new A();
13   B.prototype.b2 = function(){
14        alert('b2');
15    }
16   B.prototype.constructor = B;
17   var obj = new B(1,3);

解釋
       這個例子講的就是B繼承A。第7行類繼承:A.call(this.x);上面已講過。實現原型繼承的是第12行:B.prototype = new A();
       就是說把B的原型指向了A的1個實例對象,這個實例對象具備x屬性,爲undefined,還具備a屬性,值爲"a"。因此B原型也具備了這2個屬性(或者說,B和A創建了原型鏈,B是A的下級)。而由於方纔的類繼承,B的實例對象也具備了x屬性,也就是說obj對象有2個同名的x屬性,此時原型屬性x要讓位於實例對象屬性x,因此obj.x是1,而非undefined。第13行又定義了原型方法b2,因此B原型也具備了b2。雖然第9~11行設置了原型方法b1,可是你會發現第12行執行後,B原型再也不具備b1方法,也就是obj.b1是undefined。由於第12行使得B原型指向改變,原來具備b1的原型對象被拋棄,天然就沒有b1了。
       第12行執行完後,B原型(B.prototype)指向了A的實例對象,而A的實例對象的構造器是構造函數A,因此B.prototype.constructor就是構造對象A了(換句話說,A構造了B的原型)。alert(B.prototype.constructor)出來後就是"function A(x){...}" 。一樣地,obj.constructor也是A構造對象,alert(obj.constructor)出來後就是"function A(x){...}" ,也就是說B.prototype.constructor===obj.constructor(true),可是B.prototype===obj.constructor.prototype(false),由於前者是B的原型,具備成員:x,a,b2,後者是A的原型,具備成員:a。如何修正這個問題呢,就在第16行,將B原型的構造器從新指向了B構造函數,那麼B.prototype===obj.constructor.prototype(true),都具備成員:x,a,b2。
       若是沒有第16行,那是否是obj = new B(1,3)會去調用A構造函數實例化呢?答案是否認的,你會發現obj.y=3,因此仍然是調用的B構造函數實例化的。雖然obj.constructor===A(true),可是對於new B()的行爲來講,執行了上面所說的經過構造函數建立實例對象的3個步驟,第一步,建立空對象;第二步,obj.__proto__ === B.prototype,B.prototype是具備x,a,b2成員的,obj.constructor指向了B.prototype.constructor,即構造函數A;第三步,調用的構造函數B去設置和初始化成員,具備了屬性x,y。雖然不加16行不影響obj的屬性,但如上一段說,卻影響obj.constructor和obj.constructor.prototype。因此在使用了原型繼承後,要進行修正的操做。
       關於第十二、16行,總言之,第12行使得B原型繼承了A的原型對象的全部成員,可是也使得B的實例對象的構造器的原型指向了A原型,因此要經過第16行修正這個缺陷。

3、js繼承的6種方法

一、原型鏈繼承

爲了讓子類繼承父類的屬性(也包括方法),首先須要定義一個構造函數。而後,將父類的新實例賦值給構造函數的原型。

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

function Child(){
    this.age = 12;
}
Child.prototype = new Parent();//Child繼承Parent,經過原型,造成鏈條

var test = new Child();
alert(test.age);
alert(test.name);//獲得被繼承的屬性
//繼續原型鏈繼承
function Brother(){   //brother構造
    this.weight = 60;
}
Brother.prototype = new Child();//繼續原型鏈繼承
var brother = new Brother();
alert(brother.name);//繼承了Parent和Child,彈出mike
alert(brother.age);//彈出12

以上原型鏈繼承還缺乏一環,那就是Object,全部的構造函數都繼承自Object。而繼承Object是自動完成的,並不須要咱們本身手動繼承,那麼他們的從屬關係可使用操做符instanceof和函數isPrototypeOf()判斷,以下:

alert(test instanceof Parent);//true
alert(test instanceof Child);//true
alert(brother instanceof Parent);//true
alert(brother instanceof Child);//true
alert(Parent.prototype.isPrototypeOf(test));//true
alert(Child.prototype.isPrototypeOf(test));//true
alert(Parent.prototype.isPrototypeOf(brother));//true
alert(Child.prototype.isPrototypeof(brother));//true

問題字面量重寫原型會中斷關係,使用引用類型的原型,而且子類型還沒法給超類型傳遞參數。

僞類解決引用共享和超類型沒法傳參的問題,咱們能夠採用「借用構造函數」技術

二、借用構造函數繼承(類式繼承,這種方式能夠實現多繼承)

示例1

function Parent(firstname)
{
    this.fname=firstname;
    this.age=40;
    this.sayAge=function()
    {
        console.log(this.age);
    }
}
function Child(firstname)
{
    this.parent=Parent;
    this.parent(firstname);
    delete this.parent;//以上三行也能夠用call和apply函數改寫
    this.saySomeThing=function()
    {
        console.log(this.fname);
        this.sayAge();
    }
}
var mychild=new  Child("李");
mychild.saySomeThing();

示例2:用call函數實現

function Parent(firstname)
{
    this.fname=firstname;
    this.age=40;
    this.sayAge=function()
    {
        console.log(this.age);
    }
}
function Child(firstname)
{

    this.saySomeThing=function()
    {
        console.log(this.fname);
        this.sayAge();
    }
   this.getName=function()
   {
       return firstname;
   }

}
var child=new Child("張");
Parent.call(child,child.getName());
child.saySomeThing();

示例3:用apply函數實現

function Parent(firstname)
{
    this.fname=firstname;
    this.age=40;
    this.sayAge=function()
    {
        console.log(this.age);
    }
}
function Child(firstname)
{

    this.saySomeThing=function()
    {
        console.log(this.fname);
        this.sayAge();
    }
    this.getName=function()
    {
        return firstname;
    }

}
var child=new Child("張");
Parent.apply(child,[child.getName()]);
child.saySomeThing();

問題:類式繼承沒有原型,子類型只是繼承了父類型構造對象的屬性和方法,沒有繼承父類型原型對象的成員。

舉例說明:

function Father(x){
    this.x=x;
}

Father.prototype.say = function(){
    alert(this.x);
}

function Child(x,y){
    this.y=y;
    this.tempObj=Father;
    this.tempObj(x);
    delete this.tempObj;
}

var obj = new Child(1,2);
alert(obj.x);//1
alert(obj.y);//2
alert(obj.say);//undefined

三、組合繼承

組合繼承利用原型鏈+借用構造函數的模式解決了原型鏈繼承和類式繼承的問題。
示例

function Parent(age){
    this.name = ['mike','jack','smith'];
    this.age = age;
}
Parent.prototype.say = function(){
    return this.name + 'are both' + this.age;
}
function Child(age){
    Parent.call(this,age);
}
Child.prototype = new Parent();
var test = new Child(1);
alert(test.say());

組合式繼承是比較經常使用的一種繼承方法,其背後的思路是 使用原型鏈實現對原型屬性和方法的繼承,而經過借用構造函數來實現對實例屬性的繼承。這樣,既經過在原型上定義方法實現了函數複用,又保證每一個實例都有它本身的屬性。

問題:組合繼承的父類型在使用過程當中會被調用兩次;一次是建立子類型的時候,另外一次是在子類型構造函數的內部。
舉例說明以下:

function Parent(name){
        this.name = name;
        this.arr = ['哥哥','妹妹','父母'];
    }

    Parent.prototype.run = function () {
        return this.name;
    };

    function Child(name,age){
        Parent.call(this,age);//第二次調用
        this.age = age;
    }

    Child.prototype = new Parent();//第一次調用

這兩次調用一次是經過類式繼承實現的,另外一次是經過原型鏈繼承實現的,在上面原型鏈繼承詳解中有介紹。能夠用下面介紹的寄生組合繼承解決該問題。

四、原型式繼承

這種繼承藉助原型並基於已有的對象建立新對象。

1    function obj(o){
2        function F(){}
3        F.prototype = o;
4        return new F();
5    }
6     var person = {
7        name : 'ss',
8        arr : ['hh','kk','ll']
9    }

10    var b1 = obj(person);
11    alert(b1.name);//ss
12    b1.name = 'join';
13    alert(b1.name);//join

14    alert(b1.arr);//hh,kk,ll
15    b1.arr.push('gg');
16    alert(b1.arr);//hh,kk,ll,gg

17    var b2 = obj(person);
18    alert(b2.name);//ss
19    alert(b2.arr);//hh,kk,ll,gg

疑問:(求解答)
這裏的b1b2會共享原型對象person的屬性,那麼在12行經過b1.name='join'修改了原型對象personname屬性後,爲何並無影響到b2.name,在18行仍然輸出ss???
而在15行經過b1.arr.push('gg')修改了原型對象personarr屬性後,卻影響到了b2.arr,在19行的輸出多了gg

五、寄生式繼承

這種繼承方式是把原型式+工廠模式結合起來,目的是爲了封裝建立的過程。

function obj(o){
    function F(){}
    F.prototype = o;
    return new F()
}

function create(o){
    var f = obj(o);
    f.say = function() {
        return this.arr
    }
    return f
}

六、寄生組合繼承

寄生組合繼承解決了組合繼承中父類型兩次調用問題

function obj(o){
    function F(){}
    F.prototype = o;
    return new F()
}

function create(parent,child){//經過這個函數,實現了原型鏈繼承,但child只含有parent原型鏈中的屬性
    var f = obj(parent.prototype);
    f.constructor = child;
    child.prototype = f;
}

function Parent(name){
    this.name = name;
    this.arr = ['heheh','guagua','jiji'];
}

Parent.prototype.say = function(){
    return this.name
}

function Child(name,age){
    Parent.call(this,name);//類式繼承,這裏繼承parent構造函數中定義的屬性
    this.age = age;
}

create(Parent,Child);

var test = new Child('trigkit4',21);
test.arr.push('nephew');
alert(test.arr);//
alert(test.run());//只共享了方法
var test2 = new Child('jack',22);
alert(test2.arr);//引用問題解決

參考

1.js實現繼承的5中方式
2.ES6 Class
3.js繼承方式詳解
4.前段開發必須知道的js(一)原型和繼承
5.細說 Javascript 對象篇(二) : 原型對象
6.javascript原型概念(一)
7.Javascript基於 ‘__proto__’ 的原型鏈

相關文章
相關標籤/搜索