JavaScript學習筆記 - 面向對象設計

本文記錄了我在學習前端上的筆記,方便之後的複習和鞏固。javascript

ECMAScript中沒有類的概念,所以它的對象也與基於類的語言的對象有所不一樣。
ECMA-262把對象定義爲:"無序屬性的組合,其屬性能夠包含基本值,對象或者函數。"對象的每一個屬性或方法都有一個名字,而每一個名字映射到一個值。咱們能夠把ECMAScript的對象想象成散列表:無非就是一組名值對,其中值可使數據或函數。
每一個對象都是基於一個引用類型建立的,這個引用類型可使原生類型,也能夠是開發人員定義的類型前端

1. 理解對象

建立自定義對象最簡單的方式就是建立一個Object的實例,而後再爲它添加屬性和方法java

var person = new Object();
person.name = "Jason";
person.age = 18;
person.job = "Web";

person.sayName = function() {
    console.log(this.name);
};

對象字面量建立:web

var person = {
    name: "Jason",
    age: 18,
    job: "Web",
    sayName = function() {
        console.log(this.name);
    }
}

這兩個方法的person對象是同樣的,都有相同的屬性和方法,這些屬性在建立的時都帶有一些特徵值(characteristic),JavaScript經過這些特徵值來定義它們的行爲。數組

1.1 屬性類型

ECMAScript中有兩種屬性:數據屬性和訪問器屬性瀏覽器

  1. 數據屬性
    數據屬性包含一個數據值的位置。在這個位置能夠讀取和寫入值。數據屬性有4個描述其行爲的特性。安全

  • [[Configurable]]: 表示可否經過delete刪除屬性從而從新定義屬性,可否修改屬性的特性,或者可否把屬性修改成訪問器屬性。直接在對象上定義的屬性,它們的這個特性默認值爲true。app

  • [[Enumerable]]: 表示可否經過for-in循環返回屬性。直接在對象上定義的屬性,它們的這個特性默認值爲true。函數

  • [[Writable]]: 表示可否修改屬性的值。直接在對象上定義的屬性,它們的這個特性默認爲true。學習

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

要修改屬性默認的特性,必須使用ECMAScript5的Object.defineProperty()方法。這個方法接收三個參數: 屬性所在的對象、屬性的名字和一個描述符對象。其中,描述符(descriptor)對象的屬性必須是: configurable、enumerable、writable和value。設置其中的一或多個值,能夠修改對應的特徵值。

var person = {};
Object.defineProperty(person, "name", {
    writable: false,      //不能修改屬性的值....
    configurable: false,  //不能經過delete刪除屬性.....
    value: "Jason"        //寫入屬性值
});
console.log(person.name); //Jason
person.name = "Cor";
console.log(person.name); //Jason
delete person.name;
console.log(person.name); //Jason

一旦把屬性定義爲不能夠配置的,就不能再把它變回可配置的了。此時再調用Object.defineProperty()方法修改除writable以外的特性,都會致使錯誤

var person = {};
Object.defineProperty(person, "name", {
    configurable: false,
    value: "Jason"
});
//拋出錯誤
Object.defineProperty(person, "name", {
    comfogirable: true,    //這行代碼修改了特性致使報錯
    value: "Cor"
});

也就是說,能夠屢次調用Object.defineProperty()方法修改同一屬性,但在吧configurable特性設置爲false以後就會有限制了。

注意:在調用Object.defineProperty()方法時,若是不指定,configurable、enumerable和writable特性的默認值都是false

  1. 訪問器屬性
    訪問器屬性不包含數據值;它們包含一對兒gettersetter函數(不過,這兩個函數都不是必需的)。在讀取訪問器屬性時,會調用getter函數,這個函數負責返回有效的值;在寫入訪問器屬性時,會調用setter函數並傳入新值,這個函數負責決定如何處理數據。訪問器屬性有以下4個特性。

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

    • [[Enumerable]]: 表示可否經過for-in循環返回屬性。對於直接在對象上定義的屬性,它們的這個特性默認值爲true。

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

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

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

var book = {
    _year: 2004,
    edition: 1
};

Object.defineProperty(book, "year", {
    get: function() {
        return this._year;
    },
    set: function(newValue) {    //接受新值的參數
        if(new Value > 2004) {
            this._year = new Value;
            this.edition += newValue - 2004;
        }
    }
});
book.year = 2005;            //寫入訪問器,會調用setter並傳入新值
console.log(book.edition);  //2

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

1.2 定義多個屬性

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

var book = {};

Object.defineProperties(book, {
    _year: {
        value: 2004
    },
    edition: {
        value: 1
    },
    
    year: {
        get: function() {
            return this._year;
        },
        set: function(newValue) {
            if(newValue > 2004) {
                this._year = newValue;
                this.edition += newValue - 2004;
            }
        }
    }
});

1.3 讀取屬性的特性

使用ES5的Object.getOwnPropertyDescriptor()方法,能夠取得給定屬性的描述符。這個方法接收兩個參數:屬性所在的對象和要讀取其描述符的屬性名稱。返回值是一個對象,若是是訪問器屬性,這個對象的屬性有configurable、enumerate、get和set;若是是數據屬性,這個對象的屬性有configurable、enumerable、writable和value。

var book = {};

Object.defineProperties(book, {
    _year: {
        value: 2004
    },
    edition: {
        value: 1
    },
    
    year: {
        get: function() {
            return this._year;
        },
        set: function(newValue) {
            if(newValue > 2004) {
                this._year = newValue;
                this.edition += newValue - 2004;
            }
        }
    }
});

var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
console.log(descriptor.value);             //2004
console.log(descriptor.configurable);   //false
console.log(typeof descriptor.get);     //"undefined"
var descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor.value);          //"undefined"
console.log(descriptor.enumerable);     //false
console.log(typeof descriptor.get);        //"function"

2.建立對象

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

2.1 工廠模式

這種模式抽象了建立具體對象的過程,考慮到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("Jason", 18, "WEB");
var person2 = createPerson("Cor", 19, "WEB");

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

2.2 構造函數模式

ObjectArray這樣的原生構造函數,在運行時會自動出如今執行環境中。也能夠建立自定義構造函數,從而定義對象類型的屬性和方法。

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("Jason", 18, "WEB");
var person2 = new Person("Cor", 19, "WEB");

構造函數模式和工廠模式存在如下不一樣之處:

  • 沒有顯示地建立對象;

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

  • 沒有return語句

像上面建立的Person構造函數。構造函數使用都應該以一個大寫字母開頭,而非構造函數則應該以一個小寫字母開頭。

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

  1. 建立一個新對象

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

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

  4. 返回新對象。

在前面例子的最後,person1person2分別保存着Person的一個不一樣的實例。這兩個對象都有一個constructor(構造函數)屬性,該屬性指向Person

console.log(person1.constructor == Person);        //true
console.log(person2.constructor == Person);         //true
console.log(person1 instanceof Object);                //true
console.log(person1 instanceof Person);                //true
console.log(person2 instanceof Object);                //true
console.log(person2 instanceof Person);                //true

建立對的對象既是Object的實例,同時也是Person的實例,上面經過instanceof驗證。
建立自定義的構造函數意味着將它的實例標識一種特定類型;而這正是構造函數模式賽過工廠模式的地方。person1person2之因此同時是Object的實例,是由於全部對象均繼承自Object

以這種方式定義的構造函數是定義在Global對象(在瀏覽器就是window對象)中的

2.2.1 把構造函數當函數

//看成構造函數使用
var person = new Person("Jason", 18, "web");
person.sayName();        //"Jason"
//做爲普通函數調用
Person("Cor", 19, "web");    //添加到window
window.sayName();        //"cor"
//在另外一個對象的做用域中調用
var o = new Object();
Person.call(o, "Kristen", 22, "web");
o.sayName();               //"kriten"

當在全局做用域中調用一個函數時,this對象老是指向Global對象(在瀏覽器中的window對象),最後使用了call() ( 或者apply() )在某個特殊對象的做用域中調用Person()函數。這裏是在對象o的做用域調用的,所以調用後o就擁有了全部屬性和方法。

2.2.2 構造函數的問題

構造函數的主要問題,就是每一個方法都要在每一個實例上從新建立一遍。在前面例子中,person1和person2都有一個名爲sayName()的方法,但那兩個方法不是同一個Function的實例。ECMAScript中的函數是對象,所以每定義一個函數,也就是實例化了一個對象。從邏輯角度講,此時的構造函數也能夠這樣定義

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
        this.sayName = new Function(console.log(this.name)); //與聲明函數在邏輯上是等價的
}

以這種方式建立函數,會致使不一樣的做用域鏈和標識符解析,但建立Function新實例的機制仍然是相同的。所以不一樣的實例上的同名函數是不相等的

console.log(person1.sayName == person2.sayName);  //false
function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;
}

function sayName() {
    console.log(this.name);
}

var person1 = new Person("Jason", 18, "WEB");
var person2 = new Person("Cor", 19, "WEB");

在構造函數內部咱們把sayName屬性設置成等於全局的sayName函數。因爲構造函數的sayName屬性包含的是一個指向函數的指針,所以person1和person2對象就共享了在全局做用域中定義的同一個sayName()函數。

2.3 原型模式

咱們建立的每一個函數都有一個prototype(原型)屬性,這個屬性是一個指針,指向一個對象。而這個對象的用途是包含能夠由特定類型的全部實例共享的屬性和方法。若是按照字面意思來理解,那麼prototype就是經過調用構造函數而建立的那個對象實例的原型對象。使用原型對象的好處就是可讓全部對象實例共享它所包含的方法。

function Person() {
}

Person.prototype.name = "Jason";
Person.prototype.age = 18;
Person.prototype.job = "Web";
Person.prototype.sayName = function() {
    console.log(this.name);
}

var person1 = new Person();
person1.sayName();            //"Jason"
var person2 = new Person();
person2.sayName();            //"Jason"

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

與構造函數模式不一樣的是,新對象的這些屬性和方法是由全部實例共享。這樣說吧,person1和person2訪問的都是同一組屬性和同一個sayName()函數。

2.3.1 理解原型對象

不管何時,只要建立了一個新函數,就會根據一組特定的規則爲該函數建立一個prototype屬性,這個屬性指向函數的原型對象。在默認的狀況下,全部原型對象都會自動得到一個constructor(構造函數)屬性,這個屬性包含一個指向prototype屬性所在函數的指針,就拿前面的例子來講,Person.prototype.constructor指向Person。而經過這個構造函數,咱們還能夠繼續爲原型對象添加其餘屬性和方法。

建立了自定義構造函數以後,其原型對象默認只會取得constructor屬性;至於其餘方法,則都是從Object繼承而來的。當調用構造函數建立一個新實例後,該實例的內部將包含一個指針(內部屬性),指向構造函數的原型對象。ECMA-262第五版中管這個指正交[[Prototype]]。雖然在腳本中沒有標準的方式訪問[[Prototype]],但Firefox、Safari和Chrome在每一個對象上都支持一個熟悉 __ proto__;而在其餘實現中,這個屬性對腳本則是徹底不可見的。不過在明確的真正重要一點就是,這個鏈接存在於實例與構造函數的原型對象之間,而不是存在於實例與構造函數之間。
前面例子如圖:

clipboard.png

在此Person.prototype指向了原型對象,而Person.prototype.constructor又指回了Person。原型對象中除了都包含constructor屬性以外,還包含後來添加的屬性。Person的每一個實例person1和person2都包含一個內部屬性,該屬性僅僅指向了Person.prototype;換句話說它們與構造函數沒有直接的關係。此外,雖然這兩個實例都不包含屬性和方法,但咱們卻能夠調用person1.sayName()。這是經過查找對象屬性的過程來實現的。

雖然在全部的實現中都沒法訪問到[[Prototype]]能夠經過isPrototypeOf()方法來肯定對象之間是否存在這種關係。從本質上講,若是[[Prototype]]指向調用isPrototypeOf()方法的對象(Person.prototype),那麼這個方法就會返回true

console.log(Person.prototype.isPrototypeOf(person1))  //true
console.log(Person.prototype.isPrototypeOf(person2))  //true

ES5增長了一個新方法,叫Object.getPrototypeOf(),在全部支持的實現中,這個方法返回[[Prototype]]的值,能夠方便地獲取一個對象的原型

console.log(Object.getPrototypeOf(person1) == Person.prototype); //true
console.log(Object.getPrototypeOf(person1).name);     //"Jason"

搜索機制:
每當代碼讀取某個對象的某個屬性時,都會執行一次搜索,目標是具備給定名字的屬性。搜索首先從對象的實例自己開始。若是在實例中找到具備給定名字的屬性,則返回該屬性的值;若是沒有找到。則繼續搜索指針指向的原型對象,在原型對象中查找具備給定名字的屬性。若是在原型對象中找到這個屬性,則返回該屬性的值。簡單的說,就是會一層層搜索搜索到則返回,沒搜索到則繼續下層搜索。

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

雖然能夠經過對象實例訪問原型的值,但卻不能經過對象實例重寫原型的值。若是咱們爲對象實例添加了一個熟悉,而且該屬性名和實例原型中的一個屬性同名,就會在實例中建立該屬性,該屬性就會屏蔽原型中的相同屬性。由於上面講過讀取對象的屬性時,會進行搜索,搜索會先從對象實例先開始搜索,由於對象實例有這個屬性,原型就不必搜索了,就會返回對象實例的的屬性值。

function Person() {
}

Person.prototype.name = "Jason";
Person.prototype.age = 29;
Person.prototype.job = "Web";
Person.prototype.sayName = function() {
    console.log(this.name);
}

var person1 = new Person();
var person2 = new Person();
person1.name = "Cor";
person1.sayName();         //"Cor"
person2.sayName();        //"Jason"

若是繼續可以從新訪問原型中的屬性能夠用delete操做符

delete person1.name;
person1.sayName();       //"Jason"

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

console.log(person1.hasOwnProperty("name"));        //false
person1.name = "Cor";
console.log(person1.hasOwnProperty("name"));        //true;

2.3.2 原型與in操做符

有兩種方式使用in操做符:單獨使用何在for-in循環中使用。在單獨使用時,in操做符會在經過對象能訪問給定屬性時返回true,不管屬性存在於實例仍是原型中

console.log("name" in person1); true

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

function hasPrototypeProperty(object, name) {
    return !object.hasOwnProperty(name) && (name in object);
}

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

function Person(){}
Person.prototype.name = "Jason";
Person.prototype.age = 18;
Person.prototype.job = "Web";
Person.prototype.sayName = function() {
    console.log(this.name);
}

var keys = Object.keys(Person.prototype);
console.log(keys);        //"name,age,job,sayName"

var p1 = new Person();
p1.name = "cor";
p1.age = 11;
var p1keys = Object.keys(p1);
console.log(p1keys);    //"name,age"

全部你想獲得全部實例屬性,不管它是否能夠枚舉,均可以使用Object.getOwnPropertyNames()方法。

var keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys);      //"constructor,name,age,job,sayName"

2.3.3 更簡單的原型語法

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

function Person(){}

Person.prototype = {
    name : "Jason",
    age : 29,
    job : "Web",
    sayName : function() {
        console.log(this.name)
    }
};

用對象字面的方法和原來的方法會有區別: constructor屬性再也不指向Person了。每建立一個函數,就會同時建立它的prototype對象,這個對象也會自動得到constructor屬性。而對象字面量的寫法,本質上重寫了默認的prototype對象,所以constructor屬性也就變成了新對象的constructor屬性(指向Object構造函數),再也不指向Person函數。次數儘管instanceof操做符漢能返回正確的結果,但經過constructor已經沒法肯定對象的類型了。

var friend = new Person();

console.log(friend instanceof Object);  //true
console.log(friend instanceof Person);    //true
console.log(friend.constructor == Person);  //false
console.log(friend.constructor == Object);  //true

若是constructor的值真的很重要,能夠像下面這樣特地將它設置回適當的值。

function Person(){}

Person.prototype = {
    constructor : Person,
    name : "Jason",
    age : 29,
    job : "Web",
    sayName : function() {
        console.log(this.name);
    }
};

默認狀況下,原生的constructor屬性是不可枚舉的,所以若是你使用兼容ES5的JavaScript引擎,能夠試下Object.defineProperty()

function Person(){}

Person.prototype = {
    name : "Jason",
    age : 29,
    job : "Web",
    sayName : function() {
        console.log(this.name);
    }
};
//重設構造函數,只適用ES5兼容的瀏覽器
Object.defineProperty(Person.prototype, "constructor", {
    enumerable: false,
    value: Person
});

2.3.4 原型的動態性

因爲在原型中查找值的過程是一次搜索,所以咱們隊原型對象所作的任何修改都可以當即從實例上反映出來——即便是先建立了實例後修改原型也同樣。

var friend = new Person();

Person.prototype.sayHi = function() {
    alert("hi");
};

friend.sayHi();        //"hi"

即便person實例是在添加新方法以前建立的,但它仍然能夠訪問這個新方法。其緣由能夠歸結爲實例與原型之間鬆散鏈接關係。由於實例與原型之間的鏈接只不過是一個指針,而非一個副本,所以就能夠在原型中找到新的sayHi屬性並返回保存在那的函數。

若是是重寫整個原型對象,狀況就和上面不同了。調用函數時會爲實例添加一個指向最初原型的[[Prototype]]指針,而把原型修改成另一個對象就等於切斷了構造函數與最初原型之間的聯繫。請記住:實例中的指針僅指向原型,而不指向構造函數。

function Person(){}

var friend = new Person();

Person.prototype = {
    constructor : Person,
    name : "Jason",
    age : 29,
    job : "Web",
    sayName : function() {
        console.log(this.name);
    }
};

friend.sayName();    //error

咱們先建立了一個實例,而後重寫了其原型對象。下圖展現了整個過程

clipboard.png

如圖,重寫原型對象切斷了現有原型與任務以前已經存在的對象實例之間的聯繫;friend實例引用的仍然是最初的原型

2.3.5 原生對象的原型

原型模式的重要性不只體如今建立自定義類型方面,就連全部原生的引用類型。都採用這種模式建立的。全部原生引用類型(Object、Array、String,等等)都在其構造函數的原型上定義了方法。

console.log(Array.prototype.sort);            //function(){...}        console.log(String.prototype.substring);    //function(){...}

經過原生對象的原型,咱們也能夠本身定義新方法。

String.prototype.startsWith = function(text) {
    return this.indexOf(text) == 0;
};

var msg = "Hello world!";
console.log(msg.startsWith("Hello")); //true

2.3.6 原型對象的問題

原型的全部屬性是被不少實例共享的,這種共享對於函數很是合適。原型的問題就是其共享的本性形成的。

function Person(){}

Person.prototype = {
    constructor : Person,
    name : "Jason",
    age : 29,
    job : "Web",
    friend : ["Cor", "Sam"],
    sayName : function() {
        console.log(this.name);
    }
}

var person1 = new Person();
var person2 = new Person();

person1.friends.push("Court");
console.log(person1.friends);   //"Cor,Sam,Court"
console.log(person2.friends);   //"Cor,Sam,Court"
console.log(person1.friends === person2.friends);    //true

經過原型共享雖然所有實例均可以共享屬性和方法。但是實例通常都是要有屬於本身的所有屬性。

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

建立自定義類型的最經常使用方式,就是組合使用構造函數模式與原型模式。構造函數模式用於定義實例屬性,而原型模式用於定義方法和共享的屬性。這樣每一個實例都會有本身的一份實例屬性的副本,但同時共享着對方法的引用,最大限度地節省了內存。這種混成模式還支持向構造函數傳遞參數。結合了兩種模式之長。

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Cor", "Sam"];
}

Person.prototype = {
    constructor : Person,
    sayName : function() {
        console.log(this.name);
    }
};

var person1 = new Person("Jason", 18, "Web");
var person2 = new Person("cou", 19, "doctor");

person1.friends.push("Van");
console.log(person1.friends);    //"Cor,Sam,Van"
console.log(person2.friends);    //"Cor,Sam"
console.log(person1.friends === person2.friends); //false
console.log(person1.sayName === person2.sayName); //true

構造函數定義屬性,原型定義共享的方法和屬性。這種構造函數與原型混成的模式,是目前在ECMAScript中使用最普遍,認同度最高的一種建立自定義類型的方法。能夠說,這是用來定義引用類型的一種默認模式。

2.5 動態原型模式

動態原型模式把全部信息都封裝在了構造函數中,而經過構造函數中初始化原型(僅在必要的狀況下),又保持了同時使用構造函數和原型的優勢。簡單說能夠經過檢查某個應該存在的方法是否有效, 來決定是否須要初始化原型。

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;

    //方法
    if(typeof this.sayName != "function") {
        Person.prototype.sayName = function() {
            console.log(this.name);
        };
    }
}

var friend = new Person("Jason", 18, "Web");
friend.sayName();

方法那塊,只在sayName()方法不存在的狀況下,纔會將它添加到原型中。這段代碼只會在初次調用中構造函數時纔會執行。此後,原型已經完成初始化嗎,不須要再作什麼修改了。不過要記住,這裏對原型所作的修改,可以當即在全部實例中獲得反映。所以,這種方法確實能夠說是很是完美。其中if語句檢查的能夠是初始化以後應該存在的任何屬性和方法——沒必要用一大堆if語句檢查每一個屬性的每一個方法;只須要檢查一個便可。對於採用這種模式建立的對象,還可使用instanceof操做符肯定它的類型。

注意:使用動態原型模式時,不能使用對象字面量重寫原型。前面已經解釋過了,若是在已經建立了實例的狀況下重寫原型,那麼就會切斷現有實例與新原型之間的練習。

2.6 寄生構造函數模式

在前面幾種模式都不適用的狀況下,可使用寄生構造函數模式。這種模式的基本思想是建立一個函數,該函數的做用僅僅是封裝建立對象的代碼,而後再返回新建立的對象;但從表面上看,這個函數又很像是典型的構造函數。

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("Jason", 19, "Web");
friend.sayName();     //"Jason"

除了使用new操做符並把使用的包裝函數叫作構造函數以外,這個模式跟工廠模式實際上是如出一轍的。構造函數在不返回值的狀況下,默認會返回新對象的實例。而經過在構造函數的末尾添加一個return語句,能夠重寫調用構造函數時返回的值。

這個模式能夠在特殊的狀況下用來爲對象建立構造函數。假設咱們想建立一個具備額外方法的特殊數組。因爲不能直接修改Array構造函數,所以可使用這個模式。

function SpecialArray() {
    //建立數組
    var values = new Array();
    
    //添加值
    values.push.apply(values, arguments);

    //添加方法
    values.toPipedString = function() {
        return this.join("|");
    };

    //返回數組
    return values;
}

var colors = new SpecialArray("red", "blue", "green");
console.log(colors.toPipedString());   //"red|blue|green"

關於寄生構造函數模式,有一點須要說明: 首先,返回的對象與構造函數或者與構造函數的原型之間沒有關係;也就是說,構造函數返回的對象與在構造函數外部建立對象沒有什麼不一樣。不能依賴instanceof操做符來肯定對象類型。

2.7 穩妥構造函數模式

所謂穩妥對象,指的是沒有公共屬性,並且其方法也不引用this的對象。穩妥對象最適合在一些安全的環境中(這些環境中會禁止使用this和new)

function Person(name, age, job) {
    
    //建立要返回的對象
    var o = new Object();

    //能夠定義私有變量和函數

    //添加方法
    o.sayName = function() {
        console.log(name)''
    }
    
    //返回對象
    return o;
}

注意,在以這種模式建立的對象中,除了使用sayName()方法以外,沒有其餘辦法訪問到name的值。能夠像下面使用穩妥的Person構造函數

var friend = Person("Jason", 18, "Web");
friend.sayName();    //"Jason"

這樣,變量friend中保存的是一個穩妥對象,除了調用sayName()方法外,沒有別的方式能夠訪問其數據成員。

3.繼承

因爲函數沒有簽名,在ECMAScript中沒法實現接口繼承。ECMAScript只支持實現繼承,並且其實現繼承主要是依靠原型鏈來實現的。

3.1 原型鏈

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

回顧下構造函數、原型和實例的關係:

  1. 每一個構造函數都有一個原型對象;

  2. 原型對象都包含一個指向構造函數的指針;

  3. 實例都包含一個指向原型對象的內部指針{{Prototype}}

咱們讓原型對象等於另外一個類型的實例,原型對象將包含一個指向另外一個原型的指針,相應地,另外一個原型也包含着一個指向另外一個構造函數的指針。假如另外一個原型又是另外一個類型的實例,那麼上訴關係依然成立,如此層層遞進,就構成了實例與原型的鏈條。

實現原型鏈的基本模式

function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
}

function SubType(){
    this.subproperty = false;
}

//繼承了SuperType
//SubType的原型對象等於SubperType的實例,
//這樣SubType內部就會有一個指向SuperType的指針從而實現繼承
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function(){
    return this.subproperty;
}

var instance = new SubType();
console.log(instance.getSuperValue());  //true

SubType繼承了superType,而繼承是經過建立SuperType實例,並將該實例賦給SubType.prototype實現的。原來存在於SuperType的實例中的全部屬性和方法,如今也存在於SubType.prototype中了。這個例子中的實例、構造函數和原型之間的關係如圖:

clipboard.png

要注意instance.constructor如今指向的是SuperType,是由於SubType的原型指向了另外一個對象SuperType的原型,而這個原型對象的constructor屬性指向的是SuperType。

經過實現原型鏈,本質上擴展了前面說的原型搜索機制。在經過原型鏈實現繼承的狀況下,搜索過程就得以沿着原型鏈繼續向上。拿上面的例子來講。調用instance.getSuperValue()會經歷三個搜索步驟:

  1. 搜索實例;

  2. 搜索SubType.prototype;

  3. 搜索SuperType.prototype,最後一步纔會找到該方法。

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

1. 別忘記默認的原型

事實上,前面的例子中展現的原型鏈還少一環。全部引用類型默認都繼承了Object,而這個繼承也是經過原型鏈實現的。要記住,全部函數的默認原型都是Object的實例,所以默認原型都會包含一個內布指針,指向Object.prototype。這也正是全部自定義類型都會繼承toString()、valueOf()等默認方法的根本緣由。因此上面的例子展現的原型鏈中還應該包含另外一個繼承層次。

clipboard.png

一句話,SubType繼承了SuperType,而SuperType繼承了Object。當調用instance.toString()時,實際上調用的是保存在Object.prototype中的那個方法。

2. 肯定原型和實例的關係

有兩種方式能夠肯定。

  1. instanceof操做符

只要用這個操做符來測試實例與原型鏈中出現過的構造函數,結果就會返回true。

console.log(instance instanceof Object);    //true    
console.log(instance instanceof SuperType);    //true
console.log(instance instanceof SubType);    //true

因爲原型鏈的關係,能夠說instanceObjectSuperTypeSubType中任何一個類型的實例。所以,都返回true

  1. isPrototypeOf()方法

一樣,只要是原型鏈中出現過得原型,均可以說是該原型鏈所派生的實例的原型。

console.log(Object.prototype.isPrototypeOf(instance));      //true
console.log(SuperType.prototype.isPrototypeOf(instance)); //true
console.log(SubType.prototype.isPrototypeOf(instance));   //true

3. 謹慎地定義方法

子類型(上例的SubType)有時候須要重寫超類型(上例的SuperType)中的某個方法,或者須要添加超類型中不存在的的某個方法。但無論怎樣,給原型添加方法必定要放在替換原型的語句以後。

function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
}

function SubType(){
    this.subproperty = false;
}

//繼承超類型
SubType.prototype = new SuperType;

//添加新方法
SubType.prototype.getSubValue = function(){
    return this.subproperty;
}

//重寫超類型中的方法
SubType.prototype.getSuperValue = function(){
    return false;
}

當經過SubType的實例調用getSuperValue()時,調用的就是這個重寫的方法,這是由於搜索機制搜索首先從實例中搜索而後到子類型的原型再到超類型的原型,而後子類型重寫了該方法,搜索機制首先在子類型的原型中找到了該方法就不會繼續繼續搜索了。但經過SuperType的實例調用getSuperValue()時,還會繼續調用原來的那個方法。這裏要注意的是,必須在用SuperType的實例替換原型以後,再定義着兩個方法。

還有一點須要注意,在經過原型鏈實現繼承時,不能使用對象字面量建立原型方法。這樣作會重寫原型鏈。

function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
}

function SubType(){
    this.subproperty = false;
}

//繼承了SuperType
SubTyoe.prototype = new SuperType();

//使用字面量添加新方法,會致使上一行代碼無效
SubType.prototype = {
    getSubValue : function(){
        return this.subproperty;
    },
    someOtherMethod : function(){
        return false;
    }
}

var instance = new SubType();
console.log(instance.getSuperValue());  //error!

因爲如今的原型包含的是一個Object的實例,而非SuperType的實例,所以咱們設想中的原型鏈已經被切斷了。SubTypeSuperType之間已經沒有關係了。

4. 原型鏈的問題

原型鏈最主要的問題來自包含引用類型值的原型。包含應用類型值的原型屬性會被全部實例共享;而這也正是爲何要在構造函數中,而不是在原型對象中定義屬性的緣由。在經過原型來實現繼承時,原型實際上會變成另外一個類型的實例。因而,原先的實例實際上會變成另外一個類型的實例。因而,原生的實例屬性也就瓜熟蒂落地變成了如今的原型屬性了。

function SuperType(){
    this.colors = ["red", "blue", "green"];
}

function SubType(){
}

SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);  //"red, blue, green, black"

var instance2 = new SubType();
console.log(instance2.colors);    //"red, blue, green, black"

這個例子中的SuperType構造函數定義了一個colors屬性,該屬性包含一個數組(引用類型值)。SuperType的每一個實例都會有各自包含本身數組的colors屬性。當SubType經過原型鏈繼承了SuperType以後,SubType.prototype就變成了SuperType(),因此它也用用了一個colors屬性。就跟專門建立了一個SubType.prototype.colors屬性同樣。結果SubType得全部實例都會共享這一個colors屬性。

原型鏈的第二個問題是:在建立子類型的實例時,不能向超類型的構造函數傳遞參數。實際上,應該說是沒有辦法在不影響全部對象實例的狀況下,給超類型的構造函數傳遞參數。因此在實際運用中不多會單獨使用原型鏈。

3.2 借用構造函數

在子類型構造函數的內部調用超類型構造函數。函數只不過是在特定環境中執行代碼的對象,所以經過使用apply()call()方法也能夠在(未來)新建立的對象上執行構造函數。

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

function SubType(){
    //繼承了SuperType,同時還傳遞了參數
    SuperType.call(this, "Jason");

    //實例屬性
    this.age = 18;
}

var instance1 = new SubType();
instance1.colors. push("black");
console.log(instance1.colors);         //"red,blue,green,black"
console.log(instance1.name);        //"Jason"
console.log(instance1.age);            //18

var instance2 = new SubType();
console.log(instance2.colors);        //"red,blue,green"

爲了確保SuperType構造函數不會重寫子類型屬性,能夠再調用超類型構造函數後,再添加應該在子類型中定義的屬性。

借用構造函數的問題

若是僅僅是借用構造函數那麼也將沒法避免構造函數模式存在的問題——方法都在構造函數中定義,所以函數服用就無從談起了。而卻,在超類型的原型中定義的方法,對子類型而言也是不可見的,結果全部類型都只能使用構造函數模式。借用構造函數技術也是不多單獨使用的。

3.3 組合繼承

組合繼承(combination inheritance),有時候也叫作爲經典繼承,指的是將原型鏈和借用構造函數的技術組合到一塊,從而發揮兩者之長的一種繼承模式。其背後的思路是使用原型鏈實現對原型屬性和方法的繼承,而經過借用構造函數來實現對實例屬性的繼承。這樣,既經過在原型上定義方法實現了函數複用,又能保證每一個實例都有它本身的屬性。

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

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("Jason", 18);
instance1.colors.push("black");
console.log(instance1.colors);    //"red,blue,green,black"
instance1.sayName();            //"Jason"
instance1.sayAge();                //18

var instance2 = new SubType("Cor", 20);
console.log(instance2.colors)l    //"red,blue,green"
instance2.sayName();            //"Cor"
instance2.sayAge();                //20

這樣一來,就可讓兩個不一樣的SubType實例即分別用有本身的屬性——包括colors屬性,又可有使用相同的方法了。如圖:

clipboard.png

組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優勢,成爲JavaScript中最經常使用的繼承模式。並且,instanceofisPrototypeof()也可以用於識別基於組合繼承建立的對象。

3.4 原型式繼承

基本思想是藉助原型能夠基於已有的對象建立新對象,同時還沒必要所以建立自定義類型。

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

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

var person = {
    name: "Jason",
    friends: ["Cor", "Court", "Sam"]
};

var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends);        //"Cor,Court,Sam,Rob,Barbie"

這種原型式繼承,要求你必須有一個對象能夠做爲另外一個對象的基礎。若是有這麼一個對象的話,能夠把它傳遞給object()函數,而後再根據具體需求對獲得的對象加以修改便可。在這個例子中,能夠做爲另外一個對象的基礎是person對象,因而咱們把它傳入到object()函數中,而後該函數就會返回一個新對象。這個新對象將person做爲原型,因此它的原型中就包含一個基本類型值屬性和一個引用類型值屬性。這覺得着person.friends不只屬於person全部,並且也會被anotherPerson以及yetAnotherPerson共享。實際上,這就至關於又建立了person對象的兩個副本。

ECMAScript5經過新增Object.create()方法規範化了原型式繼承。這個方法接收兩個參數:一個用於作新對象原型的對象和(可選的)一個爲新對象定義額外屬性的對象。在傳入一個參數的狀況下,Object.create()object()方法的行爲相同

var person = {
    name: "Jason",
    friends: ["Cor", "Court", "Sam"]
};

var anotherPerson = Object.create(person);
anotherPerson.friends.push("Rob");


var yetAnotherPerson = Object.create(person, {
    name: {
        value: "Greg"
    }
});
yetAnotherPerson.friends.push("Barbie");

console.log(yetAnotherPerson.name);    //"Greg"
console.log(anotherPerson.name);    //"Jason"
console.log(person.friends);        //"Cor,Court,Sam,Rob,Barbie"

Object.create()方法的第二個參數與Object.defineProperties()方法的第二個參數格式相同:每一個屬性都是經過本身的描述符定義的。以這種方式制定的任何屬性會覆蓋原型對象上的同名屬性。

別忘了,包含引用類型值的屬性始終都會共享相應的值,就像使用原型模式同樣

3.5 寄生式模式

寄生式(parasitic)繼承是與原型式繼承緊密相關的一種思路。寄生式繼承的思路與寄生構造函數和工廠模式相似,即建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來加強對象,最後再像真的是它作了全部工做同樣返回對象。

function createAnother(original){
    var clone = object(original);     //經過調用函數建立一個新對象
    clone.sayHi = function(){         //以某種方式來加強這個對象
         alert("Hi");
    };
    return clone;                     //返回這個對象
}

能夠像下面這樣來使用createAnother()函數:

var person = {
    name: "Jason",
    friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi();     //"hi"

這個例子中的代碼基於person返回了一個新對象——anotherPerson。新對象不只具備person的全部屬性和方法,並且還有本身的sayHi()方法。

3.6 寄生組合式繼承

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

function SuperType(name){
    this.name = name;
    this.colors = ["red", "bule", "grenn"];
}

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);
}

有註釋的兩行代碼是調用SuperType構造函數的代碼,第一次調用SuperType構造函數時,SubType.prototype會有SuperType的實例屬性。第二次調用SuperType的構造函數時SubType會在構造函數中添加了SuperType的實例屬性。當建立SubType的實例它的[[Prototype]]和自身上都有相同屬性。根據搜索機制自身的屬性就會屏蔽SubType原型對象上的屬性。等於原型對象上的屬性是多餘的了。如圖:

clipboard.png

如圖所示,有兩組namecolors屬性:一組在實例上,一組在SubType原型中。這就是調用兩次SuperType構造函數的結果。解決辦法是——寄生組合式繼承

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

function inheritPrototype(subType, superType){
    var prototype = object(superType.prototype);   //建立對象
    prototype.constructor = subType;               //加強對象
    subType.prototype = prototype                  //指定對象
}

這個實力的inheritPrototype()函數實現了寄生組合式繼承的最簡單形式。這個函數接受兩個參數:子類型構造函數和超類型構造函數。在函數內部,第一步是建立超類型原型的一個副本。第二部是爲建立的副本添加constructor屬性,從而彌補因重寫原型而失去的默認的constructor屬性。最後一步,將新建立的對象(即副本)賦值給子類型的原型。

function object(o){
    function F(){}    //建立個臨時構造函數
    F.prototype = o;  //superType.prototype
    return new F();   //返回實例
}

function inheritPrototype(subType, superType){
    /*  建立對象
        傳入超類型的原型,經過臨時函數進行淺複製,F.prototype的指針就指向superType.prototype,在返回new F()    
    */
    var prototype = object(superType.prototype);   
    prototype.constructor = subType;               //加強對象
    /*  指定對象
        子類型的原型等於F類型的實例,當調用構造函數建立一個新實例後,該實例會包含一個[[prototype]]的指針指向構造函數的原型對象,因此subType.prototype指向了超類型的原型對象這樣實現了繼承,由於構造函數F沒有屬性和方法這樣就子類型的原型中就不會存在超類型構造函數的屬性和方法了。
    */
    subType.prototype = prototype                  //new F();
}

function SuperType(name){
    this.name = name;
    this.colors = ["red", "bule", "grenn"];
}

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

function SubType(name, age){
    SuperType.call(this, name);          
 
    this.age = age;
}

inheritPrototype(SubType, SuperType);A

SubType.prototype.sayAge = function(){
    console.log(this.age);
}

var ins1 = new SubType("Jason", 18);

下圖是我本身的理解:
clipboard.png

最後,若有錯誤和疑惑請指出,多謝各位大哥

相關文章
相關標籤/搜索