理解对象
关键字: 无序集合、名称映射到值、名/值对、散列表...
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要定义或修改的属性描述符。- 可包含
configurable、enumerable、writable、value,与上述数据属性一一对应(转小写)。
- 可包含
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 ,没什么好说的。
总的来说,configurable、 enumerable 和 writable 的值都默认为 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
}
}
});
注意,上例的 configurable、enumerable 和 writable 特性值未直接指定,遂都是 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
}
}
*/