Account 简答类
HTML丨CSS
DOCTYPE(⽂档类型)有什么用
DOCTYPE 声明在 HTML 的第一行,用来告诉浏览器用什么样的文档标准去解析代码。在默认的标准模式下,浏览器用 W3C 的标准来解析;而在怪异模式下,浏览器用向后兼容的方式来解析。
对 HTML 语义化的理解
HTML 语义化就是用语义化的标签去展示内容,例如 header、nav、footer 这些,而不是什么都用 div 去展示,这样可以增强代码的可读性,也有利于 SEO。
对 SEO 的理解
SEO 即搜索引擎优化,用于提升网站在搜索引擎的排序和收录数。可通过语义化 HTML 标签、合理设置 meta 标签的 keywords、description 属性和完善图片的 alt 属性等方式来进行 SEO 优化。
🍀 src 和 href 的区别
src 的解析会阻塞页面,而 href 的解析是并行加载的,不会阻塞页面。
link 和 @import 的区别
link 是 HTML 标签,用于引入样式和图片等资源,与页面同时载入;而 @import 是 CSS 语法,只能引入样式文件,并且在页面加载完才载入。
img 标签 srcset 属性的作⽤
根据屏幕分辨率自动切换不同尺寸的图片。
Canvas 和 SVG 的区别
Canvas 基于像素,每次绘制都会直接操作像素,适合做游戏等需要动态渲染的场景;而 SVG 基于矢量,放大不失真,适合做图标等可缩放图形。
对 Web Components 的理解
Web Components 可以理解为自定义的 HTML 标签,通过 Shadow DOM 封装独立样式和逻辑,避免了全局污染,而且支持跨项目使用。
🍀 说说 BEM 命名规范
BEM 命名规范是以组件化的思想来进行命名,分别用模块、元素和修饰符进行区分,并用双下划线和双横杆拼接,以提高类名的可维护性。
getElement 和 querySelector 的区别
getElement 返回的是动态节点,会随着文档的操作而改变;而 querySelector 返回的是静态节点,不会随着文档的操作发生改变。
iframe 的优缺点
iframe 用来嵌入加载较慢的内容,实现方便,但不利于 SEO。
对 sticky 粘性定位的理解
sticky 即粘性定位,当页面滚动未超出目标区域时相当于 position:relative,当超出目标区域时相当于 position:fixed。
为什么没有父级选择器
因为 CSS 的解析是自上而下的,无法回溯父元素,而且父级选择器会破坏渲染顺序,增加样式计算复杂度,影响性能,所以 CSS 不支持。
对盒子模型的理解
盒子模型可以看作是一个容器,包含自身的内容、内边距、边框和外边距;默认为标准模型,当设置为 border-box IE 模型时,边框和内边距不会将盒子撑大。
如何获取盒子模型宽高
样式设置的宽高可通过 dom.style.width 或 window.getComputedStyle(dom).width 来获取,其中 dom.style.width 只能获取行内样式的宽高;另外,元素的实际宽高可通过 dom.offsetWidth 或 dom.getBoundingClientRect().width 来获取;还有一个 dom.clientWidth 用于获取元素实际宽高减去边框后的值;
怎么理解层叠上下文
元素一旦有了层叠上下文,其层级会比普通元素高,而在层叠上下文内部,元素的层级受制于外部的层叠上下文。
怎么理解 BFC
BFC 即块级格式化上下文,是一个独立的渲染区域,创建 BFC 的元素内部无论如何布局都不会影响到外面的元素,可用来清除浮动和消除边距重叠,创建方式有设置 overflow、设置浮动或设置 display 为 inline-block 等等。
如何清除浮动
可给父元素设置 clearfix,也可以直接设置 overflow:hidden 或设置 display 为 flow-root。
display:none 与 visibility:hidden 的区别
设置 display:none 的元素不会渲染也不占空间;而设置 visibility:hidden 的元素虽然不可见但是会渲染并占用空间,而且子元素的 visibility 如果设为 visible 可以单独显示。
CSS 中 clip-path 属性的作用
clip-path 用于把图片裁剪成指定形状。例如设置 circle 为 50% 可以实现圆形头像。
background-size: cover 和 contain 区别
cover 是等比例填满容器,超出部分会被裁剪;而 contain 是等比例适应容器,可能会留白。
了解过 background-blend-mode 吗
background-blend-mode 用来混合多个背景图片或颜色,可以实现正片叠底和叠加等滤镜效果。
flex:1 写法的原理
flex 是 flex-grow、flex-shrink 和 flex-basis 的缩写,其中 flex-grow 是元素的相对扩展量,flex-shrink 是元素的相对收缩量,flex-basis 则是元素在主轴的初始大小;而 flex:1 等价于设置 flex-grow:1、flex-shrink:1 和 flex-basis:0。
IE 不兼容哪些 CSS3 属性,怎么解决
不兼容 Flex 和 Grid 布局,还有圆角和阴影等属性,可以通过 Autoprefixer 工具来解决。
渐进增强和优雅降级是什么
渐进增强是先保证基础功能,再逐步添加高级效果;而优雅降级是先做全功能,再给低版本浏览器做兼容。
如何用 CSS 绘制平行四边形
可以使用 transform 的 skew 斜切属性来实现。
如何用 CSS 绘制下拉尖角
可以通过 border 属性来实现下拉尖角,例如要实现一个尖角朝下的图标,可以设置 border-left 和 border-right 为透明,而 border-top 带颜色,以此类推实现各个方向的尖角。
如何实现瀑布流布局
定义一个共同的父元素,并设置 column-count(分多少列) 和 column-gap(列与列之间的间距)
列举左右定宽,中间自适应三栏布局的方式,哪种方式比较好
- 第一种是通过浮动,给左边设置
float: left,给右边设置float: right; - 第二种是通过绝对定位,都设置
position: absolute,给左边设置left: 0,给右边设置right: 0,然后给中间栏的left和right设为左右两边的宽度; - 第三种是通过弹性布局,给共同父元素设置
display:flex,然后给中间栏设置flex: 1; - 第四种是通过表格布局,给共同父元素设置宽高和
display:table,然后给三边设置display: table-cell; - 第五种是通过网格布局,给共同父元素设置
display:grid和grid-template-columns。
上述方式中 display:flex 和 display:table 的方式比较好;因为这两种方式在增加高度时可以将父容器正常撑开。
🍀 Less 与 Sass 的区别
在用法上 Less 用 @ 符号声明变量,而 Sass 用 $ 符号;在编译上,Less 基于 JS,支持运行时编译,而 Sass 基于 Ruby,功能更强但编译会比较慢。
JS〡ES6+〡TS〡库
JS 为什么是单线程的
因为 JS 是为了操作 DOM 而诞生,如果是多线程的话,可能会出现两个线程同时操作一个 DOM 的情况,例如在一个线程中修改 DOM,在另一个线程中删除这个 DOM,这时浏览器就需要决定哪个线程生效,为了避免这种冲突,JS 被设计成单线程。
JS 编译流程是怎样的
首先读取 JS 文件中的字符流,然后通过词法解析生成 Token,再通过语法解析将 Token 转为 AST 抽象语法树,同时验证语法,抛出语法错误,最后生成机器码执行。
对 AST 抽象语法树的理解
AST 抽象语法树是用树状的形式表现源代码的语法结构,可通过 Babel 来进行 JS 与抽象语法树的转换,添加相应的功能。
⚡️ 🍀 JS 引擎如何优化代码
可以通过 JIT 即时编译,把 JS 转为机器码执行;也可以通过内联缓存,优化对象的属性访问速度。
JS 的严格模式是什么
JS 的严格模式用来消除不严谨的语法,减少怪异行为,提升编译效率,具体表现在禁用 with 语句、对象不能有重名属性等。
基本类型有哪些
null、undefined、number、string、boolean、symbol、BigInt
引用类型有哪些
Array、Object、function 等。
基本类型和引用类型的区别
- 基本类型的值不可变,而引用类型的内容可变;
- 基本类型保存在栈中,而引用类型指针保存在栈中,内容保存在堆中;
- 基本类型的比较是判断是否相等,而引用类型的比较是判断是否指向同一对象;
- 基本类型的复制是复制它的值,而引用类型的复制是改变引用的指针,仍指向同一对象。
null 和 undefined 的区别
null 表示空值,而 undefined 表示变量声明了但未赋值。
为什么 const 声明的引用类型数据可以修改
const 保证的是变量指向的内存地址不变。基本类型的值保存在内存地址中,因此 const 声明基本类型等同于声明常量。而引用类型变量指向的内存地址是个指针,const 只保证这个指针不变,而无法保证指针指向的内容不变,因此 const 声明的引用类型可以修改。
检验数据类型的方法
typeof、instanceof、constructor、严格运算符===、Object.prototype.toString.call()。
typeof 可以判断哪些类型,有什么局限
typeof 基本类型可以判断 undefined、number、string、boolean、symbol,而引用类型除了 function,其它的判断结果都是 object,另外 typeof null 的结果也是 object(这是 JS 的历史遗留问题,跟机器码有关)
typeof NaN 的结果
返回 'number'。
isNaN 和 Number.isNaN 的区别
isNaN 用于判断是否无法通过 Number 方法转为数字;而 Number.isNaN 用于来判断参数是否严格等于 NaN。
instanceof 和 constructor 的区别
instanceof 通过原型链来判断一个对象是否为另一个对象的实例,只要在当前实例的原型链上都返回 true,无法判断是否为创建该实例的构造函数;而 constructor 可以,它是直接返回创建该实例的构造函数。
如何判断是否为数组
- 通过
Array.isArray()来判断(返回 true) - 通过
instanceof Array来判断(返回 true) - 通过
Object.prototype.toString.call()来判断(返回 [object Array])
哪些数组 API 会改变原数组,哪些不会
改变原数组的 API 有:
- push()
- pop()
- shift()
- unshift()
- splice()
- sort()
- reverse()
不改变原数组的 API 有:
- map()
- filter()
- some()
- every()
- slice()
- concat()
为什么 JS 数组不是真正的数组
传统的数组是由相同类型元素组成的连续内存。而 JS 的数组不一样,如果数组成员类型一致则分配连续内存,如果不一致则分配非连续内存。
为什么 JS 数组可以保存不同类型的值
Chrome V8 引擎中的 JSArray 继承自 JSObject,这意味着 JS 中数组是一个特殊的对象,内部能以键值对的形式存储数据,也就可以存放不同类型的值。
JS 中数组是如何存储的
JS 数组包括快数组和慢数组两种存储方式,快数组牺牲空间换取时间,申请大块连续内存以提高效率;而慢数组则牺牲时间换取空间,以哈希表的方式存储数据,不需要申请连续的空间,虽然节约内存但性能不如快数组。JS 会根据合理使用内存的原则来决定使用哪种数组。
列举强制类型转换和隐式类型转换
- 强制类型转换有:Number、parseInt 和 toString 等;
- 隐式类型转换有:加减乘除运算符、if 语句和 == 等。
== 和 === 的区别
== 会先进行类型转换再做比较,而 === 是直接比较值和类型是否严格相等。
为什么 0.1 + 0.2 !== 0.3,怎么解决
因为计算机通过二进制的方式存储数据,计算 0.1 + 0.2 其实是计算两个数的二进制之和,由于存储的位数有限(64位),在计算过程中会出现二进制的舍入操作,最后再转为十进制时就造成了计算误差。
可通过 toPrecision 控制精度,或 Number.EPSILON 误差范围判断来解决这个问题。
Object.is 与 === 的区别
Object.is 与 === 一样,都用来比较两个值是否严格相等。区别在于 === 比较 +0 和 -0 会返回 true,而 Object.is 会返回 false;另外,=== 比较两个 NaN 会返回 false,而 Object.is 会返回 true。
Object.is 的原理
Object.is 比较 +0 和 -0 会返回 false,原理是利用正无穷不等于负无穷的特性,将 +0 和 -0 转为正无穷和负无穷,使结果返回 false;另外,Object.is 比较两个 NaN 会返回 true,原理是利用 NaN 不等于 NaN 的特性,让两个值分别和自身比较,使结果返回 true。
function _is(x, y) {
if (x === y) {
// 使 +0 和 -0 返回 false
// 1/+0 = +Infinity 1/-0 = -Infinity -> +Infinity !== -Infinity
return x !== 0 || 1 / x === 1 / y;
}
// 使两个 NaN 返回 true
// 一个变量不等于自身变量,那么它一定是 NaN -> x 和 y 都是 NaN 时返回 true
return x !== x && y !== y;
};
var、let 和 const 的区别
- var 存在变量提升,可重复声明;而 let 和 const 不存在变量提升,且不能重复声明。
- let 和 const 存在块级作用域,而 var 不存在;
变量提升是什么
JS 变量提升指的是变量和函数声明会被提升到当前作用域顶部,可以在声明之前被访问。
暂时性死区是什么
暂时性死区(TDZ)指的是 let 和 const 声明变量之前的区域,由于 let 和 const 不存在变量提升,在声明前访问会报错,这样设计是为了避免变量提升导致的意外行为。
执行上下文和执行栈的区别
执行上下文指的是代码执行时的环境,在函数执行前创建;而执行栈用于管理执行上下文,是存储函数调用的栈结构,遵循先进后出的原则。
⚡️ 🍀 执行上下文的生命周期
执行上下文包括创建、执行和回收三个阶段。首先在创建阶段创建变量和进行变量提升,并创建作用域链和确定 this 指向;然后在执行阶段对变量进行赋值,并执行代码;最后在回收阶段等待回收。
作用域和执行上下文的区别
作用域指的是变量的可访问范围,在函数定义时就已经确定,不会发生改变;而执行上下文指的是代码执行时的环境,在函数执行前创建,由于 this 指向在执行时才确定,因此可能产生不同的执行上下文。
作用域和作用域链是什么
作用域指的是变量的可访问范围,分为全局作用域和局部作用域。而作用域链指的是向父级作用域逐层查找变量的链式结构。
什么是自由变量
自由变量是没在当前作用域中定义的变量,需要向父级作用域去获取。
🍀 什么是闭包
闭包是一种编程技术,它允许函数访问另一个作用域中的变量,并并将这些变量始终保存在内存中,不被垃圾回收机制处理,在下次执行时仍然可以访问这些变量。虽然可以避免全局变量污染,但也产生了更大的内存消耗,而且可能导致内存泄漏。
对内存泄漏的理解
内存泄漏指的是程序中不再需要的内存没有及时释放,导致可用内存越来越少的现象。例如:
- 全局变量会导致内存泄漏,可以使用严格模式来解决;
- 滥用闭包也会导致内存泄漏,因此要减少闭包的使用;
- 没有被清除的定时器也会导致内存泄漏,需要手动清除。
🍀 对垃圾回收机制的理解
V8 的垃圾回收机制采用分代式垃圾回收,将内存分为新生代和老生代,新生代使用 Scavenge 算法快速回收,而老生代采用标记清除、标记整理和增量更新算法,结合并发和并行回收机制,提升效率和性能。
如何正确判断 this
- 普通函数的 this 指向 window;
- 作为对象属性执行的函数,this 指向上级对象;
- 构造函数的 this 指向 new 调用的实例;
- 通过
call()、apply()、bind()等方法绑定的函数,this 指代第一个绑定的函数参数; - 而箭头函数的 this 在定义时就已经确定,继承于上下文的 this,由于没有自己的 this,因此不能用
call()、apply()、bind()等方法改变 this 指向。
🍀 call()、apply()、bind() 的区别
call()、apply()、bind() 都是用来改变函数的 this 指向,但 call 和 apply 会直接执行修改后的函数,而 bind 是返回修改后未执行的函数;另外,apply 是用数组的形式来接收参数,而 call 和 bind 是逐个传入。
🍀 callee 和 caller 区别
callee 是 arguments 的一个属性,指向拥有该 arguments 对象的函数;而 caller 指的是谁在调用当前函数。
function a() {
console.log(a.caller)
}
function b() {
a();
}
b(); // ƒ b() { a(); }
箭头函数与普通函数区别
箭头函数书写更简洁,但不能用作构造函数,也就不能被 new 调用;而且没有 arguments 对象;另外箭头函数没有自己的 this,因此不能用 call()、apply()、bind() 等方法改变 this 指向。
箭头函数 this 怎么判断
箭头函数的 this 在定义时就已经确定,继承于上下文的 this,由于没有自己的 this,因此不能用 call()、apply()、bind() 等方法改变 this 指向。
创建对象的方法
可以通过字面量的方式创建;也可以通过 new 或 Object.create() 的方式创建;还可以通过构造函数的方式创建。
⚡️ 🍀 new 运算符的原理
new 的原理是创建一个新对象,并继承原构造函数的原型,然后将 this 指向新的实例,最后返回该对象;另外,由于箭头函数没有原型和自己的 this,所以不能 new 一个箭头函数。
⚡️ 🍀 Object.create() 原理
Object.create() 的原理是创建一个新对象,将第一个参数作为新对象的隐式原型,将第二个参数的对象属性复制到新对象中,最后返回这个新对象。
⚡️ 🍀 new 和 Object.create() 的区别
new 和 Object.create() 都是创建对象,但 new 创建的对象会保留原构造函数的属性,而 Object.create() 不会;另外 new 创建的对象隐式原型指向原构造函数的原型,而 Object.create() 创建的对象隐式原型指向传入的参数对象本身。
🍀 forEach、for in 和 for of 的区别
- forEach 用来遍历数组,但不能使用 break 和 return;
- for in 用来遍历数组或对象的 key 值,可以使用 break,但不能使用 return;
- for of 用来遍历数组、Set 和 Map 等可迭代对象,可以使用 break 和 return。
for in、Object.keys 和 Object.getOwnProperty 的区别
- for in 用于遍历对象的自有属性和继承自原型的属性;
- Object.keys 用于返回对象自有属性组成的数组;
- Object.getOwnProperty 也是用于返回对象自有属性组成的数组,但包括了不可枚举属性。
⚡️ 🍀 for of 和 for await of 的区别
for of 用来遍历数组、Set 和 Map 等可迭代对象,而 for await of 用于遍历 Promise 等异步的可迭代对象,可以用同步的方式处理异步数据流。
⚡️ 🍀 for of 如何遍历普通对象
for..of 遍历普通对象会报错,可以给对象添加一个 [Symbol.iterator] 属性并指向一个迭代器来解决。
// 方法一
const obj = {
a: 1,
b: 2,
c: 3
};
obj[Symbol.iterator] = function () {
const keys = Object.keys(this);
let count = 0;
return {
next() {
if (count < keys.length) {
return {
value: obj[keys[count++]],
done: false
};
} else {
return {
value: undefined,
done: true
};
}
}
}
};
for (let val of obj) {
console.log(val);
}
// 依次输出 1 2 3
// 方法二
const obj = {
a: 1,
b: 2,
c: 3
};
obj[Symbol.iterator] = function* () {
const keys = Object.keys(obj);
for (let k of keys) {
yield obj[k]
}
};
for (let val of obj) {
console.log(val);
}
// 依次输出 1 2 3
JSON 如何进行转换
JSON.stringify()将 JS 对象序列化为 JSON 字符串;JSON.parse()将 JSON 字符串反序列化成 JS 对象。
浅拷贝和深拷贝的区别
浅拷贝是增加一个指针指向原有的地址,与原对象会相互影响,可通过 Object.assign()、扩展运算符等方式实现浅拷贝;而深拷贝是增加一个指针指向申请的新地址,与原对象互不影响,可通过封装一个递归函数或使用 lodash 的 _.cloneDeep 方法来实现深拷贝。
使用 JSON.stringify 实现深拷贝有什么问题
JSON.stringify 实现深拷贝在序列化时会产生误差,例如时间对象会转为字符串,而且正则、error 对象和函数会丢失结果。
扩展运算符是浅拷贝还是深拷贝
扩展运算符在第一层是深拷贝,从第二层开始是浅拷贝。
什么是原型和原型链
每个构造函数都有一个 prototype 属性,这个 prototype 属性对应的值就是原型,包含了该构造函数的属性和方法;对象读取属性时,会先在自身查找,如果找不到就沿着 __proto__ 这条链逐层向上查找,形成原型链。
JS 实现继承的方式有哪些
可通过构造函数实现继承,也可以通过原型链实现继承,还可以通过 ES6 class extends 实现继承。
- 构造函数实现继承
- 原型链实现继承
- class extends 实现继承
function City() {
this.city = 'Guangzhou'
}
function District(name) {
this.name = name
City.call(this)
}
const district1 = new District('Haizhu') // {name: 'Haizhu'}
function City() {
this.city = 'Guangzhou'
}
function District(name) {
this.name = name
}
District.prototype = new City()
const district1 = new District('Haizhu') // {name: 'Haizhu'}
class City {
constructor(name) {
this.city = 'Guangzhou'
this.district = name
}
}
class District extends City {
constructor(name) {
super(name);
}
}
const district1 = new District("Haizhu") // {name: 'Haizhu'}
🍀 JS 各继承方式的区别
通过原型链继承的子类,无法向父类传递参数,而通过构造函数和 class 继承的子类可以;另外,通过原型链继承的话,引用类型属性会被所有实例共享,而通过构造函数和 class 继承则不会。
ES6 Class 继承中 super 方法的作用
class 继承会将父类的属性和方法加到 this 上,子类需要通过 super 方法来拿到这个 this,从而实现继承。
对 eval 语句的理解
eval 用于将字符串解析为 JS 代码执行,会破坏作用域,有安全风险。
⚡️ 🍀 对 with 语句的理解
with 用于将作用域设置到指定对象中,非常耗性能而且会增加调试成本。
对函数式编程的理解
函数式编程是一种编程范式,通过封装和组合各个函数来计算得到结果,而且过程中不会产生副作用,复用时不需要考虑它的内部实现和外部影响,提高了代码的复用性。
什么是纯函数
纯函数指的是没有副作用的函数,相同的输入永远会返回相同的输出。
什么是副作用
副作用指的是函数的执行改变了函数外部的状态,例如改变了全局变量、文件或数据库等行为。
⚡️ 🍀 对柯里化的理解
柯里化是一种函数式编程技术,将多参函数转为只有一个参数的函数,调用时返回一个新函数去处理剩下的参数;本质是用闭包把参数保存起来,当参数足够多时开始执行函数。可以更好的处理复杂函数,提高了代码的可读性。
function curry(fn, ...args) {
if (args.length >= fn.length) {
return fn(...args)
}
return (...args2) => curry(fn, ...args, ...args2);
}
const add = (x, y) => {
console.log(x + y)
}
const curriedAdd = curry(add)
curriedAdd(1)(2) // 3
curriedAdd(1, 2) // 3
onclick 和 addEventListener 的区别
onclick 只能绑定一个事件,重复绑定时只取最后一次事件绑定,而 addEventListener 可以绑定多个事件。
⚡️ 🍀 对事件流的理解
事件流指的是页面中接收事件的顺序,包括事件捕获阶段、目标阶段和事件冒泡阶段。
描述事件捕获具体流程
从 Window 开始,到 document、html、body,逐层向下捕获,最后到达目标元素。
什么是事件冒泡,如何阻止
事件冒泡指的是,事件开始时由具体元素接收,然后逐级向上传播到父元素,可通过 event.stopPropagation() 来阻止事件冒泡。
对事件委托(代理)的理解
事件委托(事件代理)就是利用事件冒泡,给父元素添加侦听器,统一处理子元素的事件,这样就不用给每个子元素都绑定事件,可以减少内存上的消耗,而且新建的子元素也会交给父元素中的侦听器来处理事件。因此,当要给一组元素添加相同事件时,就可以进行事件委托。
// 给父层元素绑定事件
document.getElementById('list').addEventListener('click', function (e) {
if (e.target.nodeName.toLocaleLowerCase === 'li') {
console.log('the content is: ', target.innerHTML);
}
});
target 和 currenttarget 的区别
target 是指触发事件的元素,而 currenttarget 是指绑定事件的元素。
同步和异步的区别
同步任务强调顺序性,会阻塞后续代码执行;而异步任务不会阻塞后续代码的执行。
对 Event Loop 事件循环的理解
JS 的运行过程中,遇到同步任务时会直接执行,而遇到异步(宏/微)任务时,会先放入(宏/微)任务队列,等同步任务执行完再执行任务队列的异步任务,主线程不断重复这个过程,就是 Event Loop 事件循环。
对宏任务和微任务的理解
异步任务也包括宏任务和微任务,在 JS 的运行过程中,遇到宏任务和微任务都会先放入任务队列,等同步任务执行完,微任务会先被调入主线程中执行,然后才是宏任务。常见的宏任务包括 setTimeout、setInterval、I/O 操作等,而常见的微任务包括 Promise、MutationObserver 和 Node.js 的 process.nextTick 等等。
🍀 Node 的 Event Loop 和浏览器的区别
浏览器中,微任务会在事件循环的宏任务结束后交替执行;而 Node.js 的 Event Loop 基于 Libuv 实现,微任务会在事件循环的各个阶段之间执行,可以更好地处理异步任务,而且微任务中的 process.nextTick 优先级更高。
⚡️ 🍀 对 Generator 语法的理解
Generator 用来控制循环流程,以解决异步编程嵌套层级较深的问题。可通过一个 * 号配合 yield 语句来构造生成器对象,然后调用 next 来控制循环。
🍀 Promise 的实现原理是什么
Promise 的本质是个状态机,包括 pending、fulfilled 和 rejected 三种状态,状态一旦改变就不可逆。Promise 的实现基于发布订阅模式,其中 then 方法会把回调存起来,当状态变为 fulfilled 或 rejected 时依次执行这些回调;而且这些回调会被放到微任务队列中,等同步任务执行完再执行。
🍀 Promise.all() 的实现原理
首先接收 Promise 数组,同时用一个计数器来记录完成情况,等待 Promise 数组全部 resolve 后返回保存结果的数组,如果有一个 reject,则立即终止。
function myPromiseAll(promises) {
return new Promise((resolve, reject) => {
// 存储结果的数组
const results = [];
// 已完成的 Promise 计数
let completed = 0;
// 处理空数组的情况
if (promises.length === 0) {
resolve(results);
return;
}
// 遍历每个 Promise
promises.forEach((promise, index) => {
// 将非 Promise 对象转换为 Promise
Promise.resolve(promise)
.then((value) => {
// 将结果存入对应索引位置
results[index] = value;
// 增加完成计数
completed++;
// 所有 Promise 都已完成时,返回结果数组
if (completed === promises.length) {
resolve(results);
}
})
.catch((error) => {
// 任何一个 Promise 失败,立即 reject
reject(error);
});
});
});
}
// 使用示例
const promise1 = Promise.resolve(1);
const promise2 = new Promise((resolve) => setTimeout(() => resolve(2), 1000));
const promise3 = 3; // 非 Promise 对象会被自动转换为 Promise
myPromiseAll([promise1, promise2, promise3])
.then((values) => console.log(values)) // 输出: [1, 2, 3]
.catch((error) => console.error(error));
🍀 async/await 的底层原理
async/await 是基于 Promise 和 Generator 实现的语法糖,将异步代码转为同步写法。其中,async 函数会被编译成 Generator 函数,await 则相当于 yield 语句,用于暂停函数的执行;JS 引擎会通过自动执行器监听 await 后的 Promise 状态,根据状态调用 generator 的 next 或 throw 方法,最终 async 函数会返回一个 Promise 对象。
ES6 新增了哪些东西
新增解构赋值、扩展运算符、模板字符串;以及新增字符串的方法,例如 includes() 来判断字符串中是否包含指定字符;然后新增了数组的方法,例如 Array.from() 来将伪数组转为数组;还有新增箭头函数、class 定义类等等。
对尾调用的理解
尾调用指的是函数在最后一步调用另一个函数,只在严格模式下有效;JS 在函数中调用另一个函数时会保留当前的执行上下文,然后新建另一个执行上下文加入栈中。而使用尾调用时,由于已经是函数的最后一步,所以可以不保留当前的执行上下文,节省了内存,这就是尾调用优化。
function f(x) {
return g(x);
}
什么是 Symbol
Symbol 是 ES6 新增的基本类型,用来定义一个独一无二的值。
数据结构 Set 和 WeakSet 的区别
Set 结构与数组类似,但数组内的值可以重复,而 Set 不可以;WeakSet 结构与 Set 类似,也是不重复值的集合,但 WeakSet 的成员只能是对象类型,而且这些对象是弱引用,不计入垃圾回收机制,会被自动回收,可以防止内存泄漏。
⚡️ 🍀 数据结构 Map 和 WeakMap 的区别
Map 结构与对象类似,但对象的 key 值只能是字符串或数字,而 Map 的 key 值可以是任何数据类型;WeakMap 结构与 Map 类似,但 WeakMap 的 key 值只能是对象类型,而且这些对象是弱引用,不计入垃圾回收机制,会被自动回收,可以防止内存泄漏。
🍀 对 ES6 Iterator 遍历器的理解
Iterator 遍历器基于迭代器模式,为数组和 Map 等可遍历数据结构提供一个统一的遍历接口,通过 Symbol.iterator 属性返回一个遍历器对象,可调用 next 方法来获取当前成员的信息对象。
什么是 Proxy
Proxy 用于创建对象的代理,实现基本操作的拦截和自定义,例如 Vue3 就是用 Proxy 代理来实现响应式。
⚡️ 🍀 Proxy 与 Object.defineProperty 的区别
Proxy 用于创建对象的代理,实现基本操作的拦截和自定义;而 Object.defineProperty 用于在对象上定义新属性或修改现有属性,并返回该对象。其中,definedProperty 劫持的是对象现有属性,新增属性需要再次 definedProperty,这也是 Vue2 无法跟踪响应对象新增属性的原因;而 Proxy 劫持的是整个对象,监听对象读写不需要做特殊的处理,而且还能监听到删除属性等更多操作,性能也更好,因此 Vue3 使用 Proxy 来重构响应式。
⚡️ 🍀 ES6 Module 与 CommonJS 的区别
- ES6 模块通过 export 导出、import 导入;而 CommonJS 模块通过 module.exports 导出,require 导入;
- ES6 模块导出的是个引用,模块内的修改会同步到外部;而 CommonJS 导出的是模块的拷⻉,模块内的修改不会同步到外部;
- ES6 模块的导入是在编译时加载(支持 Tree-Shaking),而 CommonJS 模块的导入是在运行时才加载。
🍀 ES6 的 import 和 export 是静态还是动态的
ES6 的 import 和 export 是静态的,在编译阶段就能确定模块的依赖关系,方便做静态分析,例如实现 Tree Shaking 消除没被用到的代码。
export 和 export default 的区别
一个文件可以多次使用 export,但只能使用一次 export default;另外 export 导出的模块在导入时需要加花括号 {},而且需要与模块名保持一致;而 export default 导出的模块在导入时不用加花括号,可以自定义导入的模块名。
TS 中 type 和 interface 的区别
type 使用 & 符号来实现继承,而 interface 使用 extends 来实现;然后就是 type 支持声明联合类型和元组类型,而 interface 不支持:
- 联合类型
- 元组类型
type ID = number | string;
let userId: ID = 123; // 合法,number 是 ID 的一种可能类型
let postId: ID = "abc"; // 合法,string 是 ID 的一种可能类型
// let invalidId: ID = true; // 非法,boolean 不是 ID 的可能类型
type UserInfo = [string, number, boolean];
let user: UserInfo = ["Alice", 30, true]; // 合法,类型顺序和数量匹配
// let invalidUser: UserInfo = [123, "Bob", false]; // 非法,类型顺序不匹配
// let shortUser: UserInfo = ["Charlie", 25]; // 非法,元素数量不足
TS 中 any 和 unknown 的区别
any 表示任意类型,但会跳过类型检查;unknown 也能表示任意类型,但更安全,操作前必须先进行类型检查,缩小类型范围。
TS 中 void 和 never 的区别
void 表示函数没有返回值,但函数会正常结束执行;而 never 表示永远不会出现的值,即函数不会正常结束执行,一般是抛出错误或陷入死循环。
对 TS 泛型的理解
泛型指的是函数、接口或类在定义时不指定具体类型,而在使用时才根据参数类型进行推断的机制。TS 中可以用尖括号的形式来使用泛型。
🍀 TS 中的类型断言和类型守卫是什么
类型断言是直接将类型告诉编译器,而类型守卫是一种运行检查机制,通过 typeof 等方法检查判断,缩小类型范围。
TS 中 Pick 的作用
Pick 用于从对象类型中选出一部分属性,生成新类型。
type User = {
name: string;
age: number;
email: string;
};
// 从 User 类型中只选择 'name' 和 'age' 属性
type UserBasicInfo = Pick<User, 'name' | 'age'>;
// 相当于:
type UserBasicInfo = {
name: string;
age: number;
};
🍀 TS 中 Partial 的作用
Partial 用于把对象类型的所有属性变为可选;
type User = {
name: string;
age: number;
email: string;
};
// 将 User 的所有属性变为可选
type PartialUser = Partial<User>;
// 相当于:
type PartialUser = {
name?: string;
age?: number;
email?: string;
};
🍀 TS 中 Record 的作用
Record 用于构造一个新类型,接收两个参数,分别作为 key 和 value 的类型。
// 使用 Record 定义一个对象,其键为 string,值为 number
type ScoreMap = Record<string, number>;
// 相当于:
type ScoreMap = {
[key: string]: number;
};
// 创建一个符合 ScoreMap 类型的对象
const scores: ScoreMap = {
"Alice": 95,
"Bob": 88,
"Charlie": 76,
};
🍀 TS 中 infer 的作用
infer 用于在条件类型中推断类型。
// 定义一个条件类型,用于提取函数的返回值类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// 使用 ReturnType 提取函数的返回值类型
type Num = ReturnType<() => number>; // 结果:number
type Str = ReturnType<(x: string) => string>; // 结果:string
$(document).ready() 的作用
$(document).ready() 在 DOM 完全加载时执行代码,可用于解决浏览器的兼容问题。
jQuery 中的 $(document).ready() 与 window.onload 有什么区别
jQuery 的 ready 函数 只要 DOM 加载完就会触发,其它资源可能还未加载完;而 onload 在所有资源加载完才触发;
jQuery 和其它框架的区别
Vue 和 React 等框架数据和视图是分离的,且由数据驱动视图,而 jQuery 不是。
jQuery 源码有哪些写的好的地方
jQuery 源码封装在一个匿名函数的自执行环境中,有助于防止变量的全局污染;而且 jQuery 实现的链式调用可以节约代码,提高代码效率。
React
⚡️ 🍀 React 18 有什么升级
React 18 引入新的 Render API,支持并发模式的渲染,可以用 Transition API 进行并发控制;而且 React 18 支持自动批处理,在 setTimeout、setInterval、addEventListener 等原生事件中也会进行合并批量更新,以提升性能;另外,React 18 还用 Suspense 组件为服务端渲染提供更快的页面加载。
🍀 React 18 的并发模式解决了什么问题
React 18 的并发模式主要是解决用户交互卡顿的问题,通过中断渲染过程,优先响应用户的操作,提升用户体验。
⚡️ 🍀 React 19 有什么升级
React 19 对 form 标签进行了加强,允许传递 action 来处理表单逻辑,而且新增了 useActionState 和 useFormStatus 等钩子,可以更灵活的处理表单的加载状态,还有就是内置乐观更新的支持,新增 useOptimistic 钩子显示乐观状态;另外,React 19 对 Context API 简化了 Provider 的写法;对 ref 回调新增了清理功能,允许在组件卸载时自动执行清理逻辑;而且 React 19 还改进了错误日志和错误处理机制。
🍀 什么是乐观更新
乐观更新是假设异步操作会成功,并立即将 UI 更新到操作成功后的状态,而不是等到操作完成。以 useOptimistic 钩子为例,如果操作失败,useOptimistic 会自动将界面恢复到原来的样子,就像什么都没发生过一样,可以让用户感觉应用反应很快,体验更流畅。
React 严格模式的作用
使用严格模式,可以识别不安全的生命周期,并在使用废弃 API 时发出警告。
React 有什么特点
组件化、虚拟 DOM、JSX 语法、单向数据流、服务器渲染等。
React 组件的生命周期
- 挂载阶段为 constructor()、render()、componentDidMount()
- 更新阶段为 render()、componentDidUpdate()
- 卸载阶段为 componentWillUnmount()
⚡️ 🍀 对 JSX 的理解
JSX 是 React 使用的 JS 扩展语法,浏览器无法直接读取,需要通过 Babel 进行转换,本质是 React.createElement 的语法糖,用来创建虚拟 DOM,然后由 ReactDOM 的 render 函数渲染到指定容器上,完成真实 DOM 的转换。
⚡️ 🍀 React 的渲染机制
React 的渲染机制是调用 render() 函数构建 DOM 树,当 state 或 props 发生变化时,React 不会重新渲染整个页面,而是进行局部更新,减少 DOM 树的频繁操作,从而提升页面渲染性能。具体是触发协调过程,通过 Diff 算法找到需要更新的元素,放到更新队列中,然后在渲染阶段遍历该队列,更新渲染对应的元素。
🍀 React key 值的作用
React 遍历节点时需要给每个节点设置一个 key 值作为唯一标识,使 diff 算法可以正确识别此节点,找到正确的位置插入新节点,从而高效的更新虚拟 DOM。
React 中 key 值需要注意的地方
key 值不能重复,而且不能使用对象或数组等非基本类型作为 key 的值;另外,不建议使用索引作为 key 值,因为列表节点的顺序可能发生变化,直接使用索引作为 key 值会影响性能或出现 bug。
对虚拟 DOM 和 diff 算法的理解
虚拟 DOM 是用 JS 模拟 DOM 结构,避免 DOM 树的频繁更新,从而提升页面渲染性能。而 diff 算法就是用来高效地对比新旧虚拟 DOM,找出真实 DOM 的变化之处。
对 React 中 diff 算法的理解
React 的 diff 算法使用三大策略来降低 diff 算法的复杂度,首先只对同一层级的节点进行比较;其次,如果比较的组件是同一类型,则继续往下进行 diff 运算,如果不是同一类型,则直接删除被比较的组件及其子节点并重新创建;最后,同层级的一组子节点,还会通过 key 值进行区分。除了三大策略优化 diff 算法外,React 还引入了 Fiber 架构,比 Vue 的 diff 算法多了时间切片的能力。
对 React Fiber 的理解
Fiber 是 React 16 开始使用的协调引擎,替换了原来的 Stack 协调器,支持虚拟 DOM 的渐进式渲染,可以把耗时过长的任务分片执行,让出 CPU 的执行权,使浏览器有时间进行页面渲染工作,同时 Fiber 会通过 window 的 requestIdleCallback() 方法,在浏览器空闲时继续执行未完成的任务,解决了单线程环境导致的渲染阻塞问题,提高了用户体验。
🍀 React 的事件机制是什么样的
React 的事件不是绑定到真实 DOM 上的,而是通过事件代理的方式,将合成事件统一绑定到 Root 元素上。其中,合成事件是 React 模拟原生事件的一个事件对象,可以兼容不同的浏览器,而且方便事件统一管理和事务机制。但 React 的合成事件无法通过 return false 的方式阻止浏览器的默认行为,需要使用 preventDefault() 来实现。
props 与 state 的区别
props 是组件外部传进来的,在组件内不能直接修改;而 state 在组件内部管理,可以进行修改;
state 可以直接修改吗
在类组件的构造函数中可以直接通过 this.state 来修改 state 的值,但这种方式不会触发页面更新。
setState 批量更新策略是怎样的 / 是同步还是异步
React 18 之后的版本支持自动批处理,除了原来的组件生命周期函数跟合成事件外,在 setTimeout、setInterval、addEventListener 等原生事件中也会进行合并批量更新,以提升性能,因此 setState 的表现是异步的,可以通过 flushSync API 来让 setState 立即生效。
import { flushSync } from 'react-dom';
const handleClick = () => {
flushSync(() => {
setCount(1); // 立即更新状态并渲染
});
// 此时 DOM 已更新为 count=1
console.log(document.getElementById('count').textContent); // 输出: "1"
};
🍀 setState 的调用原理 / 调用 setState 发生了什么
调用 setState 会触发 React 的调度机制:将新的状态存入更新队列,并标记组件需要更新,然后 React 会自动批处理同一事件循环内的所有状态更新,进行合并处理;随后触发协调过程,通过 Diff 算法找到需要更新的元素,放到更新队列中,接着在渲染阶段遍历该队列,更新渲染对应的元素。
如何理解受控和非受控组件
受控组件的值由 React 进行管理,通过传入的 value 来固定组件的值,并通过 onChange 事件来进行值的更新;而非受控组件不传入 value,一般只传入 defaultValue 作为初始的默认值,即使 defaultValue 的值固定,组件的值也不受影响。
React 怎么访问 DOM 节点
- 访问当前组件内的 DOM 节点可以通过回调 ref、createRef、或 useRef 钩子的方式来实现 ↓
- 类组件回调 ref
- 函数组件 createRef 或 useRef
<div ref={node => this.myDiv = node}>123</div>
const myRef = React.createRef()
// 或
const myRef = useRef(null)
<div ref={myRef} />
- 跨组件访问 DOM 节点可以通过 forwardRef() 来进行转发,也可以通过 props 传递 ref 的方式来实现 ↓
- forwardRef 转发
- 通过 props 传递 ref
import React, { useEffect, useRef } from 'react'
const MyButton = React.forwardRef((props, curRef) => {
return <button ref={curRef}>按钮</button>
})
const App = () => {
const btnRef = useRef(null)
useEffect(() => {
console.log(btnRef.current) // <button>按钮</button>
})
return <MyButton ref={btnRef} />
}
export default App
import React, { useEffect, useRef } from 'react'
const MyButton = (props) => {
return <button ref={props.btnRef}>按钮</button>
}
const App = () => {
const btnRef = useRef(null)
useEffect(() => {
console.log(btnRef.current) // <button>按钮</button>
})
return <MyButton btnRef={btnRef} />
}
export default App
React 如何实现跨多级传参
通过 React 的 createContext API 包裹下层组件,然后在组件中通过 useContext 钩子获取传入的值,从而实现跨级传参。
🍀 React Context 的原理
Context 的原理是在 JSX 渲染时把 Provider 和 Consumer 对象保存到虚拟 DOM 上,然后在 reconcile 阶段转移到 fiber 的 type 属性中,处理 fiber 节点时,如果是 Provider 则根据传入的 value 修改 context 的值,如果是 Consumer 或 useContext 钩子则通过 readContext 读取 context 的值,然后触发子组件的渲染,实现跨级传参。
React 如何将节点挂载到任意位置
通过 ReactDOM.createPortal(child, container) 方法实现。
类组件与函数组件的区别
类组件基于面向对象编程,需要继承 React.Component 并创建 render 函数来返回 React 元素;而函数组件是个纯函数,基于函数式编程,接收 props 并直接返回 React 元素;
另外类组件存在 this 和生命周期,而函数组件不存在,但有 Hook 钩子函数可以弥补相应的功能。
React Hooks 解决了什么问题
原有的函数组件也被称为无状态组件,只负责渲染工作,而引入 React Hooks 后,函数组件也可以是有状态的组件,可以维护自身的状态、做一些逻辑处理。
🍀 Hooks 的实现原理
Hooks 的实现原理是利用闭包来保存状态,使用链表保存一系列的 Hooks,并将链表中的第一个 Hook 与 Fiber 关联。在 Fiber 树更新时,就能从 Hooks 中计算出最终状态并执行对应的副作用。
为什么 useState 要使用数组而不是对象
useState 返回数组可以直接按顺序进行解构,更为灵活,比使用对象更方便。
useEffect 的返回值有什么用
useEffect 可以返回一个函数,在组件卸载时执行,用来清理副作用。
🍀 useEffect 与 useLayoutEffect 的区别
useEffect 在浏览器 DOM 绘制完成之后执行;而 useLayoutEffect 在浏览器 DOM 绘制之前执行,会阻塞浏览器的绘制,一般在需要测量或修改 DOM 布局时使用。
useCallback 与 useMemo 的区别
useCallback 与 useMemo 类似,都是通过缓存来提升性能,区别在于 useCallback 缓存的是函数的引用,返回一个未执行的函数;而 useMemo 缓存的是函数的返回值,返回函数执行的结果。
⚡️ 🍀 对 React 高阶组件的理解
高阶组件(HOC)是基于装饰器模式复用组件逻辑的纯函数,接受一个组件作为参数并返回改造后的新组件,可用于逻辑复用。
高阶组件与普通组件的区别
高阶组件是接收组件并返回一个新组件,而普通组件是接收 props 并返回 UI。
🍀 高阶组件与自定义 Hook 的区别
高阶组件通过注入状态化的 props 来扩展组件的功能,可能改变组件结构;而自定义 Hook 是在组件中复用状态逻辑,不影响组件结构。
React 什么时候会重新渲染组件
当 props 或 state 发生变化、以及父组件渲染时都会导致组件重新渲染。
⚡️ 🍀 React 性能优化 / 如何避免不必要的渲染
类组件可通过 shouldComponentUpdate 方法或继承 PureComponent 纯组件的方式来实现;函数组件可通过 React.memo 高阶组件进行包装,也可以直接用 useCallback 或 useMemo 钩子来减少不必要的子组件渲染。
🍀 React 如何实现服务器端流式渲染
React 18 可以通过 renderToPipeableStream 方法,把 React 组件渲染成可流式传输的数据流,允许服务器分块传输 HTML,浏览器一边接收一边渲染,减少首屏加载时间。
React 中 Suspense 组件的作用
Suspense 组件用于处理异步加载的场景,通过 fallback 属性指定加载中的占位内容,加载完成后会渲染 Suspense 包裹的内容。
⚡️ 🍀 React 中如何实现懒加载
对于组件的懒加载,React 跟 Vue 的 defineAsyncComponent 一样,提供了 React.lazy 用于动态导入组件,再配合 Suspense 展示加载组件,可以实现懒加载;而对于路由的懒加载,react-router 也是可以通过 React.lazy 配合 Suspense 来实现懒加载。
React 中如何实现国际化
可以通过 react-i18next 这个库,按照不同语言创建了对应的 JSON 格式翻译文件,并编写了 i18n 配置文件,在其中设置默认语言;
在组件中则用 useTranslation 钩子获取翻译函数,对文本进行翻译,在语言的切换这块,则用到了 i18n 的 changeLanguage 方法实现,最终实现国际化。
Redux 的三大原则是什么
Redux 的三大原则分别是单一数据源、状态只读和使用纯函数来修改数据。
Redux 的 combineReducers 有什么用
combineReducers 用于合并多个 reducer,并将它们返回的结果合并成一个 state 对象。
Redux 的工作流程
Redux 将公共数据存放在 Store 中,当一个组件修改 Store 的数据,其他组件可以感知 Store 的变化并拿到最新的数据,从而间接实现了组件间的数据传递。其中,组件修改 Store 的数据需要发送 Action,根据 Action 匹配 Reducer,然后 Reducer 计算得到新的 State,实现 Store 数据的更新。
⚡️ 🍀 Redux Store 的状态值是怎么注入组件的
react-redux 提供了 <Provider/> 组件来全局注入 Store,原理是创建一个 context 和 subscription,然后订阅 Store,当 Store 发生变化时执行 subscription 的 onStateChange,触发相应的监听回调。
在项目中是如何使用 Redux 的
我一般用 rematch 配合 react-redux 来使用 Redux,其中用 rematch 的 init 方法来创建 Store,然后用 react-redux 的 <Provider/> 组件来全局注入 Store,在组件中用 useSelector 钩子来拿到 Store 的数据,并用 useDispatch 钩子来发送 Action,根据 Action 匹配 Reducer,然后 Reducer 计算得到新的 State,实现 Store 数据的更新。
🍀 什么是 react-redux
react-redux 是官方的 Redux 绑定库,提供了 <Provider/> 组件来全局注入 Store,在组件中用 useSelector 钩子来拿到 Store 的数据,并用 useDispatch 钩子来发送 Action,根据 Action 匹配 Reducer,然后 Reducer 计算得到新的 State,实现 Store 数据的更新。
🍀 react-redux 中 connect 的原理
connect 是个高阶组件,它会订阅 Redux 的 store,将其中的 state 和 dispatch 作为 props 传递给包装的组件,然后通过 mapStateToProps 和 mapDispatchToProps,实现状态和方法的绑定。
⚡️ 🍀 什么是 rematch
rematch 简化了 Redux 的写法,省略了 action types 和 action creators,并且在 reducer 中用对象替代了 switch 语句,写法更加友好,另外还支持集中书写状态和方法。
⚡️ 🍀 对 Redux 中间件的理解
Redux 中间件用于拦截 dispatch Action,添加其他功能,例如处理异步请求和日志监控等等。中间件的本质是函数柯里化,Redux 通过 applyMiddleware 来添加中间件,将所有中间件放进了一个数组 chain 中,然后嵌套执行,最后用 store 的 dispatch 来结束调用链。
Redux 常用的中间件
redux-thunk 和 redux-saga 用来处理异步请求,redux-logger 用来记录日志。
Redux 怎么处理异步请求
可通过 redux-thunk 或 redux-saga 中间件处理异步请求。redux-thunk 会判断 dispatch 的参数是否为 thunk 延时函数,是的话则先处理 thunk 里面的东西,再执行这个函数;而 redux-saga 使用了 ES6 Generator 语法,对 Action 进行拦截,然后在单独的 sagas 文件中处理异步操作,最后返回一个新的 Action 传给 Reducer,再去更新 store。
React-Router 的实现原理
React Router 的原理是通过 JS 监听 URL 的变化,对页面进行处理。其中:默认的 hash 模式通过 hashchange 事件监听 url 的 hash 值来实现路由跳转;而 history 模式则利用 pushState 和 replaceState 来修改浏览器的历史记录栈,并通过 popState 事件监听历史记录的变化来实现路由跳转,由于是单页面应用,所以 history 模式还需要服务端配置,否则刷新会 404。
hash 模式与 history 模式的区别和原理
hash 模式会在 url 中夹带上 #,而且兼容性比 history 模式更好;另外,hash 模式通过 hashchange 事件监听 url 的 hash 值来实现路由跳转;而 history 模式则利用 pushState 和 replaceState 来修改浏览器的历史记录栈,并通过 popState 事件监听历史记录的变化来实现路由跳转,由于是单页面应用,所以 history 模式还需要服务端配置,否则刷新会 404。
React-Router Link 标签和 a 标签的区别
Link 标签是路由跳转,只改变 URL 和页面内容,不刷新页面;而 a 标签是跳转到一个新页面,会触发刷新。
React-router 如何定义动态路由和读取参数
通过 :加参数的方式定义动态路由,通过 useParams 钩子读取路由参数。
🍀 React-Router v6 的路由定义方式有什么新变化
从 v6 开始,可以通过 useRoutes 配置数组对象的形式来定义路由,比以前嵌套 Route 组件的写法更简洁,路径匹配也更友好。
import { Link, useRoutes } from 'react-router-dom';
function App() {
const routes = useRoutes([
{ path: '/', element: <h1>Home</h1> },
{ path: 'about', element: <h1>About</h1> },
]);
return (
<Router>
<nav>
<Link to="/">Home</Link> |
<Link to="/about">About</Link>
</nav>
{routes}
</Router>
);
}
export default App;
⚡️ 🍀 SPA 单页面和 MPA 多页面的区别
SPA 单页面应用在页面初始化时加载必要的代码,之后不需要重新加载,原理是通过 JS 监听 URL 的变化,对页面进行处理,实现路由跳转,有更好的用户体验,但首次加载时间长,而且不利于 SEO;而 MPA 多页面应用在页面跳转时是整页刷新,对 SEO 更友好。
SPA 单页面应用如何做 SEO
SPA 单页面应用可通过 SSR 服务端渲染做 SEO,在服务端完成 HTML 的渲染,然后返回给浏览器解析,常见的框架有 Next.js 和 Nuxt.js。
对服务端渲染的理解
SSR 服务端渲染指的是在服务端完成 HTML 的渲染,再返回给浏览器解析;可以提高首屏加载速度,而且有利于 SEO;但会增加服务器的负担。
Vue
Vue 的三要素和两个核心
Vue 的三要素是响应式、模板引擎和渲染;Vue 的两个核心是数据驱动和组件化。
Vue 的生命周期
Vue 生命周期指的是 Vue 实例从创建到销毁的过程,包括:
- beforeCreate、created;
- beforeMount、mounted;
- beforeUpdate、updated;
- beforeUnmount、unmounted。
🍀 created 和 mounted 的区别
created 在创建实例后调用,此时 DOM 还未生成,而 mounted 在 DOM 完成挂载后调用,可以进行 DOM 操作。
Vue 和 React 的区别
Vue 支持数据双向绑定,使用模板语法;而 React 是单向数据流,使用 JSX 语法;另外 Vue 和 React 在插槽、组件通信、受控处理等方面的写法也不一样。
🍀 MVC、MVP、MVVM、Flux 的区别
- MVC 即 Model、View 和 Controller,对应模型、视图和控制器,其中 View 发送指令给 Controller,Controller 处理业务逻辑,对 Model 进行修改,然后 Model 通知 View 更新页面;
- MVP 是 MVC 的变体,其中 P 即 Presenter,既负责处理业务逻辑,又负责 Model 和 View 之间的通信;
- MVVM 即 Model、View 和 ViewModel,其中 ViewModel 用来连接 Model 和 View,而 Model 通过数据绑定来操作 View、View 通过事件绑定来操作 Model;
- Flux 模式采用单向数据流,以解决 MVVM 数据流混乱的问题,包括 View、Action、Dispatcher 和 Store,其中 View 发送 Action 给 Dispatcher,Dispatcher 根据 Action 来更新 Store 的数据,然后 Store 通知 View 更新页面。
🍀 Vue 模板渲染/挂载原理
首先执行 new Vue 进行数据初始化,然后调用 $mount 进行页面的挂载,挂载时将 template 解析为 AST 抽象语法树,再转成 render 语法字符串生成 render 方法;接着在组件渲染阶段执行 render 方法生成虚拟 DOM,然后调用 update 转为真实 DOM 并更新到页面中。
Vue 如何是实现响应式的
Vue2 采用数据劫持结合发布订阅模式的方法,通过 Object.defineProperty() 来劫持各个属性的 getter 和 setter,在数据变化时发布消息给订阅者,触发相应的监听回调,但无法跟踪响应对象的属性添加操作。而 Vue3 采用 Proxy 代理来实现响应式,data 中的数据不再进行响应式追踪,而是转为 Proxy 代理进行追踪更新。
为什么 Vue3 要用 Proxy 重构响应式
Proxy 用于创建一个对象的代理,实现基本操作的拦截和自定义;而 Object.defineProperty 用于在对象上定义新属性或修改现有属性,并返回该对象。其中,definedProperty 劫持的是对象现有属性,新增属性需要再次 definedProperty,这也是 Vue2 无法跟踪响应对象新增属性的原因;而 Proxy 劫持的是整个对象,监听对象读写不需要做特殊的处理,而且还能监听到删除属性等更多操作,性能也更好,因此 Vue3 使用 Proxy 来重构响应式。
Vue3 有哪些更新
Vue3 使用 Proxy 来重构响应式,解决了 defineProperty 无法跟踪响应对象新增属性的问题;而且新增了 Composition API,可以更灵活的组织逻辑;另外,Vue3 还在 Diff 算法、静态提升和事件监听缓存等方面进行了优化,大幅度提升了性能。
🍀 Vue3 在性能方面进行了什么优化
Vue 3 对 diff 算法进行了优化,新增了静态标记,只对比带有标记的节点,减少比较次数以提升性能;Vue 3 还对不参与更新的元素做静态提升,只创建一次,在渲染时直接复用,避免节点的重复创建;另外,Vue3 还做了事件监听缓存优化,将事件缓存起来复用,以提升性能。
对 Vue runtime 的理解
Vue runtime 主要是对 render 函数的调用,在 render 函数中去执行 patch 算法和 diff 算法。
⚡️ 🍀 对 Vue 中 render 函数和 patch 算法的理解
render 函数用于生成虚拟 DOM,而 patch 算法用于遍历所有 VNode 虚拟节点,根据节点类型进行渲染并挂载到根节点上。
🍀 对 Vue diff 算法的理解
Vue 的 diff 算法会对比新旧虚拟 DOM,其中通过双端对比,将两个指针一个从前面开始,一个从后面开始,过滤出中间改变的部分,然后使用最长递增子序列算法,保证节点的移动次数最少。
v-show 和 v-if 的区别
v-show 是通过切换 display 属性来控制显示隐藏,而 v-if 是控制元素是否渲染在页面中,有更高的切换开销,因此 v-show 更适合频繁切换显示的场景。
v-html 的原理
v-html 用于渲染 HTML 片段,它会移除节点下的内容,然后使用 innerHTML 属性把绑定的 HTML 片段渲染到页面中。
🍀 v-model 双向绑定的原理
v-model 用于实现表单元素的双向绑定,本质是个语法糖,在父组件中传递 modelValue 给子组件,在子组件中通过 update:modelValue 事件来更新数据。
🍀 Vue 如何实现受控组件
一般组件可以通过传入 value 配合 computed 计算属性来实现受控;而输入框组件中,Vue 不像 React 可以通过绑定 value 来固定输入框的值,需要在 onInput 事件中使用 nextTick 进行处理,修改原生输入框的值,从而实现受控。
- 一般组件
- input 组件
<template>
<button @click="handleSwitch">
{{ innerValue ? '开' : '关' }}
</button>
</template>
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
value: Boolean,
defaultValue: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['change'])
const _switchValue = ref(props.defaultValue)
const innerValue = computed(() => props.value ?? _switchValue.value)
const handleSwitch = () => {
const newVal = !innerValue.value
_switchValue.value = newVal
emit('change', newVal)
}
</script>
<template>
<input
ref="inputRef"
:value="innerValue"
@input="handleInput"
/>
</template>
<script setup>
import { ref, computed, nextTick } from 'vue'
const props = defineProps({
value: [String, Number],
defaultValue: {
type: [String, Number],
default: ''
}
})
const emit = defineEmits(['input'])
const _inputValue = ref(props.defaultValue)
const innerValue = computed(() => props.value ?? _inputValue.value)
const inputRef = ref(null)
const handleInput = (e) => {
const newVal = e.target.value
_inputValue.value = newVal
emit('input', newVal, e)
nextTick(() => {
if (inputRef.value && innerValue.value !== inputRef.value.value) {
inputRef.value.value = innerValue.value
}
})
}
</script>
data 为什么是个函数
data 如果是个对象的话,复用组件时就会共用一个 data,导致数据相互影响;而函数 data 在组件实例化时会返回一个对象并分配一个内存地址,实例化几次就分配几个内存地址,使这些对象的内存地址独立,这样改变其中一个组件的状态就不会影响到其它组件。
reactive 的原理
reactive 的原理是通过 createReactiveObject 方法校验目标对象类型,然后根据类型进行依赖的收集和触发,最后返回生成的 Proxy 响应式对象。
ref 的原理
ref 的原理是通过类的 get 和 set 进行依赖的收集和触发,完成响应式追踪,最后返回 ref 响应式对象。
🍀 reactive 和 ref 的区别
reactive 和 ref 都用来实现响应式数据。但 reactive 接收的是引用类型,而 ref 既可以接收基本类型、也可以接收引用类型,当接收引用类型时,ref 的内部会通过 reactive 转为 Proxy 响应式对象。而在内部实现方面,reactive 通过 createReactiveObject 方法校验目标对象类型,然后根据类型进行依赖的收集和触发,最后返回生成的 Proxy 响应式对象;而 ref 通过类的 get 和 set 进行依赖的收集和触发,完成响应式追踪,最后返回 ref 响应式对象。在使用方面,reactive 对象可以直接使用,而 ref 对象在操作时需要使用 .value 属性。
🍀 toRef 和 toRefs 的区别
toRef 和 toRefs 都是将响应式对象的属性转成 ref,延续响应式并且保持和原属性的双向同步,但 toRef 针对的是单个属性,而 toRefs 是对整个对象的所有属性进行批量转换。
Vue 怎么获取 DOM 节点
在节点上添加一个 ref 属性,然后声明一个同名的 ref 来获取该节点的引用。
computed 和 watch 的区别
computed 根据依赖进行计算并返回结果;而 watch 用于监听指定数据源,在数据源变化时执行回调函数。
🍀 computed 的原理
computed 根据依赖进行计算并返回结果,本质是个惰性求值的观察者,只在 getter 取值操作时收集依赖,当依赖变化时通过 dirty 变量进行标记,判断是否需要重新计算,最后返回一个 ref 实例。
watch 的原理
watch 用于监听指定数据源,在数据源变化时执行回调函数;原理是根据数据源的类型构造 getter 函数,根据监听配置项构造调度器,通过 getter 函数和调度器完成监听。
watch 和 watchEffect 的区别
watch 用于监听指定数据源,在数据源变化时执行回调函数;而 watchEffect 会立即执行回调函数,同时自动收集依赖的数据源,当数据源变化时再次执行回调函数,而且 watchEffect 的返回值还可以用来停止监听。另外 watch 可以访问数据源变化前的值,而 watchEffect 不可以。
Vue 怎么注册全局组件
可以通过 Vue 实例的 app.component() 方法注册全局组件。
🍀 Vue 如何将节点挂载到任意位置
通过包裹 Teleport 内置组件来实现。
Vue 父子组件通信的方式有哪些
父组件可以通过 props 或 provide 配合 inject 的方式给子组件传值;而子组件可以通过 emit 事件或作用域插槽的方式给父组件传值。
🍀 Vue provide inject 的原理
provide inject 的原理是利用原型链来实现跨级传参,当 inject 获取 provides 对象时,会沿着原型链逐层向上查找。
🍀 什么是作用域插槽
作用域插槽指的是子组件给插槽内容传递数据,在父组件中可以通过 v-slot 指令拿到子组件插槽传过来的数据。
<!-- 子组件 -->
<slot txt="123" :count="111" />
<!-- 父组件 -->
<Comp v-slot="slotProps">
<button>{{ slotProps.txt }}</button>
<button>{{ slotProps.count }}</button>
</Comp>
⚡️ 🍀 Vue 自定义指令的理解
Vue 自定义指令用于复用 DOM 操作逻辑,例如输入框自动聚焦逻辑。除了通过 directive 来进行注册,在 setup 中以 v 开头的驼峰命名变量也可以直接用作一个自定义指令。
Vue 常用的事件修饰符有哪些
- .stop:阻止事件冒泡;
- .prevent:阻止默认行为;
- .capture:使用捕获模式添加事件;
- .self:事件只在自身触发;
- .once:事件只触发一次;
- .passive:优化移动端的滚屏性能。
🍀 Vue 插件是什么,如何注册
Vue 插件用于增强 Vue 的全局功能,可通过 app.use() 进行注册;
Vue 如何注册全局属性/方法
可通过 app.config.globalProperties 来注册全局属性/方法。
🍀 对 Vue nextTick 的理解
Vue 在响应式状态改变时,会开启一个异步更新队列缓冲所有状态变更,视图需要等队列中所有状态改变后,再统一进行更新,类似 React 的批量更新策略;而 nextTick 可以在状态改变后立即拿到更新后的 DOM;其原理是通过 Event Loop 事件循环来进行异步操作,在 Vue 的事件循环结束后执行 nextTick 回调函数,而且会优先使用微任务来执行。
🍀 对 KeepAlive 的理解
KeepAlive 用于缓存组件销毁前的状态,原理是通过 cache 和 keys 两个变量来缓存实例,并根据 LRU 策略来保证缓存个数不超出限制,最后标记该组件的缓存状态并返回组件;另外,被 KeepAlive 缓存的组件移除时,生命周期不会触发 unmounted,而是触发 deactivated,当组件再次渲染时生命周期会直接从 activated 开始执行。
LRU 策略是什么
LRU 策略的设计原则是在空间不够时,移除最长时间没被访问到的数据。
Vue 中 Suspense 组件的作用
Suspense 组件用于处理异步加载的场景,通过 fallback 插槽指定加载中的占位内容,加载完成后会渲染 default 插槽的内容。
Vue 中如何实现懒加载
对于组件的懒加载,Vue 跟 React.lazy 一样,提供了 defineAsyncComponent 用于动态导入组件,再配合 Suspense 展示加载组件,可以实现懒加载;而对于路由的懒加载,vue-router 中可通过箭头函数配合 import() 语法动态导入来实现懒加载。
🍀 Vue 中如何实现国际化
可以通过 vue-i18n 这个库,按照不同语言创建对应的 JSON 格式翻译文件,并编写 i18n 配置文件,在其中设置默认语言;
在组件中使用 useI18n 钩子获取翻译函数,对文本进行翻译,在语言的切换这块,则用到了 i18n 的 setLocaleMessage 方法实现,最终实现国际化。
🍀 Vue 中如何处理错误边界
全局的错误可以通过 app.config.errorHandler 来统一处理,组件局部的错误可以通过 onErrorCaptured 来进行捕获。
🍀 Vuex 与 Redux 的区别
Vuex 和 Redux 都基于 Flux 架构,但 Vuex 用 Mutation 取代了 Reducer,在 Mutation 中直接操作 State;而且 Vuex 对 State 的更新是直接修改数据,而 Redux 是用新的数据覆盖旧数据;另外 Vuex 还提供了 getter 来计算 State。
🍀 Vuex 是什么,怎么用
Vuex 是一个状态管理模式,核心为 Store,包括 State、Getter、Mutation、Action 和 Modules,其中 State 用来存放公共数据;Getter 作为 state 的计算属性,用来过滤数据;Mutation 用来定义修改数据的方法;而 Action 用来触发 Mutation,通过 dispatch 调用;Modules 则对应模块,store 可拆分为多个模块,方便管理。
Vuex 中 Mutation 和 Action 的区别
Mutation 直接操作 State,必须同步执行;而 Action 用来触发 Mutation,间接操作 State,可以异步执行。
为什么 Mutation 必须同步执行
Mutation 如果是异步的话,就无法知道状态什么时候更新,难以进行状态追踪。
🍀 Vuex 与 Pinia 的区别
Pinia 舍弃了 Mutation,直接用 Action 来操作 State,而且不需要通过 dispatch 调用,语法更加简洁;另外 Pinia 支持多个 store,而 Vuex 是通过 module 来进行模块拆分;还有就是 Pinia 对 TS 更友好。
🍀 Vuex 的 store 如何注入到组件中,Pinia 呢
Vuex 通过 mixin 机制,在 beforeCreate 调用 vuexInit 方法,将 store 注入到组件中;而 Pinia 在 Vue2 中通过 mixin 机制,在 beforeCreate 注入 Pinia 实例,在 Vue3 中则通过 provide、inject 的方式来注入。
Vue-router (路由) 的原理
Vue Router 的原理是通过 JS 监听 URL 的变化,对页面进行处理。其中:默认的 hash 模式通过 hashchange 事件监听 url 的 hash 值来实现路由跳转;而 history 模式则利用 pushState 和 replaceState 来修改浏览器的历史记录栈,并通过 popState 事件监听历史记录的变化来实现路由跳转,由于是单页面应用,所以 history 模式还需要服务端配置,否则刷新会 404。
vue-router 如何定义动态路由和读取参数
通过 :加参数的方式定义动态路由,通过 route.params 的方式读取路由参数。
router 和 $route 的区别
router 是路由实例对象,包括路由的跳转方法和钩子函数等;而 $route 是路由信息对象,包括路径和参数等信息。
vue-router 如何实现懒加载
vue-router 中可通过箭头函数配合 import() 语法动态导入来实现懒加载。
Vue Router 路由守卫是什么
vue-router 路由守卫提供了一些钩子函数,用于在路由跳转的各个阶段执行指定操作。
🍀 Vue 路由钩子有哪些
- 全局路由钩子有:beforeEach、beforeResolve、afterEach;
- 单个路由独享的钩子有:beforeEnter;
- 组件内的路由钩子有:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave。
移动端〡小程序
移动端的物理像素及逻辑像素是什么
物理像素是屏幕实际的硬件像素点,而逻辑像素是开发者使用的抽象单位,两者通过设备像素比换算,一逻辑像素可能对应多个物理像素。
em 和 rem 的区别
em 是相对当前父元素的字体大小,而 rem 是相对于根元素的字体大小。
移动端适配方案
可以设置 Viewport 配合 @media 媒介查询,也可以通过插件使用 rem 进行自适应,还可以使用 vw 或 vh 单位进行适配。
移动端 1 像素问题的原因及解决方案
由于移动端设备的物理像素和逻辑像素不一致,导致一个逻辑像素被渲染为多个物理像素,造成视觉上的粗线,可以通过伪元素加 transform 设置 scale 为 0.5 的缩放,实现一个物理像素的边框。
小程序与 H5 的区别
H5 运行在浏览器,而小程序运行在微信环境,有自己的生态,性能和体验更好,而且不需要考虑浏览器的兼容性。
🍀 小程序与 Vue 的区别
- 在显示隐藏上,Vue 用 v-if 和 v-show 来控制显示隐藏,而小程序用 wx:if 和 hidden 来控制;
- 在数据绑定上,Vue 用冒号
:绑定数据,而小程序用双花括号{{}}来绑定; - 在事件绑定上,Vue 用 v-on 来绑定事件,而小程序用 bindtap 或 catchtap 来绑定;
- 在双向绑定上,Vue 支持 v-model 双向绑定,而小程序不支持。
🍀 小程序的原理
小程序基于微信的双线程架构,逻辑层用 JavaScript,视图层用 WebView,二者通过微信的 JSBridge 通信。
⚡️ 🍀 小程序的生命周期
组件的生命周期包括 created、attached、ready、moved、detached 和 error;而组件所在页面的生命周期包括 show、hide、resize 和 routeDone。
bindtap 和 catchtap 的区别
bindtap 和 catchtap 都是点击事件,但 bindtap 事件会冒泡,而 catchtap 会阻止冒泡。
🍀 小程序的路由方法有哪些
主要有 navigateTo()、redirectTo()、navigateBack()、switchTab() 和 reLaunch()
wx.navigateTo() 的作用
wx.navigateTo() 会保留当前页面,然后跳转到非 tabBar 页面
wx.redirectTo() 的作用
wx.redirectTo() 会关闭当前页面,然后跳转到非 tabBar 页面
wx.navigateBack() 的作用
wx.navigateBack() 会关闭当前页面,然后返回到指定页面
wx.switchTab() 的作用
wx.switchTab() 会关闭其他所有非 tabBar 的页面,然后跳转到 tabBar 页面
wx.reLaunch() 的作用
wx.reLaunch() 会关闭其他所有页面,然后跳转到指定页面
⚡️ 🍀 小程序如何实现组件间的通信
父组件通过 properties 向子组件传递数据,子组件通过 triggerEvent 触发事件向父组件传值,从而实现父子组件通信;也可以通过 app.js 的 globalData 来获取和设置全局数据,实现全局数据共享;还可以创建一个事件总线对象,在不同组件之间触发和监听事件,实现数据传递。
⚡️ 🍀 小程序如何实现页面间的通信
可以通过 url 参数传递数据,在跳转页面时携带参数;也可以通过 app.js 的 globalData 来获取和设置全局数据,实现全局数据共享;还可以创建一个事件总线对象,在不同组件之间触发和监听事件,实现数据传递。
小程序中如何管理全局状态
可以通过 app.js 的 globalData 来获取和设置全局数据,实现全局数据共享;也可以创建一个事件总线对象,在不同组件间触发和监听事件,实现数据传递;还可以用 mobx (某宝克斯)库,在 store 中进行全局状态管理。
小程序如何实现下拉刷新
首先在全局配置中开启 enablePullDownRefresh,然后在下拉刷新时调用 onPullDownRefresh 处理刷新逻辑,处理完成后调用 wx.stopPullDownRefresh 停止下拉刷新。
小程序的性能优化方法有哪些
首先避免频繁调用 setData,然后对图片进行压缩并使用懒加载,还有启用分包、合理使用缓存都可以优化小程序的性能。
小程序如何实现图片懒加载
给 image 组件设置 lazy-load 属性,小程序会自动处理基本的懒加载,然后可以创建一个 IntersectionObserver 对象,监听元素是否进入可视区域,从而决定是否加载图片资源。
🍀 小程序如何进行分包
在 app.json 中配置 subPackages 字段,把不常用的页面或功能拆分为独立子包,按需加载以降低主包体积,优化启动速度。
小程序的缓存机制
可以通过 wx.setStorage 存储数据,然后通过 wx.getStorage 获取数据。
Uni-app 多平台运行的原理
Uni-app 底层通过 AST 抽象语法树将 Vue 代码拆解,编译成各个平台的原生代码,从而实现多平台运行。
Uni-app 的条件编译是什么
Uni-app 的条件编译可以通过注释标记,将指定的代码编译到不同平台。
⚡️ 🍀 Uni-app 如何实现下拉刷新和上拉加载
Uni-app 跟小程序类似,首先在全局配置中开启 enablePullDownRefresh,然后在下拉刷新时调用 onPullDownRefresh 处理刷新逻辑,而上拉加载可以通过监听 onReachBottom 事件实现。
前端工程化
常用的 Git 命令
- git init 初始化;
- git clone 克隆代码;
- git add 添加到暂存区;
- git commit 提交代码;
- git pull 拉取代码;
- git push 推送代码;
- git status 查看工作区;
- git branch 查看分支;
- git branch 加分支名创建分支;
- git checkout 加分支名切换分支;
- git merge 合并分支。
说说 git rebase
git rebase 变基操作是将提交到某一分支上的修改应用到另一分支上,风险是会重写提交历史。例如我在公共分支上做了 rebase,然后强制推送,可能会导致他人的本地历史跟远程不一致,需要重新拉取并处理合并问题。因此一般只在个人分支上用 rebase,在公共分支上用 merge。
git revert 和 git reset 的区别
git revert 是用新的提交抵消之前的修改,而 git reset 是直接进行版本回退。
git 如何撤销 git add
可通过 git reset 来撤销 git add。
Git 和 SVN 的区别
git 是分布式的,每个开发者都有完整仓库,可以离线工作;而 svn 是集中式的,所有版本历史都在服务器上。
⚡️ 🍀 前端为何要进行打包和构建
主要是为了优化性能和提高可维护性。首先,通过打包将多个文件进行合并,可以减少 HTTP 请求;其次,通过 Babel 对 ES6 等语法进行转换,可以兼容老版本的浏览器;另外,工程化开发可以统一构建流程和产出标准,并集成项目规范。
⚡️ 🍀 对前端工程化的理解
从模块化的角度看,工程化是对代码进行拆分,JS 可以通过 CommonJS 或 ES Module 来实现,而 CSS 可以通过 Styled-Component 等模块化方案来实现;从组件化的角度看,工程化是将页面拆分成各个组件,方便管理;从规范化的角度看,工程化包括 ESLint 等代码规范和 Git 工作流;从自动化的角度看,工程化包括 Webpack、Vite 等打包构建工具和 CI/CD 等自动化流程。
⚡️ 🍀 对 CI/CD 的理解
CI/CD 是一种持续集成和持续交付的解决方案。其中 CI 指的是频繁合并代码到主分支,并进行自动化测试;而 CD 指的是自动化部署到生产环境。这种流程可以快速迭代和保证质量。
如何捕获项目中的错误
可通过 try catch 或 onerror 事件来捕获错误,另外 React 还可以通过创建错误边界组件来捕获错误,Vue 可以通过 app.config.errorHandler 来捕获错误。
对前端低代码的理解
前端低代码是通过可视化界面或拖拽操作来生成代码的技术,可以降低开发门槛,快速开发应用,适用于表单后台等高复用的场景。
🍀 pnpm 有什么优势
pnpm 通过硬链接和符号链接,取代传统的依赖安装方式,节省了磁盘空间,安装速度更快,而且还可以避免重复安装。
peerDependencies 是干嘛的
peerDependencies 用于声明当前包正常运行时所依赖的包版本,主要用于避免重复安装和版本冲突。
Prettier 和 ESLint 有什么区别
Prettier 专注于代码格式化,保证代码风格统一;而 ESLint 用于检查代码质量,保证语法准确,两者可结合使用。
🍀 对 babel 的理解
babel 是一个 JS 编译器,用来转换和兼容语法。原理是读取 JS 文件中的字符流,然后通过词法解析生成 Token,再通过语法解析将 Token 转为 AST 抽象语法树,接着遍历抽象语法树的节点,进行分析和转换,得到新的抽象语法树,最后编译成向后兼容的代码。
⚡️ 🍀 babel-runtime 和 babel-polyfill 的区别
babel-runtime 主要是处理新 API 的语法转换,不会污染全局环境;而 babel-polyfill 主要是解决浏览器兼容性问题,填补缺失的新 API,会修改全局对象,而且还会增加打包体积。
为何 Proxy 不能被 Polyfill
Proxy 是浏览器原生 API,用于拦截对象的基本操作。而 Polyfill 只能用 ES5 语法模拟,其中 Object.defineProperty 无法实现底层拦截,所以 Proxy 不能被 Polyfill。
⚡️ 🍀 babel 和 webpack 的区别
babel 是编译工具,只做语法转换,不处理模块化和打包;而 webpack 是打包工具,处理模块依赖,对资源进行打包。
🍀 Webpack 的打包原理
webpack 是一个 JS 打包工具,原理是根据文件的依赖关系进行静态分析,然后将模块按指定规则生成静态资源,在处理程序时递归构建一个依赖关系图,最后将所需模块打包成 bundle 文件,通过代码分割成单元片段并按需加载。
⚡️ 🍀 Webpack 的构建流程
首先初始化参数并进行编译,然后从入口文件开始,调用 loader 对模块进行转换,得到模块依赖关系图,然后根据依赖关系图生成 Chunk,再把 Chunk 转换成文件并输出到文件系统中。
loader、plugin、bundle、chunk、module 分别是什么
- loader 是转换器,用于转换模块的源代码;
- plugin 是插件,用于扩展 webpack 的功能;
- bundle 是 webpack 打包出来的文件;
- chunk 是 webpack 进行模块依赖分析分割出来的代码块;
- module 是模块,一个模块对应着一个文件。
webpack 常见的 loader 有哪些
- babel-loader 用于将 ES6 转为 ES5;
- ts-loader 用于将 TS 转为 JS;
- sass-loader 用于将 sass 转为 css。
webpack 常见的插件有哪些
HtmlWebpackPlugin 用于自动生成 HTML 文件并将 webpack 打包后的资源注入该文件中;然后 MiniCssExtractPlugin 用于提取 CSS 到单独的文件中;还有一个 HotModuleReplacementPlugin 可用于实现热更新功能。
🍀 对 HMR 热更新的理解
HMR 热更新指的是在不刷新页面的情况下去更新修改后的模块。原理是通过 HMR Server 监听模块的变化,生成记录更新的 manifest 和 update chunk 文件,然后通过 WebSocket 长连接发送给浏览器,浏览器再通过 HMR Runtime 机制加载这两个文件,对修改的模块进行更新。
⚡️ 🍀 如何提⾼ Webpack 的打包/构建速度
首先可以打开 cache 配置项,通过 Webpack5 自带的持久化缓存来避免重复打包;其次可以通过 include 和 exclude 来限制 loader 的处理范围,提高匹配速度;另外,Webpack 打包是单线程的,可以通过 thread-loader 开启多线程打包,让 Webpack 同时处理多个打包任务,充分利用多核 CPU 的优势,提升构建速度。
🍀 Webpack5 之前的版本怎么实现缓存
Webpack5 之前的版本可以使用 cache-loader,或给 babel-loader 设置 cacheDirectory 为 true,来缓存 Babel 转译结果。
⚡️ 🍀 如何⽤ Webpack 来优化前端性能
可以通过插件来压缩代码;也可以使用 Tree Shaking,删除未被引用的代码;还可以通过插件对代码进行分块,按需加载,从而提升性能。
⚡️ 🍀 Webpack 如何实现懒加载
Webpack 可以通过 import() 语法动态导入来实现懒加载,Webpack 在处理该语法时会进行代码分割,将 import 的模块单独打包成一个代码快,等到运行时再按需加载。
🍀 Webpack5 有哪些升级
webpack5 加入持久化缓存来提高构建性能;而且对 tree shaking 进行了优化,减小了打包体积;另外还引入了模块联邦功能,允许在多个应用之间共享代码,实现微前端等场景的依赖复用。
⚡️ 🍀 Webpack 的持久化缓存是什么
Webpack 通过配置 cache 的 type 为 filesystem 来开启持久化缓存,开启后会缓存编译结果到磁盘或内存中,以减少重复编译,提升构建速度。
⚡️ 🍀 对 Tree Shaking 的理解
Tree shaking 用来删除未被引用的代码,减小打包体积以提升性能;原理是通过 ES6 模块的静态分析,检查代码中的导入导出,找到未被引用的模块和变量,通过 AST 抽象语法树对它们进行删除,从而减小最终的打包体积。
如何实现 Tree Shaking
可以在 package.json 中配置 sideEffects,标记文件是否有副作用;然后在 webpack 配置中设置 mode 为 production 生产模式,以启用 Tree Shaking。
对 Webpack 模块联邦的理解
Webpack 的模块联邦允许在多个应用之间共享代码,实现微前端等场景的依赖复用。
🍀 Webpack 与 Vite 的区别
Webpack 基于 Bundle,启动和热更新都要打包所有模块重新构建,项目越大速度越慢;而 Vite 基于原生 ES 模块,无需打包,启动更快,不过生产环境下还是会用 Rollup 进行打包。
⚡️ 🍀 Vite 的实现原理
Vite 基于 ES Module 实现按需加载,开发环境下使用 ESBuild 预构建依赖,快速编译,而生产环境下用 Rollup 快速打包。
🍀 Rollup 与 Webpack 的区别
Rollup 基于 ESM 实现高效的 Tree-shaking,适用于工具库的打包;而 Webpack 的代码分割及热更新等功能更为齐全,适用于大型项目的构建。
ESbuild 的源码是怎样的
ESbuild 的源码是用 Go 编写的,它能同时处理多个任务,而且会对 AST 抽象语法树进行优化,运行时还没有额外的负担,因此构建速度很快。
对微前端的理解及解决方案
微前端是将前端应用拆分成多个子应用,每个子应用可以独立开发、测试跟部署,而且可以实现全局资源共享,减少重复加载,提升整体的一个协作效率。可以通过 qiankun 等主流框架接入微前端。
⚡️ 🍀 接入微前端的过程有没有遇到什么问题
在项目中有遇到过微前端的热更新问题,主要表现在子应用的热更新失效,得手动刷新页面才能看到变化。后面定位到是因为微前端沙箱拦截了子应用的 WebSocket 连接,而热更新需要依赖这个连接。解决办法是在主应用中配置沙箱去减少 DOM 拦截,在子应用中配置 Access-Control-Allow-Origin 允许跨域,同时配合 optimizeDeps 选项排除对微前端运行时的依赖优化,去解决热更新问题。
🍀 说说微前端的沙箱隔离机制
微前端中多个子应用共享同一运行环境,容易导致全局变量污染和样式冲突,而沙箱隔离技术可以为每个子应用创建独立的环境,避免互相干扰;像 qiankun 就通过快照沙箱和代理沙箱来隔离 JS 环境,同时通过 Shadow DOM 和作用域沙箱来隔离样式。
快照沙箱和代理沙箱的区别
快照沙箱是记录子应用加载前的全局状态,在子应用卸载时还原该状态,同时缓存子应用运行期间产生的变更,仅支持单实例运行,无法同时处理多个子应用的状态隔离。而代理沙箱是基于 Proxy,为每个子应用创建独立的全局代理对象,子应用的修改仅作用在代理对象上,支持多实例同时运行,qiankun 就默认使用代理沙箱。
🍀 说说微前端的生命周期
子应用一般会暴露 bootstrap、mount 和 unmount 等钩子,由主应用统一调度加载、渲染和销毁。
⚡️ 🍀 微前端如何进行路由管理
微前端通过主应用统一管理路由,像 qiankun 就是在主应用进行路由监听,动态加载或卸载子应用。
🍀 微前端各应用间如何通信
qiankun 中可以通过 initGlobalState 创建全局状态,然后在子应用中通过 onGlobalStateChange 进行监听,再通过 setGlobalState 去修改状态。
浏览器丨性能
🍀 常见浏览器的内核
- Chrome 和 Edge 是 Blink 内核;
- Safari 是 WebKit;
- Firefox 是 Gecko;
- IE 以前是 Trident 现在也是 Blink。
🍀 浏览器渲染进程有哪些线程
浏览器渲染进程包括 GUI 渲染线程、JS 引擎线程、事件触发线程、定时器线程和异步 http 请求线程。
浏览器的渲染过程
浏览器的渲染过程是解析 HTML 和 CSS,分别构建 DOM 树和 CSS 规则树,然后两者结合生成 Render 渲染树,再生成布局,绘制到页面中。
渲染过程中遇到 JS 文件会如何处理
浏览器渲染过程中遇到 JS 会暂停文档的解析,直到 JS 执行完。如果要加快首屏渲染,可以将 script 标签放在后面,也可以给 script 标签设置 defer 或 async 属性。
script 标签 defer 和 async 的作用
浏览器渲染过程中遇到 JS 会暂停文档的解析,直到 JS 执行完。如果 script 标签设置了 defer 属性,JS 会并行加载,但会等到页面渲染完再按顺序执行;而如果设置了 async,JS 脚本也会并行加载,但加载完会立即执行,而且不保证执行顺序。
onload 和 DOMContentLoaded 的区别
onload 在所有资源加载完才触发;而 DOMContentLoaded 只要 DOM 树构建完就会触发,其它资源可能还未加载完。
🍀 浏览器输入 url 发生了什么
首先解析域名;然后三次握手建立 TCP 连接;接着浏览器向服务器发送 HTTP 请求,拿到响应结果后处理页面;最后四次挥手断开 TCP 连接。
对三次握手的理解
TCP 三次握手指的是建立连接时,需要在客户端和服务器之间依次发送 SYN、SYN-ACK 和 ACK 这三个包,以确认双方的发送能力和接收能力都正常。如果只有两次握手,客户端可以确认自己的发送能力和接收能力正常,但服务器只能确认自己的接收能力正常,而无法确认发送能力是否正常。
对四次挥手的理解
TCP 断开连接需要四次挥手,首先,客户端发送断开连接的 FIN 请求作为第一次挥手,服务器收到后不会立即断开连接,而是先发送一个 ACK 包作为第二次挥手,告知客户端已收到断开连接的请求;等到服务器的数据发送完毕后,才发送 FIN 请求作为第三次挥手;而为了确认这个 FIN 请求发送出去,需要客户端最后发送一个 ACK 包作为第四次挥手,服务器收到后就可以断开连接了。
BOM 和 DOM 的区别
BOM 是浏览器对象模型,用来控制浏览器的行为;而 DOM 是文档对象模型,用来操作页面元素。
查看浏览器的类型
navigator.userAgent。
🍀 查看当前页面的宽高
window.innerWidth、window.innerHeight。
浏览器前进、后退
history.forward()、history.back()、history.go()。
如何实现前端自动化性能监控
通过 PerformanceObserver 等 API 采集性能指标,结合埋点上报,定期收集和分析数据。
对数据埋点的理解
数据埋点主要是收集用户行为数据,用于分析用户习惯和优化产品。一般可以利用 img 标签没有跨域访问的限制,通过 src 属性拼接参数发送日志来实现埋点;也可以通过像友盟这类第三方 SDK 来实现埋点。
⚡️ 🍀 如何提升页面性能
首先采集页面关键的性能指标,然后根据各个指标,拆分成视觉交互和加载时长两个方向的优化。针对视觉交互,可以从屏幕帧率入手,减少重排和重绘,并利用防抖和节流,减少调用频率;而针对加载时长,可以对资源进行压缩合并,减少 HTTP 请求,配合 CDN 来加速静态资源的载入;其次,充分利用懒加载,可以减少首次加载的资源量,充分利用缓存,可以减少重复请求;另外,可以利用 SSR 服务端渲染来提升首屏加载速度。
⚡️ 🍀 首屏加载缓慢原因,如何优化
首屏加载缓慢有可能是网络延时、资源太大或脚本阻塞等原因导致的;解决方法有对资源进行压缩合并,减少 HTTP 请求,配合 CDN 来加速静态资源的载入;其次,充分利用懒加载,可以减少首次加载的资源量,充分利用缓存,可以减少重复请求;另外,可以利用 SSR 服务端渲染来提升首屏加载速度。
🍀 白屏时间是什么,如何计算
白屏时间指的是从用户输入 URL 到页面首次渲染的时间,可以通过 performance.timing 的 domLoading 和 navigationStart 作差来计算白屏时间。
🍀 重排和重绘的区别
重排(reflow)指的是元素的几何属性发生变化,导致重新布局;而重绘(repaint)指的是元素样式变化但不影响布局;其中,重排一定会触发重绘,性能开销更大。
如何避免重排和重绘
尽量使用 transform 来实现位移、避免频繁修改样式和操作 DOM 等等,都可以避免重排和重绘。
🍀 requestAnimationFrame 的作用
requestAnimationFrame 会在浏览器下次重绘前执行回调函数,可以替代定时器来制作动画,由于动画频率与浏览器刷新频率一致,因此不会出现闪动,可以保证动画的流畅。
防抖和节流的区别和使用场景
防抖(Debounce)是每隔一段时间后执行任务,如果这期间又被触发,则重新计时;而节流(Throttle)是在每段时间内只执行一次任务。防抖可用于优化搜索联想,节流可用于优化元素拖拽。
Web 页面如何实现图片的懒加载
给 img 标签设置 loading="lazy",浏览器会自动处理基本的懒加载,然后可以创建一个 IntersectionObserver 对象,监听元素是否进入可视区域,从而决定是否加载图片资源。
懒加载和预加载的区别
懒加载是按需加载,目的是减少首次加载的资源量;而预加载是提前加载,目的是提高后续页面的加载速度。
🍀 接口返回大量数据如何优化
最常见的是通过分页,减少单次请求的数据量;也可以使用虚拟滚动,只渲染可视区域的元素;还可以启用 gzip,对返回的数据进行压缩,以提高传输效率;另外可以通过 Web Worker,将数据处理放在后台线程中进行,这样不会阻塞主线程。
🍀 对虚拟滚动的理解
虚拟滚动是一种优化长列表渲染性能的技术,只渲染可视区域的元素。原理是通过监听滚动事件,计算出当前需要渲染的数据范围,并复用 DOM 节点,模拟完整的滚动列表,减少内存占用和 DOM 操作,提高渲染效率。
<div class="container" id="virtualContainer"></div>
// 渲染可见区域的项目
const renderVisibleItems = (container, data, itemHeight) => {
// 1. 计算当前滚动位置
const scrollTop = container.scrollTop;
// 2. 计算可见区域的起始索引:当前滚动高度 / 每项高度(向下取整)
const visibleStartIndex = Math.floor(scrollTop / itemHeight);
// 3. 计算可见区域的结束索引:起始索引 + 可视区域能容纳的项数(向上取整)
const visibleEndIndex = Math.min(
visibleStartIndex + Math.ceil(container.clientHeight / itemHeight),
data.length
);
// 4. 清空当前内容,准备重新渲染
container.innerHTML = '';
// 5. 创建一个与总数据高度相同的占位元素,用于模拟完整列表的滚动体验
const scrollArea = document.createElement('div');
scrollArea.className = 'scroll-area';
scrollArea.style.height = `${data.length * itemHeight}px`;
container.appendChild(scrollArea);
// 6. 只渲染可见区域内的数据项
for (let i = visibleStartIndex; i < visibleEndIndex; i++) {
const item = data[i];
const itemElement = document.createElement('div');
itemElement.className = 'item';
// 7. 使用绝对定位将元素放置在正确的位置
itemElement.style.top = `${i * itemHeight}px`;
// 8. 设置元素内容
itemElement.textContent = `${item.name} - ${item.content}`;
// 9. 将元素添加到滚动区域
scrollArea.appendChild(itemElement);
}
};
// 初始化虚拟滚动
document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('virtualContainer');
const data = [...]; // 生成10000条测试数据
const itemHeight = 30; // 每个列表项的高度(固定值)
// 初始渲染:加载页面时显示第一屏内容
renderVisibleItems(container, data, itemHeight);
// 监听滚动事件:用户滚动时重新计算并渲染可见区域
container.addEventListener('scroll', () => {
renderVisibleItems(container, data, itemHeight);
});
// 虚拟滚动的核心优势:
// 1. 无论数据量多大(10000条甚至更多),DOM中始终只有可见区域的元素
// 2. 避免了一次性渲染大量数据导致的性能问题
// 3. 用户体验与传统长列表无异,但性能显著提升
});
什么是 PWA
PWA(Progressive Web App)是一种渐进式 Web 应用,通过 Service Worker 实现离线访问、推送通知等功能,具备原生应用的体验。
🍀 对 Service Worker 的理解
Service Worker 是一个运行在浏览器后台的独立线程,可以拦截网络请求、缓存资源和实现离线功能,是实现 PWA 渐进式 Web 应用的关键技术。
⚡️ 🍀 对 Web Worker 的理解
Web Worker 是浏览器提供的一种多线程技术,可以把一些繁重的任务放到一个独立线程中去跑,这样不会阻塞主线程,处理完的任务通过 postMessage 传回主线程,然后主线程通过 onmessage 事件来接收。Web Worker 虽然不能直接操作 DOM,但用来处理文件导出和音视频解析等这类吃 CPU 的任务很实用。
⚡ 🍀 对 IndexedDB 的理解
IndexedDB 是一个运行在浏览器上的非关系型数据库,用于在客户端存储大量结构化数据,存储容量由浏览器和磁盘空间决定,支持事务和索引,适合离线应用。
⚡ 🍀 如何实现浏览器标签页之间的通信
可以使用 WebSocket 来通过服务器中转;也可以使用 BroadcastChannel API 创建一个消息通道,各标签页可以通过该通道发送和接收消息;还可以直接通过 localStorage 来实现通信,当一个标签页修改了 localStorage 时,其他标签页可以通过监听 storage 事件来获取变化后的数据。
网络丨服务端
OSI 七层模型是怎样的
应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。

TCP/IP 五层模型是怎样的
应用层、传输层、网络层、数据链路层、物理层。
🍀 TCP 与 UDP 的区别
TCP 和 UDP 都是传输层协议;但 TCP 是面向连接的协议,保证数据可靠送达;而 UDP 是无连接的协议,速度快但不可靠,适用于视频直播这种实时性要求高的场景。
🍀 TCP 的有哪些机制(TCP 为什么可靠)
TCP 的机制除了三次握手和四次挥手外,还有重传机制,会在数据丢失时重新发送;以及流量控制,防止发送方发得太快;另外还有拥塞控制,通过慢启动算法控制数据发送量,避免网络堵塞。
🍀 DNS 是什么
DNS 是域名系统,负责把输入的域名转为 IP 地址。
🍀 对 HTTP 的理解
HTTP 是基于应用层的超文本传输协议,用于客户端和服务器之间的通信,它是无状态的,每次请求都是独立的。
为什么 HTTP 是无状态的
HTTP 的无状态指的是每个请求都是独立的,服务器不保存请求的信息,这么设计了为了简化通信过程,如果要保存状态,需要用到 Cookie 或 Session。
HTTP 报文的组成部分
HTTP 报文包括请求报文和响应报文,请求报文包括请求行、请求头、空行和请求体,响应报文包括状态行、响应头、空行和响应体。
HTTP 的报文头部有哪些字段
常见的像 Host 字段表示请求的服务器域名;User-Agent 表示客户端信息;Content-Type 表示数据类型;Cache-control 表示缓存策略等等。
HTTP 状态码有哪些
- 1 开头表示指示信息;
- 2 开头表示响应成功;
- 3 开头表示重定向;
- 4 开头表示客户端错误;
- 5 开头表示服务端错误。
HTTP 301、302、304 的区别
- 301 表示永久重定向;
- 302 表示临时重定向;
- 304 表示请求的资源未发生变化,可以直接复用本地缓存;
301 和 302 哪个对 SEO 更友好
301 对 SEO 更友好,因为永久重定向会传递 SEO 权重,搜索引擎会更新索引;而 302 临时重定向不传递权重。
HTTP 400、401、403、404 的区别
- 400 表示请求报文语法错误;
- 401 表示请求需要通过 HTTP 认证;
- 403 表示请求资源被服务器禁止访问;
- 404 表示请求资源不存在;
常见的 HTTP 请求方法
- GET 和 POST 用于获取和提交数据;
- PUT:用于更新资源;
- DELETE:用于删除资源;
- HEAD:用于获取头信息;
- OPTIONS:用于查询服务器支持的方法。
GET 和 POST 的区别
GET 的请求参数放在 URL 中,有长度限制;而 POST 的请求参数放在请求体中,更为安全而且没有长度限制;
对短连接和长连接(持久连接)的理解
HTTP 1.0 默认使用短连接,每次响应后立即断开;而 HTTP 1.1 默认使用长连接(持久连接),客户端与服务器的连接持续有效,后续的请求可以通过这个连接发送,减少开销。
🍀 对管线化和多路复用的理解
管线化是在一个连接上并行发送多个请求,但响应需要按顺序一个一个传回,容易阻塞;而多路复用是 HTTP2 引入的新技术,现在多个响应也可以在一个连接上并行传回了,解决了管线化的阻塞问题。
🍀 HTTP 与 HTTPS 的区别
HTTP 无需证书,默认端口是 80;而 HTTPS 需要 SSL 证书,可进行加密传输,更为安全,URL 以 https 开头,默认端口是 443。
HTTPS 是如何保证安全的
HTTPS 在 HTTP 的基础上加了 SSL/TLS 加密层,通过对称加密和非对称加密来保证数据隐私,通过摘要算法来保证数据完整,通过数字证书来验证服务器身份。
🍀 HTTPS 的加密算法是怎么样的
HTTPS 使用对称加密和非对称加密。其中,在交换密钥阶段使用非对称加密,发送方用公钥加密,接收方用私钥解密;在交换报文阶段则使用对称加密,将密钥发送给对方,以共享密钥的方式加密数据。
🍀 HTTPS 的摘要算法是怎么样的
HTTPS 的摘要算法把数据进行摘要计算,生成固定长度的哈希值,用于验证数据的完整性。
🍀 HTTP 2.0 有什么升级
HTTP/2 基于 TCP 协议,支持二进制分帧传输,而且引入了 Header 压缩和多路复用,提升了传输效率,另外还允许服务器主动推送资源。
🍀 HTTP 3.0 有什么升级
HTTP/3 使用 QUIC 协议,而 QUIC 基于 UDP,UDP 是无连接的,没有握手和挥手的过程,提高了传输效率,而且解决了 TCP 的队头阻塞问题。
对 WebSocket 的理解
WebSocket 是 HTML5 新增的全双工通信协议,客户端和服务器只需一次握手,就可以创建持久连接,并进行双向通信,适用于实时聊天和股票等场景。
🍀 什么是轮询
轮询指的是客户端定时向服务器发送请求,包括短轮询和长轮询。其中,短轮询是在服务器每次收到请求后立刻响应;而长轮询是在请求的过程中,先将连接挂起,直到服务器有了新数据才响应,以减少不必要的请求次数。
HTTP 与 WebSocket 的区别
HTTP 是单向短连接协议,需要三次握手;而 WebSocket 是全双工通信协议,只需一次握手,就可以创建持久连接,并进行双向通信;
🍀 长连接与 WebSocket 的区别
长连接指的是客户端与服务器的连接持续有效,后续的请求可以通过这个连接发送,但仍是单向通信且需客户端主动轮询;而 WebSocket 是真正意义上的全双工通信,服务器可以主动推送数据,不需要客户端轮询。
对 CDN 的理解
CDN 即内容分发网络,把资源缓存到离用户最近的服务器上,降低延迟。
🍀 CDN 的原理
当用户访问 CDN 地址时,会通过 DNS 调度,确定最适合的 CDN 节点,然后返回 IP 地址转发给用户去完成重定向。
对 Ajax 的理解
AJAX 是一种异步请求技术,允许在不刷新页面的情况下,通过 XHR 对象向服务器发送异步请求,拿到数据后局部更新页面内容。
XMLHttpRequest 是什么
XMLHttpRequest 对象用来向服务器发送请求和处理响应,整个过程不会触发页面刷新。
如何创建一个 Ajax
- 首先创建 XMLHttpRequest 对象;
- 然后调用
open方法创建 HTTP 请求; - 接着设置响应 HTTP 请求状态变化的函数;
- 最后调用
send方法向服务器发送请求,获取异步调用返回的数据。
Axios 是什么
Axios 是个基于 Promise 的 HTTP 请求库,支持 Promise API,可以拦截请求和响应,也可以转换请求和响应数据,还可以防止 CSRF 的攻击。
⚡️ 🍀 Axios 的原理
Axios 的原理是对 XMLHttpRequest 或 Fetch 进行封装,在请求派发前依次执行请求拦截器,在派发时处理配置,然后 HTTP 适配器根据处理后的配置发起请求,在响应后依次执行响应拦截器,最终返回 Promise。
🍀 对 Fetch 的理解
Fetch 是基于 Promise 的网络请求 API,写法更友好,而且通过数据流来处理数据,可以分块读取;但只对网络请求报错,服务器返回 400 或 500 时会当作成功的请求。
Ajax、Axios、Fetch 的区别
Ajax 是异步请求技术,Axios 是网络请求库,Fetch 是网络请求 API。
url 的组成
url 包括协议、域名、端口和路径。
如何获取当前 url 参数、网址和协议
- 当前 url 参数可通过
location.search配合URLSearchParams来获取; - 当前网址可通过
location.href来获取; - 当前协议可通过
location.protocol来获取。
// 当前 url:https://www.xxx.com/xxx?a=123&b=456
const queryString = location.search; // ?a=123&b=456
const params = new URLSearchParams(queryString);
params.get('a') // 123
params.get('b') // 456
location.href // https://www.xxx.com/xxx?a=123&b=456
location.protocol // https:
对同源策略和跨域的理解
同源策略是浏览器的一个安全机制,当协议、域名和端口都相同时就是同源,反之就是跨域。
如何解决跨域
可以通过代理、CORS 跨域资源共享或 JSONP 来解决跨域。
对代理的理解
代理用于接收和转发请求,包括正向代理和反向代理。正向代理代理的是客户端,服务器不知道自己收到的是来自代理的访问,例如 VPN 就是正向代理;而反向代理代理的是服务器,客户端不知道自己访问的是代理服务器,可以用 Nginx 来做反向代理。
🍀 Node.js 反向代理具体怎么配
可以通过原生的 http-proxy 或 express 的 http-proxy-middleware 中间件配置代理路径和目标地址,将请求转发到后端服务并处理跨域,实现反向代理。
⚡️ 🍀 对 Nginx 的理解
Nginx 是一个高性能的 HTTP 服务器和反向代理服务器,可用于反向代理和负载均衡。
🍀 对 CORS 的理解
CORS 即跨域资源共享,用来解决跨域问题。简单请求可直接给响应头设置 Access-Control-Allow-Origin 来实现 CORS;而复杂请求会先触发 CORS 的预检请求,需要先通过预检请求,再设置响应头来实现 CORS。
🍀 对 JSONP 的理解
JSONP 是利用 <script> 标签没有跨域限制的漏洞,通过 src 属性发送 GET 请求,配合服务器拼凑拿到数据,从而解决跨域问题。虽然兼容性好,但只支持 GET 请求,而且不安全。
⚡ 🍀 CSRF 攻击原理与防御措施
CSRF 即跨站请求伪造,主要是诱导用户访问危险网站,然后利用用户的登录状态来冒充用户发送危险请求;防御措施有使用 CSRF Token 验证、对请求头的 Referer 进行验证、以及设置 Cookie 的 Samesite 属性来限制第三方 Cookie 等等。
⚡ 🍀 XSS 攻击原理与防御措施
XSS 即跨域脚本攻击,主要是通过注入恶意脚本来进行攻击,具体包括注入数据库的存储型 XSS 攻击、注入 URL 的反射型 XSS 攻击以及注入前端行为的 DOM 型 XSS 攻击;防御措施有对脚本进行转义、使用 CSP 建立资源白名单、以及设置 Cookie 的 http-only 属性等等。
🍀 什么是 SQL 注入
SQL 注入是一种注入数据库的存储型 XSS 攻击,主要是将恶意的 SQL 语句注入到前端请求的参数中,然后在后端解析执行;防御措施有对脚本进行转义、使用 CSP 建立资源白名单和设置 Cookie 的 http-only 属性等等。
常用的 Linux 命令有哪些
- cd 切换目录
- ls 查看当前目录和文件
- touch 创建文件
- mkdir 创建目录
- rm 删除文件或目录
- cat 查看当前文件内容
Nodejs 是什么
Node.js 是个基于 Chrome V8 引擎的 JS 运行环境。在 Nodejs 出现前 JS 只能在浏览器运行,Nodejs 出现后 JS 可以在安装 Nodejs 的环境下运行,使得 JS 也能开发后端服务。
Nodejs 与前端 JS 的区别
Nodejs 和前端 JS 都基于 ECMAScript 语法,但前端 JS 用的是 Web API,在浏览器运行,而 Nodejs 用的是 Node API,在服务端运行。
Nodejs 如何调试
可以使用 node --inspect 命令启动调试,然后在浏览器中打开 chrome://inspect 页面,选择要调试的 Nodejs 进程,就可以使用 Chrome 开发者工具进行调试,例如设置断点、查看变量值等操作。
🍀 Nodejs 如何获取当前文件和路径
可通过全局变量 __filename 来获取当前文件的绝对路径;通过 __dirname 来获取当前文件所在的目录路径。
path.join 和 path.resolve 的区别
path.join 和 path.resolve 都用于拼接文件路径;但 path.join 只是按路径片段进行简单拼接;而 path.resolve 会解析绝对路径,如果参数中有带 '/' 的绝对路径,则从该绝对路径开始进行拼接。
对 Cookie 的理解,有什么缺点
Cookie 是服务器发送给浏览器并保存在本地的一小块数据,它会在浏览器下次发送请求时携带给服务器,可用于辨别用户身份。缺点是存储空间有限,通常只有 4k 左右;而且每次发送请求时携带 Cookie,会增加请求开销;另外 Cookie 存在安全隐患。
⚡️ 🍀 Cookie 有什么安全问题,如何解决
Cookie 可能被 XSS 或 CSRF 攻击。可以设置 HttpOnly 属性,防止 Cookie 被 JS 读取;也可以设置 SameSite 属性为 Strict 或 Lax,限制 Cookie 的跨站请求;还可以设置 Secure 属性,使 Cookie 只能通过 HTTPS 协议传输。以此来保证 Cookie 的安全性。
对 Session 的理解
Session 保存在服务器,用于存储用户的会话状态。当浏览器首次请求时,服务器会根据提交的信息创建 Session,并生成唯一的 Session ID,通过 Cookie 返回给浏览器,浏览器在后续请求中会携带该 Session ID,服务器根据这个 Session ID 查找对应的 Session,从而获取用户的会话状态。
🍀 Cookie 和 Session 的区别
Cookie 保存在客户端,存储空间有限,而且存在安全隐患;而 Session 保存在服务端,存储空间更大,安全性也更高。另外,Cookie 可以设置过期时间,而 Session 一般在浏览器关闭后失效。
SessionStorage 和 LocalStorage 是什么
SessionStorage 用于保存临时数据,当会话关闭时数据会被清空;而 LocalStorage 将数据永久保存在浏览器内,需要手动清除。
浏览器的缓存机制是什么
浏览器的缓存机制是将请求的资源存储到本地,供下次访问使用,以提高加载速度;主要包括强缓存和协商缓存。
强缓存和协商缓存的区别
强缓存指的是浏览器不向服务器发送请求,直接从缓存中读取资源;而协商缓存指的是强缓存失效后,浏览器向服务器发送请求,用携带的 ETag 或 Last-Modified 验证是否过期,如果请求的资源未发生变化则返回 304,直接复用本地缓存。
🍀 什么是 ETag
ETag 是服务器为每个资源生成的唯一标识,客户端在下次请求时会携带该 ETag,服务器根据这个 ETag 来判断资源是否发生变化,如果没变则返回 304,告知客户端可以直接复用本地缓存。
如何设计一个登录业务
- 用户密码方面,可以限制输入简单密码,对密码进行加密存储,同时使用 HTTPS 传输来提升安全性;
- 登录状态方面,可以用 session 来保存用户登录状态,并用 Redis 来存储 session;
- 密码破解方面,可以用验证码配合输入失败次数来防止密码被多次尝试。
Session 是如何验证登录状态的
当浏览器首次请求时,服务器会根据提交的信息创建 Session,并生成唯一的 Session ID,通过 Cookie 返回给浏览器,浏览器在后续请求中会携带该 Session ID,服务器根据这个 Session ID 查找对应的 Session,从而获取用户的会话状态。
🍀 Session 为何要存储到 Redis
因为 Redis 的读写速度快,而且支持分布式存储,多个服务器可以共享 Session 数据,因此将 Session 存储到 Redis 中。
⚡️ 🍀 什么是 JWT?如何实现无状态认证
JWT 是包含签名令牌的 JSON 结构,由 Header、Payload 和 Signature 组成,客户端携带令牌发送给服务器,服务器通过验证签名实现无状态身份认证。
⚡️ 🍀 什么是单点登录,前端如何实现单点登录
单点登录(SSO)允许用户一次登录,访问多个系统,实现过程是前端将用户重定向至认证服务器获取 JWT,存储后在请求头携带 Bearer Token 进行鉴权,子系统验证令牌有效性后授权访问。
⚡️ 🍀 前端项目如何缓存服务端请求
可以利用一些第三方库来实现,例如 React 中的 React Query 和 Vue 中的 Vue Query,这些库可以很方便的去实现数据的缓存和管理。
Koa 和 Express 的区别
Koa 不涉及路由及其他中间件的捆绑,更为轻量化,而且 Koa 支持 async/await 的写法,在异步处理上比 Express 的 Promise 回调写法更友好;另外,Koa 使用洋葱圈模型的函数调用栈执行中间件,先调用的后执行完,而 Express 使用先进先出的任务队列执行中间件,先调用的先执行完。
⚡️ 🍀 描述 Express 和 Koa2 中间件机制
中间件是个处理 HTTP 请求的函数,在匹配请求路由时执行指定操作,调用 next() 方法会继续往下匹配。Express 使用先进先出的任务队列执行中间件,先调用的先执行完;而 Koa 使用洋葱圈模型的函数调用栈执行中间件,先调用的后执行完。
描述 koa 洋葱圈模型
Koa 的洋葱圈模型是在中间件之间通过 next() 方法联系,当中间件调用 next() 方法后,会将控制权交给下一个中间件,以此类推,直到下一个中间件不再执行 next() 方法,此时沿路返回,依次将控制权交回上一个中间件,因此先调用的后执行完。
Nodejs 如何逐行操作日志
原生 Nodejs 可通过 readline 模块配合 fs 模块的 Stream 数据流来逐行操作日志;而 Koa / Express 等框架可以使用 morgan 中间件来实现。
🍀 Nodejs 线上为何要开启多进程,如何开启
Node.js 开启多进程可以充分利用多核 CPU,提高应用的性能和并发处理能力。可通过 PM2 进程管理工具来开启多进程,除此之外 PM2 还可用于进程守护和线上日志记录等功能。
🍀 SSR 和 SSG 的区别
SSG 是静态站点生成,而 SSR 是服务端渲染,都是在服务端完成 HTML 的渲染,再返回给浏览器解析。但 SSG 的 HTML 是在构建时生成,而 SSR 的 HTML 是在请求时生成,因此 SSG 性能比 SSR 更好,但数据修改时需要重新部署,适用于博客等静态页面。
设计模式丨算法
描述 SOLID 五大设计原则
- S 是单一功能原则;
- O 是开放封闭原则;
- L 是里式替换原则;
- I 是接口隔离原则;
- D 是依赖反转原则。
JS 有哪些设计模式
工厂模式、单例模式、适配器模式、装饰器模式、迭代器模式、观察者模式、代理模式。
描述工厂模式
工厂模式指的是封装一个接口来创建不同类型的对象;可以把类的实例化推迟到子类中进行,以减少耦合。
描述单例模式
单例模式指的是确保一个类只有一个实例,避免创建多个实例造成资源浪费。
描述适配器模式
适配器模式指的是将一个接口转为另一个接口,以解决兼容性问题。
描述装饰器模式
装饰器模式指的是在不改变原对象结构的基础上,动态添加相应的功能,例如 JS 的高阶函数或 @decorator 语法。
描述迭代器模式
迭代器模式指的是在不暴露集合内部结构的情况下,对集合进行遍历。
描述观察者模式
观察者模式指的是将一个对象的状态变化通知给观察它的对象。
描述代理模式
代理模式指的是将一个对象作为另一个对象的代理,可以控制对象的访问,实现基本操作的拦截和自定义。
数组和链表的区别
数组是连续存储的线性结构,查询速度更快;而链表是不连续存储的非线性结构,插入和删除速度更快。
算法稳定性是什么,哪些是不稳定算法
稳定算法指的是相同值在排序前后的相对位置没有改变。不稳定算法包括:堆排序、希尔排序、快速排序、选择排序。
描述冒泡排序
冒泡排序是比较相邻两个数,大的往后移,重复这个过程完成排序;排序的性能取决了数组的有序程度,时间复杂度范围从 O(n) 到 O(n²);由于相同值在排序前后的相对位置没有改变,因此是稳定算法。
function bubbleSort(arr) {
for (let i = arr.length - 1; i > 0; i--) {
for (let j = 0; j < i; j++) {
if (arr[j] > arr[j + 1]) {
const temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return arr;
}
const arr = [6, 5, 3, 4, 2, 1];
console.log(bubbleSort(arr)); // [1, 2, 3, 4, 5, 6]
描述选择排序
选择排序是在每一轮中选出最小的元素放到左边,重复这个过程完成排序;时间复杂度是 O(n²);由于相同值在排序前后的相对位置可能发生变化,因此是不稳定算法。
function selectionSort(arr) {
for (let i = 0, len = arr.length; i < len; i++) {
let minIndex = i;
for (let j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
const temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
return arr;
}
const arr = [6, 5, 3, 4, 2, 1];
console.log(selectionSort(arr)); // [1, 2, 3, 4, 5, 6]
V8 引擎是怎么对数组进行排序的
Chrome V8 引擎中数组排序的算法取决于数组长度,数组长度小于 10 使用插入排序,大于 10 使用快速排序。
🍀 描述插入排序
插入排序是将数组分为已排序和未排序两部分,每次从未排序部分中取出元素,插入到已排序部分的合适位置,重复这个过程完成排序;排序的性能取决于数组长度,时间复杂度范围从 O(n) 到 O(n²);由于相同值在排序前后的相对位置没有改变,因此是稳定算法。
function insertionSort(arr) {
for (let i = 1, len = arr.length; i < len; i++) {
const insert = arr[i];
for (var j = i - 1; j >= 0; j--) {
if (insert < arr[j]) {
const tmp = arr[j];
arr[j + 1] = tmp;
} else {
break;
}
}
arr[j + 1] = insert;
}
return arr;
}
const arr = [6, 5, 3, 4, 2, 1];
console.log(insertionSort(arr)); // [1, 2, 3, 4, 5, 6]
描述归并排序
归并排序是一种分治算法,将数组分为两个子数组分别排序再合并,递归重复这个过程完成排序;时间复杂度为 O(𝑛㏒𝑛);由于相同值在排序前后的相对位置没有改变,因此是稳定算法。
const mergeSort = arr => {
if (arr.length <= 1) {
return arr;
}
const mid = Math.floor(arr.length / 2);
const left = arr.slice(0, mid);
const right = arr.slice(mid);
// 合并成一个有序数组
const merge = (left, right) => {
const result = [];
// 将左右子数组较小的元素放入 result,直到有一个子数组遍历完毕
while (left.length && right.length) {
if (left[0] < right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}
// 此时将另一个子数组剩余的元素放入 result
return result.concat(left).concat(right);
}
return merge(mergeSort(left), mergeSort(right));
}
const arr = [6, 5, 3, 4, 2, 1];
console.log(mergeSort(arr)); // [1, 2, 3, 4, 5, 6]
🍀 描述快速排序
快速排序是一种分治算法,选择一个元素作为基准,对数组进行遍历,小于基准的放到左边,大于基准的放到右边,形成左右两个数组,对它们递归地重复以上步骤完成排序;排序的性能取决于基准的选择,时间复杂度范围从 O(𝑛㏒𝑛) 到 O(𝑛²);由于相同值在排序前后的相对位置可能发生变化,因此是不稳定算法。
const quickSort = (arr) => {
const len = arr.length;
if (len < 2) {
return arr;
} else {
const flag = arr[0];
const left = [];
const right = [];
for (let i = 1; i < len; i++) {
const temp = arr[i];
if (temp < flag) {
left.push(temp);
} else {
right.push(temp);
}
}
return quickSort(left).concat(flag, quickSort(right));
}
};
const arr = [6, 5, 3, 4, 2, 1];
console.log(quickSort(arr)); // [1, 2, 3, 4, 5, 6]
列举数组去重的方式及区别
可以通过嵌套循环的方式去重,也可以通过 ES6 的 Set 数据结构来去重,还可以利用对象 key 值的唯一性来去重。其中,嵌套循环的方式无法对 NaN 和对象去重,ES6 的 Set 数据结构无法对对象去重,利用对象 key 值唯一性的方式则可以对所有类型去重。
// 嵌套循环的方式
const unique = (arr) => {
for (let i = 0, len = arr.length; i < len; i++) {
for (let j = i + 1; j < len; j++) {
if (arr[i] === arr[j]) {
// 如果有重复,就从原数组中删去重复的元素,并修改 len 和 j 的值
arr.splice(j, 1);
len--;
j--;
}
}
}
return arr;
}
// ES6 Set
const unique = (arr) => [...new Set(arr)]
// 对象的方式
const unique = (arr) => {
const result = [];
const obj = {}; // 用来去重的对象
for (let i = 0; i < arr.length; i++) {
if (!obj[arr[i]]) {
result.push(arr[i]);
obj[arr[i]] = 'x';
}
}
return result;
}