文章目录
- 一.关于ES6
- 二.关于变量声明
- let声明变量
- const 声明常量
- 三.变量的解构赋值
- 四.字符串的扩展
- 五.函数的扩展
- 函数默认参数
- rest参数
- 箭头函数(函数的新写法)
- 六.数组的扩展
- 七.对象的扩展
- 语法上的简化
- 对象的解构赋值
- 八.Symbol:新的数据类型(类似于字符串)独一无二
- 九.Set:新的数据结构(类似于数组)其中数据独一无二,数组去重
- Set的属性和方法:都是定义在原型上的
- Set的遍历操作
- 关于for of循环
- Set 与数组的转换
- 十.Map:新的数据结构(类似于对象),属性名可是任何类型的
- Map的属性和方法
- Map的遍历方法
- Map与数组的转换
- 十一.promise对象:解决异步问题(面试常问)
- 基本概念
- 使用
- 关于catch
- Promise.all 全部
- Promise.race 比赛
- 十二.async await (ES7 2017Promise优化版)
- 十三. module 模块化
- 基本用法:
- export 改名
- import改名
- export default 默认导出
- 整个文件的引入
- 十四.class 类
- 基本概念
- constructor方法
- class的继承
- 十五.高级
- 事件循环eventloop解决异步问题(面试常问)
- 手撕promise源码(面试大厂可能会问)
- Promise完整源码--黑马
- 构造函数
- 状态及原因
- then方法
- then方法--成功和失败回调
- then方法-异步和多次调用
- 异步任务
- 链式编程
- 链式编程-fulfilled状态
- 链式编程-rejected状态
- 链式编程-pending状态
- 实例方法-Promise.catch()
- 实例方法-Promise.finally()
- 静态方法-Promise.resolve()
- 静态方法-Promise.reject()
- 静态方法-Promise.race()
- 静态方法-Promise.all()
- 静态方法-Promise.allSettled()
- 静态方法-Promise.any()
- 十六.find()
- 十七.面试题
阮一峰 ES6文档
由于JS语言出现的过于仓促,其中出现了很多并不合理的地方,es6就是对这些不合理的地方加以完善修改。
ES6只是用在VUE和REACT项目中用的比较多,而在jQuery中是不用ES6的。
一.关于ES6
JS:EcmaScript+DOM+BOM
ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。
ES6 的第一个版本,就这样在 2015 年 6 月发布了,正式名称就是《ECMAScript 2015 标准》(简称 ES2015)。2016 年 6 月,小幅修订的《ECMAScript 2016 标准》(简称 ES2016)如期发布,这个版本可以看作是 ES6.1 版,因为两者的差异非常小(只新增了数组实例的includes方法和指数运算符),基本上是同一个标准。根据计划,2017 年 6 月发布 ES2017 标准。
因此,ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。本书中提到 ES6 的地方,一般是指 ES2015 标准,但有时也是泛指“下一代 JavaScript 语言”。
二.关于变量声明
在原来js中的变量声明用var,使用var可以变量提升;可以重复声明;未声明直接赋值自动声明成全局变量;
let声明变量
- 没有变量提升
console.log(a)
let a=1; //ReferenceError: Cannot access 'a' before initialization
- 不允许重复声明
let a=1;
let a=2; //SyntaxError: Identifier 'a' has already been declared
- 只在块级作用域内有效
块级作用域:{},像 if(){}、for(){}、函数、while{}循环
<h1></h1>
应用:如在js中this给4个div绑定点击事件,点谁显示谁的内容。之前解决这种问题时使用this,现在可以使用let变量声明变量i,这样在每次循环时都会声明一个变量i,总共声明4个i,每次使用的对应的i。
<div>1</div><div>2</div><div>3</div><div>4</div><script>// var divs = document.getElementsByTagName("div")// for (var i = 0; i < divs.length; i++) {// divs[i].onclick = function () {// console.log(divs[i].innerText)// }// }// 出现错误,因为i是全局变量,在循环完成之后i=4,而点击之后没有divs[4],无法读取到其内容// 第一种解决方法:使用this指向当前调用函数的元素// var divs = document.getElementsByTagName("div")// for (var i = 0; i < divs.length; i++) {// divs[i].onclick = function () {// console.log(this.innerText)// }// }// 第二种解决方法:使用let声明i变量,由于let声明变量只在块级作用域内有效,发生4次循环,产生4个i变量,每次使用的都是当前的ivar divs = document.getElementsByTagName("div")for (let i = 0; i < divs.length; i++) {divs[i].onclick = function () {console.log(divs[i].innerText)}}</script>
{let a = 10;var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
var a = [];
for (var i = 0; i < 10; i++) {a[i] = function () {console.log(i);};
}
a[6](); // 输出结果:10 无论是a[几]()结果都是10,因为函数只有在调用的时候才会执行,在调用之前循环已经结束了,使用var声明的全局变量i=10,,在调用时输出值为10.
//如果将i用let声明,则调用a[几]()输出的就是几
- **暂时性死区:**在一个块级作用域内,如果存在有let声明的变量,那么该变量自动绑定当前作用域,在该作用域内,使用这个名字的变量只能用let声明的。总之,在代码块内,使用let命令声明变量之前,该变量都不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。”暂时性死区“也意味着typeof不再是一个百分百安全的操作。
var tmp = 123;
if (true) {tmp = 'abc'; // ReferenceError,必须使用let声明的tmp,不能使用其他类型的声明let tmp;
}
const 声明常量
const声明一个只读的常量。一旦声明,常量的值就不能改变。但是如果使用常量存储的数组或对象,其中他的内容是可以更改的,因为const声明的变量中存储的是指针是地址,其值并没有发生更改,改的是数组和对象里面的内容。
应用:获取元素对象、正则表达式都不会更改。
const obj = {name: "小明",age: 18}obj.name = "李雷"console.log(obj)//输出结果:{name: '李雷', age: 18},因为const常量中存储的是对象的地址,并没有发生改变
const a=5;
a=6;//TypeError: Assignment to constant variable.
const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。
const的作用域与let命令相同:只在声明所在的块级作用域内有效。
const命令声明的常量也是不提升。
const声明的变量同样存在暂时性死区,只能在声明的位置后面使用。
const声明的常量,也与let一样不可重复声明。
const a //SyntaxError: Missing initializer in const declaration
三.变量的解构赋值
本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。假如两边的变量和值的数量不一致,则依次对应即可,多余的变量就是undefined,多余的值就不用。
let [变量名1,变量名2,变量名3]=[值1,值2,值3];
案例:
let [foo, [[bar], baz]] = [1, [[2], 3]];//1 2 3
let [foo] = [];//undefined
let [bar, foo] = [1];//1 undefined
let [x, y] = [1, 2, 3];//1 2
四.字符串的扩展
之前在js中的字符串拼接很复杂麻烦,如DOM操作中的innerHTML赋值,在ES6中使用模板字符串就不用拼接了。
模板字符串:
- 支持换行
- 可以写变量
${变量名}
<body><div id="app"></div><script>let app = document.getElementById("app")let msg = "我是模板字符串"let id = "box"app.innerHTML = `<div id=${id}><div>${msg}</div><h1>${3 > 2 ? "haha" : "hehe"}</h1></div>`</script></body>
五.函数的扩展
函数默认参数
用es5实现函数默认参数:如果调用函数时未传入参数值,使用默认值
function fn(a){a=a||5 //利用或||的短路性console.log(a)
}
fn(0/flase/null/"")
//弊端,如果传入0 false null "" 等数据会认为没有传参数,从而使用默认值
es6的写法:将默认的值写在函数名后面的括号中直接对参数赋值。如果在调用函数时传了参数,则默认参数没有用,使用的是传的参数;如果在调用函数时没有传参数,则使用的是默认参数的值。
function fn(a=5){console.log(a)
}
fn() // 5
fn(0) // 0
rest参数
ES6 引入 rest 参数(形式为…变量名),用于获取函数的剩余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
function fn(a,...b){console.log(a)//1console.log(b)//[2, 3, 4]
}
fn(1,2,3,4)
arguments对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用Array.from先将其转为数组。rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用
注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
// 报错
function f(a, ...b, c) {// ...
}
箭头函数(函数的新写法)
与普通函数相比,箭头函数在写法上的优化:取消了function关键字;在参数和函数体之间用箭头=>
代替;
ES5的写法:
var fn=function(m,n){console.log(m,n)}
ES6的写法
let fn=(m,n)=>{console.log(m,n)}
ES6箭头函数的特殊情况:
- 如果只有一个参数的情况下,默认参数的括号可以省略;
- 如果函数只是想返回一个值,那么可以省略return及{};
var fn=function(m){return m;}//普通函数let fn=m=>m//箭头函数
箭头函数和普通函数的区别:
- 写法不同:箭头函数取消了function关键字,在参数和函数体之间用箭头
=>
连接。并且在特殊情况只有一个参数时可以省略小括号;函数只是做一个返回值时可以省略return和{}.- 关于this指向:箭头函数中的this指向的是定义时所在的对象,也就是他的父级(内部的this就是定义时上层作用域中的this ).也就是说箭头函数没有自己的this,箭头函数内部的this指向是固定的.相比之下,普通函数的this指向是可变的,指向的是调用他时的元素对象。
- 箭头函数不能当作构造函数,不可以对箭头函数使用new命令。因为构造函数的原理中有一步是要更改this指向,而箭头函数的this指向是无法更改的。
- 箭头函数不可以使用arguments对象,该对象在函数体内不存在,如果要用,可以用rest参数代替。
function Timer() {this.s1 = 0;this.s2 = 0;// 箭头函数 this指向定义它时的对象timer,其自身的s1++setInterval(() => this.s1++, 1000);// 普通函数 定时器的this指向window,自身的s2并没有发生变化setInterval(function () {this.s2++;}, 1000);
}var timer = new Timer();setTimeout(() => console.log('s1: ', timer.s1), 3100);// 3 输出的是timer的s1和s2
setTimeout(() => console.log('s2: ', timer.s2), 3100);// 0
在之后的框架vue和react中基本上都用的是箭头函数了,使用普通函数很少了。
箭头函数不适用的场合:
- 定义对象的方法,且该方法内部包括
this
。
const cat = {lives: 9,jumps: () => {this.lives--;}
}
// 上面代码中,cat.jumps()方法是一个箭头函数,这是错误的。调用cat.jumps()时,如果是普通函数,该方法内部的this指向cat;如果写成上面那样的箭头函数,使得this指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致jumps箭头函数定义时的作用域就是全局作用域。
- 需要动态
this
的时候,也不应使用箭头函数.
var button = document.getElementById('press');
button.addEventListener('click', () => {this.classList.toggle('on');
});
// 上面代码运行时,点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。
- 另外,如果函数体很复杂,有许多行,或者函数内部有大量的读写操作,不单纯是为了计算值,这时也不应该使用箭头函数,而是要使用普通函数,这样可以提高代码可读性。
六.数组的扩展
扩展运算符...
:
… 扩展运算符 可以把数组、类数组、对象、字符串(之前还得用用空字符拼接)、Set、Map结构拆分成以逗号分隔的参数序列。可以用于合并。
应用:
- Math.min(参数序列)/max(参数序列),如Math.min(1,4,5,3,2):只接受参数序列当参数,不接受数组,而如果想要使用数组,需要ES6的数组扩展。
var arr=[1,5,8]
console.log(Math.min(...arr))
- 数组/对象合并
- 法一:concat方法
var arr1 = [1, 2, 3]
var arr2 = [4, 5, 6]
var arr3 = arr1.concat(arr2)
console.log(arr3)//[1,2,3,4,5,6]
- 法二:将数组拆成参数序列,再放进数组中。对象也是同样的方法。
var arr1 = [1, 2, 3]
var arr2 = [4, 5, 6]
console.log([...arr1,...arr2])
七.对象的扩展
语法上的简化
如果对象的属性值是变量,并且变量的名字跟属性名同名,键值对可以省略为一个 。
如果对象的属性值是函数,可以省略 :function
let birth = '2000/01/01';const Person = {name: '张三',//等同于birth: birthbirth,// 等同于hello: function ()...hello() { console.log('我的名字是', this.name); }//箭头函数的写法:hello:()=>{console.log('我的名字是', this.name)}
};
对象的解构赋值
我们的目的是要拿出对象的属性值,不需要再像以前一样声明一个变量存储var name=obj.name;只需要让{}={}.
.嵌套的解构赋值解构的是最里层的,外层的拿不到,
对象和数组的解构模式有两种:绑定和赋值。在使用赋值模式时,当使用对象文字解构赋值而不带声明时,在赋值语句周围必须添加括号 ( ... )
.如果你的编码风格不包括尾随分号,则 ( ... )
表达式前面需要有一个分号,否则它可能用于执行前一行的函数。
最简单案例:
let obj={name:"黎明",age:18,salary:3500,hobby:{type:"打篮球"}} ;let {name,age,salary,hobby,hobby:{type}}=objconcole.log(name,age,salary,hobby)// 小明 18 3500 {type:"打篮球"}//嵌套结构解构最里层时 变量名:{里层变量名}console.log(type)//打篮球
使用对象解构赋值的函数定义方式:
let obj = { name: "李雷", age: 18 };
function fn({ name, age }) {console.log(name, age)
}//函数中的参数传的是对象,再进行解构赋值
fn(obj)//李雷 18
配合扩展运算符:扩展运算符的解构赋值,不能复制继承自原型对象的属性
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }
配合函数默认参数:
// 写法一
function m1({x = 0, y = 0} = {}) {return [x, y];
}// 写法二
function m2({x, y} = { x: 0, y: 0 }) {return [x, y];
}// 函数没有参数的情况
m1() // [0, 0]
m2() // [0, 0]// x 和 y 都有值的情况
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]// x 有值,y 无值的情况
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]// x 和 y 都无值的情况
m1({}) // [0, 0];
m2({}) // [undefined, undefined]m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]
嵌套解构:
let {name,age,hobby:{sport}}={name:"小明",age:18,hobby:{sport:"basketball"}}
八.Symbol:新的数据类型(类似于字符串)独一无二
ES6 引入了一种新的原始数据类型Symbol,类似于字符串,表示独一无二的值。
Symbol数据要通过Symbol函数来生成,但生成的Symbol数据都是不一样的
let a=Symbol("name");
,里面传的参数是没有任何实际意义的,只是为了更有描述性方便看而已。应用:对象的属性名必须是唯一的,独一无二的。如果自定义的属性或方法与原来对象本身就存在的属性或方法重名的话,就会将原来的属性或方法给改了,后面的把原来的给覆盖了,影响到原来的属性或方法的使用。
js中的原始数据类型有5+1种:string number boolean null undefined symbol.
typeof的返回值有string、number、boolean、undefined、function、object、symbol。
// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();s1 === s2 // false,Symbol数据是独一无二的,即使是看起来一样,实际上都是不一样的// 有参数的情况
let s1 = Symbol('foo');
let s2 = Symbol('foo');s1 === s2 // false 里面的参数没有任何实际意义,只是为了方便查看而已
symbol做为对象的属性名:在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。
let mySymbol = Symbol();// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';// 第二种写法
let a = {[mySymbol]: 'Hello!'
};
九.Set:新的数据结构(类似于数组)其中数据独一无二,数组去重
Set在工作中的使用场景较少,基本只有数组去重,其他时候我们还是使用的是数组。
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值(相当于自动去重),并且相对于数组没有角标。
使用new Set()创建数据时里面传的参数是数组;而在数组中使用new Array(1,2,3)创建数组时传的参数是参数序列。
const set = new Set([1, 2, 3, 4, 4]);//{1,2,3,4} 自动去重let arr = new Array(1, 2, 3, 4, 3, 2, 1)
console.log(arr)//数组 [1, 2, 3, 4, 3, 2, 1]
let set = new Set(arr)
console.log(set)// {1, 2, 3, 4}
实际场景的应用:利用set数组去重:
[...new Set(array)]
Set的属性和方法:都是定义在原型上的
Set的这些属性和方法定义在原型中。
- size :返回Set实例的成员总数
- add(value) :添加某个值,返回 Set 结构本身,支持链式调用
- delete(value) :删除某个值,返回一个布尔值,表示删除是否成功。不支持链式调用
- has(value):返回一个布尔值,表示该值是否为Set的成员。
- clear():清除所有成员,没有返回值。
案例:
s.add(1).add(2).add(2);
// 注意2被加入了两次,但最终加入的只有一个s.size // 2s.has(1) // true
s.has(2) // true
s.has(3) // falses.delete(2);
s.has(2) // false
Set的遍历操作
- keys():返回键名的遍历器
- values():返回键值的遍历器 由于Set结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。
- entries():返回键值对的遍历器
- forEach():使用回调函数遍历每个成员,对每个成员做同样的事情。
关于for of循环
循环对象可以用for in,不能用for of;循环数组既可以用for in,有可以用for of.
数组可以用for循环有角标,而set数据没有角标,不能用for循环。
//循环对象const obj = {a: 1,b: 2,c: 3}for (let i in obj) {console.log(i)// a// b// c}for (let i of obj) {console.log(i)// Uncaught TypeError: obj is not iterable 不可迭代的 报错了}
循环数组用for in 和for of都行
//循环数组
const arr = ['a', 'b', 'c']
// for in
for (let i in arr) {console.log(i)// 0// 1// 2
}
// for of
for (let i of arr) {console.log(i)// a// b// c
}
循环Set数据用for of
//循环set
let set=new Set([1,2,3,4,3,2,1])
//遍历keys键 在set中keys和values都是一样的,但是在Map中是不一样的。
for(let item of set.keys()){console.log(item)//1 2 3 4
}
//遍历values
for(let item of set.values()){console.log(item)//1 2 3 4
}
//遍历整体entries,输出的是整体的键值对
for(let item of set.entries()){console.log(item)//[1,1]//[2,2]//[3,3]//[4,4]
}
//foreach()
set.foreach((item)=>{console.log("haha"+item)
})
结论:
关于for in:专注于角标key
for … in 循环返回的值都是数据结构的 键值名。遍历对象返回的对象的属性名key值,遍历数组返回的数组的下标(key),遍历字符串返回的是角标。
关于for of: 专注于值value
for of 循环用来直接获取一对键值对中的值,而 for in 获取的是 键名。
一个数据结构只要部署了 Symbol.iterator 属性, 就被视为具有 iterator接口, 就可以使用 for of循环。 要想知道是否能用for of ,即是否具有iterator接口可以通过查看其原型看是否有Symbol(Symbol.iterator) console.log(Array.prototype),Object没有,而Set和Array有。
哪些对象可以使用for of循环?
- 数组 Array
- Map
- Set
- String
- arguments对象 类数组
- Nodelist对象, 就是获取的dom列表集合 dom获取的元素对象
Set 与数组的转换
使用扩展运算符...
//Set转换成数组 把数组当作Set的参数
let set = new Set([1, 2, 3, 4, 3, 2, 1])
console.log([...set])//[1,2,3,4]//数组转换成Set
let arr = [1, 2, 3, 4, 3, 2, 1]
let set2 = new Set(arr)
console.log(set2)
十.Map:新的数据结构(类似于对象),属性名可是任何类型的
Map在实际工作中使用较少,常作为数据中转,拿到其中的键、值。
ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围即属性名类型不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应。
Map接收一个数组当参数,数组中又有若干个小数组,在小数组中有两项,一项代表key(不限制数据类型),一项代表value.
const map = new Map([['name', '张三'],['title', 'Author']
]);const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')//使用set()给map添加项
Map的属性和方法
Map的这些属性和方法定义在原型中。
- size :返回 Map 实例的成员总数
- set(key, value) :添加项,set方法设置键名key对应的键值为value
- get(key) get方法读取key对应的键值,如果找不到key,返回undefined。
let map=new Map()
let arr=[1,2,3,4]
map.set(arr,180)
console.log(map.get(arr))//180
console.log(map.get([1, 2, 3, 4]))//undefined,因为两个的地址不一样,不是同一个
- has(key):has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中
- delete(key):delete方法删除某个键,返回true。如果删除失败,返回false。
- clear():clear方法清除所有成员,没有返回值。
Map的遍历方法
- keys():返回键名的遍历器
- values():返回键值的遍历器
- entries():返回键值对的遍历器
- forEach():使用回调函数遍历每个成员
Map与数组的转换
Map转换成数组:使用扩展运算符…
数组转换成Map:使用Map()
//数组转换成Map 把数组当作Map的参数即可
const map = new Map([[1, 'one'],[2, 'two'],[3, 'three'],
]);//Map转换成数组
[...map.keys()]
// [1, 2, 3][...map.values()]
// ['one', 'two', 'three'][...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']][...map]
// [[1,'one'], [2, 'two'], [3, 'three']]
十一.promise对象:解决异步问题(面试常问)
/* 1.发送请求,根据手机号查询个人身份证号2.发生请求,根据身份证号查询银行卡余额3.发送请求,根据银行卡余额查询贷款信息*/$.ajax({//1})$.ajax({//2})$.ajax({//3})/*这种方法是错误的,无法实现异步,无法确定是哪一个先执行结束,能否在后一个执行前拿到前一个的结果作为后一个的参数。如果加入定时器的话,设定时间过长会影响用户体验,设定时间过短又无法保证按照顺序执行,在万不得已的情况下不要使用定时器来解决异步的问题1s而promise就是用来解决这种异步问题的,保证后一个的请求在前一个请求结束之后进行。*/
基本概念
异步:先写的代码不一定先执行,重新开一个线程执行 。目前学到的异步有定时器、ajax。
我们想要使用上一个请求执行结束之后得到的结果来做下一个请求的参数,使用ajax请求执行的顺序由于网速等各种原因无法得到控制,可能会导致在发送请求时拿不到想要的结果。为了解决这种问题,如果使用定时器来解决这个问题,由于定时器的时间难以把控,在万不得已的情况下不要使用。
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。
1.什么是异步编程? 传统方案怎么解决异步编程?
简单来说,异步编程就是在执行一个指令之后不是马上得到结果,而是继续执行后面的指令,等到特定的事件触发后,才得到结果。
之前我们一般通过回调函数的方式解决异步编程,如下
$.ajax({success:function(){$.ajax({success: function(){$.ajax({success:function(){... }})}})}})
setTimeout(function () {console.log(1)setTimeout(function () {console.log(2)setTimeout(function () {console.log(3)setTimeout(function() {console.log(4)}, 500);}, 1000)}, 2000)}, 3000)
这样的代码看层级少了当然还是可以凑合看的,但是在某些场景下,我们就需要一次又一次的回调…回调到最后,会发现我们的代码就会变成金字塔形状?这种情况被亲切地称为回调地狱。
回调地狱不仅看起来很不舒服,可读性比较差,难以维护,除此之外还有比较重要的一点就是对异常的捕获无法支持。
使用回调函数解决异步问题的缺点:采用传统的回调函数来解决异步问题,会出现回调地狱的问题,代码呈现横向发展的趋势,可读性差,难以维护,当业务逻辑较多时对异常的捕获难以支持。
使用
Promise是一个对象,能保证执行顺序,即使是后来的再简单也能保证先来的先执行。主要是靠then()管控,promise本身是同步的,promise中的then是异步的。
Promise有三种状态
- pending: 初始状态,既不是成功,也不是失败状态。
- fulfilled: 意味着操作成功完成。
- rejected: 意味着操作失败。
基本语法:
/*1.构造函数创建Promise对象 接收函数为参数,函数中的默认参数resolve和reject是两个函数名resolve():在异步操作成功的时候调用,把DOM操作过后的值传出去,传到then()中的参数中,将要传出去的值放在resolve中的参数。reject():在异步操作失败的时候调用,把值传出去。p.then()保证Promise先走,走完之后再走then()里面的事情,要把需要控制执行顺序的所有的步骤都写在Promise中,交给Promise监管。then()以函数为参数,将resolve中传出来的值作为函数的参数,做下面的操作。在promise执行结束之后需要调用resolve函数将状态由pending改为fulfilled,才能执行下面的then。在链式的then()中要return new Promise(),用Promise来限制住then()中的执行顺序,并使用resolve()或rejected()来转换状态,否则then()中就是同步的了,无法按照我们想要的顺序来执行。
*/
let promise = new Promise(function(resolve, reject) {// … 异步操作的代码if (/* 异步操作成功 */){resolve(value);} else {reject(error);}
});
Resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 fulfilled),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
Reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise实例生成以后,可以用then方法分别指定fulfilled状态和rejected状态的回调函数。成功的函数作为then()的第一个参数,失败的函数作为then()的第二个参数。resolve()和reject()本质上是一样的,执行哪一个都可以,只是习惯上在成功时使用resolve()在失败时使用reject(),resolve()和reject()函数与ajax中的success()和error()是不一样的,resolve()和reject()是自己人为规定的,而ajax中的success()和error()是请求成功和失败只能这么走。
promise.then(function(value) {// success
}, function(error) {// failure
});
then方法是Promise实例状态改变时的回调函数
then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为fulfilled调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式调用写法,即then方法后面再调用另一个then方法。then()中需要return new Promise(),因为需要使用Promise来限制执行顺序,在then()中不使用promise的话是同步执行的。
//使用Promise实现解决定时器异步的案例,这种写法很乱,一般不使用,我们之后使用的是把要做的Promise封装起来配合axios使用let promise = new Promise((resolve, reject) => {//异步操作代码setTimeout(() => {console.log(1)resolve()//将Promise对象的状态由pending改为fulfilled,才能继续执行then()的内容}, 3000);/*错误1 :将resolve()写在这里,结果是先执行2再执行1,因为Promise解决异步是靠then()的,定时器是异步的,resolve()是同步的,先执行的resolve(),一进来就告诉状态改为了成功可以执行then()了。在Promise内部写的代码的执行顺序与在外面写的执行顺序是一样的,解决异步是靠then()*/})promise.then(() => {//在then()中需要return new Promise()使用Promise来监管执行顺序return new Promise((resolve, reject) => {setTimeout(() => {console.log(2)reject("我是失败的数据")//失败的函数 将Promise对象的状态由pending改为rejected}, 2000);})}).then(() => {return new Promise((resolve, reject) => {setTimeout(() => {console.log(3)resolve()}, 1000);})}, (error) => {console.log(30, error)//这里没有使用resolve()和reject()也继续往下面走了,这是因为没有使用Promise限制执行顺序,是同步}).then(() => {setTimeout(() => {console.log(4)}, 500);//最后一个不需要再用return new Promise})
封装函数的Promise案例:
//将要异步做的事情封装成函数,直接调用,简化过程。在后面的实际工作中常与axios一起使用,是使用的第三方插件自动返回的就是promise,基本不用自己做封装函数,直接写后面的调用和then()function p() {return new Promise((resolve, reject) => {setTimeout(() => {console.log(1)resolve()}, 3000);})}function p1() {return new Promise((resolve, reject) => {setTimeout(() => {console.log(2)resolve()}, 2000);})}function p2() {return new Promise((resolve, reject) => {setTimeout(() => {console.log(3)resolve()}, 1000);})}p().then((res) => {return p1()//在函数中都要加上return,上面p1函数中的return是p1函数中的return,不是then()的return,如果不加return 的话then()就会返回一个默认的空的Promise}).then(() => {return p2()}).then(() => {console.log("完事了")})
关于catch
Promise.prototype.catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。与then()并列写。
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。是错误具有”冒泡“性质,不是catch()能冒泡,只要reject()了,即使是使用的then()的第二个参数也能实现;如果把处理错误的函数放在了其他then()的第二个参数中了,其他的then()也能使用,不用每一个then()都写一个处理错误的函数了,同理catch()也是。也就是说,错误总是会被下一个catch语句捕获。
catch和then中第二个函数的区别:
主要区别就是,如果在then的第一个函数里抛出了异常,后面的catch能捕获到,而then的第二个函数捕获不到,只能从后面的捕获。如下
new Promise((resolve, reject) => {resolve(1)
}).then(() => {return new Promise((resolve, reject) => {// 在第一个函数中抛出错误throw new Error("失败了嘤嘤嘤")})
}, (err) => {console.log(111,err); // 在then的第二个函数中处捕获不到异常,只能在后续的then中捕获到异常
}).then(()=>{},(err)=>{console.log(222,err) // 在这里才被捕获到了异常
})
new Promise((resolve, reject) => {resolve(1)
}).then(()=>{return new Promise((resolve, reject) => {// 在第一个函数中抛出错误throw new Error("失败了嘤嘤嘤")})
}).catch(err=>{console.log("catch捕获到了",err) //在catch这里异常被捕获到了
})
Promise.all 全部
p1,p2,p3是三个promise实例
const p = Promise.all([p1, p2, p3]);
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
用法:
let p1 = new Promise((resolve, reject) => {resolve('成功了')
})let p2 = new Promise((resolve, reject) => {resolve('success')
})Promise.all([p1, p2]).then((result) => {console.log(result) //['成功了', 'success']
}).catch((error) => {console.log(error)
})let p3 = Promise.reject('失败')Promise.all([p1,p3,p2]).then((result) => {console.log(result)
}).catch((error) => {console.log(error) // 失败了,打出 '失败'
})
特殊的:
const p1 = new Promise((resolve, reject) => {resolve('hello');
})
.then(result => result)
.catch(e => e);const p2 = new Promise((resolve, reject) => {reject("报错了")
})
.then(result => result)
.catch(e => e);Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));//['hello' '报错了']
上面代码中,p1会resolved,p2首先会rejected,但是p2有自己的catch方法,该方法返回的是一个新的 Promise 实例,p2指向的实际上是这个实例。该实例执行完catch方法后,也会变成resolved,导致Promise.all()方法参数里面的两个实例都会resolved,因此会调用then方法指定的回调函数,而不会调用catch方法指定的回调函数。
如果p2没有自己的catch方法,就会调用Promise.all()的catch方法。
Promise.race 比赛
const p = Promise.race([p1, p2, p3]);
只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
谁快谁管用,如果快的是resolve()则后面执行then(),如果快的是reject()则后面执行catch().
let p1 = new Promise((resolve, reject) => {setTimeout(() => {resolve('success')},1000)
})let p2 = new Promise((resolve, reject) => {setTimeout(() => {reject('failed')}, 500)
})Promise.race([p1, p2]).then((result) => {console.log("成功的",result)
}).catch((error) => {console.log("失败的",error) // 打印的是 '失败的 failed' 因为p2更快,而p2是reject(),执行catch
})
同样,特殊的如果p2有自己的catch,可以自己处理,则处理了错误之后变成了resolved,输出结果变成了’ 成功的 failed’.
十二.async await (ES7 2017Promise优化版)
async:异步函数,函数内部有异步操作。
async 函数的返回值是promise(可以调用then()和catch()),return的值默认就是async函数的resolve的值。
await如果等到的是promise,整个表达式的值即await promise就是Promise中resolve的值,并不是reject的值,想要处理reject的值就需要fn().catch()(可以catch是因为异步函数的返回值是Promise),失败就catch()。
async 是“异步”的简写,而 await 可以认为是 async wait 的简写。所以应该很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。
async函数会返回一个promise对象,如果在函数中return一个值,那么该值会通过Promise.resolve()传递出去。
一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。
await 是个运算符,用于组成表达式,await 表达式的运算结果取决于它等的东西。
如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。
如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。
async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。(语法糖:本质上没有什么区别,只是样子上变了,更好理解了 )
//使用函数来存储Promise,手动控制Promise的执行
function p1(){return new Promise((resolve,reject)=>{setTimeout(()=>{console.log("aaa");resolve("结束了aaa")},2000)})}function p2(){return new Promise((resolve,reject)=>{setTimeout(()=>{console.log("bbb");reject("结束了bbb")},1000)})}async function fn(){//异步操作let a=await p1() //先阻塞了let b=await p2() //后阻塞了console.log(a,b);}
//rejectfn().catch((err)=>{console.log(err)})
//aaa
//bbb
//结束了aaa 结束了bbb
因为async的返回值是promise,所以也可以使用then来处理和catch来处理reject
比如:
async function f() {// 等同于// return 123;return await 123;//async函数中return的值默认是async函数的resolve的值 resolve(123)
}f().then(v => console.log(v))//123
Promise 和 async await 的对比:
function p1(){return new Promise((resolve,reject)=>{setTimeout(() => {console.log(1)resolve("1aaa")}, 3000);})
}
function p2(){return new Promise((resolve,reject)=>{setTimeout(() => {console.log(2);reject("2bbb")}, 2000);})
}
function p3(){return new Promise((resolve,reject)=>{setTimeout(() => {console.log(3)resolve("3ccc")}, 1000);})
}
// 方法一:Promise
p1().then((res)=>{console.log("p1 resolve的"+res)// 注意:这里执行p2函数之后一定要returnreturn p2()
}).then(()=>{},(err)=>{console.log("p2 reject的"+err)return p3()
}).then((res)=>{console.log("p3 resolve的"+res)
})// 方法二:async awiat
async function fn(){let a=await p1()let b=await p2()}
// async await reject的在catch中处理
fn().catch((err)=>{console.log(err)p3()
})
十三. module 模块化
基本用法:
1.export :导出的一定是个完整的变量声明语句,同时导出多个可以 export{ 变量名1,变量名2}
export let a =1; export function fn(){};
2.import:引入的时候注意,路径名如果是当前目录需要加 ./,不然引入的就是核心模块node_modules.
import {a,b} from “./a.js”;
export 改名
通常情况下,export
输出的变量就是本来的名字,但是可以使用as
关键字重命名。
导出的同时改名字:
function v1() { ... }
function v2() { ... }export {v1 as streamV1,v2 as streamV2,
};
这是时候其他文件引入的时候
import {streamV1 } from 路径名
import改名
import { lastName as surname } from './profile.js';
注意:
import命令输入的变量都是只读的,因为它的本质是输入接口。
也就是说,不允许在加载模块的脚本里面,改写接口。
如果a是一个对象,改写a的属性是允许的.(因为改的是属性,地址指针是没变化的)
import {a} from './xxx.js'a = {}; // Syntax Error : 'a' is read-only;//如果a是一个对象,改写a的属性是允许的。a.foo = 'hello'; // 合法操作
export default 默认导出
- 默认导出,导出的是一个值,不是一个声明语句。因为default代表的就是变量,后面只需要跟值就可以
- 引入的时候变量名可以是任意的,且没有{}.因为核心原理as重新命名了,而去掉{}是因为默认导出只能有一个,不可能有多个。
- 一个文件中只能有一个默认导出。
export default function() {console.log('foo');
}//这里默认导出的应该是值,不能是声明语句。import 任意名字 from "路径"//这个名字无论叫什么都是对应的fn()
核心原理:
export {a as default}
import {default as c任意}
既想要引入默认导出又想要引入普通导出:普通导出放在{}中,非普通导出在外面,用,分隔。
import hello,{b} from "./a,js"
整个文件的引入
整个文件引入时不需要导出
import "./index.css"
import "../index.js" //此情况被引入的文件无需导出
十四.class 类
class类在vue中基本不用,在react中的类组件中占比逐年降低,但是class类在晋升往架构师源码方向会非常有用。
基本概念
类中定义的所有属性和方法本质上都是定义在原型上的。
ES6 的 class 可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到。
本质上 class (类)是构造函数的语法糖。
新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
JavaScript 语言中,生成实例对象的传统方法是通过构造函数。
例如:
// ES5的构造函数的写法
function Point(x, y) {this.x = x;this.y = y;
}Point.prototype.toString = function () {return '(' + this.x + ', ' + this.y + ')';
};var p = new Point(1, 2);
上面的代码用 ES6 的class
改写,就是下面这样。
每一个类都有一个默认的构造方法(等同于原来的构造函数),在实例化对象的时候会自动调用类默认的构造方法。
**在类中定义的所有方法默认都是定义在原型上的。**类中的方法不需要function关键字,方法之间也不需要逗号
,
每个构造函数都有原型属性,(原型是个属性)这个原型指向原型对象,原型对象中拥有一个属性constructor,它指回构造函数。
// ES6的class类的写法
class Point {constructor(x, y) { //这就是构造方法this.x = x; //代表实例对象this.y = y;}
//注意,定义toString()方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法与方法之间不需要逗号分隔,加了会报错。toString() {return '(' + this.x + ', ' + this.y + ')';}
}
let p=new Point(1,2)//本质上是调用Point对象的方法
ES6 的类,完全可以看作构造函数的另一种写法。
class Point {// ...
}typeof Point // "function"
Point === Point.prototype.constructor // true
使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。
class Bar {doStuff() {console.log('stuff');}
}const b = new Bar();
b.doStuff() // "stuff"
构造函数的prototype属性,在 ES6 的“类”上面继续存在。
事实上,类的所有方法都定义在类的prototype属性上面。
class Point {constructor() {// ...}toString() {// ...}toValue() {// ...}
}
// 等同于
Point.prototype = {constructor() {},toString() {},toValue() {},
};
prototype对象的constructor()属性,直接指向“类”的本身,这与 ES5 的行为是一致的。
Point.prototype.constructor === Point // true
constructor方法
constructor()方法是类的默认方法,通过new命令生成对象实例时,会自动调用该方法。
一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加。
class Point {
}// 等同于
class Point {constructor() {}
}
class的继承
class 可以通过extends关键字实现继承,让子类继承父类的属性和方法。
extends 的写法比 ES5 的原型链继承,要清晰和方便很多。
在继承时必须先调用父类的构造方法,如果子类没有写自己的constructor(),系统默认调用父类的constructor();如果子类写了自己的constructor(),则在里面必须使用super(“共同参数1”,“共同参数2”,…)主动调用父类的constructor()。
ES5的继承:先创建一个子类实例对象,再往这个对象上面添加属性,实例在前 继承在后。(new关键字的实现原理就是先创建一个空对象)
ES6的继承:直接给一个匿名对象加上父类的属性,再把这个对象赋予子类作为子类的实例,继承在前 实例在后。(子类要主动使用super调用父类的构造函数)
使用ES6的class类的方法:把子类中的共同的属性和方法抽出来定义成父类,如果子类没有定义自己的constructor(),默认调用父类的constructor();如果子类定义了自己的constructor(),则必须先使用super()主动调用父类的构造方法,再加入属于自己的参数。另外,对于需要传参的构造方法,子类的constructor()要传入所有的参数,子类constructor()中的super(实参)也要传入父类构造函数中所需要的实参,这个实参是通过子类的constructor()的形参传进来的。
基本语法:
class Point {fn(){console.log(1)}}class ColorPoint extends Point { } let a=new ColorPoint();a.fn()
Point是父类,ColorPoint是子类,它通过extends关键字,继承了Point类的所有属性和方法。
Object.getPrototypeOf()
:用来从子类上获取父类。
class Point { /*...*/ }class ColorPoint extends Point { /*...*/ }Object.getPrototypeOf(ColorPoint) === Point
// true
因此,可以使用这个个方法判断,一个类是否继承了另一个类。
super的用法:super代表父类,super()代表父类的构造方法。
super
这个关键字,既可以当作函数使用,也可以当作对象使用。 在这两种情况下,它的用法完全不同。
第一种情况,super
作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super()
函数。
class A {}class B extends A {constructor() {super();}
}
调用super()
的作用是形成子类的this
对象,把父类的实例属性和方法放到这个this
对象上面。子类在调用super()
之前,是没有this
对象的,任何对this
的操作都要放在super()
的后面。
第二种情况,super
作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
ES6 规定,在子类普通方法中通过super
调用父类的方法时,方法内部的this
指向当前的子类实例。
class Point {fn(){console.log(1)}aa(){console.log("父类")}
}class ColorPoint extends Point {aa(){super.aa()//super当作父类的对象来使用}
}
let a=new ColorPoint();
a.aa()
ES6 规定,子类必须在constructor()方法中调用super(),否则就会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法。如果不调用super()方法,子类就得不到自己的this对象。
class Point { /* ... */ }class ColorPoint extends Point {constructor() {}
}let cp = new ColorPoint(); // ReferenceError
为什么子类的构造函数,一定要调用super()?原因就在于 ES6 的继承机制,与 ES5 完全不同。ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即“实例在前,继承在后”。ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”。这就是为什么 ES6 的继承必须先调用super()方法,因为这一步会生成一个继承父类的this对象,没有这一步就无法继承父类。
注意,这意味着新建子类实例时,父类的构造函数必定会先运行一次
class Foo {constructor() {console.log(1);}
}class Bar extends Foo {constructor() {super();//super当作父类的构造函数来使用console.log(2);}
}const bar = new Bar();
//1
//2
上面示例中,子类 Bar 新建实例时,会输出1和2。原因就是子类构造函数调用super()时,会执行一次父类构造函数。
另一个需要注意的地方是,在子类的构造函数中,只有调用super()之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,必须先完成父类的继承,只有super()方法才能让子类实例继承父类。
class Point {constructor(x, y) {this.x = x;this.y = y;}
}class ColorPoint extends Point {constructor(x, y, color) {//this.color = color; // ReferenceErrorsuper(x, y);//主动调用父类的构造方法,要带有实参,参数从子类的构造方法中传进来this.color = color; // 正确}
}
let a = new ColorPoint(5, 9, "red")
console.log(a)//{x: 5, y: 9, color: 'red'}
使用es5和es6对比的例子:
//es5没有继承的写法/* function Benz(color, price, name, luxury) {this.color = colorthis.price = pricethis.name = namethis.luxury = luxury}function Bmw(color, price, name, sport) {this.color = colorthis.price = pricethis.name = namethis.sport = sport}function Audi(color, price, name, business) {this.color = colorthis.p rice = pricethis.name = namethis.business = business}let bz = new Benz("白色", 100000, "bz", "便捷")let bm = new Bmw("黑色", 20000, "bm", "运动")let ad = new Audi("粉色", 500000, "ad", "商务")console.log(bz, bm, ad)*///es6的class类继承写法class Car {constructor(color, price, name) {this.color = colorthis.price = pricethis.name = name}fn() {console.log("我是父类的方法")}}class Benz extends Car {constructor(color, price, name, luxury) {super(color, price, name)this.luxury = luxury}}class Bmw extends Car {constructor(color, price, name, sport) {super(color, price, name)this.sport = sport}}class Audi extends Car {constructor(color, price, name, business) {super(color, price, name)this.business = business}}let bz1 = new Benz("白色", 100000, "bz", "便捷")let bz2 = new Benz("黑色", 200000, "bz", "贼便捷")let bm = new Bmw("黑色", 20000, "bm", "运动")let ad = new Audi("粉色", 500000, "ad", "商务")console.log(bz1, bz2, bm, ad)bz1.fn()
例子的执行结果:
十五.高级
事件循环eventloop解决异步问题(面试常问)
事件循环:所有的同步任务都在主线程上执行,遇到异步任务将其存放入任务队列中,当主线程执行结束之后再从任务队列中按照“先进先出”的原则检查代码拿到主线程中去执行,执行结束之后再去任务队列中再取出下一个异步任务拿到主线程去执行,这样依次反复实现事件循环。
什么是事件循环?同步任务优先执行,异步任务放在事件队列中,遇到异步任务分为宏任务和微任务,每次宏任务执行完,需要清除本轮所有的微任务,再从事件队列中取出下一个任务,以此往复。
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
- 遇到异步任务, 进入Event Table并注册回调函数; 等到指定的事件完成(如ajax请求响应返回, setTimeout延迟到指定时间)时,Event Table会将这个回调函数移入Event Queue。
- 当栈中的代码执行完毕,执行栈(call stack)中的任务为空时,主线程会先检查micro-task(微任务)队列中是否有任务,如果有,就将micro-task(微任务)队列中的所有任务依次执行,直到micro-task(微任务)队列为空; 之后再检查macro-task(宏任务)队列中是否有任务,如果有,则取出第一个macro-task(宏任务)加入到执行栈中,之后再清空执行栈,检查micro-task(微任务),以此循环,直到全部的任务都执行完成
遇到同步任务直接执行,遇到异步任务分类为宏任务(macro-task)和微任务(micro-task)。
宏任务:整体的js代码、 setTimeout、 setInterval
微任务:Promise中的then、 process.nextTick
Promise是同步
执行原则:
有微则微,无微则宏
如果微任务列表里面有任务 会执行完毕后在执行宏任务。
执行原则:从整体到局部,先从整体判微宏,先执行整体的微,如果整体微中有宏,将其放到整体宏的后面,直至所有的整体微执行结束;再执行整体宏,如果整体宏中还有宏,再继续往宏队列后面放。
案例1:
//这是一个同步任务直接被执行 console.log('1')、//这是一个宏任务,放进宏任务列表setTimeout(function () {console.log('2')});new Promise(function (resolve) {console.log('3');//这是同步任务直接被执行 异步任务在then中resolve();}).then(function () {//then是个微任务 ,放进微任务列表console.log('4')setTimeout(function () {console.log('5')});//又开了一个宏任务,需要将之前的宏任务执行结束之后,再执行新的宏任务})
//最终结果为: 1、3、4、2、5
案例:
console.log('script start');//1setTimeout(function() {console.log('timeout1');
}, 10);//5new Promise(resolve => {console.log('promise1');//2resolve();setTimeout(() => console.log('timeout2'), 10);//6
}).then(function() {console.log('then1')//4
})console.log('script end');//3
案例:
Promise.resolve().then(()=>{console.log('Promise1') //2setTimeout(()=>{console.log('setTimeout2')//5},0)
});
setTimeout(()=>{console.log('setTimeout1')//3Promise.resolve().then(()=>{console.log('Promise2') //4 })
},0);
console.log('start');//1
案例:注意:此处,这里先执行4再执行hello,是因为把resolve放到定时器里面了,只有resolve出去看才能执行then,否则根本不能走then.
console.log(1)new Promise((resolve,reject)=>{console.log(2);setTimeout(()=>{resolve("hello");console.log(4)})}).then((res)=>{console.log(res)})console.log(3)//1 2 3 4 hello
// 注意:这里先执行4再执行hello,是因为把resolve放到定时器里面了,只有resolve出去看才能执行then,否则根本不能走then
案例:
console.log(1)//同步 第1个执行
setTimeout(() => {console.log(2)
});//异步 宏任务 放一放
//Promise的异步是在then中的
new Promise((resolve) => {console.log(3)//同步 第2个执行resolve()setTimeout(() => {console.log(5)});
}).then(() => {//异步 微任务console.log(4)
})
//1 3 4 2 5Promise.resolve().then(() => {console.log(1)setTimeout(() => {console.log(2)});
})
setTimeout(() => {console.log(3)Promise.resolve().then(() => {console.log(4)})
});
console.log(5)
//5 1 3 4 2
手撕promise源码(面试大厂可能会问)
1.基本结构:
//Promise的三种状态pengding fulfilled rejected 默认是在pending,使用常量存储三种状态
/*使用常量将字符串状态存储起来,并且用大写字母表示。使用常量将状态存储起来,而不是直接使用字符串:这是因为使用常量容易找到错误,使用字符串的话拼写错误不容易发现。使用大写:这是因为使用常量存储一些字符串不打算修改的话都是用大写,以便于跟普通变量做区分。
*/
/*Promsie的初始值:初始状态是pending
*/
const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";
class MyPromise{constructor(fn){this.status = PENDING;//给当前对象加属性使用this,默认状态时pending状态this.value=undefined;//存放resolve传递出来的值fn(this.resolve,this.reject);//构造函数接收一个函数当参数,函数中有两个参数resolve函数和reject函数}resolve(res){//更改Promise的状态从pending->fulfilledif(this.status===PENDING){this.status=FULFILLED;this.value=res;}}reject(res){if(this.status===PENDING){this.status=REJECTEDthis.value=res;}}
}
new MyPromise((resolve,reject)=>{resolve("hello")
}) //Uncaught TypeError: Cannot read properties of undefined (reading 'status') 类型错误 对象和属性不匹配
这里报错的原因主要是因为this指向的问题,实例化之后,resolve中的this指向的是undefined
解决方案改为箭头函数即可:因为箭头函数的this指向不会发生改变,始终指向定义时所在的对象
const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";
class MyPromise{constructor(fn){this.status = PENDING;// 默认初始状态是pendingthis.value=undefined;//存放resolve传递出来的值fn(this.resolve,this.reject);}// 注意点1:resolve和reject函数要用箭头函数,否则this指向undefinedresolve=(res)=>{// resolve函数要做的把状态改为fulfilled,并把resolve的值传出去if(this.status===PENDING){this.status=FULFILLED;this.value=res;}}reject=(res)=>{if(this.status===PENDING){this.status=REJECTEDthis.value=res;}}
}new MyPromise((resolve,reject)=>{resolve("hello")
})
2.实现then功能
const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";
class MyPromise{constructor(fn){this.status = PENDING;// 默认初始状态是pendingthis.value=undefined;//存放resolve传递出来的值fn(this.resolve,this.reject);}// 注意点1:resolve和reject函数要用箭头函数,否则this指向undefinedresolve=(res)=>{// resolve函数要做的把状态改为fulfilled,并把resolve的值传出去if(this.status===PENDING){this.status=FULFILLED;this.value=res;}}reject=(res)=>{if(this.status===PENDING){this.status=REJECTEDthis.value=res;}}/**************then的实现*********************///then()中有两个参数,一个是resolve的处理函数,一个是reject的处理函数 通过status的状态来判断执行的是resolve还是rejectthen(onFulfilled,onRejected){if(this.status===FULFILLED){onFulfilled(this.value)//resolve的参数}if(this.status===REJECTED){onRejected(this.value)}}
}
new MyPromise((resolve,reject)=>{reject("hello")
}).then((res)=>{console.log("成功",res)
},(error)=>{console.log("失败",error)
})
有一个细节问题,对于原生promise,如果then中传入的不是函数,运行是不会报错的,如下
但是我们的代码运行很明显是会报错的,因为我们是直接将他们当作函数来用的,下面我们做一下处理
const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";
class MyPromise{constructor(fn){this.status = PENDING;this.value=undefined;//存放resolve传递出来的值fn(this.resolve,this.reject);}// 注意点1:resolve和reject函数要用箭头函数,否则this指向undefinedresolve=(res)=>{if(this.status===PENDING){this.status=FULFILLED;this.value=res;}}reject=(res)=>{if(this.status===PENDING){this.status=REJECTEDthis.value=res;}}then(onFulfilled,onRejected){/**************************** 加入如下判断 判断onFulfilled的类型是否为函数,如果是直接处理,如果不是把他们当作空函数*******************************/onFulfilled=typeof(onFulfilled)=="function"?onFulfilled:()=>{}onRejected=typeof(onRejected)=="function"?onRejected:()=>{}/*******************************************************************/ if(this.status===FULFILLED){onFulfilled(this.value)}if(this.status===REJECTED){onRejected(this.value)}}
}new MyPromise((resolve,reject)=>{resolve("hello")
}).then("",(error)=>{console.log("失败",error)
});
3.实现异步功能
/*对于默认的promise*/
console.log(1);
new Promise((resolve,reject)=>{console.log(2);resolve("hello")}).then((res)=>{console.log(res)});console.log(3)
//输出结果1 2 3 hello
---------------------------------------------------------------------------------
/*对于我们现在的MyPromise,还未实现异步*/console.log(1);new MyPromise((resolve,reject)=>{console.log(2);resolve("hello")}).then((res)=>{console.log(res)});console.log(3)
//输出结果 1 2 hello 3
解决方案:在then()中的resolve和reject处理中加入定时器延迟执行:
问题暂时可以解决。
但是:对于这个案例,正常应该输出1 2 3 4 hello
但是我们封装的promise显示的是1 2 4 3 hello
const PENDING = "PENDING";const FULFILLED = "FULFILLED";const REJECTED = "REJECTED";class MyPromise {constructor(fn) {this.status = PENDING;this.value = undefined;//存放resolve传递出来的值/*定义两个数组 存放resolve和reject后的值 使用数组是因为使用链式调用时不止一个resolve*/this.resovleArr = [];this.rejectArr = []fn(this.resolve, this.reject);}resolve = (res) => {/******延迟执行**********/setTimeout(() => {if (this.status === PENDING) {this.status = FULFILLED;this.value = res;this.resovleArr.forEach(cb => {cb(res)})}})}reject = (res) => {setTimeout(() => {if (this.status === PENDING) {this.status = REJECTEDthis.value = res;this.rejectArr.forEach(cb => {cb(res)})} })}then(onFulfilled, onRejected) {onFulfilled = typeof (onFulfilled) == "function" ? onFulfilled : () => { }onRejected = typeof (onRejected) == "function" ? onRejected : () => { }if (this.status === PENDING) {/*****将方法存储起来 等到resolve和reject的时候再使用*****/this.resovleArr.push(onFulfilled);this.rejectArr.push(onRejected)/*****将方法存储起来*****/}if (this.status === FULFILLED) {/*********这里需要设置延迟****************/setTimeout(() => {onFulfilled(this.value)}) }if (this.status === REJECTED) {/*********这里需要设置延迟****************/setTimeout(() => {onRejected(this.value)})}}}
- 最后我们的then可以链式调用,then函数要返回的是Promise
//完整版的 MyPromise 源码
const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";
class MyPromise {constructor(fn) {this.status = PENDING;this.value = undefined;//存放resolve传递出来的值this.resovleArr = [];this.rejectArr = []fn(this.resolve, this.reject);}resolve = (res) => {setTimeout(() => {if (this.status === PENDING) {this.status = FULFILLED;this.value = res;this.resovleArr.forEach(cb => {cb(res)})}})}reject = (res) => {setTimeout(() => {if (this.status === PENDING) {this.status = REJECTEDthis.value = res;this.rejectArr.forEach(cb => {cb(res)})} })}then(onFulfilled, onRejected) {// then函数要返回Promise才能链式调用return new MyPromise((resolve, reject) => {/**************************** 加入如下判断*******************************/onFulfilled = typeof (onFulfilled) == "function" ? onFulfilled : () => { }onRejected = typeof (onRejected) == "function" ? onRejected : () => { }/*******************************************************************/if (this.status === PENDING) {this.resovleArr.push(onFulfilled);this.rejectArr.push(onRejected)}if (this.status === FULFILLED) {setTimeout(() => {onFulfilled(this.value)})}if (this.status === REJECTED) {setTimeout(() => {onRejected(this.value)})}})}
}
Promise完整源码–黑马
需要完成的:
- 实现Promise的核心功能
- Promise的实例方法:
catch
、finally
- Promise的静态方法:
resolve
、reject
、race
、all
、allSettled
、any
- Promise/A+标准,并跑通872个单元测试
构造函数
需求:
- 实现
MyPromise
类,可以用如下的方式实例化 - 实例化时传入回调函数
const p = new MyPromise((resolve, reject) => {resolve('success')// reject('error')
})
- 回调函数立刻执行
- 回调函数接收函数`resolve`和`reject`
核心步骤:
- 定义类
MyPromise
- 添加构造函数
constructor
- 定义
resolve/reject
- 执行回调函数
// 1. 定义类MyPromise
class MyPromise {// 2. 添加构造函数constructorconstructor(func) {// 3. 定义resolve/reject// 注意点1:resolve和reject函数要用箭头函数const resolve = (res) => {}const reject = (res) => {}// 4. 执行回调函数func(resolve, reject)}
}
**面试回答:**手写Promise-构造函数
- 定义类
MyPromise
,内部添加构造函数constructor
,构造函数需要接收回调函数func
- 在构造函数中定义
resolve
和reject
- 构造函数内部调用
func
并将resolve
和reject
传入:func(resolve,reject)
状态及原因
需求:
MyPromise
增加state
属性,只能是如下3个值pending
:待定,默认状态fulfilled
:已兑现,操作成功rejected
:已拒绝,操作失败
MyPromise
增加result
属性,记录成功/失败原因- 调用
resolve
或reject
,修改状态,并记录成功/失败原因
const p = new HMPromise((resolve, reject) => {resolve('success') // pending -> fulfilled// reject('error') // pending -> rejected
})
p.state // 状态
p.result // 原因
核心步骤:
- 定义常量保存状态,避免硬编码
MyPromise
中定义- 属性:
state
保存状态,result
成功/失败原因 - 修改
state
的私有方法,修改状态并记录result
- 注意:
state
只有在pending
时,才可以修改,且不可逆
- 属性:
const PENDING = "pending"
const FULFILLED = "fulfilled"
const REJECTED = "rejected"
class MyPromise {constructor(func) {// 1. 添加状态this.state = PENDING;// 2. 添加原因this.result = undefined;// 3. 调整resolve和reject// 4. 状态不可逆// 改状态:pending->fulfilled// 记录原因const resolve = (result) => {if (this.state = PENDING) {this.state = FULFILLEDthis.result = result}}// 改状态:pending->rejected// 记录原因const reject = (result) => {if (this.state = PENDING) {this.state = REJECTEDthis.result = result}}func(resolve, reject);}
}
**面试回答:**手写Promise-状态、成功or失败原因
- 定义3个常量用来保存状态,
pending
,fulfilled
,rejected
MyPromise
内部定义属性state
和result
分别用来保存状态和原因- 调用
resolve
时传入具体原因,如果状态为pending
则更改状态并记录兑现原因 - 调用
reject
时传入具体原因,如果状态为pending
则更改状态并记录拒绝原因
then方法
then方法–成功和失败回调
需求:
- then方法的回调函数1: 状态变为
fulfilled
时触发,并获取成功结果 - then方法的回调函数2: 状态变为
rejected
时触发,并获取失败原因 - then方法的回调函数1或2没有传递的特殊情况处理,参考:then方法的参数
const p = new MyPromise((resolve, reject) => {resolve('success')// reject('error')
})
p.then(res => {console.log('成功回调:', res)
}, err => {console.log('失败回调:', err)
})
核心步骤:
- 增加
then
方法,根据不同的状态执行对应的回调函数,并传入result
- 参数1:成功的回调函数
- 参数2:失败的回调函数
- 判断参数
- 没有传递
onFulfilled
,onRejected
时 - 设置默认值(参考文档)如果
onFulfilled
不是一个函数,则内部会被替换为一个恒等函数((x) => x
),它只是简单地将兑现值向前传递。如果onRejected
不是一个函数,则内部会被替换为一个抛出器函数((x) => { throw x; }
),它会抛出它收到的拒绝原因。
- 没有传递
const PENDING = "pending"
const FULFILLED = "fulfilled"
const REJECTED = "rejected"
class MyPromise {constructor(func) {this.state = PENDING;this.result = undefined;const resolve = (result) => {if (this.state = PENDING) {this.state = FULFILLEDthis.result = result}}const reject = (result) => {if (this.state = PENDING) {this.state = REJECTEDthis.result = result}}func(resolve, reject);}// 1. 添加实例方法then(onFulfilled, onRejected) {// 2. 参数判断// 如果onFulfilled不是一个函数,则内部会被替换为一个恒等函数(x) => x,它只是简单地将兑现值向前传递。// 如果onRejected不是一个函数,则内部会被替换为一个抛出器函数(x) => { throw x; },它会抛出它收到的拒绝原因。onFulfilled = typeof (onFulfilled) === "function" ? onFulfilled : x => xonRejected = typeof (onRejected) === "function" ? onRejected : x => { throw x; }// 3. 执行成功回调// 4. 执行失败回调if (this.state === FULFILLED) {onFulfilled(this.result)} else if (this.state === REJECTED) {onRejected(this.result)}}
}
面试回答:手写Promise-then方法-成功和失败回调
- 添加
then
方法,接收2个回调函数:- 成功回调
onFulfilled
- 失败回调
onRejected
- 成功回调
- 判断传入的
onFulfilled
和onRejected
是否为函数,如果不是设置默认值 - 根据状态调用
onFulfilled
或onRejected
并传入兑现或拒绝原因
then方法-异步和多次调用
需求:
- 实例化传入的回调函数,内部支持异步操作
then
方法支持多次调用(非链式编程)
const p = new MyPromise((resolve, reject) => {setTimeout(() => {resolve('success')// reject('error')}, 2000);
})
p.then(res => {console.log('then1:', res)
}, err => {console.log('then1:', err)
})
p.then(res => {console.log('then2:', res)
}, err => {console.log('then2:', err)
})
核心步骤:
- 定义属性:保存传入的回调函数:
[]
- 保存回调函数:状态为
pending
时 - 调用成功回调:
resolve
内部 - 调用失败回调:
reject
内部
const PENDING = "pending"
const FULFILLED = "fulfilled"
const REJECTED = "rejected"
class MyPromise {constructor(func) {this.state = PENDING;this.result = undefined;// 1. 定义实例属性 保存调用then时状态为pending时的回调函数this.resolveCb = [];this.rejectCb = [];const resolve = (result) => {if (this.state = PENDING) {this.state = FULFILLEDthis.result = result// 3. 调用成功的回调this.resolveCb.forEach(p => p() )}}const reject = (result) => {if (this.state = PENDING) {this.state = REJECTEDthis.result = result// 4. 调用失败的回调this.rejectCb.forEach(p => p() )}}func(resolve, reject);}then(onFulfilled, onRejected) {onFulfilled = typeof (onFulfilled) === "function" ? onFulfilled : x => xonRejected = typeof (onRejected) === "function" ? onRejected : x => { throw x; }if (this.state === FULFILLED) {onFulfilled(this.result)} else if (this.state === REJECTED) {onRejected(this.result)} else if (this.state === PENDING) {// 2. 保存回调函数this.resolveCb.push(() => {onFulfilled(this.result)})this.rejectCb.push(() => {onRejected(this.result)})}}
}
**面试回答:**手写Promise-then方法-异步和多次调用
MyPromise
添加属性resolveCB
和rejectCB
resolveCB
:保存then方法调用时状态为pending
状态的回调函数rejectCB
:保存then方法调用时状态为pending
状态的回调函数- 格式为数组
- 调整
then
方法中当前状态为pending
时,保存回调函数。 - 构造函数内部调整
resolve
和reject
的逻辑:- 调用
resolve
时取出数组resolveCB
中的所有回调函数进行调用 - 调用
reject
时取出数组rejectCB
中的所有回调函数进行调用
- 调用
异步任务
需求:
让then方法的回调函数以异步任务的方式执行
console.log('top')
const p = new MyPromise((resolve, reject) => {resolve('success')
})
p.then(res => {console.log(res)
})
console.log('bottom')
核心步骤:
调整then
中的逻辑,fulFilled
,rejected
,pending
3种状态时的回调函数,使用定时器延迟进行包装
const PENDING = "pending"
const FULFILLED = "fulfilled"
const REJECTED = "rejected"
class MyPromise {constructor(func) {this.state = PENDING;this.result = undefined;this.resolveCb = [];this.rejectCb = [];const resolve = (result) => {if (this.state = PENDING) {this.state = FULFILLEDthis.result = resultthis.resolveCb.forEach(p => p())}}const reject = (result) => {if (this.state = PENDING) {this.state = REJECTEDthis.result = resultthis.rejectCb.forEach(p => p())}}func(resolve, reject);}then(onFulfilled, onRejected) {onFulfilled = typeof (onFulfilled) === "function" ? onFulfilled : x => xonRejected = typeof (onRejected) === "function" ? onRejected : x => { throw x; }if (this.state === FULFILLED) {// 1. fulfilled状态的回调函数用定时器包装setTimeout(() => {onFulfilled(this.result)}, 0);} else if (this.state === REJECTED) {// 2. rejected状态的回调函数用定时器包装setTimeout(() => {onRejected(this.result)}, 0);} else if (this.state === PENDING) {this.resolveCb.push(() => {// 3. pending状态的回调函数用定时器包装setTimeout(() => {onFulfilled(this.result)}, 0);})this.rejectCb.push(() => {// 4. pending状态的回调函数用定时器包装setTimeout(() => {onRejected(this.result)}, 0);})}}
}
**面试回答:**手写Promise-异步任务
调整then
中的逻辑,fulFilled
,rejected
,pending
3种状态时的回调函数,使用定时器延迟进行包装
链式编程
链式编程-fulfilled状态
需求:
then
的链式编程- 目前只考虑
then
的第一个回调函数
const p = new MyPromise((resolve, reject) => {resolve(1)
})
const p2 = p.then(res => {throw 'error'// return p2// return 2/* return new MyPromise((resolve, reject) => {// resolve('HMPromise-res')reject("HMPromise-err")}) */
})
p2.then(res => {console.log('p2-res:', res)
}, err => {console.log('p2-err:', err)
})
1. 内部出现异常
2. 返回普通值
3. 返回Promise
4. 重复引用
核心步骤:
- 调整
then
方法,返回一个新的MyPromise
对象 - 使用
try-catch
捕获内部异常,并通过reject
传递 - 内部获取
onFulfilled
的执行结果判断是否为MyPromise
实例- 若是,调用返回值的
then
方法,获取兑现和拒绝的原因并通过resolve
和reject
传递即可 - 若不是,将返回值
resolve
传递
- 若是,调用返回值的
- 判断
onFulfilled
函数的返回值是否和then
方法内部返回的MyPromise
相同,如果相同抛出错误new TypeError('Chaining cycle detected for promise #<Promise>')
const PENDING = "pending"
const FULFILLED = "fulfilled"
const REJECTED = "rejected"
class MyPromise {constructor(func) {this.state = PENDING;this.result = undefined;this.resolveCb = [];this.rejectCb = [];const resolve = (result) => {if (this.state = PENDING) {this.state = FULFILLEDthis.result = resultthis.resolveCb.forEach(p => p())}}const reject = (result) => {if (this.state = PENDING) {this.state = REJECTEDthis.result = resultthis.rejectCb.forEach(p => p())}}func(resolve, reject);}then(onFulfilled, onRejected) {onFulfilled = typeof (onFulfilled) === "function" ? onFulfilled : x => xonRejected = typeof (onRejected) === "function" ? onRejected : x => { throw x; }// 1. 返回新的Promise实例const p2 = new MyPromise((resolve, reject) => {if (this.state === FULFILLED) {setTimeout(() => {// 2. try-catch处理内部异常try {// 3. 获取回调函数返回值const x = onFulfilled(this.result)// 4. 处理返回值// 4.3 重复引用if (x === p2) {throw new TypeError("Chaining cycle detected for promise #<Promise>")}// 4.2 返回值是Promiseif (x instanceof MyPromise) {x.then(res => {resolve(res)}, err => {reject(err)})} else {// 4.1 返回值是普通值resolve(x)}} catch (error) {reject(error)}}, 0);} else if (this.state === REJECTED) {setTimeout(() => {onRejected(this.result)}, 0);} else if (this.state === PENDING) {this.resolveCb.push(() => {setTimeout(() => {onFulfilled(this.result)}, 0);})this.rejectCb.push(() => {setTimeout(() => {onRejected(this.result)}, 0);})}})return p2}
}
面试回答:手写Promise-链式编程-fulfilled状态
- 链式编程的本质
then
方法会返回一个新的MyPromise
对象 - 将原本的代码迁移到返回的
MyPromise
对象的回调函数中 - 内部通过
try-catch
捕获异常,出现异常通过reject
传递异常 - 获取
onFulfilled
的执行结果,判断是否为MyPromise
实例- 若是,调用返回值的
then
方法,获取兑现和拒绝的原因并通过resolve
和reject
传递即可 - 若不是,将返回值通过
resolve
传递
- 若是,调用返回值的
- 处理重复引用,判断
onFulfilled
函数的返回值是否和then
方法内部返回的MyPromise
相同,如果相同抛出错误。
链式编程-rejected状态
需求:
then
的第二个回调函数,执行reject
时的链式编程
const p = new MyPromise((resolve, reject) => {reject(1)
})
const p2 = p.then(undefined, err => {throw 'error'// return p2// return 2// return new MyPromise((resolve, reject) => {// resolve('HMPromise-res')// // reject('HMPromise-err')// })
})
p2.then(res => {console.log('p2-res:', res)
}, err => {console.log('p2-err:', err)
})
核心步骤:
- 处理内部异常:
onRejected
的异常 - 获取返回值:
onRejected
的返回值 - 将
fulfilled
状态中的处理逻辑抽取为函数resolvePromise
并复用 fulfilled
和rejected
状态中调用函数resolvePromise
const PENDING = "pending"
const FULFILLED = "fulfilled"
const REJECTED = "rejected"
// 3. then()方法中实现链式编程-处理返回值操作的抽取函数
function resolvePromise(p2, x, resolve, reject) {if (x === p2) {throw new TypeError("Chaining cycle detected for promise #<Promise>")}if (x instanceof MyPromise) {x.then(res => {resolve(res)}, err => {reject(err)})} else {resolve(x)}
}
class MyPromise {constructor(func) {this.state = PENDING;this.result = undefined;this.resolveCb = [];this.rejectCb = [];const resolve = (result) => {if (this.state = PENDING) {this.state = FULFILLEDthis.result = resultthis.resolveCb.forEach(p => p())}}const reject = (result) => {if (this.state = PENDING) {this.state = REJECTEDthis.result = resultthis.rejectCb.forEach(p => p())}}func(resolve, reject);}then(onFulfilled, onRejected) {onFulfilled = typeof (onFulfilled) === "function" ? onFulfilled : x => xonRejected = typeof (onRejected) === "function" ? onRejected : x => { throw x; }const p2 = new MyPromise((resolve, reject) => {if (this.state === FULFILLED) {setTimeout(() => {try {const x = onFulfilled(this.result)// 5. fulfilled状态下改为调用函数resolvePromise(p2, x, resolve, reject)} catch (error) {reject(error)}}, 0);} else if (this.state === REJECTED) {setTimeout(() => {// 1. 处理内部异常try {// 2. 获取返回值const x = onRejected(this.result)// 4. 调用函数resolvePromise(p2, x, resolve, reject)} catch (error) {reject(error)}}, 0);} else if (this.state === PENDING) {this.resolveCb.push(() => {setTimeout(() => {onFulfilled(this.result)}, 0);})this.rejectCb.push(() => {setTimeout(() => {onRejected(this.result)}, 0);})}})return p2}
}
**面试回答:**手写Promise-链式编程-rejected状态
- 判断
onRejected
可能出现的异常,如果出现通过reject
传递 - 获取
onRejected
函数的执行结果 - 将
fulfilled
状态时的处理逻辑抽取为函数,rejected
状态时调用函数复用逻辑
链式编程-pending状态
需求:
- 执行异步操作时,支持链式编程
const p = new MyPromise((resolve, reject) => {setTimeout(() => {resolve(1)}, 2000)
})
const p2 = p.then(res => {throw 'error'// return p2// return 2// return new MyPromise((resolve, reject) => {// resolve('resolve-2')// // reject('reject-2')// })
})
p2.then(res => {console.log('p2-res:', res)
}, err => {console.log('p2-err:', err)
})
核心步骤:
- 处理异常:
pending
状态时推入回调函数数组时增加try-catch
- 获取返回值:
- 推入数组时,增加获取返回值的操作
- 调用上一节封装的函数
resolvePromise
const PENDING = "pending"
const FULFILLED = "fulfilled"
const REJECTED = "rejected"
function resolvePromise(p2, x, resolve, reject) {if (x === p2) {throw new TypeError("Chaining cycle detected for promise #<Promise>")}if (x instanceof MyPromise) {x.then(res => {resolve(res)}, err => {reject(err)})} else {resolve(x)}
}
class MyPromise {constructor(func) {this.state = PENDING;this.result = undefined;this.resolveCb = [];this.rejectCb = [];const resolve = (result) => {if (this.state = PENDING) {this.state = FULFILLEDthis.result = resultthis.resolveCb.forEach(p => p())}}const reject = (result) => {if (this.state = PENDING) {this.state = REJECTEDthis.result = resultthis.rejectCb.forEach(p => p())}}func(resolve, reject);}then(onFulfilled, onRejected) {onFulfilled = typeof (onFulfilled) === "function" ? onFulfilled : x => xonRejected = typeof (onRejected) === "function" ? onRejected : x => { throw x; }const p2 = new MyPromise((resolve, reject) => {if (this.state === FULFILLED) {setTimeout(() => {try {const x = onFulfilled(this.result)resolvePromise(p2, x, resolve, reject)} catch (error) {reject(error)}}, 0);} else if (this.state === REJECTED) {setTimeout(() => {try {const x = onRejected(this.result)resolvePromise(p2, x, resolve, reject)} catch (error) {reject(error)}}, 0);} else if (this.state === PENDING) {this.resolveCb.push(() => {setTimeout(() => {// 1. 处理异常try {// 2. 获取返回值const x = onFulfilled(this.result)// 3. 调用函数resolvePromise(p2, x, resolve, reject)} catch (error) {reject(error)}}, 0);})this.rejectCb.push(() => {setTimeout(() => {// 1. 处理异常try {// 2. 获取返回值const x = onRejected(this.result)// 3. 调用函数resolvePromise(p2, x, resolve, reject)} catch (error) {reject(error)}}, 0);})}})return p2}
}
**面试回答:**手写Promise-链式编程-pending状态
then
方法中pending
状态时推入数组的函数增加try-catch
捕获异常- 获取推入数组的回调函数的返回值
- 调用上一节封装的函数并传入获取的值
实例方法-Promise.catch()
需求:
- 实现实例方法
catch
,可以实现如下调用
const p = new HMPromise((resolve, reject) => {reject('reject-error')// throw 'throw-error'
})
p.then(res => {console.log('res:', res)
}).catch(err => {console.log('err:', err)
})
核心步骤:
- 参考文档,catch等同于:
then(undefined,onRejected)
- 直接添加
catch
方法,内部调用then
- 使用
try-catch
包裹constructor
中的func
捕获异常
const PENDING = "pending"
const FULFILLED = "fulfilled"
const REJECTED = "rejected"
function resolvePromise(p2, x, resolve, reject) {if (x === p2) {throw new TypeError("Chaining cycle detected for promise #<Promise>")}if (x instanceof MyPromise) {x.then(res => {resolve(res)}, err => {reject(err)})} else {resolve(x)}
}
class MyPromise {constructor(func) {this.state = PENDING;this.result = undefined;this.resolveCb = [];this.rejectCb = [];const resolve = (result) => {if (this.state = PENDING) {this.state = FULFILLEDthis.result = resultthis.resolveCb.forEach(p => p())}}const reject = (result) => {if (this.state = PENDING) {this.state = REJECTEDthis.result = resultthis.rejectCb.forEach(p => p())}}// 2. 处理异常try {func(resolve, reject);} catch (error) {reject(error)}}then(onFulfilled, onRejected) {onFulfilled = typeof (onFulfilled) === "function" ? onFulfilled : x => xonRejected = typeof (onRejected) === "function" ? onRejected : x => { throw x; }const p2 = new MyPromise((resolve, reject) => {if (this.state === FULFILLED) {setTimeout(() => {try {const x = onFulfilled(this.result)resolvePromise(p2, x, resolve, reject)} catch (error) {reject(error)}}, 0);} else if (this.state === REJECTED) {setTimeout(() => {try {const x = onRejected(this.result)resolvePromise(p2, x, resolve, reject)} catch (error) {reject(error)}}, 0);} else if (this.state === PENDING) {this.resolveCb.push(() => {setTimeout(() => {try {const x = onFulfilled(this.result)resolvePromise(p2, x, resolve, reject)} catch (error) {reject(error)}}, 0);})this.rejectCb.push(() => {setTimeout(() => {try {const x = onRejected(this.result)resolvePromise(p2, x, resolve, reject)} catch (error) {reject(error)}}, 0);})}})return p2}// catch方法 允许链式编程// MDN官网上对catch的实现方法是:Promise.prototype.then(undefined,onRejected)的一种简写形式// 1. 直接添加catch方法,内部调用thencatch(onRejected) {return this.then(undefined, onRejected)}
}
面试回答:手写Promise-实例方法catch
- 定义
catch
方法,接收拒绝的回调函数onRejected
catch
方法的本质是内部调用then
方法- 调用形式为第一个回调函数传入
undefined
,第二个回调函数传入onRejected
即可 - 链式调用,return
- 使用
try-catch
包裹constructor
中的func
捕获异常—这一步建议放在构造函数那一步中
实例方法-Promise.finally()
需求:
- 无论成功失败都会执行
finally
的回调函数 - 回调函数不接受任何参数
const p = new MyPromise((resolve, reject) => {// resolve('resolve-res')// reject('reject-error')// throw 'throw-error'
})
p.then(res => {console.log('res:', res)
}).catch(err => {console.log('err:', err)
}).finally(() => {console.log('finally')
})
核心步骤:
- 参考文档:finally方法类似于调用
then(onFinally,onFinally)
,且不接受任何回调函数 - 链式调用,return。
// 实例化方法 finally 无论成功失败都要执行的函数 支持链式编程(要return)
// MDN官网上对finally的实现方法是:类似于调用 then(onFinally,onFinally)
finally(onFinally) {return this.then(onFinally,onFinally)
}
面试回答:手写Promise-实例方法finally
- 添加
finally
方法,接收最终执行的回调函数onFinally
finally
方法的本质为内部调用then
方法- 调用形式为第一个和第二个回调函数均传入
onFinally
即可
静态方法-Promise.resolve()
静态方法-Promise.reject()
静态方法-Promise.race()
静态方法-Promise.all()
静态方法-Promise.allSettled()
静态方法-Promise.any()
十六.find()
find()
方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined
;找到满足条件,后续代码不执行。
用法:
- find() 方法返回通过测试(函数内判断)的数组的第一个元素的值。
- 如果没有符合条件的元素返回 undefined
- find() 对于空数组,函数是不会执行的。
- find() 并没有改变数组的原始值。
- array.find(function(currentValue, index, arr),thisValue),其中currentValue为当前项,index为当前索引,arr为当前数组
例子:
const inventory = [{name: 'apples', quantity: 2},{name: 'bananas', quantity: 0},{name: 'cherries', quantity: 5}
];function findCherries(item) { return item.name === 'bananas';
}console.log(inventory.find(findCherries)); // { name: 'bananas', quantity: 0 }
十七.面试题
一、参考:判断一个对象上是否包含 一个属性的几种方法
1,“!==”进行判断,返回是布尔值,这种方法很常见,例如:
let obj = {name:"zhang" , age:20};
obj.name !=== undefined; // true 表示有这个属性
obj.sex !=== undefined;// fasle 表示无 sex 这个属性
obj.toString !=== undefined; // true 表示对象继承toString属性
2,in的语法:attr in obj;返回是 布尔值,in方法可以检测对象的所有属性,不管是私有还是公有,只要有都显示true
'name' in obj; // true 表示有这个属性
'sex' in obj;// fasle 表示无 sex 这个属性
'toString' in obj; // true 表示对象继承toString属性
in 与上面的undefined效果是一样的,但是唯一不同点是 in 可以区分 undefined 的属性值
let obj = {value:undefined};
obj.value === undefined; //true
'value' in obj ; //true
3、for … in:判断一个属性是否在对象上
var obj={ name:‘张学友’,age:18;}
var isExsit =“name”in obj;// name 没有加引号,浏览器默认为变量,会报错,未定义
console.log(isExsit); // true
4、hasOwnProperty() 方法 是检测对象是都某一属性名,返回布尔值,这里只能检测对象的私有属性,继承属性检测不出来
let obj = {name:"zhang" , age:20};
obj.hasOwnProperty(‘name’); // true 表示有这个属性
obj.hasOwnProperty(‘sex’);// fasle 表示无 sex 这个属性
obj.hasOwnProperty(‘toString’);// fasle 表示没有继承这个属性
5、hasPubProperty:用来检查属性是否为对象的公有属性
function foo() {this.name = 'foo'this.sayHi = function () {console.log('Say Hi')}
}foo.prototype.sayGoodBy = function () {console.log('Say Good By')
}let myPro = new foo()console.log(myPro.hasOwnProperty('sayHi')) // true
console.log(myPro.hasOwnProperty('sayGoodBy')) // true
6、typeof x;返回的是它的数据类型;
注:typeof-----检测数据类型作用,不能细分object下面的对象,数组,正则...
typeof true; // 'boolean'typeof "abc"; //'string'typeof function() {};// 'function'typeof {};// 'object'typeof [];// 'object'typeof null;//object
7、instanceof------即可检测是否是当前实例的类,还可以检查一个实例是否属于这个类
注:可以判断是否是一个数组
var arr=new Array;
arr instanceof Array;// true =>是为true,不是为false
var arr=[]
arr instanceof Array; // true =>是为true,不是为false
8、includes():
var name = "王汉炎"; //目标字符串
name.includes('炎'); // true;返回的是一个布尔值search(keywords) {return this.list.filter(item => {if (item.name.includes(keywords)) {// 判断是否包含,包含 true,不包含 falsereturn item}})
}
9、indexof():返回某个指定的字符串值在字符串中首次出现的位置。
stringObject.indexOf(searchvalue,fromindex)
searchvalue:必需。规定需检索的字符串值
fromindex:可选;规定在字符串中开始检索的位置。它的合法取值是 0 到 stringObject.length - 1。如省略该参数,则将从字符串的首字符开始检索。
var nameList = ['A', 'B', 'C', 'D', 'E', 'F']; //目标字符串
nameList .indexOf('C') > -1; // true;返回的是一个布尔值
- indexOf() 方法对大小写敏感!
- 如果要检索的字符串值没有出现,则该方法返回 -1。
10、isPrototypeOf():检测一个对象是否是另一个对象的原型。或者说一个对象是否被包含在另一个对象的原型链中
var p = {x:1};//定义一个原型对象
var o = Object.create(p);//使用这个原型创建一个对象
p.isPrototypeOf(o);//=>true:o继承p
Object.prototype.isPrototypeOf(p);//=> true p继承自Object.prototype
11,【ES6】for of :
for in遍历的是数组的索引(即键名),而for of遍历的是数组元素值。
// 示例 一:
var arr = ['nick','freddy','mike','james'];
for(var item of arr){ console.log(item); // nick,freddy,mike,james,undefined 遍历数组中的每一项
}// 示例二:
var arr = [{ name:'nick', age:18 },{ name:'freddy', age:24 },{ name:'mike', age:26 },{ name:'james', age:34 }
];
for(var item of arr){ console.log(item.name,item.age);
}
// 'nick',18
// 'freddy',24
// 'mike',26
// 'james',34
// undefined
与 for in 的区别
1,for in可以遍历对象,for of无法循环遍历对象,
var userMsg = {0: 'nick', 1: 'freddy', 2: 'mike', 3: 'james'
};for(var key in userMsg){console.log(key, userMsg[key]);
}
// 0: 'nick'
// 1: 'freddy'
// 2: 'mike'
// 3: 'james'for(var item of userMsg){ console.log(item);// TypeError: userMsg is not iterable
}
2, 遍历输出结果不同
var arr = ['nick','freddy','mike','james'];
for(var key in arr){console.log(key); // 0 1 2 3 返回索引值
}for(var item of arr){ console.log(item);// nick,freddy,mike,james
}
3, for in 会遍历所有自定义属性,for of不会遍历新加的属性
var arr = ['nick','freddy','mike','james'];
arr.name = "数组";for(var key in arr){console.log(key+': '+arr[key]);
}
// 0: nick
// 1: freddy
// 2: mike
// 3: james
// name: 数组for(var item of arr){ console.log(item);// nick,freddy,mike,james
}
4, for of 可以正确响应break、continue和return语句,但是for in 不行
以上的几种方法就是我暂时学到的几种方法。