对象(Object)是 JavaScript 中非常重要的一种数据类型。如果说函数是每种编程语言都不可或缺的部分,那么对象则是面对对象范式编程语言不可或缺的部分。

对象的结构

在 JavaScript 中,对象其实就是一种键值对组成的数据结构。

{
key: value,
}

其中 key 可以用两种基本类型来表示:

  • string
  • symbol

value 可以使用所有的数据类型来表示:

  • undefined
  • null
  • boolean
  • number
  • string
  • symbol
  • object

除了 object 是引用类型,其他都是基本类型。

所以,object 内可以存储 object 数据类型,也就是允许嵌套。

对象不是基本数据类型,因为对象的值不像其他基本数据类型一样存储 Primitive Value,而是存储一个地址,表示对象数据结构在内存中的存储位置的地址。一般也将该地址称为引用,所以对象被称为引用类型

Primitive Value(原始值)

属性

实际上,在 JavaScript 中,组成对象的键值对中的 key 有一个官方名称 - propertyProperty 是面对对象编程范式的一个术语,如 Java 中也是有 property 的概念。

property,属性。

let obj = {
name: 'Li Lei',
}
// 等价于
let obj = {
'name': 'Li Lei',
}

对象 obj 就具有一个属性(即键值对),属性名为 name,是一个字符串;属性值为 Li Lei,也是一个字符串。

我们知道字符串需要使用 ''"" 包裹起来,但这里的 name 并没有使用引号。
对象会将自动将未加引号的 name 当成字符串来处理,你也可以手动加上引号。

属性名可以是字符串(可省略引号),也可以是 symbol

const name = Symbol.for('name');
const obj = {
name: 'Li Lei',
};
const obj2 = {
[name]: 'Li Lei',
};
obj.name // 'Li Lei'
obj['name'] // 'Li Lei'
obj[name] // undefined
obj2.name // undefined
obj2['name'] // undefined
obj2[name] // 'Li Lei'

我们使用 Symbol.for() 创建一个 symbol

obj 对象中,我们定义了一个字符串属性 name

obj2 对象中,我们使用属性表达式创建了一个 symbol 属性 name

我们可以将属性表达式的结果作为属性名,如果表达式的值不是 symbol 类型,那么就会隐式转换成字符串。

访问对象的属性

既然定义的对象的属性,自然也需要访问对象的属性,即通过对象的属性名获取对象的属性值。

上一节,我们说到了定义对象属性的两种写法:

  • 普通属性写法(定义静态字符串)
  • 属性表达式写法(定义动态字符串和 symbol

其中,属性表达式的语法如下:

[expression]

与定义属性相对应,访问属性也有两种写法:

  • 普通属性访问(通过静态字符串访问)
  • 属性表达式访问(通过动态字符串或 symbol 访问)
obj.name // 'Li Lei'
obj['name'] // 'Li Lei'
obj[name] // undefined
obj2.name // undefined
obj2['name'] // undefined
obj2[name] // 'Li Lei'

上一节中,我们就使用了 obj.name 方式访问 obj 对象的字符串属性 name,返回的属性值 'Li Lei'

这就是使用了普通属性访问,通过 . 操作符来实现对象属性访问。

object.property

注意
无法使用 object.'property' 访问对象的属性。

另外,我们还使用了 obj['name'] 方式访问。此时, 'name' 就是一个表达式,返回一个字符串 'name'。而 obj['name'] 返回 obj 对象的字符串属性 name 的属性值 'Li Lei'

语法如下:

object[expression]

我们接着看 obj[name],同样,name 是一个表达式,此时 name 不是返回一个字符串 'name',而是返回变量 name 的值 Symbol.for('name'),这是一个 symbol。又因为 obj 对象并没有定义一个 Symbol.for('name') 的属性名,'obj[name]' 返回 undefined

  1. name => Symbol.for('name')
  2. obj[name] => undefined

属性的特性

我们再接着深入了解下属性的特性(Attribute)。

属性的 Attribute 指的是描述属性本身拥有的数据,也可以称为元数据

属性本身拥有哪些可描述的数据呢?

比如是否可修改属性值。

JavaScript 提供了 Object.definePropertyReflect.defineProperty 方法来定义对象的属性。该方法同时允许定义该属性的一些特性。

Object.defineProperty(obj, prop, descriptor)
Reflect.defineProperty(obj, prop, descriptor)

这两个方法是等价的,其中 Reflect 是 ES6 新增的对象,是为了代替使用 Object 来操作对象。

我们可以使用该方法定义一个与之前章节一摸一样的 obj 对象。

const obj = {};
Reflect.defineProperty(obj, 'name', {
configurable: true,
enumerable: true,
value: 'Li Lei',
writable: true,
}
obj.name // 'Li Lei'
obj['name'] // 'Li Lei'

我们来分析来 descriptor 参数接收的对象。

{
configurable: true,
enumerable: true,
value: 'Li Lei',
writable: true,
}
  • enumerable 是一个 boolean,表示是否可以通过某些遍历方法进行遍历。
const a = Object.create(null);
Reflect.defineProperty(a, 'upper', {
configurable: true,
enumerable: true,
value: 'A',
writable: true,
}); // true
// 可以通过 for...in 遍历 a.upper
for (let prop in a) {
console.log(prop);
}
// 'A'
Reflect.defineProperty(a, 'upper', {
configurable: true,
enumerable: false,
value: 'A',
writable: true,
}); // true
// 无法通过 for...in 遍历 a.upper
for (let prop in a) {
console.log(prop);
}
// undefined
  • value 表示属性值,默认为 undefined
  • writable 是一个 boolean,表示属性值是否可以被修改。
const a = {};
Reflect.defineProperty(a, 'upper', {
configurable: true,
enumerable: true,
value: 'A',
writable: true,
}); // true
// 可以修改 a.upper 的属性值
a.upper // 'A'
a.upper = 'AA'
a.upper // 'AA'
Reflect.defineProperty(a, 'upper', {
configurable: true,
enumerable: true,
value: 'A',
writable: false,
}); // true
// 无法修改 a.upper 的属性值
a.upper // 'A'
a.upper = 'AA'
a.upper // 'A'
  • configurable 是一个 boolean,表示是否能够再次修改该属性的特性以及删除该属性。

    • configurable = true
      • 可以修改 enumerablewritablevalue 只有当 writable = true 时才能修改,与 configurable 无关。
      • 可以删除属性。
    • configurable = flase,
      • 不可以修改 enumerable
      • writable = true 时,可以修改 writable = false,之后无法再次修改 writable
      • value 只有当 writable = true 时才能修改,与 configurable 无关。
      • 不可以删除属性。
const [a, b] = [{}, {}];
Reflect.defineProperty(a, 'upper', {
configurable: true,
enumerable: true,
value: 'A',
writable: true,
}); // true
Reflect.defineProperty(b, 'upper', {
configurable: false,
enumerable: true,
value: 'B',
writable: true,
}); // true
a.upper // 'A'
b.upper // 'B'
// 可以再次配置 a.upper 的特性
Reflect.defineProperty(a, 'upper', {
configurable: true,
enumerable: true,
value: 'AA',
writable: true,
}); // true
a.upper // 'AA'
// 可以删除 a.upper
Reflect.deleteProperty(a, 'upper'); // true
a.upper // undefined
// 不允许再次配置 b.upper 的特性(除了 writable)
Reflect.defineProperty(b, 'upper', {
configurable: true,
enumerable: true,
value: 'BB',
writable: false,
}); // false
b.upper // 'BB'
b.upper = 'B'
b.upper // 'BB'
// 不可以删除 b.upper
Reflect.deleteProperty(b, 'upper'); // false
b.upper // 'BB'

属性的 getter 和 setter

JavaScript 还提供了一种很有意思的设置与获取属性的方式。

const obj = {
name: '',
};
const proxy = {
name: '',
};
Reflect.defineProperty(obj, 'name', {
configurable: true,
enumerable: true,
get: function () {
return '[' + proxy.name + ']';
},
set: function (value) {
proxy.name = value;
}
});
obj.name // "[]"
obj.name = 'Li Lei';
proxy.name // "[Li Lei]"
obj.name // "[Li Lei]"

我们可以通过 Reflect.definePropertyObject.defineProperty 的参数 descriptorgetset 配置项来定义属性的 gettersetter

gettersetter 实际上都是函数。

getter 会在尝试获取对象的属性值时调用,函数返回值即对象访问表达式的值,即属性值。

obj.name // "[]"
obj['name'] // "[]"

setter 会在尝试设置对象的属性值时调用,执行函数体的内容,而不会执行默认的赋值操作。

像不像 C++ 的重载赋值运算符。😏

obj.name = 'Li Lei';
proxy.name // "[Li Lei]"

但是,必须要注意的是,valuewritable 是一对相关联的配置项,而 setget 也是一对相关联的配置项,这两对配置项是互斥的。

也就是说,使用 defineProperty 定义属性时无法同时使用这两对配置项,并且当你已经使用其中一对配置项配置过了属性,那么使用另一对配置项配置属性时会覆盖之前的配置(configurable = true)。

gettersetter 的真正用途是拦截对象属性的存取操作,很多框架都依赖该特性。

属性简写

ES6 提供了一种简便书写同名属性的语法。

const name = 'Li Lei';
const obj = { name };
// 等价于
const obj = { name: name };

方法

在面对对象编程语言中,方法是指定义在对象上的函数。

{
name: 'Li Lei',
// 方法
getName: function () {
return name;
},
}

在 JavaScript 中,方法实际上也是属性,因为函数在 JavaScript 也是一个对象

方法这个术语有其特有的语境,所以我们大多数情况下会使用方法而不是属性去描述定义在对象里的函数。但有时概括性地介绍对象的属性时,你应该要知道方法也是属性。

ES6 中也增加了方法的简写:

{
name: 'Li Lei',
// 简写方法
getName () {
return name;
},
}

对象的静态方法

在面对对象编程语言中,静态方法指的是所有同一类型的对象的共享方法,且无法通过对象实例调用该方法。

在 JavaScript 中,静态方法指的是定义在构造器(constructor) 上的方法。

Object 就是所有对象的构造器,本质上是一个函数。

typeof Object // "function"

而构造器 Object 创建的对象实例都继承于 Object.prototype,所以对象实例无法调用 Object 上的方法,而且只能通过 Object.method 来调用,这就符合了静态方法的定义了。

我们可以使用 Reflect.ownKeysObject.getOwnPropertyNames 方法来查看一个对象上的所有的属性(不包括继承的属性)。

Reflect.ownKeys(Object);
// 等价于
Object.getOwnPropertyNames(Object);

返回的结果中在不同的浏览器中可能不同。

Google Chrome 返回 25 个结果。

["length", "name", "arguments", "caller", "prototype", "assign", "getOwnPropertyDescriptor", "getOwnPropertyDescriptors", "getOwnPropertyNames", "getOwnPropertySymbols", "is", "preventExtensions", "seal", "create", "defineProperties", "defineProperty", "freeze", "getPrototypeOf", "setPrototypeOf", "isExtensible", "isFrozen", "isSealed", "keys", "entries", "values"]

SafariFirefox 只返回 23 个结果,不包括 argumentscaller

Google Chrome 版本: 60.0.3112.113
Safari 版本:10.1.2 (12603.3.8)
Firefox 版本:Developer Edition 56.0b6 (64 位)

我们可以修改一下,以获取所有的方法而不是属性。

Reflect.ownKeys(Object)
.filter(key => typeof Object[key] === 'function');
// 返回 20 个结果
0: "assign"
1: "getOwnPropertyDescriptor"
2: "getOwnPropertyDescriptors"
3: "getOwnPropertyNames"
4: "getOwnPropertySymbols"
5: "is"
6: "preventExtensions"
7: "seal"
8: "create"
9: "defineProperties"
10:"defineProperty"
11:"freeze"
12:"getPrototypeOf"
13:"setPrototypeOf"
14:"isExtensible"
15:"isFrozen"
16:"isSealed"
17:"keys"
18:"entries"
19:"values"

接下来,将分组介绍各个方法。

创建对象

创建对象的方法:

  • 对象字面量
  • 对象构造器 new Object()
  • Object.create()
  • Object.assign()

在这之前,我们先看看使用对象字面量(literal)和 new Object() 的构造器语法创建对象。

// 对象字面量
{
name: 'Li Lei',
children: {
name: 'Li MeiMei',
},
}
new Object() // {}
new Object(null) // {}
new Object(undefined) // {}
new Object(true) // Boolean {[[PrimitiveValue]]: true}
new Object(123) // Number {[[PrimitiveValue]]: 123}
new Object('a') // String {0: "a", length: 1, [[PrimitiveValue]]: "a"}
new Object(Symbol('any')) // Symbol {[[PrimitiveValue]]: Symbol(any)}
new Object({name: 'Han MeiMei'}) // {name: "Han MeiMei"}

Object() 只接收一个参数 value

  1. 返回空对象
    • 参数为空(等价于 undefined)
    • null
    • undefind
  2. 返回包装对象
    • boolean 原始类型
    • number 原始类型
    • string 原始类型
    • symbol 原始类型
  3. 返回源对象
    • object 引用类型

Object() 接收除 nullundefined 外的原始类型参数时,会将其转换成对应的包装对象。这就类似于强制转换功能。

实际上,Object() 还提供了不使用 new 操作符版本。你可以去掉 new 操作符,同样可以构建一个对象。

Object.create() 方法可以创建一个继承于另一个对象的对象。

Object.create(null); // {}
Object.create({name: 'Lu Fei'}); // {}

上面两条语句的返回的对象看似一样,其实不然,其区别在于一个隐式属性 __proto__ 的不同。

const obj1 = Object.create(null);
const obj2 = Object.create({name: 'Lu Fei'});
obj1.__proto__ // undefined
obj2.__proto__ // {name: 'Lu Fei'}
// 可访问继承的 `name` 属性
obj2.name // 'Lu Fei'
  • obj1 不存在 __proto__ 属性。
  • obj2__proto__ 属性指向了 create 方法的第一个参数。
// 语法
Object.create(proto, properties);

参数 proto 只接受 null 和对象,其他都会抛出 TypeError

参数 properties 用来指定新对象的 own 属性。

Object.create({name: 'Mr.Lu'}, {
name: {
configurable: true,
value: 'Ms.Lu',
writable: true,
},
age: {
configurable: true,
value: 21,
writable: true,
},
});
// {name: "Ms.Lu", age: 21}

Object.create 的优点是可以定义属性描述符,缺点是定义普通属性太麻烦了。

其实 Object.create 的最主要的特点是可以实现继承。

但有时候,我们并不想用继承的方式创建对象,而是将对象间的属性合并在一起。

ES6 增加的 Object.assign 方法就可以很简单做到这一点。

// 语法
Object.assign(target, ...sources)

Object.assign 方法可以把多个 sources可枚举属性拷贝到 target 对象上。

可枚举属性:enumerable = true 的属性

const proto = {name: 'prototype'};
Object.create(proto); // {}
Object.assign({}, proto);
// {name: "prototype"}

Object.assign 有几点注意项:

  • 会修改 target 对象
  • sources 的同名属性会发生覆盖
  • 返回 target 对象
var obj = {a: 1};
var assign = Object.assign(obj, {b: 2}, {b: 3, c: 4})
// 修改 target 对象
obj // {a: 1, b: 3, c: 4}
// 覆盖同名属性
obj.b // 3
// 返回 target 对象
assign === obj // true
  • 浅拷贝,只拷贝对象的引用
  • 赋值只读属性时会抛出 TypeError,并中止之后的赋值。
var obj = {};
// 定义只读属性 a
Reflect.defineProperty(obj, 'a', {
value: 1,
writable: false,
});
const ref = {b: {name: '引用'}}
// 覆盖只读属性出错
Object.assign(obj, ref, {a: 2}, {c: 3}) // TypeError
// 中止了发生异常之后的赋值操作
obj // {b: {name: '引用'}, a: 1}
obj.c // undefined
// 只拷贝了对象的引用
obj.b.name = 'reference';
ref.b.name // "reference"
  • 只拷贝 own 属性,且必须是可枚举属性。
  • 不拷贝属性描述符。
const A = {name: 'a'};
const B = Object.create(A, {
age: {
configurable: false,
enumerable: true,
value: 21,
writable: true,
},
phone: {
value: '12345678901',
},
address: {
configurable: false,
enumerable: true,
get() { return 'China'; },
set() { console.log('Can\'t change'); }
}
});
// {age: 21, phone: "12345678901"}
B.address // "China"
const C = Object.assign({}, B);
// {age: 21, address: "China"}
// 不拷贝继承属性
B.name // 'a'
C.name // undefined
// 不拷贝不能枚举的 own 属性
B.phone // "12345678901"
C.phone // undefined
// 不拷贝访问器,只拷贝 get 返回值
// 实际上不会拷贝属性描述符
Object.getOwnPropertyDescriptor(B, 'address')
// {enumerable: true, configurable: false, get: ƒ, set: ƒ}
Object.getOwnPropertyDescriptor(C, 'address')
// {value: "China", writable: true, enumerable: true, configurable: true}

属性操作

设置:

  • Object.defineProperty
  • Object.defineProperties

获取:

  • Object.getOwnPropertyDescriptor
  • Object.getOwnPropertyDescriptors
  • Object.getOwnPropertyNames
  • Object.getOwnPropertySymbols
  • Object.keys
  • Object.values
  • Object.entries

ES6 将上面一些方法移动到了 Reflect 对象。

  • Reflect.defineProperty
  • Reflect.getOwnPropertyDescriptor
  • Reflect.ownKeys
let obj = {}
Object.defineProperty(obj, 'name', {
configurable: true,
enumerable: true,
value: 'Li Li',
writeable: true,
});
obj.name // "Li Li"
Object.defineProperties(obj, {
name: {
value: 'Li Lei',
writable: true,
},
age: {
value: 21,
},
[Symbol.for('id')]: {
value: '01',
},
});
obj.name // "Li Lei"
obj.age // 21
obj[Symbol.for('id')] // "01"
Object.getOwnPropertyDescriptor(obj, 'name')
// {value: "Li Lei", writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptors(obj)
/*
{
age: {value: 21, writable: false, enumerable: false, configurable: false},
name: {value: "Li Lei", writable: true, enumerable: true, configurable: true},
Symbol(id): {value: "01", writable: false, enumerable: false, configurable: false},
}
*/
Object.getOwnPropertyNames(obj)
// ["name", "age"]
Object.getOwnPropertySymbols(obj)
// [Symbol(id)]
Object.keys(obj)
// ["name"]
Object.values(obj)
// ["Li Lei"]
Object.entries(obj)
// [["name", "Li Lei"]]

原型操作

  • Object.getPrototypeOf
  • Object.setPrototypeOf

ES6 也将上面方法移动到了 Reflect 对象。

  • Reflect.getPrototypeOf
  • Reflect.setPrototypeOf
let A = {name: 'a'};
let B = {name: 'b'};
let C = Object.create(A);
Object.getPrototypeOf(C)
// {name: 'a'}
Object.setPrototypeOf(C, B)
// {}
Object.getPrototypeOf(C)
// {name: 'b'}

其他操作

  • Object.freeze
  • Object.isFrozen
  • Object.seal
  • Object.isSealed
  • Object.preventExtensions
  • Object.isExtensible
  • Object.is

ES6 将上面一些方法移动到了 Reflect 对象。

  • Reflect.preventExtensions
  • Reflect.isExtensible

Object.freeze 方法会冻结对象,无法操作对象的属性(除了读取操作)。
但是如果对象的属性值是一个未冻结对象的引用,那么可以执行未冻结的属性操作,即 freeze 是浅冻结。

let obj = {a: 1, b: 2};
obj.a = 'a';
obj.c = 3;
delete obj.c // true
Object.isFrozen(obj) // false
Object.freeze(obj) // {a: "a", b: 2}
'use strict'; obj.a = 1;
// TypeError: Cannot assign to read only property 'a' of object
'use strict'; obj.c = 3;
// TypeError: Cannot add property c, object is not extensible
'use strict'; delete obj.a;
// TypeError: Cannot delete property 'a'
'use strict'; Object.defineProperty(obj, 'a', {
configurable: true,
enumerable: true,
value: 1,
writable: true,
});
// TypeError: Cannot redefine property: a

Object.seal 方法会密封对象,密封对象不可扩展(添加新属性和修改 __proto__),设置对象所有属性的 configurable = false

Object.preventExtensions 方法会阻止对象扩展,即对象不可扩展(添加新属性),也不可修改 __proto__,即不可修改原型。

Object.is 是对 === 的部分改进。

+0 === -0 // true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true