你可能遇到过这样的JS面试题:
var obj = {
foo: function(){
console.log(this)
}
}
var bar = obj.foo
obj.foo() // 打印出的 this 是 obj
bar() // 打印出的 this 是 window
请解释最后两行函数的值为什么不一样。初学者关于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 对象
总结
this 就是你 call 一个函数时,传入的 context。如果你的函数调用形式不是 call 形式,请按照「转换代码」将其转换为 call 形式。以后你遇到所有跟 this 有关的笔试题,都不会有疑问了。
PS
- 使用 new 时,情况又不一样,较复杂,会另写文章讲。
- 有人说你怎么不讲 strict mode 呢,strict mode 也会影响 this 呀。我认为 strict mode 只是影响了 context 的默认值而已,你看懂此文稍微看看 strict mode 就懂了。我只讲最重要的内容。
- 有人问怎么不讲 bind 对 this 的影响。那是因为 bind 本质就是自动在你调用一个函数的时候,把 context 换掉而已,你看看 bind 的 polyfill 就知道,其核心依然是 apply(跟 call 差不多)。