前端面试真题深度解析:从原型到安全,七道题看透核心能力

155 阅读5分钟

1. JS 的原型

问题: 能讲讲 JavaScript 的原型机制吗?__proto__prototype 有什么区别?

回答:

JavaScript 是基于原型的面向对象语言,没有传统类的概念(ES6 的 class 只是语法糖)。它的继承机制依赖于“原型链”。

每个函数都有一个 prototype 属性,它指向一个对象,这个对象就是该函数作为构造函数时,其实例的原型。而每个对象都有一个内部属性 [[Prototype]],在浏览器中通常暴露为 __proto__,它指向其构造函数的 prototype

当访问一个对象的属性时,如果该对象本身没有,就会沿着 __proto__ 向上查找,直到 null,这就是原型链。

function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function () {
  console.log(`Hello, I'm ${this.name}`);
};

const p1 = new Person('Alice');
p1.sayHello(); // "Hello, I'm Alice"

原型链查找过程

%% 添加动画演示原型查找过程
sequenceDiagram
    participant JS as JavaScript引擎
    participant I as 实例p1
    participant P as Person.prototype

    JS->>I: p1.sayHello()
    alt 方法存在
        I-->>JS: 直接执行 ✅
    else 不存在
        I->>P: 查找 __proto__
        P->>P: 找到 sayHello()
        P-->>JS: 执行并返回结果
    end

图解:

  • A → B:尝试调用 p1.sayHello(),JS 引擎首先检查 p1 自身是否有该属性。
  • B → C → D:没有,于是通过 __proto__ 指向其构造函数 Personprototype 对象。
  • D → E → G:在 Person.prototype 上找到了 sayHello 方法,绑定 thisp1 并执行。

关键点:

  • prototype 是函数才有的属性,用于实例继承。
  • __proto__ 是对象的内部原型引用(现代应使用 Object.getPrototypeOf())。
  • 所有对象最终原型链都会指向 Object.prototype,其 __proto__null

2. 变量作用域链

问题: 什么是作用域链?它在变量查找中起什么作用?

回答:

作用域链是 JavaScript 用来确定变量访问权限的机制。它是在函数定义时就确定的,基于词法作用域(静态作用域),而不是函数调用时。

当一个函数被定义,它会“记住”自己所在的作用域环境,形成一个作用域链。在查找变量时,从当前作用域开始,逐层向外查找,直到全局作用域。

const x = 1;

function outer() {
  const y = 2;
  function inner() {
    const z = 3;
    console.log(x, y, z); // 1, 2, 3
  }
  inner();
}
outer();

作用域链变量查找

%% 动态演示作用域查找过程(可用于 Live Editor 播放)
sequenceDiagram
    participant Engine as JS引擎
    participant Inner as inner()
    participant Outer as outer()
    participant Global as 全局

    Engine->>Inner: 调用 inner()
    Inner->>Inner: 创建执行上下文
    Inner->>Inner: 查找 x
    Inner->>Outer: 沿作用域链查找 x
    Outer->>Global: 继续查找 x
    Global-->>Inner: 返回 x=1
    Inner->>Outer: 查找 y
    Outer-->>Inner: 返回 y=2
    Inner->>Inner: 找到 z=3
    Inner-->>Engine: 输出 1,2,3

图解:

  • A → Binner 执行时,创建执行上下文,其作用域链包含:inner 自身 → outer → 全局。
  • C → D → E → F → G → Hx 不在 innerouter 中定义,最终在全局找到。
  • I → Jyouter 中定义,沿链找到。
  • K → Lzinner 中定义,直接使用。

关键点:

  • 作用域链在函数定义时确定,与调用位置无关。
  • 闭包的本质就是函数保留了对外部作用域的引用,即使外部函数已执行完毕。

3. call、apply、bind 的区别

问题: callapplybind 有什么区别?什么时候用哪个?

回答:

三者都用于改变函数执行时的 this 指向,但调用方式和返回值不同。

  • call(thisArg, arg1, arg2, ...): 立即执行函数,参数逐个传入。
  • apply(thisArg, [argsArray]): 立即执行函数,参数以数组形式传入。
  • bind(thisArg, arg1, arg2, ...): 返回一个新函数,不会立即执行,可后续调用。
function greet(greeting, punctuation) {
  console.log(`${greeting}, I'm ${this.name}${punctuation}`);
}

const person = { name: 'Bob' };

greet.call(person, 'Hi', '!');     // "Hi, I'm Bob!"
greet.apply(person, ['Hey', '?']); // "Hey, I'm Bob?"
const boundGreet = greet.bind(person, 'Hello');
boundGreet('.'); // "Hello, I'm Bob."

三者调用对比

sequenceDiagram
    participant Caller
    participant greet as greet函数
    participant person as person对象

    Caller->>greet: call(person, 'Hi', '!')
    greet-->>Caller: 立即执行,this=person

    Caller->>greet: apply(person, ['Hey', '?'])
    greet-->>Caller: 立即执行,this=person

    Caller->>greet: bind(person, 'Hello')
    greet-->>Caller: 返回新函数 boundGreet
    Caller->>boundGreet: boundGreet('.')
    boundGreet-->>Caller: 执行,this=person

图解:

  • callapply 都是立即调用,区别仅在参数形式。
  • bind延迟绑定,返回一个 this 已固定的新函数,适合事件回调、setTimeout 等场景。

实战建议:

  • 数组参数用 apply(如 Math.max.apply(null, arr))。
  • 需要预设部分参数时用 bind(柯里化)。
  • call 更通用,参数明确时优先。

4. 防抖和节流的区别

问题: 防抖和节流有什么区别?分别适用什么场景?

回答:

两者都是控制函数执行频率的手段,用于优化高频触发事件(如 resize、scroll、input)。

  • 防抖(Debounce):事件频繁触发时,只执行最后一次。如果持续触发,执行会被不断推迟。
  • 节流(Throttle):事件触发后,在一定时间窗口内只执行一次,之后可再次执行。
// 防抖
function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer); // 🔍 清除上一次未执行的定时器
    timer = setTimeout(() => {
      fn.apply(this, args); // 🔍 保证 this 和参数正确传递
    }, delay);
  };
}

// 节流(定时器版)
function throttle(fn, delay) {
  let timer = null;
  return function (...args) {
    if (timer) return; // 🔍 如果已有定时器,跳过本次
    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = null; // 🔍 执行后释放锁
    }, delay);
  };
}

Mermaid 状态图:防抖 vs 节流

stateDiagram-v2
    [*] --> Idle

    state Debounce {
        Idle --> Pending: 事件触发
        Pending --> Idle: delay 内无新事件 → 执行
        Pending --> Pending: 新事件触发 → 重置定时器
    }

    state Throttle {
        Idle --> Executing: 事件触发
        Executing --> Cooldown: 执行函数
        Cooldown --> Idle: delay 时间到
        Executing --> Executing: 事件触发 → 忽略
    }

    [*] --> Debounce
    [*] --> Throttle

图解:

  • 防抖:每次触发都重置计时,只有“静默期”结束后才执行。适合搜索框输入、窗口停止调整后执行。
  • 节流:保证在 delay 时间内最多执行一次。适合滚动加载、按钮防重复点击。

踩坑提醒:

  • 防抖在持续触发时可能永远不执行,需结合业务判断。
  • 节流还有“时间戳版”,性能更好但首次执行时机不同。

5. 介绍各种异步方案

问题: JS 异步发展经历了哪些阶段?Promise、async/await 解决了什么问题?

回答:

JS 异步经历了回调 → Promise → async/await 的演进,核心是解决“回调地狱”和错误处理问题。

// 1. 回调(Callback Hell)
getData(function (a) {
  getMoreData(a, function (b) {
    getEvenMoreData(b, function (c) {
      console.log(c);
    });
  });
});

// 2. Promise(链式调用)
getData()
  .then(a => getMoreData(a))
  .then(b => getEvenMoreData(b))
  .then(c => console.log(c))
  .catch(err => console.error(err)); // 🔍 统一错误处理

// 3. async/await(同步写法)
async function fetchData() {
  try {
    const a = await getData();
    const b = await getMoreData(a);
    const c = await getEvenMoreData(b);
    console.log(c);
  } catch (err) {
    console.error(err); // 🔍 错误冒泡,像同步代码一样处理
  }
}

异步演进对比

%% =========================================================
%% 专业级 JavaScript 事件循环时序图
%% 主题:宏任务 / 微任务 / async-await 全流程
%% 适配:深色 / 浅色模式自适应
%% 图标:FontAwesome 6.5
%% =========================================================

%% 1. 全局样式
%% 2. 参与者定义
%% 3. 消息流
%% 4. 高亮与分组
%% 5. 动效提示(hover 说明)

%% ========== 1. 全局样式 ==========
%% 主色:#0ea5e9(sky-500)
%% 辅色:#10b981(emerald-500)
%% 警告:#f59e0b(amber-500)
%% 背景:#ffffff / #111827(自适应)
%% 字体:Inter, system-ui, sans-serif
%% 圆角:8px
%% 阴影:0 4px 6px -1px rgba(0,0,0,0.1)

%% ========== 2. 参与者定义 ==========
sequenceDiagram
    autonumber
    participant Main   as <i class="fa fa-microchip"></i> 主线程
    participant API    as <i class="fa fa-cogs"></i> Web API
    participant MacroQ as <i class="fa fa-list-ol"></i> 宏任务队列
    participant MicroQ as <i class="fa fa-list-ul"></i> 微任务队列
    participant AsyncF as <i class="fa fa-magic"></i> async 函数

    %% ========== 3. 消息流 ==========
    rect rgb(14,165,233,0.1)
        note left of Main: ① 经典回调
        Main  ->> API   : setTimeout / XHR(回调)
        API   -->> MacroQ: 完成后入队
        MacroQ->>Main   : 事件循环取出
        Main  ->>Main   : 执行回调
    end

    rect rgb(16,185,129,0.1)
        note left of Main: ② Promise.then
        Main  ->> API   : Promise.then()
        API   -->> MicroQ: 微任务入队
        MicroQ->>Main   : 立即优先执行
        Main  ->>Main   : then 回调
    end

    rect rgb(245,158,11,0.1)
        note left of Main: ③ async/await
        Main  ->> AsyncF: await promise
        Main  ->> Main   : 暂停函数,不阻塞线程
        API   -->> MicroQ: promise 完成后
        MicroQ->>AsyncF : 恢复执行
    end

    %% ========== 4. 高亮与分组 ==========
    %% 关键消息高亮
    activate Main
        note over Main: 事件循环持续检查队列
    deactivate Main

    %% ========== 5. 动效提示(hover 说明) ==========
    %% 部署时可通过 CSS 或 JS 实现:
    %% .participantBox[id*="Main"]:hover::after { content: "主线程单线程执行 JS"; }
    %% .participantBox[id*="MicroQ"]:hover::after { content: "微任务队列优先级高于宏任务"; }

图解:

  • 回调:嵌套深,错误处理分散。
  • Promise:扁平化,支持链式调用和 .catch()
  • async/await:用同步语法写异步,可配合 try/catch,可读性最强。

关键点:

  • Promise 是微任务,比 setTimeout(宏任务)优先执行。
  • await 只能在 async 函数内使用。
  • 错误必须用 try/catch 捕获,否则会变成未处理的 Promise rejection

6. XSS 与 CSRF

问题: 说说 XSS 和 CSRF 的区别?如何防范?

回答:

XSS(跨站脚本攻击):攻击者注入恶意脚本,当其他用户浏览页面时执行,窃取 cookie、session 等。

CSRF(跨站请求伪造):攻击者诱导用户在已登录状态下发起非本意的请求,如转账、发帖。

<!-- XSS 示例:注入脚本 -->
<script>
  fetch('/api/steal-cookie', {
    method: 'POST',
    body: document.cookie // 🔍 窃取用户 cookie
  });
</script>

<!-- CSRF 示例:诱导提交表单 -->
<img src="http://bank.com/transfer?to=attacker&amount=1000" width="0">

XSS 与 CSRF 攻击路径

%% =========================================================
%% 专业级 Web 安全威胁对比图
%% 主题:XSS vs CSRF 攻击链可视化
%% 适配:深色 / 浅色模式自适应
%% 图标:FontAwesome 6.5
%% =========================================================

%% 1. 全局样式
%% 2. 节点定义
%% 3. 连线定义
%% 4. 子图分组
%% 5. 动效提示(hover 说明)

%% ========== 1. 全局样式 ==========
%% 主色:#ef4444(red-500)- 攻击
%% 辅色:#f97316(orange-500)- 中间步骤
%% 成功:#10b981(emerald-500)- 攻击达成
%% 背景:#ffffff / #111827(自适应)
%% 字体:Inter, system-ui, sans-serif
%% 圆角:8px
%% 阴影:0 4px 6px -1px rgba(0,0,0,0.1)

%% ========== 2. 节点定义 ==========
graph LR
    classDef attack    fill:#ef4444,stroke:#dc2626,color:#fff,stroke-width:2px,rx:8px,ry:8px
    classDef step      fill:#f97316,stroke:#ea580c,color:#fff,stroke-width:2px,rx:8px,ry:8px
    classDef success   fill:#10b981,stroke:#059669,color:#fff,stroke-width:2px,rx:8px,ry:8px
    classDef user      fill:#3b82f6,stroke:#2563eb,color:#fff,stroke-width:2px,rx:8px,ry:8px
    classDef server    fill:#6366f1,stroke:#4f46e5,color:#fff,stroke-width:2px,rx:8px,ry:8px

    %% XSS 攻击链
    A["<i class='fa fa-user-ninja'></i> 攻击者提交含 script 的内容"]:::attack
    B["<i class='fa fa-database'></i> 服务器存储或返回"]:::server
    C["<i class='fa fa-user'></i> 用户访问页面"]:::user
    D["<i class='fa fa-bolt'></i> 浏览器执行恶意脚本"]:::step
    E["<i class='fa fa-mask'></i> 窃取数据或冒充用户"]:::success

    %% CSRF 攻击链
    F["<i class='fa fa-user-check'></i> 用户登录 bank.com"]:::user
    G["<i class='fa fa-user-ninja'></i> 用户访问 attacker.com"]:::attack
    H["<i class='fa fa-paper-plane'></i> 页面自动发起 bank.com 请求"]:::step
    I["<i class='fa fa-cookie-bite'></i> 浏览器携带 cookie"]:::step
    J["<i class='fa fa-shield-alt'></i> bank.com 认为是合法请求"]:::server
    K["<i class='fa fa-money-bill-transfer'></i> 执行转账等操作"]:::success

    %% ========== 3. 连线定义 ==========
    A --> B
    B --> C
    C --> D
    D --> E

    F -.-> G
    G --> H
    H --> I
    I --> J
    J --> K

    %% ========== 4. 子图分组 ==========
    subgraph XSS 攻击链
        direction LR
        A
        B
        C
        D
        E
    end

    subgraph CSRF 攻击链
        direction LR
        F
        G
        H
        I
        J
        K
    end

    %% ========== 5. 动效提示(hover 说明) ==========
    %% 注:Mermaid 暂不支持原生 hover,以下为建议实现方式
    %% 实际部署时可通过 CSS 或 JS 实现:
    %% .node[id^="A"]:hover::after { content: "攻击者通过输入框、评论等提交恶意脚本"; }
    %% .node[id^="E"]:hover::after { content: "可窃取 Cookie、Session、DOM 数据或执行任意操作"; }
    %% .node[id^="K"]:hover::after { content: "利用用户已登录状态,执行非预期操作"; }

    %% 关键路径高亮
    linkStyle 0 stroke:#ef4444,stroke-width:3px
    linkStyle 1 stroke:#f97316,stroke-width:3px
    linkStyle 2 stroke:#3b82f6,stroke-width:3px
    linkStyle 3 stroke:#10b981,stroke-width:3px
    linkStyle 4 stroke:#3b82f6,stroke-width:3px,stroke-dasharray: 5 5
    linkStyle 5 stroke:#ef4444,stroke-width:3px
    linkStyle 6 stroke:#f97316,stroke-width:3px
    linkStyle 7 stroke:#6366f1,stroke-width:3px
    linkStyle 8 stroke:#10b981,stroke-width:3px

图解:

  • XSS:攻击目标是用户浏览器,利用信任执行脚本。
  • CSRF:攻击目标是服务器接口,利用用户身份伪造请求。

防范措施:

  • XSS
    • 输入转义(HTML 实体编码)
    • 使用 CSP(Content Security Policy)
    • 设置 HttpOnly cookie(防止 JS 读取)
  • CSRF
    • 使用 SameSite cookie 属性(推荐 StrictLax
    • 验证 Referer / Origin
    • 关键操作使用 Token(如 CSRF Token)

7. HTTP 缓存控制

问题: HTTP 缓存有哪些机制?强缓存和协商缓存有什么区别?

回答:

HTTP 缓存分为强缓存协商缓存,浏览器优先使用强缓存,失效后再走协商缓存。

  • 强缓存:不发请求,直接使用本地缓存。由 Cache-ControlExpires 控制。
  • 协商缓存:发请求,由服务器判断是否更新。由 ETag/If-None-MatchLast-Modified/If-Modified-Since 实现。
# 响应头(服务器设置)
Cache-Control: max-age=3600
ETag: "abc123"
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
%% 专业级 HTTP 缓存流程图
%% 作者:Mermaid 可视化架构师
%% 主题:HTTP 缓存机制(强缓存 + 协商缓存)
%% 适配:深色 / 浅色模式自适应
%% 图标:FontAwesome 6.5

%% 1. 全局样式
%% 2. 节点定义
%% 3. 连线定义
%% 4. 子图分组

%% ========== 1. 全局样式 ==========
%% 使用 HSL 色轮生成协调色系
%% 主色:#0ea5e9(sky-500)
%% 辅色:#10b981(emerald-500)
%% 警告:#f59e0b(amber-500)
%% 异常:#ef4444(red-500)
%% 背景:#ffffff / #111827(自适应)
%% 字体:Inter, system-ui, sans-serif
%% 圆角:8px
%% 阴影:0 4px 6px -1px rgba(0,0,0,0.1)

%% ========== 2. 节点定义 ==========
graph TD
    classDef default     fill:#0ea5e9,stroke:#0284c7,color:#fff,stroke-width:2px,rx:8px,ry:8px
    classDef decision    fill:#f59e0b,stroke:#d97706,color:#fff,stroke-width:2px,rx:8px,ry:8px
    classDef success     fill:#10b981,stroke:#059669,color:#fff,stroke-width:2px,rx:8px,ry:8px
    classDef error       fill:#ef4444,stroke:#dc2626,color:#fff,stroke-width:2px,rx:8px,ry:8px
    classDef cache       fill:#8b5cf6,stroke:#7c3aed,color:#fff,stroke-width:2px,rx:8px,ry:8px

    %% 节点定义(带图标)
    A["<i class='fa fa-paper-plane'></i> 发起请求"]:::default
    B{"<i class='fa fa-question-circle'></i> 强缓存有效?"}:::decision
    C["<i class='fa fa-check-circle'></i> 直接使用本地缓存"]:::success
    D["<i class='fa fa-arrow-right'></i> 发送请求,带协商头"]:::default
    E["<i class='fa fa-server'></i> 服务器比对 ETag / Last-Modified"]:::default
    F["<i class='fa fa-info-circle'></i> 返回 304 Not Modified"]:::success
    G["<i class='fa fa-download'></i> 返回 200 + 新资源"]:::cache
    H["<i class='fa fa-save'></i> 使用本地缓存"]:::success
    I["<i class='fa fa-sync-alt'></i> 更新缓存"]:::cache

    %% ========== 3. 连线定义 ==========
    A --> B
    B -- 是 --> C
    B -- 否 --> D
    D --> E
    E -- 未改变 --> F
    E -- 已改变 --> G
    F --> H
    G --> I

    %% ========== 4. 子图分组 ==========
    subgraph 强缓存阶段
        A
        B
        C
    end

    subgraph 协商缓存阶段
        D
        E
        F
        G
        H
        I
    end

    %% 关键路径高亮
    linkStyle 0 stroke:#0ea5e9,stroke-width:3px
    linkStyle 1 stroke:#10b981,stroke-width:3px
    linkStyle 2 stroke:#f59e0b,stroke-width:3px
    linkStyle 3 stroke:#0ea5e9,stroke-width:3px
    linkStyle 4 stroke:#10b981,stroke-width:3px
    linkStyle 5 stroke:#8b5cf6,stroke-width:3px
    linkStyle 6 stroke:#10b981,stroke-width:3px
    linkStyle 7 stroke:#8b5cf6,stroke-width:3px

图解:

  • B → Cmax-age 未过期,直接读本地缓存,不发请求。
  • D → E → F → H:强缓存过期,发请求,服务器发现资源未变,返回 304,浏览器继续用旧资源。
  • D → E → G → I:资源已更新,返回新内容并更新缓存。

关键点:

  • Cache-Control 优先级高于 Expires
  • ETag 精度更高(内容哈希),Last-Modified 可能因秒级精度误判。
  • 静态资源建议设置长期强缓存 + 文件名哈希(如 app.a1b2c3.js),避免更新问题。