JavaScript 常用面试题目|全面解析高频考点与实战技巧

本文系统整理 javascript面试题 中最常被问及的 20+ 核心问题,覆盖基础语法、闭包、原型链、事件循环、ES6+ 新特性、异步编程、性能优化、框架底层原理等模块,结合真实企业面试案例、错误答案分析、标准解法与扩展延伸,助您高效突破技术关卡。

一、为什么 JavaScript 是前端面试的“核心战场”?

在前端技术栈中,javascript常用面试题目之所以占据绝对权重,是因为 JavaScript 不仅是浏览器的唯一脚本语言,更是现代全栈开发的重要支柱。无论是 Vue、React 还是 Node.js,其底层逻辑都深深植根于 JS 语言机制。

🔹 面试考察的三大层次

  • 基础理解层:变量提升、作用域、this 指向、类型转换等,考察语言基本功
  • 机制原理层:原型链、执行上下文、事件循环、垃圾回收,考察底层认知深度
  • 工程应用层:模块化、异步方案、性能调优、调试技巧,考察工程化能力

例如:某大厂一面常问“请说说 javascript常用面试题目 中你认为最易被误解的三个知识点”,这并非考察死记硬背,而是考察你对语言本质的反思能力与教学能力——能否把复杂概念讲得清晰易懂,是判断工程师是否具备技术影响力的关键。

二、基础概念高频题(附深度解析)

⚡ 题目1:var / let / const 的核心区别是什么?

常见错误答案:「var 有变量提升,let 和 const 没有」——这不完整!

标准答案应包含以下四维对比:

特性 var let const
作用域 函数作用域 块级作用域({ }) 块级作用域({ })
变量提升 是(提升并初始化为 undefined) 是(但存在“暂时性死区”TDZ) 是(同样存在 TDZ)
重复声明 允许 不允许 不允许
初始化要求 可不初始化 可不初始化 必须初始化

▶️ 延伸考点:TDZ(暂时性死区)导致的错误示例:

console.log(a); // undefined(var)
console.log(b); // ReferenceError: Cannot access 'b' before initialization(let)
let b = 2;

⚠️ 误区提醒:const 声明的“常量”并非完全不可变!对象和数组的属性/元素仍可修改:

const obj = { name: 'Alice' };
obj.name = 'Bob'; // ✅ 允许
obj = {}; // ❌ TypeError: Assignment to constant variable.

⚡ 题目2:null 与 undefined 的区别?

常见混淆点:「null 是空对象指针,undefined 是未定义」——这是定义,不是区别!

正确理解应从三方面展开:

  • 语义:null 表示“有意 absence of any object value”;undefined 表示“missing value”
  • 类型转换:Number(null) → 0;Number(undefined) → NaN
  • == 与 ===:null == undefined → true;null === undefined → false

▶️ 实战场景:API 返回空数据时,应返回 null 还是 undefined?

  • 返回 null:表示“存在该字段,但值为空”(如:用户信息存在,但邮箱未填)
  • 返回 undefined:表示“该字段不存在”(如:旧版 API 没有 phone 字段)

⚡ 题目3:== 与 === 的使用原则

“== 会进行类型转换,=== 不会”——这是现象,不是原则!

工程化建议:

  • ✅ 优先使用 ===(避免隐式转换陷阱,如 [] == ![] → true)
  • ⚠️ 特殊场景可用 ==:判断 null/undefined(`if (val == null)` 等价于 `val === null || val === undefined`)
  • ❌ 禁用:`if (val == '')`、`if (val == 0)` 等模糊比较

▶️ 扩展:Symbol.toPrimitive 如何影响 == 比较?

const obj = {
  valueOf() { return 1; },
  toString() { return '2'; }
};
console.log(obj == 1); // true(优先调用 valueOf)
console.log(obj == '2'); // true(字符串比较时调用 toString)

三、闭包:80% 面试者说不清的本质问题

⚡ 题目4:闭包的定义、作用与风险

❌ 错误答案:"闭包是函数嵌套函数"

✅ 正确定义:闭包是函数记住其词法作用域的能力,即使在其词法作用域之外执行

▶️ 核心三要素:

  1. 函数可以访问其外层作用域的变量
  2. 外层函数已返回,但内层函数仍被引用
  3. 被引用的变量不会被垃圾回收
function createCounter() {
  let count = 0; // 被闭包引用,不会释放
  return function() {
    return ++count;
  };
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2

⚠️ 内存泄漏风险:若闭包引用了大型 DOM 或循环引用,需手动解除引用:

element.onclick = function() {
  // 错误:引用了外部变量且未清理
  bigData.push(this.dataset);
};

⚡ 题目5:经典闭包面试题解析

问:以下代码输出什么?

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}

常见错误:输出 0,1,2

✅ 正确答案:输出 3,3,3(因 var 的函数作用域 + 闭包引用同一变量)

▶️ 三种解决方案:

// 方案1:let 块级作用域
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// 方案2:IIFE 创建新作用域
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(() => console.log(j), 100);
  })(i);
}
// 方案3:bind 绑定参数
for (var i = 0; i < 3; i++) {
  setTimeout(console.log.bind(null, i), 100);
}

四、原型链:从 [[Prototype]] 到继承机制

⚡ 题目6:__proto__ 与 prototype 的关系

❌ 常见混淆:"__proto__ 是对象的属性,prototype 是函数的属性"

✅ 精准表述:

  • __proto__:对象的内部属性 [[Prototype]] 的访问器(非标准但广泛支持)
  • prototype:函数对象独有的属性,用于设置其实例的 [[Prototype]]
  • 关键关系:`obj.__proto__ === Constructor.prototype`
function Person() {}
const p = new Person();
console.log(p.__proto__ === Person.prototype); // true

▶️ 图解原型链:

object → Person.prototype → Function.prototype → Object.prototype → null

⚠️ 陷阱:箭头函数无 prototype 属性!

⚡ 题目7:class 与 prototype 的等价性

问:`class Person {}` 与 `function Person() {}` 本质相同吗?

✅ 是!class 是语法糖,编译后等价于:

function Person() {} // constructor
Object.defineProperty(Person, 'prototype', {
  writable: false,
  configurable: false,
  // ...其他 descriptor
});

▶️ 继承对比:

// 传统原型链继承
function Animal() {}
Animal.prototype.eat = function() {}
function Dog() {}
Dog.prototype = new Animal(); // ❌ 问题:实例属性被共享
// class 继承(ES6)
class Animal {
  eat() {}
}
class Dog extends Animal {} // ✅ 使用 Object.setPrototypeOf

五、事件循环:宏任务与微任务的执行顺序

⚡ 题目8:事件循环机制详解

❌ 错误理解:"JS 是单线程,所以异步任务会排队"

✅ 正确模型:

  • 调用栈(Call Stack):执行同步代码
  • 任务队列(Task Queue):存放宏任务(setTimeout、I/O)
  • 微任务队列(Microtask Queue):存放微任务(Promise.then、MutationObserver)
  • 执行规则:栈空 → 执行所有微任务 → 渲染 → 取一个宏任务
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
// 输出顺序:A → D → C → B

▶️ 延伸:宏任务 vs 微任务

类型 示例 触发时机
宏任务 setTimeout, setInterval, setImmediate 下一轮事件循环
微任务 Promise.then, queueMicrotask, MutationObserver 当前事件循环末尾

⚡ 题目9:async/await 的底层实现

问:`async function` 返回什么?

✅ 返回一个 Promise 对象!其内部是 Generator 的语法糖。

async function foo() {
  return 1;
}
// 等价于:
function foo() {
  return Promise.resolve(1);
}

▶️ 执行流程:

  1. 遇到 await → 暂停执行,将后续代码作为微任务注册
  2. await 的 Promise resolve 后 → 恢复执行
async function test() {
  console.log(1);
  await console.log(2);
  console.log(3);
}
test();
// 输出:1 → 2 → 3(注意:await 后的 console.log(2) 是同步执行的!)

六、ES6+ 核心特性:从 let 到 Proxy

⚡ 解构赋值:不只是简化代码

常见误区:`const { a, b } = { a: 1, b: 2 }` 只是语法糖

✅ 深度价值:

  • 默认值:`const { a=1, b=2 } = obj`(仅当值为 undefined 时生效)
  • 重命名:`const { name: userName } = user`
  • 嵌套解构:`const { user: { profile: { age } } } = data`

▶️ 面试陷阱:解构时的错误边界处理

const { name } = null; // TypeError: Cannot destructure property 'name' of 'null'

⚡ 展开运算符:对象浅拷贝与合并

问:`{ ...obj1, ...obj2 }` 与 `Object.assign(obj1, obj2)` 有何区别?

✅ 关键差异:

特性 展开运算符 Object.assign
可枚举性 只复制可枚举属性 只复制可枚举属性
原型链 不复制原型 不复制原型
getter/setter 调用 getter 并设置新值 调用 getter 并设置新值
语法位置 可插入任意位置 目标对象必须在首位

▶️ 实际应用:数组去重(ES6)

const arr = [1, 2, 2, 3];
const unique = [...new Set(arr)]; // [1,2,3]

⚡ Proxy:拦截与定制对象操作

问:Proxy 与 Object.defineProperty 的区别?

✅ 核心优势:

  • 拦截能力:Proxy 可拦截 13 种操作(get/set/has/deleteProperty 等)
  • 数组支持:Object.defineProperty 无法拦截数组索引操作
  • 性能:Proxy 在属性多时性能更优(无需遍历所有属性)
const handler = {
  get(target, key) {
    return key in target ? target[key] : 'default';
  }
};
const p = new Proxy({}, handler);
console.log(p.name); // 'default'

⚠️ 限制:不能拦截 Proxy 内部的 this 绑定!

七、性能优化:从内存泄漏到渲染瓶颈

⚡ 题目10:如何排查内存泄漏?

✅ 四步排查法:

  1. 工具定位:Chrome DevTools → Memory → Heap Snapshot
  2. 关键指标:关注 "Detached DOM Trees"
  3. 常见场景:
    • 未清理的定时器(setInterval)
    • 事件监听未移除
    • 闭包引用大对象
    • 全局变量累积
  4. 解决方案:使用 WeakMap/WeakSet 存储缓存数据
// 错误:全局数组累积
const cache = [];
function loadData() {
  cache.push(fetchData()); // 内存持续增长
}
// 正确:WeakMap 自动清理
const cache = new WeakMap();
function loadData(data) {
  cache.set(data, processData(data));
}

⚡ 题目11:减少重排与重绘

❌ 错误实践:频繁操作 DOM

// 禁止:多次触发重排
div.style.width = '100px';
div.style.height = '100px';
div.style.color = 'red';

✅ 优化方案:

  • 批量操作:使用 documentFragment
  • 离线操作:display:none → 修改 → display:block
  • 样式分离:通过 className 替代 inline style
  • 硬件加速:transform/opacity 替代 top/left

▶️ 进阶:requestAnimationFrame 与 throttle 结合

let pending = false;
function update() {
  if (!pending) {
    pending = true;
    requestAnimationFrame(() => {
      // 执行 DOM 更新
      pending = false;
    });
  }
}

八、框架原理:从 React 到 Vue 的底层逻辑

⚡ 题目12:虚拟 DOM 的优缺点

❌ 误区:"虚拟 DOM 更快"

✅ 真实对比:

场景 虚拟 DOM 直接 DOM 操作
首次渲染 慢(需创建 JS 对象) 快
小规模更新 快(diff 算法) 快(直接操作)
大规模更新 快(批量更新) 慢(多次重排)
复杂交互 优势明显 代码复杂易出错

▶️ 关键机制:

  • diff 算法:同层比较(O(n) 而非 O(n³))
  • key 的作用:标识节点身份(避免移动导致重渲染)
  • 静态节点提升:跳过不变节点的 diff

⚡ 题目13:响应式原理对比

Vue 2:Object.defineProperty

// 限制:无法监听数组索引与新增属性
const obj = {};
Object.defineProperty(obj, 'name', {
  get() { return value; },
  set(v) { update(v); }
});

Vue 3:Proxy

const obj = new Proxy({}, {
  get(target, key) { return Reflect.get(...); },
  set(target, key, value) { return Reflect.set(...); }
});

React:状态驱动 + 重渲染

  • 函数组件:通过 useState 更新状态
  • 类组件:通过 setState 触发 shouldComponentUpdate
  • 关键机制:Fiber 架构实现可中断渲染
// useState 的本质:调度更新
const [count, setCount] = useState(0);
setCount(count + 1); // 触发调度 → 新渲染单元