ES6

var,let,const有哪些区别

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

  • 变量提升

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

  • 重复声明

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

  • 作用域

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

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

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

数组新增了哪些扩展

扩展运算符 …

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

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

构造函数Array的新增方法

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

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

Array.from()

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

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

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

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

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
[1, 5, 10, 15].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'}] 数组中的这两个对象是同一个对象

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

严格模式

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

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
}
};

箭头函数

形如:

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

  • 只有一个参数的时候可以省略括号,只有一行代码且是return语句,可以省略大括号return关键字,如果返回的是一个对象,则需要加括号。

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

  • 没有自己的原型对象,所以不能当作构造函数使用,不能用来创造实例。

  • 无法使用applycallbind等方法改变箭头函数内部的this指向,但是可以调用这些方法。

  • 不可以使用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__

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

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

  • 集合
    是由一堆无序的、相关联的,且不重复的内存结构【数学中称为元素】组成的组合
  • 字典
    是一些元素的集合。每个元素有一个称作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
let set = new Set(['red', 'green', 'blue']);

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
// 数组
let arr = [3, 5, 2, 2, 5, 5];
//创建一个集合的同时传入一个数组,然后再把这个集合转变成数组,从而起到去重的作用
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类型是键值对的有序列表,而键和值都可以是任意类型

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

1
const m = new Map()

增删查改

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

  • size 属性

    size属性返回 键值对的个数

    1
    2
    3
    4
    5
    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 对象之中

    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(undefined, 'nah');
    m.has(undefined) // true

    m.delete(undefined)
    m.has(undefined) // 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);
});

你是怎么理解ES6中 Promise的

是什么

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

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

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

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

下面我们来正式介绍promise:

状态

promise对象仅有三种状态

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

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

一旦状态改变(从pending变为fulfilled和从pending变为rejected),就不会再变

实例方法

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

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

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

  • resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”
  • reject函数的作用是,将Promise对象的状态从“未完成”变为“失败“

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

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

then

then是实例状态发生改变时的回调函数,第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数

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

catch

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

一般来说,使用catch方法代替then()第二个参数

finally

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

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

静态方法

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

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

手写一个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
    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(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对象,执行构造函数本身也是同步的)

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

    因为p的状态还没有改变,所以我们还不能执行传入then方法的回调函数,那我们还怎么确保1sp状态改变,这个回调函数会被调用呢?

    我们先把回调函数存储到一个数组#handler中,当p状态改变的时候再查找这个数组,执行对应的回调函数

    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状态改变之时,就是回调函数该被调用的时候
    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状态改变之时,就是回调函数该被调用的时候
    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方法把传入的回调函数的返回值包装成promise对象后返回

    我们先修改一下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
    38
    39
    40
    41
    42
    //传入的resolve,reject,是被用来修改then方法返回的myPromise实例的状态的
    //这个wrap方法到底做了什么?
    //1.调用传入的回调函数,把promise对象的值(this.result)传入
    //2.分析回调函数的返回值,调用传入的,能够改变then方法返回的promise对象状态的函数
    //修改then方法返回对象的状态
    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)
    }
    }

    then(onFulFilled, onRejected) {
    return new myPromise((resolve, reject) => {
    if (this.state == FULFILLED) {
    this.wrap(onFulFilled, resolve, reject)
    } else if (this.state == REJECTED) {
    this.wrap(onRejected, resolve, reject)
    } else {
    //如果promise实例的状态还未改变
    this.#handler.push({
    onFulFilled:()=>{this.wrap(onFulFilled, resolve, reject)}
    , onRejected:()=>{this.wrap(onRejected, resolve, reject)}
    })
    }
    })
    }

    举例测试

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

    结果就是1s后输出1,3秒后输出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,无论是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静态方法

    要求传入的数组中的所有 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静态方法

    就是和all相反。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    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) {
    reject(new AggregateError(errs, 'All promises were rejected'))
    }
    })
    })
    })
    }
  • 实现allSettled方法

    传入Promise都变成已敲定,即可获取兑现的结果

    结果数组[{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
    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(result)
    }
    }, err => {
    result[index] = { state: REJECTED, reason: err }
    if (++count == arr.length) {
    resolve(result)
    }
    })
    })
    })
    }

使用场景

使用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()

通过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(){
resolve(img);
}
//img.src = "https://b-gold-cdn.xitu.io/v3/static/img/logo.a7995ad.svg"; 正确的
img.src = "https://b-gold-cdn.xitu.io/v3/static/img/logo.a7995ad.svg1";
});
return p;
}

//延时函数,用于给请求计时
function timeout(){
var p = new Promise(function(resolve, reject){
setTimeout(function(){
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() 的方法,该方法返回一个包含两个属性对象: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 函数会返回一个迭代器对象,可以依次遍历 Generator 函数内部的每一个状态

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

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

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

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

使用

Generator 函数会返回一个迭代器对象,即具有Symbol.iterator属性,并且返回给自己

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
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 语句给出的值。
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
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);
}
asyncFunc();

你是怎么理解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
    const queuedObservers = new Set();

    //添加一个订阅
    const observe = fn => queuedObservers.add(fn);
    //返回一个代理对象
    const observable = obj => new Proxy(obj, {set});

    //当数据改变,就调用所有的订阅方法
    function set(target, key, value, receiver) {
    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一文。