面向對象

面向對象的語言有一個標誌,那就是它們都有類的概念,而經過類能夠建立任意多個具備相同屬性和方法的對象編程

建立對象

雖然Object構造函數或者對象字面量均可以用來建立單個對象,但這些方式有個明顯的缺點:使用同一個接口建立不少對象,會產生大量重複代碼(爲何或產生大量重複的代碼?)。app

工廠模式

function createPerson(name, age, job){ 
    var o = new Object(); 
    o.name = name; 
    o.age = age; 
    o.job = job; 
    o.sayName = function(){ 
        alert(this.name); 
    }; 
    return o; 
}
var person1 = createPerson("Nicholas", 29, "Software Engineer"); 
var person2 = createPerson("Greg", 27, "Doctor");

工廠模式的特色:解決了建立多個類似對象的問題,但卻沒有解決對象識別的問題(即怎麼知道一個對象的類型);函數

構造函數模式

構造函數始終都應該以一個大寫字母開頭,而非構造函數則應該以一個小寫字母開頭。這個作法借鑑自其餘 OO 語言,主要是爲了區別於 ECMAScript中的其餘函數;由於構造函數自己也是函數,只不過能夠用來建立對象而已。性能

function Person(name, age, job){ 
    this.name = name; 
    this.age = age; 
    this.job = job; 
    this.sayName = function(){ 
        alert(this.name); 
    }; 
} 
var person1 = new Person("Nicholas", 29, "Software Engineer"); 
var person2 = new Person("Greg", 27, "Doctor");

要建立 Person 的新實例,必須使用 new 操做符。這中方式調用構造函數實際上回會經歷4個步驟this

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

缺點: 每一個方法都要在每一個實例上從新建立一遍。spa

原型模式

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

function Person(){ 
} 
Person.prototype.name = "Nicholas"; 
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer"; 
Person.prototype.sayName = function(){ 
    alert(this.name); 
}; 
var person1 = new Person(); 
person1.sayName(); //"Nicholas" 

var person2 = new Person();

person2.sayName(); //"Nicholas" 
alert(person1.sayName == person2.sayName); //true

新對象的這些屬性和方法是由全部實例共享的。換句話說,person1 和 person2 訪問的都是同一組屬性和同一個 sayName()函數指針

理解原型對象

構造函數、原型和實例的關係:每一個構造函數都有一個原型對象(prototype屬性),原型對象都包含一個指向構造函數的指針(constructor(構造函數)屬性),而實例有包含一個指向原型對象的內部指針([[Prototype]]、Firefox、Safari 和 Chrome 在每一個對象上都支持一個屬性__proto__)code

圖片描述

更簡單的原型語法

function Person(){ 
} 
Person.prototype = { 
    constructor : Person,   //
    name : "Nicholas", 
    age : 29, 
    job: "Software Engineer", 
    sayName : function () { 
        alert(this.name); 
    } 
};

本質上徹底重寫了默認的 prototype 對象,所以 constructor 屬性也就變成了新對象的 constructor 屬性(指向 Object 構造函數),再也不指向 Person 函數。因此須要手動設置 constructor 屬性,並將它的值設置爲 Person,確保經過該屬性可以訪問到適當的值。對象

原型與in操做符

原型的動態性

原生對象的原型

原型模式的重要性不只體如今建立自定義類型方面,就連全部原生的引用類型,都是採用這種模式建立的。全部原生引用類型(Object、Array、String等)都在期構造函數的原型定義了方法。例如,在Array.prototype中能夠找到sort()方法,而在String.prototype中能夠找到substring()方法。

alert(typeof Array.prototype.sort); //"function" 
alert(typeof String.prototype.substring); //"function"

經過原生對象的原型,不只能夠取得全部默認方法的引用,並且也能夠定義新方法。能夠像修改自定義對象的原型同樣修改原型對象的原型,所以能夠隨時添加方法。下面的代碼就給基本包裝類型String添加了一個名爲 startsWith()的方法。

String.prototype.startsWith = function (text) { 
    return this.indexOf(text) == 0; 
}; 
var msg = "Hello world!"; 
alert(msg.startsWith("Hello")); //true

原型對象的問題

缺點:

  1. 由於省略了爲構造函數傳遞初始化參數這一環節,結果全部在默認狀況下都將取得相同的屬性值
  2. 最大問題是由其共享的本性所致使的。

原型中全部屬性是被不少實例共享的,這種共享對於函數很是合適。對於那些包含基本值的屬性倒也說的過去,畢竟(如前面的例子所示),經過在實例上添加同一個同名屬性,能夠隱藏在原型中的對應屬性。然而,對於包含引用類型值的屬性來講,問題就比較突出了。

function Person(){ 
} 
Person.prototype = { 
    constructor: Person, 
    name : "Nicholas", 
    age : 29, 
    job : "Software Engineer", 
    friends : ["Shelby", "Court"], 
    sayName : function () { 
        alert(this.name); 
    } 
}; 
var person1 = new Person(); 
var person2 = new Person(); 
person1.friends.push("Van"); 
alert(person1.friends); //"Shelby,Court,Van" 
alert(person2.friends); //"Shelby,Court,Van" 
alert(person1.friends === person2.friends); //true

繼承

原型鏈

基本思想:利用原型讓一個引用類型繼承另外一個類型的屬性和方法。

function SuperType(){ 
    this.property = true; 
}   
SuperType.prototype.getSuperValue = function(){ 
    return this.property; 
}; 
function SubType(){ 
    this.subproperty = false; 
} 
//繼承了 SuperType 
SubType.prototype = new SuperType(); 
SubType.prototype.getSubValue = function (){ 
    return this.subproperty; 
}; 
var instance = new SubType(); 
alert(instance.getSuperValue()); //true

圖片描述

原型的搜索機制:當以讀取模式方式訪問一個實例屬性時,首先會在實例中搜索該屬性。若是沒有找到改屬性,則會繼續搜索實例的原型。在經過原型鏈實現繼承的狀況,搜索過程就得以沿着原型鏈繼續向上。在找不到屬性或方法的狀況下,搜索過程是要一環一環地前行到原型鏈末端纔會停下來。

默認的原型

咱們引用類型默認都繼承了Object,而這個繼承也是經過原型鏈實現的。你們要記住,全部函數的默認原型都是Object的實例,所以默認原型都會包含一個內部指針,指向Object.prototype。這也正式全部自定義類型都會繼承toString()、valueOf()等默認方法的根本緣由。
完整的原型鏈:
圖片描述

原型鏈的問題

主要問題來自於包含引用類型的原型(引用類型值的原型屬性會被全部實例共享)。在經過原型來實現繼承時,原型實際上回變成另外一個類型的實例。因而,原先的實例屬性也就瓜熟蒂落地變成了如今的原型屬性了。

function SuperType(){ 
   this.colors = ["red", "blue", "green"];
} 
function SubType(){ 
}
//繼承了 SuperType 
SubType.prototype = new SuperType(); 
var instance1 = new SubType(); 
instance1.colors.push("black"); 
alert(instance1.colors); //"red,blue,green,black" 
var instance2 = new SubType(); 
alert(instance2.colors); //"red,blue,green,black"

原型鏈的第二個問題是:在建立子類型的實例時,不能向超類型的構造函數中傳遞參數。實際上,應該說是沒有辦法在不影響全部對象實例的狀況下,給超類型的構造函數傳遞參數。有鑑於此,在加上前面剛剛討論過的因爲原型中包含引用類型值所帶來的問題,實踐中不多會單獨使用原型鏈。

借用構造函數

基本思想:在子類型構造函數的內部調用超類型構造函數。經過apply()和call()方法也能夠在(未來)新建立的對子那個上執行構造函數

function SuperType(){ 
    this.colors = ["red", "blue", "green"]; 
} 
function SubType(){ 
    //繼承了 SuperType 
    SuperType.call(this); 
} 
var instance1 = new SubType(); 
instance1.colors.push("black"); 
alert(instance1.colors); //"red,blue,green,black" 
var instance2 = new SubType(); 
alert(instance2.colors); //"red,blue,green"

優勢

傳遞參數.借用構造函數能夠在子類型構造函數中向超類型構造函數傳遞參數

function SuperType(name){ 
    this.name = name; 
} 
function SubType(){ 
    //繼承了 SuperType,同時還傳遞了參數
    SuperType.call(this, "Nicholas"); 
    //實例屬性
    this.age = 29; 
} 
var instance = new SubType(); 
alert(instance.name); //"Nicholas"; 
alert(instance.age); //29

問題

構造函數存在的問題——方法都在構造函數定義,所以函數就沒法複用。並且在超類型的原型中定義方法,對子類型而言也是不可見的,結果全部類型都只能在使用構造函數模式

組合繼承

組合繼承:有時候也叫僞經典繼承,指的是將原型鏈和借用構造函數的技術組合到一塊,從而發揮兩者之長的一種繼承模式。
基本思想: 使用原型鏈實現對原型屬性和方法的繼承,而經過借用構造函數來實現對實例屬性的繼承。這樣,即經過在原型上定義方法實現了函數複用,又可以保證每一個實例都有它本身的屬性。

function SuperType(name){ 
    this.name = name; 
    this.colors = ["red", "blue", "green"]; 
} 
SuperType.prototype.sayName = function(){ 
    alert(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(){ 
    alert(this.age); 
}; 
var instance1 = new SubType("Nicholas", 29); instance1.colors.push("black"); 
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas"; 
instance1.sayAge(); //29 
var instance2 = new SubType("Greg", 27); 
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg"; 
instance2.sayAge(); //27

instanceof 和 isPrototypeOf()也可以用於識別基於組合繼承建立的對象

小結

ECMAScript 支持面向對象(OO)編程,但不使用類或者接口。對象能夠在代碼執行過程當中建立和加強,所以具備動態性而非嚴格定義的實體。在沒有類的狀況下,能夠採用下列模式建立對象。

  • 工廠模式,使用簡單的函數建立對象,爲對象添加屬性和方法,而後返回對象。這個模式後來被構造函數模式所取代。
  • 構造函數模式,能夠建立自定義引用類型,能夠像建立內置對象實例同樣使用new操做符。不過,構造函數模式也有缺點,即它的每一個成員都沒法獲得複用,包括函數。因爲函數能夠不侷限於任何對象(即與對象具備鬆散耦合的特色),所以沒有理由在多個對象間共享函數。
  • 原型模式,使用構造函數的prototype屬性來指定那些應該共享的屬性和方法。組合使用構造函數模式和原型模式時,使用構造函數定義實例屬性,而使用原型定義共享的屬性和方法。

JavaScript主要經過原型鏈實現繼承。原型鏈的構建是經過講一個類型的實例賦值給另外一個構造函數的原型實現的。這樣,子類型就可以訪問超類型的全部屬性和方法,這一點與基於類的繼承很類似。原型鏈的問題是對象實例共享全部繼承的屬性和方法,所以不適宜單獨使用。解決這個問題的技術是借用構造函數,即在子類型構造函數的內部調用超類型構造函數。這樣就能夠作到每一個實例都具備本身的屬性,同時還能保只使用構造函數模式來定義類型。使用最多的繼承模式是組合繼承,這種模式使用原型鏈繼承共享的屬性和方法,而經過借用構造函數繼承實例屬性。
此外,還存在下列可供選擇的繼承模式。

  • 原型式繼承,能夠在沒必要預先定義構造函數的狀況下實現繼承,其本質是執行對給定對象的淺複製。而複製獲得的副本還能夠獲得進一步改造。
  • 寄生式繼承,與原型式繼承很是類似,也是基於對象或某些寫信建立一個對象,而後加強對象。最後返回對象。爲了解決組合繼承模式因爲屢次調用超類型構造函數而致使的低效率問題,能夠將這個模式與組合繼承一塊兒使用。
  • 寄生組合式繼承,集寄生式繼承和組合繼承的有點與一身,是實現基於類型繼承的最有效方式
相關文章
相關標籤/搜索