JavaScript Tips: JavaScript 中的 t
this
的引用问题一直是 JavaScript 新人比较头疼的问题。前段时间阅读了方应航老师关于this
的文章,加深了对this
的理解。同时,在实际项目中遇到了一些文中没有提到的关于this
的用法,特此整理一下。
勘误:之前发布的文章,将
call
误写成了apply
。call
和apply
的效果是一样的,第一个参数都接收的是函数的context
。只不过apply
将函数的参数变成了数组进行传递而已。
上下文环境( context )
简单来讲,上下文环境指的是当前代码片段(函数)运行时所处的环境。
在 JavaScript 中,每一个函数在执行的时候都会被赋予一个 context
,即函数运行的上下文环境(执行上下文),这个环境通常是一个对象。在函数中,我们使用 this
访问函数的执行上下文。这个上下文环境随着函数的调用方式、形式、位置等的不同会发生变化,因此我们无法直接依赖函数声明时的上下文环境来进行某些操作。这其中最典型的就是 setTimeout
这样的异步函数。
举几个简单的例子,来观察一下函数的执行上下文:
-
这是一个定义在全局环境的函数
foo
,我们在全局环境中调用它:
得到了全局对象
Window
,蛮合理的。(当然这么理解是不完全正确的,慢慢往下看) -
我们定义一个
obj
对象,其中的foo
属性指向刚才定义的foo
函数:
此时虽然
obj
中的foo
直接指向了全局foo
函数,但是其执行结果却变成了obj
对象。 -
我们反过来再试一下:
结果也反过来了。
更难受的是setTimeout
这样的方法:
由此证明,函数的执行上下文与函数声明时的上下文不一定相同。所以我们有的时候会看到这样的写法,用来保存函数依赖的上下文环境:
为什么会有这种差别呢?
在 JavaScript 中,“万物皆对象”。每一个 function
其实是由 Function
类生成的一个对象。在执行函数调用时,其实是执行了一个语法糖,真正被调用的是函数的 call
内置方法。这个方法接收两种参数:call(context, [arg1, [arg2..)
。context
便是这个函数执行的上下文,即 this
。来做一个有点暴力的实验:
我们尝试强制指定 foo
的 context
为 obj2
,结果显然 foo
的 this
被绑定为了 obj2
。
而 JavaScript 又是如何执行这个语法糖的呢?我们肯定会这么猜:JavaScript 会自动向前调用这个函数的的对象,并将这个对象作为 context
再执行 call
。这样说并没有错,但是不全面,来看下边几个实验:
-
先创建一个
father
对象:
显然这个是符合我们猜想的。
-
然后我们再创建一个
child
对象:
显然也符合我们的猜想。
-
现在我们把两个对象结合起来:
想必和一些人猜想的不一样吧。
JavaScript 只会寻找最终调用该函数的对象,而不会向前追溯。
不过还有一个问题,为什么直接执行函数的时候,会输出 window
这个对象。是因为在浏览器中所有的对象都是 window
的属性,所以 foo()
等价于 window.foo()
吗?答案是否定的。
在 JavaScript 中,如果函数是直接调用的,而不是源自于某个对象,函数的 call
方法的 context
将会被定义成 undefined
。所以 foo()
与 foo.call(undefined)
是完全等价的:
在浏览器策略中,函数 context
如果为 undefined
,将会自动绑定全局对象 window
。这种绑定在 JavaScript 严格模式下会被禁止。
按照规矩来也不行?
有些写在函数里的函数(或者说,闭包),会丢失原函数的上下文。其实也不怪它,因为函数的执行上下文是不会继承的:
如果你理解了刚才对 call
的解读,你也许就会认为:inner
并没有被任何对象调用,而是直接被执行了,自然会丢失上下文。这样的理解在这个例子中是正确的,但是当函数作为回调时会复杂一些。
回调函数的调用方式与回调函数的执行者有关,其 this
与执行者执行函数时为其指定的 context
有关。没有指定 context
的结果与上边的结果是一致的,但指定了 context
的就不一定了,要仔细阅读文档。
关于 bind
很多时候,由于执行者的不可靠性,或者其他的原因,我们想为函数手动绑定 context
。JavaScript 为我们提供了 bind
方法,返回一个绑定了上下文的函数,来改写一下上边出现的 setTimeout
的例子:
特殊语法:[]
function fn () {
console.log(this)
}
var arr = [fn]
arr[0]()
这样的函数调用,调用对象是数组 arr
本身,所以它将被作为 context
传入:
箭头函数
ES6 为了解决 this binding 这个让人非常头疼的问题,提供了一种新的函数声明方式:箭头函数。箭头函数会自动绑定函数声明时所在的上下文的 this
。关于箭头函数具体的信息可以查阅箭头函数 | MDN
我们可以用箭头函数改写上面出现的 setTimeout
的实验:
总结
函数的 this
最核心的地方就是掌握函数的 call
方法和函数 call
的 context
的推导规则。还有就是注意回调函数和闭包的 this
,因为他们的执行可能并没有经过对象调用,所以很可能丢失 context
,或者指向了别的 context
。