前端面试准备 01:JavaScript 运行时——执行上下文、闭包、原型链与事件循环(能推导版)
把 JS 面试高频点组织成“可推导”的模型:执行上下文/作用域链/this/原型链/事件循环。附典型追问、手写题拆解与易错边界。
- 前端面试
- JavaScript
- 运行时
- 异步
这篇只做一件事:把 JS 高频题从“背答案”变成“能推导”。
你要建立的不是零散知识点,而是一套“运行时模型”:
- 执行:代码如何进入调用栈、如何创建执行上下文
- 绑定:this 如何确定、原型链如何查找属性
- 调度:异步任务如何排队、何时执行(事件循环)
当你能画出这三张图(栈 / 原型链 / 队列),大多数变形题都不怕。
01 执行上下文:你要能画出栈#
面试问“闭包是什么/this 指向是什么”,本质都离不开执行上下文。
你至少要讲清:
- JS 执行时维护一个调用栈(Call Stack)
- 每次函数调用会创建一个执行上下文:变量环境/词法环境/this 绑定
- 作用域解析遵循词法作用域(代码写在哪,就在哪找变量)
高频追问#
- var/let/const 的差异为何会导致 TDZ?
- 函数声明提升 vs 函数表达式提升的本质差别?
02 闭包:不是“函数套函数”,是“携带了外部环境”#
闭包的面试表达建议:
闭包是“函数 + 它创建时的词法环境”的组合。函数返回后,其外部变量仍可能被引用而不被回收。
你要能讲清两个点:
- 为什么外部变量不会销毁:因为仍被引用(可达性)
- 代价是什么:更长的生命周期,可能引发内存占用与泄漏
实战落地#
- 订阅/事件监听务必在卸载时解除
- 定时器/异步回调引用大对象要小心
03 this:把绑定规则背成一张表#
你不需要讲玄学,只要把规则说清:
- 默认绑定:严格模式下为 undefined,否则为全局对象
- 隐式绑定:obj.fn() -> this = obj(但会被“引用丢失”打断)
- 显式绑定:call/apply/bind
- new 绑定:构造调用优先级最高
易错点:
- 箭头函数没有自己的 this,捕获定义时外层 this
04 原型链:属性查找与继承的真实路径#
必会表达:
- 每个对象都有
[[Prototype]](可通过Object.getPrototypeOf观察) - 函数有
prototype属性,用于 new 时设置实例的原型 - 属性查找沿原型链向上,直到 null
高频追问:
instanceof原理是什么?- 如何实现
Object.create?
05 事件循环:宏任务/微任务不是背分类,是理解“何时入队”#
你要能讲清:
- 执行栈清空后,取一个宏任务执行
- 宏任务执行过程中产生的微任务会在本轮宏任务结束后全部清空
- DOM 渲染/绘制时机与任务队列之间的关系(不同浏览器实现细节可简述)
建议你能手写并解释:
- Promise.then 的回调为什么比 setTimeout 更早执行
- async/await 如何拆成 Promise 链
06 自测清单(60 秒版本)#
- 我能画出调用栈解释闭包产生与释放吗?
- 我能用绑定规则解释 3 个 this 变形题吗?
- 我能说明原型链查找过程与
instanceof原理吗? - 我能口述事件循环执行顺序并解释原因吗?
下一篇我们进入 TypeScript:把“会写 TS”升级为“会用类型系统提升工程质量”。
07 深入:把 4 个核心模型讲清(面试可用表述)#
A. 调用栈(Call Stack)#
- JS 单线程执行:同一时刻只有一个栈顶函数在跑
- 栈溢出通常来自递归或同步死循环
B. 作用域与词法环境(Lexical Environment)#
- 作用域由“代码位置”决定,不由“调用位置”决定(词法作用域)
- 闭包是函数携带词法环境导致外部变量延长生命周期
C. 原型链(Prototype Chain)#
- “查属性”走原型链,“改属性”通常在当前对象上新增/覆盖
Object.create(null)没有原型链(没有toString等)
D. 任务队列(Task Queues)#
- 宏任务:脚本、定时器、I/O、事件回调(分类会因环境不同略有差异)
- 微任务:Promise reaction、queueMicrotask(优先级更高)
08 笔试题(选择 / 问答 / 输出)#
题 1(输出题):变量提升与 TDZ#
下面代码输出什么?为什么?
console.log(a);
var a = 1;
try {
console.log(b);
} catch (e) {
console.log('err');
}
let b = 2;参考答案(原理)#
最终输出:
- 第一段:
undefined - 第二段:
err
下面按“创建阶段(声明处理)→ 执行阶段”推导。
第一段为什么是 undefined
代码:
console.log(a);
var a = 1;推导步骤:
- 进入脚本/函数作用域时会有“创建阶段”。
var a会被加入到当前执行上下文的 Variable Environment。 var的语义是:声明提升 + 立即初始化为undefined(所以声明前可读)。- 执行到
console.log(a)时,a已存在且值为undefined,因此输出undefined。 - 下一行
a = 1才发生赋值,改变a的值。
第二段为什么会抛错并被 catch 到
代码:
try {
console.log(b);
} catch (e) {
console.log('err');
}
let b = 2;推导步骤:
let b同样会在创建阶段被登记到当前执行上下文的 Lexical Environment。- 但
let/const的语义是:声明会提升,但不会初始化。在执行到声明语句前,变量处于 TDZ(暂时性死区),不可读。 - 执行到
console.log(b)时读取 TDZ 变量,会抛出ReferenceError。 - 错误被
catch捕获,输出err。 - 执行到
let b = 2时,b才完成初始化并变为可读。
易错点与加分点
- TDZ 不是“没声明”,而是“已声明但不可访问”。
const同样有 TDZ,且必须初始化,否则是语法错误。- 在 ES Module 顶层,
var不会挂到全局对象上,但“提升 + 初始化为 undefined”的行为仍成立。
题 2(输出题):this 绑定与引用丢失#
const obj = {
x: 1,
f() {
return this.x;
},
};
const g = obj.f;
console.log(obj.f(), g());参考答案(原理)#
这题考点是:this 不是由“函数定义位置/归属对象”决定,而是由**调用点(call-site)**决定。
代码:
const obj = {
x: 1,
f() {
return this.x;
},
};
const g = obj.f;
console.log(obj.f(), g());结论
obj.f():输出1。g():- 严格模式/模块模式:抛
TypeError(this === undefined,读取this.x报错) - 非严格模式:通常输出
undefined(this === globalThis,但全局一般没有x)
- 严格模式/模块模式:抛
推导(逐步)
obj.f()
- 调用点是“点调用”(
obj.f())。 - 隐式绑定规则:点左侧对象作为 this。
- 所以
this === obj,this.x === 1。
g()
const g = obj.f只是把函数值赋给变量g,并不会携带 obj。- 调用点变成普通函数调用(
g())。 - 普通调用下:
- 严格模式:
this === undefined - 非严格模式:
this === globalThis
- 严格模式:
常见追问(加分)
- 如何修复引用丢失:
obj.f.bind(obj)或() => obj.f() - 为什么箭头函数不适合写需要动态 this 的方法:箭头函数 this 固定为定义时外层 this。
面试表达要点:隐式绑定需要“调用点”有宿主对象,不是“函数来自哪个对象”。
题 3(输出题):事件循环#
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);参考答案(原理)#
代码:
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);结论
输出顺序:1 4 3 2
推导(事件循环)
把整段脚本当作一次“宏任务”执行:
- 同步执行阶段(仍在当前宏任务内)
- 输出 1
setTimeout注册回调:进入“未来的宏任务队列”(到期后才可执行)Promise.then回调:进入微任务队列- 输出 4
- 当前宏任务结束后:清空微任务队列
- 执行 then 回调,输出 3
- 进入下一轮宏任务
- 执行定时器回调,输出 2
易错点与边界
setTimeout(fn, 0)并不保证“0ms 后立刻执行”,它只是“尽快排到下一轮宏任务”,还会受最小延迟与浏览器节流影响。- Node.js 事件循环阶段更细,但 Promise microtask 依旧会在进入下一轮宏任务前被清空。
09 编码题(手写实现 + 推导)#
题 4:实现 myBind#
实现一个简化版的 bind,支持:
- 绑定
this - 预置参数(partial application)
- 使用
new调用时,this绑定应被忽略(new优先级更高)
Function.prototype.myBind = function (thisArg, ...preset) {
// TODO
};参考答案(原理 + 代码)#
目标:实现简化版 bind,同时正确处理两类调用:
- 直接调用:
bound()→this应该是thisArg - 构造调用:
new bound()→this应该是新实例,忽略thisArg
原理拆解(为什么要判断 instanceof)
new bound() 会先创建新对象,然后以新对象作为 this 调用 bound。因此在 bound 里可用 this instanceof bound 判断是否构造调用,从而选择正确的 this。
原型链处理(面试常追问)
为了让 new bound() 创建的实例能够访问 target.prototype 上的成员,需要让:
bound.prototype的原型指向target.prototype
Function.prototype.myBind = function (thisArg, ...preset) {
const target = this;
function bound(...args) {
const isNew = this instanceof bound;
const ctx = isNew ? this : thisArg;
return target.apply(ctx, [...preset, ...args]);
}
// 让 new bound() 的实例能够沿原型链访问 target.prototype
if (target.prototype) {
bound.prototype = Object.create(target.prototype);
Object.defineProperty(bound.prototype, 'constructor', {
value: bound,
writable: true,
configurable: true,
});
}
return bound;
};题 5:实现 myInstanceof#
function myInstanceof(obj, Ctor) {
// TODO
}参考答案(原理 + 代码)#
原理:obj instanceof Ctor 等价于判断 Ctor.prototype 是否出现在 obj 的原型链上(沿 [[Prototype]] 向上查找)。
function myInstanceof(obj, Ctor) {
if (obj == null) return false;
if (typeof Ctor !== 'function') {
throw new TypeError('Right-hand side of instanceof is not callable');
}
const proto = Ctor.prototype;
let p = Object.getPrototypeOf(obj);
while (p !== null) {
if (p === proto) return true;
p = Object.getPrototypeOf(p);
}
return false;
}加分边界
- 规范里还支持
Ctor[Symbol.hasInstance]自定义instanceof行为;面试可补一句“我这里实现的是默认原型链语义”。
10 面试官常见追问(你要准备的边界)#
- Promise 链式调用的错误如何传播?(throw/return Promise.reject)
- 为什么
async函数返回值会被包成 Promise? Object.create(null)用于什么场景?(字典/Map 替代,避免原型污染)
11 本篇小结(你应该掌握什么)#
- 能画出调用栈解释闭包与 TDZ
- 能用“调用点”解释 this
- 能用“原型链包含关系”解释 instanceof
- 能用“本轮宏任务结束清空微任务”解释事件循环
相关阅读
基于内容相似度 + 标签/系列加权
- 5 分钟前端面试准备(8 年一线工程师知识体系与学习路线)
前端面试准备 00:知识体系地图(8 年工程师版)与复习节奏
给你一张可执行的“面试知识体系地图”:从 JS/浏览器/网络/React/工程化/性能/安全/架构到项目与行为面。附 2/4/8 周复习节奏与每一块的验收标准。
- 前端面试
- 学习路线
- 方法论
- 3 分钟前端面试准备(8 年一线工程师知识体系与学习路线)
前端面试准备 02:TypeScript 工程能力——类型系统、泛型、条件类型与“类型体操”的边界
TS 面试别只讲 keyof/extends:要能说清类型系统能力边界、如何在团队里落地、如何避免类型体操失控。附高频类型题与工程规范。
- 前端面试
- TypeScript
- 工程化
- 2 分钟前端面试准备(8 年一线工程师知识体系与学习路线)
前端面试准备 03:浏览器与网络——渲染流水线、缓存、CORS、Cookie、HTTP/2(高频追问一网打尽)
把浏览器与网络题整理成“从输入到像素”的链路:渲染流水线、回流重绘、合成、缓存策略、CORS、Cookie/SameSite、HTTP/2/3。
- 前端面试
- 浏览器
- 网络
- 2 分钟前端面试准备(8 年一线工程师知识体系与学习路线)
前端面试准备 04:React 核心——渲染、更新、Hooks 心智模型、并发与 SSR(面试表达模板)
用“渲染=计算 UI”与“更新=调度”两句话讲清 React:Hooks 规则、闭包陷阱、性能优化点、并发渲染与 SSR/Streaming 的取舍。
- 前端面试
- React
- Hooks