Appearance
八股文
静态作用域和动态作用域
- 因为 JavaScript 采用的是词
法作用域
,函数的作用域在函数定义的时候就决定了
。而与词法作用域相对的是动态作用域
,函数的作用域是在函数调用的时候才决定的
。
执行上下文-变量对象
- 当 JavaScript 代码
执行一段可执行代码
(executable code)时,会创建对应的执行上下文
(execution context)。 对于每个执行上下文,都有三个重要属性: - 变量对象(Variable object,VO);
- 作用域链(Scope chain);
- this;
函数上下文
在函数上下文中,我们用
活动对象
(activation object, AO)来表示变量对象
。活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被
激活
,所以才叫 activation object,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。
- 全局上下文的变量对象初始化是全局对象;
- 函数上下文的变量对象初始化只包括 Arguments 对象;
- 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值;
- 在代码执行阶段,会再次修改变量对象的属性值;
js
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1);
// 在进入执行上下文后,这时候的 AO 是
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
// 当代码执行完后,这时候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
第一段会报错:Uncaught ReferenceError: a is not defined。 第二段会打印:1。 这是因为函数中的 "a" 并没有通过 var 关键字声明,所有不会被存放在 AO 中。
js
function foo() {
console.log(a);
a = 1;
}
// 第一段执行 console 的时候, AO 的值是:
AO = {
arguments: {
length: 0,
},
};
foo(); // Uncaught ReferenceError: a is not defined。
function bar() {
a = 1;
console.log(a); // AO中没有会去全局上下文中查找
}
bar(); // 1
作用域链
- 当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
js
function foo() {
function bar() {
...
}
}
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
];
执行分析
js
var scope = "global scope";
function checkscope() {
var scope = "local scope";
function f() {
return scope;
}
return f();
}
checkscope();
- 执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈
js
ECStack = [globalContext];
- 全局上下文初始化
js
globalContext = {
VO: [global],
Scope: [globalContext.VO],
this: globalContext.VO,
};
- 初始化的同时,checkscope 函数被创建,保存作用域链到函数的内部属性[[scope]]
js
checkscope.[[scope]] = [
globalContext.VO
];
- 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
js
ECStack = [checkscopeContext, globalContext];
- checkscope 函数执行上下文初始化:
js
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope: undefined,
f: reference to function f(){}
},
Scope: [AO, globalContext.VO],
this: undefined
}
- f 函数执行,沿着作用域链查找 scope 值,返回 scope 值;
- f 函数执行完毕,f 函数上下文从执行上下文栈中弹出;
js
ECStack = [checkscopeContext, globalContext];
- checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
js
ECStack = [globalContext];
ECMAScript 中所有函数的参数都是按值传递的。
- 把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。
- 函数外部的值复制给函数内部的参数,如果是基本类型的值,在函数内部修改参数值,将不会影响到函数外的值。
- 如果把引用类型的值复制给函数内部的参数,在函数内部修改参
引用(属性)
值,将影响到函数外的值。如果直接给内部参数直接赋值不会影响外部的值
js
var obj = {
value: 1,
};
function foo(o) {
o = 2;
console.log(o); //2
}
// 共享传递
foo(obj);
console.log(obj.value); // 1
js
Function.prototype.call2 = function (context, ...args) {
if (typeof context === "undefined" || context === null) {
context = window;
}
context.fn = this;
let data = context.fn(...args);
delete context.fn;
return data;
};
function fn(a, b) {
console.log(this.name, a, b);
}
let obj = {
name: "obj",
};
fn.call2(obj, 1, 2); // obj 1 2
js
Function.prototype.myBind = function (context) {
// 判断是否是undefined 和 null
if (typeof context === "undefined" || context === null) {
context = window;
}
self = this;
return function (...args) {
return self.apply(context, args);
};
};
js
function myNew(Fn){
let o = {};
o.__proto = Fn.prototype
let ret = Fn.call(o);
return typeof ret === 'object' ? ret : obj;
}
ajax vs fetch
fetch()的功能与 XMLHttpRequest 基本相同,但有三个差异:
- fetch使用 Promise,不使用回调函数,因此大大简化了写法,写起来更简洁;
- fetch采用模块化设计,API 分散在多个对象上(Response 对象、Request 对象、Headers 对象),更合理一些;相比之下,XMLHttpRequest 的 API 设计并不是很好,输入、输出、状态都在同一个接口管理,容易写出非常混乱的代码;
- fetch通过数据流(Stream 对象)处理数据,可以分块读取,有利于提高网站性能表现,减少内存占用,对于请求大文件或者网速慢的场景相当有用。XMLHTTPRequest 对象不支持数据流,所有的数据必须放在缓存里,不支持分块读取,必须等待全部拿到后,再一次性吐出来;
let 的底层实现过程
编译阶段 在代码编译阶段,编译器会扫描整个函数体(或全局作用域),查找所有使用 let 定义的变量,为这些变量生成一个初始值为 undefined 的词法环境(LexicalEnvironment)并将其保存在作用域链中。
进入执行上下文 当进入执行块级作用域(包括 for、if、while 和 switch 等语句块)后,会创建一个新的词法环境。如果执行块级作用域中包含 let 变量声明语句,这些变量将被添加到这个词法环境的环境记录中。
绑定变量值 运行到 let 定义的变量时,JavaScript 引擎会在当前词法环境中搜索该变量。首先在当前环境记录中找到这个变量,如果不存在变量,则向包含当前环境的外部环境记录搜索变量,直到全局作用域为止。如果变量值没有被绑定,JavaScript 引擎会将其绑定为 undefined,否则继续执行其他操作。
实现块级作用域 使用 let 定义变量时,在运行时不会在当前作用域之外创建单独的执行上下文,而是会创建子遮蔽(shadowing)新环境。在子遮蔽的词法环境中,变量的值只在最接近的块级作用域内有效。
垃圾回收机制(GC)
- JavaScript在创建变量的时候,会
自动分配内存
,内存在不使用的时候就被垃圾回收器自动回收
释放内存
引用计数
- 当一个变量每次被其他变量
引用时
,计数器就会加一
,每当被释放时计数器就减一
,如果计数器为零
就被自动回收释放内存
- 优点:引用计数算法的优点我们对比标记清除来看就会清晰很多,首先引用计数在引用值为 0 时,也就是在变成垃圾的那一刻就会被回收,所以它可以
立即回收垃圾
- 缺点:首先它需要一个计数器,而此计数器需要占很大的位置,因为我们也不知道被引用数量的上限,还有就是无法解决循环引用无法回收的问题,这也是最严重的
标记清除
- 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
- 然后从各个根对象开始遍历,把不是垃圾的节点改成1
- 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
- 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收
- 优点: 标记清除算法的优点只有一个,那就是实现比较简单,打标记也无非打与不打两种情况,这使得一位二进制位(0和1)就可以为其标记,非常简单
- 缺点: 标记清除算法有一个很大的缺点,就是在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了内存碎片
标记整理
- 整理清除出现的内存碎片
新生代、老生代
- 新生代:新创建的对象所在的代。由于许多对象在创建后不久就会变成垃圾,因此这个代的回收频率较高,回收速度也较快。
- 老生代:这包含了经过
多次新生代回收仍然存活的对象
。长时间存活的对象会进入这个代。老生代的回收频率较低,但可能会消耗更多时间。 - 分代式机制把一些新、小、存活时间短的对象作为新生代,采用一小块内存频率较高的快速清理,而一些大、老、存活时间长的对象作为老生代,使其很少接受检查,新老生代的回收机制及频率是不同的,可以说此机制的出现很大程度提高了垃圾回收机制的效率
什么是函数式编程
- 在JavaScript中,函数式编程是一种编程范式,它强调使用
纯函数
、不可变性
和函数作为一等公民
。
纯函数:纯函数在JavaScript中的概念与其他编程语言中相似。它们没有副作用
,对于相同的输入
始终产生相同的输出
。这有助于代码的可测试性
和可维护性
。
js
// 纯函数的例子
function add(a, b) {
return a + b;
}
不可变性: 函数式编程强调不可变数据,即一旦创建就不能被修改
。在JavaScript中,可以使用const声明常量,以确保变量不被重新赋值。也可以使用一些库(如Immutable.js)来实现不可变性。
js
// 不可变性的例子
const numbers = [1, 2, 3, 4];
// 使用不可变性的方式添加一个新元素
const newNumbers = [...numbers, 5];
高阶函数: JavaScript中的函数是一等公民,可以被传递给其他函数,也可以从其他函数中返回。高阶函数是接受一个或多个函数作为参数,并返回一个新函数的函数
js
// 高阶函数的例子
function multiplyBy(factor) {
return function (number) {
return number * factor;
};
}
const double = multiplyBy(2);
console.log(double(5)); // 输出 10
Map、Filter和Reduce: 这些是常见的函数式编程操作,用于对列表进行转换、过滤和折叠。
js
const numbers = [1, 2, 3, 4, 5];
// 使用map和filter进行转换和过滤
const squaredEvenNumbers = numbers
.filter(num => num % 2 === 0)
.map(num => num ** 2);
// 使用reduce进行折叠
const sum = squaredEvenNumbers.reduce((acc, curr) => acc + curr, 0);
console.log(sum); // 输出 20
发布订阅和观察者模式的区别?
发布-订阅模式(Pub/Sub)和观察者模式(Observer)都属于软件设计模式,它们用于实现对象之间的通信和协作,但有一些关键区别。
耦合度:
- 在发布-订阅模式中,发布者和订阅者之间是
解耦
的。发布者不需要知道订阅者的存在,反之亦然。消息的发布者只负责发布消息,而订阅者只负责订阅感兴趣的消息。 - 在观察者模式中,被观察者和观察者之间是
相互耦合
的。观察者需要直接注册到被观察者
上,被观察者
需要知道其观察者
的存在。
- 在发布-订阅模式中,发布者和订阅者之间是
通信方式:
- 发布-订阅模式中,发布者和订阅者通过
消息通道进行通信
。发布者发布消息到通道,订阅者从通道中接收消息。 - 观察者模式中,
被观察者
通过直接调用观察者的方法
来通知它们状态的变化。
- 发布-订阅模式中,发布者和订阅者通过
灵活性:
- 发布-订阅模式更为灵活,允许
动态地添加或移除订阅者
,而不影响系统的其他部分。 - 观察者模式的灵活性较差,因为
被观察者
和观察者
之间存在直接的关系
,添加或移除观察者可能需要修改被观察者
的代码。
- 发布-订阅模式更为灵活,允许
扩展性:
- 发布-订阅模式更容易扩展,可以支持多个发布者和多个订阅者,而且它们之间的关系是松散的。
- 观察者模式相对较难扩展,因为主题和观察者之间的关系是紧密耦合的。
JavaScript中函数参数到底是值传递还是引用传递?
- 函数的参数是
值传递
js
let obj ={a:1}
function test(a) {
// 这里a是值传递,修改a的值不会影响obj的值
a=null
}
test(obj)
console.log(obj) // {a: 1}
产生精度丢失问题?
- 1、小数运算
js
let result = 0.1 + 0.2; // 结果不是 0.3,而是一个近似值
- 2、整数运算溢出
js
let largeNumber = 9007199254740991;
let result = largeNumber + 1; // 结果不是 9007199254740992,而是近似的值
- 3、数值过大或过小
js
let largeNumber = 1e17;
let result = largeNumber + 1; // 结果近似于 largeNumber,失去了精度
那为什么0.1+0.1=0.2、0.1+0.2!=0.3?
0.1 的二进制表示并不是一个有限的小数,因此在计算机中以二进制浮点数表示时,
存在近似值
。当你执行 0.1 + 0.1 时,这个近似值与另一个 0.1 相加,导致了舍入误差
。这个误差通常很小,可能在小数点后很多位之后
。0.1 + 0.2 != 0.3,是因为 0.2 在二进制中也是一个无法精确表示的值,所以这
两个近似值的误差可能更加明显
。
为什么JavaScript里面typeof(null)的值是"object"
- 在JavaScript最初的实现中,值是由一个
表示类型的标签和实际数据值表示的
。对象的类型标签是 0
。由于 null 代表的是空指针(大多数平台下值为0x00
),因此,null 的类型标签是 0,typeof null 也因此返回 "object"。
js
typeof null === 'object';
WeakMap和Map的区别
- WeakMap是弱引用类型,当存储的对象被释放后,存储的值也会被垃圾回收器回收。
- Map是强引用类型,当存储的对象被释放后,存储的值不会被垃圾回收器回收。
js
let w = new WeakMap();
let m = new Map();
function fn() {
let obj = { name: 10 };
w.set(obj, obj);
m.set(obj, obj);
}
fn();
console.log(w); // WeakMap { <items unknown> }
console.log(m); // Map(1) { { name: 10 } => { name: 10 } }
小程序特殊的双线程架构
- 传统Web的架构模型是单线程架构,其渲染线程和脚本线程是互斥的,这也就是说为什么长时间的脚本运行可能会使页面失去响。
- 小程序能够具备更卓越用户体验的关键在于起架构模型有别于传统 Web,小程序为
双线程架构
,其渲染线程
和脚本线程
是分开
运行的。渲染层的界面使用了WebView
进行渲染,逻辑层采用JsCore
线程运行 JS 脚本。
对前端工程化的理解
- 前端工程化是一种系统化、规范化的前端开发方式,旨在提高开发效率、代码质量和项目可维扩性。其核心理念是将前端开发看作一个工程化过程,通过对前端开发流程、工具、规范等方面的优化,使得开发人员能够更加专注于业务逻辑的实现,而不是被繁琐的工作流程和技术细节所床扰。
- 1.模块化: 将前端代码按照功能划分为不同的模块,每个模块只关注自身的功能,可以独立开 发、测试、调试和部署。常见的模块化方案不全文CommonJS、AMD、ES6 模块等
- 2.规范化开发:制定前端代码的编写规范,包括代码风格、注释、命名规范、目录结构等,从而保证团队成员之间的代码风格统一,方便代码维护和交流。
什么是重绘与回流?
- 重绘:简单来说就是重新绘画,当给一个元素更换颜色、更换背景,虽然不会影响页面布局,但是颜色或背景变了,就会重新渲染页面,这就是重绘。
- 回流:当增加或删除dom节点,或者给元素修改宽高时,会改变页面布局,那么就会重新构造dom树然后再次进行渲染,这就是回流。
setTimeout 和 requestAnimationFrame 的区别
- setTimeout 属于
计时器
,会在指定的时间后执行回调函数,但是其中的回调函数会在主线程中执行
,会阻塞主线程的执行
,所以会造成页面的卡顿
。 - requestAnimationFrame 属于
动画帧
,会在下一帧中执行回调函数
,但是其中的回调函数会在浏览器的空闲时间
执行,不会阻塞主。另外当切换到后台时requestAnimationFrame会暂停不会触发执行。
script 引入方式
- html 静态 script 引入
- js 动态插入 script
script defer
- 1.浏览器开始解析 HTML 页面
- 2.遇到有 defer 属性的 script 标签,浏览器继续往下面解析页面,且会并行下载 script 标签的外部 js 文件
- 3.解析完 HTML 页面,再执行刚下载的 js 脚本(在 DOMContentLoaded 事件触发前执行,即刚刚解析完 html,且可保证执行顺序就是他们在页面上的先后顺序)
script sync
- 1.浏览器开始解析页面
- 2.遇到有 sync 属性的 script 标签,会继续往下解析,并且同时另开进程下载脚本
- 3.脚本下载完毕,浏览器停止解析,开始执行脚本,执行完毕后继续往下解析
使用场景区分
- 1.脚本之间没有依赖关系的,使用 sync
- 2.脚本之间有依赖关系的,使用 defer
- 3.若同时使用 sync 和 defer,defer 不起作用,sync 生效
内存泄露
- 前端内存泄漏是指在网页或应用程序中,由于代码错误或者其他原因,导致分配给页面或应用程序的内存无法被垃圾回收器回收。这会导致内存使用量不断增加,最后可能导致应用程序崩溃或者变得超级缓慢。内存泄漏问题一旦发生,不仅会对网页或应用程序的性能造成负面影响,还会影响用户的体验。
内存泄露场景
- 意外的全局变量: 无法被回收
- 定时器: 未被正确关闭,导致所引用的外部变量无法被释放
- 事件监听: 没有正确销毁 (低版本浏览器可能出现)
- 对象循环引用: 如果两个对象相互引用,且不存在其他对象对它们的引用,就会导致这两个对象无法被正常释放,从而导致内存泄漏。
- 比如在vue中,绑定这个定时器或者全局的滚动事件,组件在销毁的时候没有被释放掉,当多次重复使用该组件会导致多次的定时器绑定,会造成内存泄漏
- 闭包一般情况不会造成内存泄漏,但是闭包内的引用是无法被垃圾回收的。
如何分析内存泄露
- 1、可以通过Performance 面板进行录制,观察
js heap
是不是成阶梯式的增长
,且无法下降。 - 2、可以通过JS堆快照进行分析,分别看对比快照内存的占用情况
Vite为何启动快
- 在开发环境直接使用的ES6 Module,代码不需要编译
- 生产环境使用的rollup,并不会快很多
Composition API 和 React Hooks 对比
- Vue3中的setup 只会被调用一次,而React Hook函数会被多次调用
- Vue3中的setup无需顾虑调用顺序,而React Hook需要保证 hooks 的顺序一致
Vue vs React
Vue使用模版拥抱html
Vue 是真正意义上的做到了组件级更新,每一个组件就对应一个渲染的 effect,让响应式数据去收集 effect
Vue 采用的是递归的方式来渲染页面不可中断
React使用的JSX拥抱JS
React 更新都是从跟节点开始调度,会讲一个大的任务拆分成多个小的任务单元
React 更新策略是循环的方式,有任务优先级的概念,可中断执行
Map和Objecj区别
- API不同,Map可以任意类型为key
- Map是有序结构
Set和数组的区别
- API不同
- Set元素不能重复
- Set是无序的结构
Map、WeakMap
- 1、Map 是字典的数据结构
- 2、Map 的 key 可以是任意的值
- 3、WeakMap 的 key 只能是对象(弱引用)
js
let map = new WeakMap();
let a = {};
map.set(a, 1);
a = null; // map会被自动释放掉
module chunk bundle 的区别
- module -各个源码文件,webpack 中一切皆模块
- chunk -多模块合并成的,如entry importo splitChunk
- bundlle-最终的输出文件
ES6 Module 和 Commonjs 区别
- ES6 Module 静态引入,编译时引入
- commonjs 动态引入,执行时引入
- Module是导入值的引用,Commonjs是导入值的拷贝
- 只有 ES6 Module 才能静态分析,实现 Tree-Shaking
前端为何要进行打包和构建?
- 体积更小 (Tree-Shaking、压缩、合并),加载更快
- 编译高级语言或语法(TS,ES6+,模块化,sCSs)
- 兼容性和错误检查 ( Polyfill、postcss、 eslint)
正式面试时,要这样问
- 部门所做的产品和业务,产品的用户量、规模
- 部门有多少人,都有什么角色?
- 项目的技术栈,以及对接的其他技术团队
项目构建
- 代码仓库,发布到哪个npm 仓库(如有需要)
- 框架 vue React
- 代码目录规范
- 打包构建 webpack 等,做打包优化
- eslint prettier commit-lint
- pre-commit
- 单元测试
- CI/CD 流程
- 开发环境,预发布环境
- 开发文档
开发环境
- DEV development 开发
- SIT System Integrate Test 系统整合测试(内测)
- UAT User Acceptance Test 用户验收测试
- PET Performance Evaluation Test 性能评估测试(压测)
- IM simulation 仿真
- RD/PROD production 产品/正式/生产
forEach for in、 for of 的差异
- forEach:
break不能中断
循环执行,如果想终止循环,可以把数组的长度设置为0
,或者抛出异常 - for...in 遍历 key , for...of 遍历 value
- for...in 可以遍历对象,for...of 不可以
- for...of 可以遍历 Map/Set ,for...in 不可以
- for...of 可遍历 generator ,for...in 不可以
js
let arr = [1,2,3,4,5]
arr.filter(_=> {
console.log(_)
break // 不能中断循环执行
return // return不会中断下面代码的执行
})
for (const key in arr) {
console.log(key)
break // 可以中断循环执行
return // 可以中断循环执行,return后面的代码无法执行
}
for (const iterator of arr) {
console.log(iterator)
break // 可以中断循环执行
return // 可以中断循环执行,return后面的代码无法执行
}
数据类型undefined
、null
undefined
:变量声明为初始化具体的值null
:表示一个对象的空指针
递减操作符合
递减在前,先减在计算
js
let num1 = 2;
let num2 = 20;
let num3 = --num1 + num2;
let num4 = num1 + num2;
console.log(num3); // 21
console.log(num4); // 21
递减在后,先计算在减
js
let num1 = 2;
let num2 = 20;
let num3 = num1-- + num2;
let num4 = num1 + num2;
console.log(num3); // 22
console.log(num4); // 21