easycodesniper

blog by chen qiyi

读JS红宝书

for-in 和 for-of

for-in

for-in语句用于枚举对象中的非符号键属性,for-in语句不能保证返回对象属性的顺序

语法:

1
2
3
for ( const key in obj ) {
// ...
}

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let obj = {
name: 'cqy',
age: 11,
hobby: ['play', 'code', 'sleep'],
family: {
mom: 'mother',
father: 'father'
},
isMale: true,
[Symbol()]: 'tag'
}

for( const key in obj ) {
console.log( key );
}

// name
// age
// hobby
// family
// isMale

for-of

for-of语句用于遍历可迭代对象的属性,会按照可迭代对象的next()方法产生值的顺序迭代元素

语法:

1
2
3
for ( const key of iterator ) {
// ...
}

使用示例:

1
2
3
for ( const el of [1,2,3,4] ) {
console.log(el)
}

执行上下文与作用域

变量或函数的上下文决定了它们可以访问哪些数据,每个上下文都有一个关联的变量对象,这个上下文中定义的所有变量和函数都存在于这个对象上。

上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数。

上下文栈:
每个函数调用都有自己的上下文。当代码执行流进入函数,函数的上下文被推到一个上下文栈上。在函数执行完后,上下文栈会弹出该函数上下文,将控制权还给之前的执行上下文。

作用域链:
上下文中的代码在执行时,会创建 变量对象 的一个 作用域链。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。

正在执行的上下文的 变量对象 始终位于作用域链的最前端(即最先被访问)

代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。搜索从作用域链的最前端开始,逐级往后,直到找到标识符。

原始值包装类型

为了方便操作原始值,ES提供了3种特殊的引用类型:BooleanNumberString

每当用到某个原始值的方法或属性时,后台都会创建一个相应原始包装类型的对象,从而暴露出操作原始值的各种方法

1
2
3
4
5
let name = 'easy code sniper'

let arr = name.split(' ')

console.log(arr); // [ 'easy', 'code', 'sniper' ]

在这里 name 是一个字符串类型的变量,是一个原始值。但却可以调用split方法。原始值本身不是对象,因此逻辑上不应该有方法。

name原始值能够调用split方法,是后台进行了许多处理。在以读模式(读取变量保存的值)访问字符串值的任何时候:

  • 后台会创建一个String类型的实例
  • 调用实例上的特定方法
  • 销毁实例

可以将上述3步想象成执行了如下3行代码:

1
2
3
let name = new String('easy code sniper')
let arr = name.split(' ')
name = null

这种行为可以让我们在原始值上调用对象的方法。对于布尔值和数值,也是执行以上步骤,只不过使用的是BooleanNumber包装类型而已。

这种自动创建的原始值包装对象只存在于访问它的那行代码执行期间,这意味着不能在运行时给原始值添加属性和方法

Array.from 和 Array.of

Array.from

Array.from用于将 类数组结构 转换为 数组。第一个参数是一个类数组对象(任何可迭代的结构),或者有一个length属性和可索引元素的结构

使用场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 字符串会被拆分为单字符数组
*/
console.log(Array.from('cqy')); // [ 'c', 'q', 'y' ]

/**
* 对现有数组进行浅拷贝
*/
let arr1 = [1, 2, { name: 'cqy' }]

let arr2 = Array.from(arr1)

arr2[2].name = 'easy code sniper'

console.log(arr1); // [ 1, 2, { name: 'easy code sniper' } ]

/**
* 将arguments转化为数组
*/
function func() {
console.log(Array.from(arguments));
}

func(1,2,3) // [ 1, 2, 3 ]

Array.from()还接受 第二个可选的映射函数参数,新数组的每一项都会调用这个映射函数,并将每一项作为参数传入映射函数。
还可以接受 第三个可选参数 ,用于指定映射函数中this的值(箭头函数中不适用

1
2
let arr = [1, 2, 3, 4]
let arr1 = Array.from(arr, function(x) { return x * this.num }, { num: 2 }) // [ 2, 4, 6, 8 ]

Array.of

Array.of用于将一组参数转换为数组实例

数组迭代方法

ES为数组定义了5个迭代方法。每个方法接收两个参数:

  • 以每一项为参数运行的函数,该函数接收3个参数:数组元素、索引、数组本身
  • 可选的 作为函数运行上下文的作用域对象(影响函数中的this的值)

5个迭代方法如下:

  • every:对数组的每一项都运行传入的函数,如果每一项函数都返回true,则这个方法返回true
  • filter:对数组每一项都运行传入的函数,函数返回true的项会组成数组之后返回
  • forEach:对数组每一项都运行传入的函数,没有返回值
  • map:对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组
  • some:对数组每一项都运行传入的函数,如果有一项函数返回true,则这个方法返回true

数组归并方法

ES为数组定义了2个归并方法:reduce()reduceRight()。这两个方法都会迭代数组的所有项,并在此基础上构建一个最终返回值。

reduce()方法从数组第一项开始遍历到最后一项,而reduceRight()从最后一项开始遍历到第一项

这两个方法都接收两个参数:

  • 对每一项都会运行的 归并函数
    • 归并函数接收4个参数:上一个归并值、当前项、当前项索引、数组本身
    • 归并函数的返回值会作为下一次调用本归并函数的第一个参数(如果没有给这两个方法传入可选的第二个参数(作为归并起点值),则第一次迭代将从数组的第二项开始,因此传给归并函数的 第一个参数是数组的第一项 ,第二个参数是数组的第二项)
  • 可选的 以之为归并起点的初始值
1
2
3
4
5
let arr = [1,2,3,4,5]

let sum = arr.reduce((prev, current, index, array) => prev + current)

console.log(sum); // 15

迭代器

循环是迭代机制的基础,它可以指定迭代的次数,以及每次迭代要执行什么操作。

但循环又不适用于所有的数据结构。首先数组可以通过[]操作符取得特定索引位置上的项,这并不适用于所有数据结构;其次通过递增索引来访问数据是特定于数组的方式,并不适用于其他具有隐式顺序的数据结构

Array.prototype.forEach方法向通用迭代需求迈进了一步,解决了单独记录索引和通过数组对象取得值的问题。但是无法标识迭代何时终止,且回调结构也比较笨拙

迭代器模式

迭代器模式把有些结构称为「可迭代对象(iterable)」,因为它们实现了正式的Iterable接口,而且可以通过迭代器Iterator消费

可迭代对象是抽象的说法,可以将它理解成数组、集合类型的对象 或者 具有类似数组行为的其他数据结构。它们包含的元素都是有限的,而且都具有无歧义的遍历顺序。

迭代器(iterator)是按需创建的一次性对象,每个迭代器都会关联一个可迭代对象,并且迭代器会暴露用于迭代 可迭代对象 的API。迭代器无须了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值

可迭代对象必须暴露一个属性作为「默认迭代器」,这个属性使用特殊的Symbol.iterator作为键。这个默认迭代器属性引用一个迭代器工厂函数,调用这个工厂函数返回一个新迭代器

很多内置类型都实现了Iterable接口:

  • 字符串
  • 数组
  • 映射
  • 集合
  • arguments对象
  • NodeList等DOM集合类型

检查是否存在默认迭代器属性:

1
2
3
4
5
6
7
8
9
10
11
12
// 没有实现迭代器工厂函数
let num = 1
console.log(num[Symbol.iterable]) // undefined

// 实现了迭代器工厂函数
let str = 'cqy'
console.log(str[Symbol.iterator]); // [Function: [Symbol.iterator]]

//调用这个工厂函数会生成一个迭代器
let str = 'cqy'
console.log(str[Symbol.iterator]()); // Object [String Iterator] {}

在实际开发中,不需要显式调用这个工厂函数来生成迭代器,一些原生语言结构会在后台调用可迭代对象的工厂函数

  • for-of循环
  • 数组解构
  • 扩展操作符
  • Array.from()
  • 创建集合、映射
  • Promise.all()Promise.race()
  • yield*操作符

迭代器API使用next()方法在可迭代对象中遍历数据,每次成功调用next(),都会返回一个IteratorResult对象,其中包括:迭代器返回的下一个值valuedone状态(done为true 表示「耗尽」,即没有下一个值了)

1
2
3
4
5
6
7
8
let arr = ['aa', 'bb', 'cc']

let iter = arr[Symbol.iterator]()

console.log(iter.next()); // { value: 'aa', done: false }
console.log(iter.next()); // { value: 'bb', done: false }
console.log(iter.next()); // { value: 'cc', done: false }
console.log(iter.next()); // { value: undefined, done: true }

每个迭代器都表示对可迭代对象的一次性有序遍历。不同迭代器的实例相互之间没有联系,只会单独的遍历可迭代对象

自定义迭代器

任何实现Iterator接口的对象都可以作为迭代器使用,下面的例子中的Counter类只能被迭代一定的次数

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
class Counter {
constructor(limit) {
this.count = 1
this.limit = limit // 限制迭代的次数
}

next() {
if(this.count <= this.limit) {
return { done: false, value: this.count++ }
} else {
return { done: true, value: undefined }
}
}

[Symbol.iterator]() { // 返回实例,实例中定义了next方法用于迭代
return this
}
}

let counter = new Counter(3)

// 当使用for-of循环counter时,后台会去查找counter身上是否有 [Symbol.iterator]() 工厂函数,并调用它,
for(const i of counter) {
console.log(i); // 1 2 3
}

这个类实现了Iterator接口,但是它的每个实例只能被迭代一次。第二次迭代没有输出的原因是,第一次迭代已经使count === limit,所以第二次迭代不会返回任何内容

1
2
3
4
5
6
7
8
for(const i of counter) {
console.log(i); // 1 2 3
}


for(const i of counter) {
console.log(i); // 没有输出
}

要实现可以创建多个迭代器,必须美创建一个迭代器就对应一个新计数器。基于此需求,可以把计数器变量放到闭包中,通过闭包返回迭代器

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
class Counter {
constructor(limit) {
this.count = 1
this.limit = limit
}

[Symbol.iterator]() {
let count = 1
let limit = this.limit

return {
next() {
if(count <= limit) {
return { done: false, value: count++ }
} else {
return { done: true, value: undefined }
}
}
}
}
}

let counter = new Counter(3)

for(const i of counter) {
console.log(i); // 1 2 3
}

for(const i of counter) {
console.log(i); // 1 2 3
}

提前终止迭代器

提前终止迭代也是一个常见的需求,for-of循环可以通过break、continue、return或者throw提前退出,在退出时会寻找实现Iterator接口的对象上是否有return()方法。如果对象上有这个方法,在退出时就会调用这个方法

return()方法必须返回一个有效的IteratorResult对象,可以只简单的返回{ done: true }

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
class Counter {
constructor(limit) {
this.count = 1
this.limit = limit
}

[Symbol.iterator]() {
let count = 1
let limit = this.limit

return {
next() {
if(count <= limit) {
return { done: false, value: count++ }
} else {
return { done: true, value: undefined }
}
},

return() { // 自定义提前退出迭代方法
console.log('Exiting iter');
return { done: true }
}
}
}
}

let counter = new Counter(3)

for(const i of counter) {
if(i > 1) {
break
}
console.log(i);
}
// 1
// Exiting iter

生成器

生成器是 ES6 新增的结构,可以让函数拥有在函数块内暂停和恢复代码执行的能力

生成器的形式是一个函数,函数名称前面加一个星号( * )表示它是一个生成器,标识生成器函数的星号( * )不受两侧空格的影响:

1
2
3
4
5
6
7
function * generatorFn() {}
function* generatorFn() {}
function *generatorFn() {}

let generatorFn = function* () {}

console.log(generatorFn); // [GeneratorFunction: generatorFn]

调用生成器函数会产生一个生成器对象,生成器对象一开始处于暂停执行的状态。

生成器对象也实现了Iterator接口,因此具有next()方法,调用这个方法会让生成器开始或恢复执行

next()方法的返回值类似于迭代器,有一个done属性和一个value属性。函数体为空的生成器函数中间不会停留,调用一次next()就会让生成器到达done: true状态

1
2
3
4
5
6
7
function * generatorFn() {}

console.log(generatorFn()); // Object [Generator] {}

console.log(generatorFn().next); // [Function: next]

console.log(generatorFn().next()); // { value: undefined, done: true }

value属性是生成器函数的返回值,默认是undefined,可以通过生成器函数的返回值来指定

生成器函数只会在初次调用next()方法后开始执行

1
2
3
4
5
6
7
function * generatorFn() {
return 'easy code sniper'
}

console.log(generatorFn()); // 不会打印 easy code sniper

console.log(generatorFn().next()); // { value: 'easy code sniper', done: true }

yield中断执行

yield关键字可以让生成器停止和开始执行,也是生成器最有用的地方。生成器函数在遇到 yield 关键字之前会正常执行。遇到这个关键字后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用next()方法来恢复执行

yield关键字有点像函数的中间返回语句,它生成的值会出现在next()方法返回的对象中

与 return 的区别:通过yield关键字退出的生成器函数处于done: false状态;通过return关键字退出的生成器函数处于done: true状态

1
2
3
4
5
6
7
8
9
10
11
12
function * generatorFn() {
yield 'cqy';
yield 'kyrie';
return 'easy code sniper'
}

let generatorObj = generatorFn()

console.log(generatorObj.next()); // { value: 'cqy', done: false }
console.log(generatorObj.next()); // { value: 'kyrie', done: false }
console.log(generatorObj.next()); // { value: 'easy code sniper', done: true }

显式的调用next()方法用处不大,但是可以在需要自定义迭代对象时,把生成器对象当作可迭代对象使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
function * generatorFn() {
yield 'cqy';
yield 'kyrie';
yield 'easy code sniper'
}

for(const item of generatorFn()) {
console.log(item);
}

// cqy
// kyrie
// easy code sniper

理解对象

JS使用一些内部特性来描述对象的属性的特征,对象的属性分两种:数据属性 和 访问器属性

数据属性包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。数据属性有4个特性描述它们的行为:

  • [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认是 true
  • [[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下是 true
  • [[Writable]]:表示属性的值是否可以被修改。默认情况下是 true
  • [[Value]]:属性实际的值。默认是 undefined

使用Object.defineProperty()方法可以修改属性的默认特性。

该方法接收3个参数;要给其添加属性的对象、属性名称 和 一个描述符对象(描述符对象上的属性可以包含:configurable、enumerable、writable、value)

1
2
3
4
5
6
7
8
9
10
11
12
13
let obj = {}

// 为 obj 创建一个 name 属性,并配置为不可修改
Object.defineProperty(obj, 'name', {
value: 'easy code sniper',
writable: false
})

console.log(obj.name); // easy code sniper

obj.name = 'cqy'

console.log(obj.name); // easy code sniper

访问器属性不包含数据值。它包含一个获取函数(getter)和一个设置函数(setter)。在读取访问器属性时,会调用getter,这个函数的责任就是返回一个有效的值。在写入访问器属性时,会调用setter函数并传入新值,这个函数必须对数据做出修改

访问器属性有4个特性描述它们的行为:

  • [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认是 true
  • [[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下是 true
  • [[Get]]:获取函数
  • [[Set]]:设置函数

使用Object.defineProperty()方法可以定义访问器属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义 _name 为 伪私有变量,通过 name 属性去访问和设置 _name 属性
let obj = {
_name: 'easy code sniper'
}

Object.defineProperty(obj, 'name', {
get() {
return this._name
},
set(newValue) {
this._name = newValue
}
})

console.log(obj.name); // easy code sniper

obj.name = 'cqy'

console.log(obj.name); //cqy

理解原型

构造函数 和 原型对象

只要创建一个函数,就会按照规则为该函数创建一个prototype属性,该属性指向原型对象

所有的原型对象自动获得一个名为constructor属性,指回与之关联的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person() {}

console.log(Person.prototype);

// 输出
// {
// constructor: f Person(),
// __proto__: Object
// }

console.log(Person.prototype.constructor); // f Person()

console.log(Person.prototype.constructor === Person); // true

实例 和 原型对象

每次调用构造函数创建一个实例,实例内部的[[Prototype]]就会被赋值为 构造函数的原型对象

实例通过__proto__属性可以访问到构造函数的原型

关键在于理解:实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有

1
2
3
4
5
6
7
function Person() {}

let person = new Person()

console.log(person.__proto__ === Person.prototype); // true

console.log(person.__proto__.constructor === Person); // true

isPrototypeOf()可以确定两个对象之间的关系。本质上,isPrototypeOf()会在传入参数的[[Prototype]]指向调用它的对象时返回 true

1
console.log(Person.prototype.isPrototypeOf(person)) // true

Object.getPrototypeOf()方法可以返回参数的内部特性[[Prototype]]

1
console.log(Object.getPrototypeOf(person) == Person.prototype)

Object.setPrototypeOf(obj, prototype)方法可以向实例的私有特性[[Prototype]]写入一个新值,即重写原型

1
2
3
4
5
6
7
8
9
function Person() {}

let person = new Person()

function Pig() {}

Object.setPrototypeOf(person, Pig)

console.log(Object.getPrototypeOf(person)); // [Function: Pig]

in 操作符
in 操作符会在可以通过对象访问指定属性时返回 true,无论该属性是在实例上还是在原型上

1
2
3
4
5
6
7
8
function Person() {}

Person.prototype.name = 'cqy'

let person = new Person()

console.log(person.hasOwnProperty('name')); // false
console.log('name' in person); // true