JavaScript 核心

S 级 · 前端面试必考 · 每题都要能讲清楚

1 闭包与作用域

什么是闭包?使用场景有哪些?
闭包是指一个函数能够访问其词法作用域中的变量,即使该函数在其作用域之外执行。本质是函数 + 它引用的外部变量形成的"背包"。

使用场景:

  • 数据私有化:模块模式,用闭包隐藏内部状态
  • 函数工厂makeAdder(5) 返回一个加 5 的函数
  • 回调/事件处理:setTimeout、addEventListener 中保持对外部变量的引用
  • 柯里化curry(fn) 逐步收集参数
function createCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    getCount: () => count
  };
}
const counter = createCounter();
counter.increment(); // 1 — count 被闭包"记住"了

注意:闭包会延长变量生命周期,可能导致内存泄漏。不再需要时将引用置为 null。

var / let / const 区别?暂时性死区?
var:函数作用域,存在变量提升(声明提升,值为 undefined)。
let:块作用域,有暂时性死区(TDZ),不可重复声明。
const:块作用域 + 不可重新赋值(但对象属性可修改)。

暂时性死区(TDZ):从块的开始到 let/const 声明语句之间,变量存在但不可访问,访问会抛 ReferenceError。

console.log(a); // undefined(var 提升)
var a = 1;

console.log(b); // ReferenceError(TDZ)
let b = 2;
作用域链是什么?
JS 采用词法作用域(静态作用域),作用域在代码书写时就确定。查找变量时,从当前作用域逐级向上查找,直到全局作用域,这条链就是作用域链。

每个执行上下文都有一个 [[Scope]],指向外层作用域。函数嵌套越深,链越长。

2 原型与继承

原型链是什么?__proto__ 和 prototype 的区别?
每个对象都有一个隐式原型 __proto__,指向其构造函数的 prototype。查找属性时沿着 __proto__ 一路向上,直到 null,这条链就是原型链。
function Person(name) { this.name = name; }
Person.prototype.sayHi = function() { return 'Hi ' + this.name; };

const p = new Person('Tom');
p.__proto__ === Person.prototype;       // true
Person.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null;    // 链终点

总结:prototype 是函数才有的属性,__proto__ 是每个对象都有的(推荐用 Object.getPrototypeOf())。

new 操作符做了什么?
四步:① 创建空对象 → ② 将空对象的 __proto__ 指向构造函数的 prototype → ③ 用该对象作为 this 执行构造函数 → ④ 如果构造函数返回对象则用它,否则返回新对象。
function myNew(Constructor, ...args) {
  const obj = Object.create(Constructor.prototype);
  const result = Constructor.apply(obj, args);
  return result instanceof Object ? result : obj;
}
ES6 class 和原型继承的关系?
class 是原型继承的语法糖。class 方法定义在 prototype 上,extends 通过修改原型链实现继承,super 调用父类构造函数。

区别:class 不会提升(TDZ),内部自动严格模式,必须用 new 调用。

3 this 指向

this 的四种绑定规则?优先级?
默认绑定:独立调用 → window(严格模式 undefined)
隐式绑定:obj.fn() → obj
显式绑定:call/apply/bind → 指定对象
new 绑定:new Fn() → 新创建的对象
优先级:new > 显式 > 隐式 > 默认

隐式丢失陷阱:把方法赋值给变量后调用,this 会回到默认绑定。

const obj = {
  name: 'Tom',
  greet() { console.log(this.name); }
};
const fn = obj.greet;
fn(); // undefined — 隐式绑定丢失
箭头函数的 this 有什么不同?
箭头函数没有自己的 this,它捕获定义时所在作用域的 this(词法 this),且无法通过 call/apply/bind 修改。也没有 arguments、prototype,不能作为构造函数。
call / apply / bind 区别?
call(thisArg, a, b):立即执行,参数逐个传。
apply(thisArg, [a, b]):立即执行,参数以数组传。
bind(thisArg, a):不立即执行,返回一个绑定了 this 的新函数。

4 事件循环

事件循环机制?宏任务/微任务?
JS 是单线程的,通过事件循环处理异步。每轮循环:执行一个宏任务 → 清空所有微任务 → 渲染(如果需要)→ 下一个宏任务。

宏任务:script 整体、setTimeout、setInterval、I/O、UI 渲染

微任务:Promise.then/catch/finally、MutationObserver、queueMicrotask

关键:微任务优先级高于宏任务。一个宏任务执行完,会把所有排队的微任务全部执行完(包括微任务中产生的新微任务),然后才执行下一个宏任务。

经典输出题:Promise + setTimeout 执行顺序
console.log('1');              // 同步
setTimeout(() => console.log('2'), 0);  // 宏任务
Promise.resolve()
  .then(() => console.log('3'))   // 微任务
  .then(() => console.log('4'));  // 微任务
console.log('5');              // 同步

// 输出: 1 → 5 → 3 → 4 → 2
执行栈先走完同步(1、5)→ 清空微任务队列(3、4)→ 执行宏任务(2)。
async/await 的原理?
async/await 是 Generator + Promise 的语法糖。async 函数返回 Promise,await 后面的表达式会被包装成 Promise.resolve(),await 之后的代码相当于 .then() 回调,属于微任务
async function foo() {
  console.log('A');       // 同步执行
  await bar();            // bar() 同步执行,await 之后的代码变成微任务
  console.log('B');       // 微任务
}
// 等价于:
function foo() {
  console.log('A');
  return bar().then(() => console.log('B'));
}

5 Promise

Promise 三种状态?状态转换规则?
pending(进行中)→ fulfilled(已成功)或 rejected(已失败)。
状态一旦改变就不可逆,只能从 pending 转换一次。
Promise.all / allSettled / race / any 区别?
all:全部成功才成功,任一失败就失败(快速失败)。
allSettled:等全部完成,返回每个的状态和值/原因。
race:第一个完成的结果(无论成功失败)。
any:第一个成功的,全部失败才失败(AggregateError)。

面试话术:需要并发请求且全部需要成功 → all;需要容错(部分失败也要结果)→ allSettled;需要最快响应 → race;需要最快成功 → any。

手写 Promise.all
function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    const results = [];
    let count = 0;
    const len = promises.length;
    if (len === 0) return resolve([]);

    promises.forEach((p, i) => {
      Promise.resolve(p).then(val => {
        results[i] = val;
        if (++count === len) resolve(results);
      }, reject);
    });
  });
}

关键点:① 用 Promise.resolve(p) 兼容非 Promise 值;② 用索引 i 保证顺序;③ 计数器判断全部完成。

6 类型与转换

== 和 === 的区别?隐式转换规则?
=== 严格相等,不做类型转换。== 宽松相等,会隐式类型转换。
建议始终用 ===,只有和 null 比较时用 == 有价值(x == null 同时匹配 null 和 undefined)。

== 的转换规则:

  • null == undefined → true(仅这两者互等)
  • Number 和 String → String 转 Number
  • Boolean 参与 → Boolean 先转 Number
  • Object 和原始值 → 调用 valueOf / toString
typeof 和 instanceof 的区别?
typeof:返回字符串,判断原始类型(注意 typeof null === 'object' 是历史 bug)。
instanceof:判断对象是否在某构造函数的原型链上。
typeof 42        // 'number'
typeof null      // 'object' — 历史 bug
typeof []        // 'object' — 无法区分数组

[] instanceof Array  // true
// 精确判断类型:
Object.prototype.toString.call([]) // '[object Array]'
深拷贝 vs 浅拷贝?实现深拷贝?
浅拷贝只复制第一层,嵌套对象仍是引用。深拷贝递归复制所有层级,完全独立。

浅拷贝方法:Object.assign()、展开运算符 {...obj}Array.from()

深拷贝方法:

  • structuredClone(obj) — 原生 API,支持循环引用,推荐
  • JSON.parse(JSON.stringify(obj)) — 简单但丢失 undefined/函数/Symbol/Date/循环引用
  • 手写递归 — 面试常考,注意处理循环引用(用 WeakMap)
function deepClone(obj, map = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (map.has(obj)) return map.get(obj);

  const clone = Array.isArray(obj) ? [] : {};
  map.set(obj, clone);
  for (const key of Object.keys(obj)) {
    clone[key] = deepClone(obj[key], map);
  }
  return clone;
}

7 ES6+ 重点

Map / Set / WeakMap / WeakSet 区别?
Map:键值对集合,键可以是任意类型(Object 键只能是字符串/Symbol)。
Set:值不重复的集合,自动去重。
WeakMap/WeakSet:键必须是对象,对键是弱引用,不阻止垃圾回收。不可遍历,无 size。

使用场景:WeakMap 常用于缓存、私有数据存储(避免内存泄漏);Set 用于数组去重 [...new Set(arr)]

Proxy 和 Reflect 是什么?
Proxy 可以拦截对象的基本操作(get/set/delete/has 等),是 Vue 3 响应式的核心。
Reflect 提供与 Proxy handler 一一对应的静态方法,作为 Proxy 中的默认行为转发。
const handler = {
  get(target, key, receiver) {
    console.log(`读取 ${key}`);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    console.log(`设置 ${key} = ${value}`);
    return Reflect.set(target, key, value, receiver);
  }
};
const proxy = new Proxy({}, handler);
解构赋值、可选链、空值合并
解构const { a, b: alias, c = 'default' } = obj 从对象/数组提取值。
可选链 ?.:安全访问深层属性,遇到 null/undefined 短路返回 undefined。
空值合并 ??:左侧为 null/undefined 时取右侧值(区别于 || 会把 0、'' 也当假值)。