[TOC]
## 概述
JavaScript原生提供一個Object對象(注意起首的O是大寫),所有其他對象都繼承自這個對象。Object本身也是一個構造函數,可以直接通過它來生成新對象。
~~~
var o = new Object();
~~~
Object作為構造函數使用時,可以接受一個參數。如果該參數是一個對象,則直接返回這個對象;如果是一個原始類型的值,則返回該值對應的包裝對象。
~~~
var o1 = {a:1};
var o2 = new Object(o1);
o1 === o2 // true
new Object(123) instanceof Number
// true
~~~
> 注意,通過new Object() 的寫法生成新對象,與字面量的寫法 o = {} 是等價的。
與其他構造函數一樣,如果要在Object對象上面部署一個方法,有兩種做法。
(1)部署在Object對象本身
比如,在Object對象上面定義一個print方法,顯示其他對象的內容。
~~~
Object.print = function(o){ console.log(o) };
var o = new Object();
Object.print(o)
// Object
~~~
(2)部署在Object.prototype對象
所有構造函數都有一個prototype屬性,指向一個原型對象。凡是定義在Object.prototype對象上面的屬性和方法,將被所有實例對象共享。(關于prototype屬性的詳細解釋,參見《面向對象編程》一章。)
~~~
Object.prototype.print = function(){ console.log(this)};
var o = new Object();
o.print() // Object
~~~
上面代碼在Object.prototype定義了一個print方法,然后生成一個Object的實例o。o直接繼承了Object.prototype的屬性和方法,可以在自身調用它們,也就是說,o對象的print方法實質上是調用Object.prototype.print方法。。
可以看到,盡管上面兩種寫法的print方法功能相同,但是用法是不一樣的,因此必須區分“構造函數的方法”和“實例對象的方法”。
## Object對象的方法
### Object()
Object本身當作工具方法使用時,可以將任意值轉為對象。其中,原始類型的值轉為對應的包裝對象(參見《原始類型的包裝對象》一節)。
~~~
Object() // 返回一個空對象
Object(undefined) // 返回一個空對象
Object(null) // 返回一個空對象
Object(1) // 等同于 new Number(1)
Object('foo') // 等同于 new String('foo')
Object(true) // 等同于 new Boolean(true)
Object([]) // 返回原數組
Object({}) // 返回原對象
Object(function(){}) // 返回原函數
~~~
上面代碼表示Object函數將各種值,轉為對應的對象。
如果Object函數的參數是一個對象,它總是返回原對象。利用這一點,可以寫一個判斷變量是否為對象的函數。
~~~
function isObject(value) {
return value === Object(value);
}
~~~
### Object.keys(),Object.getOwnPropertyNames()
Object.keys方法和Object.getOwnPropertyNames方法很相似,一般用來遍歷對象的屬性。它們的參數都是一個對象,都返回一個數組,該數組的成員都是對象自身的(而不是繼承的)所有屬性名。它們的區別在于,Object.keys方法只返回可枚舉的屬性(關于可枚舉性的詳細解釋見后文),Object.getOwnPropertyNames方法還返回不可枚舉的屬性名。
~~~
var o = {
p1: 123,
p2: 456
};
Object.keys(o)
// ["p1", "p2"]
Object.getOwnPropertyNames(o)
// ["p1", "p2"]
~~~
上面的代碼表示,對于一般的對象來說,這兩個方法返回的結果是一樣的。只有涉及不可枚舉屬性時,才會有不一樣的結果。
~~~
var a = ["Hello", "World"];
Object.keys(a)
// ["0", "1"]
Object.getOwnPropertyNames(a)
// ["0", "1", "length"]
~~~
上面代碼中,數組的length屬性是不可枚舉的屬性,所以只出現在Object.getOwnPropertyNames方法的返回結果中。
由于JavaScript沒有提供計算對象屬性個數的方法,所以可以用這兩個方法代替。
~~~
Object.keys(o).length
Object.getOwnPropertyNames(o).length
~~~
一般情況下,幾乎總是使用Object.keys方法,遍歷數組的屬性。
### Object.observe()
Object.observe方法用于觀察對象屬性的變化。
~~~
var o = {};
Object.observe(o, function(changes) {
changes.forEach(function(change) {
console.log(change.type, change.name, change.oldValue);
});
});
o.foo = 1; // add, 'foo', undefined
o.foo = 2; // update, 'foo', 1
delete o.foo; // delete, 'foo', 2
~~~
上面代碼表示,通過Object.observe函數,對o對象指定回調函數。一旦o對象的屬性出現任何變化,就會調用回調函數,回調函數通過一個參數對象讀取o的屬性變化的信息。
該方法非常新,只有Chrome瀏覽器的最新版本才部署。
### 其他方法
除了上面提到的方法,Object還有不少其他方法,將在后文逐一詳細介紹。
(1)對象屬性模型的相關方法
* Object.getOwnPropertyDescriptor():獲取某個屬性的attributes對象。
* Object.defineProperty():通過attributes對象,定義某個屬性。
* Object.defineProperties():通過attributes對象,定義多個屬性。
* Object.getOwnPropertyNames():返回直接定義在某個對象上面的全部屬性的名稱。
(2)控制對象狀態的方法
* Object.preventExtensions():防止對象擴展。
* Object.isExtensible():判斷對象是否可擴展。
* Object.seal():禁止對象配置。
* Object.isSealed():判斷一個對象是否可配置。
* Object.freeze():凍結一個對象。
* Object.isFrozen():判斷一個對象是否被凍結。
(3)原型鏈相關方法
* Object.create():生成一個新對象,并該對象的原型。
* Object.getPrototypeOf():獲取對象的Prototype對象。
## Object實例對象的方法
除了Object對象本身的方法,還有不少方法是部署在Object.prototype對象上的,所有Object的實例對象都繼承了這些方法。
Object實例對象的方法,主要有以下六個。
* valueOf():返回當前對象對應的值。
* toString():返回當前對象對應的字符串形式。
* toLocalString():返回當前對象對應的本地字符串形式。
* hasOwnProperty():判斷某個屬性是否為當前對象自身的屬性,還是繼承自原型對象的屬性。
* isPrototypeOf():判斷當前對象是否為另一個對象的原型。
* propertyIsEnumerable():判斷某個屬性是否可枚舉。
本節介紹前兩個方法,其他方法將在后文相關章節介紹。
### Object.prototype.valueOf()
valueOf方法的作用是返回一個對象的值,默認情況下返回對象本身。
~~~
var o = new Object();
o.valueOf() === o // true
~~~
上面代碼比較o的valueOf方法返回值與o本身,兩者是一樣的。
valueOf方法的主要用途是,JavaScript自動類型轉換時會默認調用這個方法(詳見上一章《數據類型轉換》一節)。
~~~
var o = new Object();
1 + o // "1[object Object]"
~~~
上面代碼將對象o與數字1相加,這時JavaScript就會默認調用valueOf()方法。所以,如果自定義valueOf方法,就可以得到想要的結果。
~~~
var o = new Object();
o.valueOf = function (){return 2;};
1 + o // 3
~~~
上面代碼自定義了o對象的valueOf方法,于是1 + o就得到了3。這種方法就相當于用o.valueOf覆蓋Object.prototype.valueOf。
### Object.prototype.toString()
toString方法的作用是返回一個對象的字符串形式。
~~~
var o1 = new Object();
o1.toString() // "[object Object]"
var o2 = {a:1};
o2.toString() // "[object Object]"
~~~
上面代碼表示,對于一個對象調用toString方法,會返回字符串[object Object]。
字符串[object Object]本身沒有太大的用處,但是通過自定義toString方法,可以讓對象在自動類型轉換時,得到想要的字符串形式。
~~~
var o = new Object();
o.toString = function (){ return 'hello' };
o + ' ' + 'world' // "hello world"
~~~
上面代碼表示,當對象用于字符串加法時,會自動調用toString方法。由于自定義了toString方法,所以返回字符串hello world。
數組、字符串和函數都分別部署了自己版本的toString方法。
~~~
[1,2,3].toString() // "1,2,3"
'123'.toString() // "123"
(function (){return 123}).toString() // "function (){return 123}"
~~~
### toString()的應用:判斷數據類型
toString方法的主要用途是返回對象的字符串形式,除此之外,還有一個重要的作用,就是判斷一個值的類型。
~~~
var o = {};
o.toString() // "[object Object]"
~~~
上面代碼調用空對象的toString方法,結果返回一個字符串“object Object”,其中第二個Object表示該值的準確類型。這是一個十分有用的判斷數據類型的方法。
實例對象的toString方法,實際上是調用Object.prototype.toString方法。使用call方法,可以在任意值上調用Object.prototype.toString方法,從而幫助我們判斷這個值的類型。不同數據類型的toString方法返回值如下:
* 數值:返回[object Number]。
* 字符串:返回[object String]。
* 布爾值:返回[object Boolean]。
* undefined:返回[object Undefined]。
* null:返回[object Null]。
* 對象:返回"[object " + 構造函數的名稱 + "]" 。
~~~
Object.prototype.toString.call(2) // "[object Number]"
Object.prototype.toString.call('') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(Math) // "[object Math]"
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call([]) // "[object Array]"
~~~
可以利用這個特性,寫出一個比typeof運算符更準確的類型判斷函數。
~~~
var type = function (o){
var s = Object.prototype.toString.call(o);
return s.match(/\[object (.*?)\]/)[1].toLowerCase();
};
type({}); // "object"
type([]); // "array"
type(5); // "number"
type(null); // "null"
type(); // "undefined"
type(/abcd/); // "regex"
type(new Date()); // "date"
~~~
在上面這個type函數的基礎上,還可以加上專門判斷某種類型數據的方法。
~~~
['Null',
'Undefined',
'Object',
'Array',
'String',
'Number',
'Boolean',
'Function',
'RegExp',
'Element',
'NaN',
'Infinite'
].forEach(function (t) {
type['is' + t] = function (o) {
return type(o) === t.toLowerCase();
};
});
type.isObject({}); // true
type.isNumber(NaN); // false
type.isElement(document.createElement('div')); // true
type.isRegExp(/abc/); // true
~~~
## 對象的屬性模型
ECMAScript 5對于對象的屬性,提出了一個精確的描述模型。
### 屬性的attributes對象,Object.getOwnPropertyDescriptor()
在JavaScript內部,每個屬性都有一個對應的attributes對象,保存該屬性的一些元信息。使用Object.getOwnPropertyDescriptor方法,可以讀取attributes對象。
~~~
var o = { p: 'a' };
Object.getOwnPropertyDescriptor(o, 'p')
// Object { value: "a",
// writable: true,
// enumerable: true,
// configurable: true
// }
~~~
上面代碼表示,使用Object.getOwnPropertyDescriptor方法,讀取o對象的p屬性的attributes對象。
attributes對象包含如下元信息:
* value:表示該屬性的值,默認為undefined。
* writable:表示該屬性的值(value)是否可以改變,默認為true。
* enumerable: 表示該屬性是否可枚舉,默認為true,也就是該屬性會出現在for...in和Object.keys()等操作中。
* configurable:表示“可配置性”,默認為true。如果設為false,表示無法刪除該屬性,也不得改變attributes對象(value屬性除外),也就是configurable屬性控制了attributes對象的可寫性。
* get:表示該屬性的取值函數(getter),默認為undefined。
* set:表示該屬性的存值函數(setter),默認為undefined。
### Object.defineProperty(),Object.defineProperties()
Object.defineProperty方法允許通過定義attributes對象,來定義或修改一個屬性,然后返回修改后的對象。它的格式如下:
~~~
Object.defineProperty(object, propertyName, attributesObject)
~~~
Object.defineProperty方法接受三個參數,第一個是屬性所在的對象,第二個是屬性名(它應該是一個字符串),第三個是屬性的描述對象。比如,新建一個o對象,并定義它的p屬性,可以這樣寫:
~~~
var o = Object.defineProperty({}, "p", {
value: 123,
writable: false,
enumerable: true,
configurable: false
});
o.p
// 123
o.p = 246;
o.p
// 123
// 因為writable為false,所以無法改變該屬性的值
~~~
如果一次性定義或修改多個屬性,可以使用Object.defineProperties方法。
~~~
var o = Object.defineProperties({}, {
p1: { value: 123, enumerable: true },
p2: { value: "abc", enumerable: true },
p3: { get: function() { return this.p1+this.p2 },
enumerable:true,
configurable:true
}
});
o.p1 // 123
o.p2 // "abc"
o.p3 // "123abc"
~~~
上面代碼中的p3屬性,定義了取值函數get。這時需要注意的是,一旦定義了取值函數get(或存值函數set),就不能將writable設為true,或者同時定義value屬性,否則會報錯。
~~~
var o = {};
Object.defineProperty(o, "p", {
value: 123,
get: function() { return 456; }
});
// TypeError: Invalid property.
// A property cannot both have accessors and be writable or have a value,
~~~
上面代碼同時定義了get屬性和value屬性,結果就報錯。
Object.defineProperty() 和Object.defineProperties() 的第三個參數,是一個屬性對象。它的writable、configurable、enumerable這三個屬性的默認值都為false。
writable屬性為false,表示對應的屬性的值將不得改寫。
~~~
var o = {};
Object.defineProperty(o, "p", {
value: "bar"
});
o.p // bar
o.p = "foobar";
o.p // bar
Object.defineProperty(o, "p", {
value: "foobar",
});
// TypeError: Cannot redefine property: p
~~~
上面代碼由于writable屬性默認為false,導致無法對p屬性重新賦值,但是不會報錯(嚴格模式下會報錯)。不過,如果再一次使用Object.defineProperty方法對value屬性賦值,就會報錯。
configurable屬性為false,將無法刪除該屬性,也無法修改attributes對象(value屬性除外)。
~~~
var o = {};
Object.defineProperty(o, "p", {
value: "bar",
});
delete o.p
o.p // bar
~~~
上面代碼中,由于configurable屬性默認為false,導致無法刪除某個屬性。
enumerable屬性為false,表示對應的屬性不會出現在for...in循環和Object.keys方法中。
~~~
var o = {
p1: 10,
p2: 13,
};
Object.defineProperty(o, "p3", {
value: 3,
});
for (var i in o) {
console.log(i, o[i]);
}
// p1 10
// p2 13
~~~
上面代碼中,p3屬性是用Object.defineProperty方法定義的,由于enumerable屬性默認為false,所以不出現在for...in循環中。
### 可枚舉性(enumerable)
可枚舉性(enumerable)用來控制所描述的屬性,是否將被包括在for...in循環之中。具體來說,如果一個屬性的enumerable為false,下面三個操作不會取到該屬性。
* for..in循環
* Object.keys方法
* JSON.stringify方法
因此,enumerable可以用來設置“秘密”屬性。
~~~
var o = {a:1, b:2};
o.c = 3;
Object.defineProperty(o, 'd', {
value: 4,
enumerable: false
});
o.d
// 4
for( var key in o ) console.log( o[key] );
// 1
// 2
// 3
Object.keys(o) // ["a", "b", "c"]
JSON.stringify(o // => "{a:1,b:2,c:3}"
~~~
上面代碼中,d屬性的enumerable為false,所以一般的遍歷操作都無法獲取該屬性,使得它有點像“秘密”屬性,但還是可以直接獲取它的值。
至于for...in循環和Object.keys方法的區別,在于前者包括對象繼承自原型對象的屬性,而后者只包括對象本身的屬性。如果需要獲取對象自身的所有屬性,不管enumerable的值,可以使用Object.getOwnPropertyNames方法,詳見下文。
考慮到JSON.stringify方法會排除enumerable為false的值,有時可以利用這一點,為對象添加注釋信息。
~~~
var car = {
id: 123,
color: red,
owner: 12
};
var owner = {
id: 12,
name: Javi
};
Object.defineProperty( car, 'ownerOb', {value: owner} );
car.ownerOb // {id:12, name:Javi}
JSON.stringify(car) // '{id: 123, color: "red", owner: 12}'
~~~
上面代碼中,owner對象作為注釋,加入car對象。由于ownerOb屬性的enumerable為false,所以JSON.stringify最后正式輸出car對象時,會忽略ownerOb屬性。
### Object.getOwnPropertyNames()
Object.getOwnPropertyNames方法返回直接定義在某個對象上面的全部屬性的名稱,而不管該屬性是否可枚舉。
~~~
var o = Object.defineProperties({}, {
p1: { value: 1, enumerable: true },
p2: { value: 2, enumerable: false }
});
Object.getOwnPropertyNames(o)
// ["p1", "p2"]
~~~
一般來說,系統原生的屬性(即非用戶自定義的屬性)都是不可枚舉的。
~~~
// 比如,數組實例自帶length屬性是不可枚舉的
Object.keys([]) // []
Object.getOwnPropertyNames([]) // [ 'length' ]
// Object.prototype對象的自帶屬性也都是不可枚舉的
Object.keys(Object.prototype) // []
Object.getOwnPropertyNames(Object.prototype)
// ['hasOwnProperty',
// 'valueOf',
// 'constructor',
// 'toLocaleString',
// 'isPrototypeOf',
// 'propertyIsEnumerable',
// 'toString']
~~~
上面代碼可以看到,數組的實例對象([])沒有可枚舉屬性,不可枚舉屬性有length;Object.prototype對象也沒有可枚舉屬性,但是有不少不可枚舉屬性。
### Object.prototype.propertyIsEnumerable()
對象實例的propertyIsEnumerable方法用來判斷一個屬性是否可枚舉。
~~~
var o = {};
o.p = 123;
o.propertyIsEnumerable("p") // true
o.propertyIsEnumerable("toString") // false
~~~
上面代碼中,用戶自定義的p屬性是可枚舉的,而繼承自原型對象的toString屬性是不可枚舉的。
### 可配置性(configurable)
可配置性(configurable)決定了是否可以修改屬性的描述對象。也就是說,當configure為false的時候,value、writable、enumerable和configurable都不能被修改了。
~~~
var o = Object.defineProperty({}, 'p', {
value: 1,
writable: false,
enumerable: false,
configurable: false
});
Object.defineProperty(o,'p', {value: 2})
// TypeError: Cannot redefine property: p
Object.defineProperty(o,'p', {writable: true})
// TypeError: Cannot redefine property: p
Object.defineProperty(o,'p', {enumerable: true})
// TypeError: Cannot redefine property: p
Object.defineProperties(o,'p',{configurable: true})
// TypeError: Cannot redefine property: p
~~~
上面代碼首先生成對象o,并且定義屬性p的configurable為false。然后,逐一改動value、writable、enumerable、configurable,結果都報錯。
需要注意的是,writable只有在從false改為true會報錯,從true改為false則是允許的。
~~~
var o = Object.defineProperty({}, 'p', {
writable: true
});
Object.defineProperty(o,'p', {writable: false})
// 修改成功
~~~
至于value,只要writable和configurable有一個為true,就可以改動。
~~~
var o1 = Object.defineProperty({}, 'p', {
value: 1,
writable: true,
configurable: false
});
Object.defineProperty(o1,'p', {value: 2})
// 修改成功
var o2 = Object.defineProperty({}, 'p', {
value: 1,
writable: false,
configurable: true
});
Object.defineProperty(o2,'p', {value: 2})
// 修改成功
~~~
可配置性決定了一個變量是否可以被刪除(delete)。
~~~
var o = Object.defineProperties({}, {
p1: { value: 1, configurable: true },
p2: { value: 2, configurable: false }
});
delete o.p1 // true
delete o.p2 // false
o.p1 // undefined
o.p2 // 2
~~~
上面代碼中的對象o有兩個屬性,p1是可配置的,p2是不可配置的。結果,p2就無法刪除。
需要注意的是,當使用var命令聲明變量時,變量的configurable為false。
~~~
var a1 = 1;
Object.getOwnPropertyDescriptor(this,'a1')
// Object {
// value: 1,
// writable: true,
// enumerable: true,
// configurable: false
// }
~~~
而不使用var命令聲明變量時(或者使用屬性賦值的方式聲明變量),變量的可配置性為true。
~~~
a2 = 1;
Object.getOwnPropertyDescriptor(this,'a2')
// Object {
// value: 1,
// writable: true,
// enumerable: true,
// configurable: true
// }
// 或者寫成
this.a3 = 1;
Object.getOwnPropertyDescriptor(this,'a3')
// Object {
// value: 1,
// writable: true,
// enumerable: true,
// configurable: true
// }
~~~
上面代碼中的`this.a3 = 1`與`a3 = 1`是等價的寫法。this指的是當前的作用域,更多關于this的解釋,參見《面向對象編程》一章。
這種差異意味著,如果一個變量是使用var命令生成的,就無法用delete命令刪除。也就是說,delete只能刪除對象的屬性。
~~~
var a1 = 1;
a2 = 1;
delete a1 // false
delete a2 // true
a1 // 1
a2 // ReferenceError: a2 is not defined
~~~
### 可寫性(writable)
可寫性(writable)決定了屬性的值(value)是否可以被改變。
~~~
var o = {};
Object.defineProperty(o, "a", { value : 37, writable : false });
o.a // 37
o.a = 25;
o.a // 37
~~~
上面代碼將o對象的a屬性可寫性設為false,然后改變這個屬性的值,就不會有任何效果。
這實際上將某個屬性的值變成了常量。在ES6中,constant命令可以起到這個作用,但在ES5中,只有通過writable達到同樣目的。
這里需要注意的是,當對a屬性重新賦值的時候,并不會拋出錯誤,只是靜靜地失敗。但是,如果在嚴格模式下,這里就會拋出一個錯誤,即使是對a屬性重新賦予一個同樣的值。
關于可寫性,還有一種特殊情況。就是如果原型對象的某個屬性的可寫性為false,那么派生對象將無法自定義這個屬性。
~~~
var proto = Object.defineProperty({}, 'foo', {
value: 'a',
writable: false
});
var o = Object.create(proto);
o.foo = 'b';
o.foo // 'a'
~~~
上面代碼中,對象proto的foo屬性不可寫,結果proto的派生對象o,也不可以再自定義這個屬性了。在嚴格模式下,這樣做還會拋出一個錯誤。但是,有一個規避方法,就是通過覆蓋attributes對象,繞過這個限制,原因是這種情況下,原型鏈會被完全忽視。
~~~
Object.defineProperty(o, 'foo', { value: 'b' });
o.foo // 'b'
~~~
### 存取器(accessor)
除了直接定義以外,屬性還可以用存取器(accessor)定義。其中,存值函數稱為setter,使用set命令;取值函數稱為getter,使用get命令。
~~~
var o = {
get p() {
return "getter";
},
set p(value) {
console.log("setter: "+value);
}
}
~~~
上面代碼中,o對象內部的get和set命令,分別定義了p屬性的取值函數和存值函數。定義了這兩個函數之后,對p屬性取值時,取值函數會自動調用;對p屬性賦值時,存值函數會自動調用。
~~~
o.p // getter
o.p = 123 // setter: 123
~~~
存取器往往用于,某個屬性的值需要依賴對象內部數據的場合。
~~~
var o ={
$n : 5,
get next(){return this.$n++ },
set next(n) {
if (n >= this.$n) this.$n = n;
else throw "新的值必須大于當前值";
}
};
o.next // 5
o.next = 10;
o.next //10
~~~
上面代碼中,next屬性的存值函數和取值函數,都依賴于對內部屬性$n的操作。
下面是另一個存取器的例子。
~~~
var user = {}
var nameValue = 'Joe';
Object.defineProperty(user, 'name', {
get: function() { return nameValue },
set: function(newValue) { nameValue = newValue; },
configurable: true
});
user.name //Joe
user.name = 'Bob';
user.name //Bob
nameValue //Bob
~~~
上面代碼使用存取器,將user對象name綁定在nameValue屬性上了。
存取器也可以使用Object.create方法定義。
~~~
var o = Object.create(Object.prototype, {
foo: {
get: function () {
return 'getter';
},
set: function (value) {
console.log('setter: '+value);
}
}
});
~~~
如果使用上面這種寫法,屬性foo必須定義一個屬性描述對象。該對象的get和set屬性,分別是foo的取值函數和存值函數。
利用存取器,可以實現數據對象與DOM對象的雙向綁定。
~~~
Object.defineProperty(user, 'name', {
get: function() {
return document.getElementById("foo").value;
},
set: function(newValue) {
document.getElementById("foo").value = newValue;
},
configurable: true
});
~~~
上面代碼使用存取函數,將DOM對象foo與數據對象user的name屬性,實現了綁定。兩者之中只要有一個對象發生變化,就能在另一個對象上實時反映出來。
## 控制對象狀態
JavaScript提供了三種方法,精確控制一個對象的讀寫狀態,防止對象被改變。最弱一層的保護是preventExtensions,其次是seal,最強的freeze。
### Object.preventExtensions方法
Object.preventExtensions方法可以使得一個對象無法再添加新的屬性。
~~~
var o = new Object();
Object.preventExtensions(o);
Object.defineProperty(o, "p", { value: "hello" });
// TypeError: Cannot define property:p, object is not extensible.
o.p = 1;
o.p // undefined
~~~
如果是在嚴格模式下,則會拋出一個錯誤。
~~~
(function () {
'use strict';
o.p = '1'
}());
// TypeError: Can't add property bar, object is not extensible
~~~
不過,對于使用了preventExtensions方法的對象,可以用delete命令刪除它的現有屬性。
~~~
var o = new Object();
o.p = 1;
Object.preventExtensions(o);
delete o.p;
o.p // undefined
~~~
### Object.isExtensible方法
Object.isExtensible方法用于檢查一個對象是否使用了preventExtensions方法。也就是說,該方法可以用來檢查是否可以為一個對象添加屬性。
~~~
var o = new Object();
Object.isExtensible(o)
// true
Object.preventExtensions(o);
Object.isExtensible(o)
// false
~~~
上面代碼新生成了一個o對象,對該對象使用Object.isExtensible方法,返回true,表示可以添加新屬性。對該對象使用Object.preventExtensions方法以后,再使用Object.isExtensible方法,返回false,表示已經不能添加新屬性了。
### Object.seal方法
Object.seal方法使得一個對象既無法添加新屬性,也無法刪除舊屬性。
~~~
var o = { p:"hello" };
Object.seal(o);
delete o.p;
o.p // "hello"
o.x = 'world';
o.x // undefined
~~~
Object.seal還把現有屬性的attributes對象的configurable屬性設為false,使得attributes對象不再能改變。
~~~
var o = { p: 'a' };
// seal方法之前
Object.getOwnPropertyDescriptor(o, 'p')
// Object {value: "a", writable: true, enumerable: true, configurable: true}
Object.seal(o);
// seal方法之后
Object.getOwnPropertyDescriptor(o, 'p')
// Object {value: "a", writable: true, enumerable: true, configurable: false}
Object.defineProperty(o, 'p', { enumerable: false })
// TypeError: Cannot redefine property: p
~~~
從上面代碼可以看到,使用seal方法之后,attributes對象的configurable就變成了false,然后如果想改變enumerable就會報錯。
可寫性(writable)有點特別。如果writable為false,使用Object.seal方法以后,將無法將其變成true;但是,如果writable為true,依然可以將其變成false。
~~~
var o1 = Object.defineProperty({}, 'p', {writable: false});
Object.seal(o1);
Object.defineProperty(o1,'p',{writable:true})
// Uncaught TypeError: Cannot redefine property: p
var o2 = Object.defineProperty({}, 'p', {writable: true});
Object.seal(o2);
Object.defineProperty(o2,'p',{writable:false})
Object.getOwnPropertyDescriptor(o2, 'p')
/* { value: '',
writable: false,
enumerable: true,
configurable: false } */
~~~
上面代碼中,同樣是使用了Object.seal方法,如果writable原為false,改變這個設置將報錯;如果原為true,則不會有問題。
至于屬性對象的value是否可改變,是由writable決定的。
~~~
var o = { p: 'a' };
Object.seal(o);
o.p = 'b';
o.p // 'b'
~~~
上面代碼中,Object.seal方法對p屬性的value無效,是因為此時p屬性的writable為true。
### Object.isSealed方法
Object.isSealed方法用于檢查一個對象是否使用了Object.seal方法。
~~~
var o = { p: 'a' };
Object.seal(o);
Object.isSealed(o) // true
~~~
另外,這時isExtensible方法也返回false。
~~~
var o = { p: 'a' };
Object.seal(o);
Object.isExtensible(o) // false
~~~
### Object.freeze方法
Object.freeze方法可以使得一個對象無法添加新屬性、無法刪除舊屬性、也無法改變屬性的值,使得這個對象實際上變成了常量。
~~~
var o = {p:"hello"};
Object.freeze(o);
o.p = "world";
o.p // hello
o.t = "hello";
o.t // undefined
~~~
上面代碼中,對現有屬性重新賦值(o.p = "world")或者添加一個新屬性,并不會報錯,只是默默地失敗。但是,如果是在嚴格模式下,就會報錯。
~~~
var o = {p:"hello"};
Object.freeze(o);
// 對現有屬性重新賦值
(function () { 'use strict'; o.p = "world";}())
// TypeError: Cannot assign to read only property 'p' of #<Object>
// 添加不存在的屬性
(function () { 'use strict'; o.t = 123;}())
// TypeError: Can't add property t, object is not extensible
~~~
### Object.isFrozen方法
Object.isFrozen方法用于檢查一個對象是否使用了Object.freeze()方法。
~~~
var o = {p:"hello"};
Object.freeze(o);
Object.isFrozen(o) // true
~~~
### 局限性
需要注意的是,使用上面這些方法鎖定對象的可寫性,但是依然可以通過改變該對象的原型對象,來為它增加屬性。
~~~
var o = new Object();
Object.preventExtensions(o);
var proto = Object.getPrototypeOf(o);
proto.t = "hello";
o.t
// hello
~~~
一種解決方案是,把原型也凍結住。
~~~
var o = Object.seal(
Object.create(Object.freeze({x:1}),
{y: {value: 2, writable: true}})
);
Object.getPrototypeOf(o).t = "hello";
o.hello // undefined
~~~
## 參考鏈接
* Axel Rauschmayer,?[Protecting objects in JavaScript](http://www.2ality.com/2013/08/protecting-objects.html)
* kangax,?[Understanding delete](http://perfectionkills.com/understanding-delete/)
* Jon Bretman,?[Type Checking in JavaScript](http://techblog.badoo.com/blog/2013/11/01/type-checking-in-javascript/)
* Cody Lindley,?[Thinking About ECMAScript 5 Parts](http://tech.pro/tutorial/1671/thinking-about-ecmascript-5-parts)
* Bjorn Tipling,?[Advanced objects in JavaScript](http://bjorn.tipling.com/advanced-objects-in-javascript)
* Javier Márquez,?[Javascript properties are enumerable, writable and configurable](http://arqex.com/967/javascript-properties-enumerable-writable-configurable)
* Sella Rafaeli,?[Native JavaScript Data-Binding](http://www.sellarafaeli.com/blog/native_javascript_data_binding): 使用存取函數實現model與view的雙向綁定
- 第一章 導論
- 1.1 前言
- 1.2 為什么學習JavaScript?
- 1.3 JavaScript的歷史
- 第二章 基本語法
- 2.1 語法概述
- 2.2 數值
- 2.3 字符串
- 2.4 對象
- 2.5 數組
- 2.6 函數
- 2.7 運算符
- 2.8 數據類型轉換
- 2.9 錯誤處理機制
- 2.10 JavaScript 編程風格
- 第三章 標準庫
- 3.1 Object對象
- 3.2 Array 對象
- 3.3 包裝對象和Boolean對象
- 3.4 Number對象
- 3.5 String對象
- 3.6 Math對象
- 3.7 Date對象
- 3.8 RegExp對象
- 3.9 JSON對象
- 3.10 ArrayBuffer:類型化數組
- 第四章 面向對象編程
- 4.1 概述
- 4.2 封裝
- 4.3 繼承
- 4.4 模塊化編程
- 第五章 DOM
- 5.1 Node節點
- 5.2 document節點
- 5.3 Element對象
- 5.4 Text節點和DocumentFragment節點
- 5.5 Event對象
- 5.6 CSS操作
- 5.7 Mutation Observer
- 第六章 瀏覽器對象
- 6.1 瀏覽器的JavaScript引擎
- 6.2 定時器
- 6.3 window對象
- 6.4 history對象
- 6.5 Ajax
- 6.6 同域限制和window.postMessage方法
- 6.7 Web Storage:瀏覽器端數據儲存機制
- 6.8 IndexedDB:瀏覽器端數據庫
- 6.9 Web Notifications API
- 6.10 Performance API
- 6.11 移動設備API
- 第七章 HTML網頁的API
- 7.1 HTML網頁元素
- 7.2 Canvas API
- 7.3 SVG 圖像
- 7.4 表單
- 7.5 文件和二進制數據的操作
- 7.6 Web Worker
- 7.7 SSE:服務器發送事件
- 7.8 Page Visibility API
- 7.9 Fullscreen API:全屏操作
- 7.10 Web Speech
- 7.11 requestAnimationFrame
- 7.12 WebSocket
- 7.13 WebRTC
- 7.14 Web Components
- 第八章 開發工具
- 8.1 console對象
- 8.2 PhantomJS
- 8.3 Bower:客戶端庫管理工具
- 8.4 Grunt:任務自動管理工具
- 8.5 Gulp:任務自動管理工具
- 8.6 Browserify:瀏覽器加載Node.js模塊
- 8.7 RequireJS和AMD規范
- 8.8 Source Map
- 8.9 JavaScript 程序測試
- 第九章 JavaScript高級語法
- 9.1 Promise對象
- 9.2 有限狀態機
- 9.3 MVC框架與Backbone.js
- 9.4 嚴格模式
- 9.5 ECMAScript 6 介紹
- 附錄
- 10.1 JavaScript API列表
- 草稿一:函數庫
- 11.1 Underscore.js
- 11.2 Modernizr
- 11.3 Datejs
- 11.4 D3.js
- 11.5 設計模式
- 11.6 排序算法
- 草稿二:jQuery
- 12.1 jQuery概述
- 12.2 jQuery工具方法
- 12.3 jQuery插件開發
- 12.4 jQuery.Deferred對象
- 12.5 如何做到 jQuery-free?
- 草稿三:Node.js
- 13.1 Node.js 概述
- 13.2 CommonJS規范
- 13.3 package.json文件
- 13.4 npm模塊管理器
- 13.5 fs 模塊
- 13.6 Path模塊
- 13.7 process對象
- 13.8 Buffer對象
- 13.9 Events模塊
- 13.10 stream接口
- 13.11 Child Process模塊
- 13.12 Http模塊
- 13.13 assert 模塊
- 13.14 Cluster模塊
- 13.15 os模塊
- 13.16 Net模塊和DNS模塊
- 13.17 Express框架
- 13.18 Koa 框架