前端开发面试题目汇总

120 阅读43分钟

一. 手写call,apply,bind

1、 手写 call

原理

  1. call 改变函数执行时的 this 指向。
  2. 将函数临时添加为目标对象(context)的一个属性。
  3. 执行这个属性函数(此时 this 自然指向了 context)。
  4. 删除临时属性。
/**
 * 手写 myCall
 * @param {Object} context - 需要绑定的 this 上下文
 * @param {...any} args - 传递给函数的参数列表(逗号分隔)
 */
Function.prototype.myCall = function(context, ...args) {
  // 1. 边界判断:如果调用 myCall 的不是一个函数,抛出错误
  if (typeof this !== 'function') {
    throw new TypeError('Error: myCall must be called on a function');
  }

  // 2. 处理 context 上下文
  // 如果传入 null 或 undefined,默认指向全局对象 (window 或 global)
  // 如果传入原始值 (如 1, "string"),需要用 Object() 转换为对象
  context = (context === null || context === undefined) ? window : Object(context);

  // 3. 创建一个唯一的 Key
  // 使用 Symbol 防止覆盖 context 上已有的同名属性
  const fnKey = Symbol('fn');

  // 4. 将当前函数 (this) 挂载到 context 上
  // 此时 context[fnKey] 就是当前我们要执行的函数
  context[fnKey] = this;

  // 5. 执行函数
  // 使用 spread 操作符 (...) 将 args 数组展开作为参数传递
  // 此时函数的 this 指向就是 context
  const result = context[fnKey](...args);

  // 6. 删除临时添加的属性,保持 context 干净
  delete context[fnKey];

  // 7. 返回函数执行结果
  return result;
};

2、 手写 apply

原理: 逻辑与 call 几乎完全一致,唯一的区别在于 参数的处理call 接收参数列表,apply 接收参数数组。

/**
 * 手写 myApply
 * @param {Object} context - 需要绑定的 this 上下文
 * @param {Array} argsArr - 传递给函数的参数数组
 */
Function.prototype.myApply = function(context, argsArr) {
  // 1. 边界判断:确保调用者是函数
  if (typeof this !== 'function') {
    throw new TypeError('Error: myApply must be called on a function');
  }

  // 2. 处理 context (同 call)
  context = (context === null || context === undefined) ? window : Object(context);

  // 3. 创建唯一 Key (同 call)
  const fnKey = Symbol('fn');

  // 4. 挂载函数
  context[fnKey] = this;

  // 5. 执行函数并获取结果
  let result;
  // 判断是否有参数数组传入
  if (argsArr && Array.isArray(argsArr)) {
    result = context[fnKey](...argsArr); // 有参数则展开
  } else {
    result = context[fnKey](); // 无参数直接调用
  }

  // 6. 删除临时属性
  delete context[fnKey];

  // 7. 返回结果
  return result;
};

3、 手写 bind (重难点)

原理bind 不会立即执行函数,而是返回一个新的函数。 核心考点

  1. 柯里化 (Currying):支持预传参数。比如 bind(obj, 1) 后,调用时再传 (2),实际参数是 (1, 2)
  2. 构造函数 (new):如果返回的绑定函数被 new 调用,bind 指定的 context 会失效,this 应该指向 new 创建的实例。
/**
 * 手写 myBind
 * @param {Object} context - 绑定的 this 上下文
 * @param {...any} args1 - 初始预设参数
 */
Function.prototype.myBind = function(context, ...args1) {
  // 1. 边界判断
  if (typeof this !== 'function') {
    throw new TypeError('Error: myBind must be called on a function');
  }

  // 保存当前的函数对象,因为下方返回的函数里 this 会变
  const self = this;

  // 2. 返回一个新的函数
  const fBound = function(...args2) {
    // 3. 核心逻辑:判断是否是通过 'new' 调用的
    // 如果是 new 调用,this 指向实例对象 (instanceof fBound 为 true)
    // 此时忽略传入的 context,将 this 指向实例
    // 如果是普通调用,this 指向 window (或 undefined),此时使用传入的 context
    const isNew = this instanceof fBound;

    // 4. 执行原函数
    // 使用 apply 来改变指向
    // 参数合并:将 bind 时的参数 args1 和 调用时的参数 args2 合并
    return self.apply(
      isNew ? this : context, 
      [...args1, ...args2]
    );
  };

  // 5. 维护原型链 (非常重要!)
  // 如果原函数有 prototype (箭头函数没有 prototype),需要继承
  // 这样 new fBound() 出来的实例才能继承原函数原型链上的属性
  if (self.prototype) {
    // 使用 Object.create 创建一个空对象作为中介,避免直接修改 fBound.prototype 影响原函数
    fBound.prototype = Object.create(self.prototype);
  }

  return fBound;
};

4、 面试高分技巧(解释要点)

在写代码时,或者写完后,向面试官解释以下细节,可以大幅加分:

  1. Symbol 的作用:在 callapply中,我使用了 Symbol 作为 key。这是为了防止 context 对象上原本就有个叫 fn 的属性,直接覆盖会导致原数据丢失。Symbol 保证了属性名的唯一性。
  2. 基本类型包装:我处理了 context1"abc" 的情况,使用 Object() 将其转为对象,这是符合原生 API 行为的。
  3. Bind 的 new 操作符:这是 bind 最难的地方。我会判断 this instanceof fBound。如果是 new 调用的,由于 JS 机制,this 会指向新创建的实例,这时候我们不能强行把 this 改为 context,否则构造函数就失效了。
  4. 原型链丢失问题:在 bind 中,我通过 Object.create 继承了原函数的原型,确保 new 出来的实例能访问原函数原型上的方法。

这份笔记整理了面试中关于模块化的核心考点,涵盖了 CommonJS vs ES6 模块的区别底层机制对比 以及 require 的源码级原理。可以直接复制到你的笔记软件(Notion/Obsidian)中复习。


二. CommonJS 与 ES6 Module 核心考点

1、 CommonJS vs ES6 Module 核心区别

这是面试必考题,建议先背熟表格,再深入理解“值的拷贝”与“值的引用”。

1.1 对比总结表

维度CommonJS (CJS)ES6 Module (ESM)
输出机制值的拷贝 (Value Copy)值的引用 (Live Reference)
加载时机运行时加载 (Runtime)编译时输出接口 (Static Analysis)
对象形态导出是一个对象导出不是对象,是静态定义
this 指向指向当前模块对象undefined
主要场景Node.js 服务端浏览器、现代构建工具 (Webpack/Vite)
Tree Shaking不支持支持 (因为是静态分析)

1.2 深度解析:值的拷贝 vs 值的引用 (必考代码题)

这是最能体现你理解深度的点。

  • CommonJS (拷贝):模块输出的是一个值的复制。一旦输出,模块内部的变化不会影响到外部引用的值(基本数据类型)。
  • ES6 Module (引用)import 是一个只读引用(Live Binding)。脚本执行时,会去被加载的模块中取值,模块内部变了,外部也会跟着变

代码示例:

// --- counter.js ---
let count = 1;
function add() { count++; }

// CJS 导出
// module.exports = { count, add }; 

// ESM 导出
// export { count, add };

// --- main.js ---

// 1. 如果是 CommonJS:
const { count, add } = require('./counter');
console.log(count); // 1
add();
console.log(count); // 1 (依然是1!因为 require 出来的是一份缓存拷贝)

// 2. 如果是 ES6 Module:
import { count, add } from './counter';
console.log(count); // 1
add();
console.log(count); // 2 (变成了2!因为 import 指向的是内存中的引用)

2、 模块引入与执行机制的区别

2.1 CommonJS (require)

  • 同步加载:代码执行到 require 这一行时,才会去加载文件、执行代码。
  • 动态路径:支持动态引入,如 require('./' + fileName)
  • 缓存优先:第二次加载同一个模块,直接从缓存 Module._cache 读取,不会重复执行。

2.2 ES6 Module (import)

  • 静态分析:在代码运行前(编译/解析阶段),JS 引擎就会扫描 import 语句,构建依赖关系图。
  • 变量提升import 语句会自动提升到文件顶部优先执行。
  • 位置限制:只能写在顶层,不能写在 if 块或函数中(除非使用 import() 动态导入语法)。

3、 require 的原理详细解析 (手写原理)

require 本质上是一个函数,Node.js 内部通过 fs 读取文件并包裹执行。

3.1 核心五步流程

当执行 require('./a.js') 时:

  1. 路径分析 (Path Resolving)
    • 将相对路径转为绝对路径。
    • 自动补全后缀(.js, .json, .node)。
  2. 缓存检查 (Caching)
    • 检查 Module._cache 是否有该文件的缓存。
    • -> 直接返回 module.exports
    • -> 进入下一步。
  3. 创建模块 (Module Creation)
    • new Module(filename),初始化一个对象,包含 id, exports = {} 等属性。
  4. 编译执行 (Compile & Execute)
    • 包裹 (Wrapper): 读取文件内容,将其包裹在一个函数中(为了隔离作用域)。
    • 执行: 运行这个包裹函数,将 module.exports 的控制权交给用户。
  5. 返回结果
    • 返回 module.exports

3.2 什么是“包裹函数” (Wrapper)?

这就是为什么你在 Node.js 文件里可以直接用 __dirname, __filename, exports 的原因。它们不是全局变量,而是函数的参数

// Node 内部会将你的代码变成这样:
(function (exports, require, module, __filename, __dirname) {
    // --- 你的代码在这里 ---
    const a = 1;
    module.exports = a;
    // -------------------
});

3.3 手写精简版 require (笔记专用)

const path = require('path');
const fs = require('fs');
const vm = require('vm');

function myRequire(filename) {
  // 1. 获取绝对路径
  const absPath = path.resolve(__dirname, filename);

  // 2. 缓存检查
  if (myRequire.cache[absPath]) {
    return myRequire.cache[absPath].exports;
  }

  // 3. 定义模块对象
  const module = {
    id: absPath,
    exports: {}
  };

  // 4. 读取文件内容
  const code = fs.readFileSync(absPath, 'utf8');

  // 5. 核心:函数包裹 (隔离作用域,注入变量)
  const wrapper = [
    '(function (exports, require, module, __filename, __dirname) { ',
    '\n})'
  ];
  const wrappedCode = wrapper[0] + code + wrapper[1];

  // 6. 运行代码
  // 使用 vm.runInThisContext 将字符串变成可执行函数
  const script = new vm.Script(wrappedCode);
  const compiledWrapper = script.runInThisContext();

  // 7. 执行函数,传入参数 (exports, require, module...)
  // 这里的 module.exports 被传入,用户在内部修改它
  compiledWrapper.call(
    module.exports, // this
    module.exports, // exports
    myRequire,      // require
    module,         // module
    absPath,        // __filename
    path.dirname(absPath) // __dirname
  );

  // 8. 写入缓存
  myRequire.cache[absPath] = module;

  // 9. 返回结果
  return module.exports;
}

// 初始化缓存
myRequire.cache = {};

3.4 一句话记忆总结

CommonJS 是运行时加载对象,输出的是值的拷贝(即使内部变了外部也不变);ES6 Module 是编译时静态引用,输出的是值的引用(内部变了外部跟着变),且支持 Tree Shaking。Require 的原理就是读取文件 -> 包裹函数 -> 注入 exports/module 参数 -> 执行并返回 module.exports。

三. 代码的执行顺序

// Promise 1: 直接返回 Promise.resolve() -> 慢两拍
Promise.resolve().then(() => {
    console.log(0);
    // 这里返回了一个 Promise,导致后续的 then 被推迟
    return Promise.resolve(4);
}).then((res) => {
    console.log(res);
});

// Promise 2: 只有空的 then -> 标准速度
Promise.resolve().then(() => {
    console.log(1);
}).then(() => {
    console.log(2);
}).then(() => {
    console.log(3);
}).then(() => {
    console.log(5);
}).then(() => {
    console.log(6);
});

执行顺序解析:

  1. 第一轮(同步代码)

    • P1 的第一个 then 入队。
    • P2 的第一个 then 入队。
  2. 微任务队列处理 - 第 1 轮

    • P1 执行:打印 0。发现返回了 Promise,产生额外微任务 A(入队)。
    • P2 执行:打印 1。正常链式调用,产生 P2 的下一个 then(入队)。
  3. 微任务队列处理 - 第 2 轮

    • 执行“额外微任务 A”:准备拆包。产生额外微任务 B(入队)。
    • P2 执行:打印 2。正常链式调用,产生 P2 的下一个 then(入队)。
  4. 微任务队列处理 - 第 3 轮

    • 执行“额外微任务 B”:拆包完成,把 4 传给 P1 外部。P1 的最终 log 入队
    • P2 执行:打印 3
  5. 微任务队列处理 - 第 4 轮

    • P1 终于执行:打印 4
    • P2 执行:打印 5

最终输出结果:
0, 1, 2, 3, 4, 5, 6

四. Pnpm的出现解决了什么问题

Pnpm (Performant NPM) 是目前前端社区非常流行的包管理工具。简单来说,它主要解决了 NPM(以及 Yarn v1)存在的磁盘空间浪费安装速度慢以及**依赖安全隐患(幽灵依赖)**这三大核心问题。

以下是详细的解析,面试时可以按照这三个维度来回答:

4.1 解决了“磁盘空间浪费”问题

核心机制:Content-Addressable Storage (CAS) + 硬链接 (Hard Links)

  • NPM 的问题: NPM 的安装机制是“复制”。如果你有 10 个项目都用了 Vue,NPM 会在硬盘上把 Vue 的文件复制 10 次。这导致磁盘空间被大量重复文件占用。

  • Pnpm 的解法: Pnpm 引入了全局存储(Store)。

    • 所有项目的依赖都物理存储在硬盘的同一个位置(~/.pnpm-store)。
    • 当你在项目中 pnpm install 时,Pnpm 并不是把文件拷贝过来,而是通过**硬链接(Hard Link)**将项目中的 node_modules 直接指向全局 Store 中的文件。
    • 结果:即使你有 100 个项目使用了同一个版本的 Vue,硬盘上只存了一份物理文件。

4.2 解决了“幽灵依赖 (Phantom Dependencies)”问题

核心机制:非扁平化的 node_modules 结构 + 符号链接 (Symlinks)

这是 Pnpm 在工程化安全方面最大的贡献。

  • NPM 的问题(扁平化带来的弊端): NPM v3 之后,为了解决“嵌套层级过深”的问题,采用了**扁平化(Hoisting)**策略。

    • 场景:你的项目依赖了 AA 依赖了 B
    • NPM 做法:把 AB 都提升到 node_modules 的顶层。
    • 隐患:你的代码可以直接 import B,即使你并没有在 package.json 里声明 B。这就是幽灵依赖
    • 后果:某天 A 升级了,不再依赖 B,你的项目因为找不到 B 而直接报错崩溃,但你却一脸懵逼(因为你以为 A 只是小版本升级)。
  • Pnpm 的解法: Pnpm 恢复了严格的树状结构,但利用了**符号链接(软链接)**来解决路径过深问题。

    • 在 Pnpm 的管理下,如果你的 package.json 里只有 A,那么你的 node_modules 下就只有 A,没有 B。你无法非法访问 B
    • A 实际上是一个软链,指向 .pnpm 目录下的真实位置,而那个位置里,Anode_modules 下才有 B
    • 结果:彻底杜绝了非法访问未声明依赖的可能,让项目更健壮。

4.3 解决了“安装速度”问题

核心机制:三阶段并发 + 链接操作

  • NPM 的问题: 需要大量的 I/O 操作(文件复制),不仅慢,而且对硬盘读写损耗大。

  • Pnpm 的解法:

    • 由于不需要物理复制文件,只需要创建“链接”(Link),这个操作在操作系统层面是非常快的(毫秒级)。
    • Pnpm 的安装过程分为:解析 (Resolution) -> 获取 (Fetch) -> 链接 (Link)。这也是高度并行化的。
    • 结果:冷安装速度通常比 NPM 快 2-3 倍,热安装(有缓存)几乎是瞬间完成。

4.4 解决了“NPM Doppelgangers(依赖分身)”问题

  • NPM 的问题: 在扁平化算法中,如果不同的包依赖了同一个库的不同版本(例如 lib-v1lib-v2),由于顶层只能放一个版本,另一个版本不得不被嵌套在深层。复杂的依赖关系会导致同一个版本的包在 node_modules 中出现多次。

  • Pnpm 的解法: 由于 Pnpm 使用的是基于内容寻址的 Store,相同的包永远只存储一份。在项目结构中,它通过软链精准地组织目录,不会产生重复的物理文件。


4.5 面试回答总结模板

"Pnpm 主要解决了 NPM 的三个核心痛点:

  1. 磁盘空间效率:NPM 对每个项目都进行文件复制,导致大量占用磁盘。Pnpm 使用硬链接机制,让所有项目共享同一个全局 Store 中的物理文件,极大节省了空间。
  2. 安全性(幽灵依赖):NPM 的扁平化机制导致我们可以访问未在 package.json 中声明的依赖,容易造成项目不稳定。Pnpm 默认采用非扁平化node_modules 结构,配合软链接,严格限制了源码只能访问声明过的依赖。
  3. 安装速度:得益于文件链接机制,Pnpm 省去了大量的文件 I/O 复制操作,安装速度远快于 NPM。"

五. js中的垃圾回收机制

1. JavaScript 内存管理与垃圾回收机制深度解析

1.1 核心概念:什么是垃圾回收?

在 C 或 C++ 等底层语言中,开发者必须手动管理内存(使用 malloc()free())。这给予了极大的控制权,但也容易导致内存泄漏(忘记释放)或程序崩溃(提前释放)。

JavaScript 是一门自动垃圾回收的语言。JS 引擎(如 Chrome 的 V8)有一个后台进程称为“垃圾回收器”,它会监控所有对象的内存分配,并自动回收那些不再需要的内存。

1.2 判定标准:可达性 (Reachability)

垃圾回收的核心算法依据是**“可达性”**。

  • 根(Roots):全局变量(Window/Global)、当前执行栈的局部变量等被视为“根”。
  • 可达:如果一个对象可以通过引用链从“根”访问到,它就是可达的(活的)。
  • 不可达:如果从“根”出发无法找到该对象,它就被视为垃圾。

2. 内存的数据结构:栈与堆

在了解 GC 之前,必须先明确数据存在哪里:

  1. 栈内存 (Stack)

    • 存储基础数据类型(Number, String, Boolean, null, undefined, Symbol)和引用类型的指针
    • 特点:空间小,系统自动分配释放,遵循先进后出(LIFO)。
    • 注意:栈内存不在 GC 的主要管理范围内,因为它随函数执行结束自动清理。
  2. 堆内存 (Heap)

    • 存储引用数据类型(Object, Array, Function)。
    • 特点:空间大,动态分配。GC 主要治理的就是堆内存。

3. 垃圾回收策略的演进

3.1 引用计数法 (Reference Counting) —— 已被淘汰

这是最初级的垃圾回收算法(IE6/7 曾使用)。

  • 原理:记录每个值被引用的次数。
    • 被赋值给变量:引用次数 +1。
    • 变量被覆盖或销毁:引用次数 -1。
    • 当次数为 0 时:回收。
  • 致命缺陷:循环引用 (Circular Reference)
    function cycle() {
        let objA = {};
        let objB = {};
        
        objA.friend = objB; // B 引用 +1
        objB.friend = objA; // A 引用 +1
        
        return; // 函数结束,但 A 和 B 的引用次数依然是 1,永远无法回收!
    }
    
    这就是为什么现代浏览器不再使用此算法的原因。
3.2 标记-清除算法 (Mark-and-Sweep) —— 现代标准

这是目前主流浏览器(包括 V8)的核心算法逻辑。

  • 原理
    1. 标记阶段 (Mark):垃圾回收器从“根”开始遍历,把所有能遍历到的对象标记为“活动对象”。
    2. 清除阶段 (Sweep):遍历整个堆内存,把所有没被标记的对象(即不可达对象)直接销毁,释放内存。
  • 优点:完美解决了循环引用问题(因为虽然 A 和 B 互相引用,但从 Root 出发找不到它们,依然会被回收)。
  • 缺点内存碎片化。回收后的内存空间是不连续的,像蜂窝煤一样,如果要分配一个大对象,可能找不到足够大的连续空间。

4. V8 引擎的高效垃圾回收机制 (重点)

V8 并不只使用单一的算法,而是采用了分代式垃圾回收 (Generational GC)

V8 基于一个**“代际假说” (The Generational Hypothesis)**:

大部分对象在内存中存活的时间很短(朝生夕死),只有少部分对象会长久存活。

基于此,V8 将堆内存分为两块:新生代 (New Space)老生代 (Old Space)

4.1 新生代 (New Space)
  • 特点:对象存活时间短,空间小(通常 1MB - 8MB)。
  • 算法Scavenge 算法(基于 Cheney 算法)
  • 工作流程
    1. 将新生代空间一分为二:From-Space (使用区)To-Space (空闲区)
    2. 新对象都分配在 From-Space。
    3. GC 开始时,检查 From-Space 里的存活对象。
    4. 复制:将存活对象复制到 To-Space(复制过程中自动整理内存,无碎片)。
    5. 清空:直接清空 From-Space。
    6. 交换:From-Space 和 To-Space 角色对调。
  • 晋升 (Promotion): 如果一个对象在新生代中经历了多次(通常是 2 次)回收依然存活,或者 To-Space 空间占用超过 25%,它就会被移入老生代
4.2 老生代 (Old Space)
  • 特点:对象存活时间长,占用空间大。
  • 算法标记-清除 (Mark-Sweep) + 标记-整理 (Mark-Compact)
  • 工作流程
    1. 标记-清除:主要负责回收垃圾。
    2. 标记-整理:为了解决内存碎片问题。当碎片过多时,V8 会把所有存活对象往内存的一端移动,然后清理掉边界外的内存。

5. V8 的性能优化:如何避免全停顿 (Stop-the-World)

垃圾回收运行是需要占用主线程的。如果 GC 正在运行,JS 脚本就必须暂停,这被称为 "Stop-the-World" (全停顿)。如果堆内存很大,GC 可能导致页面卡顿几百毫秒。

为了解决这个问题,V8 引入了名为 Orinoco 的优化架构:

  1. 增量标记 (Incremental Marking)

    • 将一口气完成的标记任务,拆分成很多个小任务。
    • JS 执行一会儿 -> 标记一会儿 -> JS 执行一会儿。
    • 让 GC 和应用逻辑交替进行,减少肉眼可见的卡顿。
  2. 惰性清理 (Lazy Sweeping)

    • 标记完成后,不需要立刻清除所有垃圾。垃圾回收器会根据内存需要,稍微清理一部分,甚至延迟清理。
  3. 并发回收 (Concurrent GC)

    • 利用多核 CPU,让 GC 辅助线程在后台进行标记和整理,完全不阻塞主线程 JS 的执行。

6. 常见的内存泄漏场景 (面试必考)

理解了 GC,还要知道哪些代码会导致 GC 无法回收内存:

  1. 意外的全局变量
    function foo() {
        bar = "this is global"; // 没有声明 var/let/const,挂载到了 window 上,常驻内存
    }
    
  2. 被遗忘的定时器
    setInterval(() => {
        let node = document.getElementById('node'); // 如果 node 节点被移除了,但定时器还在跑,node 就无法回收
    }, 1000);
    
  3. 闭包 (Closures)
    • 不合理的闭包会持有外部函数作用域的变量,导致无法释放。
  4. 脱离 DOM 的引用
    let btn = document.getElementById('button');
    document.body.removeChild(btn); // 页面上没了
    // 但是 btn 变量还引用着这个 DOM 对象,所以内存里还没被回收
    btn = null; // 手动释放
    

7. 总结笔记 (Cheat Sheet)

  1. 核心思想:可达性(Reachability)。从根出发,找得到的留着,找不到的删掉。
  2. 栈 vs 堆:栈存基础类型(系统管理);堆存对象(GC 管理)。
  3. 分代回收 (V8)
    • 新生代 (New Space):存活短,用 Scavenge 算法(复制+交换),速度快但空间换时间。
    • 老生代 (Old Space):存活长,用 Mark-Sweep (清除) + Mark-Compact (整理),空间利用率高。
  4. 对象晋升:新生代里的老油条会被踢到老生代。
  5. 优化手段:增量标记、并发标记(为了减少页面卡顿)。

六. 为什么vue3支持树摇 vue2不支持

这是一个非常经典的架构设计问题,也是 Vue 3 重构的核心驱动力之一。

简单来说,核心区别在于 API 的设计结构

  • Vue 2 是基于 “类/对象” (Class/Object-based) 的单例设计,导致“牵一发而动全身”。
  • Vue 3 是基于 “函数” (Function-based) 的模块化设计,实现了“按需引入”。

下面详细解析:


6.1 根本原因:Tree Shaking 的工作原理

首先你要理解 Tree Shaking(摇树优化)依赖于 ES6 Module (ESM)静态分析

  • 打包工具(Webpack/Rollup)在编译时分析 importexport
  • 它需要确定:“这个函数被引入了,那个函数没被引入”。
  • 关键点:静态分析很难处理对象属性。如果你引入了一个大对象 Obj,打包工具很难判断你到底用了 Obj.a 还是 Obj.b,为了安全起见,它只能把整个 Obj 都打包进去。

6.2 Vue 2 为什么不行?(全家桶模式)

Vue 2 的设计思想是“基于对象”的。所有的功能都挂载在 Vue 这个全局对象(或者它的原型 prototype)上。

代码示例:

import Vue from 'vue';

// 即使你只用了 nextTick
Vue.nextTick(() => { ... });

// 但是!
// Vue 对象的定义里包含了:
// Vue.component
// Vue.directive
// Vue.mixin
// Vue.util ...

问题所在: 当你 import Vue from 'vue' 时,你引入的是一个巨大的单例对象。

  1. 挂载在类上:Vue 2 的源码里,功能是通过 extend 方式一个个挂载到 Vue 构造函数上的(比如 initUse(Vue), initMixin(Vue))。
  2. 无法拆分:打包工具看到你引入了 Vue 对象,它不敢把 Vue.componentVue.use 删掉,因为 JavaScript 的对象属性是动态的,打包工具无法保证你在代码的其他地方没有通过 Vue['use'] 这种方式去调用它。

结果: 哪怕你只写了一个 Hello World,整个 Vue 2 的运行时(包括你没用到的指令、组件系统、工具函数)都会被打包进去。


6.3 Vue 3 为什么可以?(按需自助模式)

Vue 3 在重构时,把所有的 API 都抽取成了独立的函数(Top-Level Function Exports)。

代码示例:

// 你只引入了 nextTick 和 onMounted
import { nextTick, onMounted } from 'vue';

nextTick(() => { ... });

优化机制:

  1. 独立导出nextTickcomputedwatch 这些现在都是独立的函数,不再是 Vue 对象上的属性。
  2. 静态分析生效:打包工具(如 Webpack)在分析 AST(语法树)时,清清楚楚地看到:
    • 你引入了 nextTick -> 保留
    • 你没引入 reactive -> 删除(Shake 掉)
    • 你没引入 KeepAlive 组件 -> 删除
  3. 编译器配合:Vue 3 的模板编译器在编译 HTML 模板时,生成的代码也是导入特定函数。
    • 比如模板里用了 v-model,编译后的 JS 代码就会自动 import { vModelText } from 'vue'。如果你没用 v-model,相关的处理代码压根不会出现在最终包里。

6.4 总结对比

特性Vue 2Vue 3
API 挂载方式挂载在 Vue 全局对象或原型上作为独立的 ES 模块导出
引用方式import Vue from 'vue'import { ref, nextTick } from 'vue'
打包工具视角这是一个整体对象,不可拆分这是一堆独立函数,没用的扔掉
结果All in One(体积大)Pay as you go(按需打包,体积更小)

6.5 面试回答模板

"Vue 2 无法进行 Tree Shaking 的主要原因是其 Global API 设计是基于对象的。所有的全局 API(如 nextTick, use, component)都作为属性挂载在 Vue 构造函数或原型上。由于 JS 对象的动态特性,打包工具无法在静态分析阶段安全地剔除未使用的属性,导致引入 Vue 就等于引入了整个包。

而 Vue 3 进行了架构重构,采用了模块化设计。所有的 API(包括 Composition API 和内部辅助函数)都重写为了独立的顶层导出函数。当我们 import { nextTick } from 'vue' 时,构建工具可以明确知道我们只使用了这一个函数,从而利用 ES Module 的静态分析能力,安全地移除其他未引用的代码。这就是 Vue 3 能实现 Tree Shaking 的核心原因。"

七. 前端的攻击手段有哪些,如何预防?

这是一个非常经典且高频的前端面试题。前端安全是工程化中不可忽视的一环。

以下是前端常见的攻击手段及其防御方案的详细总结,适合你做笔记和背诵。


7.1 XSS (Cross-Site Scripting) - 跨站脚本攻击

这是前端最常见、危害最大的攻击方式。

1. 攻击原理: 攻击者通过在网页中注入恶意的 JavaScript 脚本,当用户浏览该网页时,脚本自动执行。

  • 后果:窃取 Cookie(Session ID)、监听用户键盘输入、重定向到钓鱼网站、破坏页面结构。

2. 常见类型:

  • 存储型 (Stored):恶意脚本被存到了数据库中(比如评论区)。所有看评论的人都会中招。
  • 反射型 (Reflected):恶意脚本在 URL 参数中(比如搜索框)。攻击者给用户发个链接,用户点开就中招。
  • DOM 型:前端 JS 代码逻辑有问题,直接把不可信的输入插入到了 DOM 中(如 innerHTML)。

3. 防御手段 (记重点):

  • 输入过滤,输出转义 (Escaping)
    • 永远不要信任用户的输入。
    • 在输出到 HTML 之前,把 < 变成 &lt;> 变成 &gt; 等。
    • 现代框架(Vue/React)默认在数据绑定时(如 {{}})会自动转义,除非你使用了 v-htmldangerouslySetInnerHTML(尽量少用这两者)。
  • HttpOnly Cookie
    • 后端设置 Cookie 时加上 HttpOnly 属性,这样 JS 脚本(document.cookie)就无法读取 Cookie,防止 Token 被盗。
  • CSP (Content Security Policy) 内容安全策略
    • 最硬核的防御。通过 HTTP 响应头 Content-Security-Policy 告诉浏览器:只允许加载哪里的脚本(比如只允许本域名)。
    • 禁止内联脚本(<script>...</script>)执行。
  • 输入长度限制:限制输入长度,增加攻击难度。

7.2 CSRF (Cross-Site Request Forgery) - 跨站请求伪造

1. 攻击原理: 攻击者诱导用户进入一个第三方网站,然后冒用用户的身份(利用浏览器会自动携带 Cookie 的机制),向被攻击网站发送操作请求(如转账、点赞、发邮件)。

  • 比喻:攻击者借你的手(Cookie),发了一封你并不想发的信。

2. 防御手段:

  • Cookie 的 SameSite 属性 (最重要的防御)
    • 设置 SameSite=StrictLax。告诉浏览器,如果是第三方发起的请求,不要带上 Cookie。
  • CSRF Token
    • 用户打开页面时,服务器下发一个随机 Token(通常在 HTML 表单隐藏域或 Header 中)。
    • 提交请求时必须带上这个 Token,服务器验证。攻击者拿不到这个 Token,所以请求失败。
  • 同源检测
    • 后端检查 HTTP 请求头中的 OriginReferer,确保请求是从合法的源发出的。

7.3 Clickjacking - 点击劫持

1. 攻击原理: 攻击者搞一个透明的 iframe,覆盖在一个诱导性的按钮上。

  • 用户以为自己点的是“领取奖品”,实际上点的是透明 iframe 里的“银行转账”按钮。

2. 防御手段:

  • X-Frame-Options 响应头
    • DENY:不允许任何网站通过 iframe 嵌入该页面。
    • SAMEORIGIN:只允许同源网站嵌入。
  • CSP 的 frame-ancestors 指令
    • 功能类似 X-Frame-Options,但更灵活。

7.4 MITM (Man-in-the-Middle) - 中间人攻击

1. 攻击原理: 攻击者在用户和服务器的通信链路中间(比如公共 WiFi),拦截、窃听或篡改数据。

2. 防御手段:

  • 全站 HTTPS:使用 TLS/SSL 加密传输,确保数据无法被窃听。
  • SRI (Subresource Integrity) 子资源完整性
    • 针对 CDN 资源。在 <script> 标签中加 integrity 属性(hash 值)。
    • 如果 CDN 被劫持,文件被改了,Hash 对不上,浏览器就会拒绝执行该脚本。

7.5 SQL 注入 (虽然是后端,但前端需配合)

1. 攻击原理: 在输入框输入 SQL 命令(如 ' OR 1=1 --),骗过后端数据库执行。

2. 前端防御:

  • 虽然核心防御在后端(使用预编译语句),但前端应该对输入内容进行类型校验正则过滤(如禁止输入特殊字符)。

7.6 总结

如果面试官问:“你知道哪些前端安全问题?”,你可以这样有条理地回答:

"前端主要的安全问题有 XSS、CSRF 和点击劫持。

  1. XSS(跨站脚本攻击)
    • 本质:恶意脚本注入。
    • 解决:核心是不信任用户输入。利用框架自动转义、开启 CSP、设置 Cookie HttpOnly
  2. CSRF(跨站请求伪造)
    • 本质:冒用用户身份(Cookie)。
    • 解决:核心是验证请求来源。利用 Cookie 的 SameSite 属性、使用 CSRF Token、检查 Referer。
  3. 点击劫持
    • 本质:透明 iframe 覆盖。
    • 解决:设置 X-Frame-Options 响应头禁止被嵌入 iframe。

此外,还有中间人攻击,主要靠 HTTPSSRI 来防御。"

根据你的目录结构(接上文的“七、前端安全”),这里为你整理了一个新的章节。手写 instanceof 是考察 JS 原型链 最直接的代码题。


八. 手写 instanceof 原理与实现

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

8.1 核心原理

  1. 右边:获取构造函数的显式原型 prototype
  2. 左边:获取实例对象的隐式原型 __proto__ (推荐使用 Object.getPrototypeOf())。
  3. 比对过程
    • 判断左边的原型是否等于右边的 prototype
    • 如果不等,就沿着左边的原型链继续往上找 (proto = proto.__proto__)。
    • 直到找到相等(返回 true),或者找到原型链顶端 null(返回 false)。

8.2 代码实现 (标准面试版)

/**
 * 手写 instanceof
 * @param {Object} left - 实例对象 (如 [1, 2])
 * @param {Function} right - 构造函数 (如 Array)
 * @returns {Boolean}
 */
function myInstanceof(left, right) {
    // 1. 边界判断:基础数据类型直接返回 false
    // instanceof 只能用于判断对象(引用类型), 'abc' instanceof String 为 false
    if ((typeof left !== 'object' && typeof left !== 'function') || left === null) {
        return false;
    }

    // 2. 获取右边构造函数的 prototype
    const prototype = right.prototype;

    // 3. 获取左边实例的隐式原型
    // 推荐使用 Object.getPrototypeOf(left),它等价于 left.__proto__
    let proto = Object.getPrototypeOf(left);

    // 4. 循环遍历原型链
    while (true) {
        // 如果走到了原型链尽头 (null),说明没找到
        if (proto === null) return false;

        // 如果找到了相同的原型对象
        if (proto === prototype) return true;

        // 继续沿着原型链向上查找
        proto = Object.getPrototypeOf(proto);
    }
}

8.3 测试用例

你可以用以下代码在面试官面前验证:

class Parent {}
class Child extends Parent {}

const c = new Child();

console.log(myInstanceof(c, Child));   // true
console.log(myInstanceof(c, Parent));  // true (继承关系)
console.log(myInstanceof(c, Object));  // true (万物皆对象)
console.log(myInstanceof([], Array));  // true
console.log(myInstanceof(123, Number)); // false (基础类型)

8.4 面试高分技巧(补充说明)

  1. 关于基础类型:原生的 instanceof 对于基础类型(如 1 instanceof Number)会返回 false,只有通过 new Number(1) 创建的对象才返回 true。我的代码中开头加了 typeof 判断,完美模拟了这一行为。
  2. 关于 API 选择:虽然 left.__proto__ 写起来更方便,但它是非标准的(虽然浏览器都支持)。在生产环境或严格的面试中,使用 Object.getPrototypeOf(left) 是更规范、更专业的写法。

这是一个非常精准且考验底层原理的问题。

九.是否有不触发冒泡或者捕获的事件

结论先行:

  1. 不冒泡的事件:非常多,属于常见现象。
  2. 不捕获的事件几乎没有(除了事件源本身就是最顶层的 window 对象)。即便是不冒泡的事件(如 focus),也依然会经历捕获阶段。

9.1、 没有“冒泡”的事件(很常见)

大部分事件默认是冒泡的,但为了性能或逻辑正确性,以下事件默认不冒泡(即 event.bubblesfalse):

9.1.1. 焦点相关

  • focus:获得焦点。
  • blur:失去焦点。
    • 注意:如果你想用事件委托监听焦点,不能用这两个。要用 focusinfocusout(这两个是专门设计来支持冒泡的)。

9.1.2. 鼠标移入移出

  • mouseenter:鼠标移入。
  • mouseleave:鼠标移出。
    • 原理:为了避免鼠标在父子元素间移动时频繁触发,它们不冒泡。对应的冒泡版本是 mouseovermouseout

9.1.3. 资源加载相关

  • load:资源加载完成(如 <img>, <script>)。
  • unload:资源卸载。
  • error:加载失败。
  • abort:加载中止。
    • 解释:比如 imgload 事件不应该冒泡到 document,否则页面里任何一张小图加载完都会触发整个页面的 onload 逻辑,这显然不合理。

9.1.4. UI 滚动与尺寸

  • resize:元素尺寸改变。
  • scroll:滚动条滚动。
    • 注意scroll 在普通元素上触发时不冒泡;但在 document 上触发时会“冒泡”到 window(这是浏览器的特殊行为,为了方便监听全屏滚动)。

9.1.5. 媒体相关(Video/Audio)

  • play, pause, ended, volumechange 等大部分媒体事件都不冒泡。
    • 技巧:想在父容器监听所有视频的播放,必须用捕获

9.2、 没有“捕获”的事件(极少)

这是面试中的陷阱题

很多人认为“因为 focus 不冒泡,所以它也不捕获”,这是错的

绝大多数 DOM 事件都会经历捕获阶段。 即使是上面提到的 focusblurload,它们虽然不冒泡,但依然可以被捕获

唯一的例外:

只有当事件目标(Event Target)本身就是 DOM 树的最顶层时,才不存在“捕获阶段”这个说法。

  • 场景:事件直接发生在 window 对象上。
  • 例子window.onresize
  • 原因:捕获是从 window -> document -> ... -> target。如果 target 也就是 window,那起点就是终点,没有“从上往下传”的过程,所以不存在捕获阶段。

9.3、 如何验证?

我们可以写一段代码来验证:focus 事件虽然不冒泡,但能被捕获

const parent = document.getElementById('parent');
const input = document.getElementById('myInput');

// 1. 监听冒泡阶段 (默认 false)
parent.addEventListener('focus', () => {
    console.log('冒泡:父元素收到了 focus'); 
}, false);

// 2. 监听捕获阶段 (true)
parent.addEventListener('focus', () => {
    console.log('捕获:父元素收到了 focus');
}, true);

// 结果:
// 当你点击 input 时:
// 控制台只会打印:“捕获:父元素收到了 focus”
// 不会打印冒泡的那句。

9.4、总结(面试满分回答)

"关于事件流:

  1. 不冒泡的事件有很多:比如 focus/blur(焦点)、mouseenter/mouseleave(鼠标)、load/error(资源)、以及 video/audio 的媒体事件。这些事件的 event.bubbles 属性为 false,不会向上传播。
  2. 不捕获的事件几乎没有:在 DOM 树中,即使是不冒泡的事件(如 focus),依然会完整地走完捕获阶段(Window -> Target)。我们完全可以通过 addEventListener(..., true) 在父节点上监听到这些事件。
  3. 例外:唯一的例外是事件源本身就是 window 对象(如 window 的 resize),因为它是顶层,不存在父级,所以没有捕获路径。"

十、手写深拷贝函数

/**
 * 深拷贝函数
 * @param {any} obj - 要拷贝的对象
 * @param {WeakMap} hash - 用于解决循环引用的缓存
 */
function deepClone(obj, hash = new WeakMap()) {
  // 1. 边界判断:如果是 null 或者不是对象(是基本类型),直接返回
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 2. 特殊对象处理:Date 和 RegExp
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);

  // 3. 解决循环引用
  // 如果这个对象已经被拷贝过,直接返回之前拷贝的结果,不再递归,防止栈溢出
  if (hash.has(obj)) {
    return hash.get(obj);
  }

  // 4. 初始化容器
  // 使用 constructor 可以保留对象的原型链(支持 Array、Map、Set、Object)
  const cloneObj = new obj.constructor();

  // 记录到缓存中(注意:必须在递归之前记录)
  hash.set(obj, cloneObj);

  // 5. 处理 Map
  if (obj instanceof Map) {
    obj.forEach((value, key) => {
      cloneObj.set(key, deepClone(value, hash));
    });
    return cloneObj;
  }

  // 6. 处理 Set
  if (obj instanceof Set) {
    obj.forEach((value) => {
      cloneObj.add(deepClone(value, hash));
    });
    return cloneObj;
  }

  // 7. 处理对象和数组的通用属性
  // 使用 Reflect.ownKeys 可以获取到 Symbol 类型的 key,也可以遍历不可枚举属性
  // 如果面试官觉得太偏,可以用 for...in + hasOwnProperty 替代
  Reflect.ownKeys(obj).forEach(key => {
    cloneObj[key] = deepClone(obj[key], hash);
  });

  return cloneObj;
}

这是为您整理的 Markdown 格式笔记文档,结构清晰,包含原理、字段详解及 Nginx 配置示例,可以直接复制到笔记软件中。


十一、 浏览器缓存 (Browser Caching)

浏览器缓存是性能优化的重要一环。它主要分为两大类:强缓存协商缓存

11.1 缓存分类概览

缓存类型HTTP 状态码是否发送请求到服务器读取位置控制字段
强缓存200 (from memory/disk cache) (直接读取本地)内存或硬盘Expires, Cache-Control
协商缓存304 (Not Modified) (询问服务器)内存或硬盘Last-Modified, ETag

11.2 强缓存 (Strong Caching)

浏览器加载资源时,首先检查强缓存。如果命中,直接使用,不会与服务器发生交互。

11.2.1. Expires (HTTP/1.0)

  • 语法Expires: Wed, 22 Oct 2025 08:41:00 GMT
  • 原理:服务器返回一个绝对时间。如果当前客户端时间早于这个时间,则命中缓存。
  • 缺点:受限于客户端本地时间。如果用户修改了电脑时间,缓存可能失效或死循环。
  • 现状:已被 Cache-Control 取代,仅作兼容。

11.2.2. Cache-Control (HTTP/1.1) —— 核心

  • 语法Cache-Control: max-age=3600
  • 原理:指定一个相对时间(秒)。例如 max-age=3600 代表资源在 1 小时内有效。
  • 优先级:高于 Expires
  • 常见属性值
    • max-age=xxx:缓存有效时长(秒)。
    • no-cache注意坑点。它不是“不缓存”,而是跳过强缓存,直接进入协商缓存阶段(每次都去服务器问一下)。
    • no-store:真正的“不缓存”,绝对禁止存储任何副本。
    • public:客户端和代理服务器(CDN)都能缓存。
    • private:只能被客户端(浏览器)缓存。

11.3 协商缓存 (Negotiated Caching)

当强缓存失效(超过 max-age)或设置了 no-cache 时,浏览器会向服务器发起请求,验证资源是否有更新。

11.3.1. Last-Modified / If-Modified-Since (基于时间)

  • 流程
    1. 首次请求,Server 返回 Last-Modified: Fri, 27 Oct 2023... (文件最后修改时间)。
    2. 再次请求,Browser 自动带上 If-Modified-Since: Fri, 27 Oct 2023...
    3. Server 对比时间:
      • 时间一致 -> 返回 304 (直接用旧的)。
      • 时间不一致 -> 返回 200 + 新资源 + 新的 Last-Modified
  • 缺点
    • 精度只能到级。如果 1 秒内文件修改了多次,无法感知。
    • 如果文件被“摸”了一下(保存了但内容没变),修改时间变了,也会导致缓存失效。

11.3.2. ETag / If-None-Match (基于内容指纹) —— 更精准

  • 流程
    1. 首次请求,Server 计算文件 Hash 值,返回 ETag: "abc-123456"
    2. 再次请求,Browser 自动带上 If-None-Match: "abc-123456"
    3. Server 对比 Hash 值:
      • Hash 一致 -> 返回 304
      • Hash 不一致 -> 返回 200 + 新资源 + 新的 ETag
  • 优点:非常精准,只要内容变了 ETag 必变。
  • 缺点:服务器计算 Hash 需要消耗少量 CPU 性能。
  • 优先级:高于 Last-Modified

11.4 缓存判断流程图

  1. 浏览器发起请求
  2. 检测强缓存 (Cache-Control / Expires):
    • ✅ 命中 -> 直接使用 (200 OK)。
    • ❌ 未命中 -> 进入下一步
  3. 发送请求到服务器 (携带 If-None-MatchIf-Modified-Since)。
  4. 检测协商缓存
    • ✅ 命中 (服务器判定未修改) -> 返回 304 (浏览器读取本地缓存)。
    • ❌ 未命中 (服务器判定已修改) -> 返回 200 (下载新资源)。

11.5 如何设置?(Nginx 配置示例)

前端通常不直接写缓存策略,而是在服务器(如 Nginx)上配置。

最佳实践策略:

  • HTML 文件:使用协商缓存no-cache)。保证用户每次刷新都能拿到最新的 HTML,避免发版后用户还在用旧页面。
  • 静态资源 (JS/CSS/Img):使用强缓存max-age 设得很大,如 1 年)。因为现代打包工具(Webpack/Vite)会给文件名加上 Hash(如 app.a1b2.js),文件变了文件名就变了,不会有缓存冲突。

Nginx 配置代码:

server {
    # 1. 对于 HTML 页面:不做强缓存,只做协商缓存
    location ~ .*\.(html|htm)$ {
        # no-cache 表示让浏览器每次都去服务器询问(协商)
        add_header Cache-Control "no-cache"; 
    }

    # 2. 对于静态资源 (js, css, 图片):开启强缓存,过期时间设为 1 年
    # 因为文件名带 Hash,内容变了文件名也会变,所以放心缓存
    location ~ .*\.(js|css|png|jpg|gif)$ {
        # 31536000s = 365天
        add_header Cache-Control "max-age=31536000"; 
    }
}

十二、代码的执行顺序(事件循环)

// 选手 A:resolve 里面直接包一个值(现金)
new Promise((resolve) => {
    console.log('A 启动');
    resolve('A 成功'); 
}).then((res) => {
    console.log(res);
});

// 选手 B:resolve 里面包了一个 Promise(支票)
new Promise((resolve) => {
    console.log('B 启动');
    // 关键点在这里!传入了一个已经成功的 Promise
    resolve(Promise.resolve('B 成功')); 
}).then((res) => {
    console.log(res);
});

// 选手 C:用来计时的参照物
Promise.resolve()
    .then(() => console.log('C1 (Tick 1)'))
    .then(() => console.log('C2 (Tick 2)'))
    .then(() => console.log('C3 (Tick 3)'));

结论先行

当你在 resolve() 中传入一个 Promise 时,外层的 Promise 状态不会立即变成成功

它会说:“哦,你给了我一个 Promise?那我得等待这个 Promise 状态改变,并且我要拆开它拿到里面的值,才能算我成功。”

这个“等待 + 拆包”的过程,在 V8 引擎中会产生 2 个微任务的延迟(和在 .then 里 return Promise 的效果是一模一样的)。

为了好理解,我们可以把这个代码简化成这样理解

// 选手 A:resolve 里面直接包一个值(现金)
new Promise((resolve) => {
    console.log('A 启动');
    resolve('A 成功'); 
}).then((res) => {
    console.log(res);
});

// 选手 B:resolve 里面包了一个 Promise(支票)
new Promise((resolve) => {
    console.log('B 启动');
    // 关键点在这里!传入了一个已经成功的 Promise
    Promise.resolve().then(() => { // 【第二拍:拆包】
    // 引擎必须确保 P_In 的状态变化了,并且拿到值   P_In就是Promise.resolve('B 成功')
        P_In.then((val) => { // 【终局:按下开关】 
    // 只有到了这里,总开关才被按下 
    // P_Out 的状态才从 pending 变成 fulfilled 
            resolve(val); 
        }) 
    });
}).then((res) => {
    console.log(res);
});

// 选手 C:用来计时的参照物
Promise.resolve()
    .then(() => console.log('C1 (Tick 1)'))
    .then(() => console.log('C2 (Tick 2)'))
    .then(() => console.log('C3 (Tick 3)'));

执行结果

A 启动
B 启动
A 成功       <-- A 没有任何延迟,直接出来
C1 (Tick 1) 
C2 (Tick 2)
B 成功       <-- B 竟然排到了 C2 后面!(慢了两拍)
C3 (Tick 3)

十三、说一下你对cookie和localSrorage、session、indexDB 的理解

13.1. 核心对比总结表 (必背)

特性CookielocalStoragesessionStorageIndexedDB
数据生命周期可设置失效时间,默认是关闭浏览器失效永久有效,除非手动删除仅在当前 Tab 页有效,关闭标签页即消失永久有效,除非手动删除
存储大小很小,约 4KB较大,约 5MB较大,约 5MB巨大,通常 > 250MB (取决于硬盘)
与服务器通信自动携带在 HTTP 请求头中 (浪费流量)不参与服务器通信 (纯前端)不参与服务器通信 (纯前端)不参与服务器通信 (纯前端)
易用性 (API)难用,需自己封装 (document.cookie)简单 (setItem, getItem)简单 (setItem, getItem)复杂 (基于事件的异步 API)
访问权限任意窗口同源的所有窗口共享同源且同一窗口 (Tab)同源的所有窗口共享
数据类型只能存字符串只能存字符串 (需 JSON.stringify)只能存字符串支持二进制、对象、文件等复杂类型
操作过程同步同步 (阻塞主线程)同步 (阻塞主线程)异步 (不阻塞主线程)

13.2. 详细解析与应用场景

13.2.1. Cookie

老前辈,主要为了“维持状态”。 因为 HTTP 是无状态的,Cookie 的诞生是为了让服务器知道“你是谁”。

  • 优点:可以设置 HttpOnly 防止 XSS 攻击读取;可以设置过期时间。
  • 缺点:容量太小;每次请求都会带上,浪费带宽;API 操作繁琐。
  • 核心场景
    • 身份认证 (Token/SessionId)。
    • 广告追踪 (第三方 Cookie)。

13.2.2. localStorage

持久化的本地仓库。

  • 特点:除非用户主动清理缓存或代码手动删除,否则数据永远存在。
  • 核心场景
    • 用户偏好设置 (如:深色模式、字体大小)。
    • 长期存储的数据 (如:购物车数据、搜索历史)。
    • 注意:不要存敏感数据 (密码),容易被 XSS 攻击窃取。

13.2.3. sessionStorage

临时会话仓库。

  • 特点:生命周期最短,仅限于当前标签页。
    • 注意:你在 A 标签页存了 sessionStorage,打开同源的 B 标签页是访问不到的!(localStorage 可以)。
    • 特例:通过 window.open 或链接打开的新标签页,会复制一份 sessionStorage,但之后互不影响。
  • 核心场景
    • 表单分步填写 (防止刷新丢失,但又不希望关了浏览器还留着)。
    • 一次性登录信息 (某些安全性要求高的银行网站)。

13.2.4. IndexedDB

浏览器里的 NoSQL 数据库。

  • 特点
    • 容量大:几乎没有上限。
    • 异步:读写数据不会阻塞页面渲染(性能最好)。
    • 支持事务索引
    • 存万物:可以存图片 (Blob)、文件 (ArrayBuffer)、JS 对象。
  • 核心场景
    • 离线应用 (PWA):缓存大量 API 数据以便离线访问。
    • 编辑器/工具类应用:如 Figma、VSCode Web 版,需要存储大量本地文件或状态。
    • 性能优化:存储大量数据供 Web Worker 使用。

13.3. 面试高分回答逻辑

如果面试官问:“它们有什么区别?”,你可以这样回答:

  1. 总述:它们都是浏览器端的存储方案,但在容量生命周期应用场景上有很大区别。
  2. Web Storage (Local/Session)
    • 它是为了解决 Cookie 存储空间不足(4KB vs 5MB)和不必要的网络传输问题而设计的。
    • localStorage 是持久化的,适合存用户配置;sessionStorage 是会话级的,关闭标签页就没了,适合存表单临时数据。
  3. Cookie
    • 它的本质是维持服务器状态,每次请求都会带上,所以不适合存大量数据。主要用于身份验证(Token)。
  4. IndexedDB
    • 当需要存储大量数据(如几百 MB)或者二进制文件/结构化数据时,Web Storage 就不够用了,这时候要用 IndexedDB。它是异步的,性能更好,适合做离线应用。

十四、this的指向问题

14.1 代码执行结果

User

var name = 'name'  
const obj = {  
    name:'objname',  
    getName(){  
        console.log(this.name)  
    },  
    getName2:()=>{  
        console.log(this.name)  
    },  
    getName3(){  
        return function () {  
            console.log(this.name)  
        }  
    },
     getName4(){
        return ()=>{
             console.log(this.name)
        }
    }
}  
obj.getName()  
obj.getName2()  
obj.getName3()()   //这种写法其实就是下面的写法的合并
const fn = obj.getName3()  
fn()
obj.getName4()()
结果:
objname
name
name
name
objname

十五、Es6新特性

15.1 变量声明:let 和 const

  • 特性

    • 块级作用域:只在 {} 内有效,解决了 var 变量提升和污染全局变量的问题。
    • 不存在变量提升:存在“暂时性死区”(TDZ),必须先声明后使用。
    • 不可重复声明
    • const 用于声明常量(引用的地址不可变,但对象内部属性可变)。

15.2 函数增强:箭头函数 (Arrow Functions)

特性

  • 语法简洁:const add = (a, b) => a + b;
  • this 指向不同(重点) :箭头函数没有自己的 this,它的 this 是**继承自上层作用域(词法作用域)**的。这完美解决了回调函数中 this 指向 window 的痛点。
  • 不能作为构造函数:不能使用 new。
  • 没有 arguments 对象:需使用剩余参数 ...args 代替

15.3 解构赋值 (Destructuring)

  • 数组解构:const [a, b] = [1, 2];
  • 对象解构:const { name, age } = person;
  • 应用场景:交换变量值、提取接口返回的数据、函数参数定义。

15.4 字符串扩展:模板字符串 (Template Literals)

  • 特性:使用反引号 `。

  • 功能

    • 支持多行字符串(保留换行和空格)。
    • 支持变量插值:${variable},不再需要繁琐的 + 号拼接。

15.5 参数与运算符扩展

  • 默认参数:function foo(x = 1) {}。
  • 剩余参数 (Rest) :function foo(...args) {},args 是一个真数组,替代了 arguments。
  • 展开运算符 (Spread) :...,用于数组合并 [...arr1, ...arr2]、对象浅拷贝 {...obj}、将数组转为参数序列。

15.6 面向对象:Class 类

  • 特性:JS 原型继承的语法糖,让写法更像 Java/C++ 等传统 OOP 语言。
  • 关键字:class, constructor(构造函数), extends(继承), super(调用父类), static(静态方法)。

15.7 新的数据结构:Map 和 Set

  • Set:类似数组,但成员值都是唯一的(常用于数组去重 [...new Set(arr)])。
  • Map:类似对象,但键(Key)可以是任意类型(对象、函数等),而不仅限于字符串。

15.8 新的数据类型:Symbol

  • 特性:表示独一无二的值。
  • 应用:通常用于定义对象的唯一属性名,防止属性名冲突;或用于实现私有属性。

15.9 对象的操作、语法糖

语法糖:让写法更简单

  • 属性简写
    如果属性名和变量名一样,可以简写。

    codeJavaScript

    const name = 'Tom';
    // 旧写法
    const obj = { name: name };
    // 新写法
    const obj = { name };
    
  • 方法简写
    不再需要写 function 关键字。

codeJavaScript
```
const obj = {
    // 旧写法
    run: function() { ... },
    // 新写法
    run() { ... }
};
```
  • 属性名表达式(动态 Key)
    允许用表达式作为属性名(放在方括号 [] 内)。这在 React/Vue 动态修改表单数据时非常有用。
codeJavaScript
```
const key = 'age';
const obj = {
    [key]: 18, // 相当于 age: 18
    ['user_' + key]: 20 // 相当于 user_age: 20
};
```
  • 对象的扩展运算符 ... (ES2018)

**对象浅拷贝**
```
const obj = { a: 1, b: 2 };
const cloneObj = { ...obj }; // 也就是把 obj 里的东西“展开”放进去
```

**合并对象**:  

```
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const merged = { ...obj1, ...obj2 }; // { a: 1, b: 2 }
```

### 新增的常用静态方法 (API)
  • Object.assign(target, ...sources)

    • 作用:将源对象的所有可枚举属性复制到目标对象。
    • 场景:在扩展运算符 ... 出来之前,这是合并对象的主流方式。
    • 区别:Object.assign 是修改目标对象,而 ... 通常用于创建新对象。
  • Object.keys(obj) / Object.values(obj) / Object.entries(obj)

    • 作用:分别返回对象的 键数组值数组[键, 值] 数组
    • 场景:当你需要遍历对象渲染列表时(比如 Vue 的 v-for="(val, key) in obj" 底层逻辑)。

    codeJavaScript

    const obj = { a: 1, b: 2 };
    Object.keys(obj);   // ['a', 'b']
    Object.values(obj); // [1, 2]
    Object.entries(obj);// [['a', 1], ['b', 2]]
    
    • Object.is(value1, value2)

    • 作用:比较两个值是否严格相等。

    • 面试考点:它和 === 的区别?

      • Object.is(+0, -0) 为 false(=== 认为是 true)。
      • Object.is(NaN, NaN) 为 true(=== 认为是 false)。
  • Object.fromEntries(iterable)  (ES2019):

    • 作用:是 Object.entries 的逆操作。把键值对数组转回对象。
    • 场景:配合 Map 使用,或者过滤对象属性。