6.1、了解對象
建立對象最簡單的兩個方法:
(1)使用 Object ;
(2)使用對象字面量。
// 使用Objcet建立對象
var person = new Object();
person.name = 'Nicholas';
person.age = 29;
person.job = 'Software';
person.sayName = function() {
alert(this.name);
}
// 使用對象字面量建立對象
var person1 = {
name: 'Nicholas 1',
age: 29,
job: 'Software',
sayName: function() {
alert(this.name);
}
};
person1.sayName();
6.1.1、屬性類型
ES中有兩種屬性:資料屬性和通路器屬性,這兩種屬性有各自的特性,描述了屬性的各種特征。
1.資料屬性
資料屬性包含一個資料值的位置。在這個位置可以讀取和寫入值。資料屬性有4個描述其行為的特性。
- [[Confiturable]]:表示能否通過 delete 删除屬性進而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為通路器屬性。
- [[Enumerable]]:表示能夠通過 for-in 循環傳回屬性。
- [[Writable]]:表示能否修改屬性的值。
- [[Value]]:包含這個屬性的資料值。讀取屬性值的時候,從這個位置讀;寫入屬性值的時候,把新值儲存在這個位置。這個特性的預設值為 undefined 。
要修改屬性預設的特性,必須使用ES5的 Object.defineProperty() 方法。這個方法接受三個參數:屬性所在的對象、屬性的名字、一個描述符對象。描述符對象的屬性必須是:configurable、enumerable、writable、value。調用該方法時configurable、enumerable、writable 特性的預設值為 false。
var person2 = {};
Object.defineProperty(person2, 'name', {
writable: false, // 表示屬性值不可修改
value: 'Nicholas'
});
console.log(person2.name);
person2.name = 'Greg';
console.log(person2.name); // 修改沒有效果,Nicholas
注意:一旦把屬性的特性定義為不可配置(即:configurable: false),則隻能修改屬性的 writable 特性,除此之外,都會導緻錯誤。具體代碼如下所示:
var person4 = {};
Object.defineProperty(person4, 'name', {
configurable: false, // 不能從對象中删除該屬性
value: 'Nicholas 4'
});
Object.defineProperty(person4, 'name', {
configurable: true, //錯誤,因為前面已經定義為不可配置
value: 'Nicholas 4'
});
2.通路器屬性
通路器屬性不包含資料值:它們包含一對 getter 和 setter 函數(不過,這兩個函數都不是必需的)。通路器屬性有如下4個特性:
- [[Confiturable]]:表示能否通過 delete 删除屬性進而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為資料屬性。
- [[Enumerable]]:表示能夠通過 for-in 循環傳回屬性。
- [[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 (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
});
book.year = 2005;
console.log(book.edition);
注意:_year 前面的下劃線是一種常用的記号,用于表示隻能通過對象方法通路的屬性。
6.1.2、定義多個屬性特性
ES5定義了一個 Object.defineProperties() 方法。利用這個方法可以一次定義多個屬性特性。
該方法接收兩個對象參數:第一個對象是要添加和修改其屬性的對象,第二個對象的屬性與第一個對象要添加或修改的屬性一一對應。
var book = {};
Object.defineProperties(book, {
_year : {
writable: true,
value: 2004
},
edition : {
writable: true,
value: 1
},
year : {
get : function() {
return this._year;
},
set : function(newValue) {
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
// 當執行該語句時,實際上會執行 set() 函數
book.year = 2005;
console.log(book._year); // 2005
console.log(book.year); // 2005
console.log(book.edition); // 2
6.1.3、讀取屬性的特性
ES5的 Object.getOwnPropertyDescriptor() 方法,可以取得給定屬性的描述符(特性)。Object.getOwnPropertyDescriptor(屬性所在對象, 屬性名稱)。其傳回值是一個對象。
注意:在JS中,可以針對任何對象——包括DOM和BOM對象,使用 Object.getOwnPropertyDescriptor() 方法。
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 descriptor1 = Object.getOwnPropertyDescriptor(book, 'year');
console.log(descriptor.configurable); // false
console.log(descriptor.enumerable); // false
console.log(typeof descriptor.set); // function
console.log(typeof descriptor.get); // function
// 資料屬性
var descriptor2 = Object.getOwnPropertyDescriptor(book, 'edition');
console.log(descriptor2.configurable); // false
console.log(descriptor2.enumerable); // false
console.log(descriptor2.writable); // true
console.log(descriptor2.value); // 2
在JS中,可以針對任何對象——包括DOM和BOM對象,使用 Object.getOwnPropertyDescriptor() 方法。
6.2、建立對象
使用 Object 構造函數或 對象字面量 來建立單個對象具有明顯的缺點:使用同一個接口建立很多對象,會産生大量重複代碼。
建立對象有幾種方法:
(1)使用Object建立對象
(2)使用對象直面量
(3)工廠模式:沒有解決對象的識别問題(即怎樣知道一個對象的類型)
(4)構造函數模式:每個方法都要在每個執行個體上重新建立一遍
當然可以把方法定義在全局作用域中,但是如此會有如下問題:
全局作用域中定義的函數實際上隻能被特定的對象調用;
如果對象需要很多方法,那麼需要定義很多全局函數,會造成沒有封裝性。
(5)原型模式:共享問題
6.2.1、工廠模式
工廠模式:用函數來封裝以特定接口建立對象的細節。
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('Nicholas 1', 29, 'Software');
var person2 = createPerson('Nicholas 2', 30, 'Web');
person1.sayName(); // Nicholas 1
person2.sayName(); // Nicholas 2
6.2.2、構造函數模式
構造函數如下所示:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
alert(this.name);
}
}
var person1 = new Person('Nicholas 1', 29, 'Software');
var person2 = new Person('Nicholas 2', 30, 'Web');
person1.sayName(); // Nicholas 1
person2.sayName(); // Nicholas 2
alert(person1.constructor == Person); // true
alert(person2.constructor == Person); // true
alert('-------------------------------');
alert(person1 instanceof Person); // true
alert(person1 instanceof Object); // true
alert(person2 instanceof Person); // true
alert(person2 instanceof Object); // true
要建立 Person 的新執行個體,必須使用 new 操作符。以這種方式調用構造函數實際上會經曆以下 4 個步驟:
(1)建立一個對象;
(2)将構造函數的作用域賦給對象(是以 this 就指向了這個新對象);
(3)執行構造函數中的代碼(為這個新對象添加屬性);
(4)傳回新對象。
使用構造函數建立的對象有一個 constructor(構造函數) 屬性,該屬性指向構造函數(Person)。
注意:所有對象均繼承自 Object。
1、将構造函數當做函數
普通函數和構造函數的差別在于是否使用 new 操作符來調用。使用 new 操作符來調用的函數為構造函數,否則為普通函數。
// 當做構造函數使用
var person3 = new Person('Nicholas 3', 31, 'Software 3');
person3.sayName();
// 作為普通函數使用
Person('Nicholas 4', 32, 'Software 4');
window.sayName();
console.log(window.age); // 32
// 在另一個對象的作用域中調用
var o = new Object();
Person.call(o, 'Nicholas 5', 33, 'Software 5');
o.sayName();
注意:當在全局作用域中調用一個函數時,this 對象總是指向 Global 對象(在浏覽器中就是 window 對象)。
2、構造函數的問題
使用構造函數的主要問題,就是每個方法都要在每個執行個體上重新建立一遍。可以通過把函數定義轉移到構造函數外部來解決這個問題。具體代碼如下所示:
function Person1(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
alert(this.name);
}
var person2 = new Person1('Nicholas', 29, 'Software');
var person3 = new Person1('Greg', 27, 'Doctor');
alert(person2.sayName == person3.sayName); // true
如果把構造函數中的方法在外部進行定義也會出現問題:(1)在全局作用域中定義的函數實際上隻能被某個對象調用,讓全局作用域有點名不副實;(2)導緻自定義的引用類型沒有封裝性。可以使用原型模式解決這些問題。
6.2.3、原型模式
我們建立的每個函數都有一個 prototype(原型)屬性,這個屬性是一個指針,指向一個對象,而這個對象的用途可以由特定類型的所有執行個體共享的屬性和方法。如果按照字面意思來了解,那麼 prototype 就是通過調用構造函數而建立的那個對象執行個體的原型對象。使用原型對象的好處是可以讓所有對象執行個體共享它所包含的屬性和方法。
function Person() {
}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function() {
alert(this.name);
};
var person1 = new Person();
person1.sayName(); // Nicholas
var person2 = new Person();
person2.sayName(); // Nicholas
alert(person1.sayName == person2.sayName); // true
person11 和 person21 通路的都是同一組屬性和同一個 sayName() 函數。
1、了解原型對象
無論什麼時候,隻要建立了一個新函數,就會根據一組特定的規則為該函數建立一個 prototype 屬性,這個屬性指向函數的原型對象。在預設情況下,所有原型對象都會自動擷取一個 constructor(構造函數) 屬性,這個屬性是一個指向 prototype 屬性所在函數的指針。就拿前面的例子來說,Person.prototype.constructor 指向 Person。
建立了自定義的構造函數之後,其原型對象預設隻會取得 constructor 屬性;至于其他方法,則都是從 Object 繼承而來。
Person的每個執行個體——person1和person2都包含一個内部屬性,該屬性僅僅指向了 Person.prototype;換句話說,它們與構造函數沒有直接的關系。
prototypeObject(原型對象).isPrototypeOf(對象),在此調用中如果對象繼承自原型對象,則傳回true;否則,傳回false。
console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Person.prototype.isPrototypeOf(person2)); // true
ES5中增加了一個新方法:Object.getPrototypeOf(),傳入參數的對象,傳回該對象的原型對象(即傳入對象的 [[Prototype]] 值)。
alert(Object.getPrototypeOf(person1) == Person.prototype); // true
alert(Object.getPrototypeOf(person1).name); // 'Nicholas'
代碼讀取某個對象的某個屬性時的搜尋路徑:
(1)如果在執行個體中找到了具有給定名字的屬性,則傳回該屬性的值;
(2)如果沒有找到,則繼續搜尋指針指向的原型對象,在原型對象中查找具有給定名字的屬性。
注意:
(1)原型最初隻包含 constructor 屬性,而該屬性也是共享的,是以可以通過對象執行個體通路。
(2)雖然可以通過對象執行個體通路儲存在原型中的值,但卻不可能通過對象執行個體重寫原型中的值。如果執行個體建立的屬性和原型中的屬性名相同,則該屬性将會屏蔽原型中的那個屬性。
function Person1() {
}
Person1.prototype.name = 'Nicholas 1';
Person1.prototype.age = 29;
Person1.prototype.job = 'Software Engineer';
Person1.prototype.sayName = function() {
alert(this.name);
};
var person1 = new Person1();
var person2 = new Person1();
person1.name = 'Greg';
alert(person1.name); // Greg
alert(person2.name); // Nicholas 1
注意:使用 delete 操作符則可以完全删除執行個體屬性,進而讓我們能夠重新通路原型中的屬性。
function Person() {
}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
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); // Nicholas
delete person1.name; // 删除執行個體中的屬性
alert(person1.name); // Nicholas
使用 hasOwnProperty() 方法可以檢測一個屬性是存在于執行個體中,還是存在于原型中。如果 屬性存在于對象執行個體中則傳回 true。
function Person() {
}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'Software';
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); // 來自原型——Nicholas
alert(person2.hasOwnProperty('name')); // false
delete person1.name;
alert(person1.name); // 來自原型——Nicholas
alert(person1.hasOwnProperty('name')); // false
2、原型與 in 操作符
有兩種方法使用 in 操作符:單獨使用和在 for-in 循環中使用。
(1)在單獨使用時,in 操作符會在通過對象能夠通路給定屬性時傳回 true,無論該屬性存在于執行個體中還是原型中。
function Person() {
}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'Software';
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')); // true
alert('name' in person1); // true
alert(person2.name); // Nicholas——來自原型
alert(person2.hasOwnProperty('name')); // false
alert('name' in person2); // true
delete person1.name;
alert(person1.name); // Nicholas——來自原型
alert(person1.hasOwnProperty('name')); // false
alert('name' in person1); // true
(2)使用 for-in 循環時,傳回的是所有能夠通過對象通路的、可枚舉的(enumerable)屬性,其中既包括存在于執行個體中的屬性,也包括存在于原型中的屬性。屏蔽了原型中不可枚舉屬性(即将 [[Enumerable]] 标記為 false 的屬性)的執行個體也會在 for-in 循環中傳回,因為根據規定,所有開發人員定義的屬性都是可枚舉的——隻有在 IE8 及更早版本中例外。
ES5 也将 constructor 和 prototype 屬性的 [[Enumerable]] 特性設定為 false。
function Student() {
}
Object.defineProperty(Student.prototype, 'name', {
writable: true, // 必須設定為 true
value: 'Nicholas'
});
var st = new Student();
st.name = 'Greg';
for (var property in st) {
console.log(property);
}
要取得對象上所有可枚舉的執行個體屬性,可以使用ES5的 Object.keys() 方法。這個方法接收一個對象作為參數,傳回一個包含所有可枚舉屬性的字元串數組(隻會傳回目前對象中的屬性,原型中的屬性不會傳回)。
function Person() {
}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'Software';
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"
如果你想要得到所有執行個體屬性,無論它是否可枚舉,都可以使用 Object.getOwnPropertyNames() 方法。
var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys); // "constructor,name,age,job,sayName"
3、更簡單的原型文法
function Person() {
}
Person.prototype = {
name: 'Nicholas',
age: 29;
job: 'Software',
sayName: function() {
alert(this.name);
}
};
上面代碼執行結果框圖如下所示:
由上圖可知,當給構造函數的原型指派為對象字面量時,實際上是重寫了構造函數的原型對象。最終該原型的 constructor 屬性指向的是 Object 構造函數。
var friend = new Person();
alert(friend instanceof Object); // true
alert(friend instanceof Person); // true
alert(friend.constructor == Person); // false
alert(friend.constructor == Object); // true
4、原型的動态性
我們對原型對象所做的任何修改都能夠立即從執行個體上反映出來——即使是先建立了執行個體後修改原型也照樣如此。
var friend = new Person();
Person.prototype.sayHi = function() {
alert('Hi');
}
friend.sayHi(); // Hi
注意:執行個體中的指針僅指向原型,而不指向構造函數。
function Person() {
}
var friend = new Person();
Person.prototype = { // 重寫了原型對象
constructor: Person,
name: 'Nicholas',
age: 29,
job: 'Software',
sayName: function() {
alert(this.name);
}
};
friend.sayName(); // 會出現錯誤
上面代碼的具體圖解如下所示:
如圖所示,重寫原型對象切斷了現有原型與任何之前已經存在的對象執行個體之間的聯系;它們引用的任然是最初的原型。
5、原生對象的原型
通過原生對象的原型,不僅可以取得所有預設方法的引用,而且也可以定義新方法。
String.prototype.startsWidth = function (text) {
return this.indexOf(text) == 0;
};
var msg = 'Hello World!';
alert(msg.startsWith('Hello')); // true
6、原型對象的問題
原型對象的缺點:
(1)它省略了為構造函數傳遞初始化參數這一環節,結果所有執行個體在預設的情況下都将取得相同的屬性值。
(2)原型模式的最大問題是由其共享的本性所導緻的。
function Person() {}
Person.prototype = {
constructor: Person,
name: 'Nicholas',
age: 29,
job: 'Software',
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
6.2.4、組合使用構造函數模式和原型模式
建立自定義類型的最常見方式,就是組合使用構造函數模式與原型模式。
構造函數模式:用于定義執行個體屬性;
原型模式 :用于定義方法和共享的屬性。
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');
var person2 = new Person('Greg', 27, 'Doctor');
person1.friends.push('Van');
alert(person1.friends); // "Shelby,Court,Van"
alert(person2.friends); // "Shelby,Court"
alert(person1.friends === person2.friends); // false
alert(person1.sayName === person2.sayName); // true
6.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() {
alert(this.name);
};
}
}
var friend = new Person('Nicholas', 29, 'Software');
friend.sayName(); // Nicholas
6.2.6、寄生構造函數
這種模式的基本思想是建立一個函數,該函數的作用僅僅是封裝建立對象的代碼,然後再傳回新建立的對象;但從表面上看,這個函數又很想典型的構造函數。
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('Nicholas', 29, 'Software');
friend.sayName(); // Nicholas
構造函數在不傳回值的情況下,預設會傳回新對象執行個體。通過在構造函數的末尾添加一個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');
alert(colors.toPipedString());
注意:寄生構造函數模式,傳回的對象與構造函數或者與構造函數的原型屬性之間沒有關系;也就是說,構造函數傳回的對象與在構造函數外部建立的對象沒有什麼不同。
6.2.7、穩妥構造函數模式
所謂穩妥對象,指的是沒有公共屬性,而且其方法也不引用 this 的對象。
不同點:(1)新建立對象的執行個體方法不引用 this;(2)不使用 new 操作符調用構造函數。
function Person(name, age, job) {
// 建立要傳回的對象
var o = new Object();
// 可以在這裡定義私有變量和函數
// 添加方法
o.sayName = function() {
alert(name);
};
// 傳回對象
return o;
}
var friend = Person('Nicholas', 29, 'Software');
friend.sayName(); // Nicholas
參考文獻
[1]《JavaScript進階程式設計(第3版)》