DaraW

Code is Poetry

《你不知道的JS上卷》阅读小记之setTimeout的this指向问题

这几天翻看了下被传的神乎其神的《你不知道的JS》这本书,其实以前就看过一次,不过当时的level并不高,而且感觉这本书讲的有点绕,所以看了一点就没坚持下去。
这次翻看感觉还是比较轻松的,有些地方写的很好,有的地方还是感觉讲的有点绕(可能是翻译的问题),但总的来说这本书还是很不错的,基本都是JS中有坑、新手难以理解的点,简直就是《JS:The Bad Parts》(哈,开个玩笑~)。
这个小记不是打算记录书中内容的笔记,而是想补充纠正书中的讲的不完善的地方。

this的问题

在第二部分2.2.2隐式绑定一节中,提到了setTimeout的传入函数this的问题,书里说传入回调函数在执行的时候context为全局对象,所以this指向了全局对象:

1
2
3
setTimeout(function() {
console.log(this)
}, 200)

在Chrome56中测试上面的一段代码确实输出为window全局对象,符合书中的描述,然而如果你在Node.js中测试这段代码你会发现输出是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
➜  Desktop  node -v
v6.8.1
➜ Desktop node test.js
Timeout {
_called: true,
_idleTimeout: 200,
_idlePrev: null,
_idleNext: null,
_idleStart: 94,
_onTimeout: [Function],
_timerArgs: undefined,
_repeat: null }

What? 输出的是一个Timeout实例对象!
打开node的源码,在node\timers.js中有着setTimeout的实现,这里大概的讲一下,有兴趣的可以自己再去看看代码:
有一个Timeout构造函数,用来构造定时器对象,用一个链表存着所有的Timeout的实例对象,也就是每次执行暴露出来的setTimeout都会在链表中插入一个Timeout实例timer。下面是Timeout构造函数的代码:

1
2
3
4
5
6
7
8
9
10
function Timeout(after, callback, args) {
this._called = false;
this._idleTimeout = after;
this._idlePrev = this;
this._idleNext = this;
this._idleStart = null;
this._onTimeout = callback;
this._timerArgs = args;
this._repeat = null;
}

定时的部分TimerWrap则由C++来做处理,这里不是现在我们关注的关键点也脱离了JS的范畴暂且不细谈,在定时结束后,通过ontimeout函数来处理timer对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function ontimeout(timer) {
var args = timer._timerArgs;
var callback = timer._onTimeout;
if (!args)
callback.call(timer);
else {
switch (args.length) {
case 1:
callback.call(timer, args[0]);
break;
case 2:
callback.call(timer, args[0], args[1]);
break;
case 3:
callback.call(timer, args[0], args[1], args[2]);
break;
default:
callback.apply(timer, args);
}
}
if (timer._repeat)
rearm(timer);
}

ontimeout函数中正藏着this指向问题的真相:callback.call(timer, ...)

在JS中,setTimeout应该是属于Event LoopMacro Tasks,与I/OTasks同等级,在浏览器中有Web APIs规范来定义这部分的实现,node没有(或者是我没找到,还请告知)。但我大概翻了下Web APIs的规范,也没有找到对this context 的规定。虽然不理解为什么node这样做,但是好歹也找出了与Chrome浏览器不同的原因。

所以,关于setTimeout传入函数的this,我的建议是即使你写的代码只会在浏览器里运行,也最好不要依赖this会自动绑定到全局对象上去,而是应该手动借助bind绑定。当然使用ES6的箭头函数是没什么问题的,因为没有创建新的contextthis都会毫无疑问的绑定在当前的context上:

1
2
3
4
5
let that = this

setTimeout(() => {
console.log(this == that) // true
}, 200)

Proudly powered by Hexo and Theme by Hacker
© 2022 DaraW