js篇

请配合 简书侧边栏文章导航 食用,因为很长

● JS-变量类型和计算

▶ 1. 值类型有哪些6?

值类型

UndefinedNullBooleanNumberStringSymbol (new in ES 6)

▶ 2. 引用类型有哪些9?

引用类型

Object 类型Array 类型Date 类型RegExp 类型Function 类型 SetMapweakSetweakMap

▶ 3. 手写深拷贝

 function deepClone(target,map = new Map() ){
            
    if(typeof target !=='object' || target ==null ){
    
    //不是引用类型直接返回
    return target
    
    }
    let cloneTarget = Array.isArray(target) ?[]:{}
    //解决循环引用,利用map开辟一个新的存储空间, 存储当前对象和拷贝对象之间的关系
    if(map.get(target)){
         return target
     }
     map.set(target,cloneTarget)
     
     for(let key in target){
         cloneTarget[key] = deepClone(target[key],map )
     }
     return cloneTarget
 
 }

▶ 4. ==和=== 运算符

//除了== null,之外 全部都用===
 
const obj = {x:100}
//if(obj.a===null || obj.a === undefined ){}
if(obj.a==null){
    
}
    
    

▶ 5. if语句和逻辑运算

truly 变量 !!a ===false

falsely 变量 !!a ===false

!!0 === false
!!NaN===false
!!''===false
!!null===false
!!undefined===false
!!false===false

▶ 6. typeof 能判断哪些类型?

  1. 识别所有值类型, null 除外
  2. 识别函数
  3. 判断是否是引用类型(不可再细分) object array 都是object

● js-原型和原型链

es5时代的继承

一篇文章理解js继承

JS中的继承(上)

JS中的继承(下)

apply、call 的区别和用途

知乎

call、apply、bind使用和区别

手写call apply bind

call apply bind

**核心:改变函数运行时的this指向**
  • call、apply与bind的差别

    call和apply改变了函数的this上下文后便执行该函数,而bind则是返回改变了上下文后的一个函数。

  • call、apply的区别

    他们俩之间的差别在于参数的区别,call和apply的第一个参数都是要改变上下文的对象,而call从第二个参数开始以参数列表的形式展现,apply则是把除了改变上下文对象的参数放在一个数组里面作为它的第二个参数。

    let arr1 = [1, 2, 19, 6];
    //例子:求数组中的最值
    console.log(Math.max.call(null, 1,2,19,6)); // 19
    console.log(Math.max.call(null, arr1)); // NaN
    console.log(Math.max.apply(null, arr1)); //  19 直接可以用arr1传递进去
    
    function fn() {
        console.log(this);
    }
    // apply方法结果同下
    fn.call(); // 普通模式下this是window,在严格模式下this是undefined
    fn.call(null); // 普通模式下this是window,在严格模式下this是null
    fn.call(undefined); // 普通模式下this是window,在严格模式下this是undefined
    

__proto__

  • 是js中 只有对象才有的属性 称作 隐式原型

  • 指向的是它的构造函数的prototype

  • 这个属性是浏览器给提供的

prototype

  • 是js中 只有函数才有的 的特殊属性 叫做 原型 也称作 显式原型

  • 这个属性是 函数fn的一个指针,指向一个对象,这个对象的用途就是包含所有实例共享的属性和方法(原型对象

  • 原型对象也有一个属性,叫做constructor,这个属性包含了一个指针,指回原构造函数。

  • 这个属性 JavaScript 标准提供的

对象 没有 prototype

函数也是对象 所以也有 __proto__

原型链继承

核心:将父类的实例作为子类的原型

优点:父类方法可以复用
缺点:

  • 父类的引用属性会被所有子类实例共享
  • 子类构建实例时不能向父类传递参数
//核心
SubClass.prototype = new SuperClass() 
// 所有涉及到原型链继承的继承方式都要修改子类构造函数的指向,否则子类实例的构造函数会指向SuperType。
SubClass.prototype.constructor = SubClass;


//缺点1:父类的引用属性会被所有子类实例共享
function SuperType(){
    this.colors = ['red','black']
}
function SubClass () {
}
// 将父类的实例作为子类的原型对象
SubClass.prototype = new SuperClass()
SubClass.prototype.constructor = SubClass;
o1 = new SubClass()
o2 = new SubClass()
o1.splice(1, 1, 'yellow');
console.log(o1.colors) // ['red', 'yellow']
console.log(o2.colors) // ['red', 'yellow']


//缺点2:子类构建实例时不能向父类传递参数
function SuperClass (color) {
  this.color=color
}
function SubClass () {
}
SubClass.prototype = new SuperClass(['red'])
SubClass.prototype.constructor = SubClass;
var o1 = new SubClass()
var o2 = new SubClass()
// 打印o1和o2的color
console.log(o1.color) // ['red']
console.log(o2.__proto__.color)// ['red']

构造函数继承

核心:将父类构造函数的内容复制给了子类的构造函数。这是所有继承中唯一一个不涉及到prototype的继承。

优点:和原型链继承完全反过来。

  • 父类的引用属性不会被共享
  • 子类构建实例时可以向父类传递参数

缺点:父类的方法不能复用,子类实例的方法每次都是单独创建的。

//核心
SuperClass.apply(this,arguments);

function SuperClass (color) {
  this.color=color;
  this.say = function () {
    alert('hello');
  }
}

function SubClass () {
  SuperClass.apply(this, arguments)
}

var o1 = new SubClass(['red'])
var o2 = new SubClass(['yellow'])

console.log(o1.color) // ['red']
console.log(o2.color) // ['yellow']

//缺点:父类的方法不能复用,子类实例的方法每次都是单独创建的。
console.log(o1.say === o2.say)//false

组合继承

核心:原型式继承和构造函数继承的组合,兼具了二者的优点。

优点:

  • 父类的方法可以被复用
  • 父类的引用属性不会被共享
  • 子类构建实例时可以向父类传递参数

缺点:
调用了两次父类的构造函数,第一次给子类的原型添加了父类的name, arr属性,第二次又给子类的构造函数添加了父类的name, arr属性,从而覆盖了子类原型中的同名参数。这种被覆盖的情况造成了性能上的浪费。


function SuperClass (color) {
  this.color=color;

}
//利用原型链继承要共享的属性
SuperClass.prototype.say = function () {
   console.log(this.color);
 }

function SubClass () {
    //利用构造函数继承要独享的属性
  SuperClass.apply(this, arguments) // 第二次调用SuperClass
}
 SubClass.prototype = new SuperClass() // 第一次调用SuperClass
 SubClass.prototype.constructor = SubClass;

var o1 = new SubClass(['red'])
var o2 = new SubClass(['yellow'])
console.log(o1.color) // ['red']
console.log(o2.color) // ['yellow']
o1.say() // ['red']
o2.say() // ['yellow']

原型式继承

通过修改原型链结构,使得实例能够使用原型链上的所有方法

核心:原型式继承的object方法本质上是对参数对象的一个浅复制。
优点:父类方法可以复用
缺点:

  • 父类的引用属性会被所有子类实例共享
  • 子类构建实例时不能向父类传递参数

es5的Object.create()函数,就是基于原型式继承的

原型式继承是道格拉斯-克罗克福德 2006 年在 Prototypal Inheritance in JavaScript一文中提出的

function clone(o){
  function F(){}
  F.prototype = o;
  return new F();
}

var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};
var o1 = clone(person);
o1.name = "Greg";
o1.friends.push("Rob");

var o2 = clone(person);
o2.name = "Linda";
o2.friends.push("Barbie");
//缺点 父类的引用属性会被所有子类实例共享
console.log(person.friends);//"Shelby,Court,Van,Rob,Barbie"


寄生式继承

核心:使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力。
优缺点:仅提供一种思路,没什么优点

function cloneAndStrengthen(proto){
    function F () {}
    F.prototype = proto
    let f = new F()
    f.say = function() { 
        console.log('I am a person')
    }
    return f
}

寄生组合继承

核心:因为是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费

优点:

  • 解决组合继承有一个会两次调用父类的构造函数造成浪费的缺点

  • 避免了在 SubClass.prototype 上面创建不需要的、多余的属性。

  • 原型链还能保持不变;能够正常使用 instanceofisPrototypeOf()


//原型式继承
function clone(o) {
    function F() {}
    F.prototype = o
    return new F()
}

//寄生式继承
// 2. 通过原型式继承和寄生式继承方法,实现方法共享
//接收子类的构造函数,和超类的构造函数
function base(sub, supers) {
    //创建一个超类显式原型的副本,浅拷贝
    let prototype = clone(supers.prototype)
    //添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性。
    prototype.constructor = sub
    //将新创建的对象(副本)赋值给子类型的原型。
    sub.prototype = prototype
}

function SuperClass(color){
    this.color = color
}
SuperClass.prototype.say=function(){
    console.log(this.color)
}

function SubClass(){
    //1. 通过借用构造函数来继承属性,解决引用类型共享的问题
    SuperClass.apply(this, arguments);
}

  base(SubClass,SuperClass)

let o1 = new SubClass(['green']);
console.log(o1)

左侧是组合继承, 右侧是寄生组合继承


ES6 Class extends

核心: ES6继承的结果和寄生组合继承相似,本质上,ES6继承是一种语法糖。但是,寄生组合继承是先创建子类实例this对象,然后再对其增强;而ES6先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

class A {}

class B extends A {
  constructor() {
    super();
  }
}

ES6实现继承的具体原理:

class A {
}

class B {
}

Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);

// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);

ES6继承与ES5继承的异同:
相同点:本质上ES6继承是ES5继承的语法糖
不同点:

  • ES6继承中子类的构造函数的原型链指向父类的构造函数,ES5中使用的是构造函数复制,没有原型链指向。
  • ES6子类实例的构建,基于父类实例,ES5中不是。

总结

  • ES6 Class extends是ES5继承的语法糖
  • JS的继承除了构造函数继承之外都基于原型链构建的
  • 可以用寄生组合继承实现ES6 Class extends,但是还是会有细微的差别

es6

    //class 实际上是函数,可见是语法糖
 typeof People // fuction
 typeof Student //function
 //隐式原型
console.log(xiaoluo.__proto__)
//显式原型
console.log(xiaoluo.prototype)
//true 引用同一个内存地址
console.log(xiaoluo.__proto__===Student.prototype)
每个class 都有显示原型prototype
每个实例都有隐式原型__proto__
实例的__proto__指向对应class的prototype

▶ 1. 如何准确判断一个变量是不是数组?

  let  a = {}
  a instanceof Array

▶ 2. class的原型本质(原型链)

prototype 指向一块内存,这个内存里面有共用属性

proto 指向同一块内存

prototype 和 __proto__ 的不同点在于

prototype 是构造函数的属性,而 proto 是对象的属性

难点在于……构造函数也是对象!

如果没有 prototype,那么共用属性就没有立足之地

**如果没有 __proto__ ,那么一个对象就不知道自己的共用属性有哪些

▶ 3. 手写 call apply bind


Function.prototype.myCall = function(context = window, ...args) {
  context = context || window; // 参数默认值并不会排除null,所以重新赋值
  context.fn = this; // this是调用call的函数
  const result = context.fn(...args);
  delete context.fn; // 执行后删除新增属性
  return result;
}

Function.prototype.myApply = function(context = window, args = []) {
  context = context || window; // 参数默认值并不会排除null,所以重新赋值
  context.fn = this; // this是调用call的函数
  const result = context.fn(...args);
  delete context.fn;
  return result;
}

Function.prototype.myBind = function(context, ...args) {
  const _this = this;
  return function Bind(...newArgs) {
    // 考虑是否此函数被继承
    if (this instanceof Bind) {
      return _this.myApply(this, [...args, ...newArgs])
    }
    return _this.myApply(context, [...args, ...newArgs])
  }
}

● js-作用域和闭包

作用域

我用了两个月的时间才理解 let

深入理解JavaScript作用域和作用域链

核心:作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性

  • 作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是【隔离变量】,不同作用域下同名变量不会有冲突。

  • 全局作用域 函数作用域 es6块级作用域

全局作用域

  • 最外层函数 和在最外层函数外面定义的变量拥有全局作用域

    var outVariable = "我是最外层变量"; //最外层变量
    function outFun() { //最外层函数
        var inVariable = "内层变量";
     }
    console.log(outVariable); //我是最外层变量
    outFun(); //内层变量
    console.log(inVariable); //inVariable is not defined
    
    
  • 所有末定义直接赋值的变量自动声明为拥有全局作用域

    function outFun2() {
        variable = "未定义直接赋值的变量";
        var inVariable2 = "内层变量2";
    }
    outFun2();//要先执行这个函数,否则根本不知道里面是啥
    console.log(variable); //未定义直接赋值的变量
    console.log(inVariable2); //inVariable2 is not defined
    
  • 所有 window 对象的属性拥有全局作用域

    • 例如 window.name、window.location、window.top 等等。

函数作用域

  • 是指声明在函数内部的变量,和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,

  • 作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行。

function doSomething(){
    var blogName="浪里行舟";
    function innerSay(){
        alert(blogName);
    }
    innerSay();
}
alert(blogName); //脚本错误
innerSay(); //脚本错误

值得注意的是:块语句(大括号“{}”中间的语句),如 if 和 switch 条件语句或 for 和 while 循环语句,不像函数,它们不会创建一个新的作用域。在块语句中定义的变量将保留在它们已经存在的作用域中。

if (true) {
    // 'if' 条件语句块不会创建一个新的作用域
    var name = 'Hammad'; // name 依然在全局作用域中
}
console.log(name); // logs 'Hammad'

es6块级作用域

块级作用域可通过新增命令 let 和 const 声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:

  1. 在一个函数内部
  2. 在一个代码块(由一对花括号包裹)内部

let 声明的语法与 var 的语法一致。你基本上可以用 let 来代替 var 进行变量声明,但会将变量的作用域限制在当前代码块中。块级作用域有以下几个特点:

  • 声明变量不会提升到代码块顶部

let/const 声明并不会被提升到当前代码块的顶部,因此你需要手动将 let/const 声明放置到顶部,以便让变量在整个代码块内部可用。

function getValue(condition) {
if (condition) {
let value = "blue";
return value;
} else {
// value 在此处不可用
return null;
}
// value 在此处不可用
}
  • 禁止重复声明

如果一个标识符已经在代码块内部被定义,那么在此代码块内使用同一个标识符进行 let 声明就会导致抛出错误。例如:

var count = 30;
let count = 40; // Uncaught SyntaxError: Identifier 'count' has already been declared

闭包

JS 中的闭包是什么?

 **核心: 「函数」和「函数内部能访问到的变量」(也叫环境)的总和,就是一个闭包。**
  • 闭包内的自由变量的查找,是在函数定义的地方,向上级作用域查找,不是在执行的地方

    作用:用来隐藏变量,私有变量。不允许直接访问此变量,所以需要暴露一个访问器【函数】,可以让访问者【间接访问】

为什么要嵌套函数?

因为使用闭包的目的是为了创造局部变量,所以变量必须放在函数中,否则就是全局的变量,无法实现闭包目的-隐藏变量

所以函数套函数只是为了造出一个局部变量,跟闭包无关。

为什么要return 函数呢?

因为如果不return ,就没有办法使用这个闭包。return xxx 改成window.xxx =xxx 是一样的,目的就是为了让外面可以访问到这个xxx函数,所以return xxx 只是为了使用 xxx, 也跟闭包无关。

this

this 的值到底是什么?一次说清楚

核心:this的值是在函数执行时确认的,不是在函数定义时确认的
  • func.call(context, p1, p2) 才是正常的调用形式,其他的都是语法糖

  • this 就是你 call 一个函数时,传入的第一个参数。由于你从来不用 call 形式的函数调用,所以你一直不知道。

function func(){
  console.log(this)
}

func()
//转换为
func.call(undefined) // 可以简写为 func.call()

//按理说打印出来的 this 应该就是 undefined 了吧,但是浏览器里有一条规则:

//如果你传的 context 是 null 或 undefined,那么 window 对象就是默认的 context(严格模式下默认 context 是 undefined)
//因此上面的打印结果是 window。

//如果你希望这里的 this 不是 window,很简单:

func.call(obj) // 那么里面的 this 就是 obj 对象了

箭头函数

实际上箭头函数里并没有 this,如果你在箭头函数里看到 this,直接把它当作箭头函数外面的 this 即可。外面的 this 是什么,箭头函数里面的 this 就还是什么,因为箭头函数本身不支持 this。

有人说「箭头函数里面的 this 指向箭头函数外面的 this」,这很傻,因为箭头函数内外 this 就是同一个东西,并不存在什么指向不指向。

立即执行函数

什么是立即执行函数?有什么作用?

作用:创建一个独立的作用域。这个作用域里面的变量,外面访问不到(即避免「变量污染」)。

为什么还要用另一对括号把匿名函数包起来呢?

其实是为了兼容 JS 的语法。

如果我们不加另一对括号,直接写成

function(){alert('我是匿名函数')}()

浏览器会报语法错误。想要通过浏览器的语法检查,必须加点小东西,比如下面几种

(function(){alert('我是匿名函数')} ()) // 用括号把整个表达式包起来
(function(){alert('我是匿名函数')}) () //用括号把函数包起来
!function(){alert('我是匿名函数')}() // 求反,我们不在意值是多少,只想通过语法检查。
+function(){alert('我是匿名函数')}()
-function(){alert('我是匿名函数')}()
~function(){alert('我是匿名函数')}()
void function(){alert('我是匿名函数')}()
new function(){alert('我是匿名函数')}()

▶ 1. this的不同应用场景,如何取值?

▶ 3. 实际开发中闭包的应用场景,举例说明?

(function(){
    var data = {}
    return {
        set(key,value){
           data[key] = value
        },
        get(key){
           return  data[key]
        }
    }
})()
const cache =  (function(){
    var data = {}
    return {
        set(key,value){
            data[key] = value
        },
        get(key){
            return  data[key]
        }
    }
})()

cache.set('sex',1)
console.log(cache.get('sex'))

● js-异步

event loop 事件循环/事件轮询

js 如何执行?

  • 从前到后,一行一行执行
  • 如果某一行执行报错,则停止下面代码的执行
  • 先把他同步代码执行完,再执行异步

event loop 过程

  1. 同步代码,一行一行放在 call stack(调用栈) 执行
  2. 遇到异步,会先“记录”下(web apis),等待时机(定时、网络请求等)
  3. 时机到了,就移动到 callback queue(回调 队列)
  4. 如call stack 为空 (即同步代码执行完毕) event loop 开始工作
  5. 轮询查找 callback queue, 如有则移动到 call stack 执行
  6. 然后继续轮询查找(永动机一样)

dom 事件也是使用回调,也是基于event loop

Promise

三种状态

pending resolved rejected

状态的表现和变化

pending ->resolved 或 peding-> rejected

变化不可逆

pending 状态, 不会触发then 和catch

resolved状态,会触发后续的then回调函数

rejected状态,会触发后续的catch回调函数

then 和catch 对状态的影响

then 正常返回 resolved,里面有报错则返回 rejected

catch 正常返回 resolved,里面有报错则返回 rejected

先回顾一下 Promise 的基本使用

// 加载图片
function loadImg(src) {
    const p = new Promise(
        (resolve, reject) => {
            const img = document.createElement('img')
            img.onload = () => {
                resolve(img)
            }
            img.onerror = () => {
                const err = new Error(`图片加载失败 ${src}`)
                reject(err)
            }
            img.src = src
        }
    )
    return p
}
const url = 'https://xxx.jpg'
loadImg(url).then(img => {
    console.log(img.width)
    return img
}).then(img => {
    console.log(img.height)
}).catch(ex => console.error(ex))

三种状态

三种状态 pending resolved rejected

(画图表示转换关系,以及转换不可逆)

// 刚定义时,状态默认为 pending
const p1 = new Promise((resolve, reject) => {

})

// 执行 resolve() 后,状态变成 resolved
const p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve()
    })
})

// 执行 reject() 后,状态变成 rejected
const p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject()
    })
})

// 直接返回一个 resolved 状态
Promise.resolve(100)
// 直接返回一个 rejected 状态
Promise.reject('some error')

状态和 then catch

状态变化会触发 then catch —— 这些比较好理解,就不再代码演示了

  • pending 不会触发任何 then catch 回调
  • 状态变为 resolved 会触发后续的 then 回调
  • 状态变为 rejected 会触发后续的 catch 回调

then catch 会继续返回 Promise ,此时可能会发生状态变化!!!

// then() 一般正常返回 resolved 状态的 promise
Promise.resolve().then(() => {
    return 100
})

// then() 里抛出错误,会返回 rejected 状态的 promise
Promise.resolve().then(() => {
    throw new Error('err')
})

// catch() 不抛出错误,会返回 resolved 状态的 promise
Promise.reject().catch(() => {
    console.error('catch some error')
})

// catch() 抛出错误,会返回 rejected 状态的 promise
Promise.reject().catch(() => {
    console.error('catch some error')
    throw new Error('err')
})

async/await

核心 :是同步语法,编写异步的代码,彻底消灭回调函数

  • 执行 async函数,返回的是Promise对象
  • await 相当于 Promise的then
  • try ... catch 可捕获异常,代替了Promise的catch
  • async/await 是消灭异步回调的终极武器
  • js还是单线程,还得是有异步,还得是基于event loop
  • async/await 只是一个语法糖,但这颗糖真香

有很多 async 的面试题,例如

  • async 直接返回,是什么
  • async 直接返回 promise
  • await 后面不加 promise
  • 等等,需要找出一个规律

语法介绍

用同步的方式,编写异步。

function loadImg(src) {
    const promise = new Promise((resolve, reject) => {
        const img = document.createElement('img')
        img.onload = () => {
            resolve(img)
        }
        img.onerror = () => {
            reject(new Error(`图片加载失败 ${src}`))
        }
        img.src = src
    })
    return promise
}

async function loadImg1() {
    const src1 = 'http://xxx.png'
    const img1 = await loadImg(src1)
    return img1
}

async function loadImg2() {
    const src2 = 'http://xxx.png'
    const img2 = await loadImg(src2)
    return img2
}

(async function () {
    // 注意:await 必须放在 async 函数中,否则会报错
    try {
        // 加载第一张图片
        const img1 = await loadImg1()
        console.log(img1)
        // 加载第二张图片
        const img2 = await loadImg2()
        console.log(img2)
    } catch (ex) {
        console.error(ex)
    }
})()

和 Promise 的关系

  • async 函数返回结果都是 Promise 对象(如果函数内没返回 Promise ,则自动封装一下)
async function fn2() {
    return new Promise(() => {})
}
console.log( fn2() )

async function fn1() {
    return 100
}
console.log( fn1() ) // 相当于 Promise.resolve(100)
  • await 后面跟 Promise 对象:会阻断后续代码,等待状态变为 resolved ,才获取结果并继续执行
  • await 后续跟非 Promise 对象:会直接返回
(async function () {
    const p1 = new Promise(() => {})
    await p1
    console.log('p1') // 不会执行
})()

(async function () {
    const p2 = Promise.resolve(100)
    const res = await p2
    console.log(res) // 100
})()

(async function () {
    const res = await 100
    console.log(res) // 100
})()

(async function () {
    const p3 = Promise.reject('some err')
    const res = await p3
    console.log(res) // 不会执行
})()
  • try...catch 捕获 rejected 状态
(async function () {
    const p4 = Promise.reject('some err')
    try {
        const res = await p4
        console.log(res)
    } catch (ex) {
        console.error(ex)
    }
})()

总结来看:

  • async 封装 Promise
  • await 处理 Promise 成功
  • try...catch 处理 Promise 失败

异步本质

await 是同步写法,但本质还是异步调用。

async function async1 () {
  console.log('async1 start')
  await async2()
  console.log('async1 end') // 关键在这一步,它相当于放在 callback 中,最后执行
}

async function async2 () {
  console.log('async2')
}

console.log('script start')
async1()
console.log('script end')

即,只要遇到了 await ,后面的代码都相当于放在 callback 里。

for...of

// 定时算乘法
function multi(num) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(num * num)
        }, 1000)
    })
}

// // 使用 forEach ,是 1s 之后打印出所有结果,即 3 个值是一起被计算出来的
// function test1 () {
//     const nums = [1, 2, 3];
//     nums.forEach(async x => {
//         const res = await multi(x);
//         console.log(res);
//     })
// }
// test1();

// 使用 for...of ,可以让计算挨个串行执行
async function test2 () {
    const nums = [1, 2, 3];
    for (let x of nums) {
        // 在 for...of 循环体的内部,遇到 await 会挨个串行计算
        const res = await multi(x)
        console.log(res)
    }
}
test2()

宏任务 macro task 和 微任务 micro task

宏任务: setTimeout,setInterval, Ajax, DOM 事件

微任务: Promise async/await

微任务执行时机比宏任务要早

event loop 和 DOM 渲染

再次回顾 event loop 的过程

  • 每一次 call stack 结束,都会触发 DOM 渲染(不一定非得渲染,就是给一次 DOM 渲染的机会!!!)
  • 然后再进行 event loop
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
            .append($p1)
            .append($p2)
            .append($p3)

console.log('length',  $('#container').children().length )
alert('本次 call stack 结束,DOM 结构已更新,但尚未触发渲染')
// (alert 会阻断 js 执行,也会阻断 DOM 渲染,便于查看效果)
// 到此,即本次 call stack 结束后(同步任务都执行完了),浏览器会自动触发渲染,不用代码干预

// 另外,按照 event loop 触发 DOM 渲染时机,setTimeout 时 alert ,就能看到 DOM 渲染后的结果了
setTimeout(function () {
    alert('setTimeout 是在下一次 Call Stack ,就能看到 DOM 渲染出来的结果了')
})

宏任务和微任务的区别

  • 宏任务:DOM 渲染后再触发
  • 微任务:DOM 渲染前会触发
// 修改 DOM
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
    .append($p1)
    .append($p2)
    .append($p3)

// // 微任务:渲染之前执行(DOM 结构已更新)
// Promise.resolve().then(() => {
//     const length = $('#container').children().length
//     alert(`micro task ${length}`)
// })

// 宏任务:渲染之后执行(DOM 结构已更新)
setTimeout(() => {
    const length = $('#container').children().length
    alert(`macro task ${length}`)
})

再深入思考一下:为何两者会有以上区别,一个在渲染前,一个在渲染后?

  • 微任务:ES 语法标准之内,JS 引擎来统一处理。即,不用浏览器有任何关于,即可一次性处理完,更快更及时。
  • 宏任务:ES 语法没有,JS 引擎不处理,浏览器(或 nodejs)干预处理。

面试题:

▶ 1. 同步和异步的区别?

同步会阻塞代码执行

异步不会阻塞代码执行,

异步基于js是单线程语言

▶ 2. 手写promise 加载一张图片?

function loadImg(src) {
    return new Promise((resolve, reject) => {
        let img = document.createElement('img')
        img.onload = () => {
            resolve(img)
        }
        img.onerror = () => {
            reject(new Error(` 图片加载失败 ${src}`))
        }
        img.src = src
    })
}

loadImg('https://xxx.jpg').then(img => {
    console.log(img.width)
    console.log(img.height)
}).catch(err => console.error(err))

▶ 3. 描述 event loop 机制 可画图?

先回答 event loop的过程

如果 面试官 往深了问 , 则 说明和dom渲染的关系

微任务和宏任务 在 event loop 过程中的不同处理

▶ 4. 什么是宏任务和微任务,两者区别 ?

宏任务: setTimeout,setInterval, Ajax, DOM 事件

微任务: Promise async/await

微任务执行时机比宏任务要早

▶ 5. Promise的三种状态,如何变化?

pending resolved rejected

pending ->resolved 或 peding-> rejected

变化不可逆

▶ 6. promise then 和catch 的连接?

// 第一题
Promise.resolve().then(() => {
    console.log(1)
}).catch(() => {
    console.log(2)
}).then(() => {
    console.log(3)
})
// 1 3

// 第二题
Promise.resolve().then(() => {
    console.log(1)
    throw new Error('erro1')
}).catch(() => {
    console.log(2)
}).then(() => {
    console.log(3)
})
// 1 2 3

// 第三题
Promise.resolve().then(() => {
    console.log(1)
    throw new Error('erro1')
}).catch(() => {
    console.log(2)
}).catch(() => { // 注意这里是 catch
    console.log(3)
})
// 1 2

▶ 7. async/await 语法问题

async function fn() {
    return 100
}
(async function () {
    const a = fn() // ??               // promise
    const b = await fn() // ??         // 100
})()

//

(async function () {
    console.log('start')
    const a = await 100
    console.log('a', a)
    const b = await Promise.resolve(200)
    console.log('b', b)
    const c = await Promise.reject(300)
    console.log('c', c)
    console.log('end')
})() // 执行完毕,打印出那些内容?
start  100  200

▶ 8. async/await 语法问题

console.log(100)
setTimeout(() => {
    console.log(200)
})
Promise.resolve().then(() => {
    console.log(300)
})
console.log(400)
// 100 400 300 200

▶ 8. 执行顺序问题

async function async1 () {
  console.log('async1 start')
  await async2() // 这一句会同步执行,返回 Promise ,其中的 `console.log('async2')` 也会同步执行
  console.log('async1 end') // 上面有 await ,下面就变成了“异步”,类似 cakkback 的功能(微任务)
}

async function async2 () {
  console.log('async2')
}

console.log('script start')

setTimeout(function () { // 异步,宏任务
  console.log('setTimeout')
}, 0)

async1()

new Promise (function (resolve) { // 返回 Promise 之后,即同步执行完成,then 是异步代码
  console.log('promise1') // Promise 的函数体会立刻执行
  resolve()
}).then (function () { // 异步,微任务
  console.log('promise2')
})

console.log('script end')

// 同步代码执行完之后,屡一下现有的异步未执行的,按照顺序
// 1. async1 函数中 await 后面的内容 —— 微任务
// 2. setTimeout —— 宏任务
// 3. then —— 微任务
 script start
 async1 start
 async2
 promise1
 script end
 async1 end
 promise2
 setTimeout

推荐阅读更多精彩内容