JavaScript里this 的值到底是什么?一次说清楚
你可能遇到过这样的 JS 面试题:
var obj = {
foo: function(){
console.log(this)
}
}
var bar = obj.foo
obj.foo() // 打印出的 this 是 obj
bar() // 打印出的 this 是 window
请解释最后两行函数的值为什么不一样。
初学者关于 this 的理解一直很模糊。今天这篇文章就要一次讲清楚了。
而且这个解释,你在别的地方看不到。看懂这篇文章,所有关于 this 的面试题,都是小菜。
有用请点赞。
函数调用
首先需要从函数的调用开始讲起。
JS(ES5)里面有三种函数调用形式:
func(p1, p2)
obj.child.method(p1, p2)
func.call(context, p1, p2) // 先不讲 apply
一般,初学者都知道前两种形式,而且认为前两种形式「优于」第三种形式。
从看到这篇文章起,你一定要记住,第三种调用形式,才是正常调用形式:
func.call(context, p1, p2)
其他两种都是语法糖,可以等价地变为 call 形式:
func(p1, p2) 等价于
func.call(undefined, p1, p2)
obj.child.method(p1, p2) 等价于
obj.child.method.call(obj.child, p1, p2)
请记下来。(我们称此代码为「转换代码」,方便下文引用)
至此我们的函数调用只有一种形式:
func.call(context, p1, p2)
这样,this 就好解释了
this,就是上面代码中的 context。就这么简单。
this 是你 call 一个函数时传的 context,由于你从来不用 call 形式的函数调用,所以你一直不知道。
先看 func(p1, p2) 中的 this 如何确定:
当你写下面代码时
function func(){
console.log(this)
}
func()
用「转换代码」把它转化一下,得到
function func(){
console.log(this)
}
func.call(undefined) // 可以简写为 func.call()
按理说打印出来的 this 应该就是 undefined 了吧,但是浏览器里有一条规则:
如果你传的 context 是 null 或 undefined,那么 window 对象就是默认的 context(严格模式下默认 context 是 undefined)
因此上面的打印结果是 window。
如果你希望这里的 this 不是 window,很简单:
func.call(obj) // 那么里面的 this 就是 obj 对象了
再看 obj.child.method(p1, p2) 的 this 如何确定
var obj = {
foo: function(){
console.log(this)
}
}
obj.foo()
按照「转换代码」,我们将 obj.foo() 转换为
obj.foo.call(obj)
好了,this 就是 obj。搞定。
回到题目:
var obj = {
foo: function(){
console.log(this)
}
}
var bar = obj.foo
obj.foo() // 转换为 obj.foo.call(obj),this 就是 obj
bar()
// 转换为 bar.call()
// 由于没有传 context
// 所以 this 就是 undefined
// 最后浏览器给你一个默认的 this —— window 对象
[ ] 语法
function fn (){ console.log(this) }
var arr = [fn, fn2]
arr[0]() // 这里面的 this 又是什么呢?
我们可以把 arr0 想象为arr.0( ),虽然后者的语法错了,但是形式与转换代码里的 obj.child.method(p1, p2) 对应上了,于是就可以愉快的转换了:
arr[0]()
假想为 arr.0()
然后转换为 arr.0.call(arr)
那么里面的 this 就是 arr 了 :)
箭头函数
我不明白为什么需要讨论箭头函数,实际上箭头函数里并没有 this,如果你在箭头函数里看到 this,你直接把它当作箭头函数外面的 this 即可。外面的 this 是什么,箭头函数里面的 this 就还是什么,因为箭头函数本身不支持 this。
有人说「箭头函数里面的 this 指向箭头函数外面的 this」,这很傻,因为箭头函数内外 this 就是同一个东西,并不存在什么指向不指向。
总结
-
this 就是你 call 一个函数时,传入的第一个参数。(请务必背下来「this 就是 call 的第一个参数」)
-
如果你的函数调用形式不是 call 形式,请按照「转换代码」将其转换为 call 形式。
以后你遇到所有跟 this 有关的笔试题,都不会有疑问了。
上面把 this 的来龙去脉说清楚了,可以搞定面试遇到的所有相关题目。
不过实际项目中 this 的使用事情可能要更晦涩一点,我们就举例来说说。
Event Handler 中的 this
btn.addEventListener('click' ,function handler(){
console.log(this) // 请问这里的 this 是什么
})
handler 中的 this 是什么呀,看了你的文章我还是不懂啊?
那是因为你没有看懂,我们说过 this 都是由 call 或 apply 指定的,那么你只需要找到 handler 被调用时的代码就行了。
可是我哪知道 addEventListener 的源代码呀
是呀,addEventListener 是浏览器内置的方法,我们看不见源代码。
所以,你只能看文档了,MDN 这样说:
通常来说this的值是触发事件的元素的引用,这种特性在多个相似的元素使用同一个通用事件监听器时非常让人满意。
当使用 addEventListener() 为一个元素注册事件的时候,句柄里的 this 值是该元素的引用。其与传递给句柄的 event 参数的 currentTarget 属性的值一样。
由于浏览器知道你不方便看源码里是怎么 call handler 的,所以直接在文档里告诉你了,你可以假想浏览器的源码是这样写的:
// 当事件被触发时
handler.call(event.currentTarget, event)
// 那么 this 是什么不言而喻
jQuery Event Handler 中的 this
那么下面代码中的 this 是什么呢:
$ul.on('click', 'li' , function(){
console.log(this)
})
同样,不要瞎猜,你要么看 jQuery 源码是怎么 call 这个函数的,要么看jQuery 文档。
jQuery 文档是这样写的:
当jQuery的调用处理程序时,this关键字指向的是当前正在执行事件的元素。对于直接事件而言,this 代表绑定事件的元素。<u style="text-decoration: none; border-bottom: 1px dashed grey;">对于代理事件而言,****this 则代表了与 selector**** 相匹配的元素。</u>(注意,如果事件是从后代元素冒泡上来的话,那么 this 就有可能不等于 event.target。)若要使用 jQuery 的相关方法,可以根据当前元素创建一个 jQuery 对象,即使用 $(this)。
总结一下如何确定 this 是值
-
看源码中对应的函数是怎么被 call 的(这是最靠谱的办法)
-
看文档
-
console.log(this)
-
不要瞎猜,你猜不到的
如何强制指定 this 的值?
自己写 call / apply 即可:
function handlerWrapper(event){
function handler(){
console.log(this) // 请问这里的 this 是什么
}
handler.call({name:'饥人谷'}, event)
}
btn.addEventListener('click', handlerWrapper)
你也可以直接使用 bind,与上面的代码效果差不多:
function handler(){
console.log(this) // 请问这里的 this 是什么
}
var handlerWrapper = handler.bind({name:'饥人谷'})
btn.addEventListener('click', handlerWrapper)
上面三句代码可以挤成一句:
btn.addEventListener('click', function(){
console.log(this) // 请问这里的 this 是什么
}.bind({name:'饥人谷'}))
这是前端里一个常见的套路。
this 不难,就看你有没有按照 call / apply 来理解函数了。
完。
P.S.
-
使用 new 时,情况又不一样,可以看另一篇文章《JS 的 new 到底是干什么的?》。
-
后续篇《你怎么还没搞懂 this? - 知乎专栏》
-
有人说你怎么不讲 strict mode 呢,strict mode 也会影响 this 呀。我认为 strict mode 只是影响了 context 的默认值而已,你看懂此文稍微看看 strict mode 就懂了。我只讲最重要的内容。
-
有人问怎么不讲 bind 对 this 的影响。那是因为 bind 本质就是自动在你调用一个函数的时候,把 context 换掉而已,你看看 bind 的 polyfill 就知道,其核心依然是 apply(跟 call 差不多)。
-
有人问箭头函数,箭头函数其实跟 this 没关系,所以如果你在箭头函数里面看到 this ,就当作是它外面的函数的 this 即可。