面向對象的語言有一個標誌,那就是它們都有“類”的概念,通過類可以創建任意多個具有相同屬性和方法的對象。JavaScript 中沒有類的概念,因此它的面向對象與基於類的語言中的對象有所不同。
JavaScript 對對象的定義是:無序屬性的集合,其屬性可以包含基本值、對象或者函數。可以把 JavaScript 對象理解成散列表,即一組名值對,其中的值可以是數據或函數。
那麼想要創建自定義對象,有以下幾種常用方法:
1.使用 Object 構造函數:
var person = new Object(); person.name = "Lucy"; person.age = 24; person.job = "nurse"; person.sayName = function() { console.log(this.name); };
2.使用對象字面量:
var person = { name: "Lucy", age: 24, job: "nurse", sayName: function() { console.log(this.name); } };
上面這兩種方式雖然都能用來創建單個對象,但是這些方式有一個明顯缺點:使用同一個接口創建很多對象時,會產生大量的重複代碼,因此產生了工廠模式。
3.工廠模式:
因爲 JavaScript中不能創建類,因此出現了下面這種函數:
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 = function("Lucy", 24, "nurse"); var person2 = function("Tom", 22, "engineer");
不難看出,工廠模式很方便地解決了創建多個相似對象的問題。不過,通過工廠模式創建的對象並不能使用 instanceof 關鍵字明確知道新創建的對象的類型,隨之而來的方案是採用構造函數模式。
4.構造函數模式:
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("Lucy", 24, "nurse"); var person2 = new Person("Tom", 22, "engineer");
對比工廠模式,構造函數模式沒有顯式地創建對象,直接將屬性和方法賦給了 this 對象,並且沒有 return 語句。另外,和其他 OO 語言相同,構造函數始終都應該以一個大寫字母開頭,使其能夠和非構造函數區分開來。最後,使用構造函數模式需要用到 new 關鍵字,經歷以下四步:
(1)創建一個新對象;
(2)將構造函數的作用域賦給新對象(即 this 指向這個新對象);
(3)執行構造函數總的代碼;
(4)返回新對象。
通過構造函數創建的對象會有一個 constructor 屬性,該屬性指向其構造函數:
person1.constructor === Person //true person2.constructor === Person //true
對象的 constructor 屬性最初是用來標識對象類型的,不過,在檢測對象類型時,使用 instanceof 關鍵字更可靠:
person1 instanceof Person //true person2 instanceof Person //true
構造函數模式相對於工廠模式的一大步是構造函數模式解決了工廠模式無法解決的對象識別問題。
雖然構造函數模式相對於工廠模式來說已經是加強版,但也存在自身的缺點,即每個方法都要在每個實例上重新創建一遍。爲什麼呢?因爲在 JavaScript 中,函數同樣也是對象,因此每定義一個函數,其實就是實例化了一個對象。將上面的 Person 構造函數寫成如下等價形式來方便理解:
function Person(name, age, job) { this.name = name; this.age= age; this.job = job; this.sayName = new Function("console.log(this.name)"); }
這樣看來,每個 Person 實例都包含一個不同的 Function 實例。以這種方式創建函數,創建 Function 新實例的機制相同,但會導致不同的作用域鏈和標識符解析:
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() { console.log(this.name); }
這樣做解決了兩個函數做同一件事的問題,但問題是在全局作用域中定義的函數實際上只能被某個對象調用,而且如果對象需要定義很多方法,那麼就要定義很多個全局函數,嚴重破壞自定義引用類型的封裝性,這時,救世主來了——原型模式。
5.原型模式:
function Person() { } Person.prototype.name = "Lucy"; Person.prototype.age= 24; Person.prototype.job = "nurse"; Person.prototype.sayName = function() { console.log(this.name); }; var person1 = new Person(); var person2 = new Person(); person1.sayName() //Lucy person2.sayName() //Lucy
我們創建的每個函數都有一個 prototype 屬性,該屬性指向一個對象,可以稱其爲原型對象,使用該對象的好處是可以讓所有對象實例共享它所包含的屬性和方法。在默認情況下,所有原型對象都會自動獲得一個 constructor 屬性,這個屬性包含一個指向 prototype 屬性所在函數的指針。構造函數(Person)、原型對象(Person.prototype)和對象實例(person1, person2)間具體指向關係如圖 1 所示:
650) this.width=650;" src="http://s3.51cto.com/wyfs02/M01/6B/BD/wKiom1U1vabimdsPAADFgUBB8WI540.jpg" title="指向關係.png" alt="wKiom1U1vabimdsPAADFgUBB8WI540.jpg" />
圖-1 指向關係
通過圖 1 我們可以看出,person1, person2 都包含一個內部屬性,該屬性僅僅指向了 Person.prototype,即對象實例與構造函數沒有直接關係。雖然在所有實現中都無法訪問到 [[Prototype]],但可以通過 isPrototypeOf() 方法來確定對象之間是否存在這種關係:
Person.prototype.isPrototypeOf(person1) //true Person.prototype.isPrototypeOf(person2) //true
在 ECMAScript 5 中增加了一個方法 Object.getPrototypeOf(),在所有支持的實現中,這個方法返回[[Prototype]]的值:
Object.getPrototypeOf(person1) === Person.prototype //true Object.getPrototypeOf(person1).name //"Lucy"
該方法返回的對象實際就是對象實例所對應的原型對象。使用 Object.getPrototypeOf() 可以方便地取得一個對象的原型,這在利用原型實現繼承的情況下很重要,我們會在之後的博客中提到 JavaScript 的繼承。
在讀取某個對象的某個屬性時,都會執行一次搜索,順序是對象實例(person1)--> 原型對象(Person.prototype) --> 構造函數(Person)。一旦找到相應屬性,就停止向上尋找,並返回值。這是多個對象實例共享原型所保存的屬性和方法的基本原理。(比如 person1 沒有 constructor 屬性,但是 person1.constructor 依然有值,該值就是找的原型對象 Person.prototype 中的 constructor 屬性,即 person1.constructor --> Person.prototype.constructor)。
雖然可以通過對象實例訪問保存在原型對象中的值,但卻不能通過對象實例重寫原型對象中的值:
person1.name = "Tom"; person1.name //Tom (來自對象實例) person2.name //Lucy (來自原型對象)
當爲對象實例添加一個屬性時,這個屬性就會屏蔽原型對象中保存的同名屬性。如果想去除該屏蔽,使用 delete 關鍵字:
delete person1.name; person1.name //Lucy (來自原型對象)
通過 hasOwnProperty() 方法可以確定什麼時候訪問的是對象實例屬性,什麼時候訪問的是原型對象屬性:
person1.hasOwnProperty("name"); //false person1.name = "Tom"; person1.hasOwnProperty("name"); //true
in 關鍵字有兩種用法,單獨使用和在 for-in 循環中使用。單獨使用時,作用類似於 hasOwnProperty() ,區別是隻要在通過對象實例能訪問給定屬性時返回 true,無論該屬性存在於實例中還是原型中:
person2.name; //Lucy (來自原型對象) "name" in person2; //true person2.hasOwnProperty("name"); //false person2.name = "Jordan"; "name" in person2; //true person2.hasOwnproperty("name"); //true
要取得對象上所有可枚舉的實例屬性,可以使用 ECMAScript 5 中的 Object.keys() 方法:
Object.keys(Person.prototype); //"name,age,job,sayName" var person1 = new Person(); person1.name = "Duncan"; person1.age= 38; Object.keys(person1); //"name,age"
剛纔我們提到的原型模式在每添加一個屬性或方法時都要敲一遍 Person.prototype,爲減少不必要的輸入,同時也從視覺上更好地封裝原型的功能,更常見的是採用如下對象字面量的形式重寫整個原型對象:
function Person() { } Person.prototype = { name: "Lucy", age: 24, job: "nurse", sayName: function() { console.log(this.name); } };
注:採用對象字面量方式聲明雖然最終結果與普通的原型模式相同,但有一個例外——constructor 屬性不再指向 Person 了。這也是爲什麼我們在前面說通過 instanceof 關鍵字判斷對象類型比通過 .constructor 屬性要更精準:
var newperson = new Person(); newperson instanceof Object; //true newperson instanceof Person; //true newperson.constructor === Object; //true newperson.constructor === Person; //false
如果 constructor 的值真的很重要,可以主動聲明其值:
Person.prototype = { constructor: Person, //主動聲明constructor name: "Lucy", age: 24, job: "nurse", sayName: function() { console.log(this.name); } };
原型具有動態性,這裏的動態性是指,我們對原型對象所做的任何修改都能夠立即從實例上反映出來,即便我們是先創建的對象實例,之後再修改的原型對象:
var person1 = new Person(); Person.prototype.sayHello = function() { console.log("hello"); } person1.sayHello() //"hello"
儘管如此,但如果是重寫整個原型對象,那麼情況就會有所不同:
function Person() { } var person1 = new Person(); Person.prototype = { constructor: Person, name: "Lucy", age: 24, job: "nurse", sayName: function() { console.log(this.name); } };
person1.sayName(); //error,報錯
這說明,重寫原型對象切斷了新原型對象與任何已經存在的對象之間的聯繫,它們引用的仍然是最初的原型對象。
原型模式不僅用來創建自定義類型的對象,同時,它也適用於原生的引用類型(Object, Array, String等)。例如,在 Array.prototype 中可以找到 sort() 方法,在 String.prototype 中可以找到 substring() 方法。可以通過修改原生原型對象定義新的方法:
String.prototype.startsWith = function (text) { return this.indexOf(text) === 0; }; var msg = "Hello Wolrd"; msg.startsWith("Hello"); //true
儘管可以這樣做,但並不推薦在產品化的過程中修改原生原型對象,因爲這可能導致命名衝突,而且有可能意外地重寫原生方法。
原型對象的最大問題是,對於引用類型值的屬性很不合適,會引起不想要的對象共享:
Person.prototype = { constructor: Person, name: "Lucy", age: 24, job: "nurse", friends: ["Lily", "Tom"], sayName: function () { console.log(this.name); } }; var person1 = new Person(); var person2 = new Person(); person1.friends.push("Jordan"); person2.friends //"Lily, Tom, Jordan" person1.friends === person2.friends //true
6.構造函數模式和原型模式組合:
爲了解決這一問題,可以組合使用構造函數模式和原型模式,這也是創建自定義類型最常見的方式,也是認同度最高的一種創建自定義類型的方法。其中,構造函數模式用於定義實例屬性,原型模式用於定義方法和共享的屬性:
function Person(name, age, job) { this.name = name; this.age= age; this.job = job; this.friends = ["Lily", "Tom"]; } Person.prototype = { constructor: Person, sayName: function() { console.log(this.name); } }; var person1 = new Person("Lucy", 24, "nurse"); var person2 = new Person("Tom", 22, "engineer"); person1.friends.push("Jordan"); person1.friends //"Lily, Tom, Jordan" person2.friends //"Lily, Tom" person1.friends === person2.friends //false person1.sayName === person2.sayName //true person1 instanceof Person //true person2 instanceof Person //true
構造函數模式和原型模式組合可以使用 instanceof 關鍵字確定它的類型。
如果你有其他 OO 語言經驗,可能更加習慣於在一個構造方法中完成定義,而不是在構造函數之外再初始化原型完成共享屬性的定義,這就需要用到動態原型模式。
7.動態原型模式:
function Person(name, age, job) { this.name = name; this.age= age; this.job = job; this.friends = ["Lily","Tom"]; if (typeof this.sayName != "function") { Person.prototype.sayName =function() { console.log(this.name); }; } }
只有在 sayName() 方法不存在的情況下,纔會將它添加到原型中。這裏對原型對象所做的修改能夠立即在對象實例中得到反映。對於採用這種模式創建的對象,同樣可以使用 instanceof 關鍵字確定它的類型。需要注意的是,使用動態原型模式時,不能使用對象字面量重寫原型,因爲這樣會切斷現有實例和新原型之間的聯繫。
如果上述幾種模式都不適用的情況下,可以使用寄生構造函數模式。
8.寄生構造函數模式:
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 person1 = new Person("Lucy", 24, "nurse"); person1.sayName(); //"Lucy"
不難看出,除了使用 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"); colors.toPipedString(); //"red|blue|green"
由於與工廠模式非常相似,因此寄生構造函數模式有着與工廠模式相同的缺點,即無法使用 instanceof 關鍵字確定對象類型。故在可以使用其他模式的情況下,不建議使用這種模式。
9.穩妥構造函數模式:
除了上述的幾種創建自定義對象的模式,還有一種穩妥構造函數模式。這裏有一個穩妥對象的概念,所謂穩妥對象,指的是沒有公共屬性,而且其方法也不引用 this 對象。穩妥對象最適合在一些安全環境中,或者防止數據被其他應用程序改動時使用:
function Person(name, age, job) { var o = new Object(); //可以在這裏定義私有變量和函數 o.sayName = function() { console.log(name); }; return o; } var person1 = Person("Lucy", 24, "nurse"); person1.sayName(); //"Lucy"
在這種模式下,除了使用 sayName() 方法之外,沒有其他方法訪問 name 的值。穩妥構造函數模式提供的這種安全性,使得它非常適合在某些安全執行環境下使用。要注意的一點是,使用穩妥構造函數模式時同樣無法用 instanceof 關鍵字判斷自定義對象的類型。
參考資料:
《JavaScript高級程序設計(第3版)》 作者: Nicholas C.Zakas 譯者:李松峯 曹力
本文出自 “細桶假狗屎” 博客,請務必保留此出處http://xitongjiagoushi.blog.51cto.com/9975742/1636386