面试|JS篇

275 阅读29分钟

一、原型和继承

1.1 面向对象的三个特征

封装: 将对象运行所需资源封装在程序对象中,比方说方法和数据。
继承: 继承可解决代码复用。当多个类存在相同属性和方法时,可以从这些类中抽象出父类,在父类中定义相同的属性和方法,子类只需要继承父类的属性和方法。
多态: 多态是指一个引用(类型)在不同情况下的多种状态。也可以理解成:多态是指向父类的引用,来调用在不同子类中实现的方法。

1.2 构造对象方法

1.2.1 工厂模式

不暴露创建对象的具体逻辑,而是将逻辑封装在一个函数中,这个函数被称为一个工厂。工厂模式只需提供正确的参数,就能生产类似的产品。

function Person(name,age,job){
    let o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    return o;
}

1.2.2 构造函数模式

和工厂模式差不多,但是有以下区别:

  1. 没有显式的创建对象;
  2. 属性方法都直接赋值给了this;
  3. 没有return。
function Person(name,age,job){
   this.name = name;
   this.age = age;
   this.job = job;
}

1.2.3 原型模式

每个函数都会创建一个proptype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。使用原型对象的好处在于,在上面定义的属性和方法可以被对象实例共享。

function Person(){}
Person.proptype.name = "Jack"
Person.proptype.age = 18
Person.proptype.job = "student"

1.3 什么是原型对象?

构造函数内部的prototype属性指向的对象就是构造函数的原型对象。
原型对象包含了可以由该构造函数的所有实例共享的属性和方法。

image.png

当使用构造函数创建了一个实例对象后,这个对象内部将包含一个指针指向构造函数的原型对象,在ES5中这个指针叫做对象的原型。
而原型对象proptype中的 constructor属性指回构造函数。

1.4 什么是原型链

原型链是一种查找规则 当访问一个对象属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象中找这个属性,这个原型对象又有着自己的原型,一直找下去,这种查找过程称为原型链。

1.5 JS实现继承的方法

1.5.1 原型链继承

关键:子类构造函数的原型为父类构造函数的实例对象

缺点:1、子类构造函数无法向父类构造函数传参。

   2、所有的子类实例共享着一个原型对象,一旦原型对象的属性发生改变,所有子类的实例对象都会收影响

   3、如果要给子类的原型上添加方法,必须放在Son.prototype = new Father()语句后面。

   function Father(name) {
     this.name = name
   }
   Father.prototype.showName = function () {
     console.log(this.name);
   }
   function Son(age) {
     this.age = 20
   }
   // 原型链继承,将子函数的原型绑定到父函数的实例上,子函数可以通过原型链查找到复函数的原型,实现继承
   Son.prototype = new Father()
   // 将Son原型的构造函数指回Son, 否则Son实例的constructor会指向Father
   Son.prototype.constructor = Son
   Son.prototype.showAge = function () {
     console.log(this.age);
   }
   let son = new Son(20, 'Jack') // 无法向父构造函数里传参
   // 子类构造函数的实例继承了父类构造函数原型的属性,所以可以访问到父类构造函数原型里的showName方法
   // 子类构造函数的实例继承了父类构造函数的属性,但是无法传参赋值,所以是this.name是undefined
   son.showName() // undefined
   son.showAge()  // 20

1.5.2 借用构造函数继承

关键:用 .call() 和 .apply()方法,在子类构造函数中,调用父类构造函数

缺点:1、只继承了父类构造函数的属性,没有继承父类原型的属性。

   2、无法实现函数复用,如果父类构造函数里面有一个方法,会导致每一个子类实例上面都有相同的方法。

    function Father(name) {
      this.name = name
    }
    Father.prototype.showName = function () {
      console.log(this.name);
    }
    function Son(name, age) {
      Father.call(this, name) // 在Son中借用了Father函数,只继承了父类构造函数的属性,没有继承父类原型的属性。
      // 相当于 this.name = name
      this.age = age
    }
    let s = new Son('Jack', 20) // 可以给父构造函数传参
    console.log(s.name); // 'Jack'
    console.log(s.showName); // undefined

1.5.3 组合继承

关键:原型链继承+借用构造函数继承

缺点:使用组合继承时,父类构造函数会被调用两次,子类实例对象与子类的原型上会有相同的方法与属性,浪费内存。

    function Father(name) {
      this.name = name
      this.say = function () {
        console.log('hello,world');
      }
    }
    Father.prototype.showName = function () {
      console.log(this.name);
    }
    function Son(name, age) {
      Father.call(this, name) //借用构造函数继承
      this.age = age
    }
    // 原型链继承
    Son.prototype = new Father()  // Son实例的原型上,会有同样的属性,父类构造函数相当于调用了两次
    // 将Son原型的构造函数指回Son, 否则Son实例的constructor会指向Father
    Son.prototype.constructor = Son
    Son.prototype.showAge = function () {
      console.log(this.age);
    }
    let p = new Son('Jack', 20) // 可以向父构造函数里传参
    // 也继承了父函数原型上的方法
    console.log(p);
    p.showName() // 'Jack'
    p.showAge()  // 20

二、闭包与作用域

2.1 什么是闭包?

浏览器在加载页面时会把代码放在栈内存中执行,函数进栈执行会产生一个私有上下文,此上下文能保护里面的使用变量不受外界干扰,并且如果当前执行上下文中的某些内容,被上下文以外的内容占用,当前上下文不会出栈释放,这样可以保存里面的变量和变量值,闭包是一种保存和保护内部私有变量的机制。

2.2 闭包的作用

闭包有两个常用用途:

  • 闭包的第一个用途是在使我们在函数外部能够访问函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量
  • 闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

2.3 闭包的使用场景

  1. 循环赋值
  2. 使用回调函数
  3. 防抖节流
  4. 函数作为参数
  5. return回一个函数

2.4 闭包执行过程

  1. 形成私有上下文
  2. 进栈执行
  3. 一系列操作
  • 初始化作用域链
  • 初始化this
  • 初始化arguments
  • 赋值形参
  • 变量提升
  • 代码执行

2.5 执行上下文栈是什么

  • JS引擎使用执行上下文栈来管理执行上下文
  • 当JS执行代码时,首先遇到全局代码,会创建一个全局执行上下文并压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有代码执行完毕后,从栈中弹出全局执行上下文。

2.6 执行上下文的三个阶段

2.6.1 创建阶段

  1. this 绑定
  • 在全局执行上下文中,this指向全局对象
  • 在函数执行上下文中,this指向取决于函数如何调用。如果它被一个引用对象调用,那么this会被设置成那个对象,否则this的值被设置为全局对象或者undefined

但是由于闭包写法所致,这个事实有的时候没有那么容易看出来

window.identity = 'The Window'

let obj = {
    identity:'My Object',
    getIdentityFunc(){
        return function() {
            return this.indetity
        }
    }
}
console.log(obj.getIdentityFunc()());//'The Window'

这里是通过调用对象来执行的,但是没有返回'My Object' ,原因在于,每个函数在被调用时都会自动创建两个特殊变量:this和arguments。返回的匿名函数执行具有全局性,因此匿名函数的this其实会指向window。内部函数永远不可能直接访问外部函数的这两个变量,但是如果把this保存在闭包可以访问的另一个变量中就可以了。

window.identity = 'The Window'

let obj = {
    identity:'My Object',
    getIdentityFunc(){
    let that = this
        return function() {
            return that.indetity
        }
    }
}
console.log(obj.getIdentityFunc()());//'My Object'

这里的that指代的是getIdentityFunc()作用域里的this,和匿名函数的this也就不冲突了,返回的也是obj里的值。 2. 创建词法环境组件

  • 词法环境是一种有标识符————变量映射的数据结构,标识符是指变量/函数名,变量是对实际对象或原始数据的引用。
  1. 创建变量环境组件
  • 变量环境也是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。

2.6.2 执行阶段

在这阶段,执行变量赋值、代码执行。
如果JS引擎在源代码中声明的实际位置找不到变量的值,那么将为其分配undefined值。

2.6.3 回收阶段

执行上下文出栈等待虚拟机回收执行上下文

.apply,.bind,.call

.apply, .call, 和 .bind 是 JavaScript 中用于控制函数执行上下文(this)以及参数传递的方法。它们都允许您在调用函数时指定要使用的 this 值,以及将参数传递给函数。

这些方法的主要区别在于它们如何设置函数执行上下文以及参数传递的方式。

.call() 方法接受一个要作为函数上下文的对象作为第一个参数,接下来的参数就是传递给函数的参数。在函数调用时,将 this 的值设置为传递给 .call() 方法的第一个参数。函数的参数通过参数列表传递。

const person = {
  name: "Alice",
  sayHello: function() {
    console.log(`Hello, my name is ${this.name}`);
  }
};

const anotherPerson = {
  name: "Bob"
};

person.sayHello.call(anotherPerson); // 输出 "Hello, my name is Bob"

.apply() 方法也接受一个要作为函数上下文的对象作为第一个参数,但是它要求参数必须以数组形式传递。在函数调用时,将 this 的值设置为传递给 .apply() 方法的第一个参数。函数的参数通过一个数组传递。

const numbers = [1, 2, 3, 4, 5];

const sum = function() {
  return Array.prototype.reduce.call(arguments, function(acc, val) {
    return acc + val;
  }, 0);
};

console.log(sum.apply(null, numbers)); // 输出 15

.bind() 方法返回一个新函数,新函数会将 this 值绑定到传递给 .bind() 方法的第一个参数,并在调用时使用传递给 .bind() 方法的其他参数作为函数的参数。新函数不会立即执行,而是返回新函数的引用,可以稍后调用。

const person = {
  name: "Alice",
  sayHello: function() {
    console.log(`Hello, my name is ${this.name}`);
  }
};

const anotherPerson = {
  name: "Bob"
};

const sayHelloToAnotherPerson = person.sayHello.bind(anotherPerson);

sayHelloToAnotherPerson(); // 输出 "Hello, my name is Bob"

2.7 作用域

2.7.1 全局作用域

  • 直接写在script标签的JS代码,都在全局作用域。在全局作用域下声明的变量叫做全局变量(在块级外部定义的变量)。
  • 全局变量在全局的任何位置都可以使用;全局作用域无法访问到局部作用域的变量。
  • 全局作用域在页面打开时候创建,在页面关闭时销毁。
  • 所有window对象的属性拥有全局作用域

var和function命令的全局变量和函数是window对象的属性和方法
let、const、class声明的全局变量,不属于window对象属性

2.7.2 函数作用域(局部作用域)

  • 调用函数时会创建函数作用域,函数执行完毕以后,作用域销毁。每调用一次函数就会创建一个新的函数作用域,他们之间是相互独立的。
  • 在函数作用域中可以访问全局变量,在函数外面无法访问函数内的变量。
  • 当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用,如果没有就向上一个作用域中寻找,直到找到全局作用域,如果全局作用域中仍然没有找到,则会报错。

2.7.3 块级作用域

  • 任何一对花括号{}中的语句集都属于一个块,在块中使用let和const声明的变量,外部访问不到,这种作用域的规则就叫块级作用域。
  • 通过var声明的变量或者非严格模式下创建的函数声明没有块级作用域。

2.7.4 词法作用域

  • 词法作用域是静态的作用域,无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。

  • 编译的词法分析阶段基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过中如何对它们进行查找。

  • 换句话说,词法作用域就是你在写代码的时候就已经决定了变量的作用域。

2.7.5 作用域链的作用

作用域链:当在JS中使用一个变量时,首先JS引擎会尝试在当前作用域下去寻找该变量,如果没有找到,再到上层作用域去找,以此类推直到找到该变量或者是已经到了全局作用域,这样的访问方式为作用域链。
作用:作用域链保证执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层变量和函数。

2.7.6 变量提升和函数提升

  • 变量提升
    在JS代码执行前会进行预编译,预编译期间会将变量声明与函数声明提升至其对应作用域最顶端函数内声明的变量只会提升至该函数作用域的最顶层当函数内部定义的一个变量与外部相同时,那么函数体内的这个变量就会被上升到最顶端
  • 函数提升
    函数提升只会提升函数声明式写法,函数表达式的写法不存在函数提升 函数提升的优先级大于变量提升的优先级,即函数提升在变量提升之上

2.7.7 浏览器垃圾回收机制

2.7.7.1 内存的生命周期

js环境中分配的内存,一般有如下生命周期:

  • 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存;
  • 内存使用:即读写内存,也就是使用变量、函数等
  • 内存回收:使用完毕,由垃圾回收自动回收不再使用的内存

全局变量一般不会回收,一般局部变量的值,不用了就会被自动回收掉

2.7.7.2 垃圾回收的概念

垃圾回收: JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不参与运动时,就需要系统收回被占用的内存空间,这就是垃圾回收。
回收机制:

  • JS具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉占用的内存。
  • JS中存在两种变量:局部变量和全局变量。全局变量的生命周期会持续到页面卸载;局部变量声明在函数中,它的生命周期从函数执行开始,直到函数执行结束,在这个过程中,局部变量会在堆或者栈中存储他们的值,当函数执行结束后,这些局部变量不再被使用,他们所占用的空间被释放。

2.7.7.3 垃圾回收方式

1. 引用计数法

声明变量并给它赋一个引用值时,这个值的引用数为1。如果同一个值又被赋给另一个量,那么引用数加1.类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减1。当一个值引用数为0时,就说明没办法再访问这个值了,下次运行时,会释放引用数为0的值的内存。
这种方法会导致循环引用:即对象A有一个指针指向对象B,而对象B也引用了对象A。比如:

function problem(){
    let objectA = new Object()
    let objectB = new Object()
    
    objectA.someOtherObject = objectB
    objectB.anotherObject = objectA
}

在这个例子中,objectA和objectB通过各自的属性相互引用,意味着他们的引用数是2。当使用循环技术时,由于函数执行完后,两个对象都离开作用域,函数执行结束,objectA和objectB还会继续存在,因此他们引用次数永远不会为0,就会引起循环引用。

2.标记清理

现代浏览器已经不再使用引用技术法而使用标记清除法。

当变量进入上下文,比如在函数内部声明一个变量时,这个变量会被加上存在于上下文中的标记。而不在上下文中的变量,逻辑上讲永远不会释放内存,因为只要上下文中的代码在运行,就有可能用到它们。当变量离开上下文时,也会被加上离开上下文的标记。

垃圾回收程序运行时候,会标记内存中存储的所有变量。然后,它会将所有上下文中的变量,以及在上下文中变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何上下文的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。

2.7.7.4 减少垃圾回收

  • 对数组进行优化: 在清空一个数组时,最简单的方法就是给其赋值为[ ],但是与此同时会创建一个新的空对象,可以将数组的长度设置为0,以此来达到清空数组的目的。

  • object进行优化: 对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。

  • 对函数进行优化: 在循环中的函数表达式,如果可以复用,尽量放在函数的外面。

2.7.7.5 内存泄漏

由于疏忽或错误造成程序未能释放已经不再使用的内存

以下四种情况会造成内存的泄漏:

  • 意外的全局变量: 由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
  • 被遗忘的计时器或回调函数: 设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
  • 脱离 DOM 的引用: 获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
  • 闭包: 不合理的使用闭包,从而导致某些变量一直被留在内存当中。

三、异步与事件循环

3.1 异步编程实现方式

JS中的异步机制可以分为以下几种:

  • 回调函数:使用回调函数有一个缺点,多个回调函数嵌套的时候会造成回调地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护性
  • Promise:使用Promise的方式可以将嵌套的回调函数作为链式调用。但是这种方法,会造成多个then的链式调用,可能会造成代码的语义不够明确。
  • generator:它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式需要考虑的问题是何时将函数的控制权转移回来,因此需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。
  • async:async函数是generator和Promise实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个await语句的时候,如果语句返回的是一个promise对象,那么函数将会等待promise对象的状态变为resolve后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。

3.2 setTimeout、setInterval、requestAnimationFrame的区别

  • setTimeout
    执行该语句时,会立即把当前定时器代码推入事件队列,当定时器在事件列表中满足设置的时间时将传入的函数加入任务队列,之后的执行就交给任务队列负责。但是如果此时任务队列不为空,则需等待,所以执行定时器内代码的时间可能会大于设置的时间。
    返回值timeoutID是一个正整数,表示定时器的编号。这个值可以传递给clearTimeout()来取消该定时器。
  • setInterval
    重复调用一个函数或执行一个代码片段,每次都精确的隔一段时间推入一个事件。它返回一个interval ID,该ID唯一地标识时间间隔,因此你可以稍后用clearInterval()来移除定时器。
  • requestAnimationFrame
    是JS实现动画的一种方式,它告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

3.3 Promise

3.3.1 Promise定义

Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,它避免了地狱回调。比传统的解决方案回调函数和事件更合理和更强大。

promise本身只是一个容器,真正异步的是它的两个回调resolve()和reject()

promise本质 不是控制 异步代码的执行顺序(无法控制) , 而是控制异步代码结果处理的顺序

3.3.2 Promise状态

Promise的实例有三个状态

  • Pending(进行中)
  • Resolved(已完成)
  • Rejected(已拒绝)

当把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、没有完成失败了就变成了Rejected。 如何改变promise的状态

  • resolve(value): 如果当前是 pending 就会变为 resolved
  • reject(error): 如果当前是 pending 就会变为 rejected
  • 抛出异常: 如果当前是 pending 就会变为 rejected

注意:一旦从进行状态变成为其他状态就永远不能更改状态了。

3.3.3 Promise实例

创建Promise实例的方法

//1.一般情况
new Promise((resolve,reject)=>{ ... })
//2.Promise.resolve
Promise.resolve(11).then((value)=>{
    console.log(value)
})
//3. Promise.reject
Promise.reject(new Errow("出错了!!"))

then

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用。第二个回调函数是Promise对象的状态变为rejecte时调用。第二个可以省略。

then方法返回的是一个新的Promise实例。因此可以采用链式写法,即then方法后面再调用另一个then方法。

catch

该方法相当于then方法的第二个参数,指向reject的回调函数。不过catch还有一个作用,就是在执行resolve回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch方法。

finally

finally方法用于Promise最后是什么状态都会执行。

3.3.4 all、race、any三个静态方法

all

all方法可以完成并发任务,它接收一个数组,数组的每一项都是一个Promise对象,返回一个Promise实例。当数组中所有的promise状态达到resolved时候,all方法状态就会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected。

race

race方法和all一样,接受的参数是一个每项都是Promise的数组,但是与all不同的是,当最先执行完的事件执行完之后,就直接返回该promise对象的值。如果第一个Promise对象状态变成resolved,那自身的状态变成了resolved,反之第一个promise变成rejected,那自身状态就会变成rejected。

any

它接受一个数组,数组的每一项都是一个Promise对象,该方法会返回一个新的Promise,数组内的任意一个Promise变成了resolved,则返回的promise就会变成resolved。如果数组内的promise状态都是rejected,那么返回rejected状态。

3.3.5 promise有什么缺点

代码层面

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

语法层面

  • Promise虽然摆脱了回调地狱,但是then的链式调用多了仍会造成阅读负担
  • Promise传递中间值非常麻烦

3.4 async/await

3.4.1 async

async关键字用于声明异步函数。它返回一个Promise对象。

异步函数如果return关键字返回了值,这个值会被Promise.resolve()包装成一个Promise对象。这个参数可作为then方法回调函数的参数。

async函数内部抛出错误,会导致返回的Promise对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到

3.4.2 await

await等待的是一个表达式,这个表达式的计算结果是Promise对象或者其他值。await不仅仅可以用于等Promise对象,它可以等任意表达式的结果,所以,await后面实际可以接普通函数调用或者直接量。

3.4.3 async/await对比Promise的优势

  • 代码读起来更加方便;
  • Promise传递中间值非常麻烦,async/await几乎是同步写法;
  • 错误处理友好,async/await可以用成熟的try/catch,Promise的错误捕获非常冗余;

3.5 事件循环Event Loop

3.5.1 JS执行机制

同步任务: 即主线程上的任务,按照顺序由上至下依次执行,当前一个任务执行完后才能执行下一个任务。
异步任务: 不进入主线程,而是进入任务队列,执行完毕之后会产生一个回调函数,并通知主线程。当主线程任务执行完后,就会调取最早通知自己的回调函数,使其进入主线程中执行。

3.5.2 Event Loop

事件循环Event Loop指的是JS代码所在运行环境(浏览器、nodejs)编译器的一种解析执行规则。

3.5.3 宏任务和微任务的概念和区别

宏任务: 为了协调任务有条不紊地在主线程上执行,页面进程引入了消息队列和事件循环,渲染进程内部也会维护多个消息队列,比如延迟执行队列和普通的消息队列。然后主线程采用一个for循环,不断从任务队列中取出任务并执行任务。这些消息队列中的任务就称为宏任务

常见的宏任务如下:

任务(代码)环境
I/O浏览器/Node
setTimeout浏览器/Node
setInterval浏览器/Node
setImmediateNode
requestAnimationFrame浏览器
网络请求(Ajax)浏览器
script标签浏览器

微任务: 微任务是一个需要异步执行的回调函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。当JS执行一段宏任务,会创建一个全局执行上下文,在创建全局执行上下文时,也会创建一个微任务队列。也就是说每个宏任务都关联了一个微任务队列

常见的微任务如下:

任务环境
Promise.then()浏览器/Node
async/await浏览器/Node

3.5.4 事件循环Event Loop执行机制

每次准备取出第一个宏任务执行前, 都要将所有的微任务一个一个取出来执行,也就是优先级比宏任务高,且与微任务所处的代码位置无关

1.进入到script标签,就进入到了第一次事件循环.

2.遇到同步代码,立即执行

3.遇到宏任务,放入到宏任务队列里.

4.遇到微任务,放入到微任务队列里.

5.执行完所有同步代码

6.执行微任务代码

7.微任务代码执行完毕,本次队列清空

8.寻找下一个宏任务,重复步骤1

四、DOM

4.1 DOM事件流

事件流描述的是页面接收事件的顺序。DOM2 Events规定事件流分为三个阶段:

  • 事件捕获
  • 到达目标
  • 事件冒泡

具体流程:

  1. 事件捕获最先发生,为提前拦截事件提供可能;
  2. 实际的目标元素接收到事件;
  3. 最后一个是冒泡,最迟在这个阶段响应事件。

假定点击<div>会触发事件,它会以下图所示顺序触发

image.png

4.2 事件冒泡

当事件被定义为从最具体的元素(文档树中最深的节点)开始触发,然后向上传播至没有那么具体的元素时,该事件流程称为事件冒泡 其流程大概示意图如下:

image.png

4.3 事件捕获

事件捕获的意思是最不具体的节点应该最先收到事件,最具体的节点应该最后收到事件。

事件捕获实际上是为了在事件到达最终目标前拦截事件。

image.png

4.4 DOM定义

DOM就是文档对象模型,是用来呈现以及与任意HTMLXML文档交互的API

它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容。

4.5 DOM常用操作

创建节点

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);

获取节点

querySelector

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

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

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[''];  获取页面中的所有元素节点的对象集合型

更新节点

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、textContent

自动对字符串进行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 >

两者的区别在于读取属性时,innerText不返回隐藏元素的文本,而textContent返回所有文本

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';

添加节点

innerHTML

如果这个DOM节点是空的,例如,<div></div>,那么,直接使用innerHTML = '<span>child</span>'就可以修改DOM节点的内容,相当于添加了新的DOM节点

如果这个DOM节点不是空的,那就不能这么做,因为innerHTML会直接替换掉原来的所有子节点

appendChild

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

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

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

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

parentElement.insertBefore(newElement, referenceElement)

子节点会插入到referenceElement之前

setAttribute

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

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

删除节点

removeChild

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

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

删除后的节点虽然不在文档树中了,但其实它还在内存中,可以随时再次被添加到别的位置

4.6 DOM树,DOM对象,document对象

DOM树

以 HTMLDocument 为根节点,其余节点为子节点,组织成一个树的数据结构的表示就是 DOM树。DOM树直接体现了标签与标签之间的关系。

DOM对象

DOM对象是浏览器根据html标签生成的Js对象

docement对象是DOM里提供的一个对象,它提供的属性和方法都是用来访问和操作网页内容的

4.7 addEventListener

addEventListener 方法可以在一个元素上添加一个事件监听器,当事件被触发时,该监听器就会执行。

element.addEventListener(event, function, useCapture);

其中:

  • event:要监听的事件的名称,比如 "click"、"load" 等等。
  • function:事件触发时要执行的函数,也可以是一个函数引用。
  • useCapture:可选参数,表示是否使用事件捕获。如果设置为 true,则表示在事件传递过程中先捕获到该元素再冒泡;如果设置为 false 或者省略,则表示在事件传递过程中先冒泡到该元素再捕获。