# 事件循环
js是单线程语言,它把执行的任务分为两类。一类是同步任务、一类是异步任务;同步任务就是严格按照代码先后顺序执行的任务。执行前被推入栈中,执行后就推出栈。而异步任务的则是进入Event Table并注册函数,然后在任务完成后,把回调函数推入到异步队列中。异步任务分两种,一种是宏任务,例如setTimeout、setInterval等,一种是微任务,例如promise、process.nextTick等。当主线程执行完同步任务后,就会执行微任务,微任务执行完以后,再从宏任务队列中取出一个宏任务执行。执行完这个宏任务以后,再去执行微任务,以此循环往复。
# 闭包
闭包指的是有权访问其他函数作用域变量的函数。主要用于创建私有变量、延长变量的生命周期、函数柯里化。缺点是容易造成内容泄漏
# 原型链
每个对象都有一个proto属性,这个属性指向其构造函数的原型对象,也就是构造函数的prototype属性,原型对象也有proto属性,最终指向Object.prototype,Object构造函数的原型对象指向null。当访问对象的一个属性时,是先在对象自身查找有无该属性,如果没有的话,则沿着原型链,一个个的对象里面去查找,找到即返回值,没有的话返回undefined。
存在两个问题,;
# this的指向
this是在运行时绑定的,指向取决于函数的调用方式。
- apply、call、bind可以变更this指向。
- 严格模式下全局的this指向undefined。
- 箭头函数中的this指向它的父级作用域,它自身不存在this。
# 继承
# 原型链直接实现继承
缺点:实例会共享原型链中的引用类型;创建子类型时,不能向超类型传递参数。
function Parent () {
this.loves = ['eat']
}
function Son () {
}
Son.prototype = new Parent()
2
3
4
5
6
# 借用构造函数(经典继承)
优点:解决实例共享原型链中引用类型的问题、创建子类型实例不能向超类型构造函数传值的问题。 缺点:超类型构造函数中定义的方法无法复用。每创建一个子实例都会生成同样的一个方法。并且超类型中定义的方法,子类型是不可见的。
function Father () {
this.loves = ['eat']
this.sayName = function () {
console.log(this.loves)
}
}
function Son () {
Father.call(this)
}
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()
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()
2
3
4
5
6
# 寄生式继承
借助原型式继承,然后封装增强对象的过程。 缺点:函数不能复用,实例会共享父对象的引用类型值。
function createOtherObj (object) {
const obj = objectCreate(object)
obj.sayHi = function () {
console.log(this.name)
}
return obj
}
2
3
4
5
6
7
# 寄生组合式继承
优点:省去了父类构造函数的调用。
function extend(Son, Father) {
const prototype = objectCreate(Father.prototype)
prototype.constructor = Son
Son.prototype = prototype
}
2
3
4
5
# new运算符
var obj = {}
obj.__proto__ = F.prototype
F.call(obj)
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的区别
- href表示超文本引用,指向网络资源所在的位置。常用于当前文档和引用资源之间确立关系。
- src表示要把文件下载到html页面中,用于替换当前内容。 浏览器碰到href是会并行下载资源。而src则是停止其他资源的下载和处理,直到该资源的加载和执行完毕。
# Promise
- Promise之所以需要引入微任务是因为回调函数需要延迟绑定。
- Promise是如何实现回调函数函数返回值穿透的。
- Promise出错后是如何通过冒泡传递给最后一个捕获异常的函数的。
# async/await
- 生成器函数
function* xxx
- 协程,运行在线程上,由生成器函数生成;next切换到协程;yield暂停协程;return结束携程,返回父协程。
- async异步执行和隐式返回promise
- 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算法:把区域划分两半,一部分是对象区域、一部分是空闲区域。当对象区域快被写满的时候,就执行垃圾清理操作。把存活的的对象复制到空闲区域,同时有序排列。然后两块区域进行反转。
- 新生区域一般会设置的很小,否则清理时间会过久,影响效率。
- 回收频繁
- 对象晋升策略:经历过两次垃圾回收依然还存活的对象。会被移动到老生区。
- 老生代:主垃圾回收器
- 大的对象会直接被分配到老生区。
- 遍历调用栈,能到达的元素称为活动对象,没有到达的元素称为垃圾数据。
- 标记-清除算法。
- 标记完以后直接清理
- 缺点:碎片过多会导致内存浪费。
- 标记-整理算法
- 标记完以后让所有存活的对象都向一段移动,直接清理掉端边界以外的内存。
- 新生代:副垃圾回收器
- 标记活动对象、非活动对象
- 回收非活动对象所占据的内存
- 内存整理,因为回收对象后,内存中存在大量不连续空间(内存碎片)。(主回收器有这一步)
全停顿:垃圾回收完毕后再恢复脚本执行
老生代执行垃圾回收时间较长
增量标记算法:把一个完整的垃圾回收任务拆分成很多小的任务。与正常任务穿插进行。
编译型语言:执行前需要经过编译器的编译过程,并且编译后保留机器能读懂的二进制文件,不需要重复编译。
- 源代码-AST-中间代码-二进制文件-执行
解释型语言:每次运行都需要通过解释器对程序进行动态解释和执行。
- 源代码-AST-字节码-执行 生成ast
- 分词(词法分析),将一行行的源码拆解成一个个的token,token指的是语法上最小的单个字符或字符串。
- 解析(语法分析),将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消除嵌套回调
- 产生嵌套函数的一个主要原因是在发起任务请求时会带上回调函数,这样当任务处理结束之后,下个任务就只能在回调函数中来处理了。
- promise实现了回调函数的延时绑定;回调函数的延时绑定在代码上体现就是先创建 Promise 对象 x1,通过 Promise 的构造函数 executor 来执行业务逻辑;创建好 Promise 对象 x1 之后,再使用 x1.then 来设置回调函数。
- 将回调函数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) }) })
这是CSS笔记 →