(注1:如果有問題歡迎留言探討,一起學習!轉(zhuǎn)載請注明出處,喜歡可以點個贊哦!)
(注2:更多內(nèi)容請查看我的目錄。)
1. 簡介
學習JS,對象是一個繞不開的話題。本章將由淺入深探討對象的創(chuàng)建方法。
2. 創(chuàng)建單個對象的三種簡單辦法
簡單的對象創(chuàng)建方法,主要用于創(chuàng)建單個的對象。但是不適合創(chuàng)建多個對象。就像是手工制品,不適合量產(chǎn)。
2.1 對象字面量
最簡單的對象創(chuàng)建方法,莫過于使用對象字面量了。
var doll = {
name: "Nicholas",
age: 29,
sayName: function () {
console.log(this.name);
}
};
這種寫法簡單干凈,給人一種數(shù)據(jù)封裝的感覺,是開發(fā)人員最青睞的創(chuàng)建對象的方法。要注意,使用對象字面量的方法來定義對象,屬性名會自動轉(zhuǎn)換成字符串。另外,一般地,對象字面量的最后一個屬性后的逗號將忽略,但在IE7-瀏覽器中導致錯誤,所以最后一個屬性后面最好不要帶逗號。
2.2 new + 內(nèi)置對象
最常用的就是new操作符后跟Object構(gòu)造函數(shù)。
var doll = new Object();
//如果不給構(gòu)造函數(shù)傳遞參數(shù)可以不加括號 var doll= new Object;
doll.name = 'Nicholas';
doll.age = 29;
當然其他內(nèi)置對象還有Array,Date,RegExp,F(xiàn)unction,基本包裝類型等。比如:
var colors = new Array("red", "blue", "green");
2.3 Object.create()
ES5定義了一個名為Object.create()的方法,它創(chuàng)建一個新對象,第一個參數(shù)就是這個對象的原型,第二個可選參數(shù)用以對對象的屬性進行進一步描述。
var o1 = Object.create({x:1,y:1}); // o1繼承了屬性x和y
console.log(o1.x); // 1
可以通過傳入?yún)?shù)null來創(chuàng)建一個沒有原型的新對象,但通過這種方式創(chuàng)建的對象不會繼承任何東西,甚至不包括基礎方法。比如toString()和valueOf()。
var o2 = Object.create(null); // o2不繼承任何屬性和方法
var o1 = {};
console.log(Number(o1)); // NaN
console.log(Number(o2)); // Uncaught TypeError: Cannot convert object to primitive value
如果想創(chuàng)建一個普通的空對象(比如通過{}或new Object()創(chuàng)建的對象),需要傳入Object.prototype。
var o3 = Object.create(Object.prototype); // o3和{}和new Object()一樣
var o1 = {};
console.log(Number(o1)); // NaN
console.log(Number(o3)); // NaN
Object.create()方法的第二個參數(shù)是屬性描述符。
var o1 = Object.create({z:3},{
x:{value:1,writable: false,enumerable:true,configurable:true},
y:{value:2,writable: false,enumerable:true,configurable:true}
});
console.log(o1.x,o1.y,o1.z); // 1 2 3
3. 創(chuàng)建多個對象的5種模式
雖然第二節(jié)中介紹的三種方法可以方便地創(chuàng)建一個對象,但這些方式有個明顯的缺點:使用同一個接口創(chuàng)建很多對象,會產(chǎn)生大量的重復代碼。例如,要創(chuàng)建10個具有name,age屬性和sayName方法的對象。利用對象字面量方法只能如下:
var doll1 = {
name: "Nicholas",
age: 29,
sayName: function () {
console.log(this.name);
}
};
var doll2 = {
name: "Nicholas",
age: 29,
sayName: function () {
console.log(this.name);
}
};
...
var doll10 = {
name: "Nicholas",
age: 29,
sayName: function () {
console.log(this.name);
}
};
為了解決這個問題,人們開始使用工廠模式來創(chuàng)建對象。該模式抽象了創(chuàng)建具體對象的過程,用函數(shù)來封裝以特定接口創(chuàng)建對象的細節(jié)。這就好比原來是手工做一樣玩具,而現(xiàn)在你把流程程序化,做成一臺機器,用機器代替了人工。
3.1 工廠模式
function createDoll(name, age) {
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function () {
alert(this.name);
};
return o;
}
var doll1 = createDoll("Nicholas", 29);
var doll2 = createDoll("Greg", 27);
createDoll函數(shù)能夠根據(jù)接收的參數(shù)來創(chuàng)建一個包含所有必要信息的Doll對象,可以無數(shù)次地調(diào)用這個函數(shù),每次都能返回具有name,age屬性和sayName方法的對象。
工廠模式雖然解決了創(chuàng)建多個相似對象的問題,但沒有解決對象識別的問題,因為使用該模式并沒有給出對象的類型。你并不知道doll1和doll2都是createDoll創(chuàng)建出來的對象,你甚至不知道它們是不是一類東西?;蛘哒f,你雖然用機器做出了玩具,但是你并不知道這是同一個機器造出的同一類玩具。
那怎么辦呢?當然是品牌化了。
3.2 構(gòu)造函數(shù)模式
前面說過,你可以用原生構(gòu)造函數(shù)來創(chuàng)建對象。同樣,你也可以通過創(chuàng)建自定義的構(gòu)造函數(shù),來定義自定義對象類型的屬性和方法。創(chuàng)建自定義的構(gòu)造函數(shù)意味著可以將它的實例標識為一種特定的類型,而這正是構(gòu)造函數(shù)模式勝過工廠模式的地方。該模式?jīng)]有顯式地創(chuàng)建對象,直接將屬性和方法賦給了this對象,且沒有return語句。
function Doll(name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
alert(this.name);
};
}
var doll1 = new Doll("Nicholas", 29);
var doll2 = new Doll("Greg", 27);
利用這種方法,我們給Doll定義了品牌‘Doll’,doll1和doll2都是Doll對象的實例,當然其本身也是Object的實例,Object本身就是最大的品牌。如下:
alert(doll1 instanceof Object); // true
alert(doll1 instanceof Doll); // true
alert(doll2 instanceof Object); // true
alert(doll2 instanceof Doll); // true
注意,構(gòu)造函數(shù)模式與工廠模式的區(qū)別:
- 沒有顯示地創(chuàng)建對象。
- 直接將屬性和方法賦給了this對象。
- 沒有return。
此外,要使用構(gòu)造函數(shù)模式創(chuàng)建對象,必須使用new操作符。以這種方式調(diào)用構(gòu)造函數(shù)實際會經(jīng)歷以下步驟:
- 創(chuàng)建一個新對象。
- 將構(gòu)造函數(shù)的作用域賦給新對象(因此this就指向了這個新對象)(參考JS入門難點解析7-this)。
- 執(zhí)行函數(shù)中的代碼(為這個新對象添加屬性)。
- 返回新對象。
那么構(gòu)造函數(shù)模式的缺點是什么呢?使用構(gòu)造函數(shù)的主要問題是每個方法都要在每個實例上重新創(chuàng)建一遍,創(chuàng)建多個完成相同任務的方法完全沒有必要,浪費內(nèi)存空間。那么,最簡單的辦法,把函數(shù)方法定義在外部不就行了么。
3.2.1 構(gòu)造函數(shù)拓展模式
在構(gòu)造函數(shù)模式的基礎上,把方法定義轉(zhuǎn)移到構(gòu)造函數(shù)外部,可以解決方法被重復創(chuàng)建的問題。
function Doll(name, age) {
this.name = name;
this.age = age;
this.sayName = sayName;
}
function sayName() {
alert(this.name);
};
var doll1 = new Doll("Nicholas", 29);
var doll2 = new Doll("Greg", 27);
但是,問題又來了。在全局作用域中定義的函數(shù)實際上只能被某個對象調(diào)用,這讓全局作用域有點名不副實。而且,如果對象需要定義很多方法,就要定義很多全局函數(shù),嚴重污染全局空間,這個自定義的引用類型沒有封裝性可言了。
當然構(gòu)造函數(shù)不止前述所說,還有以下兩個變種。
3.2.2 寄生構(gòu)造函數(shù)模式
該模式的基本思想是創(chuàng)建一個函數(shù),該函數(shù)的作用僅僅是封裝創(chuàng)建對象的代碼,然后再返回新創(chuàng)建的對象。該模式是工廠模式和構(gòu)造函數(shù)模式的結(jié)合。
寄生構(gòu)造函數(shù)模式與構(gòu)造函數(shù)模式有相同的問題,每個方法都要在每個實例上重新創(chuàng)建一遍,創(chuàng)建多個完成相同任務的方法完全沒有必要,浪費內(nèi)存空間。另外,使用該模式返回的對象與構(gòu)造函數(shù)或者與構(gòu)造函數(shù)的原型屬性之間沒有任何關系。因此,使用instanceof運算符和prototype屬性都沒有意義。所以,該模式要盡量避免使用。這種模式有點類似于你借別人的工廠生產(chǎn)的產(chǎn)品,無法貼上他們品牌的標識。你借用的是工廠,但不是品牌。
function Doll(name,age){
// 這里也可以是new Array或者其他構(gòu)造函數(shù),用于寄生
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function(){
console.log(this.name);
};
return o;
}
var doll1 = new Doll("Nicholas",29);
var doll2 = new Doll("Greg",25);
//具有相同作用的sayName()方法在doll1和doll2這兩個實例中卻占用了不同的內(nèi)存空間
console.log(doll1.sayName === doll2.sayName); // false
console.log(doll1 instanceof Doll); // false
console.log(doll1.__proto__ === Doll.prototype); // false
3.2.3 穩(wěn)妥構(gòu)造函數(shù)模式
所謂穩(wěn)妥對象指沒有公共屬性,而且其方法也不引用this的對象。穩(wěn)妥對象最適合在一些安全環(huán)境中(這些環(huán)境會禁止使用this和new)或者在防止數(shù)據(jù)被其他應用程序改動時使用。
穩(wěn)妥構(gòu)造函數(shù)與寄生構(gòu)造函數(shù)模式相似,但有兩點不同:一是新創(chuàng)建對象的實例方法不引用this;二是不使用new操作符調(diào)用構(gòu)造函數(shù)。
function Doll(name,age){
//創(chuàng)建要返回的對象
var o = new Object();
//可以在這里定義私有變量和函數(shù)
//添加方法
o.sayName = function(){
console.log(name);
};
//返回對象
return o;
}
//在穩(wěn)妥模式創(chuàng)建的對象中,除了使用sayName()方法之外,沒有其他方法訪問name的值
var doll = Person("Nicholas",29);
doll.sayName();//"Nicholas"
與寄生構(gòu)造函數(shù)模式相似,使用穩(wěn)妥構(gòu)造函數(shù)模式創(chuàng)建的對象與構(gòu)造函數(shù)之間也沒有什么關系,因此instanceof操作符對這種對象也沒有什么意義。好像是工廠針對客戶的定制生產(chǎn)一樣。
3.3 原型模式
我們創(chuàng)建的每個函數(shù)都有一個prototype(原型)屬性,這個屬性是一個指針,指向一個對象,而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。如果按照字面意思來理解,那么prototype就是通過調(diào)用構(gòu)造函數(shù)而創(chuàng)建的那個對象實例的原型對象。使用原型對象的好處是可以讓所有對象實例共享它所包含的屬性和方法。換句話說,不必在構(gòu)造函數(shù)中定義對象實例的信息,而是可以將這些信息直接添加到原型對象中。就好像做了一個模具,直接把信息刻在模具,就可以利用該模具實現(xiàn)復制量產(chǎn)了。
function Doll() {
}
Doll.prototype.name = "Nicholas";
Doll.prototype.age = 29;
Doll.prototype.job = "Software Engineer";
Doll.prototype.sayName = function () {
alert(this.name);
};
var doll1 = new Doll();
doll1.sayName(); //"Nicholas"
var doll2 = new Doll();
doll2.sayName(); //"Nicholas"
alert(doll1.sayName == doll2.sayName); //true
為原型一個個添加屬性和方法太慢反,我們可以考慮使用字面量來創(chuàng)建原型對象。
function Doll(){};
Doll.prototype = {
name: "Nicholas",
age: 29,
sayName : function(){
console.log(this.name);
}
};
var doll1 = new Doll();
doll1.sayName();//"Nicholas"
console.log(doll1.constructor === Doll);//false
console.log(doll1.constructor === Object);//true
但是,經(jīng)過對象字面量的改寫后,constructor不再指向Doll了。因為此方法完全重寫了默認的prototype對象,使得Doll.prototype的自有屬性constructor屬性不存在,只有從原型鏈中找到Object.prototype中的constructor屬性。
所以,可以顯式地設置原型對象的constructor屬性。
function Doll(){};
Doll.prototype = {
constructor:Doll,
name: "Nicholas",
age: 29,
sayName : function(){
console.log(this.name);
}
};
var doll1 = new Doll();
doll1.sayName();//"Nicholas"
console.log(doll1.constructor === Doll);//true
console.log(doll1.constructor === Object);//false
由于默認情況下,原生的constructor屬性是不可枚舉的,更妥善的解決方法是使用Object.defineProperty()方法,改變其屬性描述符中的枚舉性enumerable。
function Doll(){};
Doll.prototype = {
name: "Nicholas",
age: 29,
sayName : function(){
console.log(this.name);
}
};
Object.defineProperty(Doll.prototype,'constructor',{
enumerable: false,
value: Doll
});
var doll1 = new Doll();
doll1.sayName();//"Nicholas"
console.log(doll1.constructor === Doll);//true
console.log(doll1.constructor === Object);//false
原型模式問題在于引用類型值屬性會被所有的實例對象共享并修改,這也是很少有人單獨使用原型模式的原因。
function Doll(){};
Doll.prototype = {
constructor:Doll,
name: "Nicholas",
age: 29,
colors: ['blue', 'green'],
sayName : function(){
console.log(this.name);
}
};
var doll1 = new Doll();
var doll2 = new Doll();
doll1.colors.push('red');
console.log(doll1.colors);//['blue', 'green','red']
console.log(doll2.colors);//['blue', 'green','red']
console.log(doll1.colors === doll2.colors);//true
3.4 組合模式(使用最廣泛)
組合使用構(gòu)造函數(shù)模式和原型模式是創(chuàng)建自定義類型的最常見方式。構(gòu)造函數(shù)模式用于定義實例屬性,而原型模式用于定義方法和共享的屬性,這種組合模式還支持向構(gòu)造函數(shù)傳遞參數(shù)。實例對象都有自己的一份實例屬性的副本,同時又共享對方法的引用,最大限度地節(jié)省了內(nèi)存。該模式是目前使用最廣泛、認同度最高的一種創(chuàng)建自定義對象的模式。
function Doll(name, age) {
this.name = name;
this.age = age;
this.colors = ['blue','green'];
}
Doll.prototype = {
constructor:Doll,
sayName : function(){
console.log(this.name);
}
};
var doll1 = new Doll("Nicholas", 29);
var doll2 = new Doll("Greg", 27);
doll1.colors.push('red');
console.log(doll1.colors);//['blue', 'green','red']
console.log(doll2.colors);//['blue', 'green']
console.log(doll1.colors === doll2.colors);//false
console.log(doll1.sayName === doll2.sayName);//true
3.5 動態(tài)原型模式
動態(tài)原型模式將組合模式中分開使用的構(gòu)造函數(shù)和原型對象都封裝到了構(gòu)造函數(shù)中,然后通過檢查方法是否被創(chuàng)建,來決定是否初始化原型對象。
function Doll(name, age) {
//屬性
this.name = name;
this.age = age;
this.colors = ['blue','green'];
}
//方法
if(typeof this.sayName != "function"){
Doll.prototype.sayName = function(){
console.log(this.name);
};
Doll.prototype.sayColors = function(){
console.log(this.colors);
};
}
};
var doll1 = new Doll("Nicholas", 29);
doll1.sayName();//"Nicholas"
這里,只有在sayName和sayColors方法不存在的情況下,才會將它們添加到原型中。這段代碼只會在初次調(diào)用構(gòu)造函數(shù)時才執(zhí)行。此后,原型已經(jīng)初始化,不需要再做修改。另外,這里對原型所做的修改,能夠立即在所有實例中得到反映。因此,除了不能使用對象字面量重寫原型外,這種方法可謂相當完美。
注意,使用動態(tài)原型模式時:
- 如果原型對象中包含多個語句,只需要檢測其中一個語句即可。
- 不能使用對象字面量重寫原型。因為在已經(jīng)創(chuàng)建了實例的情況下重寫原型,就會切斷現(xiàn)有實例與新原型的聯(lián)系。
參考
javascript面向?qū)ο笙盗械诙獎?chuàng)建對象的5種模式
深入理解javascript對象系列第一篇——初識對象
JavaScript構(gòu)造函數(shù)及原型對象
BOOK-《JavaScript高級程序設計(第3版)》第6章