对象(Object)是 JavaScript 中非常重要的一种数据类型。如果说函数是每种编程语言都不可或缺的部分,那么对象则是面对对象范式编程语言不可或缺的部分。
对象的结构
在 JavaScript 中,对象其实就是一种键值对组成的数据结构。
|
其中 key
可以用两种基本类型来表示:
string
symbol
而 value
可以使用所有的数据类型来表示:
undefined
null
boolean
number
string
symbol
object
除了
object
是引用类型,其他都是基本类型。
所以,object
内可以存储 object
数据类型,也就是允许嵌套。
对象不是基本数据类型,因为对象的值不像其他基本数据类型一样存储 Primitive Value,而是存储一个地址,表示对象数据结构在内存中的存储位置的地址。一般也将该地址称为引用,所以对象被称为引用类型。
Primitive Value(原始值)
属性
实际上,在 JavaScript 中,组成对象的键值对中的 key
有一个官方名称 - property
。Property 是面对对象编程范式的一个术语,如 Java 中也是有 property 的概念。
property
,属性。
|
对象 obj
就具有一个属性(即键值对),属性名为 name
,是一个字符串;属性值为 Li Lei
,也是一个字符串。
我们知道字符串需要使用
''
或""
包裹起来,但这里的name
并没有使用引号。
对象会将自动将未加引号的name
当成字符串来处理,你也可以手动加上引号。
属性名可以是字符串(可省略引号),也可以是 symbol
。
|
我们使用 Symbol.for()
创建一个 symbol
。
在 obj
对象中,我们定义了一个字符串属性 name
。
在 obj2
对象中,我们使用属性表达式创建了一个 symbol 属性 name
。
我们可以将属性表达式的结果作为属性名,如果表达式的值不是
symbol
类型,那么就会隐式转换成字符串。
访问对象的属性
既然定义的对象的属性,自然也需要访问对象的属性,即通过对象的属性名获取对象的属性值。
上一节,我们说到了定义对象属性的两种写法:
- 普通属性写法(定义静态字符串)
- 属性表达式写法(定义动态字符串和 symbol)
其中,属性表达式的语法如下:
|
与定义属性相对应,访问属性也有两种写法:
- 普通属性访问(通过静态字符串访问)
- 属性表达式访问(通过动态字符串或 symbol 访问)
|
上一节中,我们就使用了 obj.name
方式访问 obj
对象的字符串属性 name
,返回的属性值 'Li Lei'
。
这就是使用了普通属性访问,通过 .
操作符来实现对象属性访问。
|
注意:
无法使用object.'property'
访问对象的属性。
另外,我们还使用了 obj['name']
方式访问。此时, 'name'
就是一个表达式,返回一个字符串 'name'
。而 obj['name']
返回 obj
对象的字符串属性 name
的属性值 'Li Lei'
。
语法如下:
|
我们接着看 obj[name]
,同样,name
是一个表达式,此时 name
不是返回一个字符串 'name'
,而是返回变量 name
的值 Symbol.for('name')
,这是一个 symbol
。又因为 obj
对象并没有定义一个 Symbol.for('name')
的属性名,'obj[name]'
返回 undefined
。
name
=>Symbol.for('name')
obj[name]
=>undefined
属性的特性
我们再接着深入了解下属性的特性(Attribute)。
属性的 Attribute
指的是描述属性本身拥有的数据,也可以称为元数据。
属性本身拥有哪些可描述的数据呢?
比如是否可修改属性值。
JavaScript 提供了 Object.defineProperty
和 Reflect.defineProperty
方法来定义对象的属性。该方法同时允许定义该属性的一些特性。
|
这两个方法是等价的,其中
Reflect
是 ES6 新增的对象,是为了代替使用Object
来操作对象。
我们可以使用该方法定义一个与之前章节一摸一样的 obj
对象。
|
我们来分析来 descriptor
参数接收的对象。
|
enumerable
是一个boolean
,表示是否可以通过某些遍历方法进行遍历。
|
value
表示属性值,默认为undefined
。writable
是一个boolean
,表示属性值是否可以被修改。
|
configurable
是一个 boolean,表示是否能够再次修改该属性的特性以及删除该属性。- 当
configurable = true
,- 可以修改
enumerable
和writable
,value
只有当writable = true
时才能修改,与configurable
无关。 - 可以删除属性。
- 可以修改
- 当
configurable = flase
,- 不可以修改
enumerable
- 当
writable = true
时,可以修改writable = false
,之后无法再次修改writable
value
只有当writable = true
时才能修改,与configurable
无关。- 不可以删除属性。
- 不可以修改
- 当
|
属性的 getter 和 setter
JavaScript 还提供了一种很有意思的设置与获取属性的方式。
|
我们可以通过 Reflect.defineProperty
和 Object.defineProperty
的参数 descriptor
的 get
和 set
配置项来定义属性的 getter 和 setter。
getter 和 setter 实际上都是函数。
getter 会在尝试获取对象的属性值时调用,函数返回值即对象访问表达式的值,即属性值。
|
setter 会在尝试设置对象的属性值时调用,执行函数体的内容,而不会执行默认的赋值操作。
像不像 C++ 的重载赋值运算符。😏
|
但是,必须要注意的是,value
与 writable
是一对相关联的配置项,而 set
与 get
也是一对相关联的配置项,这两对配置项是互斥的。
也就是说,使用 defineProperty
定义属性时无法同时使用这两对配置项,并且当你已经使用其中一对配置项配置过了属性,那么使用另一对配置项配置属性时会覆盖之前的配置(configurable = true
)。
getter 和 setter 的真正用途是拦截对象属性的存取操作,很多框架都依赖该特性。
属性简写
ES6 提供了一种简便书写同名属性的语法。
|
方法
在面对对象编程语言中,方法是指定义在对象上的函数。
|
在 JavaScript 中,方法实际上也是属性,因为函数在 JavaScript 也是一个对象。
方法这个术语有其特有的语境,所以我们大多数情况下会使用方法而不是属性去描述定义在对象里的函数。但有时概括性地介绍对象的属性时,你应该要知道方法也是属性。
ES6 中也增加了方法的简写:
|
对象的静态方法
在面对对象编程语言中,静态方法指的是所有同一类型的对象的共享方法,且无法通过对象实例调用该方法。
在 JavaScript 中,静态方法指的是定义在构造器(constructor) 上的方法。
Object
就是所有对象的构造器,本质上是一个函数。
|
而构造器 Object
创建的对象实例都继承于 Object.prototype
,所以对象实例无法调用 Object
上的方法,而且只能通过 Object.method
来调用,这就符合了静态方法的定义了。
我们可以使用 Reflect.ownKeys
或 Object.getOwnPropertyNames
方法来查看一个对象上的所有的属性(不包括继承的属性)。
|
返回的结果中在不同的浏览器中可能不同。
Google Chrome 返回 25 个结果。
|
而 Safari 和 Firefox 只返回 23 个结果,不包括 arguments
和 caller
。
Google Chrome 版本: 60.0.3112.113
Safari 版本:10.1.2 (12603.3.8)
Firefox 版本:Developer Edition 56.0b6 (64 位)
我们可以修改一下,以获取所有的方法而不是属性。
|
接下来,将分组介绍各个方法。
创建对象
创建对象的方法:
- 对象字面量
- 对象构造器
new Object()
- Object.create()
- Object.assign()
在这之前,我们先看看使用对象字面量(literal)和 new Object()
的构造器语法创建对象。
|
Object()
只接收一个参数 value
。
- 返回空对象
- 参数为空(等价于 undefined)
- null
- undefind
- 返回包装对象
- boolean 原始类型
- number 原始类型
- string 原始类型
- symbol 原始类型
- 返回源对象
- object 引用类型
Object()
接收除 null
和 undefined
外的原始类型参数时,会将其转换成对应的包装对象。这就类似于强制转换功能。
实际上,Object()
还提供了不使用 new
操作符版本。你可以去掉 new
操作符,同样可以构建一个对象。
Object.create()
方法可以创建一个继承于另一个对象的对象。
|
上面两条语句的返回的对象看似一样,其实不然,其区别在于一个隐式属性 __proto__
的不同。
|
obj1
不存在__proto__
属性。obj2
的__proto__
属性指向了create
方法的第一个参数。
|
参数 proto
只接受 null
和对象,其他都会抛出 TypeError
。
参数 properties
用来指定新对象的 own 属性。
|
Object.create
的优点是可以定义属性描述符,缺点是定义普通属性太麻烦了。
其实 Object.create
的最主要的特点是可以实现继承。
但有时候,我们并不想用继承的方式创建对象,而是将对象间的属性合并在一起。
ES6 增加的 Object.assign
方法就可以很简单做到这一点。
|
Object.assign
方法可以把多个 sources
的可枚举属性拷贝到 target
对象上。
可枚举属性:
enumerable = true
的属性
|
Object.assign
有几点注意项:
- 会修改
target
对象 sources
的同名属性会发生覆盖- 返回
target
对象
|
- 浅拷贝,只拷贝对象的引用
- 赋值只读属性时会抛出
TypeError
,并中止之后的赋值。
|
- 只拷贝 own 属性,且必须是可枚举属性。
- 不拷贝属性描述符。
|
属性操作
设置:
- 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
|
原型操作
- Object.getPrototypeOf
- Object.setPrototypeOf
ES6 也将上面方法移动到了
Reflect
对象。
- Reflect.getPrototypeOf
- Reflect.setPrototypeOf
|
其他操作
- Object.freeze
- Object.isFrozen
- Object.seal
- Object.isSealed
- Object.preventExtensions
- Object.isExtensible
- Object.is
ES6 将上面一些方法移动到了
Reflect
对象。
- Reflect.preventExtensions
- Reflect.isExtensible
Object.freeze
方法会冻结对象,无法操作对象的属性(除了读取操作)。
但是如果对象的属性值是一个未冻结对象的引用,那么可以执行未冻结的属性操作,即 freeze
是浅冻结。
|
Object.seal
方法会密封对象,密封对象不可扩展(添加新属性和修改 __proto__
),设置对象所有属性的 configurable = false
。
Object.preventExtensions
方法会阻止对象扩展,即对象不可扩展(添加新属性),也不可修改 __proto__
,即不可修改原型。
Object.is
是对 ===
的部分改进。
|