JavaScript 的设计是一个简单的基于对象的范式。一个对象就是一系列属性的集合,一个属性包含一个名和一个值。一个属性的值可以是函数,这种情况下属性也被称为方法。除了宿主环境内置的对象之外,也可以定义自己的对象,从而构建复杂的数据结构。
属性的定义和访问
对象有很多属性,可以理解为一个附加到对象上的变量。基本没什么区别,仅仅是属性属于某个对象,定义了对象的特征。对象的名字 (可以是普通的变量) 和属性的名字都是大小写敏感的,可以在定义一个属性的时候就给它赋值,未赋值的属性的值为 undefined(而不是 null)。
对象的属性可以通过点符号来访问一个对象的属性,也可以通过方括号访问或者设置,有时也被叫作关联数组,因为每个属性都有一个用于访问它的字符串值。
let o = { name: 'John' }
o.name = 'John'
console.log(o.name) // 'John'
o['name'] = 'Jane'
console.log(o['name']) // 'Jane'
用对象字面量语法来定义一个对象时,会自动初始化一组有限的属性。对象的属性可以是任何有效的 JavaScript 字符串,或者可以被转换为字符串的任何类型,包括空字符串。但是它们容易造成属性名的冲突,ES2015 引入了表示独一无二的值 Symbol,也可以用作属性名。
然而,一个属性的名称如果不是一个有效的 JavaScript 标识符,就只能通过方括号标记访问。这个标记法在属性名称是动态判定(属性名只有到运行时才能判定)时非常有用。
let rand = Math.random()
let symbol = Symbol(rand)
let o = {
[symbol]() {
return 'John'
}
}
o[rand] = 'John'
对象属性有两种类型,每个属性都有对应的特性(attribute):数据属性和访问器属性。可以通过 Object.defineProperty() 设置它们,或者通过 Object.getOwnPropertyDescriptor() 读取它们。
let o = {}
o.name = 'John'
数据属性
数据属性将键与值相关联。它可以通过以下属性来描述:
value:通过属性访问器获取值。可以是任意的 JavaScript 值。
console.log(Object.getOwnPropertyDescriptor(o, 'name').value) // 'John'
Object.defineProperty(o, 'name', { value: 246 })
console.log(o.name) // 246
writable:一个布尔值,表示是否可以通过赋值来改变属性。
Object.defineProperty(o, 'name', {
value: 37,
writable: false
})
o.name = 38
console.log(o.name) // 37
注意,正常模式下,对 writable 为 false 的属性赋值不会报错,只会默默失败。但是,严格模式下会报错,即使对 name 属性重新赋予一个同样的值。
'use strict'
let o = {}
Object.defineProperty(o, 'name', {
value: 37,
writable: false
})
console.log(o.name)
// Uncaught TypeError: Cannot assign to read only property 'name' of object
如果原型对象的某个属性的 writable 为 false,那么子对象将无法自定义这个属性。
const proto = Object.defineProperty({}, 'name', {
value: 'a',
writable: false
})
const o = Object.create(proto)
o.name = 'b'
console.log(o.name) // a
但是,有一个规避方法,就是通过覆盖属性描述对象,绕过这个限制。原因是这种情况下,原型链会被完全忽视。
const proto = Object.defineProperty({}, 'name', {
value: 'a',
writable: false
})
const o = Object.create(proto)
Object.defineProperty(o, 'name', {
value: 'b'
})
console.log(o.name) // a
enumerable:一个布尔值,表示是否可以通过for...in循环来枚举属性。
如果一个属性的 enumerable 为 false,下面三个操作不会取到该属性。
for..in循环Object.keys方法JSON.stringify方法
因此,enumerable 可以用来设置“秘密”属性。
Object.defineProperty(o, 'name', {
value: 123,
enumerable: false
})
for (var k in o) {
console.log(k) // undefined
}
另外,JSON.stringify 方法会排除 enumerable 为 false 的属性,有时可以利用这一点。如果对象的 JSON 格式输出要排除某些属性,就可以把这些属性的 enumerable 设为 false。
configurable:一个布尔值,表示该属性是否可以删除,是否可以更改为访问器属性,并可以更改其特性。
const o = Object.defineProperty({}, 'name', {
value: 1,
configurable: false
})
Object.defineProperty(o, 'name', { value: 2 })
// TypeError: Cannot redefine property: name
Object.defineProperty(o, 'name', { writable: true })
// TypeError: Cannot redefine property: name
Object.defineProperty(o, 'name', { enumerable: true })
// TypeError: Cannot redefine property: name
Object.defineProperty(o, 'name', { configurable: true })
// TypeError: Cannot redefine property: name
注意,writable 属性只有在 false 改为 true 时会报错,true 改为 false 是允许的。
const o = Object.defineProperty({}, 'name', {
writable: true,
configurable: false
})
Object.defineProperty(o, 'name', {writable: false})
value 属性的情况比较特殊,只要 writable 和 configurable 有一个为 true,就允许改动 value。
const o1 = Object.defineProperty({}, 'name', {
value: 1,
writable: true,
configurable: false
})
Object.defineProperty(o1, 'name', {value: 2})
const o2 = Object.defineProperty({}, 'name', {
value: 1,
writable: false,
configurable: true
})
Object.defineProperty(o2, 'name', {value: 2})
另外,writable 为 false 时,直接对目标属性赋值,不报错,但不会成功。
const o = Object.defineProperty({}, 'name', {
value: 1,
writable: false,
configurable: false
})
o.name = 2;
console.log(o.name) // 1
可配置性决定了目标属性是否可以被删除(delete)。
const o = Object.defineProperties({}, {
name1: { value: 1, configurable: true },
name2: { value: 2, configurable: false }
})
delete o.name1 // true
delete o.name2 // false
o.name1 // undefined
o.name2 // 2
目前,有四个操作会忽略 enumerable 为 false 的属性。
for...in循环:只遍历对象自身的和继承的可枚举的属性。Object.keys():返回对象自身的所有可枚举的属性的键名。JSON.stringify():只串行化对象自身的可枚举的属性。Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。
访问器属性
访问器属性将键与两个访问器函数之一关联以检索或存储值,存值函数称为 setter,使用属性描述对象的 set 属性;取值函数称为 getter,使用属性描述对象的 get 属性。访问器属性具有以下属性:
get:该函数使用一个空的参数列表,以便有权对值执行访问时,获取属性值。set:使用包含分配值的参数调用的函数。每当尝试更改指定属性时执行。
const o = Object.defineProperty({}, 'name', {
get: function () {
return 'getter'
},
set: function (value) {
console.log('setter: ' + value)
}
})
o.p // 'getter'
o.p = 123 // setter: 123
JavaScript 还提供了存取器的另一种写法。
const o = {
get p() {
return 'getter'
},
set p(value) {
console.log('setter: ' + value)
}
}
两种写法属性的读取和赋值行为是一样的,但是有一些细微的区别。第一种写法,属性的 configurable和 enumerable 都为 false,从而导致属性是不可遍历的;第二种写法,属性的 configurable 和 enumerable 都为 true,因此属性是可遍历的。
属性的遍历
在 ECMAScript 5 之前,没有原生的方法枚举一个对象的所有属性。然而,可以通过以下函数完成:
function listProperties(o) {
let result = []
for (let p = o; p !== null; p = Object.getPrototypeOf(p)) {
result = result.concat(Object.getOwnPropertyNames(p))
}
return result
}
const o = { a: 1, [Symbol('list')]: 2 }
console.log(listProperties(o))
// ['a', 'constructor', '__defineGetter__', '__defineSetter__', 'hasOwnProperty',
// '__lookupGetter__', '__lookupSetter__', 'isPrototypeOf', 'propertyIsEnumerable',
// 'toString', 'valueOf', '__proto__', 'toLocaleString']
这在展示“隐藏”(在原型中的不能通过对象访问的属性,因为另一个同名的属性存在于原型链的早期)的属性时很有用。如果只想列出可访问的属性,那么只需要去除数组中的重复元素即可。从 ECMAScript 5 开始,有五种方法可以遍历对象的属性:
for...in循环:该方法依次访问一个对象及其原型链中所有可枚举的属性(不含Symbol属性)。Object.keys(o):该方法返回对象自身包含(不包括原型中)的所有可枚举属性(不含Symbol属性)的名称的数组。Object.getOwnPropertyNames(o): 该方法返回对象自身包含(不包括原型中)的所有属性 (不含Symbol属性,但是包括不可枚举属性)的名称的数组。Object.getOwnPropertySymbols(o):该方法返回一个数组包含对象自身的所有Symbol属性的名称。Reflect.ownKeys(o):该方法返回一个数组,包含对象自身的(不包括原型中)所有键名,不管键名是Symbol或字符串,也不管是否可枚举。
function getProps(o) {
let result = []
for (let p in o) {
result.push(p)
}
return result
}
console.log(getProps(o)) // ['a']
console.log(Object.keys(o)) // ['a']
console.log(Object.getOwnPropertyNames(o)) // ['a']
console.log(Object.getOwnPropertySymbols(o)) // [Symbol(list)]
console.log(Reflect.ownKeys(o)) // ['a', Symbol(list)]
以上的 5 种方法遍历对象的属性名,都遵守同样的属性遍历的次序规则。
- 首先遍历所有数值键,按照数值升序排列。
- 其次遍历所有字符串键,按照加入时间升序排列。
- 最后遍历所有
Symbol键,按照加入时间升序排列。
删除属性
一个对象本身没有任何方法可以删除自己的属性。要删除一个对象的属性,可使用 delete 运算符。ES2015还提供了 Reflect.deleteProperty 删除属性,前提是 configurable 为 true。
const o = { a: 1, b: 2 }
delete o.a // {b: 2}
Reflect.deleteProperty(o, 'b') // {}
Object对象
在 JavaScript 中,几乎所有的对象都是 Object 的实例;一个典型的对象从 Object.prototype 继承属性(包括方法),尽管这些属性可能被覆盖(或者说重写)。唯一不从 Object.prototype 继承的对象是那些 null 原型对象,或者是从其他 null 原型对象继承而来的对象。
对象原型属性
避免调用任何 Object.prototype 方法,特别是那些不打算多态化的方法(即只有其初始行为是合理的,且无法被任何继承的对象以合理的方式重写)。所有从 Object.prototype 继承的对象都可以自定义一个具有相同名称但语义可能与你的预期完全不同的自有属性。此外,这些属性不会被 null 原型对象继承。现代 JavaScript 中用于操作对象的工具方法都是静态的。更具体地说:
valueOf()、toString()和toLocaleString()存在的目的是为了多态化,定义自己的实现并具有合理的行为,因此可以将它们作为实例方法调用。但是,valueOf()和toString()通常是通过强制类型转换隐式调用的,因此你不需要在代码中自己调用它们。__defineGetter__()、__defineSetter__()、__lookupGetter__()和__lookupSetter__()已被弃用,不应该再使用。使用静态方法Object.defineProperty()和Object.getOwnPropertyDescriptor()作为替代。__proto__属性已被弃用,不应该再使用。使用静态方法Object.getPrototypeOf()和Object.setPrototypeOf()作为替代。propertyIsEnumerable()和hasOwnProperty()方法可以分别用静态方法Object.getOwnPropertyDescriptor()和Object.hasOwn()替换。- 如果检查一个构造函数的
prototype属性,通常可以用instanceof代替isPrototypeOf()方法。
如果不存在语义上等价的静态方法,或者真的想使用 Object.prototype 方法,应该通过 call() 直接在目标对象上调用 Object.prototype 方法,以防止因目标对象上原有方法被重写而产生意外的结果。
null 原型对象
几乎所有的 JavaScript 对象最终都继承自 Object.prototype。然而,使用 Object.create(null) 或定义了 __proto__: null 的对象字面量语法来创建 null 原型对象。还可以通过调用 Object.setPrototypeOf(o, null) 将现有对象的原型更改为 null。
const o = Object.create(null)
const o2 = { __proto__: null }
null 原型对象可能会有一些预期外的行为表现,因为它不会从 Object.prototype 继承任何对象方法。这在调试时尤其需要注意,因为常见的对象属性转换/检测实用方法可能会产生错误或丢失信息(特别是在使用了忽略错误的静默错误捕获机制的情况下)。
在实践中,null 原型对象通常被用作 map 的简单替代品。由于存在 Object.prototype 属性,会导致一些错误:
const ages = { alice: 18, bob: 27 }
function hasPerson(name) {
return name in ages;
}
hasPerson('hasOwnProperty')
使用一个 null 原型对象可以消除这种风险,同时不会令 hasPerson 函数变得复杂:
const ages = Object.create(null, {
alice: { value: 18, enumerable: true }
})
JavaScript 还具有内置的 API,用于生成 null 原型对象,特别是那些将对象用作临时键值对集合的 API。
Object.groupBy()方法的返回值RegExp.prototype.exec()方法返回结果中的groups和indices.groups属性Array.prototype[@@unscopables]属性(所有@@unscopables对象原型都应该为null)import.meta对象- 通过
import * as ns from "module"或import()获取的模块命名空间对象
控制对象状态
有时需要冻结对象的读写状态,防止对象被改变。JavaScript 提供了三种冻结方法:
Object.preventExtensions方法可以使得一个对象无法再添加新的属性。Object.isExtensible方法用于检查一个对象是否使用了Object.preventExtensions方法。也就是说,检查是否可以为一个对象添加属性。Object.seal方法使得一个对象既无法添加新属性,也无法删除旧属性。
对象比较
在 JavaScript 中 object 是一种引用类型。两个独立声明的对象永远也不会相等,即使他们有相同的属性,只有在比较一个对象和这个对象的引用时,才会返回 true.
let fruit = { name: 'apple' }
let fruitbear = { name: 'apple' }
console.log(fruit == fruitbear) // false
console.log(fruit === fruitbear) // false
let fruit2 = fruit
console.log(fruit == fruitbear) // true
console.log(fruit === fruitbear) // true