QX-AI
GPT-4
QX-AI初始化中...
暂无预设简介,请点击下方生成AI简介按钮。
介绍自己
生成预设简介
推荐相关文章
生成AI简介

前言

一直没有把ES6标准系统地进行学习,虽然很多新特性已经用上了,但也该系统地学习一下

还是阮一峰讲得全面些:ECMAScript 6 入门 | 阮一峰 很多新特性,多用就记住了

ES6简介

ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言

ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准

let和const

let 声明变量,const声明常量(内存地址所保存的数据不得改动)

1、块级作用域

1
2
3
4
5
6
var a = 2;
{
let a = 1;
console.log(a); // 1
}
console.log(a); // 2

2、同一个块内不允许覆盖、重复声明

1
2
3
4
5
6
7
8
9
var a = 1;
var a = 2;
console.log(a); // 2
let b = 1;
let b = 2; // 'b' has already been declared
{
let b = 2;
console.log(b); // 2
}

3、没有变量提升

1
2
3
4
console.log(a); // undefined
var a = 1;
console.log(b); // Cannot access 'b' before initialization
let b = 2;

4、暂时性死区
只要块级作用域内存在let命令,它所声明的变量就绑定这个区域,不再受外部的影响

只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

变量一定要在声明之后使用
1
2
3
4
5
console.log(typeof a); // undefined
console.log(typeof b); // undefined
var b = 1;
console.log(typeof x); // ReferenceError
let x = 2;

Tip:
1、循环中的 var 与 let:

var 会提升进行声明,数组中所有函数访问的都是同一个 i

1
2
3
4
5
6
7
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10

变量 i 是 let 声明的,当前的 i 只在本轮循环有效,所以每一次循环的 i 都是一个新的变量,但在循环中,JavaScript 引擎会记住每一次 let 的值,下一次创建的时候,直接在这个值上递进

1
2
3
4
5
6
7
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6

2、const 声明的对象仍然是可写的,可以使用 Object.freeze() 冻结对象

当对象嵌套时,需要递归冻结

1
2
3
4
5
6
7
8
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};

解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值

数组解构

1、只要等号两边的模式相同,左边的变量就会被赋予对应的值

1
2
3
let [a, b, c, ...d] = [1, 2, 3, 4, 5, 6, 7, 8];
console.log(a); // 1
console.log(d); // [ 4, 5, 6, 7, 8 ]

2、如果解构不成功,变量的值就等于undefined

1
2
3
4
let [a] = [];
console.log(a); // undefined
let [b, c] = [1];
console.log(c); // undefined

3、解构赋值允许指定默认值,只有当一个数组成员严格等于undefined,默认值才会生效

1
2
let [b, c = 2] = [1];
console.log(c); // 2

如果一个数组成员是null,默认值就不会生效,因为null不严格等于undefined

1
2
3
4
let [x = 1] = [undefined];
x // 1
let [x = 1] = [null];
x // null

如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值

1
2
3
4
5
function f() {
console.log('123');
}
let [a = f()] = [1]; // 不执行f()
let [b = f()] = []; // 123

对象解构

1、解构提取对象中的属性

1
2
3
4
5
6
const obj = {
a: 1,
b: 2,
}
let { a, b } = obj // 变量名与属性名需相同
console.log(a) // 1

变量名与属性名不一致,需写成下面这样

1
2
3
4
5
const obj = {
a: 1
}
let { a : x } = obj
console.log(x) // 1

2、嵌套结构的对象

1
2
3
4
5
6
7
8
9
const obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p, p: [x, { y }] } = obj;
console.log(x); // Hello
console.log(y); // World

3、剩余运算符,将其它属性展开到一个对象中存储

1
2
3
4
5
6
7
8
9
10
const obj = {
a: 1,
b: 2,
}
const a = {
...obj,
b: 3, // 可以覆盖
c: 4, // 默认值
}
console.log(a.a, a.b, a.c); // 1 3 4

4、可以取到继承的属性

1
2
3
4
5
const obj1 = {};
const obj2 = { a: 1 };
Object.setPrototypeOf(obj1, obj2);
const { a } = obj1;
console.log(a); // 1

函数参数解构

函数的参数也可以使用解构赋值

1
2
3
4
5
function add([x, y]){
return x + y;
}
add([1, 2]); // 3
[[1, 2], [3, 4]].map(([a, b]) => a + b); // [ 3, 7 ]

技巧

1、交换变量的值

1
2
3
let x = 1;
let y = 2;
let [x,y] = [y,x];

2、函数返回多个值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 返回一个数组
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();

// 返回一个对象
function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();

函数的扩展

1、带参数默认值的函数

1
2
function f(a, b = 1) { }
function f(a, b = fun()) { }

2、rest 参数
获取函数的多余参数,不需要使用 arguments 对象,rest 参数之后不能再有其他参数

1
2
3
4
function f(a, ...b) {
console.log(a, b); // 1 [2, 3]
}
f(1, 2, 3);

3、name 属性
返回函数名

1
2
function foo() {}
foo.name // "foo"

箭头函数

使用“箭头”(=>)定义函数

1
2
3
4
5
var f = v => v;
// 等同于
var f = function (v) {
return v;
};

多种写法

1
2
3
var f = v => v;
var f = v => { return v };
var f = (a, b) => a + b;

简化回调函数

1
2
3
4
[1,2,3].map(function (x) {
return x * x;
});
[1,2,3].map(x => x * x);

箭头函数的特性:
1、没有自己的 this
2、不可以当作构造函数
3、不可以使用 arguments 对象
4、不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数

对象的扩展

1、属性简洁表示

1
2
3
4
5
6
let [a, b] = [1, 2];
const obj = {
a,
b,
fun(){ } // 简写对象方法
}

2、属性名表达式
属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串
属性名表达式与简洁表示法,不能同时使用

1
2
3
4
5
6
obj['a' + 'bc'] = 123; // obj.abc = 123
let propKey = 'foo';
let obj = {
[propKey]: true,
['a' + 'bc']: 123
};

Object方法

1、is()
判断两个值是否相同,解决了 NaN 问题

1
2
3
Object.is(null,undefined); // false
Object.is(NaN,NaN); // true
NaN === NaN // false

2、assign()
合并多个对象,并返回第一个参数的引用,冲突属性后面覆盖前面

1
2
3
4
let obj1 = { a: 1 }
let obj2 = { a: 2, b: 3 }
let obj3 = Object.assign(obj1, obj2); // {a: 2, b: 3}
console.log(obj3 === obj1); // true

3、keys() 返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历属性的键名

4、values() 返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历属性的键值

5、entries() 返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历属性的键值对数组

6、fromEntries() entries() 的逆操作,用于将一个键值对数组转为对象

7、hasOwn() 判断某个属性是否为自身的属性

数组的扩展

1、扩展运算符 ...
扩展运算符将一个数组转为用逗号分隔的参数序列

1
2
3
4
console.log(...[1, 2, 3]) // 1 2 3
console.log(1, ...[2, 3, 4], 5) // 1 2 3 4 5
function add(x, y) { return x + y }
add(...[4, 38]) // 42

替代用到 apply() 和 push() 的某些操作

1
2
3
4
5
6
Math.max.apply(null, [11, 33, 22]) // 33
Math.max(...[11, 33, 22]) // 33
let arr1 = [0, 1, 2]
let arr2 = [3, 4, 5]
arr1.push.apply(arr1, arr2)
arr1.push(...arr2)

Array方法

1、Array.from() 将两类对象转为真正的数组:类似数组的对象和可遍历的对象(包括 ES6 新增的 Set 和 Map)

1
2
3
4
5
6
7
8
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
console.log([].slice.call(arrayLike)) // [ 'a', 'b', 'c' ]
console.log(Array.from(arrayLike)) // [ 'a', 'b', 'c' ]

接收第二个参数,回调函数,对每个元素进行处理

1
2
3
4
5
6
let arrayLike = {
'0': 1,
'1': 2,
length: 2
};
console.log(Array.from(arrayLike, item => item * 2)) // [ 2, 4 ]

2、Array.of() 将一组任意类型的值,转换为数组

弥补Array的不足
1
2
3
Array.of(1, 2) // [1, 2]
Array(3) // [, , ,]
Array.of(3) // [3]

实例方法

1、copyWithin(target, start = 0, end = this.length) 将指定位置的成员复制到其他位置(会覆盖),然后返回当前数组
target(必需):从该位置开始替换数据。如果为负值,表示倒数。
start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。
end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。

1
[1,2,3,4,5].copyWithin(0,3) // [4, 5, 3, 4, 5]

2、find() 找出第一个符合条件的数组成员。传入一个回调函数,没有符合条件的则返回 undefined

1
2
3
4
[1, -2, -5, 10].find(n => n < 0) // -2
[1, -2, -5, 10].find(function(value, index, arr) {
return value < 0;
})

findIndex() 返回第一个符合条件的数组成员的位置,没有符合条件的则返回 -1
findLast() findLastIndex() 从数组的最后一个成员开始,依次向前检查

可以接受第二个参数,用来绑定回调函数的this对象

3、fill() 使用给定值,覆盖填充一个数组
可以接受第二、三个参数,用于指定填充的起始位置和结束位置

1
2
['a', 'b', 'c'].fill(7) // [7, 7, 7]
new Array(3).fill(7) // [7, 7, 7]

若填充对象,数组中填充的是该对象的引用

4、entries() keys() values() 用于遍历数组,都返回一个遍历器对象
keys() 是键名的遍历,values() 是键值的遍历,entries() 是键值对的遍历

1
2
3
4
5
6
7
8
9
10
11
for (let index of ['a', 'b'].keys()) {
console.log(index); // 0 // 1
}

for (let elem of ['a', 'b'].values()) {
console.log(elem); // 'a' // 'b'
}

for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem); // 0 "a" // 1 "b"
}

5、includes() 判断数组中是否包含某个值,可选第二个参数表示搜索的起始位置,默认为0

1
2
[1, 2, 3].includes(2) // true
[1, 2, NaN].includes(NaN) // true

6、flat() 将多维数组解构为一维,传入参数表示总共解构多少层,默认 1
该方法返回一个新数组,对原数据没有影响

如果不管有多少维,都要转成一维数组,可以用 Infinity 作为参数。
如果原数组有空位,flat()方法会跳过空位

1
2
3
4
5
[1, 2, [3, 4]].flat() // [1, 2, 3, 4]
[1, 2, [3, 4, [5, 6]]].flat() // [1, 2, 3, 4, [5, 6]]
[1, 2, [3, 4, [5, 6]]].flat(2) // [1, 2, 3, 4, 5, 6]
[1, [2, [3]]].flat(Infinity) // [1, 2, 3]
[1, 2, , 4, 5].flat() // [1, 2, 4, 5]

flatMap() 传入一个遍历函数,先执行 map()flat() 只能展开一层数组。
可选第二个参数,用来绑定遍历函数里面的this

1
2
3
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]

7、at() 返回指定索引上的值,允许负索引
该方法不仅可用于数组,也可用于字符串和类型数组

1
2
3
let arr = [1, 2, 3]
arr[arr.length - 1] // 3
arr.at(-1) // 3

8、toReversed() toSorted() toSpliced() with()
含义和用法完全一样,但不改变原数组,而返回一个原数组的拷贝:
toReversed() 对应 reverse(),用来颠倒数组成员的位置。
toSorted() 对应 sort(),用来对数组成员排序。
toSpliced() 对应 splice(),用来在指定位置,删除指定数量的成员,并插入新成员。
with(index, value) 对应 splice(index, 1, value),用来将指定位置的成员替换为新的值。

9、group((item, index, array) => {}) 数组成员分组
根据分组函数的运行结果,将数组成员分组,分组函数的返回值应该是字符串(或者可以自动转为字符串),以作为分组后的组名

还未实装的提案

1
2
3
4
5
let arr = [1,2,3,4,5];
let result = arr.group(item => {
return item % 2 === 0 ? 'even': 'odd';
});
console.log(result);

运算符的扩展

1、指数运算符**
多个指数运算符连用时,是从最右边开始计算的

1
2
3
4
2 ** 2 // 4
2 ** 3 ** 2 // 相当于 2 ** (3 ** 2)
let b = 4;
b **= 3; // 等同于 b = b * b * b;

2、链判断运算符?.
如果读取对象内部的某个属性,往往需要判断一下,属性的上层对象是否存在

1
2
3
4
5
6
7
// 错误的写法
const firstName = message.body.user.firstName || 'default';
// 正确的写法
const firstName = (message
&& message.body
&& message.body.user
&& message.body.user.firstName) || 'default';

使用 ?. 运算符简化写法
在链式调用的时候判断左侧的对象是否为 null 或 undefined。如果是,则不再往下运算,直接返回 undefined

1
const firstName = message?.body?.user?.firstName || 'default';

判断对象方法是否存在,如果存在就立即执行:

1
obj.fun?.()

3、NULL 判断运算符??
使用 || 运算符指定默认值时,只要属性的值为 null 或 undefined,默认值就会生效,但是属性的值如果为空字符串或 false 或 0,默认值也会生效

Null 判断运算符 ?? 类似 ||,但是只有运算符左侧的值为 null 或 undefined 时,才会返回右侧的值

与链判断运算符 ?. 配合使用,为 null 或 undefined 的值设置默认值。

1
const a = obj?.a ?? 10;

Symbol类型

Symbol 是新的基本数据类型,表示独一无二的值

1
2
3
let s = Symbol();
console.log(typeof s) // symbol
console.log(Symbol('sss') == Symbol('sss')) //false

Symbol 值通过 Symbol() 函数生成,允许接受一个字符串作为参数,表示对 Symbol 实例的描述。这主要是为了在控制台显示,或者转为字符串时,比较容易区分。
如果 Symbol 的参数是一个对象,就会调用该对象的 toString() 方法,将其转为字符串,然后再生成一个 Symbol 值

1
2
3
4
5
6
7
8
9
10
11
let s = Symbol('sss');
console.log(s) // Symbol(sss)
console.log(s.toString()) // Symbol(sss)

const obj = {
toString() {
return 'abc';
}
}
s = Symbol(obj);
console.log(s) // Symbol(abc)

Symbol 值也可以转为布尔值(true),但是不能转为数值

1
2
3
let s = Symbol();
console.log(Boolean(s)) // true
console.log(Number(s)) // TypeError

作为属性名

现在,对象的属性名可以是字符串或 Symbol

由于每一个 Symbol 值都是不相等的,这意味着只要 Symbol 值用于对象的属性名,就能保证不会出现同名的属性

两次Symbol('123')返回值并不相同
1
2
3
4
5
6
7
8
9
let s = Symbol('123');
const obj = {
a: 1,
[s]: 's'
}
obj[Symbol('123')] = 123
console.log(obj) // { a: 1, [Symbol(123)]: 's', [Symbol(123)]: 123 }
console.log(obj[s]) // s

Symbol 值作为对象属性名时,用点运算符无法获取该属性

1
2
3
4
5
6
7
let s = Symbol('s');
const obj = {
[s]: 's',
s: 123
};
console.log(obj.s) // 123
console.log(obj[s]) // s

Symbol 值作为属性名,遍历对象的时候,该属性不会出现在for…in、for…of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回,所以通常可以当做私有属性来使用

Object.getOwnPropertySymbols() 方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值
Reflect.ownKeys() 方法可以返回所有类型的键名,包括常规键名和 Symbol 键名

1
2
3
4
5
6
7
8
9
10
11
12
const obj = {
a: 1,
[Symbol('s')]: 's',
};
for (let key in obj) {
console.log(key); // a
}
console.log(Object.keys(obj)); // [ 'a' ]

console.log(Object.getOwnPropertySymbols(obj)); // [ Symbol(s) ]
console.log(Reflect.ownKeys(obj)); // [ 'a', Symbol(s) ]

Symbol方法

1、Symbol.for() 将生成的 Symbol 值登记在全局环境中供搜索,若有相同描述的 Symbol 值则复用返回

Symbol() 写法没有登记机制

其登记机制可以用在不同的 iframe 或 service worker 中取到同一个值

1
2
3
4
5
let s1 = Symbol('s');
let s2 = Symbol.for('s');
let s3 = Symbol.for('s');
console.log(s1 === s2); // false
console.log(s2 === s3); // true

2、Symbol.keyFor() 返回一个已登记的 Symbol 类型值的参数 key

1
2
3
4
let s1 = Symbol('s');
let s2 = Symbol.for('s');
console.log(Symbol.keyFor(s1)) // undefined
console.log(Symbol.keyFor(s2)) // s

Map对象

Map对象保存键值对,元素会保持其插入时的顺序。

Map的键可以是任意数据类型,包括函数、对象或任意基本类型。

在需要进行很多新增操作,且需要储存许多数据的时候,使用 Map 会更高效

创建一个Map
1
2
3
4
5
6
var m = new Map();
//或传入一个嵌套数组
m = new Map([
['x', 1],
['y', 2]
]);

Object与Map增删改查基本操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var o = {};
var m = new Map();
//添加
o.x = 1;
m.set('x', 1);
//修改
o.x = 2;
m.set('x', 2);
//递增
o.x++;
m.set('x', m.get('x')+1);
//获取
o.x;
m.get('x');
//删除
delete o.x;
map.delete('x');

Map的键值对个数可以通过size属性获取

1
2
3
4
5
var m = new Map();
m.set('x', 1);
m.set('y', 2);
console.log(m);//Map(2) {'x' => 1, 'y' => 2}
console.log(m.size);//2

Map的方法

基本方法:

  1. get()获取元素
  2. set()设置元素
  3. has()检查是否有指定key
  4. clear()清空map
  5. delete()删除指定元素

遍历方法:

  1. keys()提取键并返回的迭代器MapIterator对象
  2. values()提取值并返回的迭代器MapIterator对象
  3. entries()提取键值对并返回取键值对的迭代器MapIterator对象
  4. forEach()传入回调函数(value, key)=>{}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var m = new Map([
['x', 1],
['y', 2]
]);

console.log(m.keys());//MapIterator {'x', 'y'}
console.log(m.values());// MapIterator {1, 2}
console.log(m.entries());// MapIterator {'x' => 1, 'y' => 2}

//迭代器可以用for-of遍历
for (let [key, value] of m.entries()) {
console.log(key, value);//x 1, y 2
}

m.forEach((value, key) => {
console.log(key, value);//x 1, y 2
})

Set对象

Set唯一值的集合,与map类似,map存放的是键值对,而set只存放唯一值。

创建set对象:

1
2
var s = new Set();
var s = new Set([1,2,3]);

set对象与数组也很像,可以互相转换

1
2
3
var arr1 = [1, 2, 3];
var s = new Set(arr1);//数组转为set对象,会去重
var arr2 = [...s];//set对象转为数组

可利用set值唯一的特性做数组去重、并集、交集、差集操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//1、去重
var s = new Set([1,2,3,3,2,1]);
var arr = [...s];//[1,2,3]

//2、并集
var arr1 = [1, 2, 3];
var arr2 = [2, 3, 4];
var s = new Set([...arr1, ...arr2]); // {1, 2, 3, 4}
var arr = [...s];// [1,2,3,4]

//3、交集,arr1和arr2共有的元素
var arr1 = [1, 2, 3];
var arr2 = [2, 3, 4];
var s1 = new Set(arr1);//把数组转为set对象方便操作
var s2 = new Set(arr2);
var result = arr1.filter(x => s2.has(x));//[2, 3]

//4、差集,arr1去除arr2中的元素
var arr1 = [1, 2, 3];
var arr2 = [2, 3, 4];
var s1 = new Set(arr1);//把数组转为set对象方便操作
var s2 = new Set(arr2);
var result = arr1.filter(x => !s2.has(x));//[1]

Set值个数可以通过size属性获取

1
2
3
var s = new Set([1,2,3]);
console.log(s);//Set(3) {1, 2, 3}
console.log(s.size);//3

Set的方法

  1. add()添加新元素
  2. delete()删除指定元素
  3. clear()清空所有元素
  4. has()判断是否存在某值
  5. forEach()遍历每个元素,传入回调函数
  6. keys()返回一个 Iterator 对象,这个对象以插入Set 对象的顺序包含了原 Set 对象里的每个元素
  7. values()同keys()
  8. entries()返回 [value, value] 形式的数组迭代器对象,value 是给定集合中的每个元素,迭代器对象元素的顺序即集合对象中元素插入的顺序

add()、delete()、clear()、has() :

1
2
3
4
5
6
7
8
var s = new Set([1,2,3]);
s.add(4);
console.log(s);//Set(4) {1, 2, 3, 4}
s.delete(2);
s.has(2);//false
s.clear();
console.log(s.size);// 0

keys()、values() :返回一个 Iterator 对象,这个对象以插入Set 对象的顺序包含了原 Set 对象里的每个元素

1
2
3
4
5
6
var s = new Set([1,2,3]);
var setIter = s.values();
console.log(setIter);//SetIterator {1, 2, 3}
console.log(setIter.next().value); // 1
console.log(setIter.next().value); // 2
console.log(setIter.next().value); // 3

entries() :返回 [value, value] 形式的数组迭代器对象,value 是给定集合中的每个元素,迭代器对象元素的顺序即集合对象中元素插入的顺序

1
2
3
4
5
6
var s = new Set([1,2,3]);
var setIter = s.entries();
console.log(setIter);//SetIterator {1 => 1, 2 => 2, 3 => 3}
console.log(setIter.next().value); // [1, 1]
console.log(setIter.next().value); // [2, 2]
console.log(setIter.next().value); // [3, 3]

forEach() :遍历每个元素,传入回调函数,参数:回调函数、thisArg执行回调函数时可以当作this来使用。
回调函数参数:值(key)、值(value)、set对象

1
2
3
4
var s = new Set([1,2,3]);
s.forEach((key,value,set)=>{
console.log(key,value);//1 1, 2 2, 3 3
});

迭代/遍历器Iterator

ES6之后,表示“集合”的数据结构,在原先 Array 和 Object 基础上,又增加了 Map 和 Set

遍历器 Iterator 用于规范统一地对集合进行遍历操作,是一个能访问数据的接口

Iterator 的遍历过程:
(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

每一次调用next方法,都会返回数据结构的当前成员的信息(一个包含 value、done 属性的对象)
value:当前成员的值
done:布尔值,表示遍历是否结束

总之,遍历器使得不同的数据结构都可以被 for-of 遍历,只要实现了 Symbol.iterator

Symbol.iterator

Symbol.iterator 是一个预定义好的、类型为 Symbol 的特殊值,是 Iterator 接口

一个数据结构只要具有 Symbol.iterator 属性,就是可遍历的,也就是可以用 for-of 进行遍历

Symbol.iterator 属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器

1
2
3
4
5
6
7
8
9
10
11
12
const obj = {
[Symbol.iterator] : function () {
return {
next: function () {
return {
value: 1,
done: true
};
}
};
}
};

原生具备 Iterator 接口的数据结构:Array、Map、Set、String、TypedArray、函数的 arguments 对象、NodeList 对象

获取数组的迭代器
1
2
3
4
5
6
7
8
9
10
11
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();
iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }
// for-of 遍历
let arr = ['a', 'b', 'c'];
for(let item in arr) {
console.log(item);
}

生成器Generator

生成器是一种函数,function关键字与函数名之间有一个星号 *,函数体内部使用yield表达式

调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是一个遍历器对象

yield 关键字会暂停函数的执行,调用遍历器对象的 next 方法,能让函数执行到下一个 yield 暂停处(或是return)

总之 Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function* fun() {
console.log(1);
yield 'qx';
console.log(2);
yield 'chuckle';
console.log(3);
}
const f = fun();
console.log(f.next())
console.log(f.next())
console.log(f.next())
// 1
// { value: 'qx', done: false }
// 2
// { value: 'chuckle', done: false }
// 3
// { value: undefined, done: true }

next() 会返回一个包含 value、done 属性的对象,其中 value 值是 yield 后面表达式的值

next() 可以传入参数,yield 是将其返回后,函数再继续执行到下一个 yield

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* fun() {
let x = yield;
console.log(x);
let y = yield;
console.log(x+y);
}
const f = fun();
console.log(f.next(1))
console.log(f.next(2))
console.log(f.next(3))
// { value: undefined, done: false }
// 2
// { value: undefined, done: false }
// 5
// { value: undefined, done: true }

使用 Generator 函数能够很方便地实现 Symbol.iterator,其目的也正是如此

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function* objectEntries(obj){
// 获取对象的所有 key 保存到数组 [name, age]
const propKeys = Object.keys(obj);
for(const propkey of propKeys){
yield [propkey, obj[propkey]]
}
}
const obj = {
name: 'chuckle',
age: 19
}
// 把 Generator 生成器函数赋值给对象的Symbol.iterator属性, 为该对象加上遍历器接口
obj[Symbol.iterator] = objectEntries;
// objectEntries(obj) 等价于 obj[Symbol.iterator](obj)
for(let [key, value] of objectEntries(obj)){
console.log(`${key}: ${value}`);
}
// name: chuckle
// age: 19

Generator的应用

1、异步代码同步化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function* main(){
let res = yield request('https://v1.hitokoto.cn/');
console.log('1:'+res);
res = yield request('https://v1.hitokoto.cn/');
console.log('2:'+res);
res = yield request('https://v1.hitokoto.cn/');
console.log('3:'+res);
}
const ite = main();
ite.next();

function request(url){
fetch(url)
.then(res=>res.json())
.then(data=>{
ite.next(data.hitokoto)
})
}
// 1:自己永远是孤单的,但你可以让其他人变得不孤单。
// 2:我就是小偷,专门来偷走哥哥的心
// 3:电脑的处理器总比执行单元小。

2、正确的执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function* main(){
fun1();
yield fun2();
fun3();
}
let ite = main();
ite.next();
// 1 2 3

function fun1(){
console.log('1');
}
function fun2(){
setTimeout(()=>{
console.log('2');
ite.next();
},1000)
}
function fun3(){
console.log('3');
}

Promise

之前已经记过笔记了:Promise异步编程

Class类

在 ES5 中生成实例对象的传统方法是通过构造函数

1
2
3
4
5
6
7
8
9
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayName = function () {
console.log(this.name);
}
let p = new Person('chuckle', 18);
p.sayName() // chuckle

ES6新增了class语法糖,类本质上是函数,类本身指向其构造函数 constructor(),所以类的构造函数也就是类本身

1
2
class A { }
console.log(typeof A === 'function'); // true

构造函数和普通函数容易混淆,现在,可以通过 Class 关键字来声明一个类了,其内置的 constructor() 在实例化对象时会立即被调用

用法还是大差不差,并且和之前一样,类方法都在原型上

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
// 实例化的时候会立即被调用
constructor(name, age){
this.name = name;
this.age = age;
}
//等同于Person.prototype.sayName = function sayName(){}
sayName(){
console.log(this.name);
}
}
let p = new Person('chuckle', 18);
p.sayName() // chuckle

由于类中定义的方法都在其构造函数的原型上,所以可以用 Object.assign() 向类添加多个方法

1
2
3
4
5
6
7
class Person {
constructor(){}
}
Object.assign(Person.prototype, {
toString(){},
toValue(){}
});

constructor函数

constructor() 方法是类的默认方法,在创建实例对象时,自动调用

默认返回实例对象(即this),也可以指定返回(return)另外一个对象

1
2
3
constructor() {
return Object.create(null);
}

如果一个实例属性不需要接受外部的传参赋值,可以不在 constructor 方法中定义

1
2
3
4
5
6
7
8
9
10
class Person {
constructor(name, age){
this.name = name;
this.age = age;
}
country = 'china';
}
let p = new Person('chuckle', 18);
console.log(p)
// Person { country: 'china', name: 'chuckle', age: 18 }

Class表达式

可以写成表达式的形式

1
2
3
4
5
6
7
8
const MyClass = class { 
name = 'chuckle';
SayName() {
console.log(this.name);
}
};
const myClass = new MyClass();
myClass.SayName() // chuckle

可以写出立即执行的 Class

1
2
3
4
5
6
7
8
9
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}('张三');
person.sayName(); // 张三

Static

在类成员前,加上static关键字,就表示静态属性或方法

静态即不会被实例继承,而是直接通过类来调用

静态方法可以与非静态方法重名

1
2
3
4
5
6
7
8
9
class Class{ 
name = 'chuckle';
static name = 'qx';
static SayName() {
console.log(this.name); // 静态方法的this指类本身,只能访问静态属性
}
};
Class.SayName() // qx
new Class().SayName(); // TypeError: myClass.SayName is not a function

父类的静态成员,可以被子类继承

1
2
3
4
5
6
7
8
9
10
class Foo {
static num = 123;
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
}
Bar.classMethod() // 'hello'
Bar.num // 123

私有方法和属性

私有,即只能在类的内部访问的方法和属性,外部不能访问,有利于代码的封装

早期并没有途径实现真正的私有,只能通过特殊的命名规则加以区别

1
2
3
4
class Class {
foo () {} // 公有
_fun() {} // 加上_表示私有
}

或者借助 Symbol 来让成员不易被获取,但仍然可以被 Reflect.ownKeys() 等方法获取到

1
2
3
4
5
6
7
8
9
10
11
12
13
let s1 = Symbol('name');
let s2 = Symbol('fun')
class Class {
name = 'qx';
[s1] = 'chuckle';
[s2](){
console.log(this.name);
};
}
const myClass = new Class()
console.log(myClass) // Class { name: 'qx', [Symbol(name)]: 'chuckle' }
myClass[s2]() // qx
console.log(Object.getOwnPropertySymbols(myClass)) // [ Symbol(name) ]

ES2022 正式为 class 添加了私有成员,方法是在属性或方法名之前使用 # 表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Class {
#count = 0;
get value(){
return this.#count;
}
increment() {
this.#count++;
}
}
const myClass = new Class();
myClass.increment();
console.log(myClass.value) // 1
console.log(myClass.#count) // SyntaxError: Private field '#count' must be declared in an enclosing class
console.log(myClass.count) // undefined

不管在类的内部或外部,如果读取一个不存在的私有成员,会报错

私有成员前面,也可以加上 static 关键字,表示这是一个静态的私有成员

类的继承

Class 通过 extends 关键字实现继承,写法比 ES5 的原型链继承,要清晰和方便很多

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
class Animal {
constructor(name, age){
this.name = name;
this.age = age;
}
sayName(){
return this.name;
}
sayAge(){
return this.age;
}
}

class Dog extends Animal{
constructor(name, age, color){
// 子类的构造函数必须执行一次 super() 函数
super(name, age); // 等同于 Animal.call(this,name,age); 继承父类的属性
this.color = color;
}
// 子类自己的方法
sayColor(){
return `${this.name}${this.age}岁了,它的颜色是${this.color}`;
}
// 重写父类的方法
sayName(){
// super 相同于 Animal
return this.name + super.sayAge + this.color;
}
}

let d1 = new Dog('小黄', 28, 'red');
console.log(d1.sayAge()); // 调用继承父类的方法
console.log(d1.sayColor()); // 调用子类自己的方法
console.log(d1.sayName()); // 调用重写父类的方法

继承的原型链

类的继承是原型链继承,子类的原型的proto指向父类的原型,详见:原型与原型链

同时为了静态方法能够继承,子类的构造函数的proto指向父类的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
name = 'a'
getName() {
console.log(this.name)
}
}
class B extends A {
name = 'b'
}
// 继承实例方法
console.log(Object.getPrototypeOf(B.prototype) === A.prototype) // true
// 继承静态的类方法
console.log(Object.getPrototypeOf(B) === A) // true

两条原型链:

1
2
B.__proto__ === A
B.prototype.__proto__ === A.prototype

super

super 关键字,既可以当作函数使用,也可以当作对象使用

super 作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次 super() 函数
调用super()的作用是形成子类的this对象,任何对子类 this 的操作都要放在 super() 的后面

super() 相当于 A.prototype.constructor.call(this)(在子类的this上运行父类的构造函数)

1
2
3
4
5
6
7
class A {}
class B extends A {
constructor() {
super();
this.name = 'qx';
}
}

super 作为对象时,在普通方法中,指向父类的原型对象,在静态方法中,指向父类

定义在父类实例上的方法或属性,无法通过 super 调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A {
p() {
return 2;
}
}

class B extends A {
constructor() {
super();
console.log(super.p()); // 2
}
}

let b = new B();

Module模块化

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量,而 CommonJS 和 AMD 模块,都是运行时的

1
2
3
4
// CommonJS模块
let { stat, exists, readfile } = require('fs');
// ES6模块
import { stat, exists, readFile } from 'fs';c

ES6 模块主要有两个命令构成:exportimport

export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量

1
2
3
4
5
6
const name = '张三';
const age = 18;
const sayName = function() { console.log(fristName); }
export {name, age, sayName}

import {name, age, sayName} from './modules/index.js';

默认暴露 export default 为模块指定默认输出,import命令可以为该匿名函数指定任意名字

1
2
3
4
5
6
7
8
export default function(){
console.log('chuckle');
}

import sayName from './modules/index.js'
import { default as sayName } from './modules/index.js'
import sayName, { name } from './modules/index.js'
sayName(); // chuckle

整体加载:模块暴露的内容都会作为该对象的成员

1
import * as obj from './modules/index.js';

在浏览器环境中,需要给 script 标签加上 type='module' 属性

1
2
3
<script type='module'>
import { name } from './modules/index.js';
</script>

动态import()

使用 import() 函数动态引入模块

1
2
3
4
5
if(true){
import('./modules').then(mod=>{
console.log(mod) // mod对象中包含模块暴露的成员
})
}