基础问题

1. 详细介绍一下JS的原型和原型链?什么是原型?什么是原型链?为什么需要有原型链?怎么使用原型链?要注意什么问题?

JavaScript 原型及相关机制详解


1. 原型与原型链基础

1.1 显式原型(prototype)
  • 每个函数(构造函数)都有一个 prototype 属性,指向一个对象,称为“显式原型”或“原型对象”。
  • 箭头函数没有 prototype 属性,且不能作为构造函数。
1.2 隐式原型([[Prototype]] / proto
  • 每个对象都有一个隐式原型,通常通过 __proto__ 访问(标准是内部属性 [[Prototype]])。
  • 通过构造函数 new 创建的实例,其隐式原型指向构造函数的 prototype
1.3 原型链
  • 当访问对象属性时,JS 引擎先查找对象自身属性,找不到时沿隐式原型链向上查找,直到找到 Object.prototype,其隐式原型为 null
  • 这条链条即为“原型链”,它决定了继承和属性查找机制。

2. 原型存在的意义

  • JS 没有传统语言的类型元数据,通过原型实现继承。
  • 原型链实现共享方法,减少内存开销。
  • 动态修改原型链提供了灵活的继承机制。

3. 使用原型链的方法

  • 通过构造函数的 prototype 定义共享方法。
  • 使用 Object.create(p) 创建一个以 p 为隐式原型的新对象。
  • 使用 Object.setPrototypeOf(a, b) 动态设置对象 a 的隐式原型为 b(不建议频繁使用,性能开销较大)。

4. 构造函数与原型关系

  • 构造函数有 prototype 属性,指向显式原型对象。
  • 由构造函数 new 出来的对象,其隐式原型指向该构造函数的 prototype

5. new 操作符内部执行流程

  1. 创建一个空对象。
  2. 设置该对象的隐式原型指向构造函数的 prototype
  3. 将构造函数内部的 this 指向该对象,执行构造函数代码。
  4. 返回构造函数返回的对象(若无则返回新创建对象)。

6. instanceof 判断原理

  • 判断对象 A 的原型链上是否存在构造函数 Bprototype
  • 存在则返回 true,否则 false

7. constructor 属性

  • 显式原型对象上默认有一个 constructor 属性,指向该构造函数。
  • 作用:
    • 关联实例和构造函数。
    • 用于手动恢复继承时丢失的构造函数指针。
    • 支持某些场景下动态实例创建。
  • 使用 Object.create 继承时,原型会丢失 constructor,需手动修复。
  • 使用 Object.setPrototypeOf 修改原型链时,constructor 不会丢失。

8. 原型链继承的经典写法与注意

  • 不建议用 Child.prototype = new Parent() 继承,因会调用构造函数带来副作用并共享引用属性。
  • 推荐用 Child.prototype = Object.create(Parent.prototype) 干净继承原型。
  • 修改已有原型的隐式原型 Object.setPrototypeOf(Child.prototype, Parent.prototype) 可以保持 constructor,但性能可能受影响。

9. ES6 class 与箭头函数

  • 箭头函数不能作为构造函数使用,没有 [[Construct]]
  • class 是构造函数的语法糖,必须用 new 调用,且不能直接调用。
  • class 中的方法定义在原型上,属性定义在实例上。
  • class 代码默认启用严格模式。
  • 通过 extends 实现继承,本质是设置 Child.prototype 指向 Parent.prototype,以及 Child.__proto__ 指向 Parent,实现实例继承和静态继承。
  • 静态方法定义在构造函数本身,需通过类名调用。

10. super 关键字

  • super() 在子类构造函数中调用父类构造函数,且绑定子类的 this
  • 必须先调用 super(),否则访问 this 会报错(实例未初始化)。
  • super.method() 可在子类方法中调用父类方法。
  • 静态方法中 super 调用父类静态方法。

11. new.target

  • new.target 用于判断函数或类是否通过 new 调用。
  • 通过 new 调用时,new.target 指向被调用的构造函数或类。
  • 普通调用时,new.targetundefined
  • 典型用途:
    • 确保构造函数必须用 new 调用。
    • 实现抽象类,防止基类被实例化。

总结

JavaScript 的原型机制及其衍生的 newinstanceofconstructor 等核心特性构成了对象继承的基础;ES6 classsupernew.target 等语法糖和新特性在语义上清晰了函数的职责,减少了误用,提高了代码可维护性和开发效率。

2. 介绍一下事件循环?

JavaScript 是一门单线程语言,也就是说,它只有一个主线程来执行所有的代码,包括执行 JS 脚本、处理事件、更新 DOM、样式计算与页面渲染等任务。

为了防止长时间运行的 JavaScript 同步代码阻塞主线程,降低页面的响应速度,JavaScript 引入了异步编程模型。异步任务的执行并不是立刻完成,而是交由宿主环境(如浏览器或 Node.js)处理,待任务完成后再通过事件循环(Event Loop)调度回主线程中执行。

事件循环是一种不断执行的机制,用于调度任务队列中的任务,并按照一定的优先级执行异步回调。每一轮事件循环(称为一个 tick)的执行顺序如下:

  1. 从一个宏任务队列中取出一个任务,放入执行栈执行

  2. 执行完后立即清空微任务队列中的所有任务

  3. 执行渲染(如果需要)

  4. 重复以上过程,进入下一轮循环

🧱 微任务(Microtasks)
在当前宏任务执行完之后,立即执行所有微任务,优先级更高。

常见来源:

  • Promise.then / catch / finally

  • queueMicrotask()

  • MutationObserver

微任务队列是全局唯一的。

宏任务(Macrotasks)
包含一个更广泛的任务集合,由浏览器或 Node.js 注册并调度。

每次事件循环 tick 开始时,选择一个宏任务队列的第一个任务来执行。

常见来源:

  • setTimeout

  • setInterval

  • setImmediate(Node.js)

  • requestAnimationFrame(浏览器渲染前调用)

  • DOM 事件回调(如点击、输入等)

  • 网络请求回调(如 XMLHttpRequest、fetch)

3. 解释一下执行上下文

一、什么是执行上下文?

执行上下文(Execution Context) 是 JavaScript 代码被解释执行时所处的运行环境。每段代码必须在某个执行上下文中运行。

执行上下文决定了:

  • 变量、函数等标识符如何被解析
  • this 的取值
  • 所属作用域链的构建方式

二、执行上下文的类型

JavaScript 中共有 三种执行上下文类型

类型 描述
全局上下文 程序启动时默认创建,this 指向全局对象(浏览器中为 window
函数上下文 每次函数调用都会创建一个新的上下文
Eval 上下文 eval() 执行的代码会在其独立的上下文中运行(不推荐使用)

三、执行上下文的组成结构

根据 ECMAScript 规范,执行上下文由以下三部分组成:

  1. LexicalEnvironment(词法环境)

    • 存储使用 letconst、函数声明定义的变量
    • 包含外层引用(outer environment reference),形成作用域链
    • 处理块级作用域(如 iffor 等)
  2. VariableEnvironment(变量环境)

    • 存储使用 var 声明的变量
    • 和词法环境结构相似,在现代引擎中经常合并处理
    • 保留用于规范兼容性
  3. ThisBinding(this 绑定)

    • 表示当前执行上下文中的 this
    • 全局上下文中指向全局对象;函数中根据调用方式决定(普通调用、new 调用、箭头函数等)

✅ 注意:虽然执行上下文还关联了 Realm(宿主环境)等信息,但这不是其组成部分,仅是附属元数据。


四、执行上下文的生命周期

执行上下文从创建到销毁,通常经历以下三个阶段:

1. 创建阶段

在代码真正执行之前,引擎会完成以下准备工作:

  • 确定 this 的绑定
  • 创建词法环境(注册 letconst、函数声明)
  • 创建变量环境(注册 var 声明)
  • 建立作用域链

2. 执行阶段

  • 开始逐行执行代码
  • 完成变量赋值、表达式计算、函数调用等操作
  • 如果遇到函数调用,则会创建新的执行上下文并入栈执行

3. 回收阶段

  • 当前函数执行完毕后,其上下文从执行栈中弹出
  • 若该上下文中定义的变量不再被引用(无闭包),将被垃圾回收器回收
  • 全局上下文只有在页面关闭时才会回收

五、执行上下文栈(ECStack)

  • JavaScript 引擎使用一个栈结构(执行栈)来管理执行上下文
  • 每当一个函数被调用时,会创建其执行上下文并压入栈顶
  • 当前正在执行的代码始终位于栈顶的上下文中
  • 函数执行完毕后,该上下文从栈顶弹出,控制权回到前一个上下文

示例:

1
2
3
4
5
6
7
8
function foo() {
console.log('foo');
}
function bar() {
foo();
}
bar();

4. 解释一下JS的垃圾回收机制

首先说什么是垃圾回收,JS是一门高级程序设计语言,不需要像C语言那样使用mallocfree进行手动的内存分配和释放。而是由JS引擎来管理内存分配。释放不需要的对象的内存空间的过程,就是垃圾回收。垃圾回收由JS引擎自动进行,对开发者透明。但是我们也要在编码时注意,避免内存泄漏。

接着来说一下为什么需要垃圾回收,主要是为了降低语言的学习成本和使用难度,这些对内存的操作很容易出现问题,由执行引擎来处理的话,极大地降低了开发者的心智负担。

然后在说JS引擎如何进行垃圾回收。垃圾回收的主要算法有引用计数法标记清楚法

引用计数法对每个对象维护一个引用计数器,当引用增加时+1,失效时-1,当计数归零时,进行垃圾回收,释放该对象的内存空间。但是由于无法处理循环引用的问题,现代引擎已经逐步弃用。

标记清除法分为两个阶段。

  1. 标记阶段:从根对象(全局对象,活动函数调用栈等)出发,递归遍历所有可达对象并标记为“存活”
  2. 清除阶段:遍历堆内存,释放未被标记的对象(即不可达对象)占用的内存

通过这种机制,解决了循环引用的问题,无论循环引用多少次,只要全局不可达,就会释放其对应的内存。

最后说一下在基础算法的基础上,V8引擎对垃圾回收的进一步优化。

V8引擎采用复合策略来提升GC的效率。

  1. 分代回收(Generational Collection)

    • 新生代(Young Generation):存放短生命周期对象(如临时变量),使用Scavenge算法(复制算法)
      • 将内存分为From-SpaceTo-Space,新对象存入From-Space
      • From-Space满时,存活对象被复制到To-Space并整理,然后角色互换。
    • 老生代(Old Generation): 存放长生命周期对象,采用标记-清除标记-整理结合
      • 标记阶段后,将存活对象向内存一端移动,消化碎片内存空间。
  2. 增量标记(Incremental Marking)

将标记过程拆分为子任务,与JS主线程交替执行,避免GC占用时间过长。

  1. 空闲时间回收(Idle-Time GC)

利用程序空闲时间段执行GC任务,进一步降低对性能的影响。

5. 详细解释一下HTTP/HTTPS协议。

HTTP协议

  1. 基本原理
  • 定义:HTTP是一种无状态的应用层协议,基于C/S模型,通过TCP/IP传输数据(默认端口80)
  • 工作流程
    • 客户端(浏览器)发起TCP连接(三次握手)
    • 发送HTTP请求(包含方法,URL,头部等)
    • 服务器处理请求并返回响应(状态码,头部,数据)
    • 关闭连接(HTTP/1.1默认支持持久连接,可复用TCP链路)
  1. 核心组件
  • 请求报文
    • 请求行:方法(GET/POST等),资源路径,协议版本,如(GET /index.html HTTP/1.1)
    • 请求头:元数据(如Host, User-Agent, Accept)
    • 请求体:POST/PUT等方法携带的数据(如表单,文件)
  • 响应报文
    • 状态行:状态码(如 200 OK), 协议版本
    • 响应头:元数据(如Content-Type, Content-Length)
    • 响应体:返回的实际数据(HTML, JSON)
  1. 特性与局限
  • 无状态性:每次请求独立,需要依赖Cookie/Session管理状态
  • 性能问题:HTPP/1.1存在队头阻塞(逐个处理请求),影响效率
  • 安全性问题:铭文传输容易被窃听和篡改

HTTPS协议

  1. 核心原理
  • 定义:HTTPS = HTTP + SSl/TLS加密层,默认端口443
  • 加密流程:
    • 握手阶段:通过非对称加密验证身份并协商密钥
    • 数据传输:使用对称加密传输业务数据
  • 证书机制:服务器需要部署数字证书(由CA签发),包含公钥,域名等信息,客户端验证证书有效性以防止中间人攻击。
  1. SSL/TLS握手过程
  • Client Hello:客户端支持的加密套件列表 + 随机数
  • Server Hello:服务器选择加密套件 + 证书 + 随机数
  • 密钥交换:客户端用服务器公钥加密预主密钥,发送至服务器
  • 生成会话密钥:双方基于随机数生成对称密钥,用于后续加密通信

6. 详细介绍一下CJS/ESM的区别

  • 性质上
  • 语法上
  • 执行机制
  • 动态导入支持上
  • 导出值类型上
  • 循环依赖处理
  • 运行环境
  • 互操作最佳实践

7.详细介绍一下pnpm及其原理

pnpmPerformant npm的缩写,是一个兼容npmyarn的包管理器,其主要目标是:

  • 更快的安装速度
  • 更少的磁盘占用
  • 更好的依赖隔离
  1. pnpm的核心优势
特性 说明
节省磁盘空间 相同版本的包只下载一次并在项目中共享
更快的安装速度 使用硬链接机制,无需重复复制文件
严格的依赖隔离 默认禁止访问未声明的依赖,避免幽灵依赖的问题
兼容性强 支持npm registry,大部分npm/yarn命令格式
  1. pnpm的工作原理
  • 内容寻址的全局存储(Content-addressable storage)
    • 所有下载的依赖包会存储在全局目录中,默认位置是~/.pnpm-store
    • 存储路径不是按包名命名,而是通过其内容生成的哈希值命名(即内容寻址)
    • 即使多个项目依赖同一个版本的包,也只会在硬盘上下载并存储一次
  • 使用硬链接(hard link)机制
    • 在项目node_modules目录中不会复制真实的包文件,而是创建指向全局存储的硬链接.
    • 硬链接是操作系统级别的指针,占用几乎为0,不回重复占用磁盘空间。

8. 详细解释一Vite及其原理

Vite是一种基于现代浏览器的前端构建工具,其核心原理在于利用浏览器原生ES模块实现按需编译,并通过依赖预构建和高效的热更新机制显著提升开发体验。

  1. 核心设计:基于原生ESM的开发模式
  • 按需编译:Vite在开发环境中不预先打包整个应用。当浏览器请求模块(如import App from './App.vue'时),Vite服务器才动态编译该模块并返回浏览器可以执行的ESM代码。这种机制避免了传统工具的全局打包过程,实现毫秒级冷启动。
  • 路径重写与模块解析:浏览器无法直接识别裸模块导入(如import vue from 'vue')。Vite拦截此类请求,将路径重写为/@modules/vue,在映射到node_modules中的预构建依赖,确保浏览器能正确加载。
  1. 依赖预构建:性能优化的核心
  • 目的与流程:vite启动时使用esbuild(Go语言编写,速度比JS工具块10-100倍)预构建第三方依赖(如React,Lodash)
    • 扫描依赖:分析项目中的import语句,识别需预构建的node_modules
    • 转换与合并:将CommonJS/UMD模块转换为ESM格式,并合并多个小文件以减少HTTP请求数。
    • 缓存机制:预构建结构存储在node_modules/.vite目录,后序启动时直接复用(除非package.json或锁文件变更)
  • 优势:解决模块兼容性问题
  • 减少浏览器并发请求压力,提升加载速度
  1. 按需编译与请求处理流程
  • 请求链路示例:
    • 浏览器请求index.html,加载入口脚本<script type="module" src="/src/main.js">
    • 入口文件中的import语句触发浏览器发起新的请求(如App.vue)
    • Vite拦截请求,编译.vue文件为JS代码,再返回给浏览器
  • 文件类型处理:非JJS文件,通过插件链实时编译为ESM,例如:
    • .vue文件被拆解为模板,脚本和样式,分多次请求编译
    • `
  1. 热更新机制(HMR)
  • 基于WebSocket的实时通信: Vite启动WebSocket服务,监听文件变化,当文件修改时,服务器仅重新编译该模块,并通过WebSocket通知客户端更新。
  • 精准更新策略:客户端根据模块依赖图替换修改的模块,保留应用状态(如Vue组件的局部状态),无需刷新页面,例如修改单个Vue组件时,仅该组件的渲染函数更新。
  • 性能优势:传统项目的热更新速度随项目规模下降,而Vite的更新耗时与模块数量无关,通常低于100ms。
  1. 生成环境: Rollup优化
  • 切换打包工具:开发环境按需编译不适合生产环境(大量小文件导致网络延迟)。Vite使用Rollup打包生成代码,利用其成熟的Tree Shaking,代码分割和压缩能力,生成高性能静态资源。
  • 一致性配置:Vite的插件系统兼容Rollup,简化了开发与生产环境的一致性配置。

9.说一下React的核心机制及其原理

1
2
3
4
5
6
7
8
9
10
11
状态变化(State)/ 事件触发(Event)

setState / dispatch

Fiber 构建新的虚拟 DOM 树

Diff 算法计算变更

Reconciliation(协调)

DOM 更新(Commit 阶段)

React的核心机制分为5大部分

  1. 组件机制(Component System)

React的基础单位是组件,分为:

  • 函数组件(Function Component): 使用Hooks管理状态与副作用
  • 类组件(Class Component): 通过this.state和生命周期函数控制行为(已不推荐)

组件的本质:就是一个函数,输入props,输出UI结构虚拟DOM

  1. 虚拟DOM(Virtual DOM)
  • React不直接操作浏览器DOM,而是构建一个内存中的JavaScript对象树
  • 每次渲染组件时,React会生成新的虚拟DOM树,并与旧树做对比,找出变化。
  • 然后只更新真正变化的部分到真实DOM(提升性能)
  1. 状态驱动+声明式UI

React的理念是:UI = f(state)

当状态(state或props)发生变化时,React会自动重新执行组件函数并更新UI,无需手动操作DOM

  1. Fiber架构:React的渲染引擎

React16+引入的核心调度系统。

Fiber是什么?

  • 是一个数据结构,表示组件树中每一个节点的信息(虚拟DOM的增强版)
  • 每个组件都会生成一个对应的Fiber节点
  • 支持中断渲染优先级调度异步渲染等高级特性。

协调阶段

1
2
3
React 会遍历 Fiber 树,构建新的虚拟 DOM
可被中断(异步处理)
→ 生成 Effect List(变更列表)

提交阶段

1
2
React 把 Effect List 中的变更真正作用到浏览器 DOM
不可中断,必须同步完成

  1. Diff算法(Reconciliation)

虚拟DOM比较新旧树之间的差异:

  • 对比type是否相同(不同则卸载组件,挂载新组件)
  • key机制提升列表节点的diff精度(避免重复卸载/重建)
  • 避免复杂的全树比较
  1. Hooks系统
  • useState:管理局部状态
  • useEffect:副作用处理(如定时器,订阅,DOM操作)
  • useContext:跨组件共享状态
  • useMemo/useCallback:性能优化
  1. Context + Provider
  • 实现跨组件通信
  • 避免props层层传递
  1. Fiber数据结构的详细设计

Fiber的核心是一个链表化的数据结构,用于描述和调度组件的更新过程。Fiber是React的工作单元(unit of work),每一个组件在运行时都会对应一个Fiber节点。

实习经历

1. 介绍一下腾讯文档是如何处理实时协同的?