被你熟视无睹的「Object的属性」——数据属性、访问器属性

908 阅读9分钟

理解对象

关键字: 无序集合、名称映射到值、名/值对、散列表...
Object 是 ECMAScript 中最常用的类型之一。虽然 Object 的实例没有多少功能,但很适合存储和在应用程序间交换数据。

我们经常使用objX.xxx来访问、修改 Object 的属性,这种语法被称为点语法
但是 Object 的属性只是如此吗?

显式创建

先来创建一个对象,看一下我们司空见惯的属性有哪些特点?

构造函数

let person = new Person();
person.name = 'nameStr';
person[0] = zero;

对象字面量
对象字面量是对象定义的简写形式。

let person = {
    name: 'nameStr',
    0: 'zero'
}

在上述例子中,左大括号( { )表示对象字面量的开始。因为它出现在一个表达式上下文( expression context )中。

扩展:在 ECMAScript 中,**表达式上下文**( expression context )指期待返回值的上下文,此处赋值操作符后面期待一个返回值,遂左大括号是一个表达式的开始。同样是左大括号,如果出现在**语句上下文**( statement context )中,比如 if 语句后面,则表示一个语句块的开始。

上述例子中的写法等效于 new Object() ,但在使用对象字面量表示法定义对象时,并不会实际调用 Object 构造函数。
无论通过何种方式创建,数值属性会自动转换成字符串。

访问:点语法 & 中括号
属性一般通过点语法来存取,这是面向对象语言的惯例;

console.log(person.name); // 'nameStr'

但也仍可以通过中括号来存取。

console.log(person['name']); // 'nameStr'

中括号的主要优势就是可以通过变量访问属性:

let propertyName = 'name'
console.log(person[propertyName]) // 'nameStr'

中括号还有个用途就是在属性名中包含可能会导致语法错误的字符,或者包含关键字/保留字时使用

person['first name'] = 'firstNameStr' // 包含空格的字符作属性名,无法使用点语法

person.1 = 'one' // 报错'Uncaught SyntaxError: Unexpected number'
person[1] = 'one' // 虽然看起来很像数组的存取方式,但此处的1仍会被转换为字符串作为属性名索引

通常,点语法是首选的属性存取方式,除非访问属性时必须使用变量。

属性的类型

ECMA使用一些内部特性描述属性的特征。用以定义 JavaScript 实现引擎的规范。因此,开发者不能在 Javascript 中直接访问这些特性。为了标识其为内部特性,规范使用两个中括号把特性的名称括起来,如 [ [Value] ] 。

名/值对是我们对对象中的属性最直观的认知,而实际上属性的值就是存在该属性的 [ [Value] ] 中。

let person = { name: 'nameStr' }
// 此处,我们设置的实际上是对象 person 中 'name' 属性的 [ [Value] ] 特性,
// 它被设置为'nameStr'

即使你第一次听到这个概念也没有关系,往下看就会恍然大悟。

数据属性

理解数据属性
数据属性包含一个保存数据值的位置。值从这个位置存取,共有 4 个特性描述它们的行为。

  • [ [Configurable] ] : 表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。对于直接定义在对象上的属性,该特性默认为 true 。
  • [ [Enumerable] ] : 表示属性是否可以通过 for-in 循环返回。 对于直接定义在对象上的属性,该特性默认为 true 。
  • [ [Writable] ] : 表示属性的值是否可被修改。对于直接定义在对象上的属性,该特性默认为 true 。
  • [ [Value] ] : 包含属性实际的值。这就是我们真正读取和写入属性值的位置,默认为 undefined 。
let person = { name: 'nameStr' }

如上例中,将属性显示添加到对象之后, [ [Configurable] ] 、 [ [Enumerable] ] 、 [ [Writable] ] 均被设置为 true , 而 [ [Value] ] 则被设置为指定值。
(注:仅限于直接添加属性时,这三个特性值会默认设置为true,存在其他情况

在实际开发中我们并不经常直观的接触上述特性,但是除了是理解 JavaScript 对象的必要条件外,仍然有一些实际用途。比如在某些严谨的商业可视化项目中,从后端获取的数据经过转化后,要设置所有属性为只读,防止意外的、不易发觉的数据展示错误。

这种场景下,我们需要修改属性的默认特性,就要使用 Object.defineProperty() 方法。看到这个熟悉的方法,经常刷面试题的小伙伴们,有没有觉得 DNA 动了。 /doge

修改数据属性
Object.defineProperty(obj, prop, descriptor) 接收 3 个参数:

  • obj: Object 要定义属性的对象。
  • prop: String | Symbol 要定义或修改的属性的名称或 Symbol 。
  • descriptor: Object 要定义或修改的属性描述符。
    • 可包含 configurableenumerablewritablevalue ,与上述数据属性一一对应(转小写)。

Object.defineProperty() 参数三的取值

writable 可写性

let person = {}
Object.defineProperty(person, "name", {
	writable: false,
	value: 'nameStr'
})
console.log(person.name) // 'nameStr'
person.name = 'nameStrModified'
console.log(person.name) // 'nameStr'

上例创建了一个属性,名为 name , 并利用 writable: false 赋了一个只读值 'newNameStr' 。这正是 特性 writable 的所描述行为

enumerable 可枚举性

let person = { name: 'nameStr', age: 'ageNum', gender: 'genderStr' }
Object.defineProperty(person, "name", {
	enumerable: false,
	value: 'nameStr'
})
for (let key in person) { console.log(key) } // age gender
console.log(person.name) // 'nameStr'

上例创建了一个属性,名为 name , 并利用 enumerable: false 使得无法被for-in遍历到,仿佛隐身了一样,但是仍可以通过点访问符访问到。这正是 特性 enumerable 的所描述行为

configurable 可配置性
前两个特性很容易从字面意思上理解其用途,但是 configurable 相较之下在用法上有个注意点,先看例子:

let person = {}
// 首次设置configurable: false
Object.defineProperty(person, "name", {
	configurable: false,
	value: 'nameStr'
})
console.log(person.name) // 'nameStr'
delete person.name
console.log(person.name) // 'nameStr'

// 第二次设置configurable: true 
// 直接抛出错误 设置其他特性也一样报错(除了writable) 因为此时对象已不可配置了
Object.defineProperty(person, "name", {
	configurable: true,
	value: 'nameStr'
})

上例创建了一个属性,名为 name , 并设置 configurable: false ,这意味着这个属性无法从对象上删除。此外,更重要的一点是,一个属性被定义为不可配置之后,就不能再变回可配置的了。再次调用 Object.defineProperty() 并修改任何 writable 以外的属性都会导致错误。 这正是 特性 configurable 的所描述行为

value 实际值
就是数据的值,赋值就是赋 value ,没什么好说的。

总的来说,configurableenumerablewritable 的值都默认为 false, 在使用时configurable 的使用可能需要额外注意。 Object.defineProperty() 提供的这些强大的设置并不总被用得到,但要理解 JavaScript 对象重要的一部分。

访问器属性

理解访问器属性
访问器属性不包含数据值。相反,它们包含一个获取(getter)函数和一个设置(setter)函数,不 过这两个函数不是必需的。
在读取访问器属性时,会调用获取函数,这个函数的责任就是返回一个有效 的值。
在写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改。

访问器属性有 4 个特性描述它们的行为。

  • [ [Configurable] ] : 表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。对于直接定义在对象上的属性,该特性默认为 true 。
  • [ [Enumerable] ] : 表示属性是否可以通过 for-in 循环返回。 对于直接定义在对象上的属性,该特性默认为 true 。
  • [ [Get] ] : 获取函数,在读取属性时调用。对于直接定义在对象上的属性,该特性默认为 undefined 。
  • [ [Set] ] : 设置函数,在写入属性时调用。对于直接定义在对象上的属性,该特性默认为 undefined 。

必须使用 Object.defineProperty() 定义访问器属性:

let ES_edition = {
	year_: 2015,
	edition: 6
}
Object.defineProperty(ES_edition, "year", {
      get() {
        return this.year_;
      },
      set(newValue) {
				this.year_ = newValue;
				this.edition = newValue >= 2015
					? (newValue - 2015 + 6)
					: 5
			} 
});

ES_edition.year = 2020
console.log(ES_edition) // year_: 2020, edition: 11  ⚠️ 没有year
console.log(ES_edition.year) // 2020  ⚠️ 访问器函数返回了year_的值

在上例中,对象 ES_edition 有两个默认属性: year_和 edition。 随后又定义了一个访问器属性 year ,其获取函数 get year 负责返回它的值, 其设置函数 set year 在其值被修改时进行了指定的操作。
这是访问器属性的典型使用场景,即设置一个属性值驱动一些其他变化,典型的 状态模式 。 这也是Vue2中实现响应式的基础,理解了访问器属性,才能更容易学习理解Vue2的 EventBus响应式 等。

只定义获取函数意味着属性是只读的,尝试修改属性会被忽略。在严格模式下,尝试写入只定义了获取函数的属性会抛出错误。 只有一个设置函数的属性是不能读取的,非严格模式下读取会返回 undefined,严格模式下会抛出错误。

小结

  • ECMA使用一些内部特性描述属性的特征。开发者不能直接访问。规范使用两个中括号把特性的名称括起来,如 [ [Value] ]
  • 属性分为: 数据属性访问器属性
  • 使用 Object.defineProperty() 定义属性的特征

定义多个属性

使用 Object.defineProperties() 方法可以通过多个描述符一次性定义多个属性。

Object.defineProperties(obj, props) 接收 3 个参数:

  • obj: Object 在其上定义或修改属性的对象。
  • props: Object 定义其可枚举属性或修改的属性描述符的对象。

看一下示例就可以很轻易的学会其使用:

let ES_edition = {
	year_: 2015,
	edition: 6
}
Object.defineProperties(ES_edition, {
	year_: {
		value: 2015
	},
	edition: {
		value: 6
	},
	year: {
		get() {
			return this.year_;
		},
		set(newValue) {
			this.year_ = newValue;
			this.edition = newValue >= 2015
				? (newValue - 2015 + 6)
				: 5
		} 
	}
});

注意,上例的 configurableenumerablewritable 特性值未直接指定,遂都是 false

直接在一个对象上指定属性时,其默认值的表现会如 上文所说的那样 ,如:

let person = {}
person.name = 'nameStr'
// { writable: true, enumerable: true, configurable: true, value: 'nameStr' }

而调用 Object.defineProperty() 或者 Object.defineProperties() 定义时,未直接指定的特性值会是 false , 如:

let person = {}
Object.defineProperties(person, {
	name: {
		value: 'nameStr'
	}
})
// { writable: false, enumerable: false, configurable: false, value: 'nameStr' }

读取属性的特性

使用 Object.getOwnPropertyDescriptor() 方法可以取得指定属性的属性描述符。

Object.getOwnPropertyDescriptor(obj, prop) 接收 2 个参数:

  • obj: Object 需要查找的目标对象
  • prop: String 目标对象内属性名称
let person = { name: 'nameStr' }
let descriptor = Object.getOwnPropertyDescriptor(person, 'name') 
console.log(descriptor.value) // 'nameStr'
console.log(descriptor.writable) // true

上文中 对比特性默认值的不同表现 中的的输出结果就是通过这个方法打印出来的。

使用 Object.getOwnPropertyDescriptors() 方法(ES7)可以打印对象所有的属性的属性描述符,本质是在每个自有属性上调用 Object.getOwnPropertyDescriptor() 并在一个新对象中返回它们。

Object.getOwnPropertyDescriptors(obj) 接收 1 个参数:

  • obj: Object 需要查找的目标对象
let person = { name: 'nameStr', age: 18 }
Object.getOwnPropertyDescriptors(person)
/*
	{
		"name": {
			"value": "nameStr",
			"writable": true,
			"enumerable": true,
			"configurable": true
		},
		"age": {
			"value": 18,
			"writable": true,
			"enumerable": true,
			"configurable": true
		}
	}
*/