2周刷完100道后端高质量复试专业课
——————–
下栽の地止:https://www.itwangzi.cn/2596.html
——————–
01
JS继续执行业务流程
预备组织工作
须要预备继续执行 JS 而所须要的许多此基础自然环境
1. 调用了缓存中的堆和栈内部结构
2. JS全局继续执行上下文
包含了继续执行过程中的全局信息, 比如许多内置函数,全局变量等信息
3. 全局作用域
包含了许多全局变量,在继续执行过程中的数据都须要存放在缓存中
4. 调用消息循环系统
消息驱动器
消息队列
继续执行业务流程
1. V8 接收到要继续执行的 JS 源代码
源代码对 V8 来说只是一堆字符串,V8 并不能直接理解这段字符串的含义
2. V8内部结构化这段字符串,生成了{抽象语法树|AST}, 同时还会生成相关的作用域
3. 生成字节码(介于 AST 和机器代码的中间代码)
与特定类型的机器代码无关
4. 解释器(ignition),按照顺序解释继续执行字节码,并输出继续执行结果。
从图中得出一个结论:继续执行JS代码核心业务流程
先编译
后继续执行
通过V8将js转换为字节码然后经过解释器继续执行输出结果的方式继续执行JS,有一个弊端就是,如果在浏览器中再次打开相同的页面,当页面中的 JavaScript 文件没有被修改,再次编译之后的二进制代码也会保持不变,意味着编译这一步浪费了 CPU 资源。
为了,更好的利用CPU资源,V8采用JIT(Just In Time)技术提升效率: 而是混合编译继续执行和解释继续执行这两种手段。
解释继续执行的启动速度快,但是继续执行时的速度慢
编译继续执行的启动速度慢,但是继续执行时的速度快
Just-in-time 编译器:综合了解释器和编译器的优点
为了解决解释器的低效问题,后来的浏览器把编译器也引入进来,形成混合模式。
在 JavaScript 引擎中增加一个监视器(也叫分析器)。监视器监控着代码的运行情况,记录代码一共运行了多少次、如何运行的等信息。
如果同一行代码运行了几次,这个代码段就被标记成了 warm,如果运行了很多次,则被标记成 hot。
基线编译器
如果一段代码变成了 warm,那么 JIT 就把它送到编译器去编译,并且把编译结果存储起来。
代码段的每一行都会被编译成一个“桩”(stub),同时给这个桩分配一个以行号 + 变量类型的索引。如果监视器监视到了继续执行同样的代码和同样的变量类型,那么就直接把这个已编译的版本 push 出来给浏览器。
优化编译器
如果一个代码段变得 very hot,监视器会把它发送到优化编译器中。生成一个更快速和高效的代码版本出来,并且存储之。
为了生成一个更快速的代码版本,优化编译器必须做许多假设。
例如,它会假设由同一个构造函数生成的实例都有相同的形状
就是说所有的实例
1. 都有相同的属性名
2. 并且都以同样的顺序调用
那么就可以针对这一模式进行优化。
整个优化器起作用的链条是这样的
1. 监视器从他所监视代码的继续执行情况做出自己的判断
2. 接下来把它所整理的信息传递给优化器进行优化
3. 如果某个循环中先前每次迭代的对象都有相同的形状,那么就可以认为它以后迭代的对象的形状都是相同的。
可是对于 JavaScript 从来就没有保证这么一说,前 99 个对象保持着形状,可能第 100 个就少了某个属性。
正是由于这样的情况,所以编译代码须要在运行之前检查其假设是不是合理的。
如果合理,那么优化的编译代码会运行
如果不合理,那么 JIT 会认为做了一个错误的假设,并且把优化代码丢掉;这时(发生优化代码丢弃的情况)继续执行过程将会回到解释器或者基线编译器,这一过程叫做去优化。
{类型特化 | Type specialization}
优化编译器最成功一个特点叫做类型特化。
JavaScript 所使用的动态类型体系在运行时须要进行额外的解释组织工作,例如下面代码:
我们假设 arr 是一个有 100 个整数的数组。当代码被标记为 “warm” 时,基线编译器就为函数中的每一个操作生成一个桩。sum += arr[i] 会有一个相应的桩,并且把里面的 += 操作当成整数加法。
但是,sum 和 arr[i] 两个数并不保证都是整数。因为在 JavaScript 中类型都是动态类型,在接下来的循环当中,arr[i] 很有可能变成了 string 类型。整数加法和字符串连接是完全不同的两个操作,会被编译成不同的机器码。
JIT 处理这个问题的方法是编译多基线桩。
如果一个代码段是单一形态的(即总是以同一类型被调用),则只生成一个桩。
如果是多形态的(即调用的过程中,类型不断变化),则会为操作所调用的每一个类型组合生成一个桩。
这就是说 JIT 在选择一个桩之前,会进行多分枝选择,类似于决策树,问自己很多问题才会确定最终选择哪个,见下图:
02
基本数据类型
数据类型分类(7+1)
undefined
null
Boolean
String
Number
Symbol(es6)
BigInt(es2020)
Object
{常规对象|Ordinary Object}
{异质对象|Exotic Object}
存储位置不同
(1 – 7) :栈缓存 (基本primary数据类型)
(8): 堆缓存
判断数据类型的方式 (TTIC)
1. typeof
判断基本数据类型
typeof null 特例,返回的是”object”
2.Object.prototype.toString.call(xx)
判断基本数据类型
实现原理:
若参数(xx)不为 null 或 undefined,则将参数转为对象,再作判断
转为对象后,取得该对象的 [Symbol.toStringTag] 属性值(可能会遍历原型链)作为 tag,然后返回 “[object ” + tag + “]” 形式的字符串。
3. instanceof
a instanceof B判断的是 a 和 B 是否有血缘关系,而不是仅仅根据是否是父子关系。
在ES6中instanceof 操作符会使用 Symbol.hasInstance 函数来确定关系。
4. constructor
只要创建一个函数,就会按照特定的规则为这个函数创建一个 prototype 属性(指向原型对象)。
默认情况下,所有原型对象自动获得一个名为 constructor 的属性,指回与之关联的构造函数。
每次调用构造函数创建一个新实例,实例的内部[[Prototype]]指针就会被赋值为构造函数的原型对象。
实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有
通过实例和构造函数原型对象的关系,来判断是否实例类型。
null/undefined是一个假值,没有对应包装对象(无法进行装箱操作),也不是任何构造函数的实例。所以,不存在原型,即,无法使用 constructor 判断类型。
03
ES6的新特性有哪些
const 和 let
解构赋值
模板字符串
函数的扩展
函数的默认值、rest参数、函头函数
数组的扩展
– Array.from() 将类数组转为数组
– find()、findIndex() 找出第一个符合条件的成员/下标
– entries()、keys()、values() 用于遍历数组。(配合for…of)
– includes() 是否存在指定无素(返回布尔值)
对象的扩展
– 属性名可使用表达式
– Object.assign()
– Object.keys(), Object.values(), Object.entries()
Symbol
Set、Map
Promise
Iterator和for…of
– 为各种数据提供统一的,简便的访问接口
Generator与async await
04
箭头函数和普通函数的区别
1. 语法更加简洁、清晰
2. 箭头函数没有 prototype (原型),所以箭头函数本身没有this
3. 箭头函数不会创建自己的this箭头函数没有自己的this,箭头函数的this指向在定义的时候继承自外层第一个普通函数的this
4.call | apply | bind 无法改变箭头函数中this的指向
5. 箭头函数不能作为构造函数使用
6. 箭头函数不绑定arguments,取而代之用rest参数…代替arguments对象,来访问箭头函数的参数列表
7.箭头函数不能用作Generator函数,不能使用yield关键字
05
Promise VS async/await
Promise
Promise 对象就是为了解决回调地狱而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。
分析 Promise 的调用业务流程:
Promise 的构造方法接收一个executor(),在new Promise()时就立刻继续执行这个 executor 回调
executor()内部的异步任务被放入宏/微任务队列,等待继续执行
then()被继续执行,收集成功/失败回调,放入成功/失败队列
executor() 的异步任务被继续执行,触发resolve/reject,从成功/失败队列中取出回调依次继续执行
其实熟悉设计模式,很容易就能意识到这是个观察者模式,这种
收集依赖
触发通知
取出依赖继续执行
的方式,被广泛运用于观察者模式的实现
在 Promise 里,继续执行顺序是
1. then收集依赖
2. 异步触发resolve
3. resolve继续执行依赖
手写一个Promise
代码测试
async await
async/await 实际上是对 Generator(生成器)的封装,是一个语法糖。
*/yield 和 async/await 看起来其实已经很相似了,它们都提供了暂停继续执行的功能,但二者又有三点不同:
async/await自带继续执行器,不须要手动调用 next()就能自动继续执行下一步
async 函数返回值是 Promise 对象,而 Generator 返回的是生成器对象
await 能够返回 Promise 的 resolve/reject 的值
不管await后面跟着的是什么,await都会阻塞后面的代码
Generator
Generator 实现的核心在于上下文的保存,函数并没有真的被挂起,每一次 yield,其实都继续执行了一遍传入的生成器函数,只是在这个过程中间用了一个 context 对象储存上下文,使得每次继续执行生成器函数的时候,都可以从上一个继续执行结果开始继续执行,看起来就像函数被挂起了一样。
用babel编译后生成regeneratorRuntime
mark()方法为生成器函数绑定了一系列原型
wrap()相当于是给 generator 增加了一个_invoke 方法
两者的区别
Promise的出现解决了传统callback函数导致的地域回调问题,但它的语法导致了它向纵向发展行成了一个回调链,遇到复杂的业务场景,这样的语法显然也是不美观的。
而async await代码看起来会简洁些,使得异步代码看起来像同步代码,await的本质是可以提供等同于”同步效果“的等待异步返回能力的语法糖,只有这一句代码继续执行完,才会继续执行下一句。
async/await与Promise一样,是非阻塞的。
async/await是基于Promise实现的,可以说是改良版的Promise,它不能用于普通的回调函数。
06
ES6迭代器
迭代器模式
可以把有些内部结构称为{可迭代对象|iterable},它们实现了正式的 Iterable 接口
而且可以通过{迭代器|Iterator}消费
{迭代器|Iterator}是按需创建的一次性对象
每个迭代器都会关联一个可迭代对象
可迭代协议
实现 Iterable 接口(可迭代协议)要求同时具备两种能力
1 支持迭代的自我识别能力
2 创建实现 Iterator 接口的对象的能力
这意味着必须暴露一个属性作为默认迭代器,这个属性必须使用特殊的 Symbol.iterator 作为键,这个默认迭代器属性必须引用一个迭代器工厂函数。调用这个工厂函数必须返回一个新迭代器
内置类型都实现了 Iterable 接口
字符串
数组
Map
Set
arguments 对象
NodeList 等 DOM 集合类型
接收可迭代对象的原生语言特性包括
for-of 循环
数组解构
扩展操作符
Array.from()
创建Set
创建Map
Promise.all()接收由Promise组成的可迭代对象
Promise.race()接收由Promise组成的可迭代对象
yield*操作符,在生成器中使用
迭代器协议
迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象
迭代器 API 使用 next()方法在可迭代对象中遍历数据,每次成功调用 next(),都会返回一个 IteratorResult 对象,其中包含迭代器返回的下一个值。
next()方法返回的迭代器对象 IteratorResult 包含两个属性
done
一个布尔值,表示是否还可以再次调用 next()取得下一个值
value
包含可迭代对象的下一个值
每个迭代器都表示对可迭代对象的一次性有序遍历
手写一个迭代器
代码测试