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版)》