常见前端面试题详解

new 操作符

JavaScript中的 new 操作符是对一个构造函数创建实例的过程 如

1
2
3
4
5
function Person(name) {
this.name = name
}
const yueyun = new Person('yueyun')
console.log(yueyun)

new 通过构造器创建的实例能访问构造函数中的属性 可以访问到构造属性原型链的属性

1
2
3
4
5
6
7
8
9
function myNew (Func,...args) {
// 创建一个新的对象
const obj = {}
// 新对象的原型指向原型链
obj.__proto__ = Func.prototype
// 绑定this
const result = Func.apply(obj,..args)
return result instanceof Object ? result : obj
}

为什么要使用虚拟 DOM 虚拟 DOM 的性能一定比真实 DOM 差吗?

虚拟 DOM 比上真实 DOM 有如下几点原因

  • 性能优化: 真实的 DOM 操作是昂贵的(性能消耗较大)。每次更改页面上的某个小部分时,浏览器可能需要重新计算布局并重新绘制整个页面。虚拟 DOM 允许框架在 JavaScript 中构建一个轻量级的 DOM 树副本 配合使用 Diff 算法 计算出渲染为真实 dom 的最小代价操作,再渲染为真实 DOM 利用 JS 运算成本来换取 DOM 执行成本的操作,而 JS 的运算速度快很多。
    • DOM 引擎、JS 引擎 相互独立,但又工作在同一线程(主线程)
    • JS 代码调用 DOM API 必须 挂起 JS 引擎、转换传入参数数据、激活 DOM 引擎,DOM 重绘后再转换可能有的返回值,最后激活 JS 引擎并继续执行若有频繁的 DOM API 调用,且浏览器厂商不做“批量处理”优化,
    • 引擎间切换的单位代价将迅速积累若其中有强制重绘的 DOM API 调用,重新计算布局、重新绘制图像会引起更大的性能消耗。
    • 虚拟 DOM 不会立马进行排版与重绘操作
    • 虚拟 DOM 进行频繁修改,然后一次性比较并修改真实 DOM 中需要改的部分,最后在真实 DOM 中进行排版与重绘,减少过多 DOM 节点排版与重绘损耗
    • 虚拟 DOM 有效降低大面积真实 DOM 的重绘与排版,因为最终与真实 DOM 比较差异,可以只渲染局部
  • 声明式编程:使用虚拟 DOM 允许开发者通过声明式编程更加直观和简洁地描述界面应该呈现的样子。开发者只需关心数据的状态,界面应该如何根据这些状态变化而变化,而不是如何操作 DOM 来实现这些变化
  • 跨平台: 虚拟 DOM 作为一个简单的 JavaScript 对象模型,可以使得同一个应用能够运行在不同的环境中,例如在浏览器、服务器(SSR)、甚至原生移动应用中(如 React Native)。这种跨平台的能力是因为虚拟 DOM 提供了一个与具体平台无关的中间层。

SPA 和 MPA

SPA 即是(single page application) 意思是单页应用 网页应用模型 就是一张 web 页面(index.html)单页面跳转仅刷新局部资源、公共资源仅需要加载一次 即是使用 JS 去感知 URL 的变化去做动态改变

好处即是:性能更快 页面跳转时不用处理html 节约了请求花费 用户体验好 快 良好的前后端分离 传递数据方便 一般是高度交互的应用使用 SPA 比较多

缺点即使:SEO 不友好 内容通过 JavaScript 动态加载

MPA:多页面应用,MPA 在用户浏览不同的页面时,服务器将为每个请求返回新的 HTML 页面。这意味着每次用户请求新页面或刷新现有页面时,整个页面都会被重新加载 适合内容丰富的网站,如新闻网站、电商平台等,它们可以从每个页面的独立 URL 中获益。

路由的 history 和 hash 两种模式

Hash 模式主要是通过 URL 中的哈希值变化来控制页面的显示兼容性好,可以在不支持 HTML5 History API 的老旧浏览器中使用 ,而 History 模式则是利用 HTML5 的 History API 来实现 URL 的变化,URL 看起来更美观,没有#符号,用户体验更接近传统的多页面应用

hash 模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Router {
constructor() {
// 存放url 以及对应的callback
this.routes = {}
this.currentUrl = ''
window.addEventListener(
'load',
() => {
// 回调
},
false
)
window.addEventListener('hashchange', () => {}, false)
}
router(path, cb) {
this.routers[path] = cb
}
push(path) {
this.routes[path] && this.routes[path]()
}
render() {}
}

window.miniRouter = new Router()
miniRouter.route('/', () => console.log('page1'))
miniRouter.route('/page2', () => console.log('page2'))

miniRouter.push('/') // page1
miniRouter.push('/page2') // page2

history 模式

history 模式借用HTML5 history api api 提供了丰富的 router 相关属性先了解一个几个相关的 api

  • history.pushState 浏览器历史纪录添加记录
  • history.replaceState修改浏览器历史纪录中当前纪录
  • history.popStatehistory 发生变化时触发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Router {
constructor() {
// 存放url 以及对应的callback
this.routes = {}
this.listerPopState()
}
init(path) {
history.replaceState({ path: path }, null, path)
this.routes[path] && this.routes[path]()
}
route(path, cb) {
this.routes[path] = cb
}
push(path) {
history.pushState({ path: path }, null, path)
this.routes[path] && this.routes[path]()
}
listerPopState() {
window.addEventListener('popstate', (e) => {
const path = e.state && e.state.path
this.routes[path] && this.routes[path]()
})
}
}

window.miniRouter = new Router()
miniRouter.route('/', () => console.log('page1'))
miniRouter.route('/page2', () => console.log('page2'))

miniRouter.push('/') // page1
miniRouter.push('/page2') // page2

如何给 SPA 做 seo 优化?

  1. 服务端渲染 比如nuxt next
  2. 静态化(静态站点生成 SSG)
    • 一种是通过程序将动态页面抓取并保存为静态页面
    • 通过 WEB 服务器的 URL Rewrite的方式,它的原理是通过 web 服务器内部模块按一定规则将外部的 URL 请求转化为内部的文件地址,一句话来说就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的

React Fiber 架构

Fiber 是什么

Fiber 是 react16 中新的协调引擎, 主要目的是使 Virtual Dom 可以进行增量式的渲染(FiberNode)

  • React 的 Reconciler 基于 Fiber 节点实现, 即是 Fiber Reconciler
  • 作为静态的数据结构来说, 每个 Fiber 节点对应一个 React Element, 保存了该组件类型(函数组件/类组件/原生组件), 对应的 DOM 节点信息
  • 作为动态的工作单位, 每个 fiber 节点保存了本次更新的组件变更状态, 要执行的操作状态(删除/插入/更新)

无论是作为数据结构还是工作单位, 都是使用 FiberNode 的身份存在, 即在 react 架构中命令为 Fiber, 协调器就是 Fiber Reconciler

Fiber 的作用

为了解决 React15 在组件中更新时产生的卡顿现象, 构建出了 Fiber 架构, 并在 React16 中发布, 将同步递归无法终中断的更新 重构为 异步的可中断更新

  • 把可中断的任务拆分成小任务
  • 对正在做的任务调整优先次序, 重做, 复用等
  • 在父子任务间切换, 支持 React 执行过程中的布局刷新
  • 支持 render 返回多个元素
  • error bounday

React 中的架构

  • Scheduler(调度器): 一个渲染工作分解成多个小任务,并在多个帧之间分配和执行这些任务(requestldleCallback)
  • Reconciler(协调器): Reconciler 则从递归变成了可以中断的循环过程。每次循环都会调用 shouldYield 判断当前是否有剩余时间
  • Render(渲染器): 将 Reconciler 打上标签的虚拟 DOM 对象(即 FiberNode)执行成 DOM(JS 变 视图)

Fiber 理论实现

优先级分配 + 异步可中断

FiberNode 的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function FiberNode(
this: $FlowFixMe,
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode
) {
// Instance 作为静态数据结构的属性
this.tag = tag
this.key = key
this.elementType = null
this.type = null
this.stateNode = null

// Fiber 用于连接其他 FiberNode 形成 Fiber Tree
this.return = null
this.child = null
this.sibling = null
this.index = 0

this.ref = null
this.refCleanup = null

// 作为动态的工作单元的属性
this.pendingProps = pendingProps
this.memoizedProps = null
this.updateQueue = null
this.memoizedState = null
this.dependencies = null

this.mode = mode

// Effects
this.flags = NoFlags
this.subtreeFlags = NoFlags
this.deletions = null

// 调度优先级相关
this.lanes = NoLanes
this.childLanes = NoLanes

// 指向该fiber在另一次更新时对应的fiber
this.alternate = null
}

Fiber 是如何工作的

  • ReactDOM.render() (mount) 和 setState (update) 的时候开始创建更新
  • Schedule(调度器)设置优先级, 并将创建的更新加入任务队列, 等待调度
  • 在 RequestldleCallback 空闲时执行任务
  • 从根节点开始遍历 FiberNode,并且构建 WorkInProgress Tree
  • Reconciler(协调器) 阶段生成 EffectList(对其打标签,进行 Diff 对比)
  • Renderer(渲染器) 根据 EffectList 更新 DOM

设计模式

单例模式(Singleton)

确保一个类只有一个实例, 并提供全局访问点来获取该实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Singleton {
// 保存单例实例的私有静态属性
private static instance: Singleton
// 构造函数私有化,防止外部实例化
private constructor() {
// 初始化代码
}
// 获取单例实例的静态方法
public static getInstance(): Singleton {
// 如果实例不存在,创建一个新的实例
if (!Singleton.instance) {
Singleton.instance = new Singleton()
}
// 返回唯一的实例
return Singleton.instance
}
// 示例方法
public someMethod() {
console.log('This is a method of the Singleton class.')
}
}
// 使用单例
const singleton1 = Singleton.getInstance()
const singleton2 = Singleton.getInstance()

// 验证两个实例是否相同
console.log(singleton1 === singleton2) // true

// 调用示例方法
singleton1.someMethod()

工厂模式(Factory)

通过工厂方法创建对象,而不是直接使用new操作符。这样可以隐藏具体实现,并根据需要创建所需类型的对象

假设我们现在有一个产品接口Product和两个具体的产品类ConcreteProductA|ConcreteProductB 我们将使用一个工厂类ProductFactory来创建这些产品的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
interface Product {
operation(): string
}
class ConcreteProductA implements Product {
public operation():string {
return 'Result of the ConcreteProductA'
}
}
class ConcreteProductB implements Product {
public operation():string {
return 'Result of the ConcreteProductB'
}
}

class ProductFactory {
public static createProduct(type:string):Product {
switch(type) {
case: 'A':
return new ConcreteProductA()
case: 'B':
return new ConcreteProductB();
default:
throw new Error('Unknow product type.')
}
}
}

// 创建产品A的实例
const productA: Product = ProductFactory.createProduct('A');
console.log(productA.operation()); // 输出: Result of the ConcreteProductA

// 创建产品B的实例
const productB: Product = ProductFactory.createProduct('B');
console.log(productB.operation()); // 输出: Result of the ConcreteProductB

观察者模式(Observer)

定义了一种对一对多的依赖关系, 当一个对象状态发生改变时, 他的所有依赖者 (观察者) 都会收到通知并自动更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 观察目标
class SubJect {
constructor() {
this.observes = []
}
add(observe) {
this.observes.push(observe)
}
remove(observe) {
this.observes = this.observes.filter((item) => item !== observe)
}
notify() {
this.observes.forEach((item) => {
item.update()
})
}
}

// 观察者
class Observe {
constructor(name) {
this.name = name
}
update() {
console.log('update', this.name)
}
}

const observe1 = new Observe('observe1')
const observe2 = new Observe('observe2')
const sub = new SubJect()
sub.add(observe1)
sub.add(observe2)
sub.remove(observe1)
sub.notify()

装饰器模式(Decorator)

动态地将责任附加到对象上, 通过将对象包装在装饰器对象中, 可以在运行时为对象添加新的行为,对已有的功能进行拓展, 这样不会更改原有的代码, 对其他的业务产生影响 这方便我们在较少的改动下对软件功能进行拓展

比如现在给上传数据加上一个 pv 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Function.prototype.before = function (beforeFn) {
const that = this
return function (...args) {
beforeFn.apply(this, ...args)
return that.apply(this, ...args)
}
}

const log = () => {
console.log('打印上传前的日志')
}

const upload = () => {
console.log('上传数据')
}

newUpload = upload.before(log)
newUpload()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import 'reflect-metadata'

// 定义装饰器
function Get(path: string): MethodDecorator {
return (target, propertyKey) => {
Reflect.defineMetadata('path', path, target, propertyKey)
}
}

// 使用 fetch 发起请求
async function fetchRequest(path: string) {
const response = await fetch(path)
const data = await response.json()
return data
}

// 装饰器处理器
function handleRequest(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const method = descriptor.value

descriptor.value = async function (...args: any[]) {
const path = Reflect.getMetadata('path', target, propertyKey)
if (path) {
const data = await fetchRequest(path)
return method.apply(this, [data, ...args])
}
return method.apply(this, args)
}
}

// 示例控制器
class MyController {
@Get('https://jsonplaceholder.typicode.com/todos/1')
@handleRequest
async fetchData(data: any) {
console.log('Fetched Data:', data)
}
}

// 使用控制器
;(async () => {
const controller = new MyController()
await controller.fetchData()
})()

策略模式(Strategy)

定义了一系列算法,将每个算法封装起来并使它们可以相互替换。策略模式可以让算法独立于客户端而变化

比如创建一个简单的支付系统, 有不同的支付策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 策略接口
interface PaymentStrategy {
pay(amount: number): void
}

// 具体策略:信用卡支付
class CreditCardPayment implements PaymentStrategy {
pay(amount: number): void {
console.log(`Paid ${amount} using Credit Card.`)
}
}

// 具体策略:PayPal支付
class PaypalPayment implements PaymentStrategy {
pay(amount: number): void {
console.log(`Paid ${amount} using PayPal.`)
}
}

// 上下文类
class PaymentContext {
private strategy: PaymentStrategy

constructor(strategy: PaymentStrategy) {
this.strategy = strategy
}

setStrategy(strategy: PaymentStrategy) {
this.strategy = strategy
}

executeStrategy(amount: number): void {
this.strategy.pay(amount)
}
}

const creditCardPayment = new CreditCardPayment()
const paypalPayment = new PaypalPayment()

const paymentContext = new PaymentContext(creditCardPayment)
paymentContext.executeStrategy(100)

paymentContext.setStrategy(paypalPayment)
paymentContext.executeStrategy(200)

适配器模式(Adapter)

将一个类的接口转换成客户端所期望的另一个接口。适配器模式使得原本由于接口不匹配而无法一起工作的类可以协同工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class TencentMap {
show() {
console.log('腾讯地图渲染')
}
}

class BaiduMap {
display() {
console.log('百度地图渲染')
}
}

class TencentMapAdapter extends TencentMap {
constructor() {
super()
}
showMap() {
this.show()
}
}

class BaiduMapAdapter extends BaiduMap {
constructor() {
super()
}
showMap() {
this.display()
}
}

function render(map) {
map.showMap()
}

render(new TencentMapAdapter())
render(new BaiduMapAdapter())

渲染是十万条数据解决方案

  • 虚拟列表(按需渲染 || 可视区域渲染)

    虚拟列表是按需显示的一种实现, 即只对可见区域进行渲染, 对非可见区域的数据不渲染或部分渲染的技术, 通过监听 scroll 来判断是上滑还是下拉, 从而更新数据, 同理 IntersectionObserver 和 getBoundingClientReact 实现

  • 延迟渲染(即懒渲染)

    最开始不渲染所有数据,只渲染可视区域中的数据(同虚拟列表一致)。当滚动到页面底部时,添加数据(concat),视图渲染新增 DOM

  • 时间分片

    分批渲染 DOM,使用 requestAnimationFrame 来让动画更加流畅, 在浏览器渲染的每一帧的空闲时间去渲染数据

为什么 Hooks 不能存在条件判断中

在 React 中, Hooks 是基于顺序调用的, 意味着在 Hooks 的调用顺序必须保持稳定, 不能在渲染的过程中发生改变, 这样会导致顺序发生变化, 违反了 React Hooks 设计规则, 在重新 render 的时候, react 会根据 hooks 的调用顺序来确定每个 hook 的对应状态, 如果放入条件语句中会导致组件的状态混乱, 不可测

在 React Hooks 的源码中, Hooks 的状态是与组件实例相关联, 每次组件重新渲染时, React 会使用相同的顺序来调用 hooks, 这样才能确保正确的管理组件状态, Hooks 的实现是基于链表结构, 当组件函数被调用的时候, React 会根据 Hooks 的调用顺序创建一个链表, 并将这个链表与实例相关联, 而这个 hook 存在一个对象里面存在一些属性(fiber)

JavaScript 中的垃圾回收机制

从输入网址到页面显示发生了那些过程

HTTP 请求而发送过程

总体来说大致是以下几个过程

  • 生产 HTTP 信息

    解析 URL 生成发送给 Web 服务器的请求信息

  • DNS 解析

    递归的根据域名查询 IP 地址 DNS 优化中有 DNS 缓存, DNS 负载均衡

  • 协议栈

    通过 DNS 获取到 IP 地址后, 可以把 HTTP 的传输工作交给操作系统中的协议栈 TCP/UDP IP

  • 发起 TCP 请求

    三次握手(保证双方都有发送和接受的能力)和四次挥手, 建立 TCP 链接, 生成 TCP 报文

  • 发送 IP 请求定位

    IP 模块将数据封装成网络包发给通信的对象 IP 源地址(客户端地址) 根据路由表规则去匹配判断那张网卡去发送 可以手动配置静态路由, 也可以通过路由协议(RIP OSPF)去配置动态路由表

  • Mac 地址两点传输

  • 发送 HTTP 请求 (IP 层 – 链路层)

  • 服务器处理请求返回 HTTP 报文

  • 浏览器解析渲染页面

  • 断开连接

为什么 0.1 + 0.2 !== 0.3

浮点数在内存中是怎么存储的