《古兰经》中有一句很契合的话,山不过来,我就过去。
既然,外部环境我们无法去改变,那就从我们内部改变。所以,我又重新总结了一套,2023
年最新的面试集锦,以便大家一起度过寒冬,拥抱更好的未来。
note:
- 其中有些知识点,在前面的文章中,有过涉猎,为了行文的方便和资料的完整性,我就又拿来主义了,免去大家去翻找。但是,前面的文章有更深的解读,如果想更深的学习,可以移步到对应文章中。
- 如果在行文中,有技术披露和考虑不周的地方,不吝赐教。
你能所学到的知识点
- JS执行流程 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- 基本数据类型 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- ES6的新特性有哪些 推荐阅读指数⭐️⭐️⭐️
- 箭头函数和普通函数的区别 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- Promise VS async/await 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- ES6迭代器 推荐阅读指数⭐️⭐️⭐️
- 设计模式的分类 推荐阅读指数⭐️⭐️⭐️⭐️
- WebGL和canvas的关系 推荐阅读指数⭐️⭐️
- CommonJS和ES6 Module的区别 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- 声明变量的方式 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- Object/Map/WeakMap的区别 推荐阅读指数⭐️⭐️⭐️⭐️
- JS 深浅复制 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- 闭包 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- Event Loop 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- 垃圾回收机制 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- 内存问题 推荐阅读指数⭐️⭐️⭐️
- 作用域的产生 推荐阅读指数⭐️⭐️⭐️⭐️
- this指向 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- 图片懒加载 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
- PromiseQueue 推荐阅读指数⭐️⭐️⭐️⭐️
- 数组常用方法 推荐阅读指数⭐️⭐️⭐️⭐️
好了,天不早了,干点正事哇。
JS执行流程
准备工作
需要准备执行 JS
时所需要的一些基础环境
- 初始化了内存中的
堆和栈
结构 - JS全局执行上下文
- 包含了执行过程中的全局信息, 比如一些
内置函数
,全局变量
等信息
- 包含了执行过程中的全局信息, 比如一些
- 全局作用域
- 包含了一些
全局变量
, 在执行过程中的数据都需要存放在内存中
- 包含了一些
- 初始化消息循环系统
- 消息驱动器
- 消息队列
执行流程
V8
接收到要执行的JS 源代码
源代码
对V8
来说只是一堆字符串,V8
并不能直接理解这段字符串的含义
V8
结构化这段字符串,生成了{抽象语法树|AST},同时还会生成相关的作用域- 生成字节码(介于
AST
和机器代码
的中间代码)- 与特定类型的机器代码无关
- 解释器(
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
,监视器
会把它发送到优化编译器中。生成一个更快速和高效的代码版本出来,并且存储之。
为了生成一个更快速的代码版本,优化编译器必须做一些假设。
例如,它会假设由同一个构造函数生成的实例都有相同的形状
就是说所有的实例
- 都有
相同的属性名
- 并且都以
同样的顺序初始化
那么就可以针对这一模式进行优化。
整个优化器
起作用的链条是这样的
- 监视器从他所监视代码的执行情况做出自己的判断
- 接下来把它所整理的信息传递给优化器进行优化
- 如果某个循环中先前每次迭代的对象都有相同的形状,那么就可以认为它以后迭代的对象的形状都是相同的。
可是对于 JavaScript
从来就没有保证这么一说,前 99 个对象保持着形状,可能第 100 个就少了某个属性。
正是由于这样的情况,所以编译代码需要在运行之前检查其假设是不是合理的。
- 如果合理,那么优化的编译代码会运行
- 如果不合理,那么
JIT
会认为做了一个错误的假设,并且把优化代码丢掉- 这时(发生优化代码丢弃的情况)执行过程将会回到解释器或者基线编译器,这一过程叫做去优化。
{类型特化|Type specialization}
优化编译器
最成功一个特点叫做类型特化。
JavaScript
所使用的动态类型体系在运行时需要进行额外的解释工作,例如下面代码:
function arraySum(arr) {
var sum = 0;
for (var i = 0; i < arr.length; i++) {
sum += arr[i];
}
}
我们假设 arr
是一个有 100 个整数的数组。当代码被标记为 “warm” 时,基线编译器就为函数中的每一个操作生成一个桩。sum += arr[i]
会有一个相应的桩,并且把里面的 +=
操作当成整数加法。
但是,sum
和 arr[i]
两个数并不保证都是整数。因为在 JavaScript
中类型都是动态类型,在接下来的循环当中,arr[i]
很有可能变成了 string
类型。整数加法和字符串连接是完全不同的两个操作,会被编译成不同的机器码。
JIT
处理这个问题的方法是编译多基线桩。
- 如果一个代码段是单一形态的(即总是以同一类型被调用),则只生成一个桩。
- 如果是多形态的(即调用的过程中,类型不断变化),则会为操作所调用的每一个类型组合生成一个桩。
这就是说 JIT
在选择一个桩之前,会进行多分枝选择,类似于决策树
,问自己很多问题才会确定最终选择哪个,见下图:
基本数据类型
数据类型分类(7+1)
undefined
null
Boolean
String
Number
Symbol
(es6
)BigInt
(es2020
)Object
- {常规对象|Ordinary Object}
- {异质对象|Exotic Object}
存储位置不同
- (1 - 7) :栈内存 (基本primary数据类型)
- (8): 堆内存
判断数据类型的方式 (TTIC)
-
typeof
- 判断基本数据类型
typeof null
特例,返回的是"object"
-
Object.prototype.toString.call(xx)
- 判断基本数据类型
- 实现原理:
- 若参数(xx)不为
null
或undefined
,则将参数转为对象,再作判断 - 转为对象后,取得该对象的
[Symbol.toStringTag]
属性值(可能会遍历原型链)作为tag
,然后返回"[object " + tag + "
- 若参数(xx)不为