Skip to content
本页目录

浅谈“this”(上)

在JavaScript中,我们对this肯定并不陌生,在函数中,我们经常用this.foo来访问对象属性,但是在一些复杂的函数逻辑中,我们需要搞清楚的一个问题是this到底指向谁?在近期的学习回顾中,我且谈一谈自己对this的理解。

调用位置

在理解this的绑定过程之前,首先我们要理解的一个概念是调用位置调用位置就是函数在代码中被调用的位置(这里特别要强调的一点是:调用位置并不是声明位置),只有仔细分析调用位置才能搞清楚this到底指向的(或者说引用的)是什么?

通常来说,要寻找调用位置就是寻找“函数被调用的位置”,这听起来很简单,只需要看函数在哪被调用,但是做起来却并没有那么容易,因为有些编程模式可能会隐藏真正的调用位置,所以最重要的是分析调用栈

那么什么是调用栈?调用栈就是指为了到达当前执行位置所调用的所有函数,这听起来可能会有些绕口,用通俗的话来讲,函数要被调用首先得进栈,若一个调用栈中包含A,B,C三个函数,且他们的调用顺序为A->B->C,而我们关心的调用位置则是当前正在执行的函数的上一个调用,讲到这里可能就会很明白了,C的调用位置是B,而B的调用位置就是A,那A的调用位置呢?如果A处于调用栈的顶端,则A的调用位置为全局作用域。

默认绑定

在理解了调用位置之后,我们就可以来谈谈this的绑定规则了。

思考以下代码:

js
function baz(){
    console.log(this.a);
}
var a = 2;
baz(); //2

这里我们通过 var a =2 声明了一个全局变量a,但我们调用 baz 时,this.a 被解析成为了全局变量 a,那就是说我们这里的 this 指向的是全局作用域,为什么? 这就是我们要谈的第一点规则,默认绑定。在这里的调用栈即为 baz ,所以 this 被默认绑定到了全局对象上,因此 this 指向的就是全局对象。

但是这里有一个非常重要的细节,只有函数运行在非严格模式的情况下, this 才能默认绑定到全局对象,在严格模式下 this 是不允许绑定到全局对象上的,如果你要问我什么是严格模式,上百度Google一下。

js
'use strict'
function baz(){
    console.log(this.a);
}
var a = 2;
baz(); //TypeError: this is undefined

在node环境中,在开启strict模式的ES6语法下,上述代码也会是undefined,因为ES6禁止将 this 指向全局对象。

隐式绑定

这是我个人认为使用的最多的一条规则,即判断调用位置是否有上下文。

思考以下代码

js
function foo(){
    console.log(this.a);
}
var obj = {
    a : 2,
    foo : foo
};
obj.foo(); //2

首先要声明的一点是,不管 foo 是如何定义在对象obj中的,这个函数严格来说都不属于obj对象。然而,调用位置会使用obj上下文来引用函数,因此为了方便理解,可以说成函数被调用时obj对象“包含”或者“拥有”它。但如果我直接调用 foo() 而不是使用 obj.foo(),结果会怎么样呢?那就和我们上文所说的默认绑定一样,在非严格模式下,this 会绑定到全局对象,而全局对象上并没有属性 a ,则结果为 undefined 。

一种更微妙的情况发生在传入回调函数时:

js
function foo(){
    console.log(this.a);
}
function doFoo(fn){
    fn();
}
var obj = {
    a:2,
    foo:foo
}
var a = " i am global";
doFoo(obj.foo);

思考一下,这里打印的a将会是什么? 是 obj对象属性a = 2 还是全局对象属性 a = “ i am global”? 我可以很负责任的告诉你,打印出来的将会是” i am global”。

为什么?在调用foo的时候不是有obj上下文吗?为什么 this 不会指向obj对象?其实仔细分析一下函数的调用位置就会知道,函数是在doFoo里的 fn() 处被调用的,obj.foo只是一个参数传递,相当于 var bar = obj.foo; doFoo(bar);这里的bar只是函数的一个别名,实际上他引用的还是foo本身,和直接调用foo()是一个原理,因此 this 默认绑定在了全局对象上,称之为隐式丢失。

显式绑定

分析完上面两种绑定方式,你可能会想:能不能不要创建函数对象,把 this 绑定到我想要绑定的函数或者对象上去呢? 答案是可以的,JavaScript提供了call()和apply()方法帮助你实现更自由的 this 绑定。

这两个方法是如何工作的呢? 他们的第一个参数是一个对象,他们会把这个对象绑定到 this ,接着在调用函数时指定这个 this 。文字说明可能不太好懂,来看一下代码。

js
function foo(){
    console.log(this.a);
}
var obj = {
    a : 2
};
foo.call(obj); //2

通过foo.call()可以在调用foo时强制把它的this绑定到obj上。但是如果传入的值是一个字符串或者布尔类型的值,那么它将会被转换成他的对象形式,也就是 new String()、new Boolean()。

典型的应用场景就是创建一个包裹函数,传入所有的参数并返回接收到的所有值。

js
funtion foo(something){
    console.log(this.a,something);
    return this.a + something;
}
var obj = {
    a : 2
};
var bar = function(){
    return foo.apply(obj,arguments);
};
var b = bar(3);// 2 3
console.log(b);// 5

这是显式绑定的一种变种方式称之为硬绑定,他可以解决上文所提到的隐式丢失问题,防止函数调用 this 被绑定到全局对象上,但是这种绑定方式未免显得太过僵硬,大大降低了函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改 this 。那如果我需要动态修改 this 的指向呢? 也不是没有方法,思考一下,写了一上午有点饿,下篇博客再讲。

总结

判断 this 的指向,最重要的是判断函数的调用位置,可以说,函数的调用位置决定了 this 指向的对象。

Released under the MIT License.