# 事件循环

js是单线程语言,它把执行的任务分为两类。一类是同步任务、一类是异步任务;同步任务就是严格按照代码先后顺序执行的任务。执行前被推入栈中,执行后就推出栈。而异步任务的则是进入Event Table并注册函数,然后在任务完成后,把回调函数推入到异步队列中。异步任务分两种,一种是宏任务,例如setTimeout、setInterval等,一种是微任务,例如promise、process.nextTick等。当主线程执行完同步任务后,就会执行微任务,微任务执行完以后,再从宏任务队列中取出一个宏任务执行。执行完这个宏任务以后,再去执行微任务,以此循环往复。

# 闭包

闭包指的是有权访问其他函数作用域变量的函数。主要用于创建私有变量、延长变量的生命周期、函数柯里化。缺点是容易造成内容泄漏

# 原型链

每个对象都有一个proto属性,这个属性指向其构造函数的原型对象,也就是构造函数的prototype属性,原型对象也有proto属性,最终指向Object.prototype,Object构造函数的原型对象指向null。当访问对象的一个属性时,是先在对象自身查找有无该属性,如果没有的话,则沿着原型链,一个个的对象里面去查找,找到即返回值,没有的话返回undefined。

存在两个问题,;

# this的指向

this是在运行时绑定的,指向取决于函数的调用方式。

  1. apply、call、bind可以变更this指向。
  2. 严格模式下全局的this指向undefined。
  3. 箭头函数中的this指向它的父级作用域,它自身不存在this。

# 继承

# 原型链直接实现继承

缺点:实例会共享原型链中的引用类型;创建子类型时,不能向超类型传递参数。

function Parent () {
  this.loves = ['eat']
}
function Son () {
}
Son.prototype = new Parent()
1
2
3
4
5
6

# 借用构造函数(经典继承)

优点:解决实例共享原型链中引用类型的问题、创建子类型实例不能向超类型构造函数传值的问题。 缺点:超类型构造函数中定义的方法无法复用。每创建一个子实例都会生成同样的一个方法。并且超类型中定义的方法,子类型是不可见的。

function Father () {
  this.loves = ['eat']
  this.sayName = function () {
    console.log(this.loves)
  }
}
function Son () {
  Father.call(this)
}
1
2
3
4
5
6
7
8
9

# 组合继承(伪经典继承)

借用构造函数继承属性,修改原型指向超类型的实例,实现方法的共享。 优点:原型链和经典继承的问题。 缺点:调用了两次超类型的构造函数,第一次是创建子类型实例时,第二次是改变子类型原型指向时。

function Father () {
  this.loves = ['eat']
}
function Son () {
  Father.call(this)
}

Father.prototype.sayHi = function () {
  console.log(this.loves)
}
Son.prototype = new Father()

const son = new Son()
son.sayHi()
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 原型式继承

借助原型,基于现有的一个对象创建新的对象。object.create(object, props)(object.create可以设置新对象自己的属性来屏蔽父对象的属性) 缺点:实例会共享父对象的引用类型值。

function objectCreate (object) {
  function Fn () {}
  Fn.prototype = object
  return new Fn()
}
// Object.create()
1
2
3
4
5
6

# 寄生式继承

借助原型式继承,然后封装增强对象的过程。 缺点:函数不能复用,实例会共享父对象的引用类型值。

function createOtherObj (object) {
  const obj = objectCreate(object)
  obj.sayHi = function () {
    console.log(this.name)
  }
  return obj
}
1
2
3
4
5
6
7

# 寄生组合式继承

优点:省去了父类构造函数的调用。

function extend(Son, Father) {
  const prototype = objectCreate(Father.prototype)
  prototype.constructor = Son
  Son.prototype = prototype
}
1
2
3
4
5

# new运算符

var obj = {}
obj.__proto__ = F.prototype
F.call(obj)
1
2
3

# V8垃圾回收机制

主要采用分代式垃圾回收机制,v8引擎内存结构主要分为新生代和老生代,新生代主要采用scavenge算法,也就是将内存一分为二,一部分为激活空间,一部分为闲置空间,当进行一次垃圾回收时,将激活区存活的对象复制到闲置区,然后再将闲置区和激活区身份互换。缺点就是浪费一半的内存用于复制。当一个对象经历过一次scavenge算法后,在下一次垃圾回收时,会转移到老生代,或者转移时闲置区空间的内存占比已经超过25%,也会将后续的对象转移到老生代。老生代使用的的标记清除和标记整理算法。标记清理是从根节点开始遍历堆中所有的对象,然后把能访问到的对象标记为活的,然后把未被标记的对象进行清理。标记整理则是为了解决清理过后内存空间不连续的问题。所以在回收过后,将存活的对象往堆内存的一端进行移动,移动完成后再清理掉边界外的全部内存。为了减少垃圾回收的停顿时间,引入了延迟清理、增量标记和增量整理、并行标记、并行清理。习惯:减少闭包使用、少创建全局对象、清理计时器、清除DOM引用。

# async和defer的区别

async和defer都会让script标签异步下载。async是下载完以后立马执行,defer是下载完以后等全部HTML解析完且在DOMContentLoaded事件之前执行。

# 事件机制

事件捕获阶段,从document节点到目标节点,事件冒泡阶段,从目标节点再回到document节点。 e.target:事件触发的对象,可以实现事件委托。 e.currentTarget:事件监听的对象。

# src和href的区别

  1. href表示超文本引用,指向网络资源所在的位置。常用于当前文档和引用资源之间确立关系。
  2. src表示要把文件下载到html页面中,用于替换当前内容。 浏览器碰到href是会并行下载资源。而src则是停止其他资源的下载和处理,直到该资源的加载和执行完毕。

# Promise

  1. Promise之所以需要引入微任务是因为回调函数需要延迟绑定。
  2. Promise是如何实现回调函数函数返回值穿透的。
  3. Promise出错后是如何通过冒泡传递给最后一个捕获异常的函数的。

# async/await

  1. 生成器函数function* xxx
  2. 协程,运行在线程上,由生成器函数生成;next切换到协程;yield暂停协程;return结束携程,返回父协程。
  3. async异步执行和隐式返回promise
  4. await会默认创建一个promise对象,然后立即resolve()。await后续的代码会被包含到promise对象的then回调。

# 变量提升

  • 变量和函数声明是在编译阶段被js引擎放入内存(变量环境)。
  • 同名变量和函数会被覆盖

# 执行上下文

  • 定义:js执行代码时的运行环境。
  • 包括变量环境、词法环境、可执行代码、this
  • 全局执行上下文、函数执行上下文、eval执行上下文
  • 问题:函数内部的代码是到执行的时候才进行编译吗。
  • 每个执行上下文的变量环境中,都包含一个外部引用,用来指向外部的执行上下文,称为outer。

# 调用栈

  • 存入执行上下文的数据结构

# let和const

  • 词法环境:一个栈结构。
  • 会形成暂时性死区,创建提升了,初始化没提升,赋值也没提升。
  • 编译阶段放入词法环境。
  • 作用域块(大括号)内部的let、const变量会存入词法环境的单独区域。

# 词法作用域

  • 作用域链划分为动态作用域链和词法作用域链
  • 作用域链是由词法作用域决定的。
  • 词法作用域是代码中函数声明的位置来决定的。是静态的作用域。
  • 词法作用域是代码编译阶段就决定好的,跟怎么调用没有关系。
  • outer绑定的是词法作用域。

# eval

  • 传入一个字符串代码,运行时执行。
  • 性能问题

# with

  • 对象的简写方式,
  • 有性能问题

# 闭包

  • 在js中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当调用一个外部函数返回一个内部函数后,即使外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们把这些变量的集合称为外部函数的闭包。
  • 开发者工具中可以查看到。
  • closure(函数名)
  • 回收规则,当引用闭包的的函数销毁时。尽量使用局部变量。

# this

  • 跟作用域链没有太大关系,一个是this体系,一个是作用域体系。
  • 跟执行上下文是绑定的,每个执行上下文都有一个this。
  • 全局的this指向window对象。
  • call、apply、bind
  • 对象调用的方式也可以改变this
  • 箭头函数不会创建自身的执行上下文,this取决于它的外部函数。
  • 严格模式下,默认执行一个函数的执行上下文的this是undefined。

# new

  • 创建一个空对象

  • 调用函数.call(控对象)。

  • 返回空对象。

  • 使用前需要确认其变量数据类型的为静态语言

  • 运行中检查数据类型的语言称为动态语言

  • 支持隐式类型转换的语言称为弱类型语言,反之为强类型语言。

# js的数据类型(8种)

  • boolean、string、null、undefined、Object、Number、BigInt、symbol

  • typeof null === 'object'

  • 代码空间、栈空间、堆空间

  • 栈空间

    • 存储执行下上文
    • js引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了的话,会影响上下文切换的效率。
    • 在编译的过程中,js引擎会对内部函数做一次快速的词法扫描,如果发现了引用了外部函数的变量,js引擎就会在堆空间创建一个闭包对象。用来保存被引用的变量。
  • esp:记录当前执行状态的指针

  • esp移动后,后续调用的函数会直接覆盖之前函数的内存空间

# 垃圾回收器

  • 代际假说:大部分的对象一分配内存,很快就不会被访问了。而不死的对象会活的很久。
  • V8把堆分为新生代(1-8M)和老生代两个区域。
    • 新生代:副垃圾回收器
      • Scavenge算法:把区域划分两半,一部分是对象区域、一部分是空闲区域。当对象区域快被写满的时候,就执行垃圾清理操作。把存活的的对象复制到空闲区域,同时有序排列。然后两块区域进行反转。
      • 新生区域一般会设置的很小,否则清理时间会过久,影响效率。
      • 回收频繁
      • 对象晋升策略:经历过两次垃圾回收依然还存活的对象。会被移动到老生区。
    • 老生代:主垃圾回收器
      • 大的对象会直接被分配到老生区。
      • 遍历调用栈,能到达的元素称为活动对象,没有到达的元素称为垃圾数据。
      • 标记-清除算法。
        • 标记完以后直接清理
        • 缺点:碎片过多会导致内存浪费。
      • 标记-整理算法
        • 标记完以后让所有存活的对象都向一段移动,直接清理掉端边界以外的内存。
  1. 标记活动对象、非活动对象
  2. 回收非活动对象所占据的内存
  3. 内存整理,因为回收对象后,内存中存在大量不连续空间(内存碎片)。(主回收器有这一步)
  • 全停顿:垃圾回收完毕后再恢复脚本执行

  • 老生代执行垃圾回收时间较长

  • 增量标记算法:把一个完整的垃圾回收任务拆分成很多小的任务。与正常任务穿插进行。

  • 编译型语言:执行前需要经过编译器的编译过程,并且编译后保留机器能读懂的二进制文件,不需要重复编译。

    • 源代码-AST-中间代码-二进制文件-执行
  • 解释型语言:每次运行都需要通过解释器对程序进行动态解释和执行。

    • 源代码-AST-字节码-执行 生成ast
  1. 分词(词法分析),将一行行的源码拆解成一个个的token,token指的是语法上最小的单个字符或字符串。
  2. 解析(语法分析),将token数据根据语法规则转为ast。
  • 有了ast后,v8就会生成该段代码的执行上下文

  • 编译器或者解释器后续的工作都需要依赖于 AST

  • babel原理:现将ES6转成ast,再将ast转成es5的ast,再将ast转成js源代码。

  • 解释器lgnition

    • 根据ast生成字节码,并解释执行字节码。
    • 一开始v8是没有字节码的,而是直接将ast转换为机器码。但是为了小内存手机内存足以存放转换后的机器码,所以引入了字节码。就是现在的JIT即时编译(混合编译执行和解释执行)
    • 字节码介于ast和机器码之间,需要解释器转为机器码后才能执行。优点是可以减少系统内存的使用。
    • 解释器执行字节码的过程中,如果发现有热点代码,就会把该段热点的字节码编译成高效率的机器码。提升代码的执行效率。

# 消息队列和事件循环系统

  • 都是运行中主线程上
  • IO线程负责接收其他进程传进来的消息,然后往消息队列发任务。
  • 任务类型
    • 用户交互:输入事件
    • 渲染事件:js执行、解析、dom计算、样式计算、布局计算、css动画
    • 文件读写、网络请求完成
    • js脚本执行
  • 安全退出
    • chrome,确定要推出当前页面时,页面主线程会设置一个退出标志的变量,在每次执行完一个任务时,判断是否有设置退出标志。如果设置了,那就中断当前的所有任务,退出线程。
  • 微任务:为了解决高优先级任务的实时性和当前任务的效率(异步),例如DOM变化。
    • 执行时机:主函数执行结束之后,宏任务结束之前。js引擎准备退出执行上下文时。又称检查点。
    • 创建时机:v8创建执行上下文时。
  • 每个宏任务中都包含一个微任务队列,当前宏任务处理完后,就会执行微任务队列中的任务。
  • 解决单个任务过久的问题:设置回调函数。

# 延迟执行的消息队列

  • 延迟执行的任务,例如setTimeout的回调

  • 执行的时机是执行完一个宏任务就执行全部到期的任务。

  • setTimeout如果存在嵌套调用,系统会设置最短时间间隔为4ms

  • 未激活的页面,setTimeout执行最小间隔为1000ms。

  • 延迟执行时间有最大值。32个bit。超出则为会认为为0。

  • setTimeout执行的回调函数中的this指向全局环境。

  • 回调函数:作为参数传递给另一个函数的函数。在主函数返回之前执行,则为同步回调,在返回之后执行,则为异步回调。

  • 系统调用栈:循环系统维护。可以通过chrome浏览器控制台performance查看。

  • 延迟队列和消息队列的都是宏任务。

# xml

  • 后台处理完请求后,网络进程将结果发送给渲染进程的IO线程,IO线程再到消息队列加入回调函数。
  • 跨域问题
  • 混合内容问题,例如https页面带了http资源。

# Mutation Observer

  • DOM事件变更改成异步调用,多次变更会合并。
  • 用微任务插入到队列。

# promise

  • 为了解决异步回调导致的代码编程不连续的问题。
  • 通过回调函数延迟绑定和回调函数返回值穿透的技术,解决了循环嵌套的问题。
  • 延迟绑定:先声明了promise,并且执行了resolve函数,在调用then的时候才绑定回调。
  • promise的错误具有冒泡性质。

# promise消除嵌套回调

  • 产生嵌套函数的一个主要原因是在发起任务请求时会带上回调函数,这样当任务处理结束之后,下个任务就只能在回调函数中来处理了。
  1. promise实现了回调函数的延时绑定;回调函数的延时绑定在代码上体现就是先创建 Promise 对象 x1,通过 Promise 的构造函数 executor 来执行业务逻辑;创建好 Promise 对象 x1 之后,再使用 x1.then 来设置回调函数。
  2. 将回调函数onResolve的返回值穿透到最外层。
  • resolve的时候才创建了微任务
  • 关键点:resolve是怎么延迟调用回调函数的。答案:使用微任务。

# asyn/await

  • Generator(生成器):带星号的函数,例如function* xxx;可以暂停执行和恢复执行。通过yield来中断执行,和next()方法继续执行。
  • 协程:比线程更加轻量级的存在。可以理解为跑在线程上的任务,一个线程可以存在多个协程。但是线程上同时只能执行一个携程。协程是由程序所控制。(切换不像线程那样消耗资源)
  • return 会结束当前协程。
  • 调用栈如何切换:js引擎会保留子协程的调用栈信息。然后父子之间可以切换。
  • 执行生成器代码的函数称为执行器
  • async:异步执行并隐式返回promise作为结果的函数
  • await:相当于yield

# 问题

  • 垃圾清理是哪个进程负责,会阻塞渲染进程吗。
  • 新生区的翻转空间后,对象区会缩小大小吗。
  • 增量标记算法的垃圾回收任务是宏任务还是微任务,执行的时机是啥时候。
  • 为什么js采用解释型,不能采用编译型吗?
  • 每个作用域是执行的时候才解析代码?
  • v8中什么来执行源代码。
  • 解释器执行字节码怎么判断热点代码的启示位置和结束位置的呢,代码不是连续的吗。
  • 解释器执行非热点的字节码是不转成机器码了吗?
  • 渲染引擎和主线程的关系
  • promise resolve的细节。

const promise1 = new Promise((resolve, reject) => { setTimeout(() => { resolve(1) }) })