var,let,const有哪些区别

在ES5中,顶层对象(在浏览器中是window)的属性和全局变量是等价的,或者说全局变量会被挂载到window对象中

  • 变量提升

    var声明的变量存在变量提升,变量提升只提升变量声明,不提升变量赋值。而letconst不存在变量提升

  • 重复声明

    var声明的变量可以被重复声明,后面声明的会覆盖前面声明的。而letconst声明的变量无法被重复声明

  • 作用域

    var声明的变量只会产生函数作用域,不会产生块级作用域

    let用来声明一个变量,会产生一个块级作用域

    const用来声明一个常量,也会产生一个块级作用域

变量提升与函数提升

只有var声明的变量才存在变量提升,只有具名函数才存在函数提升。

函数提升优先级比变量提升要高,且不会被变量声明覆盖,但是会被变量赋值覆盖。

1
2
3
4
5
console.log(foo);//输出函数ƒ foo(){...}
function foo(){
console.log("函数声明");
}
var foo = "变量";//后声明,后赋值
1
2
3
4
5
function foo(){
console.log("函数声明");
}
var foo = "123";
console.log(foo);//输出`123`
1
2
3
4
5
var foo = "123";
function foo(){
console.log("函数声明");
}
console.log(foo);//输出`123`

简单的来说,当输出foo变量会导致输出undefined的时候,那输出的就是foo函数,否则就输出foo变量,赋值后的变量优先级最高

数组新增了哪些扩展

扩展运算符 …

扩展运算符的作用就是把数组变成一个序列

1
console.log(...[1, 2, 3]) //等同于console.log(1,2,3)
  • 用来展开数组

    1
    Math.max(...arr)//求数组arr的最大值
  • 用来合并,拷贝数组

    拷贝数组进行的是浅层次的拷贝

    1
    const arr = [...arr1,...arr2]
  • 将对象转化成数组

    定义了遍历器(Iterator)接口的对象(可迭代对象),都可以用扩展运算符转为真正的数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let nodeList = document.querySelectorAll('div');
    let array = [...nodeList];

    let map = new Map([
    [1, 'one'],
    [2, 'two'],
    [3, 'three'],
    ]);

    let arr = [...map.keys()]; // [1, 2, 3]

    如果对没有 Iterator 接口的对象,使用扩展运算符,将会报错,因为这些对象是不可迭代的。

    1
    2
    const obj = {a: 1, b: 2};
    let arr = [...obj]; // TypeError: Cannot spread non-iterable object

    除非写成{...obj}的形式,表示拷贝对象。

构造函数Array的新增方法

关于构造函数,数组新增的方法有如下:

  • Array.from()
  • Array.of()

Array.from()

将两类对象转为真正的数组:类似数组的对象(伪数组)和可迭代对象(包括 ES6 新增的数据结构 SetMap

伪数组(类似数组的对象)指的是:

  • 具有 length 属性。
  • 按照索引存储元素(即可以通过 [0], [1], [2] 等方式访问元素)

常见的伪数组对象:

  • 函数中的arguments对象:

    1
    2
    3
    4
    function example() {
    return Array.from(arguments);
    }
    console.log(example(1, 2, 3)); // 输出: [1, 2, 3]
  • DOM 操作返回的NodeList

    1
    2
    3
    const nodeList = document.querySelectorAll('div');
    const divArray = Array.from(nodeList);
    console.log(divArray); // 转换为真正的数组

示例:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
// Set
const set = new Set([1, 2, 3]);
const setArray = Array.from(set);
console.log(setArray); // 输出: [1, 2, 3]

// Map
const map = new Map([
['a', 1],
['b', 2],
['c', 3]
]);
const mapArray = Array.from(map);
console.log(mapArray); // 输出: [['a', 1], ['b', 2], ['c', 3]]

从上述例子中可以看出,数组和Set或者Map二者之间是可以相互转化的。

还可以接受第二个参数,用来对每个元素进行处理,将处理后的值放入返回的数组,效果就类似Map

1
Array.from([1, 2, 3], (x) => x * x)// [1, 4, 9]

我们在学习过程中,想必也遇到过通过[...obj]的方式把一个对象转换成数组的情况,它和Array.from()有什么区别呢?

扩展运算符 [...] 的作用是将可迭代对象展开为数组,

1
2
3
4
5
6
7
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
let arr2 = [...arrayLike]; // 报错!无法使用扩展运算符,把一个类数组对象转化成数组

扩展运算符 [...] 只能用于可迭代对象(如 SetMapString 等)

特性Array.from()[...]
支持伪数组✅ 支持❌ 不支持
支持可迭代对象✅ 支持✅ 支持
是否需要 [Symbol.iterator]不需要需要

Array.of()

用于将一组值,转换为数组

1
Array.of(3, 11, 8) // [3,11,8]

当参数只有一个的时候,实际上是指定数组的长度

参数个数不少于 2 个时,Array()才会返回由参数组成的新数组

1
2
3
Array.of() // []
Array.of(3) // [, , ,]
Array.of(3, 11, 8) // [3, 11, 8]

新增方法

  • find()、findIndex()
  • fill()
  • entries(),keys(),values()
  • includes()

find,findIndex

find()用于找出,返回第一个符合条件的数组成员

参数是一个回调函数,接受三个参数依次为当前的值、当前的位置和原数组

1
2
3
4
let a = [1, 5, 10, 15]
a.find(function(value, index, arr) {
return value > 9;
}) // 返回10
1
2
3
4
findIndex`返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回`-1
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2

fill

使用给定值,填充一个数组

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

new Array(3).fill(7)
// [7, 7, 7]

还可以接受第二个和第三个参数,用于指定填充的起始位置结束位置,左闭右开

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

注意,如果填充的类型为对象,则是浅拷贝,即被填充的数据,使用的都是同一个对象

1
2
['a', 'b'].fill({name:'tom'})
//[{name:'tom'},{name:'tom'}] 数组中的这两个对象是同一个对象

除非每次填充都使用新创建的对象

1
['a', 'b'].fill(new Array(3))

includes

用于判断数组是否包含给定的值,相比indexOf方法,优化了对NaN的判断

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

函数新增了哪些扩展

参数

ES6允许为函数的参数设置默认值

1
2
3
4
5
6
7
function log(x, y = 'World') {
console.log(x, y);
}

log('Hello') //输出 Hello World
log('Hello', 'China') //输出 Hello China
log('Hello', '') //输出 Hello

函数的形参是默认声明的,不能使用letconst再次声明

1
2
3
4
function foo(x = 5) {
let x = 1; // error
const x = 2; // error
}

解构赋值过程中也可以给形参添加默认值

1
2
3
4
5
6
7
8
function foo({x, y = 5}) {
console.log(x, y);
}

foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: Cannot read property 'x' of undefined

属性

函数本质也是个对象,有许多属性

  • func.length

    将返回没有指定默认值的参数个数,具体情况还得具体分析,感觉很鸡肋。

  • func.name

    如果把匿名函数赋值给一个变量,则name属性返回这个变量的名字

    1
    2
    3
    4
    5
    var f = function () {};
    // ES5
    f.name // ""
    // ES6
    f.name // "f"

    如果将一个具名函数赋值给一个变量,则 name属性都返回这个具名函数原本的名字

    1
    2
    const bar = function baz() {};
    bar.name // "baz"

    bind返回的函数,name属性值会加上bound前缀

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

作用域

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域

等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的

下面例子中,y=x会形成一个单独作用域,x没有被定义,所以指向全局变量x

1
2
3
4
5
6
7
8
9
let x = 1;

function f(y = x) {
// 等同于 let y = x
let x = 2;
console.log(y);
}

f() // 1

严格模式

  • 必须写在当前作用域作用域顶部才能生效

  • 当一个函数被直接调用,无论这个函数在哪儿被直接调用,先看全局作用域中是否开启了严格模式,如果开启了,则this指向undefined,如果未开启,再查看这个被直接调用的函数内部,是否开启了严格模式,如果开启了,则this指向undefined,否则this指向全局对象,在浏览器中指的就是window对象

1
2
3
4
5
6
7
8
9
let num = 117
function func1() {
console.log(this, this.num);
}
(function () {
"use strict";
console.log(this)//undefined
func1();//函数func1直接被调用,全局作用域和这个函数内部都未开启严格模式,this指向window输出117
})()//立即执行函数,属于直接被调用,而且内部开启了严格模式,所以输this指向undefined
1
2
3
4
5
6
7
8
9
10
"use strict";
let num = 117
function func1() {
console.log(this, this.num);
}
(function () {
console.log(this)//undefined
func1()
}()};//函数func1直接被调用,全局作用域开启了严格模式,this指向undefeined
//立即执行函数,属于直接被调用,全局作用域开启了严格模式,所以this=undefined

严格模式不能随便开启

只要函数形参使用了默认值解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。所以说函数内部也不能随便开启严格模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 报错
function doSomething(a, b = a) {
'use strict';
// code
}
// 报错
const doSomething = function ({a, b}) {
'use strict';
// code
};
// 报错
const doSomething = (...a) => {
'use strict';
// code
};
const obj = {
// 报错
doSomething({a, b}) {
'use strict';
// code
}
};

严格模式只能改变直接被调用的函数内部的this的指向

1
2
3
4
<script>
"use strict"
console.log(this)//输出window对象
</script>

即便开启了严格模式,全局作用域中的this还是指向全局对象,所以说,全局作用域中的this始终指向全局对象

箭头函数

形如:

1
()=>{}
  • 更适用于那些本来需要匿名函数的地方,类似lambda表达式,它和普通匿名函数一样,它属于表达式函数,不存在函数提升

  • 箭头函数看起来是匿名的,但是可以通过前面的变量名或者属性名,推断出同名的name

    1
    2
    3
    4
    const func = () => {
    console.log('你好啊')
    }
    console.log(func.name)//输出func
  • 只有一个参数的时候可以省略括号;只有一行代码且是return语句,可以省略大括号return关键字,如果返回的是一个对象,则需要加括号。

    1
    2
    item => item.name //等同于(item)=>{ return item.name }
    item => ({name:'tom'})
  • 没有自己的环境变量this,内部的this指向被定义的时候外层函数的this,this指向和如何被调用无关

  • 因为没有自己的环境变量this,所以无法使用applycallbind等方法改变箭头函数内部的this指向,但是可以调用这些方法。

  • 内部也没有arguments对象。arguments在一般函数内部可以直接使用(如同this),即便函数没有形参,也可以给函数传参,传递的所有参数都会被收集到arguments对象

  • 没有自己的原型对象(prototype),所以不能当作构造函数使用,不能用来创造实例(不能对箭头函数使用new关键字)。

  • 内部不可以使用yield命令,因此箭头函数不能用作 Generator 函数

对象新增了哪些扩展

属性的简写

ES6中,当对象键名与对应值名相等的时候,可以进行简写

1
2
3
const baz = {foo:foo}
// 等同于
const baz = {foo}

方法也能够进行简写

1
2
3
4
5
6
7
8
9
10
11
12
const o = {
//这是一种简写方式
method() {
return "Hello!";
}
};
// 等同于
const o = {
method: function() {
return "Hello!";
}
}

属性名表达式

eS6 允许字面量定义对象时,将表达式放在中括号内,当作对象的属性。

1
2
3
4
5
6
7
8
9
10
let lastWord = 'last word';

const a = {
'first word': 'hello',
[lastWord]: 'world'
};

a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"

注意,属性名表达式属性名简写,不能同时使用,会报错。

1
2
3
4
5
6
7
// 报错
const foo = 'bar';
const baz = { [foo] };

// 正确
const foo = 'bar';
const baz = { [foo]: foo};

super关键字

this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象

super=this.__proto__

解构赋值

这项特性允许开发者从复杂的数据结构如对象或数组中提取数据,并直接将这些数据赋值给变量。这种机制不仅使得代码更加简洁易读,还提高了开发效率。

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 person = {
firstName: "John",
lastName: "Doe",
age: 30,
address: {
city: "New York",
country: "USA"
}
};

// 使用ES6的对象解构赋值
const { firstName, lastName, age } = person;
console.log(firstName); // 输出: John
console.log(lastName); // 输出: Doe
console.log(age); // 输出: 30

// 如果你想使用不同的变量名,可以这样做:
const { firstName: localFirstName, lastName: localLastName } = person;
console.log(localFirstName); // 输出: John
console.log(localLastName); // 输出: Doe

// 解构嵌套对象
const { address: { city, country } } = person;
console.log(city); // 输出: New York
console.log(country);// 输出: USA

要注意的是因为使用的是const关键字,所以上述解构赋值得到的数据都是常量。

如何理解ES6新增Set、Map两种数据结构

Set是一种叫做集合的数据结构,什么是集合?什么又是字典?

  • 集合
    是由一堆无序的、相关联的,且不重复的内存结构【数学中称为元素】组成的组合
  • 字典
    是一些元素的集合。每个元素有一个称作key 的域,不同元素的key各不相同

Set

Set本身是一个构造函数,用来生成Set数据结构

1
const s = new Set();

增删改查

Set的实例关于增删改查的方法:

  • add()

    向集合中添加元素,返回 Set 结构本身,所以可以链式调用

    当添加实例中已经存在的元素,set不会进行处理添加,即会被去重

    1
    s.add(1).add(2).add(2); // 2只被添加了一次
  • delete()

    删除某个值,返回一个布尔值,表示删除是否成功

    1
    s.delete(1)
  • has()

    返回一个布尔值,判断集合中是否存在某个元素

    1
    s.has(2)
  • clear()

    清除所有成员,没有返回值

    1
    s.clear()

遍历

关于遍历的方法,有如下:

  • keys():返回键名的迭代器
  • values():返回键值的迭代器
  • entries():返回键值对的迭代器
  • forEach():使用回调函数遍历每个成员
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
let set = new Set(['red', 'green', 'blue']);

//可以看出Set其实就是键和值相等的Map
for (let item of set.keys()) {
console.log(item);
}
// red
// green
// blue

for (let item of set.values()) {
console.log(item);
}
// red
// green
// blue

for (let item of set.entries()) {
//每个entry的类型是数组,第一个元素是键名,第二个元素是键值
console.log(item);
}
// 可以看到其实Set的本质就是Map
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]

forEach

1
2
3
4
5
let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ' : ' + value))
// 1 : 1
// 4 : 4
// 9 : 9

使用场景

扩展运算符和Set 结构相结合实现数组字符串去重

1
2
3
4
5
6
7
8
9
10
// 数组
let arr = [3, 5, 2, 2, 5, 5];
// 创建一个集合的同时传入一个数组,然后再把这个集合转变成数组,从而起到去重的作用
// 说明Set实例也是可迭代对象,所以能够转化成数组
let unique = [...new Set(arr)]; // [3, 5, 2]
// 或者let unique = Array.from(new Set(arr))
// 字符串
let str = "352255";
//竟然还能传入一个字符串?给字符串去重,因该是把字符串拆分成了字符数组
let unique = [...new Set(str)].join(""); // "352"

下面的例子说明无论是map还是map.keys()都能转换成数组

1
2
3
4
5
6
7
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
console.log([...map.keys()])//[1, 2, 3]
console.log([...map])// [[1, 'one'] (2) [2, 'two'] (2) [3, 'three']]

Map

Map类型是键值对的有序列表,而键和值都可以是任意类型

Map本身是一个构造函数,用来生成 Map 数据结构

1
const m = new Map()

增删查改

Map 结构的实例针对增删改查有以下属性和操作方法:

  • size 属性

    size属性返回键值对的个数

    1
    2
    3
    4
    const map = new Map();
    map.set('foo', true);
    map.set('bar', false);
    map.size // 2
  • set()

    设置键名key对应的键值为value,然后返回整个Map结构

    如果key已经有值,则键值会被更新,否则就新生成该键

    同时返回的是当前Map对象,可采用链式写法

    1
    2
    3
    4
    5
    6
    const m = new Map();

    m.set('edition', 6) // 键是字符串
    m.set(262, 'standard') // 键是数值
    m.set(undefined, 'nah') // 键是 undefined
    m.set(1, 'a').set(2, 'b').set(3, 'c') // 链式操作
  • get()

    get方法读取key对应的键值,如果找不到key,返回undefined

    1
    2
    3
    4
    5
    6
    const m = new Map();

    const hello = function() {console.log('hello');};
    m.set(hello, 'Hello ES6!') // 键是函数

    m.get(hello) // Hello ES6!
  • has()

    has方法返回一个布尔值,表示某个是否在当前 Map 对象之中,类似于Obj中的hasOwnProperty

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const m = new Map();

    m.set('edition', 6);
    m.set(262, 'standard');
    m.set(undefined, 'nah');

    m.has('edition') // true
    m.has('years') // false
    m.has(262) // true
    m.has(undefined) // true
  • delete()

    1
    2
    3
    4
    5
    6
    7
    delete`方法删除某个键,返回`true`。如果删除失败,返回`false
    const m = new Map();
    m.set('name', 'nah');
    m.has('name') // true

    m.delete('name')
    m.has('name') // false,删除后不再存在
  • clear()

    clear方法清除所有成员,没有返回值

    1
    2
    3
    4
    5
    6
    7
    let map = new Map();
    map.set('foo', true);
    map.set('bar', false);

    map.size // 2
    map.clear()
    map.size // 0

遍历

  • keys():返回键名的迭代器
  • values():返回键值的迭代器
  • entries():返回键值对的迭代器
  • forEach():遍历 Map 的所有成员
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
41
42
//传入一个二维数组说是
const map = new Map([
['F', 'no'],
['T', 'yes'],
]);

for (let key of map.keys()) {
console.log(key);
}
// "F"
// "T"

for (let value of map.values()) {
console.log(value);
}
// "no"
// "yes"

for (let item of map.entries()) {
console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"

// 等同于使用map.entries(),默认调用entries方法得到键值对迭代器
for (let [key, value] of map) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"

//map中的forEach的用法和set中的一样
map.forEach(function(value, key, map) {
console.log("Key: %s, Value: %s", key, value);
});

Map和Obj的区别

很多时候我们都可以使用Obj来实现Map的功能,毕竟都是键值对的形式,那二者具体有什么区别呢,Map被设计出来有什么优势呢?

Map中的键可以是任意类型

当使用普通对象 {} 作为键值对存储时,默认情况下只能使用字符串符号(Symbol)作为键。如果尝试用其他类型的值作为键,JavaScript 会自动将其转换为字符串。

1
2
3
4
5
6
let obj = {};
let key = { name: 'key' };
let key2 = { name: 'key2' }
obj[key] = 'key';
obj[key2] = 'key2'
console.log(obj); // 输出: { '[object Object]': 'key2' }

从上面的例子可以看出,{ name: 'key' }{ name: 'key2' }都被转化成[object Object],它们被视为相同的键

相比之下,Map 可以直接使用任何类型的值作为键,并且不会进行隐式的类型转换。

1
2
3
4
5
6
let obj = new Map();
let key = { name: 'key' };
let key2 = { name: 'key2' }
obj.set(key, 'key')
obj.set(key2, 'key2')
console.log(obj);

键值对顺序

  • **Object**:虽然 ES6 之后的对象保留了属性插入的顺序(对于可枚举属性),但这种顺序性并不是所有情况下都保证的,特别是对于旧版浏览器。
  • MapMap 明确保持键值对插入的顺序。这意味着你可以依赖于键值对,按照它们被添加的顺序进行迭代

获取大小

  • Object

    没有直接的方法,来获取对象中属性的数量。你需要手动计算,例如通Object.keys()计算返回的数组的长度

    1
    2
    const obj = { a: 1, b: 2 };
    console.log(Object.keys(obj).length); // 输出 2
  • Map提供了size属性,可以直接获取键值对的数量。

性能

在频繁地增删键值对时Map 的性能通常优于 Object。这是因为 Map 是专门为动态场景设计的,而对象更适合静态结构的数据。

Set和Map的区别

特性SetMap
数据结构存储唯一值的集合(类似数组)存储键值对的集合(类似对象)
键值类型仅存储值,无键名键可以是任意类型(包括对象、函数等),值不限类型
唯一性规则值必须唯一(基于 SameValueZero 算法,NaN 视为相等)键必须唯一,值可重复

总结

  • Set对应数据结构中的集合,Map对应数据结构中的字典
  • Set本质是键和值相同的Map
  • Set和Map都有has,clear,delete这三个方法;
  • Set独有的方法的是add,返回Set实例本身,支持链式调用;Map独有的方法是get,set,其中set方法返回的也是Map实例本身,也支持链式调用。
  • Set和Map的遍历的方法都包括for...of...forEach,其中for...of...的对象又包括各种迭代器。

你是怎么理解es6中 Promise的

是什么

异步编程的一种解决方案,比传统的解决方案—回调函数,更加合理和更加强大

因为使用回调函数来解决异步编程问题,存在回调函数地狱问题,即在回调函数中嵌套回调函数,这样就导致代码的可读性变得很差,代码也变得难以维护。

而使用promise解决异步编程操作有如下优点:

  • 链式操作减低了编码难度
  • 代码可读性明显增强

下面我们来正式介绍promise:

状态

promise对象仅有三种状态

  • pending(进行中)
  • fulfilled(已成功)
  • rejected(已失败)

对象的状态不受外界影响,只有异步操作的结果,可以决定当前是哪一种状态

一旦状态改变(从pending变为fulfilled和从pending变为rejected),就不会再变,也就是说promise实例的状态只能改变一次。

实例方法

Promise对象是一个构造函数,用来生成Promise实例

1
2
3
4
const promise = new Promise(function(resolve, reject) {
//构造器内部代码从给出的resolve, reject中选一个调用,改变promise对象的状态
//构造器内部的代码会立即执行,但是resolve/reject可能被异步调用
});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject

  • resolve函数的作用是,将Promise对象的状态从pending变为fulfilled
  • reject函数的作用是,将Promise对象的状态从pending变为rejected

Promise构建出来的实例存在以下方法:

  • then()
  • catch()
  • finally()

then

then方法会立即调用,但是它传入的回调函数,会等到实例状态发生改变时才被调用,第一个参数是fulfilled状态会触发的回调函数,第二个参数是rejected状态会触发的回调函数

then方法返回的是一个新的Promise实例,而且是立即返回,也就是promise能链式书写的原因。

catch

catch()方法用于指定发生错误时的回调函数,本质就是在内部调用then(undefined,onRejected)

通常情况下,我们使用then的时候只传入第一个参数,即成功时的回调函数;然后再搭配catch使用,传入失败时的回调函数。

finally

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作,内部其实本质就是在调用then(onFinally,onFinally)

1
2
3
4
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

静态方法

Promise构造函数存在以下方法:

  • all()
  • any()
  • race()
  • allSettled()
  • resolve()
  • reject()

promise的静态方法的返回值都是promise对象

关于这几个静态方法的详细介绍,参考手写promise部分。

手写一个Promise

为了帮助我们更深入的理解Promise,建议尝试自己手写一个Promise

参考资料:Day02-01.手写promise-核心功能-构造函数_哔哩哔哩_bilibili

任务目标

  • 创建一个myPromise类,处理同步修改promise状态的情况

    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
    41
    42
    43
    44
    45
    const PENDING = 'pending'
    const FULFILLED = 'fulfilled'
    const REJECTED = 'rejected'
    class myPromise {
    state = PENDING//默认值
    result
    constructor(func) {
    //func是创建promise实例的时候,传入的回调函数
    //这个回调函数接受两个参数resolve,reject,我们在构造函数内部准备这两个方法
    const resolve = (res) => {
    //如果promise实例的状态还没有改变
    if (this.state = PENDING) {
    //那就改变它的状态为'fulfilled'
    this.state = FULFILLED
    //res是传入的值
    this.result = res
    }
    }
    const reject = (err) => {
    //如果promise实例的状态还没有改变
    if (this.state = PENDING) {
    //那就改变它的状态为'rejected'
    this.state = REJECTED
    this.result = err
    }
    }
    //因为用户传入的回调函数func本身也可能报错,所以我们用try-catch捕获一下
    try {
    //同步调用传入的回调函数,传入2个在构造函数内部定义好的,能改变promise实例状态的函数
    //由用户决定何时,如何改变promise实例的状态
    func(resolve, reject)
    } catch (err) {
    reject(err)
    }
    }
    //暂时只考虑同步修改promise状态,也就是then方法被调用的时候,promise的状态就已经被确定了
    //这样我们就能立马知道应该调用哪个回调函数
    then(onFulFilled, onRejected) {
    if (this.state == FULFILLED) {
    onFulFilled(this.result)
    } else if (this.state == REJECTED) {
    onRejected(this.result)
    }
    }
    }

    举例测试

    1
    2
    3
    4
    5
    6
    const p = new myPromise((resolve,reject)=>{
    //同步修改promise状态
    resolve(1)
    })
    //因为p的状态在刚被创建的时候就改变了,then方法传入的回调函数也会马上执行,拿到p实例的值
    p.then(res=>{console.log(res)},err=>{console.log(err)})//立即输出1
  • 处理异步修改promise状态的情况

    如果我们promise的状态是异步改变的,比如

    1
    2
    3
    4
    const p = new myPromise((resolve,reject)=>{
    //1s后,再调用resolve方法,把promise实例的状态修改为'fulfilled'
    setTimeout(()=>{resolve(1)},1000)
    })

    创建完实例p后我们同步调用then方法(调用then方法本身是同步的,创建promise对象,创建promise实例调用构造函数本身也是同步的)

    1
    p.then(res=>{console.log(res)},err=>{console.log(err)})

    then方法在内部拿到实例的state后,遗憾的告知传入的2个回调函数,实例的状态还未改变,你们都不能被调用,而且我也不知道你们俩该什么时候被调用,所以then方法把这2个回调函数托管给别人,这个人就是handle数组

    我们希望promise状态改变的时候,传入then方法的2个回调函数有一个会被执行,那什么时候promise状态会改变呢?1s之后,还是2s之后?我们貌似找不到一个固定的时间点,其实能让promise状态改变的,就是在构造函数内部定义的那2个函数,它们其中任意一个被调用的时候,就是promise状态被改变的时候,而这2个函数何时被调用,又是由创建promise实例的时候传入的函数决定的。

    所以我们将传入then方法的回调函数,放到在构造函数内部定义的那2个函数中执行,就能完美处理异步回调的情况。

    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
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    const PENDING = 'pending'
    const FULFILLED = 'fulfilled'
    const REJECTED = 'rejected'
    class myPromise {
    state = PENDING//默认值
    result
    #handler = []
    constructor(func) {
    //func是创建promise实例的时候,传入的回调函数
    //这个回调函数接受两个参数resolve,reject,我们在构造函数内部准备这两个方法
    const resolve = (res) => {
    //如果promise实例的状态还没有改变
    if (this.state = PENDING) {
    //那就改变它的状态为'fulfilled'
    this.state = FULFILLED
    //res是传入的值
    this.result = res
    //resolve函数执行之时,就是promise状态改变之时,就是传入then方法的回调函数该被调用的时候
    this.#handler.forEach(item => { item.onFulFilled(this.result) })
    }
    }
    const reject = (err) => {
    //如果promise实例的状态还没有改变
    if (this.state = PENDING) {
    //那就改变它的状态为'rejected'
    this.state = REJECTED
    this.result = err
    //reject函数执行之时,就是promise状态改变之时,就是传入then方法的回调函数该被调用的时候
    this.#handler.forEach(item => { item.onRejected(this.result) })
    }
    }
    //同步调用传入的回调函数,传入2个准备好的能改变promise实例状态的函数
    //因为用户传入的回调函数func本身也可能报错,所以我们用try-catch捕获一下
    try {
    func(resolve, reject)
    } catch (err) {
    reject(err)
    }
    }
    //暂时只考虑同步修改promise状态
    then(onFulFilled, onRejected) {
    if (this.state == FULFILLED) {
    //执行传入的回调函数,并把值暴露出去
    onFulFilled(this.result)
    } else if (this.state == REJECTED) {
    //执行传入的回调函数,并把值暴露出去
    onRejected(this.result)
    } else {
    //如果promise实例的状态还未改变,就先把传入的回调函数委托给别人,也可以理解为放入任务队列中
    this.#handler.push({
    onFulFilled
    , onRejected
    })
    }
    }
    }
    const p = new myPromise((resolve, reject) => {
    setTimeout(() => { resolve(123) }, 1000)
    })
    p.then(res => { console.log(res) }, err => { console.log(err) })//1s后输出123
  • 处理then的返回值

    因为then方法是支持链式调用的,意味着then方法的返回值也是一个promise对象,这个返回的promise实例的状态和值是由传入then方法的回调函数的返回值决定的。这就意味着,我们不只关心调用传入then方法的回调函数何时被执行,还要关注它们的返回值。

    我们先修改一下then方法,确保能返回一个promise对象,这样书写并不会改变代码原有的功能,因为构造函数中的代码是立即执行的,原来的代码也是同步执行的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    then(onFulFilled, onRejected) {
    return new myPromise((resolve, reject) => {
    //原来的代码顶部------------
    if (this.state == FULFILLED) {
    onFulFilled(this.result)
    } else if (this.state == REJECTED) {
    onRejected(this.result)
    } else {
    //如果promise实例的状态还未改变
    this.#handler.push({
    onFulFilled
    , onRejected
    })
    }
    //原来的代码底部---------------
    })
    }

    然后我们再思考返回的promise对象的状态与值如何确定,代码如下

    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
    then(onFulFilled, onRejected) {
    return new myPromise((resolve, reject) => {
    //无论同步还是异步,我们都把传入的回调函数交给构造函数中定义的那2个函数来执行,简化了代码
    //可以注意到onFulFilled/onRejected, resolve, reject这几个方法都没在then函数中调用
    //但是只要保证“这个函数是这个函数,无论它在哪里被调用,都会起到本来的作用”
    this.#handler.push({
    onFulFilled:()=>{this.wrap(onFulFilled, resolve, reject)}
    , onRejected:()=>{this.wrap(onRejected, resolve, reject)}
    })
    }
    })
    }

    //这个wrap方法到底做了什么?
    //1.调用传入的回调函数
    //2.分析回调函数的返回值,调用用resolve或者reject方法
    //传入的resolve,reject,是被用来修改then方法返回的myPromise实例的状态的
    wrap(func, resolve, reject) {
    //wrap中this的指向等于then的this指向,指向同一个promise对象
    //因为调用传入的onFulFilled/onRejected函数可能会报错,所以使用try-catch捕获
    try {
    //需要拿到返回值,来确定then函数返回的promise实例的状态
    const x = func(this.result)
    //如果返回的是一个myPromise对象,它的状态可能是同步改变或者异步改变
    //因为我们已经解决了异步回调的情况的,所以也能通过then方法拿到它的result,
    if (x instanceof myPromise) {
    //wow,相当于由一个promise对象的状态来确定另一个promise的状态
    x.then(res => { resolve(res) }, err => { reject(err) })
    } else {
    //如果返回值不是myPromise对象,那就简单了,直接resolve
    resolve(x)
    }
    }catch (err) {
    //如果报错直接返回状态为rejeted的promise实例
    reject(err)
    }
    }

    举例测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //创建一个1s后状态改变的promise对象
    const p = new myPromise((resolve, reject) => {
    setTimeout(() => { resolve(1) }, 1000)
    })
    p.then(res => {
    console.log(res)
    return new myPromise((resolve) => {
    setTimeout(() => {
    resolve(2)
    }, 1000);
    })
    }, err => { console.log(err) })
    //第一次调用then方法立马返回一个promise对象,但是由于对象的状态还未确定,2s后才会打印出2
    .then(res => { console.log(res) }, err => { console.log(err) })
    • p调用then方法,因为p状态未确定,回调函数被wrap包装,然后push到#handler
    • 虽然传入的回调函数没有立马被调用,但是then方法已经返回了一个状态未改变的promise对象
    • 第一个then方法返回的对象状态未改变,第2次then的调用执行了,但是没动静
    • 1s后,因为在构造函数内调用resolve(1)方法,p的状态改变,遍历执行#handler中的待执行的回调函数,执行被包装的回调函数
    • 执行被包装的函数,也就是第一个then传入的第一个回调函数,输出1,再等待1s后,调用resolve(2)方法
      此时第一个then方法返回的对象状态也被改变,于是第二个then传入的回调函数也被触发,输出2。

实例方法

  • 实现catch方法

    1
    2
    3
    catch(onRejected) {
    this.then(undefined, onRejected)
    }
  • 实现finally

    1
    2
    3
    finally(onFinally) {
    this.then(onFinally, onFinally)
    }
  • 实现resolve静态方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    static resolve(res) {
    //如果本来就是myPromise实例,则直接返回
    if (res instanceof myPromise) {
    return res
    }
    return new myPromise((resolve, reject) => {
    resolve(res)
    })
    }

静态方法

  • 实现reject静态方法

    1
    2
    3
    4
    5
    6
    7
    8
     static reject(err) {
    if (res instanceof myPromise) {
    return res
    }
    return new myPromise((resolve, reject) => {
    reject(err)
    })
    }
  • 实现race静态方法

    • 返回值是一个promise对象

    • 传入一个数组,返回最先兑现的promise,无论是resolve还是reject,只取一个值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    static race(arr) {
    //传入的必须是一个数组
    return new myPromise((resolve, reject) => {
    if (!(arr instanceof Array)) {
    reject(new TypeError('arguments is not iterable'))
    }
    //如果数组长度为0
    if (arr.length == 0) {
    resolve([])
    }
    //对每个元素都调用then方法(同时),不是myPromise对象则先包裹
    //再等待它们状态的改变,来改变最终返回的promise的状态
    arr.forEach(i => { myPromise.resolve(i).then(res => { resolve(res) }, err => { reject(err) }) })
    })
    }
  • 实现all静态方法

    • 返回值是一个promise对象

    • 要求传入的数组中的所有myPromise对象的状态都resolve后,再resolve(//包含所有对象值的数组)

    • 如果任意一个对象reject了,则reject这个对象的值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    static all(arr) {
    return new myPromise((resolve, reject) => {
    //如果传入的不是数组,报错
    if (!(arr instanceof Array)) {
    reject(new TypeError('arguments is not iterable'))
    }
    if (arr.length == 0) {
    resolve([])
    }
    const result = []
    let count = 0
    //要求返回的数组中元素的排列顺序就是传入的顺序
    arr.forEach((i, index) => {
    myPromise.resolve(i).then(res => {
    result[index] = res
    if (++count == arr.length) {
    resolve(result)
    }
    }, err => { reject(err) })
    })
    })
    }
  • 实现any静态方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    static any(arr) {
    return new myPromise((resolve, reject) => {
    if (!(arr instanceof Array)) {
    reject(new TypeError('arguments is not iterable'))
    }
    if (arr.length == 0) {
    reject(new AggregateError([], 'All promises were rejected'))
    }
    const errs = []
    let count = 0
    arr.forEach((i, index) => {
    myPromise.resolve(i).then(res => {
    resolve(res)
    }, err => {
    errs[index] = err
    if (++count == arr.length) {
    //rejects的不是errs数组,而是一个异常对象
    reject(new AggregateError(errs, 'All promises were rejected'))
    }
    })
    })
    })
    }
  • 实现allSettled方法

    传入Promise都变成已敲定,即可获取兑现的结果,返回的promise对象最终会被兑现(状态变为fulfilled)

    结果数组[{status: 'fulfilled', value: 1}, {status: 'rejected', value: 3)]

    结果数组的顺序,和传入的Promise数组的顺序一致

    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
    static allSettled(arr) {
    return new myPromise((resolve, reject) => {
    if (!(arr instanceof Array)) {
    reject(new TypeError('arguments is not iterable'))
    }
    if (arr.length == 0) {
    resolve([])
    }
    const result = []//结果数组,是一个对象数组
    let count = 0
    arr.forEach((i, index) => {
    myPromise.resolve(i).then(res => {
    result[index] = { state: FULFILLED, value: res }
    if (++count == arr.length) {
    //总是resolve一个数组
    resolve(result)
    }
    }, err => {
    result[index] = { state: REJECTED, reason: err }
    if (++count == arr.length) {
    //总是resolve一个数组
    resolve(result)
    }
    })
    })
    })
    }
  • 最终代码

    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
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    class myPromise {
    state = 'pending'
    value = ''
    handler = []
    constructor(func) {
    const resolve = (val) => {
    if (this.state === 'pending') {
    this.state = 'fulfilled'
    this.value = val
    //这里使用异步的目的是,确保在then方法被调用,push回调函数到hander之后,再查找是否有需要执行的回调函数
    //这样我们就能子啊then方法中,简化代码,无论何种情况,都把回调函数push到handler中,交给reject或者resolve执行
    //如果我们不使用异步(setTimeout),且使用上述简化方式,对于promise状态同步改变的情况
    //在调用then方法之前,resolve或者reject函数早已被执行,遍历的handler是空的。
    setTimeout(() => {
    this.handler.forEach((obj) => {
    obj.onFulFilled()
    })
    }, 0)
    }
    }
    const reject = (val) => {
    if (this.state === 'pending') {
    this.state = 'rejected'
    this.value = val
    //这里使用异步的目的是,确保在then方法调用,push回调函数到hander之后,再查找是否有需要执行的回调函数
    setTimeout(() => {
    this.handler.forEach((obj) => {
    obj.onRejected()
    })
    }, 0)
    }
    }
    try {
    func(resolve, reject)
    } catch (err) {
    reject(err)
    }
    }

    then(onFulFilled, onRejected) {
    return new myPromise((resolve, reject) => {
    this.handler.push({
    onFulFilled: () => { this.wrap(onFulFilled, resolve, reject) },
    onRejected: () => { this.wrap(onRejected, resolve, reject) }
    })
    })
    }

    wrap(func, resolve, reject) {
    try {
    //这一段是核心代码,但是我们还要考虑返回值
    const res = func(this.value)
    if (res instanceof myPromise) {
    res.then((res) => { resolve(res) }, (err) => { reject(err) })
    } else {
    resolve(res)
    }
    } catch (error) {
    reject(error)
    }
    }
    static resolve(p) {
    if (p instanceof myPromise) {
    return p
    }
    return new myPromise((resolve, reject) => {
    resolve(p)
    })
    }
    static reject(p) {
    if (p instanceof myPromise) {
    return p
    }
    return new myPromise((resolve, reject) => {
    reject(p)
    })
    }
    static all(arr) {
    //不做类型判断,假设用户传入的总是一个数组
    const list = []
    let count = 0
    return new myPromise((resolve, reject) => {
    arr.forEach((p, index) => {
    myPromise.resolve(p).then((res) => {
    list[index] = res
    count++
    if (count === arr.length) {
    resolve(list)
    }
    }, (err) => { reject(err) })
    })
    })
    }
    static any(arr) {
    //不做类型判断,假设用户传入的总是一个数组
    const list = []
    let count = 0
    return new myPromise((resolve, reject) => {
    arr.forEach((p, index) => {
    myPromise.resolve(p).then((res) => {
    resolve(res)
    }, (err) => {
    list[index] = err
    count++
    if (count === arr.length) {
    reject(list)
    }
    })
    })
    })
    }
    static race(arr) {
    return new myPromise((resolve, reject) => {
    arr.forEach((p, index) => {
    myPromise.resolve(p).then((res) => {
    resolve(res)
    }, (err) => {
    reject(err)
    })
    })
    })
    }
    static allSettled(arr) {
    return new myPromise((resolve, reject) => {
    arr.forEach((p, index) => {
    myPromise.resolve(p).then((res) => {
    list[index] = { state: 'fulfilled', value: res }
    count++
    if (count === arr.length) {
    resolve(list)
    }
    }, (err) => {
    list[index] = { state: "rejected", value: err }
    count++
    if (count === arr.length) {
    resolve(list)
    }
    })
    })
    })
    }
    }

使用场景

使用Promise.all()合并多个请求,只需设置一个loading即可

1
2
3
4
5
6
7
8
9
10
11
12
function initLoad(){
// loading.show() //加载loading
Promise.all([getBannerList(),getStoreList(),getCategoryList()]).then(res=>{
console.log(res)
loading.hide() //关闭loading
}).catch(err=>{
console.log(err)
loading.hide()//关闭loading
})
}
//数据初始化
initLoad()

不过这样取数据也就要从Promise.all的返回的promise对象中取数据了。

通过race可以设置图片请求超时时间

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
//请求某个图片资源
function requestImg(){
var p = new Promise(function(resolve, reject){
var img = new Image();
//也是异步回调确定p的状态
img.onload = function(){
//图片加载完毕后,改变promise实例的状态
resolve(img);
}
img.src = "https://b-gold-cdn.xitu.io/v3/static/img/logo.a7995ad.svg1";
});
return p;
}

//延时函数,返回一个5s后reject的promise对象
function timeout(){
var p = new Promise(function(resolve, reject){
setTimeout(()=>{
reject('图片请求超时');
}, 5000);
});
return p;
}

Promise
.race([requestImg(), timeout()])
.then(function(results){
console.log(results);
})
.catch(function(reason){
console.log(reason);
});

for in 和 for of的区别

for in

for in语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性

一个直接创建的对象是非常常见的,它的原型是Object.prototype,上面的属性都是不可枚举的,所以我们使用for in遍历这种对象时,只会得到它的自己的所有可枚举属性。

for of

for of语句可以遍历可迭代对象,包括Array,Map,Set,string,TypedArray,arguments对象等等,遍历的是继承而来的无法遍历;保证迭代顺序,而一个直接创建的对象是不可迭代的,无法直接使用for of来遍历。

可迭代对象

即满足了迭代协议的对象,需要存在一个名为[Symbol.iterator]的方法,这个方法返回一个迭代器对象

直接创建的对象不是可迭代对象,因为没有这个方法,但是我们也可以手动添加这个方法,让它变成可迭代对象

1
{ name:'1',age:'21',[Symbol.iterator]:function(){return 迭代器对象}}

迭代器对象

其实迭代器对象就是一个普通对象,包含名为 next() 的方法,并没有想象的那么复杂;next方法返回一个包含两个属性对象:value 和 done。value 是迭代的当前值,done 是一个布尔值,表示是否已经到达了迭代的末尾。

那么改如何实现一个迭代器对象呢?

  • 手动创建

    根据迭代器对象的定义,我们可以尝试手动创建一个迭代器对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const obj = {
    [Symbol.iterator]() {
    const arr = [1, 2, 3]
    let index = 0
    //返回一个迭代器对象
    return {
    next() {
    if (index < arr.length) {
    return { done: false, value: arr[index++] }
    }
    return { done: true, value: 'end' }
    }
    }
    }
    }
  • 借助生成器函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function* generator() {
    yield '1'
    yield '2'
    //如果生成器函数中有return,return后迭代也就终止了,无论后面还有没有yield
    return
    yield '3'
    }
    const obj = {
    [Symbol.iterator]() {
    //直接返回一个迭代器对象
    return generator()
    }
    }
    for(item of obj){
    console.log(item)//依次输出1,2
    }

当使用 for...of 遍历一个可迭代对象时,它实际上调用了该对象的 [Symbol.iterator] 方法,并根据迭代器提供的值进行迭代

区别

  • 遍历的东西不同

    for in 遍历的对象的属性,for of遍历的对象的值

  • 遍历的范围不同

    for in遍历的是对象自己的可枚举属性以及它继承的所有可枚举属性

    for of遍历的是对象自己的属性的值

  • 使用的范围不同

    for in可以用于遍历任何对象,而for of只能用来遍历可迭代对象

你是怎么理解ES6中 Generator的?使用场景?

是什么

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同

回顾下上文提到的解决异步的手段:

  • 回调函数
  • promise

执行 Generator 函数会返回一个迭代器对象(iterator),可以依次遍历 Generator 函数内部的每一个状态

形式上,Generator函数是一个普通函数,但是有两个特征:

  • function关键字与函数名之间有一个星号

  • 函数体内部使用yield(屈服,'叶儿得')表达式,定义不同的内部状态

    1
    2
    3
    4
    5
    function* helloWorldGenerator() {
    yield 'hello';
    yield 'world';
    return 'ending';
    }

使用

Generator 函数会返回一个迭代器对象

1
2
3
4
5
6
7
function* gen(){
// some code
}

var g = gen();

g[Symbol.iterator]() === g //true

通过yield关键字,可以暂停generator函数返回的迭代器对象的状态

1
2
3
4
5
6
7
8
9
10
function* helloWorldGenerator() {
console.log(1)
yield 'hello';
console.log(2)
yield 'world';
console.log(3)
return 'ending';
console.log(4)
}
var hw = helloWorldGenerator();//并不会输出1
1
2
3
4
console.log(hw.next())//输出1 输出{ value: 'hello', done: false }
console.log(hw.next())//输出2 输出{ value: 'world', done: false }
console.log(hw.next())//输出3 输出{ value: 'ending', done: true }
console.log(hw.next())//并不输出4 输出{ value: undefined, done: true }

done用来判断是否存在下个状态,value对应状态值

再举个例子

1
2
3
4
5
6
7
8
9
10
function* helloWorldGenerator2() {
console.log(1)
yield 'hello';
console.log(2)
yield 'world';
console.log(3)
yield 'ending';//这里做了修改
console.log(4)
}
var hw2 = helloWorldGenerator2();//并不会输出1
1
2
3
4
console.log(hw.next())//输出1 输出{ value: 'hello', done: false }
console.log(hw.next())//输出2 输出{ value: 'world', done: false }
console.log(hw.next())//输出3 输出{ value: 'ending', done: false } 这里有区别
console.log(hw.next())//输出4 输出{ value: undefined, done: true }

yield表达式本身没有返回值,或者说总是返回undefined

1
2
3
4
5
6
7
8
9
10
function* foo(x) {
var y = 2 * (yield (x + 1));//y=undefined
var z = yield (y / 3);//y/3=NaN
return (x + y + z);
}

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

通过调用next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值

1
2
3
4
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false } y=2*12 y/3=8
b.next(13) // { value:42, done:true } y=24 z=13 x=5

正因为Generator函数返回Iterator(迭代器)对象,因此我们还可以通过for...of进行遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {、
//没有输出6,这是因为 for...of 循环只处理迭代过程中由 yield 产生的值。当迭代器完成(即状态变为 done: true),循环就会终止,并不会检查或处理 return 语句给出的值。
//不过要注意的是,next是可以取出return的值的,不过for of不行。
console.log(v);// 1 2 3 4 5
}

原生对象没有遍历接口,通过Generator函数为它加上这个接口,就能使用for...of进行遍历了,for...of本质遍历的就是迭代器对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//根据传入的对象,创造一个生成器函数
function* objectEntries(obj) {
//返回一个数组,包含对象自身的所有属性,包括不可枚举属性
let propKeys = Reflect.ownKeys(obj);
for (let propKey of propKeys) {
yield [propKey, obj[propKey]];
}
}

let jane = { first: 'Jane', last: 'Doe' };
//数组解构
for (let [key, value] of objectEntries(jane)) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

总结

异步编程解决方案

回顾之前展开异步编程解决的方案:

  • 回调函数
  • Promise 对象
  • generator 函数
  • async/await

回调函数

1
setTimeout(()=>{console.log(123)},1000)

Promise

Promise就是为了解决回调地狱而产生的,将回调函数的嵌套,改成链式调用

1
2
3
4
5
6
readFile('/etc/fstab').then(data =>{
console.log(data)
return readFile('/etc/shells')
}).then(data => {
console.log(data)
})

这种链式操作形式,使异步任务的两段执行更清楚了,但是也存在了很明显的问题,代码变得冗杂了,语义化并不强,Generator就是用来解决这个问题的。

Generator

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
function fetchUser(id) {
return new Promise((resolve, reject) => {
// 模拟异步操作
setTimeout(() => resolve({ id, name: 'Alice' }), 1000);
});
}
//定义一个生成器(Generator)
function* generatorFunc() {
console.log('Start fetching user...');
let user = yield fetchUser(123);
console.log('User fetched:', user);
}
//调用生成器函数,得到一个迭代器对象(iterator)
const it = generatorFunc()

function go(result) {
//如果迭代完,直接返回,跳出函数
if (result.done) return result.value;
//否则对拿到的结果调用then方法
result.value.then(function (value) {
//等待promise状态改变后再继续执行代码,并把拿到的值当作上一次yield表达式的值,赋值给user
go(it.next(value));----------
}).catch(function (error) {
go(it.throw(error));
});
}
go(it.next());//先输出 Start fetching user... 1s后输出User fetched: {id: 123, name: 'Alice'}

虽然生成器提供了处理异步代码的一种方式,但它的使用相对复杂,还是不够简洁,于是就有了asyncawait

async/await

async 函数本质上是构建在生成器之上的语法糖,它们内部实际上使用了 Promise,并且允许你以同步的方式编写异步代码,而不需要显式地处理迭代器或手动调用 next()

1
2
3
4
5
6
async function asyncFunc() {
console.log('Start fetching user...');
let user = await fetchUser(123); // 等待 Promise 完成
console.log('User fetched:', user);//后面的代码会等待promise状态改变后再执行。
}
asyncFunc();

值得注意的是,async函数无论是否有return语句,都会返回一个promise对象。

如果return一个非promise值,该值会被包装成已解决(fulfilled)的Promise对象;

如果没有return语句,默认会返回一个已解决的Promise,其值为undefined

如果返回一个promise对象,那么最终返回的也就是这个promise对象。

其实简单的来说,async函数的返回值会被Promise.resolve()方法包装。

要点总结

  • generator(生成器)函数指的是一种函数类型,用来创建一个迭代器iterator
  • 函数体内部使用yield(屈服,'叶儿得')表达式,定义不同的内部状态,在函数体内部也可以return一个值,也可以当作一种状态。
  • 调用迭代器(iterator)next()方法,会返回迭代器中的一个状态对象,形如{ value: 'hello', done: false }done表示是否所有的状态都被遍历,value则是本次遍历获得的状态值。
  • 只有当next访问不到状态,或者访问到return语句的时候,返回的对象才会显示done:true
  • generator函数体内不只可以写yield语句,还可以写其他语句比如return语句,console.log。
  • 创建好迭代器对象(iterator),并不会执行生成器函数内的任何代码,只有调用了next()方法,才会执行在某个状态前的所有代码。
  • 其实调用next方法是可以传值的,传递的值会被当作上一个yield表达式的返回值,否则yield是被认为没有返回值的,或者说总返回undefined。

你是怎么理解ES6中Proxy的?使用场景?

是什么

Proxy 是一个构造函数,用于创建一个对象的代理,从而拦截对该对象的基本操作。

1
var proxy = new Proxy(target, handler)

target表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理)

handler是一个属性值一般都是函数的对象,各属性中的函数分别定义了在执行各种操作时代理目标对象的行为

难点就在于分析这个handler,它可以包括多种拦截属性,下面我们只介绍常见的几种:

  • get(target,propKey,receiver):拦截对象属性的读取
  • set(target,propKey,value,receiver):拦截对象属性的设置
  • deleteProperty(target,propKey):拦截delete proxy[propKey]的操作,返回一个布尔值

handler

get()

get接受三个参数,依次为目标对象、属性名和 proxy 实例本身,最后一个参数可选

用来监听对某个属性的取值。

1
2
3
4
5
6
7
8
9
10
11
var person = {
name: "张三"
};

var proxy = new Proxy(person, {
get: function(target, propKey) {
return Reflect.get(target,propKey)
}
});

proxy.name // "张三"

注意:如果一个属性不可写(writable:false),则 Proxy 不能修改该属性,否则会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const target = Object.defineProperties({}, {
foo: {
value: 123,
writable: false,//默认值就是false,表示不可被修改
configurable: false//默认值就是false,表示不可被删除
},
});//{foo:123},不过这个属性是不可重写,不可配置的

const handler = {
get(target, propKey) {
// return target[propKey] 返回123,不报错
// return Reflect.get(target, propKey) 返回123,不报错
return 'abc';//返回abc,就相当于重写了,报错,
}
};

const proxy = new Proxy(target, handler);

proxy.foo

set()

set方法用来拦截对某个属性的赋值操作,可以接受四个参数,依次为目标对象属性名属性值Proxy 实例本身。

如果目标对象自身的某个属性,不可写(writable:false),那么set方法将不起作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const obj = {};
Object.defineProperty(obj, 'foo', {
value: 'bar',
writable: false,//默认值
});

const handler = {
set: function(obj, prop, value, receiver) {
return Reflect.set(obj,prop,value)
}
};

const proxy = new Proxy(obj, handler);
console.log(proxy.foo)//bar
proxy.foo = 'baz';
console.log(proxy.foo) // "bar",属性值并未被修改

注意严格模式下,set代理如果没有返回true,就会报错

deleteProperty

deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var handler = {
deleteProperty (target, key) {
invariant(key);
Reflect.deleteProperty(target,key)
return true;
}
};
function invariant (key) {
if (key[0] === '_') {
throw new Error(`无法删除私有属性`);
}
}

var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: 无法删除私有属性,抛出了异常就不会执行Reflect.deleteProperty(target,key)

注意,目标对象自身的不可配置(configurable:false)属性,不能被deleteProperty方法删除。

取消代理

1
Proxy.revocable(target, handler);

使用场景

Proxy其功能非常类似于设计模式中的代理模式,常用功能如下:。

  • 拦截和监视外部对对象的操作

  • 降低函数或类的复杂度

  • 在复杂操作前对操作进行校验或对所需资源进行管理

  • 使用Proxy实现观察者模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const queuedObservers = new Set();

    //添加一个订阅的方法
    const observe = fn => queuedObservers.add(fn);
    //返回一个代理对象
    const observable = obj => new Proxy(obj, {
    get(target, key){
    observe(func)
    return Reflect.get(target, key)
    },
    set(){
    ///当数据改变,就调用所有的订阅方法
    const result = Reflect.set(target, key, value, receiver);
    queuedObservers.forEach(observer => observer());
    return result;
    }
    });

你是怎么理解ES6中Module的?使用场景?

为什么需要模块化

如果没有模块化,我们代码会怎样?

  • 变量和方法不容易维护,容易污染全局作用域
  • 通过手动规定script标签的书写顺序来控制资源的加载顺序
  • 资源的依赖关系模糊,代码难以维护。

而模块化具有如下特点,能解决原生开发过程中的诸多问题

  • 代码抽象
  • 代码封装
  • 代码复用
  • 依赖管理

AMD

Asynchronous ModuleDefinition(AMD),异步模块定义,采用异步方式加载模块。所有依赖模块的语句,都定义在一个回调函数中,等到模块加载完成之后,这个回调函数才会运行

1
2
3
4
5
6
7
8
9
10
11
12
13
/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
baseUrl: "js/lib",
paths: {
"jquery": "jquery.min", //实际路径为js/lib/jquery.min.js
"underscore": "underscore.min",
}
});
// 执行基本操作
require(["jquery","underscore"],function($,_){
// some code here
});

CommonJs

CommonJS 是一套 nodejs 默认支持的模块规范,用于服务端。

其有如下特点:

  • 所有代码都运行在模块作用域,不会污染全局作用域
  • 模块是同步加载的,即只有加载完成,才能执行后面的操作
  • 模块在首次执行后就会缓存,再次加载只返回缓存结果,如果想要再次执行,可清除缓存
  • require返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个值

既然存在了AMD以及CommonJs机制,ES6Module又有什么不一样?

ES6 在语言标准的层面上,实现了Module,即模块功能,完全可以取代 CommonJSAMD规范,成为浏览器服务器通用的模块解决方案。

CommonJSAMD 模块语法,导入导出的都是整个对象代码运行时才能确定具体的依赖关系,比如具体使用了模块中的哪些变量。

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

使用

具体使用方式可以参考本博客中nodejs一文。