javascript中的對象,原型,原型鏈和麪向對象

1、javascript中的屬性、方法   javascript

  1.首先,關於javascript中的函數/「方法」,說明兩點: java

  1)若是訪問的對象屬性是一個函數,有些開發者容易認爲該函數屬於這個對象,所以把「屬性訪問」叫作「方法訪問」,而實際上,函數永遠不會屬於一個對象,對象擁有的,只是函數的引用。確實,有些函數體內部使用到了this引用,有時候這些this確實會指向調用位置的對象引用,可是這種用法從本質上並無把一個函數變成一個方法,只是發生了this綁定罷了。所以,若是屬性訪問返回的是一個函數,那它也並非一個方法,屬性訪問返回的函數和其餘函數並無任何區別,只是有時候會發生隱式的this綁定罷了 c++

  2)javascript中很難肯定「複製」函數究竟意味着什麼。事實上,在javascript中,函數沒法(用標準、可靠的方法)真正地複製,可以複製的只是函數的引用。算法

  2.對象的原型[[prototype]]鏈與函數的原型chrome

  原型鏈:對象[[prototype]]l鏈是一種機制,是對象中的一個內部連接引用另外一個對象。編程

  原型:函數.prototype指向的就是一個對象,叫作函數的原型對象。設計模式

  A.對象原型鏈瀏覽器

  javascript對象有一個特殊的[[prototype]]內置屬性,其實就是對於其餘對象的引用,幾乎全部的對象在建立時[[prototype]]的屬性都會被賦予一個非空的值(除了object.create(null)).全部普通的[[prototype]]鏈最終都會指向內置的object.prototype。裏面包含了不少常見的功能,諸如.toString(),.valueOf(),.hasOwnProperty(),.isPrototypeOf();函數式編程

  原型鏈本質上是屬性查找使用的。myObject.a不只僅是在myObject中查找名字爲a的屬性。在語言規範中,myObject.a其實是執行了[[Get]]操做。對象默認的[[Get]]操做首先在對象中查找是否有名稱相同的屬性,若是找到就返回這個屬性的值。若是沒有找到,按照[[Get]]算法的定義會遍歷對象的原型鏈(prototype鏈),直到找到匹配的屬性名或者查找完整條[[prototype]]鏈,若是是後者的話,[[Get]]操做的返回值是undefined.函數

  在javascript中,給一個對象設置屬性如myObject.foo='bar',不只僅是添加一個新屬性或者修改已有的屬性,,完整的流程以下:

  1)若是對象中包含名爲foo的普通屬性,不論上層原型鏈存不存在,這條賦值語句會直接修改myObject已有的屬性名,此時會發生屏蔽;

  2)若是對象myObject中不包含名爲foo的屬性,[[prototype]]鏈就會遍歷,若是原型鏈上找不到foo,foo就會被直接添加到myObject上。

  3)若是myOjbect中不包含foo這個屬性,但foo存在於原型鏈的上層,賦值語句myObjec.foo的行爲會有些不一樣,具體以下:

    a.若是[[prototype]]鏈上層名爲foo的普通數據訪問屬性沒有被標記爲只讀(writable,false),那就會直接在myObject添加一個名爲foo的新屬性,它是屏蔽屬性;

    b.若是[[prototype]]鏈上層存在foo,且被標記爲只讀,那麼沒法修改已有屬性或者在myObject上面建立屏蔽屬性,在費嚴格模式下,這條賦值語句會被忽略。

    c.若是[[prototype]]鏈上層存在foo而且它是一個setter,那麼必定會調用這個setter,foo不會被添加到myObject上,也不會從新定義foo這個setter。

  可見只有在myObject中包含foo屬性,或者myObject與原型鏈都不包含foo屬性,或者myObject不包含foo屬性,原型鏈的foo屬性沒有被標記爲只讀的狀況下,纔會發生屏蔽。若是在上面b,c兩種狀況下也但願屏蔽foo,不能使用=操做符,而是使用object.defineProperty來向myObject添加屬性foo。

  B:函數原型

  對象原型鏈[[prototype]]與函數原型prototype,這兩個是大相徑庭的事物,雖然兩者存在必定的關聯。對象的原型鏈[[prototype]]如上所述,是對象的一個隱藏屬性,用於屬性查找。在chrome等瀏覽器的實現中,能夠經過_proto_訪問到。

  函數做爲對象,天然也有內置的隱藏屬性[[prototype]],其原型鏈的終點指向Function.prototype,而Function.prototype又指向Object.prototype.

  此外,任何函數好比function Foo()默認都有一個特殊的顯式屬性prototype,(String,Number,Object,Function這些所謂的子類型說白了就是一些內置函數,所以都有prototype屬性)。函數的prototype屬性是顯示的,它指向一個對象,這個對象一般稱之爲函數原型。

  那麼函數原型這個對象到底是什麼呢?

  最直接的解釋就是:這個對象時在調用a=new Foo()時建立的,執行這句話同時令新建立的對象a,其a.[[prototype]]連接到這個Foo.prototype所指向的原型對象。若是屢次調用new Foo(),那麼他們的[[prototype]]關聯的是同一個對象,都是Foo.prototype所指向的對象。可見:1)函數做爲對象,其Foo.[[prototype]]是不連接到Foo.prototype的,而是new 調用建立的對象鏈接到Foo.prototype指向的原型對象;2)new調用會在新建立的對象和函數原型之間建立關聯,這個關聯不存在於對象和構造函數之間,只是上述代碼會同時爲Foo.prototype添加construnctor屬性,該屬性指向「構造函數「Foo」(再次強調,javascript沒有構造函數,只有函數的構造調用),對象經過原型鏈也能訪問construnctor屬性,不過這除了營造出相似「構造對象」的假象,貌似用處不大!!!

  實際上,new Foo()這個函數調用實際上並無直接建立關聯,這個關聯只是一個意外的反作用。new Foo()只是間接完成了咱們的目的:一個關聯到其餘對象的新對象。那麼有沒有更直接的方法作到這一點呢?固然,那就是Object.create(...)!

  var bar=Object.create(foo)會建立一個新對象(bar),並把其[[prototype]]關聯到指定對象(foo)。這樣就能夠充分發揮[[prototype]]委託的威力而避免沒必要要的麻煩(好比使用new 構造函數會生成.prototype和.constuctor的引用)

  Object.create(null),會建立一個空[[prototype]]鏈的對象,這個對象沒法進行委託。所以不會受到原型鏈的干擾,很是適合用於存儲數據。

  在ES5以前的Object.create()的polyfill的代碼:   

  

if(!Object.create){
    Object.create=function(obj){
        function Foo(){};
        Foo.prototype=obj;
        return new Foo();
    };   
}

 

1、javascript中所謂「類」

  類說白了只是一種設計模式(模板模式),在編程尤爲是函數式編程中,類並非必須的編程基礎,只是一種可選的代碼抽象。只不過在有些語言如java中,並不給你選擇的機會,在c/c++中,會提供過程化和麪向類這兩種語法。類自己僅僅是一種抽象的表示,須要實例化才能對它進行操做。類經過複製操做被實例化爲對象形式,繼承操做也相似,子類會包含父類行爲的複製後的原始副本,所以子類能夠重寫全部的繼承行爲或者新行爲而不會影響到父類。可見,在面向類的語言中,類的繼承,類的實例化本質上是複製。

  javascript屬於哪種呢?事實上,javascript擁有一些近似類的語法,但在近似類的表象之下,javascript的機制和類徹底不一樣。在類的繼承以及實例化時,javascript的對象機制並不會自動執行復制行爲,而是經過原型鏈關聯到實際的父類屬性,看起來彷佛其餘語言「繼承」的是方法的簽名,而javascript繼承的是實際的方法。

  記住:javascript中只有對象,並不存在能夠實例化的「類」,一個對象並不會被複制到其餘對象中,他們只會被「關聯」起來

  一樣,在面向類的語言中,繼承、實例化着複製操做。就實例化來講,javascript不存在將類實例化爲對象的說法,javascipt中原本就只有對象;就繼承來講,javascript只會在兩個對象之間建立一個關聯,這樣一個對象就能夠經過委託訪問另外一個對象的屬性和函數。「委託」這個術語比「繼承」更準確地描述Javascipt中對象的關聯機制!

  不過,爲了使得javascript表現出和其餘語言相似的複製行爲(不論繼承仍是實例化,本質都是複製),javscript開發者想出了不少方法來模擬類的複製行爲:如混入,寄生繼承和基於原型的繼承。  

2、在javascript中模擬類的實現

  1.混入

  包括在jQuery源代碼中,模擬其餘語言的類複製行爲,這種方法就是「混入」(mixin)。

  手動實現的mixin(在不少庫中也叫作extends)代碼以下:

function mixin(sourceObj,targetObj){
    for(var key in sourceObj){
        if(!(key in targetObj)){
            targetObj[key]=sourceObj[key];
        }
    }
}

  

  1)這不可以解決javascript中的顯式僞多態問題,在javascript中調用相似父類中的同名方法沒有super這樣的用法(相對多態),只能使用絕對引用父類名.方法名.call,(顯式僞多態)。這樣的後果是,在支持多態的面向類的語言中,子類和父類的關係只須要在類定義的開頭建立便可,所以只須要在這一個地方維護兩個類的聯繫。然而javascript的顯示僞多態會在全部須要使用多態的地方引入一個函數關聯,於是增長了代碼的維護成本。

  2)混入也沒法徹底模擬類的複製行爲,由於對象(和函數)只能複製引用,沒法複製被引用的對象或函數自己。

  2.寄生式繼承

  寄生式繼承的主要推廣者是Doulgas Crockford.寄生式繼承的主要思路是建立一個用於封裝繼承過程的函數,該函數在內部以某種方式加強對象,最後返回這個對象。  

 

function SuperType(name){
    this.name=name;
}
SuperType.prototype.sayName=function(){
    return this.name;
};

//寄生式繼承
function SubType(name,age){
    var instance=new SuperType(name);//實際上寄生式繼承中,這裏不必定非得是new,凡是返回一個對象的操做均可以
    var sayName=instance.sayName;
    instance.age=age;
    //方法重寫
    instance.sayName=function(){    
        var name=sayName.call(this);
        console.log('welcome '+name);        
    };
    instance.sayAge=function(){
        console.log(this.age);
    }
    return instance;
}
 
//測試
//事實上,使用new時候會建立一個對象,可是因爲咱們返回了Instance這個對象,new建立的這個對象會被忽略,所以下面的代碼加不加new實質上是同樣的
var subInstance=new SubType('bobo',28);
subInstance.sayName();//輸出welcome bobo
subInstance.sayAge();//輸出28

 

  寄生式繼承的主要問題之一是不能複用函數,在上面的例子中,若是調用兩次SubType()[或者調用new SubType()達到的效果是一致的],那麼返回的兩個SubType「實例」各自擁有本身的一套函數。此外,引用父類的方法,須要首先將方法的引用賦值給對應變量,如上面的代碼所示。

 

  3.基於原型的繼承

   基於原型的繼承,其思路是使用原型鏈實現對原型屬性和方法的繼承,借用構造函數實現對實例屬性的繼承,這也是被廣泛使用的一種方法。下面是一個案例:  

//基於原型的繼承
function SuperType(name){
    this.name=name;
}
SuperType.prototype.getName=function(){
    return this.name;
};

function SubType(name,age){
    SuperType.call(this,name);
    this.age=age;
}
SubType.prototype=Object.create(SuperType.prototype);
SubType.prototype.sayName=function(){
    //調用this的方法
    var name=this.getName();
    console.log('welcome '+name);
}
SubType.prototype.sayAge=function(){
    console.log(this.age);
}

//測試
var subInstance=new SubType('bobo',28);
subInstance.sayName();//輸出welcome bobo
subInstance.sayAge();//輸出28

  

上面代碼最核心的部分就是:

  SubType.prototype=Object.create(SuperType.prototype).這句話調用會建立一個新對象SubType.prototype,並將其內部的[[prototype]]關聯到指定對象,本例中是SuperType.prototype.

  注意,有兩種常見的錯誤,實際上他們都有一些問題:  

//達不到想要的效果
SubType.prototype=Foo.prototype;
//基本達到需求,但存在一些反作用
SubType.prototype=new SuperType();

 

 

第一種,SubType.prototype=SuperType.prototype並不會建立一個關聯到Foo.prototype的新對象,而只是讓SubType.prototype直接引用SuperType.prototype,所以執行相似SubType.prototype.xxx的賦值語句的時候,將直接修改Foo.prototype對象自己。實際上這樣不是你想要的效果,由於此時根本不須要SubType對象,直接使用SuperType就能夠了,代碼還能夠更簡單。

第二種:SubType.prototype=new SuperType()的確會建立關聯到SuperType.protoType的新對象。但使用了SuperType的構造函數調用,若是函數SuperType有一些反作用(好比寫日誌,註冊到其餘對象等等),就會影響到SubType()的後代,形成不可預知的後果。其次是調用了兩次SuperType這個函數。第一次是在子類構造函數的內部,經過調用父類的SuperType爲子類的對象添加name屬性;第二次是在設置子類原型鏈prototype的地方,對SuperType實行了構造調用,這會致使子類的原型鏈中也存在一個name屬性(而且值爲undefined),只不過因爲屬性屏蔽,子類實例對象中的name屬性屏蔽了其原型鏈中的name屬性b罷了。

3、javascipt中的「類」關係(稱爲內省或者反射)

假設有對象a,如何尋找a委託的對象呢?

1)站在「類」的角度來判斷:

   a instanceof Foo;

instanceof回答的問題是,在a的整條[[prototype]]鏈中是否有指向Foo.prototype的對象?其左操做符是一個對象,右操做符是一個函數。不能兩個對象(好比a和b)是否經過[[prototype]]相互關聯。

2)更簡潔的判斷[[prototype]]反射的方法

  Foo.prototype.isPrototypeof(a)

isPrototypeof一樣可以回答上述問題:在a的整條[[prototype]]鏈中,是否出現過Foo.prototype?

一樣的問題,一樣的答案,但在第二種方法中並不須要訪問函數Foo,只須要兩個對象就能夠判斷他們之間的關係,

 如:b.isPrototypeOf(c)

4、面向委託的設計

記住!javascript中只有對象,並不存在能夠實例化的「類」,上述任何模擬類的方法都顯得不三不四。對象直接定義本身的行爲便可!!!

咱們不須要類來建立兩個對象的關係,只須要經過委託來關聯對象就足夠了。從如今開始,儘可能拋棄全部的function ,new (),.prototype的寫法!!

出於各類緣由,以「父類」,「子類」,「繼承」,「多態」結束的屬於(包括原型繼承)和其餘面向對象的術語都沒法幫助理解javascript的真實機制!相比之下,「委託」是一個更合適的描述,委託行爲意味着某些對象在找不到屬性或者方法引用時會把這個請求委託給另外一個對象,javascript對象之間的關係是委託而不是複製!

 

下面以具體案例爲例,對比基於類的寫法和基於委託的寫法兩者的不一樣:

1.基於類的寫法

 

//基於類的寫法
function SuperType(name){
    this.name=name;
}
SuperType.prototype.getName=function(){
    return this.name;
}

function SubType(name,age){
    SuperType.call(this,name);
    this.age=age;
}
SubType.prototype=Object.create(SuperType.prototype);
SubType.prototype.getAge=function(){
    return this.age;
};
SubType.prototype.sayHello=function(){
    var name=this.getName();
    return 'hello '+name;
};


//測試
var instance=new SubType('bobo',28);
console.log(instance.sayHello());//輸出hello bobo
console.log(instance.getAge());//輸出28

 

2.基於委託的寫法

//採用基於委託的寫法
var superObj={
    init:function(name){
        this.name=name;
    },
    getName:function(){
        return this.name;
    }
};
var subObj=Object.create(superObj);
//不能像下面這麼寫了
//對象字面量的寫法會建立一個新對象賦值給subObj,原來的關聯就不存在了
// subObj={
//     setup:function(name,age){
//         this.init(name);
//         this.age=age;
//     },
//     sayHello:function(){
//         var name=this.getName();
//         return 'hello '+name;
//     },
//     getAge:function(){
//         return this.age;
//     }

// };
 
//只能這麼寫
subObj.setup=function(name,age){
    this.init(name);
    this.age=age;
};
subObj.sayHello=function(){
    return 'hello '+this.name;
};
subObj.getAge=function(){
    return this.age;
};
var b1=Object.create(subObj);
b1.setup('bobo',28);
console.log(b1.sayHello()); //hello bobo
console.log(b1.getAge()); //28

var b2=Object.create(subObj);
b2.setup('leishao',27);
console.log(b2.sayHello()); //hello leishao
console.log(b2.getAge()); //27

對比兩種寫法:

1)基於委託的寫法中不會出現function構造函數,new,prototype,有的只是對象和對象之間的關聯;

2)基於類的寫法,子類一般和父類取相同的方法名來實現重寫的效果,而在基於委託的寫法中,則須要儘可能避免這種方法,避免重名在原型鏈查找中引發不可預測的後果。

3)基於類的寫法中,屬性通常在構造函數中聲明,建立對象和對象初始化是在一次構造函數的調用中一次性完成的(var instance=new SuperType('bobo',28));然而在基於委託的寫法中,建立對象和對象初始化分開,分紅了兩次(var b1=Object.crate(subObj);b1.setup('bobo',28)),屬性通常也在初始化函數中定義。這樣當然多謝了代碼,但同時也增長了靈活性,能夠根據須要讓他們出如今不一樣的地方。

相關文章
相關標籤/搜索