JavaScript 面向對象的程序設計(二)JavaScript 創建對象——原型prototype、創建模式

使用同一個接口創建很多對象,會產生大量的重復代碼。為解決這個問題嗎,人們開始使用工廠模式的一種變體。

工廠模式

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

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("Bert", 24, "lalala");
var person2 = createPerson("Greg", 22, "Doctor");

函數 createPerson() 能夠根據接受的參數來構建一個包含所有必要信息的 Person 對象。可以無數次地調用這個函數,而每次它優惠返回一個包含三個屬性一個方法的對象。

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

構造函數模式

ECMAScript 中的構造函數可以用來創建特定類型的對象。像 Object 和 Array 這樣的原生構造函數,在運行時會自動出現在執行環境中,此外,也可以創建自定義的構建函數,從而定義自定義對象類型的屬性和方法。

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

var person1 = new Person("Bert", 24, "lalala");
var person2 = new Person("Greg", 22, "Doctor");

上例中,Person() 函數取代了 createPerson() 函數。Person() 中的代碼除了與 createPerson() 中相同的部分外,還存在以下不同之處:

  • 沒有顯式地創建對象;
  • 直接將屬性和方法賦給了 this 對象;
  • 沒有 return 語句;

此外,還應該注意到函數名 Person 使用的是大寫字母 P。按照慣例,構造函數始終都應該以一個大寫字母開頭,而非構造函數則應該以一個小寫字母開頭。這個做法借鑒自其它OO語言,主要是為了區別于 ECMAScript 中的其他函數;因為構造函數本身也是函數,只不過可以用來創建對象而已。

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

  1. 創建一個新對象;
  2. 將構造函數的作用域賦給新對象(因此 this 就指向了這個新對象);
  3. 執行構造函數的代碼(為這個新對象添加屬性);
  4. 返回新對象;

在前面例子的最后,person1 和 person2 分別保存著 Person 的一個不同的實例。這兩個對象都有一個 constructor (構造函數)屬性,該屬性指向 Person。

alert(person1.constructor == Person) //true
alert(person2.constructor == Person) //true

對象的 constructor 屬性最初是用來表示對象類型的,但是提到檢測對象類型,還是 instanceof 操作符要更可靠一些:

alert(person1 instanceof Object) //true
alert(person1 instanceof Object) //true
alert(person2 instanceof Person) //true
alert(person2 instanceof Person) //true

創建自定義的構造函數意味著將來可以將它的實例標識為一種特定的類型;而這正是構造函數模式勝過工廠模式的地方。

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

將構造函數當做函數

構造函數與其他函數的唯一區別,就在于調用它們的方式不用。不過,構造函數畢竟也是函數,不存在定義構造函數的特殊語法。任何函數,只要通過 new 操作符來調用,那它就可以作為構造函數;而任何函數,如果不通過 new 操作符來調用,那它跟普通函數也不會有什么兩樣。

// 當做構造函數使用
var person1 = new Person("Bert", 24, "lalala");
person.sayName(); // Bert

// 作為普通函數調用
Person("Greg", 27, "Doctor"); // 添加到window
window.sayName(); // "Greg"

// 在另一個對象的作用域中調用
var o = new Object();
Person.call(o, "Kristen", 21, "Nurse");
window.sayName(); // "Kristen"

上例中前兩行代碼展示了構造函數的典型用法。 接下來的兩行代碼展示了不適用 new 操作符調用。 最后,使用Call()調用(可以使用apply())。

構造

構造函數模式雖然好用,但也并非沒有缺點。使用構造函數的主要問題,就是每個方法都要在每個實例上重新創建一遍。在前面的例子中,person1 和 person2 都有一個名為 sayName() 的方法,但那兩個方法不是同一個 Function 的實例。(ECMAScript 中的函數式對象),因此沒定義一個函數,也就是實例化了一個對象。

使用構造函數實例的對象,會導致不同的作用域鏈和標識符解析,但創建 Function 新實例的機制仍然是相同的。因此,不同實例上的同名函數不是相等的:

alert(person1.sayName == person2.sayName); // false

創建兩個完成同樣任務的 Function 實例沒有必要;有 this 對象在,根本不用再執行代碼前就把函數半丁到特定對象上,因此,通過把函數定義轉移到構造函數外部來解決這個問題:

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

function sayName() {
    alert(this.name);
};

var person1 = new Person("Bert", 24, "lalala");
var person2 = new Person("Greg", 22, "Doctor");

上例中, person1 和 person2 對象就共享了在全局作用域中定義的同一個 sayName() 函數。可是新問題又來了;在全局作用域有點名不副實。而更讓人無法接受的是:如果對象需要定義很多方法,那么就要定義很多個全局函數,于是我們這個自定義的引用類型就絲毫沒有封裝性可言了。

這些問題可言通過使用原型鏈模式來解決。

原型模式

我們創建的每個函數都有一個 prototype (原型)屬性,這個屬性是一個指針,指向一個對象,而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。如果按照字面意思來理解,那么 prototype 就是通過調用構造函數而創建的那個對象實例的原型對象。使用原型對象的好處是可以讓所有對象實例共享它所包含的屬性和方法。換句話說,不必在構造函數中定義對象實例的信息,而是可以將這些信息直接添加到原型對象中。

function Person() {}

Person.prototype.name = "Bert";
Person.prototype.age = "20";
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
    alert(this.name);
}

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

var person2 = new Person();
person2.sayName(); //Bert

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

我們將sayName() 方法和所有屬性直接添加到了 Person 的 prototype 屬性中,構造函數變成了空函數。即使如此,也仍然可以通過調用構造函數來創建新對象,而且新對象還會具有相同的屬性和方法。但與構造函數模式不同的是,新對象的這些屬性和方法是由所有實例共享的。換句話說,person1 和 person2 訪問的都是同一組屬性和同一個 sayName() 函數。

理解原型對象

無論什么時候,只要創建了一個新函數,就會根據一組特定的規則為該函數創建一個 prototype 屬性,這個屬性指向函數的原型對象。在默認情況下,所有的原型對象都會自動獲得一個 contructor (構造函數)屬性,這個屬性包含一個指向 prototype 屬性所在函數的指針。

拿前面例子來說,Person.prototype.constructor 指向 person。而通過這個構造函數,我們還可以繼續為原型對象添加其他屬性和方法。

創建了自定義的構造函數之后,其原型對象默認只會取得 constructor 屬性;至于其他方法,則都是從Object繼承來的。當調用構造函數創建一個新實例后,該實例的內部將會包含一個指針(內部屬性),指向構造函數的原型對象。ECMA-262 第五版中把這個指針叫 [[prototype]] 。雖然在腳本中沒有標準的方式訪問 [[prototype]],但Firefox、Safari和Chrome在每個對象上都支持一個屬性 __proti__;而在其他的實現中,這個屬性對腳本則是完全不可見的。不過,要明確的真正重要的一點就是,這個鏈接存在于實例與構造函數的原型對象之間,而不是存在于實例與構造函數之間。

Paste_Image.png

上圖展示了 Person 構造函數、Person的原型屬性以及 Person 現有的兩個實例之間的關系。

Person.prototype 指向了原型對象,而 Person.prototype.constructor 又指回了 Person。原型對象中除了包含 contructor 屬性之外,還包括后來添加的其他屬性。

Person的每個實例 ---- person1 和 person2 都包含一個內部屬性,該屬性僅僅指向了 Person.prototype;換句話說,它們與構造函數沒有直接的關系。此外,要格外注意的是,雖然這兩個實例都不包含屬性和方法,但我們卻可以調用 person1.sayName() 。 這是通過查找對象屬性的過程來實現的。

雖然在所有實現中都無法訪問到 [[Prototype]],但可以通過 isPrototypeOf() 方法來確定對象之間是否存在這種關系。從本質上講,如果 [[Prototype]] 指向調用 isPrototype() 方法的對象(Person.prototype),那么這個方法就返回 true:

alert(Person.prototype.isPrototypeOf(person1)); //true;
alert(Person.prototype.isPrototypeOf(person2)); //true;

這里,我們用原型對象的 isPrototypeOf() 方法測試了 person1 和 person2。因為它們內部都有一個指向 Person.prototype 的指針,因此都返回了 true。

注釋:所以 isPrototypeOf()方法內部是做了判斷,person1.prototype === Person? 如果對象的原型與 Person 相等,那么就可以證明是 Person 的實例

ECMAScript 5 增加了一個新方法,叫 Object.getPrototypeOf(),在所有支持的實現中,這個方法返回 [[Prototype]] 的值:

alert(Object.getPrototypeOf(person1) == Person.prototype); // true
alert(Object.getPrototypeOf(person1).name); // Bert

這里的第一行代碼只是確定 Object.getPrototypeOf() 返回的對象實際就是這個對象的原型。第二行代碼取得了原型對象中 name 屬性的值,也就是 "Bert"。使用 Object.getPrototypeOf() 可以方便地取得一個對象的原型,而這在利用原型實現繼承的情況下是非常重要的。

支持這個方法的瀏覽器有 IE9+、Firefox 3.5+、Safari 5+、Opera 12+ 和 chrome。

每當代碼讀取某個對象的某個屬性時,都會執行一次搜索,目標是具有給定名字屬性。搜索首先從對象實例本身開始。如果在實例中找到了具有給定名字的屬性,則返回該屬性的值;如果沒有找到,則繼續搜索指針指向的原型對象,在原型對象中查找具有給定名字的屬性。如果在原型對象中找到了這個屬性,則返回該屬性。

也就是說,在我們調用 person1.sayName() 的時候,會先后執行兩次搜索。首先,解析器會問:"實例 person1 有 sayName 屬性嗎?"回答:"沒有。"然后,它繼續搜索,再問:"person1 的原型有 sayName屬性嗎?"回答:"有。"于是,他就讀取那個保存在原型對象中的函數。當我們調用 person2.sayName() 時,將會重現相同的搜索過程,得到相同的結果。而這整是多個對象實例共享原型所保存的屬性和方法的基本原理。

原型最初只包含 constructor 屬性,該屬性也是共享的,因此可以通過對象實例訪問 。

雖然可以通過對象實例訪問保存在原型中的值,但卻不能通過對象實例重寫原型中的值。如果我們在實例中添加了一個屬性,而該屬性與實例原型中的一個屬性同名,那我們就在實例中創建該屬性,該屬性將會屏蔽原型中的那個屬性。

function Person() {}

Person.prototype.name = "Bert";
Person.prototype.age = "20";
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
    alert(this.name);
}

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

person1.name = "Greg";

alert(person1.name); // Greg —— 來自實例
alert(person2.name); // Bert —— 來自原型

上例中,person1 的 name 被一個新值給屏蔽了。但無論訪問 person1.name 還是訪問 person2.name 都能夠正常的返回值,即分別是 "Greg"(來自對象實例)和 "Bert" (來自原型)。

當為對象實例添加一個屬性時,這個屬性就會屏蔽原型對象中保存的同名屬性;換句話說,添加這個屬性只會阻止我們訪問原型中的那個屬性,但不會修改那個屬性。即使將這個屬性設置為 null,也只會在實例中設置實例的屬性,而不會恢復其指向原型的屬性。不過,使用 delete 操作符則可以完全刪除實例屬性,從而讓我們能夠重新訪問原型中的屬性。

function Person() {}

Person.prototype.name = "Bert";
Person.prototype.age = "20";
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
    alert(this.name);
}

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

person1.name = "Greg";

alert(person1.name); // Greg —— 來自實例
alert(person2.name); // Bert —— 來自原型

person1.name = null;
alert(person1.name); // null

delete person1.name;
alert(person1.name); // Bert —— 來自原型

在這個修改后的例子中,將 person1.name 賦值為 null,后訪問結果顯示 "null",使用 delete 操作符刪除了 person1.name,之前它保存的是實例的值。把它刪除以后,就恢復了對原型中name屬性的鏈接。因此接下來調用 person1.name 時,返回的就是原型中 name 屬性的值了。

使用 hasOwnProperty() 方法可以監測一個屬性是存在于實例中,還是存在于原型中。這個方法(從 Object 繼承)只在給定屬性存在于對象實例中時,才會返回 true。

function Person() {}

Person.prototype.name = "Bert";
Person.prototype.age = "20";
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
    alert(this.name);
}

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

alert(person1.hasOwnProperty("name")); // false


person1.name = "Greg";
alert(person1.name); // Greg —— 來自實例
alert(person1.hasOwnProperty("name")); // true

alert(person2.name); // Bert —— 來自原型
alert(person2.hasOwnProperty("name")); // false

person1.name = null;
alert(person1.name); // null
alert(person1.hasOwnProperty("name")); //true

delete person1.name;
alert(person1.name); // Bert —— 來自原型
alert(person1.hasOwnProperty("name")); // false

通過使用 hasOwnProperty() 方法,什么時候訪問的是實例屬性,什么時候訪問的是原型屬性就一清二楚了。調用 person1.hasOwnProperty("name")時,知有當 person1 重寫 name 屬性后才會返回 true,因為只有這個時候 name 才是一個實例屬性,而非原型屬性。

ECMAScript 5 的 Object.getOwnPropertyDescriptor() 方法只能用于實例屬性,要取得原型屬性的描述符,必須直接在原型對象上調用 Object.getOwnPropertyDescriptor() 方法

原型與 in 操作符

有兩種方式使用 in 操作符:單獨使用和在 for-in 循環中使用。在單獨使用時,in 操作符會在通用對象能夠訪問給定屬性時返回 true,無論該屬性存在于實例中還是原型中。

function Person() {}

Person.prototype.name = "Bert";
Person.prototype.age = "20";
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
    alert(this.name);
}

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

alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); // true

person1.name = "Greg";
alert(person1.name); // Greg —— 來自實例
alert(person1.hasOwnProperty("name")) // ture
alert("name" in person1) // true


alert(person2.name); // Bert —— 來自原型
alert(person2.hasOwnProperty("name")); //false
alert("name" in person2); // true

delete person1.name;
alert(person1.name); // Bert —— 來自原型
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); // true

上例代碼執行的整個過程,name 屬性要么是直接在對象上訪問到的,要么是通過原型訪問到的。因此,調用 "name" in person1 始終都返回true,無論該屬性存在于實例中還是存在于原型中。同時使用 hasOwnProperty() 方法和 in 操作符,就可以確定該屬性到底是存在于對象中,還是存在于原型中。

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

由于 in 操作符只要通過對象能夠訪問到屬性就返回 true, hasOwnProperty() 返回 fasle,就可以確定屬性是原型中的屬性。

在使用 for-in 循環時,返回的是所有能夠通過對象訪問的、可枚舉的(enumerated)屬性,其中既包括存在于實例中的屬性,也包括存在于原型中的屬性。屏蔽了原型中不可枚舉的屬性(即將[[Enumerable]]標記的屬性)的實例屬性也會在 for-in 循環中返回,因為根據規定,所有開發人員定義的屬性都是可枚舉的 ---- 只有在IE8及更早版本中例外。

IE早期版本的實現中存在一個bug,即屏蔽不可枚舉屬性的實例屬性不會出現在 for-in 循環中。

var o = {
    toString: function() {
        return "My Object";
    }
}

for (var prop in o) {
    if(prop == "toString") {
        alert("Found toString") // 在IE中不顯示
    }
}

當上例代碼運行時,應該會顯示一個警告框,表明找到了 toString() 方法。這里的對象 o 定義了一個名為 toString() 的方法,該方法屏蔽了原型中(不可枚舉)的 toSting() 方法。在IE中,由于其實現認為原型的 toSting() 方法被打上了 [[Enumerable]] 標記就應該跳過該屬性,結果我們就不會看到警告框。該Bug會影響默認不可枚舉的所有屬性和方法,包括:hasOwnProperty()、toString()和ValueOf()。

要取得對象上所有可枚舉的實例屬性,可以使用 ECMAScript 5 的 Object.keys() 方法,這個方法接受一個對象作為參數,返回一個包含所有可枚舉屬性的字符串數組。

function Person() {};

Person.prototype.name = "Bert";
Person.prototype.age = "20";
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
    alert(this.name);
}

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

var p1 = new Person();
p1.name = "Rob";
p1.age = 31;
var p1keys = Object.keys(p1);
alert(p1keys); // name,age

這里,變量 keys 中將保持一個數組,數組中是字符串 name、age、job和sayName。這個順序也是它們在 for-in 循環中出現的順序。如果是通過 Person 的實例調用,則 Object.keys() 返回的數組只包含 name 和 age。

如果想要得到所有屬性,無論它是否枚舉,都可以使用 Object.getOwnPropertyNames() 方法。

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

更簡單的原型語法

從視覺上更好的封裝原型功能,做法是用一個包含所有屬性和方法的對象字面量來重寫整個原型對象。

function Person() {}

Person.prototype = {
    name: "Bert",
    age: "20",
    job: "Software Engineer",
    sayName: function() {
        alert(this.name);
    }
}

上例為 Person.prototype 設置為等于一個以對象字面量形式創建的新對象。

這樣做會使得 constructor 屬性不再指向 Person 了。

每創建一個函數,就會同時創建它的 prototype 對象,這個對象也會自動獲取得 constructor 屬性。而我們在這里使用的語法,本質上完全重寫了默認的 prototype 對象,因此 constructor 屬性(指向 Object 構造函數),不在指向 Person 函數。

盡管 instanceof 操作符還能返回正確的結果,但通過 constructor 已經無法確定對象的類型了。

var friend = new Person();

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

用 instanceof 操作符測試 Object 和Person 仍然返回 true,但 constuctor 屬性則等于 Object 而不等于 Person 了。如果 constructor 的值很重要,可以特意將 constructor 設置回適當值。

function Person() {}

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

上例代碼特意包含了一個 constructor 屬性,并將它的值設置為 Person,從而確保了通過該屬性能夠訪問到適當的值。

注意:以這種方式重設 constructor 屬性會導致它的 [[Enumerable]] 特性被設置為 true。默認情況下,原生的 constructor 屬性是不可枚舉的,因此如果你使用兼容 ECMAScript 5 的 JavaScript 引擎,可以試試 Object.defineProperty()。

function Person() {}

Person.prototype = {
    name: "Bert",
    age: "20",
    job: "Software Engineer",
    sayName: function() {
        alert(this.name);
    }
}

// 重設構造函數,只適用于 ECMAAScript 5 兼容的瀏覽器
Object.defineProperty(Person.prototype, "constructor", {
    enumerable: false,
    value: Person
})

原型的動態性

由于在原型中查找值的過程是一次搜索,因此我們隊原型對象所做的任何修改都能夠立即從實例上反映出來 ---- 即使是先創建了實例后修改原型也是一樣。

var friend = new Person();
Person.prototype.sayHi = function() {
    alert("Hi");
}

friend.sayHi(); // Hi

盡管可以隨時為原型添加屬性和方法,并且修改能夠立即在所有對象實例中反映出來,但如果是重寫整個原型對象,那么情況就不一樣了。

調用構造函數時會為實例添加一個指向最初原型的 [[prototype]] 指針,而把原型修改為另一個對象就等于切斷了構造函數與最初原型之間的聯系。

prototype 是指向原型的地址,而修改 prototype 的值就是將其指向一個新的地址。

注意:實例中指針僅指向原型,而不指向構造函數。

function Person() {}

var friend = new Person();

Person.prototype = {
    name: "Bert",
    age: "20",
    job: "Software Engineer",
    sayName: function() {
        alert(this.name);
    }
}

friend.sayName(); // error

原生對象的原型

原型模式的重要性不僅體現在創建自定義類型方面,就連所有原生的引用類型,都是采用這種模式創建的。所有原生引用類型(Object、String、Array等)都在其構造函數的原型上定義了方法。

在 Array.prototype 中可以找到 sort() 方法,而在 String.prototype 中可以找到 substring() 方法。

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

原型對象的問題

原型模式也不是沒有確定,它省略了為構造函數傳遞初始化參數,結果所有實例在默認情況下都將取得相同的屬性。

原型中所有屬性都被很多實例共享,這種共享對于引用類型的對象的使用就存在較大的問題。

function Person() {}

Person.prototype = {
    constructor: Person,
    name: "Bert",
    age: "20",
    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

Person.prototype 對象由一個名為 friends 的屬性,該屬性包含一個字符串數組。然后,創建了 Person 的兩個實例。

此時,修改了 person1.friends 引用的數組,因為 friends 是在原型上共享的屬性,導致 person2.friends 的值也跟著改變。

正式這個原因所以很少有人單獨使用原型模式。

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

創建自定義類型的最常見方式,就是組合使用構造函數與原型模式,構造函數模式用于定義實例屬性,而原型模式用于定義方法和共享的屬性。

構造 + 原型 結合使用非常牛逼,每個實例可以初始化自己的屬性,且又有共享的屬性,最大限度地節省了內存。

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

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

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); // shelby,Court,Van
alert(person2.friends); // shelby,Court
alert(person1.sayName === person2.sayName); // true
alert(person1.friends === person2.friends); // false

這種構造函數與原型混成的模式,是目前在 ECMAScript 中使用最廣泛、認同度最高的一種創建自定義類型的方法。可以說這是用來定義引用類型的一種默認模式。

動態原型模式

把所有信息都封裝在構造函數中,而通過在構造函數中初始化原型(僅在必要的情況下),又保存了同時使用構造函數和原型的優點。

通過檢查某個應用存在的方法是否有效,來決定是否需要初始化原型。

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

    if (typeof this.sayName != "function") {
        Person.prototype.sayName = function() {
            alert(this.name);
        }
    }
}

var friends = new Person("Bert", 24, "Software Engineer");
friends.sayName();

注意:這里只在 sayName() 方法不存在的情況下,才會將它添加到原型中。這段代碼只會在初次調用構造函數時才會將它添加到原型中。不過,這里對原型所做的修改,能夠立即在所有實例中得到反映。

使用動態原型模式時,不能使用對象字面量重寫原型。如果在已經創建了實例的情況下,重寫原型,那么就會切斷現有實例與新原型之間的聯系。

寄生構造函數模式

這種模式的基本思想是創建一個函數,該函數的作用僅僅是封裝創建對象的代碼,然后再返回新創建的對象;但從表面上看,這個函數又很想是典型的構造函數。

function Person(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 friend = new Person("Bert", 24, "Software Engineer");
friend.sayName(); // Bert

除了使用 new 操作符并把使用的包裝函數叫做構造函數之外,這個模式跟工廠模式其實是一模一樣的。假設想穿件一個具有額外方法的特殊數組。由于不能直接修改 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");
alert(colors.toPipedString()); // red|blue|green

關于寄生構造函數模式,有一點需要注意:首先,返回的對象與構造函數或者與構造函數的原型屬性之間沒有關系;也就是說,構造函數返回的對象與在構造函數外部創建的對象沒有什么不同。不能依賴 instanceof 操作符來確定對象類型。由于存在上述問題,建議在可以使用其他模式的情況下,不要使用這種模式。

穩妥構造函數模式

穩妥對象指的是沒有公共屬性,而且其他方法也不引用 this 的對象。穩妥對象最適合在一些安全的環境中(這些環境中會禁止使用 this 和 new),或者在防止數據被其他應用程序(如Mashup程序)改動時使用。

穩妥構造函數遵循與寄生構造函數類似的模式,但有兩點不同:一是新創建對象的實例方法不引用 this; 二十不使用 new 操作符調用構造函數。

function Person(name, age, job) {
    var o = new Object();
    name = name;
    age = age;
    job = job;

    o.sayName = function() {
        alert(name);
    }
    return o;
}

這樣,變量 person 中保存的是一個穩妥對象,而除非調用 sayName() 方法外,沒有別的方式可以訪問其數據成員。即使有其他代碼會給這個對象添加方法或數據成功,但也不可能有別的辦法訪問摻入到構造函數中的原始數據。

備注:把函數內部的值私有化,對外暴露訪問方式,提升函數內部屬性的安全性。與寄生構造函數模式類似,這種模式用 instanceof 操作符對這種對象沒有意義。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容