1.概述:
ECMAScript、Javascript、Node.js之间的区别是什么。
ECMAScript:简称ES,是一个语言标准(循环,变量,判断,数组这些基本数据类型的构成标准。)
JavaScript:运行在浏览器端的语言,该语言使用的就是ES标准。ES+web api=JavaScript
NodeJs:运行在服务器端的语言,同上。ES+node api=nodejs
关键版本,es3.0 1999年 ,es5.02009年,es6.0 2015,从该年开始用年份代表。es4.0出了问题下架了
es6解决js无法开发大型应用的语言层面问题,是es6极其重要的原因
一、块级绑定
声明变量的问题
过去使用var 来声明变量
1.允许重复的变量声明:导致数据被覆盖
由var来进行定义变量容易重名,导致闭包。
2.变量提升;怪异的数据访问
由var声明的变量在函数中会产生变量提升导致逻辑十分的怪异,典型的有闭包问题,在循环过程中,由于变量提升,导致循环结束了,变量才进入函数
3.全局变量挂载到全局对象,全局对象成员污染问题。
var的变量如果赋值到window里已有的变量会导致全局对象原有的成员被污染。
var abc=“123”;
console.log(window.abc);//会将变量abc挂到全局对象上
var console="abc";如果给全局对象上的东西赋值会导致原本console这个对象消失(污染)
console.log(console);
由于var存在上面的问题,所以需要引入块级绑定。
let声明的变量可以解决上面问题。
let声明的变量,不允许当前作用域范围内重复声明;不会挂载到全局,解决var的使用问题,引入块级作用域的概念,代码执行时遇到花括号,会创建一个块级作用域,花括号结束,销毁块级作用域。使用方法
if(Math.random()<0.5){
let a=123;//定义在当前作用域块
console.log(a)//当前块级作用域中的a,123
}
else{
//这是另外一个块级作用域,该作用域中找不到a
console.log(a),报错, a no define
}
console.log(a);,报错
局部的可以获取全部的,全部的不能获取局部的
在底层逻辑上,let声明实际上会提升,只不过提升后会将其放入到“暂时性死区”如果放到暂时性死区会报错。范例代码:
console.log(a);let a=123;
在循环中,用let声明的循环变量,会特殊处理,每次进入循环体,都会开启一个新的作用域,并且将循环变量绑定到该作用域(每次循环,使用的都是一个全新的循环变量)
varbtns=document.getElementById("btn");
for (leti=0; i<10; i++) {
varbtn=document.createElement("button");
btn.innerHTML="按钮"+i;//btn为var声明的变量,所以下面也是点击btn
btn.οnclick=function () {
console.log(i);
}
btns.appendChild(btn)
}
使用const声明常量
const和let一样,必须在声明时赋值,不可以重新赋值,实际上,在开发中,多应用const声明变量,保证变量的值不会篡改。
根据经验,开发中的很多变量,都是不会更改,也不应该更改的。
后续的很多框架或者是第三方js库,都要求数据不可变,所以使用const
细节:
1.常量不可变,是指声明的常量的内存空间不可变,并不保证内存空间中的地址指向的其他空间不可变。
consta=1;
a=2;//报错,由于常量不可以改变,
constb={
age : kevin,
number : 123,
};
b.age=123;//可以修改,保存的是b本身,里面的内容可以改变
2.特殊的命名是不可变的,比如说一些定理之类的常量,就用const定义变量并且改成变量名使用大写,多个单词之间用下划线分割,普通的常量使用之前一样的命名规则
3.for循环当中不能使用const来定义常量(easy!)
更好的Unicode支持
早期,由于存储空间宝贵,unicode使用16位二进制来存储文字,我们将一个16位的二进制编码叫做一个码元(Code Unit),二的十六次方,不够存储文字。
后面,由于技术的发展,Unicode对文字编码进行了扩展,将某些文字扩展到了32位(占用两个码元),并且,将某个文字对应的二进制数字叫做码点(code point)。
es6解决了 码元和码点之间的矛盾,为字符串提供了方法:charCodePoint。
4.更多的字符串api
includes,startsWith,endsWith,repeat
includes判断字符串中包含指定字符串
startsWith判断字符串中是否以指定字符串开头
endsWith判断字符串中是否以指定字符串结尾
repeat把指定字符串重复n次
const text="强哥是狠人";
console.log('是否包含“狠”:',text.includes('狠'));
console.log('是否以成哥为开头:',text.startsWith('强哥'));
console.log("是否以狠人为结尾:",text.endsWith('狠人'));
console.log('重复4次:',text.repeat(4));
正则中的粘连标记:
标记名:y,
含义:匹配时,完全按照正则对象中的lastIndex位置开始匹配,并且匹配的位置 必须在lastIndex位置。
const text="Hello World!!!";
const reg=/W\w+/;//匹配以W为开头后面是任意的多个字符
console.log("reg.lastIndex:",reg.lastIndex);
console.log(reg.test(text))//true
const text1="Hello World!!!";
const reg1=/W\w+/y;//加y变为从lastindex位置开始匹配,即Hello位置开始匹配
console.log("reg1.lastIndex:",reg1.lastIndex);
console.log(reg1.test(text1))//false
如果修改lastIndex的位置也会改变结果,如果没有加粘连标志就会往后面的地方查找。
模板字符串:
es6之前处理字符串繁琐的两个方面:
1.多行字符串2.字符串拼接
在es6中,提供了模板字符串的书写,可以非常方便的换行和拼接,要做的,仅仅是将字符串的开始和结尾改为`符号
换行:
var text=`邓哥喜欢秋葵
邓哥也喜欢韭菜`;直接进行换行,换行中最好不要加tab键,否则结果也会是带有tab键。
如果要在字符串中拼接js表达式,只需要在模板字符串中使用````${JS表达式}````
模板字符串的标记(开发不常见)暂时不学
函数:
1.参数问题:
书写形参时直接给形参赋值,赋的值即为默认值,这样一来如果没有给参数赋值(给他的值是undefined)则会自动赋为默认值,不能传null,null在数学中是0;如果想传三个参数,则第二个参数传undefined其余按需求来
functionsum(a, b =2, c =3) {
returna+b+c;
}
console.log(sum(10, null, 3))//13
console.log(sum(10, undefined, 3))//15
/**
*
* @param{*}name 元素的名称
* @param{*}container 元素的父元素
* @param{*}content 元素的内容
*/
functioncreateElement(name ="div", container =document.getElementById("container"), content ="") {
constele=document.createElement(name);
if (content) {
ele.innerHTML=content;
}
container.appendChild(ele);
}
createElement(undefined, undefined, "js");
参数默认值对arguments的影响:
只要给函数形参加上默认值,该函数会自动变成严格模式(即arguments和形参脱离)
留意暂时性死区:
形参和es6中的let和const声明一样,具有作用域,并且根据参数的声明顺序,存在暂时性死区
functiontest(a=b,b){
console.log(a,b);
}
test(undefined,2);//由于a传的是undefined,所以使用默认值,但是b尚未声明,存在暂时死区,所以会报错
剩余参数
考虑到传的参数可能有无限多个,对于函数来说只会设计一次,而调用函数的情况则是十分多的
arguments的缺陷:
1.如果和形参配合使用,容易导致混乱(严格模式下二者分离,否则其中一个改变另一个跟着改变)
2.从语义上,使用arguments获取参数,由于形参的缺失,无法从函数定义上理解函数的真实意义
es6的剩余参数专门用于收集末尾的所有参数,将其放置到一个形参数组中。
functionsum(...args) {
letsum=0;
for (leti=0; i<args.length; i++) {
sum+=args[i];
}
return sum;
}
console.log(sum());
console.log(sum(1));
console.log(sum(1, 2));
console.log(sum(1, 2, 3));
语法:function(...形参名){}
细节:
一个函数仅能出现一个剩余参数;
一个函数如果有剩余参数,剩余参数必须是最后一个参数
展开运算符:
1.对数组展开(es6):
...要展开的数组(相比较剩余运算符只是位置不同)
functionsum(...args) {
let sum=0;
for (leti=0; i<args.length; i++) {
sum+=args[i];
}
return sum;
}
functiongetRandomNumbers(length) {
constarr= [];
for (leti=0; i<length; i++) {
arr.push(Math.random());
}
returnarr;
}
constnumber=getRandomNumbers(10);
console.log(number);
//console.log(sum(number));//报错,因为传入的是一个数组(一个参数)而并非多个参数,循环不能执行
console.log(...number,1,2)//相当于传入十二个参数,参数的位置也比较灵活
constarr1=[3,7,8,5];
constarr2=[...arr1];
//克隆arr1到arr2中
console.log(arr2,arr2===arr1)//深度克隆
2.对对象展开(es7)
constobj={
name:"lf",
age:18,
addr:{
country:"china"
}
}
constobj1={
...obj,
name:"wwq",//会覆盖前面的
// addr:{
// ...obj.addr,//将其单独深克隆
// }
}
console.log(obj1);
console.log(obj===obj1);//false,深度克隆
console.log(obj.addr===obj1.addr);//true,浅克隆,因为addr没有被克隆
柯里化(curry)
function cal(a,b,c,d){
return a+b*c-d;
}
/**
*
● @param {*} cal 上面所设计的函数
● @param {...any} args 固定的函数数量
*/
function curry(func,...args){
return function(...subargs){//返回一个新的函数(newcal),subargs是新函数所需的参数
const allargs=[...args,...subargs];
if(allargs.length>=func.length){//参数足够了
return func(...allargs);
}
else{//参数不足
return curry(func,...allargs);//继续固定
}
}
}
const newcal=curry(cal,1,2)//curry函数,前两位固定1,2
console.log(newcal(3,4))
console.log(newcal(5,6))
console.log(newcal(4,5))
console.log(newcal(6,7))
// const newcal2=newcal(8);//newcal只传了一个参数不够
// //curry:柯里化,用于固定某个函数前面的参数,得到一个新的函数,新的函数调用时,接收剩余参数
// console.log(newcal2(9))//再传入一个参数,结果为1+2*8-9
明确函数的双重用途:
function Person(firstName, lastName) {
//过去的判断方式
//if(!(this instanceof Person)){
//throw new Error("该函数没有使用new来调用");
//}
if(new.target===undefined){
throw new Error("该函数没有使用new来调用");
}
this.firstName = firstName;
this.lastName = lastName;
this.fullName = `${ firstName }${ lastName }`;
}
const p1 = new Person("小", "王");
console.log(p1);
const p2 = Person("小", "王");
console.log(p2);//undefined,因为没有使用构造函数,直接默认再window上,所以是undefined
const p3=Person.call(p1,"小',"王")
console.log(p3);//undefined,绕开了过去的判断方式
es6提供了一个特殊api,可以使用该api在函数内部,判断该函数是否使用了new来调用(构造函数)
·····js
new.target//该表达式,得到的是,如果没有用new来调用函数,则返回undefined;如果用new来调用,则得到的是new关键字后面的函数本身
········
箭头函数:
回顾this指向:
1.通过对象调用函数,this指向对象,
2.直接调用函数,this指向全局对象
3.如果通过new调用函数,this指向新创建的对象
4.如果通过apply、call、bind调用函数,this指向指定的数据
5.如果是DOM事件函数,this指向事件源。
使用语法:
箭头函数是一个函数表达式,理论上,任何使用函数表达式的场景都可以使用箭头函数
完整语法:
(参数1,参数2,...)=>{ //函数体
}
简单语法(只有一个参数):
参数=>{
//函数体
}
如果箭头函数只有一条返回语句,可以省略大括号,和return关键字
参数=>返回值,如果返回的是对象,应该使用()。
注意细节:
箭头函数中,不存在this,arguments,new.target,如果使用了,则使用的是函数外层的对应的this,arguments,new.target
箭头函数没有原型,所以箭头函数不能作为构造函数使用
应用场景:
1.临时性使用的函数,并不会刻意调用它比如:
事件处理函数
异步处理函数(setTimeout,setInterval)
其他临时性的函数
2.为了绑定外层this的函数
3.在不影响其他代码的情况下,保持代码的简洁,最常见的就是数组中的回调函数
const number=[2,3,7,8,15,16];
const result=number.filter(num=>num%2!==0).map(num=>num*2).reduce((a,b)=>a+b);//使用箭头函数来保持代码简洁
//reduce把两两数字累计相加
console.log(result);
相关代码:
const obj={
count:0,
start:function(){
setInterval(()=>{
this.count++
console.log(this.count);//this指向obj,因为使用箭头函数,this指向取决于位置
},1000)
},
regEvent:function(){
window.οnclick=()=>{
console.log(this.count);//同上
}
},
print:()=>{
console.log(this.count);//window,因为this没有放在函数里面,它就是指向window的
console.log(this);//window
}
//对于print他的表达形式是print:this?,在该位置this指向window
}
//如果没有使用箭头函数,立即执行函数相当于直接用函数调用this,指向全局对象;
//对于regEvent,由于用onclick函数,this指向事件源
//使用箭头函数,则与如何调用无关,而是与其所在位置有关联
// obj.start();
// obj.regEvent();
obj.print();
对于上面的print导致的问题我们一般不解决,因为开发过程中不会这样来使用this
// const isodd=function(num){
// return num%2!==0;//以前的做法
// }
// const isodd=(num)=>{
// return num%2!==0;//箭头函数用法
// }
const isodd=num=>num%2!==0;//箭头函数只有一条返回语句
console.log(isodd(3));
console.log(isodd(8));
-----------------------------------------------
const sum=(a,b)=>({
a:a,
b:b,
sum:a+b,
})
console.log(sum(3,4));
新增的对象字面量语法
1.成员速写
如果对象字面量初始化时,成员的名称来自于一个变量,并且和变量的名称相同,则可以进行简写
function create(loginId, loginPwd, nickName) {
return {
loginId,
loginPwd,
nickName,
}
}
console.log(create("abc", "123", "aaa"));
2.方法速写
对象字面量初始化时,方法可以省略冒号和function关键字
const obj={sub(){console.log(1)};
obj.sub()
3.计算属性名
有的时候,初始化对象时,某些属性名可能来自于某个表达式的值,在es6,可以使用中括号来表示该属性名是通过计算得到的
const prop1 = "name";
const prop2 = "age";
const prop3 = "sayHello";
const user = {
[prop1]: "文强",//字面量书写
[prop2]: 100,
[prop3]() {
console.log(this[prop1], this[prop2])
}
}
console.log(user[prop1]);//原本的调用对象方法
user[prop3]();
console.log(user)
object(函数)的新增API
1.object.is
用于判断两个数据是否相等,基本上跟严格相等(===)是一致的,除了以下两点:
1)NaN和NaN是相等的
2)+0和-0是不相等的(由于+0和-0的二进制不一样)
console.log(NaN===NaN);//false
console.log(+0===-0);//true
//上面是历史遗留问题
只有上面两种情况需要用到object.is才用
2.object.assign
用于混合对象(现在不常用,直接用es7的展开运算符,后面展开的覆盖前面的)
3.object.getOwnPropertyNames的枚举顺序
这个方法之前就存在,只不过官方没有明确要求对属性的顺序怎么排序,如何排序完全由浏览器决定
es6规定了该方法返回的数组排序方式如下:
先排数字(按升序排序),再排其他(按照书写顺序排序)
4.object.setPrototypeof
该函数用于设置某个对象的隐式原型
比如object.setPrototypeof(obj1,obj2)
相当于:obj1.__proto__=obj2
面向对象简介:
面向过程的切入点是功能的步骤
面向对象的切入点是对象的划分
(典例:冰箱装大象)
面向过程主要是就过程来一步步进行编程,面向对象则是创建对象方法来进行编程
前者适用于小型功能的实现,后者适合于大型项目的实现,可维护性和可扩展性高
类:构造函数的语法糖
传统的构造函数的问题
1.属性和原型方法定义分离,降低了可读性
起初js没有考虑用面向对象来进行编程,导致属性和方法定义之间可能有很多的代码,降低了可读性
2.原型成员可以被枚举(会枚举到对象的原型链上的属性,我们是不希望出现这种情况的)
3.默认情况下,构造函数仍然可以被当作普通函数使用(需要使用new.target来判断是否使用new来调用)
//面向对象中,将下面对对象的所有成员的定义,统称为类
//构造函数
function Animal(type,name,age,sex){
this.type=type;
this.name=name;
this.age=age;
this.sex=sex;
}
//实义实例方法(原型方法)
Animal.prototype.Print=function(){
console.log(`${this.type}`);
console.log(`${this.name}`);
console.log(`${this.age}`);
console.log(`${this.sex}`);
}
const a=new Animal("dog","xl","21","male");
a.Print();
es6的类的语法:
class Animal {
constructor(type, name, age, sex) {
this.type = type;
this.name = name;
this.age = age;
this.sex = sex;
}
//print函数被设置为原型链上的函数,不会被枚举
print() {
console.log(`${this.type}`);
console.log(`${this.name}`);
console.log(`${this.age}`);
console.log(`${this.sex}`);
}
}
const a = new Animal("dog", "xl", "21", "male");
a.print();
console.log(a);
es6类的特点
1.类声明不会被提升,与let和const一样,存在暂时性死区(在window里面找不到)
2.类中的所有代码均在严格模式下执行
3.类的所有方法都是不可枚举的
4.类的所有方法都无法被当作构造函数使用
5.类的构造器必须使用new来调用
类的其他书写方式
1.可计算的成员名
直接使用[变量名]来命名类名,就可以在不知道变量的值的情况下调用成员函数
2.getter和setter
Object.defineProperty可定义某个对象成员属性的读取和设置
getter和setter就是在类中定义函数,来对成员属性进行读取和设置,设置时往往收集参数,读取时往往返回参数
3.静态成员
构造函数本身的成员
使用static关键字定义的成员(属性或者方法都可以使用这种写法)
4.字段初始化器(ES7)
注意:
1).使用static的字段初始化器,添加的是静态成员
2)没有使用static的字段初始化器,添加的成员位于对象上
3)箭头函数在字段初始化器位置上,指向当前对象,如果使用箭头函数会额外占用内存空间,应适当使用
5.类表达式
const A = class {//匿名类
a = 1;
b = 2;
}
const a = new A();
console.log(a);
类的继承
如果有两个类A和B,如果可以描述为:B是A,则A和B形成继承关系
如果B是A,则:
1.B继承自A
2.A派生B
3.B是A的子类
4.A是B的父类
继承关系表示,B将拥有A的所有实例成员
对于继承关系来说,B的原型链必须指向A的原型链,否则不算继承关系
旧版的继承关系代码:
function Animal(type, name, age, sex) {
this.type = type;
this.name = name;
this.age = age;
this.sex = sex;
}
Animal.prototype.Print = function () {
console.log(`${this.type}`);
console.log(`${this.name}`);
console.log(`${this.age}`);
console.log(`${this.sex}`);
}
function Dog(name, age, sex) {
//借用父类的构造函数
Animal.call(this, "犬类", name, age, sex);
}
//修改dog的原型链到animal上,如果缺少这条代码,则dog原型链指向object,无法形成继承
Object.setPrototypeOf(Dog.prototype, Animal.prototype);
const d = new Dog("xl", 18, "male");
d.Print();
console.log(d);
新的关键字:
extends:继承,用于类的定义
super:直接当作函数调用,表示父类构造函数
直接当作对象调用,表示父类的原型
es6的继承写法:
class Animal {
constructor(type, name, age, sex) {
this.type = type;
this.name = name;
this.age = age;
this.sex = sex;
}
//print函数被设置为原型链上的函数,不会被枚举
print() {
console.log(`${this.type}`);
console.log(`${this.name}`);
console.log(`${this.age}`);
console.log(`${this.sex}`);
}
}
class dog extends Animal {
constructor(name, age, sex) {
super("犬类", name, age, sex);
}
}
const d = new dog("xl", 18, "male");
d.print();
console.log(d);
注意:ES6要求,如果定义了constructor,并且该类是子类,则必须在constructor的第一行手动调用父类的构造函数
undefined如果子类不写constructor,则会有默认的构造器,该构造器需要的参数和父类一致,并且自动调用父类构造器,如果父类的参数大于传输的参数,则没有传输的属性默认为undefined
子类单独的属性可以另外调用,调用出来时会自动填充父类没有的属性
如果在子类当中除父类所有的属性之外还有自己特有的属性,可以用super.父类的属性名()来复制父类函数中原有的,再添加自己要添加的
【冷知识】
-用js制作抽象类
-抽象类:一般是父类,不能通过new父类来进行创建对象
-正常情况下,this的指向始终指向具体的类的对象,即this指向新创建的对象
对象解构:
解构不会影响原始对象
什么是解构
使用es6的一种语法规则,将一个对象或数组的某个属性提取到某个变量中
先定义变量,然后从对象中读取同名属性,放到变量中,如果定义的变量没有同名属性则是undefined,语法:
let{name,age,sex,address}=user;
console.log(name,age,sex,address)
在解构中使用默认值(找不到的同名属性可以赋值为默认值)
{同名变量=默认值}
非同名属性解构
//先定义4个变量:name,age,gender,address
//再从对象user中读取同名属性赋值(其中gender读取的是sex属性)
let {name,age,sex:gender,address}=user
console.log(name,age,gender,address)
进一步解构:
//定义两个变量,name和province,province是对address的进一步解构,找不到address变量
const{name,address:{province}}=user;
console.log(name,province);
数组解构
const number=["a","b","c","d"];
const[n1,n2]=number//数组解构
console.log(n1,n2)//a,b
const [n1,,,n4]=number
console.log(n1,n4)//a,d
const numbers=["a","b","c","d",{
a:1,
b:2
}];
const[,,,,{a:A}]=numbers;//const{a:A} =numbers[4]效果相同
console.log(A)//1
用展开运算符解构剩余项
//解构出name,然后其他属性,放到一个新的对象当中,变量名为obj
const{name,...obj}=user
解构还能用于交换数值
let a=1,b=2;
[b,a]=[a,b];
参数解构
function ajax({
method="get",
url="/abc"
}={}){
console.log(method,url)
}
ajax()//没有传参数时,在解构时要添加一对大括号
function print({name,age,sex,address:{
province,city
}}){
console.log(`${name}`);
console.log(`${age}`);
console.log(`${sex}`);
console.log(`${address}`);
console.log(`${province}`);
console.log(`${city}`);
}
const user={
name:"kevin",
age:11,
sex:"男",
address:{
province:"四川",
city:"成都",
}
}
普通符号
符号是es6新增的一个数据类型,它通过调用函数 Symbol(符号名)来创建
符号设计的初衷,是为了给对象设置私有属性
私有属性:只有在对象内部使用,外面无法使用
符号的特点:
没有字面量
符号的typeof为symbol
每次调用symbol函数得到的符号永远不相等,无论符号名是否相同,符号都是独一无二的
符号可以作为对象的属性名存在,这种属性称之为符号属性,可以使得该属性在外部无法被访问
符号属性是不能呗枚举的,因此在for in循环中无法读取到符号属性,object.keys也无法读取到
es6新增的object.getownpropertynames可以读取到符号
符号是不能隐式转换的,但是它可以显式的转换为字符串,通过string构造函数进行转换即可,console.log之所以可以输出符号,是它在显式转化为字符串
共享符号
根据某个符号名称(符号描述)能够得到同一个符号
如果代码在同一个文件即可直接使用符号来构建函数
Symbol.for(“符号名/符号描述”)//获取共享符号
知名(公共、具名)符号
在某些场景下可以参与js内部的实现
A[Symbol.hasInstance](obj)//Function.prototype[Symbol.hasInstance]
异步处理
事件循环
js运行的环境称为宿主环境
执行上下文的定义:
当代码运行时,会产生一个对应的执行环境,在这个环境中,所有变量会被事先提出来(变量提升),有的直接赋值,有的为默认值 undefined,代码从上往下开始执行,就叫做执行上下文。
运行环境由下面三种不同的代码类型确定:
全局代码(Global Code):代码首次执行时候的默认环境
函数代码(Function Code):每当执行流程进入到一个函数体内部的时候
Eval代码(Eval Code):当eval函数内部的文本执行的时候
执行栈:call stack,一个数据结构,用于存放各种函数的执行环境,每一个函数执行之前,它的相关信息会加入到执行栈。函数调用之前,创建执行环境,然后加入到执行栈;函数调用之后,销毁执行环境
JS引擎永远执行的是执行栈的最顶部。
异步函数:某些函数不会立即执行,需要等到某个时机到达后才会执行,这样的函数称之为异步函数。比如事件处理函数。异步函数的执行时机,会被宿主环境控制。
浏览器宿主环境中包含5个线程:
- JS引擎:负责执行执行栈的最顶部代码
- GUI线程:负责渲染页面
- 事件监听线程:负责监听各种事件
- 计时线程:负责计时
- 网络线程:负责网络通信
当上面的线程发生了某些事请,如果该线程发现,这件事情有处理程序,它会将该处理程序加入一个叫做事件队列的内存。当JS引擎发现,执行栈中已经没有了任何内容后,会将事件队列中的第一个函数加入到执行栈中执行。
JS引擎对事件队列的取出执行方式,以及与宿主环境的配合,称之为事件循环。
事件队列在不同的宿主环境中有所差异,大部分宿主环境会将事件队列进行细分。在浏览器中,事件队列分为两种:
- 宏任务(队列):macroTask,计时器结束的回调、事件回调、http回调等等绝大部分异步函数进入宏队列
- 微任务(队列):MutationObserver,Promise产生的回调进入微队列
MutationObserver用于监听某个DOM对象的变化
当执行栈清空时,JS引擎首先会将微任务中的所有任务依次执行结束,如果没有微任务,则执行宏任务。
promise(解决回调地狱)
promise规范:
所有的异步场景,都可以看作是一个异步任务,每个异步任务,在JS中应该表现为一个对象,该对象称之为Promise对象,也叫做任务对象
每个任务对象,都应该有两个阶段、三个状态
- 根据常理,它们之间存在以下逻辑:
- 任务总是从未决阶段变到已决阶段,无法逆行
- 任务总是从挂起状态变到完成或失败状态,无法逆行
- 时间不能倒流,历史不可改写,任务一旦完成或失败,状态就固定下来,永远无法改变
挂起->完成,称之为resolve;挂起->失败称之为reject。任务完成时,可能有一个相关数据;任务失败时,可能有一个失败原因。
可以针对任务进行后续处理,针对完成状态的后续处理称之为onFulfilled,针对失败的后续处理称之为onRejected
promise的链式调用
catch方法
.catch(onRejected) = .then(null, onRejected)new Promise((resolve, reject) => {reject(new Error('abc'));}).catch((err) => {console.log('失败了!!', err);});
链式调用
- then方法必定会返回一个新的Promise可理解为后续处理也是一个任务
const pro1 = new Promise((resolve, reject) => {
console.log('学习');
resolve();
});
const pro2=pro1.then(()=>{
console.log("考试");
})
- 新任务的状态取决于后续处理:
- 若没有相关的后续处理,新任务的状态和前任务一致,数据为前任务的数据
- 若有后续处理但还未执行,新任务挂起。
- 若后续处理执行了,则根据后续处理的情况确定新任务的状态
- 后续处理执行无错,新任务的状态为完成,数据为后续处理的返回值
- 后续处理执行有错,新任务的状态为失败,数据为异常对象
- 后续执行后返回的是一个任务对象,新任务的状态和数据与该任务对象一致
const pro1 = new Promise((resolve, reject) => {
console.log('学习');
resolve();
});
const pro2 = pro1.then(() => {
return new Promise((resolve, reject) => {});
});
setTimeout(() => {
console.log(pro2);
}, 1000);
这个例子模拟最后一个特点,返回promise对象,并且新任务的对象没有执行,所以promise2是挂起对象
由于链式任务的存在,异步代码拥有了更强的表达力
练习题见vscode8-2Promise的链式调用
// 常见任务处理代码
/*
* 任务成功后,执行处理1,失败则执行处理2
*/
pro.then(处理1).catch(处理2)//因为成功之后进行处理一,而没有对失败进行处理,如果失败直接转到处理
//2
/*
* 任务成功后,依次执行处理1、处理2
*/
pro.then(处理1).then(处理2)
/*
* 任务成功后,依次执行处理1、处理2,若任务失败或前面的处理有错,执行处理3
*/
pro.then(处理1).then(处理2).catch(处理3)
# Promise的静态方法
| 方法名 | 含义 |
| ---------------------------- | ------------------------------------------------------------ |
| Promise.resolve(data) | 直接返回一个完成状态的任务 |
| Promise.reject(reason) | 直接返回一个拒绝状态的任务 |
| Promise.all(任务数组) | 返回一个任务<br/>任务数组全部成功则成功<br/>任何一个失败则失败,有一个挂起就是挂起 |