js面向對象及原型繼承學習筆記。

建立對象

雖然Object構造函數或對象字面量均可以用來建立單個對象,可是這些方式有明顯的缺點:使用同一個接口建立不少對象,會產生大量的重複代碼。爲解決這個問題,人們開始使用工廠模式的一種變體。設計模式

工廠模式

工廠模式是軟件工程領域一種廣爲人知的設計模式,這種模式抽象了建立具體對象的過程。考慮到ECMAScript中沒法建立類,開發人員就發明了一種函數,用函數來封裝以特定接口建立對象的細節。數組

function createPerson(name,age,job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName=function(){
        console.log(this.name)
    }
    return o;
}
var person1 = createPerson('Nicholas',29,'SoftWare');
var person2 = createPerson('gres',27,'Doctor');

函數cretePerson()可以根據接受的參數來構建一個包含全部必要信息的Person對象。能夠無數次的調用這個函數,而每次它都會返回一個包含三個屬性一個方法的對象。工廠模式雖然解決了建立多個類似對象的問題,可是卻沒有解決對象識別問題。瀏覽器

構造函數模式

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.sayName = function(){
        console.log(this.name);
    }
}
var person1 = new Person('Nicholas',29,'Software');
var person2 = new Person('gres',24,'Docotor');

在這個例子中,Person()函數取代了createPerson()函數。不一樣之處:app

1.沒有顯式的建立對象。
2.直接將屬性和方法賦給了this對象。
3.沒有return語句。

要建立Person實例必須使用new操做符。以這種方式調用構造函數實際上會經歷如下4個步驟:函數

1.建立一個對象。
2.將構造函數的做用域賦值給新的對象(所以this指向了這個新對象)。
3.執行構造函數中的代碼。(爲這個新對象添加屬性)。
4.返回新的對象。

上面的例子:person1和person2分別保存着Person的一個不一樣的實例。 這兩個對象都有一個constructor屬性,該屬性指向Person。this

person1.constructor == Person//true
person2.constructor == Person//true

person1 instanceof Object;//true
person1 instanceof Person;//true

建立自定義的構造函數意味着未來能夠將它的實例標示爲一種特定的類型;而這正是構造模式賽過工廠模式的地方。(以這種方式定義的構造函數是定義在Global對象(在瀏覽器是window對象)中的)。
構造函數與其它函數的惟一區別,就在於調用它們的方式不一樣。不過,構造函數畢竟也是函數,不存在定義構造函數的特殊語法。任何函數,只要經過new操做符來調用,那他就能夠做爲構造函數;任何函數,若是不經過new運算符調用,那它就跟普通函數沒有區別。
構造函數的問題:prototype

使用構造函數的主要問題就是每一個方法都要在每一個實例上從新建立一遍。在上面的例子中,person1和person2都有一個名爲sayName()的方法,但那兩個方法不是同一個Function的實例。在js中函數也是對象,所以每定義一個函數,也就是實例化了一個對象。
function Person(name, age){
    this.name = name;
    this.age = age;
    this.sayName = new Function(console.log(this.name))
}

從這個角度上看構造函數,更容易明白每一個Perosn實例都包含一個不一樣的Function實例的本質。說明白些,以這種方式建立函數,會致使不一樣的做用域鏈和標識符解析,可是建立Function新實例的機制仍然相同。所以不一樣實例上的同名函數是不相等的。設計

console.log(person1.sayName==person2.sayName);//false.

然而,建立兩個完成一樣任務的Function實例沒有必要;何況有this對象在,根本不用在執行代碼前就把函數綁定到特定對象上面。所以,能夠向下面這樣,經過把函數定義轉移到構造函數外部來解決這個問題。指針

function Person(name,age){
    this.name = name;
    this.age = age;
    this.sayName = sayName;
}
function sayName(){
    console.log(this.name)
}
var person1 = new Person('ll',24);
var person2 = new Person('kk',25);

原型模式

咱們建立的每個函數都有一個prototype屬性,這個屬性是一個指針,指向一個對象,而這個對象的用途是包含能夠有特定類型的全部實例共享的屬性和方法。code

function Person(){}
Person.prototype.name = 'll';
Person.prototype.age = 24;
Person.prototype.sayName=function(){
    console.log(this.name)
}
var person1 = new Person();
var person2 = new Person();

在此咱們將sayName()方法和屬性直接添加到Person的prototype屬性中,構造函數變成了空函數。即便如此,也仍然能夠經過調用構造函數來建立新的對象,並且新的對象還會具備相同的屬性和方法。但與構造函數模式不一樣的是,新對象的屬性和方法是由全部實例共享的。
理解原型對象:

不管何時,只要建立了一個新函數,就會根據必定的規則爲該函數建立一個prototype屬性,這個屬性指向函數的原型對象。在默認狀況下,全部原型對象都會自動得到一個constructor屬性,這個屬性是一個指向prototype屬性所在函數的指針。Person.prototype.constructor指向Person。而經過這個構造函數,咱們還能夠繼續爲原型對象建立其它屬性和方法。
建立了自定義構造函數以後,其原型對象默認只會獲得constructor屬性;至於其它屬性和方法都是從Object對象繼承而來的。當調用構造函數的一個新實例後,該實例內部將包含一個指針,指向構造函數的原型對象。(__proto__);person1和person2都包含一個內部屬性,該屬性僅僅指向了Person.prototype,和構造函數沒有直接的關係。
每當代碼讀取某個對象的某個屬性時,都會執行一次搜索,目標是具備給定名字的屬性,搜索首先從對象實例自己開始,若是在實例中找到了具備給定名字的屬性,則返回該屬性的值;若是沒有找到,則繼續搜索指針指向的原型對象,若是在原型對象中找到了該屬性,則返回該屬性的值。
function Person(){}
Person.prototype.name = 'll';
Person.prototype.age = 24;
Person.prototype.sayName=function(){
    console.log(this.name)
}
var person1 = new Person();
var person2 = new Person();
person1.name = 'kk';
console.log(person1.name)//kk-來自實例
console.log(person2.name)//ll來自原型

當對象實例添加一個屬性時,這個屬性就會屏蔽原型對象中保存的同名屬性。

原型模式的缺點:

原型中全部屬性是被不少實例共享的,這種共享對於函數很是合適。對於那些包含基本值的屬性也說的過去,經過在實例上添加一個同名屬性能夠隱藏原型中對應的屬性。然而對於包含引用類型值的屬性來講,問題就比較突出了:
function Person(){}
Person.prototype={
    constructor:Person,
    name:'kk',
    age:24,
    friends:['ll','jj'],
    sayName:function(){
        console.log(this.name);
    }
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push('aa');
console.log(person1.friends);//ll jj aa
console.log(person2.friends);//ll jj aa
console.log(person1.friends==person2.friends);//true

修改person1.friends引用的數組,person2一樣會修改。

組合使用構造函數和原型模式

建立自定義類型的最多見方式,就是組合使用構造函數和原型模式。構造函數模式用於定義實例屬性,原型模式用於定義共享的屬性和方法。另外這種模式還支持向構造函數穿參數。

function Person(name,age){
    this.name = name;
    this.age = age;
    this.friends=['kk','ll'];
}
Person.prototype={
    constructor:Person,
    sayName:function(){
        console.log(this.name)
    }
}
var person1 = new Person('nnn',24);
var person2 = new Person('mmm',29);
person1.friends.push('aaa');
console.log(person1.friends);//kk ll aa
console.log(person2.friends);//kk ll 
console.log(person1.friends==person2.friends);//false
console.log(person1.sayName==person2.sayName);//true

寄生構造函數模式

function Person(name,age,job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName=function(){
        console.log(this.name);
    }
    return o;
}
var friend = new Person('kk',24,'software');

在這個實例中,Person函數建立了一個新對象,並以相應的屬性和方法初始化該對象,而後又返回了這個對象。除了使用new操做符並把使用的包裝函數叫作構造函數以外,這與工廠模式沒有什麼區別。構造函數在不返回值的狀況下,默認會返回新對象實例。而經過在構造函數末尾添加一個return語句,能夠重寫調用構造函數時返回的值。
這個模式能夠在特殊的狀況下用來爲對象建立構造函數。假設咱們想建立一個具備額外方法的特殊數組。因爲不能直接修改Array的構造函數,所以可使用這個模式:

function SpecialArray(){
    console.log(this)
    var values = new Array();
    values.push.apply(values,arguments);
    values.toPipedString=function(){
        return this.join('|')
    }
    return values;
}
var colors = new SpecialArray('red','blue','pink');
console.log(colors.toPipedString())

關於寄生構造函數模式,返回的對象與構造函數或則構造函數的原型屬性之間沒有關係。

穩妥構造函數模式

所謂穩妥對象,指的是沒有公共屬性,而其方法也不引用this的對象。穩妥構造函數模式與寄生構造函數模式相似,可是有兩點不一樣,一是建立對象的實例方法不引用this,二是不使用new操做符調用構造函數。

function Person(name,age,job){
    //建立要返回的對象
    var o = new Object();
    //能夠在這裏定義私有變量和屬性
    //添加方法
    o.sayName=function(){
        console.log(name);
    }
    //返回對象
    return o;
}
var friend = Person('kk',24,'software');
friend.sayName();//kk

繼承

每一個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。那麼,假如咱們讓原型對象等於另外一個類型的實例,此時的原型對象將包含一個指向另外一個原型的指針,相應的,另外一個原型中也包含着一個指向另外一個構造函數的指針。

function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
}
function SubType(){
    this.subProperty = true;
}
SubType.prototype = new SuperType();
console.log(SubType.prototype)
SubType.prototype.getSubValue = function(){
    return this.subProperty;
}
var instance = new SubType();
console.log(instance)

以上代碼定義了兩個類型:SuperType和SubType。每一個類型分別有一個屬性和一個方法。它們的主要區別在於SubType繼承了SuperType,而繼承是經過建立SuperType的實例,並將該實例賦值給SubType.prototype實現的。實現的本質是重寫原型對象,代之以一個新類型的實例。
在上面的代碼中,咱們沒有使用Subtype默認提供的原型,而是給它換了一個新的原型:這個新的原型就是SuperType的實例。因而新原型不只具備SuperType的實例所擁有的所有屬性和方法,並且其內部還有一個指針,指向了SuperType的原型。
原型鏈的問題:

1.引用類型值的原型屬性會被全部實例共享。
2.在建立子類型的實例時,不能向超類型的構造函數中傳遞參數。

借用構造函數

在解決原型中包含引用類型值所帶來問題的過程當中,開發人員開始使用一種叫作借用構造函數的技術。這種技術的思想至關簡單,即在子類構造函數的內部調用超類型構造函數。

function SuperType(){
    this.colors = ['red','blue','pink']
}
function SubType(){
    //繼承了SuperType
    SuperType.call(this);
}
var instance = new SubType();
instance.colors.push('black');
console.log(instance.colors);//red blue pink black
var instance2 = new SubType();
console.log(instance2.colors);//red blue pink

同時,借用構造函數有一個很大的優點,便可以在子類構造函數中向超類型構造函數傳遞參數。

function SuperType(name){
    this.colors = ['red','blue','pink'];
    this.name = name;
}
function SubType(){
    //繼承了SuperType
    SuperType.call(this,'kk');
}
var instance = new SubType();
console.log(instance.name);//kk

借用構造函數的問題:

方法都須要在構造函數中定義,沒法複用。

組合繼承

function SuperType(name){
    this.name = name;
    this.colors=['red','blue','pink']
}
SuperType.prototype.sayName=function(){
    console.log(this.name);
}
function SubType(name,age){
    //繼承屬性
    SuperType.call(this,name);
    this.age = age;
}
//繼承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge=function(){
    console.log(this.age);
}
var instance1 = new SubType('kk',24);
instance1.colors.push('black');
console.log(instance1.colors);//red blue pink black
instance1.sayName();//kk
instance1.sayAge();//24

var instance2 = new SubType('ll',26);
console.log(instance2.colors);//red blue pink
instance2.sayAge();//26
instance2.sayName();//ll

組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優勢。

原型式繼承

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

在object()函數內部,先建立了一個臨時性的構造函數,而後將傳入的對象做爲這個構造函數的原型,而後返回了這個臨時類型的一個實例。從本質上講object()對傳入中的對象進行了淺複製。

var Person={
    name:'kk',
    friends:['ll','aa','cc']
}
var instance1 = object(Person);
instance1.name = 'Greg';
instance1.friends.push('dd');
console.log(instance1)

var instance2 = object(Person);
instance2.name = 'Linda';
instance2.friends.push('oo');
console.log(instance2)

obejct()會返回一個新對象,這個對象將Person做爲原型,因此它的原型中包含一個基本類型值屬性和一個引用類型值屬性。這就意味着friends不只是Person的,也同時被instance1和instance2共享。

寄生式繼承

寄生式繼承思路和寄生式構造函數和工廠模式相似,即建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來加強對象,最後返回對象。

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

function createAnother(original){
    var clone = object(original);
    clone.sayName=function(){
        console.log(this.name)
    };
    return clone;
}
var Person={
    name:'ll',
    friens:['kk','cc','aaa']
}
var instance = createAnother(Person);
console.log(instance)
instance.sayName();

寄生組合式繼承

組合繼承是js最經常使用的繼承模式;不過,他也有本身的不足。組合繼承最大的問題就是不管什麼狀況下,都會調用兩次超類型構造函數:一次是在建立子類型原型的時候,另外一次是在子類型構造函數內部。

function SuperType(name){
    this.name = name;
    this.colors=['red','blue','pink'];
}
SuperType.prototype.sayName=function(){
    console.log(this.name)
}
function SubType(name,age){
    SuperType.call(this,name);//第二次調用SuperType()
    this.age = age;
}
SubType.prototype = new SuperType()//第一次調用SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge=function(){
    console.log(this.age);
}

所謂寄生式組合繼承,即經過借用構造函數來繼承屬性,經過原型鏈的混成形式來繼承方法。其背後的基本思路:沒必要爲了指定子類型的原型而調用超類型的構造函數,咱們須要的無非是超類型原型的一個副本而已。本質上,就是使用寄生式繼承來繼承超類型的原型,而後將結果指定給子類型的原型。

function object(o){
    function F(){};
    F.prototype = o;
    return new F();
}
function inheritPrototype(subType,superType){
    var prototype = object(superType.prototype);//建立對象;
    prototype.constructor = subType;//加強對象
    subType.prototype = prototype;//指定對象
}
function SuperType(name){
    this.name = name;
    this.colors = ['red','blue','pink'];
}
SuperType.prototype.sayName=function(){
    console.log(this.name);
}
function SubType(name,age){
    SuperType.call(this,name);
    this.age = age;
}
var aaa = inheritPrototype(SubType,SuperType);
SubType.prototype.sayAge = function(){
    console.log(this.age);
}
var instance = new SubType('kk',24);
console.log(instance)

小結

在沒有類的狀況下,能夠採用下列模式建立對象。
1.工廠模式,使用簡單的函數建立對象,爲對象添加屬性和方法,而後返回對象。這個模式後來被構造函數模式所替代。
2.構造函數模式,能夠建立自定義引用類型,能夠像建立內置對象實例同樣使用new操做符。不過,構造函數也有缺點,即他的每一個成員都沒法獲得複用,包括函數。
3.原型模式,使用構造函數的prototype屬性來指定那些應該共享的屬性和方法。組合使用構造函數模式和原型模式,使用構造函數定義實例的屬性,而使用原型定義共享的屬性和方法。

JavaScript主要經過原型鏈實現繼承。原型鏈的構建是經過將一個類型的實例賦值給另外一個構造函數的原型來實現的。這樣,子類型就可以訪問超類型的全部屬性和方法,這一點基於類的繼承很類似。原型鏈的問題是對象實例共享全部屬性和方法,所以不宜單獨使用。解決這個問題的技術是借用構造函數,即在子類型構造函數內部調用超類型構造函數。這樣就能夠作到每一個實例都具備本身的屬性,同時還能保證只使用構造函數模式來定義類型。使用最多的繼承模式是組合繼承,這種模式使用原型鏈繼承共享的屬性和方法,而經過借用構造函數來繼承實例屬性。1.原型式繼承,能夠在沒必要預先定義構造函數的狀況下實現繼承,其本質是執行對給定對象的淺複製。而複製獲得的副本還能夠獲得進一步的改造。2.寄生式繼承,與原型式繼承很是類似,也是基於某個對象或某些信息建立一個對象,而後加強對象,最後返回對象。爲了解決組合繼承模式因爲屢次調用超類型構造函數而致使的低效率問題,能夠將這個模式和組合繼承一塊兒使用。3.寄生組合式繼承,集寄生式繼承和組合繼承的優勢與一身,是實現基於類型繼承的最有效方法。

相關文章
相關標籤/搜索