JavaScript 屬性描述對(duì)象

2018-07-24 11:51 更新

目錄

概述

JavaScript提供了一個(gè)內(nèi)部數(shù)據(jù)結(jié)構(gòu),用來(lái)描述一個(gè)對(duì)象的屬性的行為,控制它的行為。這被稱為“屬性描述對(duì)象”(attributes object)。每個(gè)屬性都有自己對(duì)應(yīng)的屬性描述對(duì)象,保存該屬性的一些元信息。

下面是屬性描述對(duì)象的一個(gè)實(shí)例。

{
  value: 123,
  writable: false,
  enumerable: true,
  configurable: false,
  get: undefined,
  set: undefined
}

屬性描述對(duì)象提供6個(gè)元屬性。

(1)value

value存放該屬性的屬性值,默認(rèn)為undefined。

(2)writable

writable存放一個(gè)布爾值,表示屬性值(value)是否可改變,默認(rèn)為true。

(3)enumerable

enumerable存放一個(gè)布爾值,表示該屬性是否可枚舉,默認(rèn)為true。如果設(shè)為false,會(huì)使得某些操作(比如for...in循環(huán)、Object.keys())跳過(guò)該屬性。

(4)configurable

configurable存放一個(gè)布爾值,表示“可配置性”,默認(rèn)為true。如果設(shè)為false,將阻止某些操作改寫該屬性,比如,無(wú)法刪除該屬性,也不得改變?cè)搶傩缘膶傩悦枋鰧?duì)象(value屬性除外)。也就是說(shuō),configurable屬性控制了屬性描述對(duì)象的可寫性。

(5)get

get存放一個(gè)函數(shù),表示該屬性的取值函數(shù)(getter),默認(rèn)為undefined。

(6)set

set存放一個(gè)函數(shù),表示該屬性的存值函數(shù)(setter),默認(rèn)為undefined。

Object.getOwnPropertyDescriptor()

Object.getOwnPropertyDescriptor方法可以讀出對(duì)象自身屬性的屬性描述對(duì)象。

var o = { p: 'a' };

Object.getOwnPropertyDescriptor(o, 'p')
// Object { value: "a",
//   writable: true,
//   enumerable: true,
//   configurable: true
// }

上面代碼表示,使用Object.getOwnPropertyDescriptor方法,讀取o對(duì)象的p屬性的屬性描述對(duì)象。

Object.defineProperty(),Object.defineProperties()

Object.defineProperty方法允許通過(guò)定義屬性描述對(duì)象,來(lái)定義或修改一個(gè)屬性,然后返回修改后的對(duì)象。它的格式如下。

Object.defineProperty(object, propertyName, attributesObject)

上面代碼中,Object.defineProperty方法接受三個(gè)參數(shù),第一個(gè)是屬性所在的對(duì)象,第二個(gè)是屬性名(它應(yīng)該是一個(gè)字符串),第三個(gè)是屬性的描述對(duì)象。比如,新建一個(gè)o對(duì)象,并定義它的p屬性,寫法如下。

var o = Object.defineProperty({}, 'p', {
  value: 123,
  writable: false,
  enumerable: true,
  configurable: false
});

o.p
// 123

o.p = 246;
o.p
// 123
// 因?yàn)閣ritable為false,所以無(wú)法改變?cè)搶傩缘闹?/span>

如果屬性已經(jīng)存在,Object.defineProperty方法相當(dāng)于更新該屬性的屬性描述對(duì)象。

需要注意的是,Object.defineProperty方法和后面的Object.defineProperties方法,都有性能損耗,會(huì)拖慢執(zhí)行速度,不宜大量使用。

如果一次性定義或修改多個(gè)屬性,可以使用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屬性,定義了取值函數(shù)get。這時(shí)需要注意的是,一旦定義了取值函數(shù)get(或存值函數(shù)set),就不能將writable設(shè)為true,或者同時(shí)定義value屬性,會(huì)報(bào)錯(cuò)。

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,

上面代碼同時(shí)定義了get屬性和value屬性,結(jié)果就報(bào)錯(cuò)。

Object.defineProperty()Object.defineProperties()的第三個(gè)參數(shù),是一個(gè)屬性對(duì)象。它的writable、configurable、enumerable這三個(gè)屬性的默認(rèn)值都為false。

var obj = {};
Object.defineProperty(obj, 'foo', { configurable: true });
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
//   value: undefined,
//   writable: false,
//   enumerable: false,
//   configurable: true
// }

上面代碼中,定義obj對(duì)象的foo屬性時(shí),只定義了可配置性configurabletrue。結(jié)果,其他元屬性都是默認(rèn)值。

writable屬性為false,表示對(duì)應(yīng)的屬性的值將不得改寫。

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屬性默認(rèn)為false,導(dǎo)致無(wú)法對(duì)p屬性重新賦值,但是不會(huì)報(bào)錯(cuò)(嚴(yán)格模式下會(huì)報(bào)錯(cuò))。不過(guò),如果再一次使用Object.defineProperty方法對(duì)value屬性賦值,就會(huì)報(bào)錯(cuò)。

configurable屬性為false,將無(wú)法刪除該屬性,也無(wú)法修改attributes對(duì)象(value屬性除外)。

var o = {};

Object.defineProperty(o, 'p', {
  value: 'bar',
});

delete o.p
o.p // "bar"

上面代碼中,由于configurable屬性默認(rèn)為false,導(dǎo)致無(wú)法刪除某個(gè)屬性。

enumerable屬性為false,表示對(duì)應(yīng)的屬性不會(huì)出現(xiàn)在for...in循環(huán)和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屬性默認(rèn)為false,所以不出現(xiàn)在for...in循環(huán)中。

元屬性

屬性描述對(duì)象的屬性,被稱為“元屬性”,因?yàn)樗梢钥醋魇强刂茖傩缘膶傩浴?/p>

可枚舉性(enumerable)

JavaScript的最初版本,in 運(yùn)算符和基于它的for...in循環(huán),會(huì)遍歷對(duì)象實(shí)例的所有屬性,包括繼承的屬性。

var obj = {};
'toString' in obj // true

上面代碼中,toString不是obj對(duì)象自身的屬性,但是in運(yùn)算符也返回true,導(dǎo)致被for...in循環(huán)遍歷,這顯然不太合理。后來(lái)就引入了“可枚舉性”這個(gè)概念,只有可枚舉的屬性,才會(huì)被for...in循環(huán)遍歷,同時(shí)還規(guī)定原生繼承的屬性都是不可枚舉的,這樣就保證了for...in循環(huán)的可用性。

可枚舉性(enumerable)用來(lái)控制所描述的屬性,是否將被包括在for...in循環(huán)之中。具體來(lái)說(shuō),如果一個(gè)屬性的enumerablefalse,下面三個(gè)操作不會(huì)取到該屬性。

  • for..in循環(huán)
  • Object.keys方法
  • JSON.stringify方法

因此,enumerable可以用來(lái)設(shè)置“秘密”屬性。

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屬性的enumerablefalse,所以一般的遍歷操作都無(wú)法獲取該屬性,使得它有點(diǎn)像“秘密”屬性,但不是真正的私有屬性,還是可以直接獲取它的值。

基本上,JavaScript原生提供的屬性都是不可枚舉的,用戶自定義的屬性都是可枚舉的。

與枚舉性相關(guān)的幾個(gè)操作的區(qū)別的是,for...in循環(huán)包括繼承自原型對(duì)象的屬性,Object.keys方法只返回對(duì)象本身的屬性。如果需要獲取對(duì)象自身的所有屬性,不管是否可枚舉,可以使用Object.getOwnPropertyNames方法,詳見下文。

考慮到JSON.stringify方法會(huì)排除enumerablefalse的值,有時(shí)可以利用這一點(diǎn),為對(duì)象添加注釋信息。

var car = {
  id: 123,
  color: 'red',
  ownerId: 12
};

var owner = {
  id: 12,
  name: 'Jack'
};

Object.defineProperty(car, 'ownerInfo', {
  value: owner,
  enumerable: false
});

car.ownerInfo
// {id: 12, name: "Jack"}

JSON.stringify(car)
//  "{"id": 123,"color": "red","ownerId": 12}"

上面代碼中,owner對(duì)象作為注釋部分,加入car對(duì)象。由于ownerInfo屬性不可枚舉,所以JSON.stringify方法最后輸出car對(duì)象時(shí),會(huì)忽略ownerInfo屬性。

這提示我們,如果你不愿意某些屬性出現(xiàn)在JSON輸出之中,可以把它的enumerable屬性設(shè)為false。

可配置性(configurable)

可配置性(configurable)決定了是否可以修改屬性描述對(duì)象。也就是說(shuō),當(dāng)configurablefalse的時(shí)候,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

上面代碼首先定義對(duì)象o,并且定義o的屬性pconfigurablefalse。然后,逐一改動(dòng)valuewritableenumerableconfigurable,結(jié)果都報(bào)錯(cuò)。

需要注意的是,writable只有在從false改為true會(huì)報(bào)錯(cuò),從true改為false則是允許的。

var o = Object.defineProperty({}, 'p', {
  writable: true,
  configurable: false
});

Object.defineProperty(o,'p', {writable: false})
// 修改成功

至于value,只要writableconfigurable有一個(gè)為true,就允許改動(dòng)。

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})
// 修改成功

另外,configurablefalse時(shí),直接對(duì)該屬性賦值,不報(bào)錯(cuò),但不會(huì)成功。

var o = Object.defineProperty({}, 'p', {
  value: 1,
  configurable: false
});

o.p = 2;
o.p // 1

上面代碼中,o對(duì)象的p屬性是不可配置的,對(duì)它賦值是不會(huì)生效的。

可配置性決定了一個(gè)變量是否可以被刪除(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

上面代碼中的對(duì)象o有兩個(gè)屬性,p1是可配置的,p2是不可配置的。結(jié)果,p2就無(wú)法刪除。

需要注意的是,當(dāng)使用var命令聲明變量時(shí),變量的configurablefalse。

var a1 = 1;

Object.getOwnPropertyDescriptor(this,'a1')
// Object {
//  value: 1,
//  writable: true,
//  enumerable: true,
//  configurable: false
// }

而不使用var命令聲明變量時(shí)(或者使用屬性賦值的方式聲明變量),變量的可配置性為true。

a2 = 1;

Object.getOwnPropertyDescriptor(this,'a2')
// Object {
//  value: 1,
//  writable: true,
//  enumerable: true,
//  configurable: true
// }

// 或者寫成

window.a3 = 1;

Object.getOwnPropertyDescriptor(window, 'a3')
// Object {
//  value: 1,
//  writable: true,
//  enumerable: true,
//  configurable: true
// }

上面代碼中的this.a3 = 1a3 = 1是等價(jià)的寫法。window指的是瀏覽器的頂層對(duì)象。

這種差異意味著,如果一個(gè)變量是使用var命令生成的,就無(wú)法用delete命令刪除。也就是說(shuō),delete只能刪除對(duì)象的屬性。

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對(duì)象的a屬性可寫性設(shè)為false,然后改變這個(gè)屬性的值,就不會(huì)有任何效果。

注意,正常模式下,對(duì)可寫性為false的屬性賦值不會(huì)報(bào)錯(cuò),只會(huì)默默失敗。但是,嚴(yán)格模式下會(huì)報(bào)錯(cuò),即使是對(duì)a屬性重新賦予一個(gè)同樣的值。

關(guān)于可寫性,還有一種特殊情況。就是如果原型對(duì)象的某個(gè)屬性的可寫性為false,那么派生對(duì)象將無(wú)法自定義這個(gè)屬性。

var proto = Object.defineProperty({}, 'foo', {
  value: 'a',
  writable: false
});

var o = Object.create(proto);

o.foo = 'b';
o.foo // 'a'

上面代碼中,對(duì)象protofoo屬性不可寫,結(jié)果proto的派生對(duì)象o,也不可以再自定義這個(gè)屬性了。在嚴(yán)格模式下,這樣做還會(huì)拋出一個(gè)錯(cuò)誤。但是,有一個(gè)規(guī)避方法,就是通過(guò)覆蓋屬性描述對(duì)象,繞過(guò)這個(gè)限制,原因是這種情況下,原型鏈會(huì)被完全忽視。

Object.defineProperty(o, 'foo', {
  value: 'b'
});

o.foo // 'b'

Object.getOwnPropertyNames()

Object.getOwnPropertyNames方法返回直接定義在某個(gè)對(duì)象上面的全部屬性的名稱,而不管該屬性是否可枚舉。

var o = Object.defineProperties({}, {
  p1: { value: 1, enumerable: true },
  p2: { value: 2, enumerable: false }
});

Object.getOwnPropertyNames(o)
// ["p1", "p2"]

一般來(lái)說(shuō),系統(tǒng)原生的屬性(即非用戶自定義的屬性)都是不可枚舉的。

// 比如,數(shù)組實(shí)例自帶length屬性是不可枚舉的
Object.keys([]) // []
Object.getOwnPropertyNames([]) // [ 'length' ]

// Object.prototype對(duì)象的自帶屬性也都是不可枚舉的
Object.keys(Object.prototype) // []
Object.getOwnPropertyNames(Object.prototype)
// ['hasOwnProperty',
//  'valueOf',
//  'constructor',
//  'toLocaleString',
//  'isPrototypeOf',
//  'propertyIsEnumerable',
//  'toString']

上面代碼可以看到,數(shù)組的實(shí)例對(duì)象([])沒(méi)有可枚舉屬性,不可枚舉屬性有l(wèi)ength;Object.prototype對(duì)象也沒(méi)有可枚舉屬性,但是有不少不可枚舉屬性。

Object.prototype.propertyIsEnumerable()

對(duì)象實(shí)例的propertyIsEnumerable方法用來(lái)判斷一個(gè)屬性是否可枚舉。

var o = {};
o.p = 123;

o.propertyIsEnumerable('p') // true
o.propertyIsEnumerable('toString') // false

上面代碼中,用戶自定義的p屬性是可枚舉的,而繼承自原型對(duì)象的toString屬性是不可枚舉的。

存取器(accessor)

除了直接定義以外,屬性還可以用存取器(accessor)定義。其中,存值函數(shù)稱為setter,使用set命令;取值函數(shù)稱為getter,使用get命令。

存取器提供的是虛擬屬性,即該屬性的值不是實(shí)際存在的,而是每次讀取時(shí)計(jì)算生成的。利用這個(gè)功能,可以實(shí)現(xiàn)許多高級(jí)特性,比如每個(gè)屬性禁止賦值。

var o = {
  get p() {
    return 'getter';
  },
  set p(value) {
    console.log('setter: ' + value);
  }
};

上面代碼中,o對(duì)象內(nèi)部的getset命令,分別定義了p屬性的取值函數(shù)和存值函數(shù)。定義了這兩個(gè)函數(shù)之后,對(duì)p屬性取值時(shí),取值函數(shù)會(huì)自動(dòng)調(diào)用;對(duì)p屬性賦值時(shí),存值函數(shù)會(huì)自動(dòng)調(diào)用。

o.p // "getter"
o.p = 123 // "setter: 123"

注意,取值函數(shù)Getter不能接受參數(shù),存值函數(shù)Setter只能接受一個(gè)參數(shù)(即屬性的值)。另外,對(duì)象也不能有與取值函數(shù)同名的屬性。比如,上面的對(duì)象o設(shè)置了取值函數(shù)p以后,就不能再另外定義一個(gè)p屬性。

存取器往往用于,屬性的值需要依賴對(duì)象內(nèi)部數(shù)據(jù)的場(chǎng)合。

var o ={
  $n : 5,
  get next() { return this.$n++ },
  set next(n) {
    if (n >= this.$n) this.$n = n;
    else throw '新的值必須大于當(dāng)前值';
  }
};

o.next // 5

o.next = 10;
o.next // 10

上面代碼中,next屬性的存值函數(shù)和取值函數(shù),都依賴于對(duì)內(nèi)部屬性$n的操作。

存取器也可以通過(guò)Object.defineProperty定義。

var d = new Date();

Object.defineProperty(d, 'month', {
  get: function () {
    return d.getMonth();
  },
  set: function (v) {
    d.setMonth(v);
  }
});

上面代碼為Date的實(shí)例對(duì)象d,定義了一個(gè)可讀寫的month屬性。

存取器也可以使用Object.create方法定義。

var o = Object.create(Object.prototype, {
  foo: {
    get: function () {
      return 'getter';
    },
    set: function (value) {
      console.log('setter: '+value);
    }
  }
});

如果使用上面這種寫法,屬性foo必須定義一個(gè)屬性描述對(duì)象。該對(duì)象的getset屬性,分別是foo的取值函數(shù)和存值函數(shù)。

利用存取器,可以實(shí)現(xiàn)數(shù)據(jù)對(duì)象與DOM對(duì)象的雙向綁定。

Object.defineProperty(user, 'name', {
  get: function () {
    return document.getElementById('foo').value;
  },
  set: function (newValue) {
    document.getElementById('foo').value = newValue;
  },
  configurable: true
});

上面代碼使用存取函數(shù),將DOM對(duì)象foo與數(shù)據(jù)對(duì)象username屬性,實(shí)現(xiàn)了綁定。兩者之中只要有一個(gè)對(duì)象發(fā)生變化,就能在另一個(gè)對(duì)象上實(shí)時(shí)反映出來(lái)。

對(duì)象的拷貝

有時(shí),我們需要將一個(gè)對(duì)象的所有屬性,拷貝到另一個(gè)對(duì)象。ES5沒(méi)有提供這個(gè)方法,必須自己實(shí)現(xiàn)。

var extend = function (to, from) {
  for (var property in from) {
    to[property] = from[property];
  }

  return to;
}

extend({}, {
  a: 1
})
// {a: 1}

上面這個(gè)方法的問(wèn)題在于,如果遇到存取器定義的屬性,會(huì)只拷貝值。

extend({}, {
  get a() { return 1 }
})
// {a: 1}

為了解決這個(gè)問(wèn)題,我們可以通過(guò)Object.defineProperty方法來(lái)拷貝屬性。

var extend = function (to, from) {
  for (var property in from) {
    Object.defineProperty(
      to,
      property,
      Object.getOwnPropertyDescriptor(from, property)
    );
  }

  return to;
}

extend({}, { get a(){ return 1 } })
// { get a(){ return 1 } })

這段代碼還是有問(wèn)題,拷貝某些屬性時(shí)會(huì)失效。

extend(document.body.style, {
  backgroundColor: "red"
});

上面代碼的目的是,設(shè)置document.body.style.backgroundColor屬性為red,但是實(shí)際上網(wǎng)頁(yè)的背景色并不會(huì)變紅。但是,如果用第一種簡(jiǎn)單拷貝的方法,反而能夠達(dá)到目的。這提示我們,可以把兩種方法結(jié)合起來(lái),對(duì)于簡(jiǎn)單屬性,就直接拷貝,對(duì)于那些通過(guò)屬性描述對(duì)象設(shè)置的屬性,則使用Object.defineProperty方法拷貝。

var extend = function (to, from) {
  for (var property in from) {
    var descriptor = Object.getOwnPropertyDescriptor(from, property);

    if (descriptor && ( !descriptor.writable
      || !descriptor.configurable
      || !descriptor.enumerable
      || descriptor.get
      || descriptor.set)) {
      Object.defineProperty(to, property, descriptor);
    } else {
      to[property] = from[property];
    }
  }
}

上面的這段代碼,可以很好地拷貝對(duì)象所有可遍歷(enumerable)的屬性。

控制對(duì)象狀態(tài)

JavaScript提供了三種方法,精確控制一個(gè)對(duì)象的讀寫狀態(tài),防止對(duì)象被改變。最弱一層的保護(hù)是Object.preventExtensions,其次是Object.seal,最強(qiáng)的Object.freeze。

Object.preventExtensions()

Object.preventExtensions方法可以使得一個(gè)對(duì)象無(wú)法再添加新的屬性。

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

如果是在嚴(yán)格模式下,則會(huì)拋出一個(gè)錯(cuò)誤。

(function () {
  'use strict';
  o.p = '1'
}());
// TypeError: Can't add property bar, object is not extensible

不過(guò),對(duì)于使用了preventExtensions方法的對(duì)象,可以用delete命令刪除它的現(xiàn)有屬性。

var o = new Object();
o.p = 1;

Object.preventExtensions(o);

delete o.p;
o.p // undefined

Object.isExtensible()

Object.isExtensible方法用于檢查一個(gè)對(duì)象是否使用了Object.preventExtensions方法。也就是說(shuō),檢查是否可以為一個(gè)對(duì)象添加屬性。

var o = new Object();

Object.isExtensible(o) // true
Object.preventExtensions(o);
Object.isExtensible(o) // false

上面代碼新生成了一個(gè)o對(duì)象,對(duì)該對(duì)象使用Object.isExtensible方法,返回true,表示可以添加新屬性。對(duì)該對(duì)象使用Object.preventExtensions方法以后,再使用Object.isExtensible方法,返回false,表示已經(jīng)不能添加新屬性了。

Object.seal()

Object.seal方法使得一個(gè)對(duì)象既無(wú)法添加新屬性,也無(wú)法刪除舊屬性。

var o = {
  p: 'hello'
};

Object.seal(o);

delete o.p;
o.p // "hello"

o.x = 'world';
o.x // undefined

上面代碼中,一個(gè)對(duì)象執(zhí)行Object.seal方法以后,就無(wú)法添加新屬性和刪除舊屬性了。

Object.seal實(shí)質(zhì)是把屬性描述對(duì)象的configurable屬性設(shè)為false,因此屬性描述對(duì)象不再能改變了。

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

上面代碼中,使用Object.seal方法之后,屬性描述對(duì)象的configurable屬性就變成了false,然后改變enumerable屬性就會(huì)報(bào)錯(cuò)。

可寫性(writable)有點(diǎn)特別。如果writablefalse,使用Object.seal方法以后,將無(wú)法將其變成true;但是,如果writabletrue,依然可以將其變成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,改變這個(gè)設(shè)置將報(bào)錯(cuò);如果原為true,則不會(huì)有問(wèn)題。

至于屬性對(duì)象的value是否可改變,是由writable決定的。

var o = { p: 'a' };
Object.seal(o);
o.p = 'b';
o.p // 'b'

上面代碼中,Object.seal方法對(duì)p屬性的value無(wú)效,是因?yàn)榇藭r(shí)p屬性的writabletrue。

Object.isSealed()

Object.isSealed方法用于檢查一個(gè)對(duì)象是否使用了Object.seal方法。

var o = { p: 'a' };

Object.seal(o);
Object.isSealed(o) // true

這時(shí),Object.isExtensible方法也返回false。

var o = { p: 'a' };

Object.seal(o);
Object.isExtensible(o) // false

Object.freeze()

Object.freeze方法可以使得一個(gè)對(duì)象無(wú)法添加新屬性、無(wú)法刪除舊屬性、也無(wú)法改變屬性的值,使得這個(gè)對(duì)象實(shí)際上變成了常量。

var o = {
  p: 'hello'
};

Object.freeze(o);

o.p = 'world';
o.p // hello

o.t = 'hello';
o.t // undefined

上面代碼中,對(duì)現(xiàn)有屬性重新賦值(o.p = 'world')或者添加一個(gè)新屬性,并不會(huì)報(bào)錯(cuò),只是默默地失敗。但是,如果是在嚴(yán)格模式下,就會(huì)報(bào)錯(cuò)。

var o = {
  p: 'hello'
};

Object.freeze(o);

// 對(duì)現(xiàn)有屬性重新賦值
(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方法用于檢查一個(gè)對(duì)象是否使用了Object.freeze()方法。

var obj = {
  p: 'hello'
};

Object.freeze(obj);
Object.isFrozen(obj) // true

前面說(shuō)過(guò),如果一個(gè)對(duì)象被凍結(jié),再對(duì)它的屬性賦值,在嚴(yán)格模式下會(huì)報(bào)錯(cuò)。Object.isFrozen方法可以防止發(fā)生這樣的錯(cuò)誤。

var obj = {
  p: 'hello'
};

Object.freeze(obj);

if (!Object.isFrozen(obj)) {
  obj.p = 'world';
}

上面代碼中,確認(rèn)obj沒(méi)有被凍結(jié)后,再對(duì)它的屬性賦值,就不會(huì)報(bào)錯(cuò)了。

局限性

需要注意的是,使用上面這些方法鎖定對(duì)象的可寫性,但是依然可以通過(guò)改變?cè)搶?duì)象的原型對(duì)象,來(lái)為它增加屬性。

var obj = new Object();
Object.preventExtensions(o);

var proto = Object.getPrototypeOf(obj);
proto.t = 'hello';
obj.t
// hello

一種解決方案是,把原型也凍結(jié)住。

var obj = Object.seal(
  Object.create(
    Object.freeze({x: 1}),
    {
      y: {
        value: 2,
        writable: true
      }
    }
  )
);

Object.getPrototypeOf(obj).t = "hello";
obj.hello // undefined

另外一個(gè)局限是,如果屬性值是對(duì)象,上面這些方法只能凍結(jié)屬性指向的對(duì)象,而不能凍結(jié)對(duì)象本身的內(nèi)容。

var obj = {
  foo: 1,
  bar: ['a', 'b']
};
Object.freeze(obj);

obj.bar.push('c');
obj.bar // ["a", "b", "c"]

上面代碼中,obj.bar屬性指向一個(gè)數(shù)組,obj對(duì)象被凍結(jié)以后,這個(gè)指向無(wú)法改變,即無(wú)法指向其他值,但是所指向的數(shù)組是可以改變的。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)