第3章 理解對象
3.1 定義屬性
當一個屬性第一次被添加給對象時,JavaScript在對象上調用一個名為[[Put]]的內部方法。這個操作不僅指定了初始的值,也定義了屬性的一些特征。
當一個已有的屬性被賦予一個新值時,調用的是一個名為[[Set]]的方法。
//Object實例化
var person1 = new Object();
person1.name = 'liang'; //調用[[Put]]的內部方法
//對象字面形式
var person2 = {
name: 'liang'
};
person2.name = 'zhu'; //調用[[Set]]方法
3.2 屬性探測
in 操作符在給定對象中查找一個給定名稱的屬性,如果找到則返回true。
in操作符會檢測自有屬性和原型屬性,如果只要檢測自有屬性,使用hasOwnProperty()方法。
var person1 = {
name: 'liang',
age: 26
};
console.log('name' in person1); //true
console.log(person1.hasOwnProperty('name')); //true
//tostring是原型屬性,所以用hasOwnProperty檢測結果為false
console.log('toString' in person1); //true
console.log(person1.hasOwnProperty('toString')); //false
//可用這樣一個函數來鑒別原型屬性,結果為true則是原型屬性
function check(object, name){
return (name in object) && !object.hasOwnProperty(name);
}
console.log(check(person1,'age')); //false
console.log(check(person1,'toString')); //true
3.3 刪除屬性
delete 操作符針對單個對象屬性調用名為[[Delete]]的內部方法。操作成功時,它返回true。
var person1 = {
name: 'liang',
age: 26
};
delete person1.age;
'age' in person1; //false
同名的自有屬性會覆蓋原型屬性的值。delete 可以刪除自有屬性,不能刪除原型屬性。
3.4 屬性枚舉
[[Enumerable]]是屬性的一個內部特征,指示屬性是否可枚舉,默認為true(可枚舉的)。
for-in 循環會枚舉一個對象所有的可枚舉屬性,并將屬性賦給一個變量。
var person1 = {
name: 'liang',
age: 26
};
var property;
for (property in person1){
console.log('Name' + ': ' + property); //枚舉屬性
console.log('Value' + ': ' + person1[property]); //枚舉屬性的值
}
//Name: name Value: liang Name: age Value: 26
Object.keys() 方法,獲取可枚舉屬性的名字的數組。
for-in 循環會遍歷原型屬性,而Object.keys()只返回自有屬性。
var person1 = {
name: 'liang',
age: 26
};
//獲取數組
var propertys = Object.keys(person1);
propertys; //['name', 'age']
//輸出屬性和屬性值
var i, len;
for(i=0, len=propertys.length; i<len; i++){
console.log('Name' + ': ' + propertys[i]);
console.log('Value' + ': ' + person1[propertys[i]]);
} //Name: name Value: liang Name: age Value: 26
3.5 屬性類型
對象的屬性有兩種類型:數據屬性和訪問器屬性。
數據屬性只包含一個值。
訪問器屬性不包含值,而是定義了一個當屬性被讀取時調用的函數(稱為getter),和一個當屬性被寫入時調用的函數(稱為setter)。
訪問器屬性僅需要getter或setter兩者中的任意一個,也可以兩者都有。
特殊關鍵字get和set被用在訪問器屬性名字的前面,后面跟著小括號和函數體。getter被期望返回一個值,而setter則接受一個需要被賦給屬性的值作為參數。
var person1 = {
_name: 'liang', //命名規范:下劃線表示該屬性被認為是私有的
get name(){
console.log('the name is ');
return this._name;
}, //此處加逗號
set name(value){
console.log('set name to',value);
this._name = value;
}
};
//注意:使用的是person1.name,而不是person1._name
console.log(person1.name); //the name is 'liang'
console.log(person1._name); //liang
person1.name = 'zhu'; //set name to zhu 'zhu'
console.log(person1.name); //the name is 'zhu'
console.log(Object.keys(person1)); //['_name', 'name']
通常情況下不使用訪問器屬性,但當你希望賦值操作會觸發一些行為,或讀取的值需要通過計算所需的返回值,可使用訪問器屬性。
如果只定義getter,該屬性就變成只讀。
如果只定義setter,該屬性就變成只寫。
3.6 屬性特征
3.6.1 通用特征
有兩個屬性特征是數據和訪問器屬性都具有的。
[[Enumerable]],決定了是否可以遍歷該屬性。
[[Configurable]],決定了該屬性是否可配置。delete可以刪除可配置的屬性。
Object.definedProperty() 方法,可用來改變屬性特征。有3個參數:擁有該屬性的對象、屬性名、包含需要設置特征的屬性描述對象。
var person1 = {
name: 'liang'
};
Object.defineProperty(person1,'name',{
enumerable: false
});
console.log('name' in person1); //true
console.log(Object.keys(person1)); // []
3.6.2 數據屬性特征
數據屬性額外擁有兩個特征:
[[Value]] 包含屬性的值。
[[Writable]] 指示該屬性是否可以寫入。
var person1 = {};
Object.defineProperty(person1,'name',{
value: 'liang',
writable: true,
enumerable: true,
configurable: true
});
Object.defineProperty() 方法調用時,會首先檢查該屬性是否存在,如果不存在,將根據屬性描述對象指定的特征創建。調用這個方法,如果不指定特征的布爾值,它會默認設置為false。
3.6.2 訪問器屬性特征
訪問器屬性不需要存儲值,因此沒有[[Value]]和[[Writable]]。
有另外兩個額外特征:[[Get]]和[[Set]]
之前的例子:
var person1 = {
_name: 'liang', //命名規范:下劃線表示該屬性被認為是私有的,實際上還是公有的
get name(){
console.log('the name is ');
return this._name;
}, //此處加逗號
set name(value){
console.log('set name to',value);
this._name = value;
}
};
改寫成如下形式:
var person1 = {
_name: 'liang'
};
Object.defineProperty(person1,'name',{ //注意這里的name,不是_name,也可以用別的字符串來定義訪問器屬性
get:function(){
console.log('Reading name');
return this._name;
},
set:function(value){
console.log('Setting name to ',value);
this._name=value;
},
enumerable:true,
configurable:true
});
person1.name = 'zhang'; //setting name to zhang 'zhang'
3.6.4 定義多重屬性
Object.defineProperties() 可以為一個對象同時定義多個屬性。
接受兩個參數:需要改變的對象、一個包含所有屬性信息的對象。
var person1 = {};
Object.defineProperties(person1,{
_name: {
value: 'liang',
enumerable: true,
configurable: true,
writable: true
},
name: {
get:function(){
console.log('reading name');
return this._name;
},
set: function(value){
console.log('setting name to %s',value);
this._name=value;
},
enumerable: true,
configurable: true
}
});
person1.name; //reading name 'liang'
person1._name; //'liang'
3.6.5 獲取屬性特征
Object.getOwnPropertyDescriptor() 方法獲取屬性的特征。
這個方法只適用于自有屬性。接受兩個參數:對象、屬性名。
var person1 = {
name: 'liang'
};
var descriptor = Object.getOwnPropertyDescriptor(person,'name');
descriptor; //{value: 'liang', writable: true, enumerable: true, configurable: true}
3.7 禁止修改對象
[[Extensible]] 指明該對象是否可以被修改。有3種方法來鎖定對象屬性。
Object.preventExtensible() 禁止擴展,對象不能繼續添加新的屬性。 Object.isExtensible() 判斷是否為可擴展的。
Object.seal() 封印對象,對象不能繼續添加新的屬性,且不能刪除和改變屬性類型。 Object.isSealed() 判斷是否被封印的。
Object.freeze() 凍結對象,對象不能繼續添加新的屬性,且不能刪除和改變屬性類型,還不能寫入任何數據屬性。 Object.isFrozen() 判斷是否被凍結。
var person1 = {
name: 'liang'
}
Object.seal(person1);
delete person1.name; //將無法刪除
console.log(Object.isExtensible(person1)); //false
console.log(Object.isSealed(person1)); //true
第4章 構造函數和原型對象
4.1 構造函數
構造函數名的首字母要大寫。
function Person(){
//statement
}
構造函數接受一個命名參數name并將其賦給this對象的name屬性。
function Person(name){
this.name = name;
this.sayName = function(){
console.log(this.name);
};
}
var person1 = new Person('liang');
person1.sayName(); //'liang'
構造函數中還能用Object.defineProperty() 方法來幫助我們初始化。
function Person(name){
Object.defineProperty(this,'name',{
get:function(){
return name;
},
set:function(newName){
name = newName;
},
enumerable:true,
configurable:true
});
}
4.2 原型對象
幾乎所有的函數(除了一些內建函數)都有一個名為prototype的屬性,該屬性是一個原型對象,用來創建新的對象實例。
所有創建的對象實例共享該原型對象,且這些對象實例可以訪問原型對象的屬性。
當你試圖訪問一個對象的某個屬性時,JavaScript首先在自有屬性里查找該名字,如果在自有屬性里沒有找到則查找原型屬性。
4.2.1 [[Prototype]] 屬性
對象實例通過內部屬性[[Prototype]]跟蹤其原型對象。該屬性是對象實例指向原型對象的指針。
Object.getPrototypeOf() 方法讀取[[Prototype]]屬性的值。
var obj = {};
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
//任何一個泛用對象,其[[Prototype]]屬性始終指向Object.prototype
isPrototypeOf() 方法檢測某個對象(Object)是否是另一個對象(obj)的原型對象。
var obj = {};
console.log(Object.prototype.isPrototypeOf(obj)); //true
或者說isPrototypeOf() 是檢測一個對象(person1)是否是另一個對象(Person)的對象實例。
function Person(name){
this.name = name;
this.sayName = function(){
console.log(this.name);
};
}
var person1 = new Person('liang');
Person.prototype.isPrototypeOf(person1); //true
Object.getPrototypeOf(person1) === Person.prototype; //true
4.2.2 在構造函數中使用原型對象
function Person(name){
this.name = name;
}
console.log(Person.prototype); //{constructor: ?}
Person.prototype.sayName = function(){
return this.name;
};
console.log(Person.prototype); //{sayName: ?, constructor: ?}
var person1 = new Person('liang1');
person1.sayName(); //'liang1'
上例中,sayName() 現在是一個原型屬性而不是自有屬性。
如下的構造函數,sayName是一個自有屬性,區分這兩種創建方式。
function Person(name){
this.name = name;
this.sayName = function(){
return this.name;
}
}
console.log(Person.prototype); //{constructor: ?}
原型對象上存儲其他類型的數據,存儲引用值時要注意,這些引用值會被多個實例共享。
function Person(name){
this.name = name;
}
Person.prototype.favorites= [];
var person1 = new Person('liang');
var person2 = new Person('zhu');
person1.favorites.push('pizza');
person2.favorites.push('quinoa');
person1.favorites; //['pizza', 'quinoa']
通過一個對象字面形式替換原型對象,來設置多個原型屬性。
function Person(name){
this.name = name;
}
Person.prototype={
sayName: function(){
return this.name;
},
toString: function(){
return '[Person ' + this.name + ']';
}
};
var person1 = new Person('liang');
console.log(person1.sayName()); //'liang'
console.log(person1.toString()); //'[Person liang]'
使用對象字面形式改寫原型對象改變了構造函數的屬性,因此它現在指向Object而不是Person。
console.log(person1.constructor === Person); //false
console.log(person1.constructor === Object); //true
這是因為原型對象具有一個constructor屬性,這是對象實例所沒有的。當一個函數被創建時,它的prototype屬性也被創建,且該原型對象的constructor屬性指向該函數。當使用對象字面形式改寫原型對象Person.prototype時,其contructor屬性將被置為泛用對象Object。
為避免這一點,手動重置constructor屬性:
function Person(name){
this.name = name;
}
Person.prototype={
constructor:Person,
sayName: function(){
return this.name;
},
toString: function(){
return '[Person ' + this.name + ']';
}
};
構造函數、原型對象、對象實例,這三者的關系:
對于構造函數來說,prototype是作為構造函數的屬性;對于對象實例來說,prototype是對象實例的原型對象。所以prototype即是屬性,又是對象。
所有一切對象的原型頂端,都是Object.prototype。
4.2.3 改變原型對象
給定類型的所有對象實例共享一個原型對象。[[Prototype]]屬性只是一個指向原型對象的指針,任何對原型對象的改變都立即反應到所有引用它的對象實例上。
使用Object.seal()或Object.freeze()方法,將無法添加自有屬性或改變凍結對象的自有屬性,但可以通過在原型對象上添加屬性來擴展這些對象實例。
var person1 = new Person('liang');
var person2 = new Person('zhu');
Object.freeze(person1);
Person.prototype.sayHi = function(){
console.log('Hi');
};
person1.sayHi();
person2.sayHi();
4.2.4 內建對象的原型對象
所有內建對象都有構造函數,因此也都有原型對象可改變。
Array.prototype.sum = function(){
return this.reduce(function(previous,current){ //reduce()是數組方法
return previous + current;
});
};
var numbers = [1,2,3,4,5];
numbers.sum(); //15
在sum() 內部,this指向數組的對象實例numbers,于是該方法也可以自由使用數組的其他方法,比如reduce()。
內建對象的原型對象雖然可以改變,但不建議在生產環境中使用。可以用來做實驗和驗證新功能。