啃犀牛书——对象
前言
基础不牢,地动山摇
JS对象
对象介绍
对象是一种复合值,汇聚多个值(原始值/其他对象)且允许按照名字存储这些值。
对象是一个 属性的无序集合,每个属性都有名字和值。对象把字符串映射为值(散列、散列表、字典、关联数组)。
对象除了可以维持自己的属性外,还能够从其他对象(原型)继承属性。
任何数、字符串、符号、布尔值、null、undefined的值都是对象。
JS对象是动态的,不过也可以利用对象模拟静态类型语言中的静态对象和结构体。
对象可修改,按引用操作完成(栈不同而堆同),而非按值操作。
对象属性: 自有属性, 继承属性
对象属性特性: writable可写(是否可设置) enumerable可枚举(是否可for/in遍历) configurable可配置(是否可删/修改)
对象的创建
字面量
这是创建对象的一个最为简单的方法:简单直接花括号↓
1 | let empty = {} |
值得注意的是,对象字面量实际上是一个表达式,每次求值都会创建并初始化一个船新的对象。因此,当字面量每次求值的时候,它的每个属性值也会被求值。
这很容易让人联想到用var在循环体中创建字面量对象的时候会出现什么情况——不断叠加改变,而非新创建
new 构造函数
初始化新创建的对象:let 加 constructor
1 | let obj = new Object() |
重要概念-原型和原型链
原型
原型是一个对象,用于定义其他对象的共享属性和方法。
几乎所有的对象都有原型,但只有少数有prototype属性,而这些少数对象为所有的其他对象定义了原型。
原型链(之前理解有误,现在来掰正)
原型链是JS一种主要的继承方式。它的基本思想是通过原型来继承一些引用类型的属性和方法。它的基本构想是在实例和原型间构造一条长链。
具体举例:一个构造函数,它有prototype
属性指向它的原型对象,这个原型对象中,有constructor
指回这个构造函数,使用构造函数new出来的实例对象中,也有一个属性指向这个原型对象,这个属性是__proto__
。这样一来,一个小块的小链条就连好了。我们再看这个原型对象,这个原型对象也极有可能继承其他对象的一些属性和方法,那么这个时候,这个原型对象也会有__proto__
指向它的原型对象。以此类推,一条从低端到顶端我们所规定的Object.prototype.__proto__ = null
的长链就连好了。
因此,当我们在查询某个对象中的属性或方法的时候,如果找到了,很好,如果没找到,去它原型找,如果还没找到,去原型的原型找,一直到这个顶端,找到或找不到为止。此时这一条向上查询所依附的链条就可以被认为是原型链吧。
Object.create()
这是一个很强大的技术!
第一个参数:新对象的原型
第二个参数:新对象的属性(高级特性)
1 | let obj1 = Object.create({ x: 1, y: 2 }) // 创建一个对象{x:1,y:2} |
Object的一个重要用途
防止对象被某个第三方库函数意外(非恶意)修改。
不要直接将对象传给函数,而是要传入继承自它的对象,这样一来,属性可读且修改不会影响原始对象。
1 | let obj = {x: "知法懂法守法用法"} |
查询和设置属性
引入
.
或[]
值得注意的是,[]
中必须是字符串或者能转换为字符串的符号或值
查询
1 | let name = student.name |
添加/设置
1 | student.name = '栗子' |
作为关联数组的对象
上述[字符串]
(以字符串为索引的数组)被称为“关联数组”。
与C/C++/Java等强类型语言不同,JS是松散类型语言,可以为任意对象创建数量和属性。
数组表示法结合字符串表达式访问对象属性非常灵活。
当然在es6之后使用Map类更为妙。
继承
让我们举个例子及来解释一下继承:
- 首先,需要先明确,JS对象属性一般由自有属性和继承属性两部分组成。
- 假设要从对象o中查询属性x,如果对象o没有这个自有属性,则会去o的原型对象中查询该属性;如果原型对象也没有这个自有属性,但它有原型,那么就会去它的原型中查询。
- 上述过程一直持续,直到找到该属性x,或者一直查询到原型为null的对象。
我们继续对查询和设置对象属性的分析,举个特例↓
1 | let a = Object.create(null, { |
这里b的原型对象a.x是不可修改不可设置的,所以当对b进行属性操作时,对于其继承属性的操作是直接被忽略掉的。
一个重要的JS特性
查询属性时,会用到原型链;设置属性时,不影响原型链。
属性访问错误
属性访问错误的两种情况:
- 对象不存在,直接报异常TypeError:undefined
- 属性不存在,返回undefined
几种防止此类问题发生的方法:
- 简单ififif
- 用&&连接
- 使用
?.
条件式访问
1 | let book = { author: { name: '李松峰' } } |
小结
在对象o中设置属性p在以下情况会失败:
- o有一个只读自有属性p
- o有一个只读继承属性p
- o没有自有&继承属性p,且o的extensible特性为false(不可扩展,不能添加新属性)
删除属性
主打一个delete
delete操作符
只能删除对象的自有属性,不能删除继承属性,不能删除configurable为false的属性
在非严格模式下
1 | let o = { x: 1 } |
在严格模式下
1 | delete 不可配置的属性 // TypeError |
测试属性
在实际开发中,我们往往要查一下某对象是否有某属性,这个时候就可以根据场景选用以下四种方式↓
1 | // 炒一些栗子 |
!==
这种方法可以粗略判断对象中是否有自有属性和继承属性,但不全面——它不会区分 不存在的和值本身为undefined的
1 | o.x !== undefined // => true |
!==
经常与 in
搭配使用,后者用来查询属性,前者用来判断是否为undefined
p in o
这种方法查询自有属性和继承属性,可以区分undefined和不存在
1 | x in o // => true |
o.hasOwnProperty(‘p’)
这种方法查询自有属性
1 | o.hasOwnProperty('x') // => true |
o.propertyIsEnumerable(‘p’)
这个方法细化了上一个方法,它查询的是可枚举 enumerable
的自有属性
1 | o.propertyIsEnumerable('x') // => true |
枚举属性
- for/in // let key in obj
- Object.keys() // [‘a’, ‘b’, ‘c’]
- Object.values() // [1, 2, 3]
- Object.entries() // [[‘a’, 1], [‘b’, 2], [‘c’, 3]]
for/in
该循环对指定对象的每个可枚举的属性(自有、继承)都运行一次循环体,将属性名字赋给循环变量。
对象继承的内置方法是不可枚举的qvq
应用
使用for/in和hasOwnProperty来防止枚举到的继承属性
1 | for (let p in o) { |
使用for/in和typeof跳过所有方法属性
1 | for (let p in o) { |
取得对象属名称函数
这些函数往往与for/in搭配使用来获取对象名称数组。
Object.keys()
返回对象 可枚举+自有属性名 数组
Object.getOwnPropertyNames()
返回对象 自有属性+为字符串属性名 数组
Object.getOwnPropertySymbols()
返回对象 自有属性名 数组
Reflect.ownKeys()
返回对象 所有属性名 数组
属性枚举顺序
相关方法枚举属性顺序
上述四个函数以及JSON.stringfy()等方法
- 属性名为非负整数字符串,按照数值升序(这也意味着数组和类数组对象属性会按照顺序被枚举)
- 属性名为其他字符串,按照先后顺序
- 属性名为符号对象,按照先后顺序
for/in循环枚举顺序不那么严格
- 先按照上述顺序枚举自有属性
- 再按照上述顺序枚举继承属性
- 这里需要注意的是:当已经有同名属性枚举过或者说一个同名属性是不可枚举的,那么这个属性就不会被枚举了
默认情况下,符号属性是不可枚举的。这意味着它们不会出现在对象属性的 for…in 循环中,也不会被包括在 Object.keys() 和 Object.getOwnPropertyNames() 方法的返回结果中。
扩展对象
指一个对象的属性复制到另一个对象上。
原始且常见的操作
1 | let o1 = {x: 1}, o1 = {y: 2, z: 3} |
↑各框架纷纷定义类似于extends()的辅助函数,es6以Object.assign()的形式进入核心JS语言
Object.assign()
参数
传两种参数,第一个参数是目标对象;第二个以及以后的参数是一个个来源对象。
1 | Object.assign(目标对象, 来源对象*n) |
方式
这个函数将这些 来源对象 的 可枚举自有属性 有顺序地(例如后续属性会覆盖先前同名属性)复制到目标对象中。
这个函数在执行期间,如果来源对象有获取方法/目标对象有设置方法,那么这些方法会被调用,当然它们不会被复制。
将属性从一个对象分配给另一个对象
这种做法的原因是,如果有一个默认对象里有很多属性和它们的默认值,且对象中不存在同名属性,那么就可以将这些默认值复制到一个对象中。
实现↓
1 | let defaults = {...} // 这是一个默认对象 |
拓展-重写一些辅助函数
merge()——与Object.assign()类似,但不会覆盖已经存在的属性(书中)
restrict()——从一个对象中删除另一个对象中没有的属性(自己试着写的)
subtract()——从一个对象中删除另一个对象中包含的所有属性(↑)
1 | // 注意这里不是连续操作,传参皆为以下初始值 |
序列化对象
对象序列化
对象序列化(serialization)是把对象的状态(对象在某一时刻属性和值的集合)转换为字符串的过程,之后可以从中恢复对象的状态。
JS对象序列化函数:JSON.stringify()
恢复JS对象函数:JSON.parse()
JSON语法是JS语法的子集,所以不能表示所有的JS值。
↑因此它能序列化和回复的值是有限的↓
- 对象、数组、字符串(日期对象会被序列化为ISO格式日期字符串
Date.toJSON()
,但是JSON.parse()
时会保持其字符串形式) - 有限数值
- true/false
- null(NaN、Infinity会被序列化成null)
不能被序列化或恢复的↓
- 函数
- RegExp
- Error对象
- undefined值
另外,JSON.stringify()只序列化对象的 可枚举自有属性。如果属性值无法被序列化,那么属性会从字符串中删除。
JSON.stringify() / JSON.parse()
两者都可以接收第二个参数,用来自定义序列化以及恢复操作。==(待填充具体自定义方式)==
对象方法
Object.prototype中一些通用方法
toString()
该方法的默认方法是不会显示太有用的信息。所以一般需要在对象中对该方法进行一个重新定义↓
1 | let point = { |
toLoocalString()
该方法默认是没有实现任何本地化操作,只是简单调用了一下toString()方法。因此一般需要根据本地惯例格式化数值、日期、时间等↓
1 | let point = { |
valueOf()
该方法默认也并没有做什么。因此一些内置类定义了自己的valueOf()方法。
1 | let point = { |
Date类定义的valueOf()可以将日期转换为值,这样一来就能进行比较。
toJSON()——不太理解
Object.property实际没有定义该方法,但JSON.stringify()方法会从要序列化的对象中寻找toJSON()方法。如果该方法存在,就调用然后序列化该方法的返回值,而非原始对象。
1 | let point = { |
Date类定义了自己的toJSON()方法,返回一个表示日期的序列化字符串。
对象字面量扩展语法
简写属性
ES6之后可以删掉其中分号和一份标识符,使得代码更为简洁(非常常用)。
compare↓
1 | let x = 1, y = 2 |
计算的属性名
ES6之后使用计算属性就非常讨巧了。因为可以直接在一个对象中使用中括号[]
去框计算属性,使得计算结果(必要转字符串)直接作为对象属性名。
compare↓
1 | const pName = 'herName' |
计算属性使用场景
- 书中提到的一种:假设我有一个JS代码库,我需要传给此库一组特定属性对象,而这组属性的名字在库中都是以常量的形式定义的。如果要创建传给此库的对象,可以硬编码他们的属性名,但是这样很有可能把属性名写错,而且也存在更新迭代修改属性名而出错。那么这时候不妨用一层计算属性去装这些常量,来组成这个对象。
- 数据过滤:按照一定的条件对数据进行过滤
- 数据排序:按照一定顺序对数据排序,这个时候就可以使用计算属性对数据集进行动态排序
- 数据格式化:使用计算属性格式化数据,最常见的可能是数字和日期以及字符串……都挺常见的。
- 数据聚合:比如说一些加和,一些拼接……
符号作为属性名
ES6后符号也可以作为属性名(也多亏了计算属性语法)。
- Symbol除了用作属性名以外不能做任何事。
- 为啥Symbol()函数是工厂函数而不是构造函数?——该函数返回值是原始值而不是对象,因此不需要new一个对象实例。
- 符号是一种安全机制吗?——nono,符号不是为了安全,而是为JS对象定义安全的扩展机制。假设我们要从不受控的第三方代码中得到一个对象,我们担心创建一个属性会使得对象与原对象产生冲突,或者担心第三方代码以外修改我们命名的属性,那这个时候用Symbol非常合适。当然,第三方代码完全可以通过
Object.getOwnPropertySymbols()
方法找到我们的符号并且修改删除qvq,这也是符号并非一种安全机制的原因。
扩展操作符
扩展操作符在我们之前使用Object.assign()
时提到过,这是一种简便方法,可以将对象的属性复制到新对象中,这是一种典型的插值行为,当然只在对象字面量中才是这种行为(post参数设置的时候会用到,这个栗子 来源于我最近在学做的项目中)。
- 扩展操作符不是严格意义上的操作符,是一种语法
- 扩展操作符只能扩展对象的自有属性
- 扩展操作符虽说看着很简洁,但是会给JS解释器带来巨大工作量。基于循环/递归 ( O(n) ) 不断给对象追加属性,倘若操作量很大的话,时间复杂度是非常高的,近似于 O(n^2) 。
简写方法
ES6之后对象里函数可以省去function
简写和其他类型的属性名
1 | let o1 = { |
字符串字面量和计算属性都可以作为对象中方法属性名;
使得包含Symbol属性的对象也可迭代-Generator&Symbol.iterator
符号也可作为方法名,为了让对象可迭代,必须以符号名Symbol.iterator
为它定义一个方法↓
1 | const obj = { |
在上面的代码中,我们使用了Generator
函数定义Symbol.iterator属性*[Symbol.iterator]
,该函数返回迭代器对象。该迭代器对象通过遍历对象中所有属性并返回属性值,从而实现了可迭代性。这样一来这个Symbol方法名的方法的属性也可迭代了。
值得注意Generator函数在这里使用了*
作为函数声明符号。这是定义Generator函数的一种特殊语法。这种语法将函数定义为一种生成器,使得其具有 短暂暂停/恢复执行 的能力,从而使其能生成一个可迭代对象。
属性的获取/设置方法
对象的属性除了数据属性,还有访问器属性accessor property
。
- 对象属性:属性名&属性值
- 访问器属性:一个俩的访问器方法——获取方法 getter / 设置方法 setter
访问器属性
访问器属性用法
属性名:方法名 属性值:方法返回值 ES6之后也可以使用计算的属性名来定义获取/设置方法(相当炸裂)
- 程序查询一个访问器属性的时候,通常会调用获取方法(不传参),这个方法返回值即为属性访问表达式的值。
- 程序设置修改一个访问器属性时,通常会调用设置方法(需传参),这个方法是用来设置属性的值,忽略返回值。
可读可写,只读,只写(这种属性通过数据属性无法实现,返回值
undefined
)——qvq好玩吧~
访问器属性可以通过一个字面量扩展语法定义。
1 | // 2D笛卡尔坐标,极坐标 |
访问器属性是可继承的捏!
小结
本章可谓是非常核心的一章,未来还需要我在通读和理解后续内容后与之融会贯通。
简单列一下本章大纲,方便回顾↓
- 对象及相关基本概念
- 对象字面量语法
- 对象属性的读/写/删/查/枚举
- 原型的继承
- 对象的扩展
- JS中所有非原始值的值都是对象。