JS高频面试题合集(自用,持续更新...)

1,339 阅读28分钟

JS面试题

1.1 JS数据类型

  • 基本数据类型:string number boolean undefined null symbol
  • 引用数据类型:Object

1.2 检测数据类型的方法

  • Object.prototype.toString.call():准确的检测是什么数据类型
  • typeof:能区分基本数据类型,不能区分 array object 这种引用数据类型
  • instanceof: 比如 a instanceof b,准确的说是用来检测 a 是不是 b 的实例对象(查找原型)

1.3 判断数组的方式有哪些

  • Array.isArray(ES6的)
  • Object.prototype.toString.call 最准确的
  • 通过原型链去判断

1.4 null 和 undefined 的理解

  • null 和 undefined 都是基本数据类型
  • null可以理解为空对象
    • (实际项目里面可以定义一个空对象 const a = null),
  • undefined可以理解为 未定义
    • (实际项目里面直接使用一个未定义的变量报错:console.log(a.play()),但是 a 没定义,就会报错
  • typeof null 会得到 object,因为 null 和 object 的二进制开头一样

1.5 instanceof 的实现原理

其实就是去遍历原型链

// a 是实力对象,b是某个函数
const myInstanceof(a, b) {

     let A = a.__proto__ // 隐式原型
     let B = b.prototype // 显式原型
 
     //遍历原型链查找
     while(1) {
        // object.prototype.__proto__ === null
        if (A === Null) {
          return false
        }
        
        if (A === B) {
          return true
        }
        
        A = A.__proto__
     }

}

1.6 == 操作符的强制类型转换规则是什么

    1. 判断是不是相同类型,如果不是,进行类型转换
    1. 转换规则
    • 如果是 nullundefined 比较,return true
    • 如果是 stringnumber 比较,string ----> number 再比较
    • 如果有一方是 boolean,会把 boolean ---> number 再比较
    • 如果一方是 object,一方是 string number symbol,会把 object 转 原始类型"[object Object]"再比较

1.7 JS的包装类型

JS 的基本数据类型是没有属性和方法的,但是为了方便使用,在调用基本数据类型的属性和方法时,JS会对基本数据类型自动做一层包装

const a = 'abc' //string 没有属性和方法

// 但是可以调用属性
// JS的操作是 String('abc')
console.log(a.length) // 3

1.8 this的指向问题

分为5种情况

  • 指向window:函数 a 不是对象身上的方法时, 调用 a(),此时 this 指向 window
// a 是某个全局函数
const a = () => {
  console.log(this);  //指向 window
}

a() // window
  • 指向对象:函数 a 是 A 对象身上的方法时,调用 a(),此时 this 指向对象 A\
const A = {
  a: function() {
    console.log(this);
  }
}

A.a() //指向 A,即 {a: f}
  • call(x, args) apply(x, [args]) bind(x, args或者[args]):this 指向 x
function a() {
    console.log(this.play())
}

const b = {
    play: function() {
        console.log('111')
    }
}

a.call(b) // 111
  • new: const b = new B(),this 指向 b (不多说)
  • 箭头函数的this:指向当前这个箭头函数所处上下文中的那个 this
// 处在全局执行上下文中,this就继承全局执行上下文的 this,即 window
const a = () => {
    console.log(this)
}

a() // window

// 处在某个对象的方法中
const b = {
    play: () => {
        console.log(this)
    }
}
b.play() // 箭头函数处在对象b的环境里,b是处在全局部执行上下文,所以 this 指向 window

1.9 JSON的理解

不说多的,就一个,一种数据交换的格式,实际项目中常见于前后端传参取值

  • JSON.stringify:将传入的某种数据格式转换为一个 JSON 字符串。如果传入的数据结构不符合 JSON 格式,那么在序列化的时候会对这些值进行对应的特殊处理,使其符合规范

  • JSON.parse():这个函数用来将 JSON 格式的字符串转换为一个 js 数据结构。如果传入的字符串不是标准的 JSON 格式,将会抛出错误

1.10 let、const、var的区别

不需要说很多,了解即可

  • var 可以先用再定义,但是 let const 不能
  • let var 可以重新赋值,const 不行
  • let const 是块级作用域,var 是全局作用域

1.11 new 的实现原理

  • 创建一个空对象 obj
  • 空对象的原型指向构造函数的显示原型Object.setPrototypeOf(obj, constructor.prototype)
  • 执行构造函数(拿到结果 result),且构造函数的 this 指向 这个空对象,
  • 判断 result 类型,如果 result 是对象,则返回 result,否则返回 obj
function myNew(constructor, ...args) {
    // 创建空对象
    let obj = {}

    // 空对象的原型指向构造函数的显示原型
    Object.setPrototypeOf(obj, constructor.prototype)

    // 构造函数的 this 指向空对象,执行构造函数
    const result = constructor.apply(obj, args)

    // 如果 result 是对象,则返回 result,否则返回 obj
    return result instanceof Object ? result : obj;
}

1.12 原型与原型链

原型:

  • 每个实例对象都有一个隐士原型 __proto__
  • 每个函数都有一个显示原型 prototype

原型链:

  • 实力对象的隐士原型指向构造函数的显示原型 a.__proto__ === A.prototype
  • 而构造函数的显示原型也是个对象,同理也指向它的原型对象

需要注意的是:

  • 构造函数原型对象的 constructor 指向构造函数本身,也就是:A.prototype.constructor === A

原型链的终点:Object.prototype.proto ---> Null

1.13 作用域与作用域链

作用域分为两类:

  • 全局作用域:在任意区域都能访问到
  • 局部作用域:在固定区域才能访问

作用域链:

  • 其实就是 作用域的集合
function father() {
    let result = 2
    function child () {
        console.log(result)
    }
    child()
}
father()

比如上面的例子中,child 处于局部作用域,它要访问 result 变量,那么它访问的过程是:

  • child里面有没有变量 result,如果没有,向上找
  • father里面有没有变量 result,如果没有,想上找(例子这里找到了,就输出 2)
  • 全局里面找有没有变量 result,如果没有,输出 undefined,如果有,则输出对应值

1.14 执行上下文与执行上下文栈

1. 什么是执行上下文栈

JS的所有代码的执行都存放在一个特殊的地方,这个地方就可以理解为:执行上下文栈

2. 什么是执行上下文的:

可以理解为:某段代码执行时的一个指向

就比如:写作业这一个事情(代码),是我去写作业(我去执行这个代码),那么我就是写作业这一个事情的指向

3. 执行上下文的类别:

  • 全局执行上下文
  • 局部上下文
  • eval执行上下文

不需要去解释上面的概念,直接图解

4. 图解执行上下文

首先我们执行 a 函数时,JS会创建一个全局在执行上下文栈,所有的 JS 代码都会在这个栈里面执行

image.png

当我们执行 a 时,会创建 a 对应的一个执行上下文,然后入栈

image.png

然后 a 的内部调用了b,同理,会创建一个 b 的执行上下文,然后入栈

image.png

同理,b里面执行了c,那么也会创建 c 的执行上下文,然后入栈

image.png

最后打印出 '我是ccc'

执行完毕后,此时 c 里面没有更多代码需要执行了,则需要把 c 的执行上下文出栈销毁

image.png

同理,b 和 a 的执行上下文都出栈,那么最后就剩一个全局执行上下文在栈底

image.png

最后,所有代码都执行完了,那么全局执行上下文就出栈

image.png

以上就是执行上下文与执行上下文栈

1.15 闭包

闭包的本质可以理解为:在一个作用域中可以访问另一个函数内部的局部变量的函数

图解理解闭包

比如,我想在下面的代码中,输出 name 变量

image.png 很明显,此时输出的肯定是 undefined,因为 name 是 makeFunc 函数的私有变量

那么如何访问到这个 私有变量 name 呢?

我们可以在 makeFunc 函数里面返回一个函数 displayName,执行 displayName 去访问 name

image.png

这样,我们就访问到了私有变量 name

这就体现了闭包从一个作用域去访问一个函数内部的私有变量的机制

闭包的特性

  • 能访问到父级函数的变量
  • 访问到父级函数的变量不会销毁

为什么闭包访问到的变量不会销毁

这就可以结合执行上下文去理解

首先,当执行 makeFunc 时,会创建对应的函数执行上下文入栈

然后 makeFunc 执行完毕后,对应的函数执行上下文就被销毁

此时正常的图解如下:

image.png

然而,内部返回的 displayName 作为值返回出去,此时 displayName 这个函数就可以理解为一个虫洞或者外挂,使得 name 的值得以保存下来,并且只能通过 displayName 去访问

image.png

总结就是:原本 makeFunc 函数执行完之后,就应该弹出 makeFunc 的执行栈,并销毁 makeFunc 的执行上下文的。但是在闭包场景中, makeFunc 内部的 displayName 函数使用了 makeFunc 作用域中的变量name,形成了一个作用域链,导致 makeFunc 函数不能释放。

解决闭包的方式

  • 生命周期中解绑相关事件
  • 对象使用完之后赋值 null
  • 数组使用完之后赋值[],设置长length为0

1.16 JS事件循环 Event Loop

为什么会存在 event loop

由于 JS 是单线程的,需要执行完当前任务后,才能执行下一个任务。

果遇到计算量大、耗时久的任务,那么后续的任务就会被阻塞,造成响应不及时、页面卡顿等问题。因此,JS 有了它独特的生态:EventLoop(事件循环)

event loop 流程

首先,JS的任务分成 同步任务异步任务

异步任务又分为 宏任务微任务

  • Promise.then():微任务
  • setTimeOut:宏任务
  • async、await:微任务
  • setInterval:宏任务

EventLoop 的流程可以简单来说可以概况为:同步任务 ➡ 微任务 ➡ 宏任务

具体而言:

  1. 执行同步任务
  2. 同步任务执行完毕后,去微任务队列,看有没有需要执行的微任务
  • 如果有,则把当前微任务队列里面所有的微任务推入主线程的执行栈,执行微任务,执行完毕,去宏任务队列。
  • 如果没有,则去宏任务队列
  1. 看有没有需要执行的宏任务,如果有,则推入主线程执行栈执行(只取一个宏任务)。
  2. 当前这个宏任务执行完后,又去微任务队列,看有没有需要执行的微任务。如果有,全部推入执行栈。执行完毕后,又回到宏任务队列,取出当前一个宏任务,推入执行栈执行。执行完又去微任务队列,依次循环。

图解如下: image.png

例子

第二题:

 代码解读
复制代码
//S1
setTimeout(() => {
  console.log(1); 
  //S4
  setTimeout(() => {
    //M2
    Promise.resolve().then(() => {
      console.log(9);
    }); 
  }, 0);
  //M3
  Promise.resolve().then(() => {
    console.log(7);
  }); 
}, 0); 

console.log(2); 

//M1
Promise.resolve().then(() => {
  console.log(3);
}); 

//S2
setTimeout(() => {
  console.log(8);
  //S5
  setTimeout(() => {
    console.log(5);
  }, 0); 
}, 0);

//S3
setTimeout(() => {
  //M6
  Promise.resolve().then(() => {
    console.log(4);
  }); 
}, 0);

console.log(6); 

输出顺序: 2 6 3 1 7 8 4 9 5

分析: 为了方便我们分析,我给每个宏任务取个别名 S几,微任务取别名 M几

  1. 首先,执行所有的同步任务,也就是 console.log(2)console.log(6),所以先打印 2 6
  2. 宏任务和微任务依次进入各自的队列。此时的宏任务队列、微任务队列分别是:
  • 宏任务队列:S1、S2、S3

  • 微任务队列:M1

    那么此时,取出所有的微任务,也就是 M1,执行 console.log(3)

    然后去宏任务队列,取出一个宏任务(队列先进先出,所以先取出 S1),执行 console.log(1),执行的过程中又遇到了 S4,此时 S1 还没执行完,那么 S4 入宏任务队列,然后遇到 M3,执行 console.log(7),此时 S1 执行完毕。此时打印的结果为:2 6 3 1 7

  1. 执行完前面两步后,宏任务队列、微任务队列分别是:
  • 宏任务队列:S2、S3、S4

  • 微任务队列:

    此时,微任务队列没有微任务,那么从宏任务队列取出 S2,执行 console.log(8),然后遇到 S5,此时 S2 还没执行完,所以 S5 推入宏任务队列。此时打印的结果为:2 6 3 1 7 8

  1. 经过第三步后,宏任务队列、微任务队列分别是:
  • 宏任务队列:S3、S4、S5

  • 微任务队列:

    同样,微任务队列没有任务,则从宏任务队列中,取出 S3 执行,然后遇到 微任务 M6,M6 推入微任务队列。

    此时宏任务队列、微任务队列分别是:

    • 宏任务队列:S4、S5
    • 微任务队列:M6

    那同样的,取出 M6,执行 console.log(4),此时打印结果是:2 6 3 1 7 8 4

  1. 经过第四步后,此时宏任务队列、微任务队列分别是:
  • 宏任务队列:S4、S5
  • 微任务队列:

取出 S4,执行 S4,遇到 M2,M2 入微任务队列。

  1. 此时宏任务队列、微任务队列分别是:
  • 宏任务队列:S5
  • 微任务队列:M2

取出 M2,执行 console.log(9),此时打印结果是:2 6 3 1 7 8 4 9

  1. 此时宏任务队列、微任务队列分别是:
  • 宏任务队列:S5
  • 微任务队列:

取出 M5,执行 console.log(5),此时打印结果是:2 6 3 1 7 8 4 9 5

1.17 深拷贝与浅拷贝

概念

浅拷贝:如果是基本数据类型,则拷贝值;如果是引用数据类型,则拷贝地址(拷贝前后的地址是相同的)

深拷贝:如果是基本数据类型,则拷贝值;如果是引用数据类型,则新建一个地址,递归的把原引用数据类型的属性方法都拷贝一份

常用的浅拷贝/深拷贝

浅拷贝:Object.assign() Array.concat() Array.slice() Array.reverse()等等

深拷贝:JSON.stringfy() JSON.parse(),但是这两个方法在拷贝时会忽略 undefined、Symbol 和函数等特殊值

深拷贝实现

function deepCopy(obj){ 
    //不是引用类型就不拷贝
    if(!(obj instanceof Object)) return obj 
    //如果形参obj是数组,就创建数组,如果是对象就创建对象
    let objCopy = Array.isArray(obj) ? [] : {}  

    for(let key in obj){  
        // 如果是对象,则把 key 加上,然后值继续递归(因为值可能又是个对象,所以递归)
        if(obj[key] instanceof Object){  
            objCopy[key] = deepCopy(obj[key])  
        } else {
            if(obj.hasOwnProperty(key)){  
                objCopy[key] = obj[key]  
            }
        }  
    }  
    // 最后返回
    return objCopy  
}

当然实际项目还是用 lodash

1.18 JS垃圾回收机制

引用计数法

记录每个值引用的次数, 如果次数不为0,则不清除。

会存在 循环引用 的问题

比如obj1obj2通过属性进行相互引用,两个对象的引用次数都是2。 因此它们的引用次数永远不会是0,就会引起循环引用

标记清除法

当变量进入执行环境时,就标记这个变量“进入环境”,被标记为“进入环境”的变量是不能被回收的,因为他们正在被使用。当变量离开环境时,就会被标记为“离开环境”,被标记为“离开环境”的变量会被内存释放。

如何减少垃圾回收

  • 数组:赋值 [],并设置长度为0
  • 对象:赋值 null

1.19 call、apply、bind

这三个方法其实都是改变 this 指向,是作为函数原型上的方法,区别只是参数不同

  • call(obj, arg1,arg2,arg3...)
// call 手写实现
Function.prototype.myCall = (obj, ...args) {
    // 1. 判断 this 是不是函数,这个 this 是指向函数的
    if (typeof this !== 'function') {
        throw new TypeError('调用myCall的必须是个函数')
    }

    // 2. 判断 obj 有没有传入,没有就默认 window
    obj = obj ? obj : window

    // 3. 在 context 身上添加方法,值是 this
    const fn = Symbol('key'); // symbol 防止属性名冲突,保证唯一
    obj[fn] = this;

    // 4. 执行 fn,拿到结果 result
    const result = obj[fn](...args)

    // 5. 删除 fn
    delete obj[fn]

    // 6. 返回结果
    return result
    
}
  • apply(obj, [arg])
// apply 实现
Function.prototype.myApplay = (obj, args) {
    // 1. 判断 this 是不是函数,这个 this 是指向函数的
    if (typeof this !== 'function') {
        throw new TypeError('调用myCall的必须是个函数')
    }

    // 2. 判断 args 是不是数组
    if (!Array.isArray(args)) {
        throw new TypeError("第二个参数必须是数组")
    }

    // 3. 判断 obj 有没有传入,没有就默认 window;args有没有传入,没有就 []
    obj = obj ? obj : window
    args = args || []

    // 3. 在 context 身上添加方法,值是 this
    const fn = Symbol('key'); // symbol 防止属性名冲突,保证唯一
    obj[fn] = this;

    // 4. 执行 fn,拿到结果 result
    const result = obj[fn](...args)

    // 5. 删除 fn
    delete obj[fn]

    // 6. 返回结果
    return result
    
}
  • bind(obj, args可以是数组也可以一个一个传入)
// 手写 bind
Function.prototype.myBind = function (obj, ...args) {
    // 1. 判断 this 是不是函数,这个 this 是指向函数的
    if (typeof this !== "function") {
      throw new TypeError("被调用的对象必须是函数");
    }
  
    // 2. 判断 obj 有没有传入,没有就默认 window
    obj = obj || window;
  
    // 3. 保存原始函数的引用,this就是要绑定的函数
    const that = this;
  
    // 4. 返回一个新的函数作为绑定函数
    return function fn(...innerArgs) {
      // 5. 判断返回出去的函数有没有被new
      if (this instanceof fn) {
        return new that(...args, ...innerArgs);
      }
      // 6. 使用apply方法将原函数绑定到指定的上下文对象上
      return that.apply(obj, args.concat(innerArgs));
    };
  };
  

1.20 hasOwnProperty、instanceof 的区别

hasOwnProperty()  方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

1.21 JS实现继承的方法

原型链继承

父类的实例对象作为子类的原型

child.prototype = new Father()

child.prototype.constructor = child

function Father(name) {
    this.name = name
}
Father.prototype.showName = function () {
    console.log(this.name);
}
function Child(age) {
    this.age = 20
}


// 原型链继承,将子函数的原型绑定到父函数的实例上,子函数可以通过原型链查找到复函数的原型,实现继承
// 将Child原型的构造函数指回Child, 否则Child实例的constructor会指向Father
Child.prototype = new Father()
Child.prototype.constructor = Child


Child.prototype.showAge = function () {
    console.log(this.age);
}
let child = new Child(20, 'Jolyne') // 法向父构造函数里传参
// 子类构造函数的实例继承了父类构造函数原型的属性,所以可以访问到父类构造函数原型里的showName方法
// 子类构造函数的实例继承了父类构造函数的属性,但是无法传参赋值,所以是this.name是undefined
child.showName() // undefined
child.showAge()  // 20

缺点是父子共用,所有的子类实例共享着一个原型对象,一旦原型对象的属性发生改变,所有子类的实例对象都会收到影响

构造函数继承

子类通过调用父类的构造函数去继承父类本身的属性和方法

function Father(name) {
    this.name = name
}
Father.prototype.showName = function () {
    console.log(this.name);
}
function Child(name, age) {
    // 子类调用父类的构造函数去继承父类的属性 name,相当于就是 this.name = name
    Father.call(this, name)
    this.age = age
}
let s = new Child('Jolyne', 20) // 可以给父构造函数传参
console.log(s.name); // 'Jolyne'
console.log(s.showName); // undefined

缺点是子类不能继承父类原型上的属性和方法,且不能复用

组合继承

原型链继承 + 构造函数继承

function Father(name) {
    this.name = name
}
Father.prototype.showName = function () {
    console.log(this.name);
}
function Child(name, age) {
    // 子类调用父类的构造函数继承父类的属性 name ,相当于 this.name = name
    Father.call(this, name)
    this.age = age
}
// 原型链继承
Child.prototype = new Father() //这里 相当于调用了两次
Child.prototype.constructor = Child

Child.prototype.showAge = function () {
    console.log(this.age);
}
let p = new Child('Jolyne', 20) 
p.showName() // 'Jolyne'
p.showAge()  // 20

优点:子类能继承父类本身以及父类原型上的属性和方法

缺点:调用了两次 父类的构造函数,造成内存浪费

寄生组合继承

也是 原型链继承 + 构造函数继承,只不过在原型链继承时,是把父类的原型作为参数,通过 Child.prototype = Object.create(Father.prototype) ,只需要调用一次父类的构造函数即可继承父类本身及原型上的属性和方法

function Father(name) {
    this.name = name
}
Father.prototype.showName = function () {
    console.log(this.name);
}
function Child(name, age) {
    // 子类调用父类的构造函数继承父类的属性 name ,相当于 this.name = name
    Father.call(this, name)
    this.age = age
}
// 原型链继承
Child.prototype = Object.create(Father.prototype) // Object.create方法返回一个对象,它的隐式原型指向传入的对象。
Child.prototype.constructor = Child

let p = new Child('Jolyne', 20) 
p.showName() // 'Jolyne'

1.22 函数柯里化

把接受多个参数的函数转化成接受单一参数的函数的技术

手写实现

function myCurry(fn, ...args) {
    /**
     * 如果传入的参数个数大于等于fn原本的参数个数
     */
    return args.length >= fn.length ? fn(...args)
        /**
         * 如果传入的参数个数小于fn原本的参数个数,则继续柯里化
         */
        : (...newArgs) => myCurry(fn, ...args, ...newArgs)
}
function add1(x, y, z) {
    return x + y + z;
}
const add = myCurry(add1);
console.log(add(1, 2, 3));
console.log(add(1)(2)(3));
console.log(add(1, 2)(3));
console.log(add(1)(2, 3));

1.23 Promise相关

Promise 常用方法原理及手写

Promise 是异步编程的一种解决方法,也是一个包含三种状态的对象

包含三种状态:fullfiled(成功)reject(失败) pending(状态未改变,且一个 promise 的状态只会改变一次

Promise.resolve

resolve(params) 会把 params 参数传给下一次 then(val) 中的 val

原理:

  • params 如果是普通值(string number这些 或者 实现了thenable 方法的对象,相当于包装成 Promise 并返回这个 Promise
  • params 如果是 promise,则直接返回

手写实现:

function myResolve(params) {
    //如果 params 是 Promise,则直接返回
    if (params instanceof Promise) return params
    //否则,将 params 包裹成 Promise,返回这个 Promise 对象
    return Promise.resolve(params)
}
Promise.all

原理:

  • 如果 promiseArr 里面的每一个 promise 对象都为 fulfilled,则把每一个 fulfilled 的结果添加到一个结果数组 res 中,返回 res

  • 如果 promiseArr 里面有一个 promise 对象为 rejected,则把 第一个 rejected 的值返回

手写实现:

function myAll(promiseArr) {
    let index = 0, res = []
    //返回的新的 Promise
    return new Promise((resolve, reject) => {
        //遍历 promiseArr,判断每一个 promise 的状态
        promiseArr.forEach(p => {
            //通过 Promise.resolve(p) 判断当前 p 的状态
            Promise.resolve(p).then(val => {
                index++
                //若为 fulfilled,则添加到 res 中 
                res.push(val)
                //如果相等,表示所有的 promise 均为 fulfilled,返回 res
                if (index === promiseArr.length) {
                    resolve(res)
                }
            }, err => {
                //如果有一个 promise 为 rejected
                reject(err)
            })
        })
    })
}
Promise.race

原理:

race() 和 all() 类似,也是收集 promise 对象的数组 promiseArr 作为参数,只不过对于 race() 来说,谁先有状态,就先用谁的结果。

手写实现:

function myRace(promiseArr) {
    return new Promise((resolve, reject) => {
        //遍历 promiseArr
        promiseArr.forEach(p => {
            Promise.resolve(p).then(val => {
                //最先有结果的那个 promise 是 fulfilled
                resolve(val)
            }, err => {
                //最先有结果的那个 promise 是 rejected
                reject(err)
            })
        })
    })
}
Promise.allSettled

原理:

allSettled() 方法和 all() 也类似,也接受 promiseArr 数组作为参数,它的规则如下:

  • 不管 promiseArr 里面的 promise 是 fulfilled 还是 rejected,都会把它的结果和状态都放进一个对象即:{status: 'fulfilled', value: ''} ,然后添加到结果数组 res 中。
  • 只要调用了 allSettled() 方法,原 Promise 对象的状态一定是 fulfilled

手写实现:

function myAllsettled(promiseArr) {
    let res = [], index = 0;
    return new Promise((resolve, reject) => {
        promiseArr.forEach(p => {
            index++
            Promise.resolve(p).then(val => {
                res.push({
                    status: 'fulfilled',
                    val
                })
            }, err => {
                res.push({
                    status: 'rejected',
                    val: err
                })
            }).finally(() => {
                //如果相等,则遍历完毕,且原 Promise 一定是 fulfilled
                if(index === promiseArr.length) resolve(res)
            })
        })
    })
}
Promise.any

原理:

any() 同样和前面的几个类似,也接受 promiseArr 数组作为参数,规则如下:

  • promiseArr 里至少有一个为 fulfilled,则把成功的结果返回,原 Promise 对象为 fulfilled
  • promiseArr 里如果全是 rejected 或者 promiseArr为空数组,则会报错 AggregateError

手写实现:

function myAny(promiseArr) {
    let index = 0;
    return new Promise((resolve, reject) => {
        if(index === promiseArr.length) {
            reject(new AggregateError('All promises were rejected'))
        }
        promiseArr.forEach(p => {
            Promise.resolve(p).then(val => {
                resolve(val)
            }, err => {
                index++;
                if(index === promiseArr.length) {
                    reject(new AggregateError('All promises were rejected'))
                } 
            })
        })
    })
}

如何中断 Promise 链

当使用 promise 的 then 链式调用时, 在回调函数中返回一个 pendding 状态的 promise 对象

Promise 的缺点

  • Promise一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,此时Promise内部抛出的错误,不会反应到外部
  • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)

创建 Promise 的方法有哪些

  • new Promise
  • Promise.resolve
  • Promise.reject

Promise 的实例方法有哪些

  • then
  • catch
  • finally

Promise 的静态方法有哪些

  • all
  • race
  • allSettled
  • any
  • resolve
  • reject

Promise 什么时候能拿到数据

  • 如果先指定的回调, 那当状态发生改变时, 回调函数就会调用, 得到数据
  • 如果先改变的状态, 那当指定回调时, 回调函数就会调用, 得到数据

promise 如何串连多个任务?

  • promise 的 then()返回一个新的 promise, 可以开成 then()的链式调用
  • 通过 then 的链式调用串连多个同步/异步任务

promise 异常传透是什么?

Promise 对象的错误具有冒泡性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。

  • 当使用 promise 的 then 链式调用时, 可以在最后指定失败的回调,
  • 前面任何操作出了异常, 都会传到最后失败的回调中处理

1.24 async/await相关

async/await 的概念

其实就是 Generator 函数的语法糖

实现原理:

  • 就是将 Generator 函数和自动执行器 spawn 包装在一个函数里
async function fn(args) {
  // ...
}
​
// 等同于function fn(args) {
  return spawn(function* () {  // spawn函数就是自动执行器
    // ...
  });
}

async 的返回值

  • async函数返回一个 Promise 对象
  • async返回的值,会作为下一个 then(val) 中的 val
async function fn() {
    return await 0
}

fn().then(val => console.log('aysnc返回的值是:', val)) // aysnc返回的值是:0
  • async函数内部抛出错误,返回的 Promise 对象变为reject。错误就会被catch接收到。

await 等待的结果/await 右侧的情况

  • await 右侧不是 promise 对象(比如 string 或 number),那么这个 sring 或 number 就是 await 的结果
  • await 右侧是 promise 对象,此时会阻塞后面的代码,等到 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果

await 实现休眠效果

function sleep(interval) {
    return new Promise(resolve => {
        // 每隔 interval 才执行 resolve
        setTimeout(resolve, interval);
    })
}

async function one2FiveInAsync() {
    for (let i = 1; i <= 5; i++) {
        console.log(i);
        // 阻塞后续代码,当 sleep 中的 resolve 完成后,再继续执行
        await sleep(1000);
    }
}

one2FiveInAsync();

async 如何捕获异常

  • try catch:catch捕获异常,此时 try 的后续代码不会执行
async function getCatch() {
    try {
        await new Promise(function (resolve, reject) {
            reject(new Error('错误'))
        })
        console.log('成功')  // try 抛出错误之后,就不会执行这条语句
    } catch (error) {
        console.log(error)  //  catch语句能捕获到错误信息
    }
}
  • catch 回调:只能捕获异步方法的错误,后续代码会执行
async function getCatch() {
    await new Promise(function (resolve, reject) {
        reject(new Error('错误'))
    }).catch(error => {
        console.log(error)  // .catch()能捕获到错误信息,后续代码会继续执行
    })
    console.log('成功') //  这条代码也会执行
}

1.25 DOM相关

DOM 事件流

三个阶段

  • 捕获阶段
    • 事件从父级传到子级,从上往下
    • documet ----> html ----> body ----> 某个具体的元素
  • 目标阶段
    • 事件传到了具体触发的那个元素
  • 冒泡阶段
    • 事件从子级传到父级,从下往上
    • 某个具体元素 ----> body ----> html ----> document

事件冒泡

从具体的触发事件的那个元素开始,逐级向上传播到document

<html>
  
  <head> 
    <title>Document</title> 
  </head>
  
  <body> 
    <button>按钮</button> 
  </body> 
  
</html>

当 button 触发事件时,事件冒泡的顺序为:

  • button
  • body
  • html
  • document

事件捕获

从 document 传递事件到具体触发事件的那个元素

<html>
  
  <head> 
    <title>Document</title> 
  </head>
  
  <body> 
    <button>按钮</button> 
  </body> 
  
</html>

当 button 触发事件时,事件捕获的顺序为:

  • document
  • html
  • body
  • button

事件代理

利用事件冒泡的机制,把事件绑定在父元素身上(或者说在父元素身上绑定事件监听),来管理父级及子级的所有事件

举个例子:

<body>

    <ul onclick="">
        <li></li>
        <li></li>
        <li></li>
        <li></li>
    </ul>
    
</body>

在 ul 上绑定点击事件,当 li 触发时,都能触发 click 对应的回调,并且也可以知道是哪一个 li 触发的

优点:

  • 事件委托可以减少事件注册数量,节省内存占⽤
  • 当新增子元素时,无需再次做事件绑定

DOM常见操作

(1)创建节点

createElement

创建新元素,接受一个参数,即要创建元素的标签名

const divEl = document.createElement("div");

createTextNode

创建一个文本节点

const textEl = document.createTextNode("content");

createDocumentFragment

用来创建一个文档碎片,它表示一种轻量级的文档,主要是用来存储临时节点,然后把文档碎片的内容一次性添加到DOM

const fragment = document.createDocumentFragment();

当请求把一个DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment自身,而是它的所有子孙节点

createAttribute

创建属性节点,可以是自定义属性

const dataAttribute = document.createAttribute('custom');
consle.log(dataAttribute);
(2)获取节点

querySelector

传入任何有效的css 选择器,即可选中单个 DOM元素(首个):

document.querySelector('.element')
document.querySelector('#element')
document.querySelector('div')
document.querySelector('[name="username"]')
document.querySelector('div + p > span')

如果页面上没有指定的元素时,返回 null

querySelectorAll

返回一个包含节点子树内所有与之相匹配的Element节点列表,如果没有相匹配的,则返回一个空节点列表

const notLive = document.querySelectorAll("p");

需要注意的是,该方法返回的是一个 NodeList的静态实例,它是一个静态的“快照”,而非“实时”的查询

关于获取DOM元素的方法还有如下,就不一一述说

document.getElementById('id属性值');返回拥有指定id的对象的引用
document.getElementsByClassName('class属性值');返回拥有指定class的对象集合
document.getElementsByTagName('标签名');返回拥有指定标签名的对象集合
document.getElementsByName('name属性值'); 返回拥有指定名称的对象结合
document/element.querySelector('CSS选择器');  仅返回第一个匹配的元素
document/element.querySelectorAll('CSS选择器');   返回所有匹配的元素
document.documentElement;  获取页面中的HTML标签
document.body; 获取页面中的BODY标签
document.all[''];  获取页面中的所有元素节点的对象集合型
(3)更新节点

innerHTML

不但可以修改一个DOM节点的文本内容,还可以直接通过HTML片段修改DOM节点内部的子树

// 获取<p id="p">...</p >
var p = document.getElementById('p');
// 设置文本为abc:
p.innerHTML = 'ABC'; // <p id="p">ABC</p >
// 设置HTML:
p.innerHTML = 'ABC <span style="color:red">RED</span> XYZ';
// <p>...</p >的内部结构已修改

innerText

自动对字符串进行HTML编码,保证无法设置任何HTML标签

// 获取<p id="p-id">...</p >
var p = document.getElementById('p-id');
// 设置文本:
p.innerText = '<script>alert("Hi")</script>';
// HTML被自动编码,无法设置一个<script>节点:
// <p id="p-id">&lt;script&gt;alert("Hi")&lt;/script&gt;</p >

style

DOM节点的style属性对应所有的CSS,可以直接获取或设置。遇到-需要转化为驼峰命名

// 获取<p id="p-id">...</p >
const p = document.getElementById('p-id');
// 设置CSS:
p.style.color = '#ff0000';
p.style.fontSize = '20px'; // 驼峰命名
p.style.paddingTop = '2em';
(4)添加节点

appendChild

把一个子节点添加到父节点的最后一个子节点

如果是获取DOM元素后再进行添加操作,这个js节点是已经存在当前文档树中,因此这个节点首先会从原先的位置删除,再插入到新的位置

如果动态添加新的节点,则先创建一个新的节点,然后插入到指定的位置

insertBefore

把子节点插入到指定的位置,使用方法如下:

parentElement.insertBefore(newElement, referenceElement)

子节点会插入到referenceElement之前

setAttribute

添加一个属性节点,如果元素中已有该属性改变属性值

const div = document.getElementById('id')
div.setAttribute('class', 'white');//第一个参数属性名,第二个参数属性值。
(5)删除节点

removeChild

删除一个节点,首先要获得该节点本身以及它的父节点,然后,调用父节点的removeChild把自己删掉

// 拿到待删除节点:
const self = document.getElementById('to-be-removed');
// 拿到父节点:
const parent = self.parentElement;
// 删除:
const removed = parent.removeChild(self);
removed === self; // true

addEventListener 第三个参数的作用

addEventListener有三个参数:

 element.addEventListener(event, function, useCapture)
参数描述
event必须。字符串,指定事件名。 注意: 不要使用 "on" 前缀。 例如,使用 "click" ,而不是使用 "onclick"。 提示: 所有 HTML DOM 事件,可以查看我们完整的 HTML DOM Event 对象参考手册
function必须。指定要事件触发时执行的函数。 当事件对象会作为第一个参数传入函数。 事件对象的类型取决于特定的事件。例如, "click" 事件属于 MouseEvent(鼠标事件) 对象。
useCapture可选。布尔值,指定事件是否在捕获或冒泡阶段执行。 可能值:true - 事件句柄在捕获阶段执行(即在事件捕获阶段调用处理函数)false- false- 默认。事件句柄在冒泡阶段执行(即表示在事件冒泡的阶段调用事件处理函数)

阻止冒泡/默认事件

阻止事件冒泡

element.addEventListener('click', function(e) {
       // 兼容性的写法
       if(e & e.stopPropagation) {
         e.stopPropagation();   // 现代浏览器
       }else {
         window.event.cancleBubble = true; // 老的浏览器
       }
   }, false)

*阻止默认事件

element.addEventListener('click', function(e) {
    // 兼容性的写法
    if(e & e.preventDefault) {
      e.preventDefault();   // 现代浏览器
    }else {
        window.event.returnValue = false; // 老的浏览器
    }
}, false)

DOM种元素视图尺寸的属性有哪些?

属性说明
offsetLeft获取当前元素到定位父节点的left方向的距离
offsetTop获取当前元素到定位父节点的top方向的距离
offsetWidth获取当前元素 width + 左右padding + 左右border-width
offsetHeight获取当前元素 height + 上下padding + 上下border-width
clientWidth获取当前元素 width + 左右padding
clientHeight获取当前元素 height + 上下padding
scrollWidth当前元素内容真实的宽度,内容不超出盒子宽度时为盒子的clientWidth
scrollHeight当前元素内容真实的高度,内容不超出盒子高度时为盒子的clientHeight

总结:

  • offsetLeft offsetTop: 当前元素相对于父元素左侧/顶部的距离
  • offsetWidth offsetHeigh: 当前元素的 宽高 + 对应方向的 padding + 对应方向的 border
  • clientWidth clientHeight: 当前元素的 宽高 + 对应方向的 padding
  • scrollWidth scrollHeight: 当前元素真是的宽高,内容不超过宽高时为对应的 clientWidht clientHeight

getBoundingClientRect

Element.getBoundingClientRect() 方法返回的是一个对象,对象里有这8个属性:left,right,top,bottom,width,height,x,y

IntersectionObserver

IntersectionObserver是用来监听某个元素与视口交叉状态

实际应用比如:图片懒加载 无线滚动

具体参考:# IntersectionObserver