var,let,const有哪些区别
在ES5中,顶层对象(在浏览器中是window
)的属性和全局变量是等价的,或者说全局变量会被挂载到window
对象中
变量提升
var
声明的变量存在变量提升,变量提升只提升变量声明,不提升变量赋值。而let
和const
不存在变量提升重复声明
var声明的变量可以被
重复声明
,后面声明的会覆盖前面声明的。而let
和const
声明的变量无法被重复声明
。作用域
var声明的变量只会产生
函数作用域
,不会产生块级作用域let用来声明一个
变量
,会产生一个块级作用域,const用来声明一个
常量
,也会产生一个块级作用域。
变量提升与函数提升
只有var声明的变量
才存在变量提升,只有具名函数
才存在函数提升。
函数提升优先级比变量提升要高,且不会被变量声明覆盖,但是会被变量赋值
覆盖。
1 | console.log(foo);//输出函数ƒ foo(){...} |
1 | function foo(){ |
1 | var 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
10let 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
2const obj = {a: 1, b: 2};
let arr = [...obj]; // TypeError: Cannot spread non-iterable object除非写成
{...obj}
的形式,表示拷贝对象。
构造函数Array的新增方法
关于构造函数,数组新增的方法有如下:
- Array.from()
- Array.of()
Array.from()
将两类对象转为真正的数组:类似数组的对象(伪数组
)和可迭代对象
(包括 ES6
新增的数据结构 Set
和 Map
)
伪数组(类似数组的对象)指的是:
- 具有
length
属性。 - 按照索引存储元素(即可以通过
[0]
,[1]
,[2]
等方式访问元素)
常见的伪数组对象:
函数中的
arguments
对象:1
2
3
4function example() {
return Array.from(arguments);
}
console.log(example(1, 2, 3)); // 输出: [1, 2, 3]DOM 操作返回的
NodeList
1
2
3const nodeList = document.querySelectorAll('div');
const divArray = Array.from(nodeList);
console.log(divArray); // 转换为真正的数组
示例:
1 | let arrayLike = { |
1 | // Set |
从上述例子中可以看出,数组和Set或者Map二者之间是可以相互转化的。
还可以接受第二个参数
,用来对每个元素进行处理,将处理后的值放入返回的数组,效果就类似Map
1 | Array.from([1, 2, 3], (x) => x * x)// [1, 4, 9] |
我们在学习过程中,想必也遇到过通过[...obj]
的方式把一个对象转换成数组的情况,它和Array.from()
有什么区别呢?
扩展运算符 [...]
的作用是将可迭代对象展开为数组,
1 | let arrayLike = { |
扩展运算符 [...]
只能用于可迭代对象(如 Set
、Map
、String
等)
特性 | Array.from() | [...] |
---|---|---|
支持伪数组 | ✅ 支持 | ❌ 不支持 |
支持可迭代对象 | ✅ 支持 | ✅ 支持 |
是否需要 [Symbol.iterator] | 不需要 | 需要 |
Array.of()
用于将一组值,转换为数组
1 | Array.of(3, 11, 8) // [3,11,8] |
当参数只有一个的时候,实际上是指定数组的长度
参数个数不少于 2 个时,Array()
才会返回由参数组成的新数组
1 | Array.of() // [] |
新增方法
- find()、findIndex()
- fill()
- entries(),keys(),values()
- includes()
find,findIndex
find()
用于找出,返回第一个符合条件的数组成员
参数是一个回调函数,接受三个参数依次为当前的值、当前的位置和原数组
1 | let a = [1, 5, 10, 15] |
1 | findIndex`返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回`-1 |
fill
使用给定值,填充一个数组
1 | ['a', 'b', 'c'].fill(7) |
还可以接受第二个和第三个参数,用于指定填充的起始位置
和结束位置
,左闭右开
1 | ['a', 'b', 'c'].fill(7, 1, 2) |
注意,如果填充的类型为对象,则是浅拷贝,即被填充的数据,使用的都是同一个对象
1 | ['a', 'b'].fill({name:'tom'}) |
除非每次填充都使用新创建的对象
1 | ['a', 'b'].fill(new Array(3)) |
includes
用于判断数组是否包含给定的值,相比indexOf
方法,优化了对NaN
的判断
1 | [1, 2, 3].includes(2) // true |
函数新增了哪些扩展
参数
ES6
允许为函数的参数设置默认值
1 | function log(x, y = 'World') { |
函数的形参是默认声明的,不能使用let
或const
再次声明
1 | function foo(x = 5) { |
解构赋值
过程中也可以给形参
添加默认值
1 | function foo({x, y = 5}) { |
属性
函数本质也是个对象,有许多属性
func.length
将返回没有指定
默认值
的参数个数,具体情况还得具体分析,感觉很鸡肋。func.name
如果把
匿名函数
赋值给一个变量,则name属性返回这个变量的名字1
2
3
4
5var f = function () {};
// ES5
f.name // ""
// ES6
f.name // "f"如果将一个
具名函数
赋值给一个变量,则name
属性都返回这个具名函数
原本的名字1
2const bar = function baz() {};
bar.name // "baz"bind
返回的函数,name
属性值会加上bound
前缀1
2function foo() {};
foo.bind({}).name // "bound foo"
作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域
等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的
下面例子中,y=x
会形成一个单独作用域,x
没有被定义,所以指向全局变量x
1 | let x = 1; |
严格模式
必须写在
当前作用域
的作用域顶部
才能生效当一个函数被直接调用,无论这个函数在哪儿被直接调用,先看
全局作用域
中是否开启了严格模式,如果开启了,则this指向undefined
,如果未开启,再查看这个被直接调用的函数内部,是否开启了严格模式,如果开启了,则this指向undefined
,否则this指向全局对象,在浏览器中指的就是window对象
1 | let num = 117 |
1 | ; |
严格模式不能随便开启
只要函数形参
使用了默认值
、解构赋值
、或者扩展运算符
,那么函数内部就不能显式设定为严格模式,否则会报错。所以说函数内部也不能随便开启严格模式。
1 | // 报错 |
严格模式只能改变直接被调用的函数内部的this的指向
1 | <script> |
即便开启了严格模式,全局作用域中的this还是指向全局对象,所以说,全局作用域中的this始终指向全局对象
箭头函数
形如:
1 | ()=>{} |
更适用于那些本来需要
匿名函数
的地方,类似lambda
表达式,它和普通匿名函数
一样,它属于表达式函数
,不存在函数提升
箭头函数看起来是匿名的,但是可以通过前面的变量名或者属性名,推断出同名的name
1
2
3
4const func = () => {
console.log('你好啊')
}
console.log(func.name)//输出func只有一个
参数
的时候可以省略括号;只有一行代码且是return
语句,可以省略大括号
和return
关键字,如果返回的是一个对象,则需要加括号。1
2item => item.name //等同于(item)=>{ return item.name }
item => ({name:'tom'})没有自己的环境变量
this
,内部的this指向被定义的时候外层函数
的this,this指向和如何被调用无关因为没有自己的环境变量
this
,所以无法使用apply
,call
,bind
等方法改变箭头函数内部的this
指向,但是可以调用
这些方法。内部也没有
arguments
对象。arguments
在一般函数内部可以直接使用(如同this),即便函数没有形参,也可以给函数传参,传递的所有参数都会被收集到arguments对象没有自己的
原型对象(prototype)
,所以不能当作构造函数
使用,不能用来创造实例(不能对箭头函数使用new关键字)。内部不可以使用
yield
命令,因此箭头函数不能用作Generator
函数
对象新增了哪些扩展
属性的简写
ES6中,当对象键名与对应值名相等的时候,可以进行简写
1 | const baz = {foo:foo} |
方法也能够进行简写
1 | const o = { |
属性名表达式
eS6 允许字面量定义对象时,将表达式放在中括号内,当作对象的属性。
1 | let lastWord = 'last word'; |
注意,属性名表达式
与属性名简写
,不能同时使用,会报错。
1 | // 报错 |
super关键字
this
关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super
,指向当前对象的原型对象
即super=this.__proto__
解构赋值
这项特性允许开发者从复杂的数据结构如对象或数组中提取数据,并直接将这些数据赋值给变量。这种机制不仅使得代码更加简洁易读,还提高了开发效率。
1 | const person = { |
要注意的是因为使用的是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 | let set = new Set(['red', 'green', 'blue']); |
forEach
1 | let set = new Set([1, 4, 9]); |
使用场景
扩展运算符和Set
结构相结合实现数组
或字符串
去重
1 | // 数组 |
下面的例子说明无论是map还是map.keys()都能转换成数组
1 | let map = new Map([ |
Map
Map
类型是键值对的有序列表,而键和值都可以是任意类型
Map
本身是一个构造函数,用来生成 Map
数据结构
1 | const m = new Map() |
增删查改
Map
结构的实例针对增删改查有以下属性和操作方法:
size 属性
size
属性返回键值对的个数1
2
3
4const map = new Map();
map.set('foo', true);
map.set('bar', false);
map.size // 2set()
设置键名
key
对应的键值为value
,然后返回整个Map结构如果
key
已经有值,则键值会被更新
,否则就新生成该键同时返回的是当前
Map
对象,可采用链式写法
1
2
3
4
5
6const 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
6const 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
10const 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) // truedelete()
1
2
3
4
5
6
7delete`方法删除某个键,返回`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
7let 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 | //传入一个二维数组说是 |
Map和Obj的区别
很多时候我们都可以使用Obj来实现Map的功能,毕竟都是键值对的形式,那二者具体有什么区别呢,Map被设计出来有什么优势呢?
Map中的键可以是任意类型
当使用普通对象 {}
作为键值对存储时,默认情况下只能使用字符串
或符号(Symbol)
作为键。如果尝试用其他类型的值作为键,JavaScript 会自动将其转换为字符串。
1 | let obj = {}; |
从上面的例子可以看出,{ name: 'key' }
和{ name: 'key2' }
都被转化成[object Object]
,它们被视为相同的键
相比之下,Map
可以直接使用任何类型的值作为键,并且不会进行隐式的类型转换。
1 | let obj = new Map(); |

键值对顺序
- **
Object
**:虽然 ES6 之后的对象保留了属性插入的顺序(对于可枚举属性),但这种顺序性并不是所有情况下都保证的,特别是对于旧版浏览器。 Map
:Map
明确保持键值对插入的顺序。这意味着你可以依赖于键值对,按照它们被添加的顺序进行迭代
获取大小
Object
没有直接的方法,来获取对象中属性的数量。你需要手动计算,例如通
Object.keys()
计算返回的数组的长度1
2const obj = { a: 1, b: 2 };
console.log(Object.keys(obj).length); // 输出 2Map
提供了size属性,可以直接获取键值对的数量。
性能
在频繁地增删键值对时,Map
的性能通常优于 Object
。这是因为 Map
是专门为动态场景设计的,而对象更适合静态结构的数据。
Set和Map的区别
特性 | Set | Map |
---|---|---|
数据结构 | 存储唯一值的集合(类似数组) | 存储键值对的集合(类似对象) |
键值类型 | 仅存储值,无键名 | 键可以是任意类型(包括对象、函数等),值不限类型 |
唯一性规则 | 值必须唯一(基于 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 | const promise = new Promise(function(resolve, reject) { |
Promise
构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
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 | promise |
静态方法
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
45const 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
6const 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
4const 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
60const 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
17then(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
37then(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。
- p调用then方法,因为p状态未确定,回调函数被
实例方法
实现
catch
方法1
2
3catch(onRejected) {
this.then(undefined, onRejected)
}实现
finally
1
2
3finally(onFinally) {
this.then(onFinally, onFinally)
}实现
resolve
静态方法1
2
3
4
5
6
7
8
9static resolve(res) {
//如果本来就是myPromise实例,则直接返回
if (res instanceof myPromise) {
return res
}
return new myPromise((resolve, reject) => {
resolve(res)
})
}
静态方法
实现
reject
静态方法1
2
3
4
5
6
7
8static 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
15static 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
22static 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
23static 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
27static 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
142class 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 | function initLoad(){ |
不过这样取数据也就要从Promise.all
的返回的promise对象中取数据了。
通过race
可以设置图片请求超时时间
1 | //请求某个图片资源 |
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
15const 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
16function* 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
5function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
使用
Generator
函数会返回一个迭代器对象

1 | function* gen(){ |
通过yield
关键字,可以暂停generator
函数返回的迭代器对象
的状态

1 | function* helloWorldGenerator() { |
1 | console.log(hw.next())//输出1 输出{ value: 'hello', done: false } |
done
用来判断是否存在下个状态,value
对应状态值
再举个例子
1 | function* helloWorldGenerator2() { |
1 | console.log(hw.next())//输出1 输出{ value: 'hello', done: false } |
yield
表达式本身没有返回值,或者说总是返回undefined
1 | function* foo(x) { |
通过调用next
方法可以带一个参数,该参数就会被当作上一个yield
表达式的返回值
1 | var b = foo(5); |
正因为Generator
函数返回Iterator(迭代器)
对象,因此我们还可以通过for...of
进行遍历
1 | function* foo() { |
原生对象
没有遍历接口
,通过Generator
函数为它加上这个接口,就能使用for...of
进行遍历了,for...of
本质遍历的就是迭代器对象
。
1 | //根据传入的对象,创造一个生成器函数 |
总结
异步编程解决方案
回顾之前展开异步编程解决的方案:
- 回调函数
- Promise 对象
- generator 函数
- async/await
回调函数
1 | setTimeout(()=>{console.log(123)},1000) |
Promise
Promise
就是为了解决回调地狱
而产生的,将回调函数的嵌套
,改成链式调用
1 | readFile('/etc/fstab').then(data =>{ |
这种链式操作形式,使异步任务的两段执行更清楚了,但是也存在了很明显的问题,代码变得冗杂了,语义化并不强,Generator
就是用来解决这个问题的。
Generator
1 | function fetchUser(id) { |
虽然生成器
提供了处理异步代码的一种方式,但它的使用相对复杂
,还是不够简洁,于是就有了async
和await
async/await
async
函数本质上是构建在生成器
之上的语法糖
,它们内部实际上使用了 Promise,并且允许你以同步的方式编写异步代码,而不需要显式地处理迭代器
或手动调用 next()
。
1 | async function 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 | var person = { |
注意:如果一个属性不可写(writable:false
),则 Proxy 不能修改该属性,否则会报错
1 | const target = Object.defineProperties({}, { |
set()
set
方法用来拦截对某个属性的赋值操作
,可以接受四个参数,依次为目标对象
、属性名
、属性值
和 Proxy
实例本身。
如果目标对象自身的某个属性,不可写(writable:false
),那么set
方法将不起作用
1 | const obj = {}; |
注意,严格模式
下,set
代理如果没有返回true
,就会报错
deleteProperty
deleteProperty
方法用于拦截delete
操作,如果这个方法抛出错误
或者返回false
,当前属性就无法被delete
命令删除
1 | var handler = { |
注意,目标对象
自身的不可配置(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
17const 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 | /** main.js 入口文件/主模块 **/ |
CommonJs
CommonJS
是一套 nodejs
默认支持的模块规范,用于服务端。
其有如下特点:
- 所有代码都运行在
模块作用域
,不会污染全局作用域 - 模块是
同步加载
的,即只有加载完成,才能执行后面的操作 - 模块在首次执行后就会
缓存
,再次加载只返回缓存结果,如果想要再次执行,可清除缓存 require
返回的值是被输出的值的拷贝
,模块内部的变化也不会影响这个值
既然存在了AMD
以及CommonJs
机制,ES6
的Module
又有什么不一样?
ES6
在语言标准的层面上,实现了Module
,即模块功能,完全可以取代 CommonJS
和 AMD
规范,成为浏览器
和服务器
通用的模块解决方案。
CommonJS
和AMD
模块语法,导入导出的都是整个对象
,代码运行时
才能确定具体的依赖关系,比如具体使用了模块中的哪些变量。
ES6
设计思想是尽量的静态化
,使得编译时
就能确定模块的依赖关系,以及输入和输出的变量。
使用
具体使用方式可以参考本博客中nodejs
一文。