1. 創(chuàng)建對(duì)象
創(chuàng)建定義對(duì)象的最簡(jiǎn)單方式就是創(chuàng)建一個(gè) Object 實(shí)例,然后為其添加屬性和方法。
var person = new Object();
person.name = "Hwaphon";
person.age = 19;
person.sayName = function() {
console.log(this.name);
}
當(dāng)然,我們還可以通過一個(gè)對(duì)象字面量創(chuàng)建對(duì)象,事實(shí)上這種方式也是使用最多的。
var person = {
name: "Hwaphon",
age: 19,
sayName: function() {
console.log(this.name);
}
};
雖然 Object 構(gòu)造函數(shù)或?qū)ο笞置媪慷伎梢詣?chuàng)建單個(gè)對(duì)象,但是這兩種方式存在明顯的缺點(diǎn): 使用同一接口創(chuàng)建很多對(duì)象,會(huì)產(chǎn)生大量重復(fù)的代碼。為了解決這個(gè)問題,人們開始用工廠模式的一種變體。下面就用類工廠模式解決這種問題。
function createPerson(name, age) {
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function() {
console.log(this.name);
};
return o;
}
可以頻繁的調(diào)用上面這個(gè)函數(shù)以創(chuàng)造不同的對(duì)象,但是這種方法仍然存在一個(gè)問題,就是對(duì)象識(shí)別問題,即我們無法判斷創(chuàng)建出來的對(duì)象是 Person, 我們能判斷的就是它屬于 Object 類型而已。所以這個(gè)時(shí)候又出現(xiàn)了一種創(chuàng)建對(duì)象的方式,就是構(gòu)造函數(shù)模式。
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function() {
console.log(this.name);
}
}
var person = new Person("Hwaphon", 21);
person.sayName();
這個(gè)時(shí)候我們就可以利用 instanceof 運(yùn)算符檢測(cè)我們創(chuàng)建的對(duì)象類型。
console.log(person instanceof Object); // true
console.log(person instanceof Person); // true
我們通過 new 操作符去創(chuàng)建一個(gè)對(duì)象,這也就意味著每個(gè)對(duì)象是相互獨(dú)立的,當(dāng)然也包括 sayName 方法, 但是這個(gè)方法在每個(gè)實(shí)例中實(shí)現(xiàn)的功能是相同的,實(shí)在沒必要每創(chuàng)建一個(gè)對(duì)象實(shí)例就創(chuàng)建一個(gè) Function 實(shí)例。我們通過以下代碼可以看出這種問題。
var person1 = new Person("Hwaphon", 21);
var person2 = new Person("Hello", 22);
console.log(person1.sayName == person2.sayName); // false
如果是這樣的話,我們只能將函數(shù)放置到構(gòu)造函數(shù)外去解決這個(gè)問題。
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = sayName;
}
function sayName() {
console.log(this.name);
}
var person1 = new Person("Hwaphon", 21);
person1.sayName();
雖說這種方法能解決上述問題,但是又引入了新的問題,因?yàn)檫@種方式引入了全局的 sayName 函數(shù)。更讓人無法接受的是,如果對(duì)象需要定義很多方法,那么就要定義多個(gè)全局函數(shù),這樣我們自定義的引用類型就絲毫沒有封裝性可言了。所以不得不引入原型模式來解決這個(gè)問題。
2. 原型模式
我們創(chuàng)建的每個(gè)函數(shù)都有一個(gè) prototype 屬性,這個(gè)屬性是一個(gè)指針,指向一個(gè)對(duì)象,而這個(gè)對(duì)象的用途是包含可以由特定類型的所有實(shí)例共享的屬性和方法。使用原型的好處是可以讓所有的對(duì)象實(shí)例共享它保函的屬性和方法。所以為了解決上面的問題,我們可以像下面這樣定義。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayName = function() {
console.log(this.name);
}
console.log(person1.sayName == person2.sayName); // true
通過 hasOwnProperty() 方法可以檢測(cè)實(shí)例屬性,而非原型屬性,通過 in 不僅可以檢測(cè)實(shí)例屬性也可以檢測(cè)原型屬性。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayName = function() {
console.log(this.name);
}
var person = new Person("Hwaphon", 21);
if (person.hasOwnProperty("sayName")) {
console.log("true");
}
if ("sayName" in person) {
console.log("true");
}
當(dāng)然,如果你想在原型添加多個(gè)函數(shù),那么可以將這些方法組成一個(gè)對(duì)象字面量。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype = {
sayName: function() {
console.log(this.name);
},
sayAge: function() {
console.log(this.age);
}
};
這個(gè)時(shí)候存在一個(gè)問題,因?yàn)檫@個(gè)時(shí)候 constructor 不再指向 Person 了,所以我們可以手動(dòng)設(shè)置 constructor。
Person.prototype = {
constructor: Person,
sayName: function() {
console.log(this.name);
},
sayAge: function() {
console.log(this.age);
}
};
上面我們將原型和構(gòu)造函數(shù)分開了,這可能會(huì)讓人感到困惑,所以可以使用動(dòng)態(tài)原型模式,將所有信息封裝在構(gòu)造函數(shù)中。
function Person(name, age) {
this.name = name;
this.age = age;
if (typeof this.sayName != "function") {
Person.prototype.sayName = function() {
console.log(this.name);
};
}
}
var person = new Person("Hwaphon", 21);
person.sayName();
只有在 sayName() 方法不存在的情況下才會(huì)將它添加進(jìn)原型中,所以 if 判斷語句只有在初次調(diào)用構(gòu)造函數(shù)時(shí)才會(huì)執(zhí)行。
除了上面的方法以外,還有一種寄生構(gòu)造函數(shù)模式,這種模式的基本思想是創(chuàng)建一個(gè)函數(shù),該函數(shù)的作用僅僅是封裝創(chuàng)建對(duì)象的過程,然后將新創(chuàng)建的對(duì)象返回。比如,這種模式創(chuàng)建一個(gè)對(duì)象的過程可能如下所示:
function Person(name, age) {
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function() {
console.log(this.name);
};
return o;
}
var person = new Person("hwaphon", 21);
person.sayName();
那么這種方式的優(yōu)點(diǎn)在哪里呢?正如它名稱中的 “寄生” 所說的,這種模式可以寄生在一個(gè)指定的對(duì)象中,也就是說它可以為一個(gè)已經(jīng)存在的對(duì)象增加功能。比如說,對(duì)于自帶的 Array 對(duì)象,如果不是迫不得已,一般是不應(yīng)該直接使用 Array.prototype 擴(kuò)充原生數(shù)組對(duì)象的,所以這時(shí)候寄生構(gòu)造模式可以發(fā)揮它的作用。
function SpecialArray() {
var array = new Array();
array.push.apply(array, arguments);
array.formatArray = function() {
return this.join("->");
}
return array;
}
var array = new SpecialArray(1,2,3,4),
result = array.formatArray();
console.log(result);
可見,當(dāng)你想要擴(kuò)充一個(gè)對(duì)象的功能而又不想直接修改原本的對(duì)象時(shí),可以使用寄生構(gòu)造函數(shù)模式,這和設(shè)計(jì)模式中的裝飾者模式倒是很像。
還有一個(gè)稱為穩(wěn)妥構(gòu)造方式的創(chuàng)建方法,當(dāng)你不想共享任何變量而且環(huán)境不允許使用 this, new 的情況下,可以選擇這種方式。
function Person(name, age) {
var o = new Object();
o.sayName = function() {
console.log(name);
};
o.sayAge = function() {
console.log(age);
};
return o;
}
var person = Person("Hwaphon", 33);
person.sayName();
person.sayAge();
值得注意的是,這個(gè)時(shí)候只有通過 sayName() 和 sayAge() 才可以訪問到 name 和 age 屬性,而之前介紹的方法都在要返回的對(duì)象中設(shè)置了屬性,而這種方式依賴于直接傳入的參數(shù),所以返回的對(duì)象中是不包含 name 和 age 屬性的,這倒是滿足了封裝的特性。
3. 復(fù)制對(duì)象
還有一個(gè)常見的問題就是如何復(fù)制一個(gè)對(duì)象,看下面這個(gè)例子:
function anotherFunction() {}
var anotherObject = {
c: true
};
var anotherArray = [];
var myObject = {
a: 2,
b: anotherObject,
c: anotherFunction,
d: anotherArray
};
如果我現(xiàn)在想復(fù)制 myObject 對(duì)象,可以看出只有 a 是一個(gè)基礎(chǔ)類型的值,其它 b, c, d 全是對(duì)象類型的值,根據(jù)從其它高級(jí)語言中的到的經(jīng)驗(yàn),對(duì)于基礎(chǔ)類型的值默認(rèn)是復(fù)制其值,而對(duì)于對(duì)象類型,則復(fù)制的是引用。
在 JavaScript 中自然也是這樣的,涉及淺復(fù)制和深復(fù)制。首先我們先介紹淺復(fù)制,因?yàn)?ES6 中定義了一個(gè) Object.assign() 方法,我們可以通過這個(gè)方法輕松地實(shí)現(xiàn)淺復(fù)制。
Object.assign() 第一個(gè)參數(shù)是目標(biāo)對(duì)象,之后可以跟多個(gè)源對(duì)象,它會(huì)遍歷一個(gè)或者多個(gè)源對(duì)象所有可枚舉的自有鍵并把他們復(fù)制到目標(biāo)對(duì)象,最后返回目標(biāo)對(duì)象。
var newObj = Object.assign({}, myObject);
console.log(newObj.a);
console.log(newObj.b === myObject.b); // true
console.log(newObj.c === myObject.c); // true
console.log(newObj.d === myObject.d); // true
可見,newObj 與 myObj 的對(duì)象比較都返回了 true, 這說明其實(shí)二者都是一個(gè)引用,指向同一個(gè)函數(shù)對(duì)象。
下面介紹的深復(fù)制只適用于 JSON 安全的對(duì)象,因?yàn)槲覀兊纳顝?fù)制是借助 JSON 來實(shí)現(xiàn)的。
var newObj = JSON.parse(JSON.stringify(myObject));
console.log(newObj.a);
console.log(newObj.b === myObject.b); // false
console.log(newObj.c === myObject.c); // false
console.log(newObj.d === myObject.d); // false
4. 屬性描述符
自從 ES5 開始,所有的屬性都具備了屬性描述符,下面讓我們來一一介紹。
-
Writable - 決定是否可以修改屬性的值。
var myObjet = {}; Object.defineProperty(myObjet, "a", { value: 2, writable: false, configurable: true, enumerable: true }); console.log(myObjet.a); // 2 myObjet.a = 4; console.log(myObjet.a); // 2
可見,如果將 writable 的值為 false 的時(shí)候,那么修改對(duì)象屬性的值將會(huì)失效(在嚴(yán)格模式下會(huì) TypeError)。
-
Configurable - 控制是否可以修改屬性描述符
var myObjet = {}; Object.defineProperty(myObjet, "a", { value: 2, writable: true, configurable: false, enumerable: true }); console.log(myObjet.a); // 2 delete myObjet.a; console.log(myObjet.a); // 2
從上面的例子可以看出,我們對(duì)屬性的刪除已經(jīng)失效了,另外值得注意的是,一旦將 configurable 設(shè)置為 false, 我們就不可以再利用 defineProperty() 再去修改屬性的 writable, configurable, enumerable 描述符。好吧,我承認(rèn)還有一個(gè)例外,這時(shí)候還是可以將 writrable 從 true 設(shè)置為 false,但是不可以在改為 true 。
- Enumerable - 設(shè)置屬性是否支持枚舉
一旦將 Enumerable 設(shè)置為 false, 雖然它還是可以正常訪問,但是它將不會(huì)出現(xiàn)在對(duì)象的屬性枚舉中。更具體的說,當(dāng)你使用 for...in 遍歷對(duì)象的屬性時(shí),設(shè)置為 false 的屬性會(huì)被直接跳過。
看到這里,你可能會(huì)問了,在對(duì)象中設(shè)置一個(gè)屬性有這么麻煩嗎?我平常的時(shí)候好像根本沒接觸過上面提到的這幾個(gè)屬性,那是因?yàn)樗鼈兌加幸粋€(gè)默認(rèn)的設(shè)置,怎么查看呢?看下面這個(gè)例子:
var myObjet = {
a: 2
};
var despritor = Object.getOwnPropertyDescriptor(myObjet, "a");
console.log(despritor);
它的返回值如下所示
configurable: true
enumerable:true
value:2
writable:true
有的時(shí)候,我們希望自己定義的屬性或者對(duì)象是不可改變的,那么改怎么做呢?比如說我想設(shè)置定義一個(gè)常量?
我們可以將 writable 和 configurable 都設(shè)置為 false, 這樣我們就可以輕松地創(chuàng)建一個(gè)真正意義上的常量。
var myObjet = {};
Object.defineProperty(myObjet, "PI", {
value: 3.1415926,
writable: false,
configurable: false
});
下面來看幾個(gè)實(shí)用的函數(shù)。
-
禁止拓展 - 如果你想禁止一個(gè)對(duì)象添加屬性并且保留已有屬性,可以使用
Object.preventExtensions()var myObject = { a: 2 }; Object.preventExtensions(myObject); console.log(myObject.a); // 2 myObject.b = 10; console.log(myObject.b); // undefined 密封 - 密封也就是說在禁止拓展的基礎(chǔ)上, 將
configurable也設(shè)置為false,將對(duì)象密封的方法為Object.seal()。凍結(jié) - 凍結(jié)就是在密封的基礎(chǔ)上將
writable也設(shè)置為false, 這意味著連對(duì)象中屬性的值也無法修改了,這個(gè)方法用于定義那種一旦定義了就不用在改動(dòng)的對(duì)象。將對(duì)象凍結(jié)的方法為Object.freeze()。
5. 實(shí)例
構(gòu)造函數(shù),實(shí)例以及原型的關(guān)系基本如下所示:
對(duì)于構(gòu)造函數(shù)而言,它內(nèi)部有一個(gè)
prototype屬性,這個(gè)屬性直接指向原型,而且通過此構(gòu)造函數(shù)創(chuàng)建的實(shí)例,將默認(rèn)指向該原型,這也就意味著原型中的方法在實(shí)例中也是可用的。在實(shí)例中,我們不可以通過
prototype訪問原型,不過可以通過__proto__訪問該原型。在原型中,有一個(gè)
constructor屬性,這個(gè)屬性默認(rèn)指向構(gòu)造函數(shù),而且原型中也有一個(gè)__proto__屬性,這個(gè)屬性指向Object,所以我們可以說,所有的對(duì)象都是繼承自Object的。