JavaScript 面试八股文:闭包、this、原型链
把 JS 最容易问的三个难点(闭包引用、this 绑定、原型链查找)用图解讲清楚,附带面试 30 秒速答模板与常见陷阱。
JavaScript 面试八股文:闭包、this、原型链
JS 面试里出现频率最高的三个概念:闭包、this、原型链。
很多人背过标准答案但讲不清楚原理。今天咱们用实际代码把这三个讲透,别光记书上的概念。
1. 闭包:活的外层作用域绑定
常见错误理解
// 你会说"闭包可以访问外层变量"
// 但真正的闭包是什么?
function outer() {
let count = 0
return function inner() {
count++
return count
}
}
const fn = outer()
fn() // 1
fn() // 2
fn() // 3
闭包不是"保存了一份变量",而是"持有对外层作用域的活的绑定"。
经典坑:循环中的闭包
人最容易踩的闭包坑就在 for 循环里。 const fns = for (var i = 0; i < 3; i++) { fns.push(() => console.log(i)) } fns.forEach(fn => fn()) // 输出:3, 3, 3
// 为什么?因为 var 是函数作用域,i 被所有 fn 共享
// 解法 1:用 let(块级作用域) for (let i = 0; i < 3; i++) { fns.push(() => console.log(i)) } // 输出:0, 1, 2
// 解法 2:立即执行函数创建新作用域 for (var i = 0; i < 3; i++) { fns.push((() => { const j = i return () => console.log(j) })()) } // 输出:0, 1, 2
### 面试常见延伸题
**题 1.1:内存泄漏怎么发生?**
```ts
// 常见泄漏:闭包保留了大对象
function createClosure() {
const hugeData = new Array(1000000).fill('...')
return {
getData: () => hugeData[0] // hugeData 永不释放
}
}
回答点:闭包会延长引用对象的生命期,所以要及时"断开"不需要的引用。
题 1.2:WeakMap vs Map?
闭包漏掉大对象可以用 WeakMap 让垃圾回收及时处理。
const cache = new WeakMap()
function fn(obj) {
if (cache.has(obj)) return cache.get(obj)
const result = expensive(obj)
cache.set(obj, result) // 对象被 GC 时自动释放
return result
}
面试速答模板
"闭包是函数访问外层作用域的能力。Loop 中的闭包常见坑:用 let 隔离或 IIFE 包装。内存要注意别让闭包无限期地保活大对象——WeakMap 是个可选方案。"
2. this:不是"对象",而是"调用上下文"
四种 this 绑定方式
// 1. 默认绑定(全局)
function fn() {
console.log(this)
}
fn() // 严格模式下是 undefined,非严格是 window/global
// 2. 隐式绑定(对象方法)
const obj = { name: '小明', fn }
obj.fn() // this === obj
// 3. 显式绑定(call/apply/bind)
const obj1 = { name: 'obj1' }
const obj2 = { name: 'obj2' }
fn.call(obj1) // this === obj1
fn.apply(obj2) // this === obj2
const boundFn = fn.bind(obj1)
boundFn() // this === obj1
// 4. new 绑定
function Fn() {
this.name = '新对象'
}
const instance = new Fn() // this === 新创建的对象
this 优先级
new > call/apply/bind > 对象方法 > 全局
箭头函数没有 this(继承外层)
const obj = {
name: '小明',
fn: () => {
console.log(this) // undefined(箭头函数无 this)
},
fn2() {
return () => {
console.log(this) // 是 obj(继承 fn2 的 this)
}
}
}
// 这也是为什么 React 组件方法要用箭头或 bind
class Button {
handleClick = () => {
console.log(this) // 总是 Button 实例
}
// vs
handleClick2() {
console.log(this) // 需要 bind 或直接在 JSX 用 () => this.handleClick2()
}
}
面试常见延伸题
题 2.1:手写 call/apply/bind?
Function.prototype.myCall = function(ctx, ...args) {
ctx = ctx || globalThis
ctx._fn = this
const result = ctx._fn(...args)
delete ctx._fn
return result
}
Function.prototype.myBind = function(ctx, ...args1) {
// 这题还要考虑 new 时不改 this
return (...args2) => this.call(ctx, ...args1, ...args2)
}
题 2.2:new 干了什么?
function myNew(Constructor, ...args) {
const obj = Object.create(Constructor.prototype)
const result = Constructor.apply(obj, args)
return result instanceof Object ? result : obj
}
面试速答模板
"this 是 runtime 绑定,优先级:new > call/bind > 对象方法 > 全局。箭头函数无 this,继承外层。手写 call/bind 考查闭包理解,new 考查原型链和 this 变更。"
3. 原型链:像物种分类,子继承父的特征
三个概念区分
// 1. prototype:函数的原型对象
function Person(name) {
this.name = name
}
Person常见延伸题
**题 3.1:手写 instanceof?**
```ts
function myInstanceof(obj, Constructor) {
let proto = Object.getPrototypeOf(obj)
while (proto) {
if (proto === Constructor.prototype) return true
proto = Object.getPrototypeOf(proto)
}
return false
}
// 注意:null 是特殊的,typeof null === 'object' 但 Object.getPrototypeOf(null) 报错
题 3.2:Class 和 Constructor Function 的区别?
// 本质上 class 只是 constructor 的语法糖
// 但有约束:
// 1. class 必须 new 调用(不能直接调用)
// 2. class 默认严格模式
// 3. class 方法不可枚举
const obj = {}
Object.getOwnPropertyNames(obj.constructor.prototype) // ['constructor']
// 而老 function 写法的 prototype 方法是枚举的
题 3.3:原型链污染怎么防?
// 恶意代码可能修改 Object.prototype
Object.prototype.isAdmin = true
const user = {}
console.log(user.isAdmin) // true!!! 灾难
// 防守:用 Object.create(null) 或 Object.hasOwnProperty 检查
const safeObj = Object.create(null)
console.log(safeObj.isAdmin) // undefined
const user2 = {}
console.log(user2.hasOwnProperty('isAdmin')) // false (自身不含)
面试速答模板
"原型链是查找链,沿 proto 向上。instanceof 就是查 prototype 是否在原型链上。Class 是 Constructor 的语法糖但多了约束。要小心原型污染,可用 hasOwnProperty 局部检查或 Object.create(null) 隔离
// 2. proto:对象指向构造函数的 prototype const p1 = new Person('小明') console.log(p1.proto === Person.prototype) // true
// 3. constructor:指向构造函数 console.log(p1.constructor === Person) // true
// 原型链查找:沿着 proto 一级级向上 // p1.greet 查找路径: // p1 -> p1.proto(Person.prototype) -> Person.prototype.greet
### 原型链的图例
p1 (实例对象) ↓ proto Person.prototype ↓ proto Object.prototype ↓ proto null
### 常见坑:修改 prototype
```ts
// 错的做法
Person.prototype = { greet() {} }
// 此时 p1.__proto__ 还是指向旧 prototype!
// 对的做法
Person.prototype.greet = function() {}
// 这样所有实例都能访问
面试 30 秒速答
"原型链是 JS 对象继承的机制。每个对象有 proto 指向构造函数的 prototype,属性查找时沿原型链向上查找。instanceof 就是基于原型链判断。"
总结:JS 基础的三驾马车
- 闭包:对外层作用域的活的绑定
- this:调用时的上下文
- 原型链:对象属性继承的查找路径
搞清这三点,JS 基础就不成问题了。