外观
JS 踩坑笔记
是什么
这篇只收我实际被坑过的场景:现象、当时为什么错、怎么解。不讲语法——语法是 MDN 的地盘。
检验标准:每个场景都真实发生过。以后再被 JS 坑,回到这里追加。
场景一:循环里的 setTimeout,打印出来全是同一个值
踩的时候:
js
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i))
}
// 期待 0 1 2,实际 3 3 3为什么:var 没有块级作用域,三次循环共用同一个 i;定时器是「到时候才去读 i」,等它们执行时循环早跑完了,i 已经是 3。这叫闭包捕获了变量本身,不是捕获那一刻的值。
解法:把 var 换成 let——let 每轮循环都是一个新变量,三次闭包各抓各的:
js
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i)) // 0 1 2
}场景二:this 又指到别处去了
踩的时候:对象方法里套了一层普通函数(或把方法单独提出来传给别人),this 突然变成 undefined:
js
const player = {
name: '播一段',
play() {
setTimeout(function () {
console.log(`${this.name} 开始播放`) // undefined 开始播放
}, 1000)
}
}为什么:普通函数的 this 由「怎么被调用」决定,不由「写在哪」决定。setTimeout 拿到这个函数后是以普通方式调用的,this 就不再指向 player 了。
解法:回调一律箭头函数——它没有自己的 this,沿作用域往外找,正好找到 play 的 this(即 player):
js
play() {
setTimeout(() => {
console.log(`${this.name} 开始播放`) // 播一段 开始播放
}, 1000)
}记住一条就够
「这个函数里的 this 是谁」=「谁点着它调用的」。答不出「谁」的场景(回调、事件解绑后),用箭头函数。
场景三:Promise.then 比 setTimeout 先执行
踩的时候:
js
setTimeout(() => console.log('A'))
Promise.resolve().then(() => console.log('B'))
console.log('C')
// 输出:C B A——明明 A 先"排队",却最后才跑为什么:任务队列不止一条。setTimeout 进宏任务队,Promise.then 进微任务队;每跑完一个宏任务,必须把微任务队列整个清空才轮到下一个宏任务。C(同步代码)跑完后,微任务 B 先清场,宏任务 A 才上场。
解法:这不是 bug,是规则。用它:需要「DOM 更新完 / 当前同步逻辑跑完之后」再做的事,放 then / await 之后,比 setTimeout(fn, 0) 更早也更可靠。完整规则以后在浏览器与网络的 event-loop 篇展开。
场景四:复制了对象,改副本却把原件改了
踩的时候:
js
const config = { theme: 'dark' }
const copy = config
copy.theme = 'light'
console.log(config.theme) // 'light'——原件被改了!为什么:对象赋值复制的不是内容,是地址——copy 和 config 是同一个房间的两把钥匙。数字、字符串这类原始类型才复制内容。
解法:按层级选拷贝方式:
js
const shallow = { ...config } // 浅拷贝:只有一层是新的
const deep = structuredClone(config) // 深拷贝:里里外外全新(现代首选)坑在「浅」字上:{ ...config } 之后改 copy.nested.x 照样会动到原件——嵌套的那层还是地址。
场景五:for...in 突然多遍历出了不属于它的东西
踩的时候:给某处代码加了原型方法之后,另一处 for...in 循环行为变了——循环里混进了那个方法名。
为什么:for...in 走的是整条原型链,自己身上没有的属性,顺着原型找到的也会被遍历出来(包括别人在原型上加的方法)。
解法:只关心「对象自己身上的键」时,永远用:
js
Object.keys(obj) // 自己的可枚举键,不碰原型链顺带修正一个观念:对象身上那些「没用声明却存在」的方法(obj.toString()……),全都是从原型链上一层层借来的。
追加纪律
被坑的当下记「现象 + 报错原文」,解决后当场回来补「为什么 + 解法」。写不出来的场景不收——说明还没真懂,懂了再写。