# JavaScript 属性描述符

# 前言

  此文总结了属性描述符的作用和特性,以及限制对象操作的部分方法。

# Object.defineProperty

  ;Object.defineProperty (opens new window) 用于指定对象属性的描述符。

  函数的第三个参数descriptor为属性的描述符,包括数据描述符和存取描述符两种。

var object = {}

// 数据描述符
Object.defineProperty(object, 'foo', {
  configurable: true,
  enumerable: true,
  writable: true,
  value: 1,
})

// 存取描述符
Object.defineProperty(object, 'bar', {
  configurable: true,
  enumerable: true,
  get() {},
  set() {},
})

  注意属性描述符固定包括configurableenumerablewritablevalue四个键,存取描述符固定包括configurableenumerablegetset四个键。

描述符中公共键为configurableenumerable,数据描述符的writablevalue键成对,存取描述符的getset键成对

# 默认键值

  属性描述符的键是有默认值的,但是由于对象属性的指定方式不同,存在差异。

Object.getOwnPropertyDescriptors (opens new window) 用于获取对象自身所有属性的描述符

  字面量方式。

var object = {
  foo: 1,
  get bar() {},
}

Object.getOwnPropertyDescriptors(object)
// {
//   bar: {
//     configurable: true,
//     enumerable: true,
//     get: f bar(),
//     set: undefined,
//   },
//   foo: {
//     configurable: true,
//     enumerable: true,
//     value: 1,
//     writable: true,
//   },
// }

  ;defineProperty方式。

var object = {}

Object.defineProperty(object, 'foo', {
  value: 1,
})
Object.defineProperty(object, 'bar', {
  get() {},
})
Object.defineProperty(object, 'baz', {})

Object.getOwnPropertyDescriptors(object)
// {
//   bar: {
//     configurable: false,
//     enumerable: false,
//     get: f bar(),
//     set: undefined,
//   },
//   baz: {
//     configurable: false,
//     enumerable: false,
//     value: undefined,
//     writable: false,
//   },
//   foo: {
//     configurable: false,
//     enumerable: false,
//     value: 1,
//     writable: false,
//   },
// }

  比较发现,字面量方式指定的属性的描述符默认键值都为true

  而defineProperty方式则相对为false,很好理解,defineProperty旨在细化地描述属性,即指定什么就是什么,未指定的当然为false

  另外defineProperty指定为空描述符时,默认为数据描述符形式。存取描述符中的getset指定了才会有,不指定默认为undefined

# configurable

  是否可以删除此属性。

  注意非严格模式下删除静默失败,严格模式将抛出错误。

'use strict'

var object = {}

Object.defineProperty(object, 'foo', {
  value: 1,
  configurable: false,
})

delete object.foo // Uncaught TypeError: Cannot delete property 'foo' of #<Object>

object // {foo: 1}

  是否可以修改描述符的键值,例如修改enumerable

var object = {}

Object.defineProperty(object, 'foo', {
  configurable: false,
  enumerable: true,
  value: 1,
})

Object.defineProperty(object, 'foo', {
  enumerable: false, // Uncaught TypeError: Cannot redefine property: foo at Function.defineProperty (<anonymous>)
})

  注意有两个特例,valuewritable

  ;value键的值可以修改。

var object = {}

Object.defineProperty(object, 'foo', {
  configurable: false,
  writable: true,
  value: 1,
})

Object.defineProperty(object, 'foo', {
  value: 12,
})

object // {foo: 12}

  ;writable键的值只能由true修改为false

var object = {}

Object.defineProperty(object, 'foo', {
  configurable: false,
  writable: true,
  value: 1,
})

Object.defineProperty(object, 'foo', {
  writable: false,
})

object // {foo: 1}

  但是不能由false修改为true

var object = {}

Object.defineProperty(object, 'foo', {
  configurable: false,
  writable: false,
  value: 1,
})

Object.defineProperty(object, 'foo', {
  writable: true, // Uncaught TypeError: Cannot redefine property: foo at Function.defineProperty (<anonymous>)
})

  所以configurable用于表示属性是否可删除,描述符的键是否可修改。

  当configurablefalse时,属性不能被删除。除了writablevalue之外的键(包括configurableenumerablesetget)不能修改,注意writable也只能修改为false

# enumerable

  是否可以枚举此属性。

var object = { foo: 1 }

Object.defineProperty(object, 'bar', {
  enumerable: false,
  value: 2,
})

for (var prop in object) {
  console.log(prop) // foo
}

Object.keys(object) // ['foo']

JSON.stringify(object) // {"foo":1}

Object.assign({}, object) // {foo: 1}

object // {foo: 1, bar: 2}

Object.prototype.propertyIsEnumerable (opens new window) 可用于判断对象的属性值是否可枚举

  几种遍历对象的方式。

  • for...in:遍历对象自身和继承的可枚举属性,不包括Symbol属性
  • Objects.keys / Objects.values / Objects.entries:返回对象自身的键或值组成的数组,也是可枚举属性,不包括Symbol
  • Object.getOwnPropertyNames:返回对象自身的属性,不包括Symbol属性
  • Object.getOwnPropertySymbols:返回对象自身的Symbol属性
  • Reflect.ownKeys:返回对象自身的所有属性

# writable

  是否可以修改此属性。

  注意非严格模式下修改静默失败,严格模式将抛出错误。

'use strict'

var object = {}

Object.defineProperty(object, 'foo', {
  writable: false,
  value: 1,
})

object.foo = 2 // Uncaught TypeError: Cannot assign to read only property 'foo' of object '#<Object>'

object // {foo: 1}

  ;configurabletrue时,可以先删除,再添加属性,达到修改属性的目的。

'use strict'

var object = {}

Object.defineProperty(object, 'foo', {
  configurable: true,
  writable: false,
  value: 1,
})

delete object.foo
object.foo = 2

object // {foo: 2}

# value

  指定属性值,默认为undefined

var object = {}

Object.defineProperty(object, 'foo', {})

object // {foo: undefined}

# get

  访问器getter,在访问对象属性时触发。

var object = {}

Object.defineProperty(object, 'foo', {
  get() {
    console.log('get')
  },
})

object.foo // get

  特别注意get函数内this会静默绑定。

var object = {}

Object.defineProperty(object, 'foo', {
  get() {
    console.log(this === object) // true
  },
})

object.foo

  观察发现,点运算符(.)左边的对象是谁,this就指向谁。

var object = {}

Object.defineProperty(object, 'foo', {
  get() {
    console.log(this === o) // true
  },
})

var o = Object.create(object)

o.foo

  换句话说this指向触发读取操作时的对象。

# set

  访问器setter,在修改对象属性值时触发。

var object = {}

Object.defineProperty(object, 'foo', {
  set() {
    console.log('set')
  },
})

object.foo = 1 // set

  类似的,set函数内this指向触发修改操作时的对象。

# 小结

  • 描述符分为数据和存取两种类型,数据类型包括configurableenumerablewritablevalue,存取类型包括configurableenumerablegetset
  • 字面量指定的属性描述符的默认键值为truedefinePropertyfalse
  • 若属性的configurable键值为false,不能删除此属性,也不能修改除了writablevalue之外的键,且writable也只能修改为false
  • 若属性的enumerablefalse,不可枚举此属性,或者说会被忽略。例如for...inObject.keysJSON.stringifyObject.assign
  • 若属性的writablefalse,不可修改此属性
  • 访问器属性有getset,在访问对象属性时触发get,在修改对象属性值时触发set,两者内部都会静默绑定this,指向触发操作时的对象

# 对象

  除了属性描述符可以限制对象的操作之外,Object上也有方法限制对象的操作。

# Object.preventExtensions

  ;Object.preventExtensions (opens new window) 表示让对象不可拓展。

  不能添加新的属性。

  注意非严格模式下添加属性静默失败,严格模式将抛出错误。

'use strict'

var object = {}

Object.preventExtensions(object)

object.foo = 1 // Uncaught TypeError: Cannot add property foo, object is not extensible

object // {}

  另外对象的内部槽[[prototype]]也不可变,即不能修改原型的指向。

  ;__proto__修改将抛出错误。

var object = {}

Object.preventExtensions(object)

object.__proto__ = {} // Uncaught TypeError: #<Object> is not extensible at set __proto__ [as __proto__] (<anonymous>)

  ;setPrototypeOf也会抛出错误。

var object = {}

Object.preventExtensions(object)

Object.setPrototypeOf(object, {}) // Uncaught TypeError: #<Object> is not extensible at Function.setPrototypeOf (<anonymous>)

  所以preventExtensions作用的对象,不能添加新的属性,也不能修改原型的指向。

Object.isExtensible (opens new window) 可用于判断对象是否可扩展

# Object.seal

  ;Object.seal (opens new window) 表示封闭对象。

  ;seal封闭对象时,也会将对象变为不可拓展。所以封闭的对象也有类似preventExtensions的性质,例如不能添加属性,不可修改原型的指向。

var object = { foo: 1 }

Object.seal(object)

Object.isExtensible(object) // false

  ;seal还会将对象的所有属性的描述符中的configurable置为false

  所以seal作用的对象上的所有属性,都不能删除,也不能修改描述符中除了writablevalue之外的键,且writable只能修改为false

var object = {
  foo: 1,
  get bar() {},
}

Object.defineProperty(object, 'baz', {
  configurable: true,
  value: 2,
})

Object.seal(object)

Object.getOwnPropertyDescriptors(object)
// {
//   bar: {
//     configurable: false,
//     enumerable: true,
//     get: f bar(),
//     set: undefined,
//   },
//   baz: {
//     configurable: false,
//     enumerable: false,
//     value: 2,
//     writable: false,
//   },
//   foo: {
//     configurable: false,
//     enumerable: true,
//     value: 1,
//     writable: true,
//   },
// }

Object.isSealed (opens new window) 用于判断对象是否封闭

# Object.freeze

  ;Object.freeze (opens new window) 表示冻结对象。

  ;freeze冻结对象时,会将对象封闭并变为不可拓展。

var object = { foo: 1 }

Object.freeze(object)

Object.isSealed(object) // true

Object.isExtensible(object) // false

  非严格模式修改属性静默失败,严格模式将抛出错误。

'use strict'

var object = { foo: 1 }

Object.freeze(object)

object.foo = 2 // ncaught TypeError: Cannot assign to read only property 'foo' of object '#<Object>'

object // {foo: 1}

  ;freeze还会将对象的所有属性的数据描述符中的writable置为false

var object = { foo: 1 }

Object.defineProperty(object, 'bar', {
  configurable: true,
  writable: true,
  value: 2
})

Object.freeze(object)

Object.getOwnPropertyDescriptors(object)
// {
//   bar: {
//     configurable: false,
//     enumerable: false,
//     value: 2,
//     writable: false,
//   },
//   foo: {
//     configurable: false,
//     enumerable: true,
//     value: 1,
//     writable: false,
//   },
// }

  所以freeze冻结的对象几乎不能做任何操作,但是注意freeze只是浅冻结。

var object = {
  foo: 1,
  bar: {
    baz: 2,
  },
}

Object.freeze(object)

object.bar.baz = 3

object
// {
//   bar: { baz: 3 },
//   foo: 1,
// }

Object.isFrozen (opens new window) 用于判断对象是否冻结

# 深度冻结

  兼容IE9

Object.deepFreeze = function (object) {
  var prop, value, propNames = Object.getOwnPropertyNames(object)

  for (var i = 0; i < propNames.length; i++) {
    prop = propNames[i]
    value = object[prop]

    if (typeof value === 'object' && value !== null) {
      Object.deepFreeze(value)
    }
  }

  return Object.freeze(object)
}

注意Object.keys只能获取到对象自身的不可枚举属性,而getOwnPropertyNames可以获取对象自身的所有非Symbol属性

# 解冻

  冻结实际上是不可逆的,无法解冻原对象。

  但是可以通过深度克隆并覆盖原对象,达到解冻的目的。

var object = {
  foo: 1,
  bar: {},
}

Object.deepFreeze(object)

// or JSON.parse(JSON.stringify(object))
object = deepClone(object)

object.bar.baz = 2

object
// {
//   foo: 1,
//   bar: { baz: 2 },
// }

  本质上仅仅是将变量的内存空间指向了另一个对象,原对象还是冻结的。另外若变量objectconst的方式声明,此方式也将失效。

注意 JSON.stringify 深拷贝时,可能会丢失掉部分属性

# 数组

  数组也是一类特殊的对象,数组下标相当于是对象的键。

var array = [1]

Object.defineProperty(array, 1, {
  get() {},
})

Object.getOwnPropertyDescriptors(array)
// {
//   0: {
//     configurable: true,
//     enumerable: true,
//     value: 1,
//     writable: true,
//   },
//   1: {
//     configurable: false,
//     enumerable: false,
//     get: f get(),
//     set: undefined,
//   },
//   length: {
//     configurable: false,
//     enumerable: false,
//     value: 2,
//     writable: true,
//   },
// }

# preventExtensions

  ;preventExtensions作用的数组不能添加元素,也不能修改原型的指向。

var array = [1, 2]

Object.preventExtensions(array)

array.push(3) // Uncaught TypeError: Cannot add property 2, object is not extensible at Array.push (<anonymous>)

# seal

  ;seal与对象类似,除了有preventExtensions特性外,也不能删除元素等。

var array = [1]

Object.seal(array)

array.pop() // Uncaught TypeError: Cannot delete property '0' of [object Array] at Array.pop (<anonymous>)

# freeze

  ;freeze也于对象类似,除了有preventExtensionsseal特性外,也不能修改元素。

  非严格模式修改元素静默失败,严格模式将抛出错误。

'use strict'

var array = [1]

Object.freeze(array)

array[0] = 2 // index.html:23 Uncaught TypeError: Cannot assign to read only property '0' of object '[object Array]'

array // [1]

# 小结

  • preventExtensions会将对象变为不可拓展,即不能添加属性,也不能修改原型的指向
  • seal封闭对象,不仅会将对象变为不可拓展,还会将所有属性的描述符中的configurable置为false。所以除了有preventExtensions的特性外,也不能删除属性,不能修改除了writablevalue之外的键,writable只能修改为false
  • freeze冻结对象,不仅会将对象变为不可拓展和封闭,还会将所有属性的数据描述符的writable置为false。所以除了有preventExtensionsseal特性外,不能修改属性和任何描述符的键值
  • 数组是特殊的对象,preventExtensionssealfreeze作用数组时,与作用对象的特性高度相似

preventExtensionssealfreeze均是不可逆的,并作用于对象或数组

  特性表。

特性 preventExtensions seal freeze
isExtensible false false false
isSealed false true true
isFrozen false false true
添加属性
修改原型的指向
configurable置为false
是否可删除属性
修改描述符的键值 可修改 不能修改除了writablevalue之外的键,writable只能修改为false 都不能修改
writable置为false

  关系图。

# 🎉 写在最后

🍻伙伴们,如果你已经看到了这里,觉得这篇文章有帮助到你的话不妨点赞👍或 Star (opens new window) ✨支持一下哦!

手动码字,如有错误,欢迎在评论区指正💬~

你的支持就是我更新的最大动力💪~

GitHub (opens new window) / Gitee (opens new window)GitHub Pages (opens new window)掘金 (opens new window)CSDN (opens new window) 同步更新,欢迎关注😉~

最后更新时间: 11/11/2022, 10:49:14 PM