JS高程讀書筆記--第六章原型繼承

JS高程讀書筆記--第六章

理解對象

建立自定義對象的方式有建立一個Object實例,而後爲它添加屬性和方法。還可用建立對象字面量的方式javascript

屬性類型

ECMAScript在定義只有內部採用的特性時,描述了屬性的各類特徵。
ECMAScript中有兩種屬性:數據屬性訪問器屬性。在JS中不能直接訪問它們。java

數據屬性

數據屬性包含一個數據值的位置。在這個位置能夠讀取和寫入值。數組

  • [[Configurable]]:表示可否經過delete刪除屬性從而從新定義屬性,可否修改屬性的特性,或者可否把屬性修改成訪問器屬性。默認值爲true瀏覽器

  • [[Enumerable]]:表示可否經過for-in循環返回屬性。默認值爲trueapp

  • [[Writable]]:表示可否修改屬性的值。默認值爲true函數

  • [[Value]]:包含這個屬性的數據值。讀取屬性值的時候,從這個位置讀;寫入屬性值的時候,把新值保存在這個位置。默認值爲undefined測試

要修改屬性默認的特性,必須使用ES5的Object.defineProperty()方法。這個方法接收三個參數:屬性所在的對象屬性的名字一個描述符對象。在調用這個方法時,若是不指定,configurable、enumerable和writable特性的默認值都是false;this

一旦把屬性定義爲不可配置的,就不能再把它變回可配置的。prototype

訪問器屬性

訪問器屬性不包含數據值;他們包含一對getter和setter函數(這兩個函數都不是必須的)。在讀取訪問器屬性時,會調用getter函數,這個函數負責返回有效值;在寫入訪問器屬性時,會調用setter函數並傳入新值,這個函數負責決定如何處理數據。指針

  • [[Configurable]]:表示可否經過delete刪除屬性從而從新定義屬性,可否修改屬性的特性,或者可否把屬性修改成數據屬性。默認值爲true

  • [[Enumerable]]:表示可否經過for-in循環返回屬性。默認值爲true

  • [[Get]]:在讀取屬性時調用的函數。默認值爲undefined;

  • [[Set]] :在寫入屬性時調用的函數。默認值爲undefined

訪問器屬性不能直接定義,必須使用Object.defineProperty()來定義。

不必定非要同時指定getter和setter。只指定getter意味着屬性是不能寫,嘗試寫入屬性會被忽略。在嚴格模式下,嘗試寫入只指定了getter函數的屬性會拋出錯誤。相似的,只指定setter函數的屬性也不能讀,不然在非嚴格模式下會返回undefined,而在嚴格模式下會拋出錯誤。

定義多個屬性

能夠利用ES5定義的Object.defineProperties()方法經過描述符一次定義多個屬性。這個方法接收兩個對象參數:第一個對象是要添加和修改其屬性的對象,第二個對象的屬性與第一個對象中要添加或修改的屬性一一對應。

讀取屬性的特性

使用ES5的Object.getOwnPropertyDescriptor()方法,能夠取得給定屬性的描述符。這個方法接收兩個參數:屬性所在對象要讀取其描述符的屬性名稱,返回值是一個對象

建立對象

工廠模式

這種模式抽象了建立具體對象的過程。開發人員發明了一種函數,用函數來封裝以特定接口建立對象的細節。

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('junyan','25','FE');

工廠模式雖然解決了建立多個類似對象的問題,但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)

構造函數模式

能夠建立自定義的構造函數,從而定義自定義對象類型的屬性和方法。

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
        console.log(this.name);
    };
}
var person1 = new Person('junyan','25','FE')

這種方式與工廠模式存在如下不一樣:

  • 沒有顯式的建立對象;

  • 直接將屬性和方法賦給了this對象;

  • 沒有return語句;

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

  1. 建立一個新對象;

  2. 將構造函數的做用域賦給新對象(所以this就指向了這個新對象);

  3. 執行構造函數中的代碼(爲這個新對象添加屬性);

  4. 返回新對象;

實例都有一個constructor(構造函數)屬性,該屬性指向Person。對象的這個屬性最初是用來標識對象類型的。可是,提到檢測對象類型,仍是instanceof操做符要更可靠一些。

建立自定義的構造函數意味着未來能夠將它的實例標識爲一種特定的類型

將構造函數當作函數

  • 構造函數與其餘函數的惟一區別,就在於調用它們的方式不一樣。任何函數,只要經過new操做符來調用,那它就能夠做爲構造函數;

  • 不使用new操做符調用,屬性和方法都被添加給window對象

構造函數的問題

  • 使用構造函數的主要問題就是每一個方法都要在每一個實例上從新建立一遍。

  • 不一樣實例上的同名函數是不相等的,能夠經過把函數定義轉移到構造函數外部來解決。

原型模式!!

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

prototype就是經過調用構造函數而建立的那個對象實例的原型對象

使用原型對象的好處是可讓全部對象實例共享它所包含的屬性和方法。換句話說,沒必要在構造函數中定義對象實例的信息,而是能夠將這些信息直接添加到原型對象中。

構造函、原型和實例的關係:每一個構造函數構造函數都有一個原型對象(prototype),原型對象都包含一個指向構造函數的指針(constructor),而實例都包含一個指向原型對象的內部指針(__proto__)

理解原型對象!!!

  1. 只要建立了一個新函數,就會根據一組特定的規則爲該函數建立一個prototype屬性,這個屬性指向函數的原型對象
    全部原型對象都會自動得到一個constructor(構造函數)屬性,這個屬性包含一個指向prototype屬性所在函數的指針。例:Person.prototype.constructor = Person

建立了自定義的構造函數以後,其原型對象默認只會取得constructor屬性;至於其餘方法都是從Object繼承而來的。

原型最初只包含constructor屬性,而該屬性也是共享的,所以能夠經過對象實例訪問

  1. 當調用構造函數建立一個新實例後,該實例的內部將包含一個指針(內部屬性)ECMA叫[[Prototype]]指向構造函數的原型對象,高級瀏覽器在每一個對象上都支持一個屬性__proto__

注:這個鏈接存在於實例與構造函數的原型對象之間,不存在於實例與構造函數之間。

  • 能夠經過isPrototypeOf()方法來肯定對象之間是否存在這種關係
    Person.prototype.isPrototypeOf(person1); // true;

  • ES5新增了一個方法,叫Object.getPrototypeOf(),這個方法返回[[Prototype]]的值。
    Object.getPrototypeOf(person1) == Person.prototype

使用這個方法能夠方便的取得一個對象的原型

  1. 當爲對象實例添加一個屬性時,這個屬性就會屏蔽原型對象中的同名屬性;換句話說,添加這個屬性只會阻止咱們訪問原型中的那個屬性,但不會修改那個屬性。即便將這個屬性設置爲null,也只會在實例中設置這個屬性,而不會恢復其指向原型的連接。不過,能夠經過使用delete操做符則能夠徹底刪除實例屬性,從而讓咱們可以從新訪問原型中的屬性。

使用hasOwnProperty()方法能夠檢測一個屬性是存在於實例中,仍是存在於原型中。這個方法(從Object繼承而來)只在給定屬性存在於對象實例中,在返回true。

原型與in操做符

  • 單獨使用in時,in操做符會在經過對象可以訪問給定屬性時返回true,不管該屬性存在於實例中仍是原型中

  • 同時使用hasOwnProperty()方法和in操做符,就能夠肯定該屬性究竟是存在於對象中,仍是存在於原型中

  • 在使用for-in循環時,返回的是全部可以經過對象訪問的、可枚舉的屬性,其中即包括存在於實例中的屬性,也包括存在於原型中的屬性。屏蔽了原型中不可枚舉屬性的實例屬性也會在for-in循環中返回。

  • 要取得對象上可枚舉的實例屬性,可使用ES5的Object.keys(),這個方法接收一個對象做爲參數,返回一個包含全部可枚舉屬性的字符串數組。

  • 若是想獲得全部實例屬性,不管它是否可枚舉,均可以使用Object.getOwnPropertyNames()

更簡單的原型語法

能夠用一個包含全部屬性和方法的對象字面量來重寫整個原型對象

Person.prototype = {
    name:'junyan',
    job:'FE'
}
  • 可是這樣constructor屬性就再也不指向Person,這種語法實際上徹底重寫了默認的prototype對象,所以constructor屬性就變成了新對象的constructor屬性,再也不指向Person,而是指向Object。此時instanceof還能返回正確的結果。

  • 若是constructor屬性很重要,能夠特地設定將它設置回適當的值,但這種方式重設constructor屬性會致使它的[[Enumerable]]特性被設置爲true,由於默認狀況下原生的constructor屬性是不可枚舉的。可使用ES5的Object.defineProperty()

原型的動態性

  1. 咱們對原型對象所作的任何修改都可以當即從實例上反映出來,即便是先建立了實例後修改原型也照樣如此,由於實例與原型之間的鏈接只不過是一個指針,而非一個副本;

  2. 可是若是重寫了整個原型對象,就不同。調用構造函數時會爲實例添加一個指向最初原型的[[Prototye]]指針(__proto__),而把原型修改成另外一個對象就等於切斷了構造函數與最初原型之間的聯繫

  3. 重寫原型對象切斷了現有原型與任何以前已經存在的對象實例之間的聯繫,它們引用的仍然是最初的原型

實例中的指針僅指向原型,而不指向構造函數

原生對象的原型

  1. 全部原生引用類型(Object、Array、String等)都在其構造函數的原型上定義了方法;

  2. 經過原生對象的原型,不只能夠取得全部默認方法的引用,並且也能夠定義新方法。能夠像修改自定義對象的原型同樣修改原生對象的原型,所以能夠隨時添加方法。(不建議);

原型對象的問題

原型中全部屬性被實例共享,對於基本類型值的屬性,經過在實例上添加一個同名屬性,能夠隱藏原型中的對應屬性,但對於引用類型值的屬性來講,實例上修改其值,在全部其餘實例中都會反映出來,因此基本不單獨使用原型模式

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

構造函數模式用於定義實例屬性,而原型模式用於定義方法和共享屬性。

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["xiaohua","hanghang"]
}
Person.prototype = {
    constructor:Person,
    sayName:function(){
        console.log(this.name);        
    }
}

繼承

ECMAScript只支持實現繼承,並且實現繼承主要是依靠原型鏈來實現。

原型鏈

原型鏈做爲實現繼承的主要方法。其基本思想是利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法。

若是讓原型對象等於另外一個類型的實例,此時的原型對象將包含一個指向另外一個原型的指針(__proto__)相應地,另外一個原型中也包含着一個指向另外一個構造函數的指針。若另外一個原型又是另外一個類型的實例,上述關係依然成立,如此層層遞進,就構成了實例與原型的鏈條。

在找不到屬性或方法的狀況下,搜索過程老是要一環一環地前行到原型鏈末端纔會停下來

默認的原型

全部引用類型默認都繼承了Object,而這個繼承也是經過原型鏈實現的。全部函數的默認原型都是Object實例,所以默認原型都會包含一個內部指針(__proto__),指向Object.prototype。這也正是全部自定義類型都會繼承toString()valueOf()默認方法的根本緣由

肯定原型和實例的關係

  1. 第一種方式是使用instanceof操做符,只要用這個操做符來測試實例與原型鏈中出現過的構造函數,結果就會返回true。

  2. 第二種方式是使用isPrototypeOf()方法。一樣,只要是原型鏈中出現過的原型,均可以說是該原型鏈所派生的實例的原型,所以這個方法也會返回true.

謹慎地定義方法

  • 給原型添加方法必定要放在替換原型的語句以後,重寫的方法會屏蔽原來的那個方法

  • 在經過原型鏈實現繼承時,不能使用對象字面量建立原型方法。由於這樣作就會重寫原型鏈。

原型鏈的問題

實踐中不多會單獨使用原型鏈

  • 在經過原型來實現繼承時,原型實際上會變成另外一個類型的實例,原先的實例屬性也就瓜熟蒂落的變成了如今的原型屬性

  • 在建立子類型的實例時,不能向超類型的構造函數中傳遞參數。應該說是沒有辦法在不影響全部對象實例的狀況下,給超類型的構造函數傳遞參數。

借用構造函數

在子類型構造函數的內部調用超類型構造函數,能夠經過使用apply()call()方法在新建立的對象上執行構造函數。

function SubType(){
    SuperType.call(this);
}

這一年就會在新的Subtype對象上執行SuperType()函數中定義的全部對象初始化代碼。

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

function SubType(){
    SuperType.call(this,'junyan');
}
  1. 若是僅僅是借用構造函數,那麼也將沒法避免構造函數模式存在的問題——方法都在構造函數中定義,所以函數複用就無從談起,並且在超類型的原型中定義的方法,對子類型而言也是不可見的,結果全部類型都只能使用構造函數模式。

組合繼承

有時也叫做僞經典繼承,使用原型鏈實現對原型屬性和方法的繼承,而經過借用構造函數來實現對實例屬性的繼承。這樣既經過在原型上定義方法實現了函數複用,又可以保證每一個實例都有它本身的屬性。

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

組合繼承最大的問題就是不管什麼狀況下,都會調用兩次超類型構造函數:一次是在建立子類原型的時候,另外一次是在子類型構造函數內部

原型式繼承

這個方法沒有使用嚴格意義上的構造函數,是基於已有的對象建立新對象,同時還沒必要所以建立自定義類型。

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

嚴格上講,object()對傳入其中的對象執行了一次淺複製

ES5經過新增Object.create()方法規範了原型式繼承。這個方法接收兩個參數:一個用做新對象原型的對象和(可選的)一個爲新對象定義額外屬性的對象。在傳入一個參數的狀況下,Object.create()object()方法的行爲相同。
只想讓一個對象與另外一個對象保持相似的狀況下,原型式繼承是徹底能夠勝任的,不要忘了,包含引用類型值的屬性始終都會共享相應的值,就像使用原型模式同樣。

寄生式繼承

建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來加強對象,最後再像真地是它作了全部工做同樣返回對象。

function createAnother(original){
    var clone = object(original);
    clone.sayHi = function(){
        console.log('Hi');
    };
    return clone;
}

在主要考慮對象而不是自定義類型和構造函數的狀況下,寄生式繼承也是一種有用的模式。任何可以返回新對象的函數都適用於這個模式

寄生式組合繼承

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

function inheritPrototype(subType,superType){
    var prototype = Object(superType.prototype);
    prototype.constructor = subType;
    subType.prototype = prototype;
}
  1. 第一步是建立超類型原型的一個副本

  2. 第二部是爲建立的副本添加constructor屬性,從而彌補因重寫原型而失去的默認的constructor屬性。

  3. 將新建立的對象(即副本)賦值給子類型的原型

這種繼承的高效率體如今它只調用了一次超類型構造函數,而且所以避免了在子類型.prototype上面建立沒必要要的、多餘的屬性。於此同事,原型鏈還能保持不變;所以還能正常使用instanceof()isPrototypeOf()

相關文章
相關標籤/搜索