前言

基础不牢,地动山摇

JS对象

对象介绍

对象是一种复合值,汇聚多个值(原始值/其他对象)且允许按照名字存储这些值。

对象是一个 属性的无序集合,每个属性都有名字和值。对象把字符串映射为值(散列、散列表、字典、关联数组)。

对象除了可以维持自己的属性外,还能够从其他对象(原型)继承属性。

任何数、字符串、符号、布尔值、null、undefined的值都是对象。

JS对象是动态的,不过也可以利用对象模拟静态类型语言中的静态对象和结构体。

对象可修改,按引用操作完成(栈不同而堆同),而非按值操作。

对象属性: 自有属性, 继承属性

对象属性特性: writable可写(是否可设置) enumerable可枚举(是否可for/in遍历) configurable可配置(是否可删/修改)

对象的创建

字面量

这是创建对象的一个最为简单的方法:简单直接花括号↓

1
2
3
4
5
6
7
8
9
10
11
12
let empty = {}
let point = {x: 0, y: 0}
let p1 = {x: -point.x, y: -point.y}
let std = {
"first name": '',
age: 0,
"some-hobby": [],
friend:{
name: 'raliz',
age: 20
}
}

值得注意的是,对象字面量实际上是一个表达式,每次求值都会创建并初始化一个船新的对象。因此,当字面量每次求值的时候,它的每个属性值也会被求值

这很容易让人联想到用var在循环体中创建字面量对象的时候会出现什么情况——不断叠加改变,而非新创建

new 构造函数

初始化新创建的对象:let 加 constructor

1
2
let obj = new Object()
let arr = new Array()
重要概念-原型和原型链
原型

原型是一个对象,用于定义其他对象的共享属性和方法。

几乎所有的对象都有原型,但只有少数有prototype属性,而这些少数对象为所有的其他对象定义了原型。

原型链(之前理解有误,现在来掰正)

原型链是JS一种主要的继承方式。它的基本思想是通过原型来继承一些引用类型的属性和方法。它的基本构想是在实例和原型间构造一条长链。

具体举例:一个构造函数,它有prototype属性指向它的原型对象,这个原型对象中,有constructor指回这个构造函数,使用构造函数new出来的实例对象中,也有一个属性指向这个原型对象,这个属性是__proto__。这样一来,一个小块的小链条就连好了。我们再看这个原型对象,这个原型对象也极有可能继承其他对象的一些属性和方法,那么这个时候,这个原型对象也会有__proto__指向它的原型对象。以此类推,一条从低端到顶端我们所规定的Object.prototype.__proto__ = null的长链就连好了。

因此,当我们在查询某个对象中的属性或方法的时候,如果找到了,很好,如果没找到,去它原型找,如果还没找到,去原型的原型找,一直到这个顶端,找到或找不到为止。此时这一条向上查询所依附的链条就可以被认为是原型链吧。

Object.create()

这是一个很强大的技术!

第一个参数:新对象的原型

第二个参数:新对象的属性(高级特性)

1
2
3
let obj1 = Object.create({ x: 1, y: 2 }) // 创建一个对象{x:1,y:2}
let obj2 = Object.create(null) // 参数为null创建的对象不会继承任何属性和方法,无法对其应用,也无法对其使用操作符
let obj3 = Object.create(Object.prototype) // 创建空对象写法
Object的一个重要用途

防止对象被某个第三方库函数意外(非恶意)修改。

不要直接将对象传给函数,而是要传入继承自它的对象,这样一来,属性可读且修改不会影响原始对象。

1
2
let obj = {x: "知法懂法守法用法"}
library.function(Object.create(obj))

查询和设置属性

引入

.[]

值得注意的是,[]中必须是字符串或者能转换为字符串的符号或值

查询

1
2
let name = student.name
let age = student["name"]

添加/设置

1
2
student.name = '栗子'
student["name"] = '呆子'

作为关联数组的对象

上述[字符串](以字符串为索引的数组)被称为“关联数组”。

与C/C++/Java等强类型语言不同,JS是松散类型语言,可以为任意对象创建数量和属性。

数组表示法结合字符串表达式访问对象属性非常灵活。

当然在es6之后使用Map类更为妙。

继承

让我们举个例子及来解释一下继承:

  • 首先,需要先明确,JS对象属性一般由自有属性和继承属性两部分组成。
  • 假设要从对象o中查询属性x,如果对象o没有这个自有属性,则会去o的原型对象中查询该属性;如果原型对象也没有这个自有属性,但它有原型,那么就会去它的原型中查询。
  • 上述过程一直持续,直到找到该属性x,或者一直查询到原型为null的对象。

我们继续对查询和设置对象属性的分析,举个特例↓

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let a = Object.create(null, {
x: {
value: 1,
writable: false,
configurable: false
},
y: {
value: 1,
writable: true,
configurable: true
}
});

let b = Object.create(a);

b.x = 2 // 直接忽略该操作
b.y = 2 // 同名覆盖
b.z = 1

console.log(b.x, b.y, b.z) // => 1, 2, 1
console.log(a.x, a.y) // => 1, 1

这里b的原型对象a.x是不可修改不可设置的,所以当对b进行属性操作时,对于其继承属性的操作是直接被忽略掉的。

一个重要的JS特性

查询属性时,会用到原型链;设置属性时,不影响原型链。

属性访问错误

属性访问错误的两种情况:

  • 对象不存在,直接报异常TypeError:undefined
  • 属性不存在,返回undefined

几种防止此类问题发生的方法:

  • 简单ififif
  • 用&&连接
  • 使用?.条件式访问
1
2
3
4
5
let book = { author: { name: '李松峰' } }
let name = book && book.author && book.author.name
//或
let name1 = book?.author?.name
console.log(name)

小结

在对象o中设置属性p在以下情况会失败:

  • o有一个只读自有属性p
  • o有一个只读继承属性p
  • o没有自有&继承属性p,且o的extensible特性为false(不可扩展,不能添加新属性)

删除属性

主打一个delete

delete操作符

只能删除对象的自有属性,不能删除继承属性,不能删除configurable为false的属性

在非严格模式下
1
2
3
4
5
6
7
8
9
let o = { x: 1 }
delete o.x // true
delete o.x // 不操作但true
delete o.继承属性 // 不操作但true
delete 1 // 无意义但true
delete 不可配置的属性 // false
delete globalThis.x(不可配置的) // false
delete globalThis.x(可配置的) // true
delete x // true↑
在严格模式下
1
2
3
delete 不可配置的属性 // TypeError
delete globalThis.x(可配置的) // true
delete x // SyntaxError↑

测试属性

在实际开发中,我们往往要查一下某对象是否有某属性,这个时候就可以根据场景选用以下四种方式↓

1
2
// 炒一些栗子
let o = {x: 1, y: undefined} //且继承toString属性

!==

这种方法可以粗略判断对象中是否有自有属性和继承属性,但不全面——它不会区分 不存在的和值本身为undefined的

1
2
3
4
o.x !== undefined // => true
o.y !== undefined // => false 看吧~它分不清undefined是什么个undefined原因
o.z !== undefined // => false
o.toString !== undefined // => true

!== 经常与 in 搭配使用,后者用来查询属性,前者用来判断是否为undefined

p in o

这种方法查询自有属性和继承属性,可以区分undefined和不存在

1
2
3
4
x in o // => true
y in o // => true
z in o // => false
toString in o // => true

o.hasOwnProperty(‘p’)

这种方法查询自有属性

1
2
3
4
o.hasOwnProperty('x') // => true
o.hasOwnProperty('y') // => true
o.hasOwnProperty('z') // => false
o.hasOwnProperty('toString') // => false 无法判断继承属性捏

o.propertyIsEnumerable(‘p’)

这个方法细化了上一个方法,它查询的是可枚举 enumerable 的自有属性

1
2
3
4
5
o.propertyIsEnumerable('x') // => true
o.propertyIsEnumerable('y') // => true
o.propertyIsEnumerable('z') // => false
o.propertyIsEnumerable('toString') // => false 非自有属性
Object.prototype.propertyIsEnumerable('toString') // => false 非可枚举属性

枚举属性

  • 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
2
3
for (let p in o) {
if (!o.hasOwnProperty(p)) continue
}

使用for/in和typeof跳过所有方法属性

1
2
3
for (let p in o) {
if (typeof o[p] === 'function') continue
}

取得对象属名称函数

这些函数往往与for/in搭配使用来获取对象名称数组。

Object.keys()

返回对象 可枚举+自有属性名 数组

Object.getOwnPropertyNames()

返回对象 自有属性+为字符串属性名 数组

Object.getOwnPropertySymbols()

返回对象 自有属性名 数组

Reflect.ownKeys()

返回对象 所有属性名 数组

属性枚举顺序

相关方法枚举属性顺序
上述四个函数以及JSON.stringfy()等方法
  1. 属性名为非负整数字符串,按照数值升序(这也意味着数组和类数组对象属性会按照顺序被枚举)
  2. 属性名为其他字符串,按照先后顺序
  3. 属性名为符号对象,按照先后顺序
for/in循环枚举顺序不那么严格
  1. 先按照上述顺序枚举自有属性
  2. 再按照上述顺序枚举继承属性
  3. 这里需要注意的是:当已经有同名属性枚举过或者说一个同名属性是不可枚举的,那么这个属性就不会被枚举了

默认情况下,符号属性是不可枚举的。这意味着它们不会出现在对象属性的 for…in 循环中,也不会被包括在 Object.keys() 和 Object.getOwnPropertyNames() 方法的返回结果中。

扩展对象

指一个对象的属性复制到另一个对象上。

原始且常见的操作

1
2
3
4
let o1 = {x: 1}, o1 = {y: 2, z: 3}
for (let key of Object.keys(o2)) {
o1[key] = o2[key]
}

↑各框架纷纷定义类似于extends()的辅助函数,es6以Object.assign()的形式进入核心JS语言

Object.assign()

参数

传两种参数,第一个参数是目标对象;第二个以及以后的参数是一个个来源对象。

1
Object.assign(目标对象, 来源对象*n)
方式

这个函数将这些 来源对象 的 可枚举自有属性 有顺序地(例如后续属性会覆盖先前同名属性)复制到目标对象中。

这个函数在执行期间,如果来源对象有获取方法/目标对象有设置方法,那么这些方法会被调用,当然它们不会被复制。

将属性从一个对象分配给另一个对象

这种做法的原因是,如果有一个默认对象里有很多属性和它们的默认值,且对象中不存在同名属性,那么就可以将这些默认值复制到一个对象中。

实现↓

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let defaults = {...} // 这是一个默认对象 
let o = {...} // 这是一个对象

// 第一种:简单创建一个新对象并且赋值
o = Object.assign({}, defaults, o)
// 第二种:使用扩展操作符...
o = {...defaults, ...o}

// 第三种:重写一个类Object.assign()的辅助方法
// 创建一个merge函数,这个函数与Object.assign类似,但是不会覆盖已存在的属性,避免了额外对象的创建和复制。
function merge(target, ...sources) {
for (let source in sources) {
for (let key of Object.keys(source)) {
if (!(key in target)) {
target[key] = source[key]
}
}
}
return target
}
merge(o, defaults) // => √
拓展-重写一些辅助函数

merge()——与Object.assign()类似,但不会覆盖已经存在的属性(书中)

restrict()——从一个对象中删除另一个对象中没有的属性(自己试着写的)

subtract()——从一个对象中删除另一个对象中包含的所有属性(↑)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 注意这里不是连续操作,传参皆为以下初始值
let dobj = {x: 1, y: 2, z: 3}
let obj = {x: 3}

function merge(target, ...sources) {
for (let source of sources) {
for (let key in source) {
if (!(key in target)) {
target[key] = source[key]
}
}
}
return target
}
console.log( "merge", merge(obj, dobj) ) // => {x: 1, y: 2, z: 3}

function restrict(target, ...sources) {
for (let source of sources) {
for (let key in source) {
if (key in target) {
target[key] = source[key]
}
}
}
return target
}
console.log( "restrict", restrict(obj, dobj) ) // => {x: 1}

function subtract(target, ...sources) {
// target: dobj, sources: obj
for (let source of sources) {
for (let key in source) {
if (key in target) {
delete target[key]
}
}
}
return target
}
console.log( "subtract", subtract(dobj, obj) ) // => {y: 2, z: 3}

序列化对象

对象序列化

对象序列化(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
2
3
4
5
6
7
8
let point = {
x: 1,
y: 2,
toString: function() {
return `(${this.x}, ${this.y})`
}
}
console.log(point.toString()) // => (1, 2)
toLoocalString()

该方法默认是没有实现任何本地化操作,只是简单调用了一下toString()方法。因此一般需要根据本地惯例格式化数值、日期、时间等↓

1
2
3
4
5
6
7
8
9
10
11
12
13
let point = {
x: 1000,
y: 2000,
toString: function() {
return `(${this.x}, ${this.y})`
},
toLocaleString: function() {
return `(${this.x.toLocaleString()}, ${this.y.toLocaleString()})`
}
}

console.log(point.toString()) // => (1000, 2000)
console.log(point.toLocaleString()) // => (1,000, 2,000)
valueOf()

该方法默认也并没有做什么。因此一些内置类定义了自己的valueOf()方法。

1
2
3
4
5
6
7
8
let point = {
x: 3,
y: 4,
valueOf: function() {
return Math.hypot(this.x, this.y)
}
}
console.log(Number(point))

Date类定义的valueOf()可以将日期转换为值,这样一来就能进行比较。

toJSON()——不太理解

Object.property实际没有定义该方法,但JSON.stringify()方法会从要序列化的对象中寻找toJSON()方法。如果该方法存在,就调用然后序列化该方法的返回值,而非原始对象。

1
2
3
4
5
6
7
8
9
10
11
let point = {
x: 1,
y: 2,
toString: function() {
return `(${this.x}, ${this.y})`
},
toJSON: function() {
return this.toString()
}
}
JSON.stringify([point]) // => '["(1, 2)"]'

Date类定义了自己的toJSON()方法,返回一个表示日期的序列化字符串。

对象字面量扩展语法

简写属性

ES6之后可以删掉其中分号和一份标识符,使得代码更为简洁(非常常用)。

compare↓

1
2
3
4
5
6
7
let x = 1, y = 2
let o1 = {
x: x,
y: y
}
-----------------
let o2 = { x, y }

计算的属性名

ES6之后使用计算属性就非常讨巧了。因为可以直接在一个对象中使用中括号[]去框计算属性,使得计算结果(必要转字符串)直接作为对象属性名。

compare↓

1
2
3
4
5
6
7
8
9
10
11
const pName = 'herName'
function computePName () { return 'hisName' }

let o1 = {}
o1[pName] = '栗子'
o1[computePName] = '纸鹤'
-----------------------
let o2 = {
[pName]: '栗子',
[computePName]: '纸鹤'
}
计算属性使用场景
  • 书中提到的一种:假设我有一个JS代码库,我需要传给此库一组特定属性对象,而这组属性的名字在库中都是以常量的形式定义的。如果要创建传给此库的对象,可以硬编码他们的属性名,但是这样很有可能把属性名写错,而且也存在更新迭代修改属性名而出错。那么这时候不妨用一层计算属性去装这些常量,来组成这个对象。
  • 数据过滤:按照一定的条件对数据进行过滤
  • 数据排序:按照一定顺序对数据排序,这个时候就可以使用计算属性对数据集进行动态排序
  • 数据格式化:使用计算属性格式化数据,最常见的可能是数字和日期以及字符串……都挺常见的。
  • 数据聚合:比如说一些加和,一些拼接……

符号作为属性名

ES6后符号也可以作为属性名(也多亏了计算属性语法)。

  • Symbol除了用作属性名以外不能做任何事。
  • 为啥Symbol()函数是工厂函数而不是构造函数?——该函数返回值是原始值而不是对象,因此不需要new一个对象实例。
  • 符号是一种安全机制吗?——nono,符号不是为了安全,而是为JS对象定义安全的扩展机制。假设我们要从不受控的第三方代码中得到一个对象,我们担心创建一个属性会使得对象与原对象产生冲突,或者担心第三方代码以外修改我们命名的属性,那这个时候用Symbol非常合适。当然,第三方代码完全可以通过Object.getOwnPropertySymbols()方法找到我们的符号并且修改删除qvq,这也是符号并非一种安全机制的原因。

扩展操作符

扩展操作符在我们之前使用Object.assign()时提到过,这是一种简便方法,可以将对象的属性复制到新对象中,这是一种典型的插值行为,当然只在对象字面量中才是这种行为(post参数设置的时候会用到,这个栗子 来源于我最近在学做的项目中)。

  • 扩展操作符不是严格意义上的操作符,是一种语法
  • 扩展操作符只能扩展对象的自有属性
  • 扩展操作符虽说看着很简洁,但是会给JS解释器带来巨大工作量。基于循环/递归 ( O(n) ) 不断给对象追加属性,倘若操作量很大的话,时间复杂度是非常高的,近似于 O(n^2) 。

简写方法

ES6之后对象里函数可以省去function

简写和其他类型的属性名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let o1 = {
qvq: 'qvq',
func: function() { return this.qvq }
}
------------等价-------------
let o2 = {
qvq: 'qvq',
func() { return this.qvq }
}
---属性名还可以是更时髦的形式---
let METHOD_NAME = 'm'
const symbol = Symbol()
let o3 = {
[METHOD_NAME](x) { return x++ },
[symbol](x) { return x-- },
"ah"(x) { return 'aaa'+x+'hhh' },
}
o3[symbol](1) // => 0

字符串字面量和计算属性都可以作为对象中方法属性名;

使得包含Symbol属性的对象也可迭代-Generator&Symbol.iterator

符号也可作为方法名,为了让对象可迭代,必须以符号名Symbol.iterator为它定义一个方法↓

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const obj = {
[Symbol("customMethod")]: function() {
console.log("This is a custom method using Symbol");
},
*[Symbol.iterator]() {
let keys = Object.keys(this);
for (let key of keys) {
yield this[key];
}
}
};

obj.name = "John";
obj.age = 30;
obj.address = "123 Main St";

for (let value of obj) {
console.log(value);
}

// Output:
// This is a custom method using Symbol
// John
// 30
// 123 Main St

在上面的代码中,我们使用了Generator函数定义Symbol.iterator属性*[Symbol.iterator],该函数返回迭代器对象。该迭代器对象通过遍历对象中所有属性并返回属性值,从而实现了可迭代性。这样一来这个Symbol方法名的方法的属性也可迭代了。

值得注意Generator函数在这里使用了*作为函数声明符号。这是定义Generator函数的一种特殊语法。这种语法将函数定义为一种生成器,使得其具有 短暂暂停/恢复执行 的能力,从而使其能生成一个可迭代对象。

属性的获取/设置方法

对象的属性除了数据属性,还有访问器属性accessor property

  • 对象属性:属性名&属性值
  • 访问器属性:一个俩的访问器方法——获取方法 getter / 设置方法 setter
访问器属性
访问器属性用法

属性名:方法名 属性值:方法返回值 ES6之后也可以使用计算的属性名来定义获取/设置方法(相当炸裂)

  • 程序查询一个访问器属性的时候,通常会调用获取方法(不传参),这个方法返回值即为属性访问表达式的值。
  • 程序设置修改一个访问器属性时,通常会调用设置方法(需传参),这个方法是用来设置属性的值,忽略返回值。

可读可写,只读,只写(这种属性通过数据属性无法实现,返回值undefined)——qvq好玩吧~

访问器属性可以通过一个字面量扩展语法定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 2D笛卡尔坐标,极坐标
let o = {
x: 1.0,
y: 1.0,
// 可读可写
get r() { return Math.hypot(this.x, this.y) },
set r(newvalue) {
let oldvalue = Math.hypot(this.x, this.y)
let radio = newvalue / oldvalue
this.x *= radio
this.y *= radio
}
// 只读
get theta() { return Math.atan2(this.y, this.x) }
}

p.r // => Math.SQRT2
p.theta // => Math.PI/4
访问器属性是可继承的捏!

小结

本章可谓是非常核心的一章,未来还需要我在通读和理解后续内容后与之融会贯通。

简单列一下本章大纲,方便回顾↓

  • 对象及相关基本概念
  • 对象字面量语法
  • 对象属性的读/写/删/查/枚举
  • 原型的继承
  • 对象的扩展
  • JS中所有非原始值的值都是对象。