JavaScript 面试的核心不是背 API,而是把“代码为什么这样执行”讲清楚。高频追问通常围绕执行上下文、闭包、this、原型链、异步调度和手写实现展开。

执行机制

数据类型与类型转换

知识点讲解

JS 有 7 种原始类型:stringnumberbigintbooleanundefinedsymbolnull,以及引用类型 objecttypeof null 返回 object 是历史遗留问题,不代表 null 是对象。

类型转换重点看三类:转布尔、转数字、转字符串。对象参与运算时会先走 ToPrimitive,优先调用 valueOftoString,不同运算符的偏好不同。

面试常问

问:===== 的区别?

答:=== 不做隐式类型转换,类型和值都相同才返回 true。== 会做隐式转换,规则复杂,容易出现 [] == false'' == 0 这类反直觉结果。工程里默认用 ===,只有明确需要兼容宽松输入时才考虑 ==

作用域、变量提升与执行上下文

知识点讲解

执行上下文包含变量环境、词法环境、作用域链和 this 绑定。函数执行前会先创建上下文,所以 var 声明会提升并初始化为 undefinedletconst 也会提升,但在初始化前处于暂时性死区。

面试常问

问:为什么 let 也存在提升,却不能在声明前访问?

答:因为 let 在词法环境里已经创建了绑定,但没有初始化。声明前访问会触发暂时性死区,抛出 ReferenceError

函数与对象模型

闭包

知识点讲解

闭包是函数和它能访问的外层词法环境的组合。它让函数在外层函数执行结束后,仍然可以访问外层变量。常见场景包括私有变量、函数柯里化、防抖节流、模块封装。

面试常问

问:闭包一定会造成内存泄漏吗?

答:不会。闭包只是延长了变量生命周期。只有当不再需要的引用被长期持有,导致垃圾回收无法释放,才会形成泄漏。工程上要注意移除事件监听、清理定时器、释放缓存引用。

this 绑定

知识点讲解

this 在普通函数里由调用方式决定:默认绑定、隐式绑定、显式绑定、new 绑定。箭头函数没有自己的 this,它捕获定义时外层作用域的 this

面试常问

问:箭头函数为什么不能作为构造函数?

答:箭头函数没有自己的 this,也没有 prototype,不能通过 new 创建实例,所以不能作为构造函数。

原型链与继承

知识点讲解

每个对象都有内部原型 [[Prototype]],可以通过 Object.getPrototypeOf 访问。函数有 prototype 属性,构造函数创建实例时,实例的原型会指向构造函数的 prototype。访问属性时,先查对象自身,再沿原型链向上查找。

面试常问

问:__proto__prototype 区别?

答:prototype 是函数对象上的属性,用来给实例设置原型;__proto__ 是对象访问其内部原型的历史访问器。面试回答里最好用 Object.getPrototypeOf 代替 __proto__,更规范。

异步与 ES6+

Event Loop

知识点讲解

浏览器事件循环里,同步代码先执行;遇到异步任务后,回调进入任务队列。每轮宏任务结束后,会清空当前所有微任务,然后再进入渲染和下一轮宏任务。常见微任务有 Promise.thenqueueMicrotask;宏任务有 setTimeoutsetInterval、I/O、UI 事件。

面试常问

问:Promise 和 setTimeout 谁先执行?

答:同步代码结束后,微任务先于下一轮宏任务执行。所以同一轮里 Promise.then 通常早于 setTimeout(..., 0)

Promise 与 async/await

知识点讲解

Promise 有 pending、fulfilled、rejected 三种状态,状态一旦改变不可逆。async/await 是 Promise 的语法糖,await 后面的代码会被放入微任务继续执行。

面试常问

问:try/catch 能捕获 Promise 错误吗?

答:能捕获 await 的 Promise rejection,也能捕获同步错误;但如果只是创建 Promise 而没有 awaitreturn,外层 try/catch 捕获不到。

高频手写题

防抖与节流

知识点讲解

防抖是“等用户停下来再执行”,适合搜索联想、表单校验。节流是“固定时间内最多执行一次”,适合滚动、拖拽、窗口 resize。

面试常问

问:防抖和节流怎么选?

答:如果关心最后一次结果,用防抖;如果关心过程中的稳定频率,用节流。

call、apply、bind

知识点讲解

callapply 都是立即执行并改变 this,区别是参数传递方式。bind 返回一个新函数,可以预置 this 和部分参数。手写时要注意临时挂载函数、Symbol 避免属性冲突、构造函数调用时 this 绑定规则。

面试常问

问:手写 bind 最容易漏什么?

答:容易漏 new 调用场景。被 bind 的函数如果作为构造函数调用,this 应该指向新实例,而不是绑定对象。

深拷贝

知识点讲解

简单对象可以用递归复制,但真实深拷贝要处理循环引用、Date、RegExp、Map、Set、函数、Symbol、原型等。面试里可以先写基础版本,再说明工程里优先用成熟库或 structuredClone

面试常问

问:JSON.parse(JSON.stringify(obj)) 有什么问题?

答:会丢失 undefined、函数、Symbol,不能处理循环引用,Date 会变字符串,RegExp 会变空对象,也会丢失原型信息。

底层追问与代码示例

执行上下文与闭包到底保存了什么

知识点讲解

闭包保存的不是某个变量的“值拷贝”,而是对词法环境中变量绑定的引用。外层函数执行结束后,只要内部函数仍然可达,相关词法环境就不会被 GC 回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function createCounter() {
let count = 0

return {
inc() {
count += 1
return count
},
get() {
return count
}
}
}

const counter = createCounter()
counter.inc() // 1
counter.inc() // 2
counter.get() // 2

面试中可以强调:count 不在全局,也不在返回对象上,而是在 createCounter 的词法环境里;incget 都引用了同一个绑定。

面试常问

问:循环里使用 var 为什么会拿到同一个 i?

答:var 是函数作用域,循环不会为每次迭代创建新的词法环境,所以所有回调共享同一个 ilet 是块级作用域,并且在循环每次迭代会创建新的绑定。

1
2
3
4
5
6
7
8
9
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}
// 3 3 3

for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 0)
}
// 0 1 2

手写 Promise:核心状态机

知识点讲解

Promise 的底层重点是状态不可逆、回调队列、then 返回新 Promise、异步调度。面试不一定要求写完整 A+,但要能写出主干。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class MyPromise {
constructor(executor) {
this.state = 'pending'
this.value = undefined
this.reason = undefined
this.onFulfilled = []
this.onRejected = []

const resolve = value => {
if (this.state !== 'pending') return
queueMicrotask(() => {
if (this.state !== 'pending') return
this.state = 'fulfilled'
this.value = value
this.onFulfilled.forEach(fn => fn(value))
})
}

const reject = reason => {
if (this.state !== 'pending') return
queueMicrotask(() => {
if (this.state !== 'pending') return
this.state = 'rejected'
this.reason = reason
this.onRejected.forEach(fn => fn(reason))
})
}

try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}

then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }

return new MyPromise((resolve, reject) => {
const fulfilledTask = value => {
try {
resolve(onFulfilled(value))
} catch (error) {
reject(error)
}
}

const rejectedTask = reason => {
try {
resolve(onRejected(reason))
} catch (error) {
reject(error)
}
}

if (this.state === 'fulfilled') queueMicrotask(() => fulfilledTask(this.value))
else if (this.state === 'rejected') queueMicrotask(() => rejectedTask(this.reason))
else {
this.onFulfilled.push(fulfilledTask)
this.onRejected.push(rejectedTask)
}
})
}
}

这版还没有完整处理“then 回调返回 Promise 并递归解析”的情况。专业回答要主动说明:完整 Promise Resolution Procedure 要处理 thenable、循环引用、返回自身等边界。

手写 bind:兼容 new 调用

知识点讲解

bind 的难点不是改变 this,而是处理预置参数、原型链和构造调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Function.prototype.myBind = function (context, ...presetArgs) {
const targetFn = this

function boundFn(...laterArgs) {
const isNewCall = this instanceof boundFn
const thisArg = isNewCall ? this : context
return targetFn.apply(thisArg, [...presetArgs, ...laterArgs])
}

if (targetFn.prototype) {
boundFn.prototype = Object.create(targetFn.prototype)
boundFn.prototype.constructor = boundFn
}

return boundFn
}

function User(name, age) {
this.name = name
this.age = age
}

const BoundUser = User.myBind({ name: 'ignored' }, 'Ada')
const user = new BoundUser(18)
console.log(user.name, user.age) // Ada 18

面试常问

问:为什么 new boundFn() 时不能使用绑定的 context?

答:因为构造调用的优先级高于显式绑定。new 会创建新对象并把 this 指向新实例,如果仍然使用绑定对象,就破坏了构造函数语义。

Event Loop 经典输出题

知识点讲解

输出题要先分同步、微任务、宏任务,不要凭感觉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
console.log('script start')

setTimeout(() => {
console.log('setTimeout')
}, 0)

Promise.resolve()
.then(() => {
console.log('promise1')
return Promise.resolve()
})
.then(() => {
console.log('promise2')
})

queueMicrotask(() => {
console.log('queueMicrotask')
})

console.log('script end')

输出顺序:

1
2
3
4
5
6
script start
script end
promise1
queueMicrotask
promise2
setTimeout

解释:同步代码先执行;第一轮微任务队列里先有 promise1,再有 queueMicrotaskpromise1 执行后追加后续 then,排到已有微任务之后。