Javascript语言中有一个this关键字,后端开发做多了的朋友们习惯称它为”this指针”。其作用是指向调用当前函数的那个对象。听上去很好理解的一个概念,但是对于后端出身的开发人员来说却很头疼,因为它同Java或C++的this指针效果不一样。JS作为脚本语言,太灵活了。本文就来探讨下,这个”this指针”,到底指向哪里。

显式函数调用

先看几个显式函数调用的例子

    name = "name of window";
    function show() {
        var name = "name in function show()";
        alert(this.name);
    }

    show();

上例中我们定义了全局变量name,该变量属于window对象。另外,我们在函数show()的内部定义了局部变量,名字也是name。同时在函数show()的内部打印”this指针”所指向对象的name属性。现在,我们调用show()函数,浏览器会打印什么信息呢?

this的定义中,我们了解到,它是指向调用当前函数的那个对象。现在,show()是在全局被调用,也就是window对象在调用它,那么”this指针”就是指向window对象。以此推论,this.name会打印window对象的name属性,即全局的那个name变量。我们打开浏览器试试,推理没错吧,有种神探夏洛克的赶脚:-)

我们再添加下面的代码

    var myObj = {
        name: "name of myObj",
        show: function() {
            var name = "name in myObj's function show()";
            alert(this.name);
        },
    };

    myObj.show();

现在大家都是推理高手了,一看就知道调用myObj.show()函数的对象是myObj,所以this指向myObjthis.name就是myObj.name。试下,果然如此吧。

回调函数中的this

上面的例子太简单了,我们来点复杂的。如果函数是在回调时被调用,会是什么情况呢?

    <div id="sample">Click Me</div>
    <script type="text/javascript">
    var myObj = {
        name: "name of myObj",
        show: function() {
            var name = "name in myObj's function show()";
            alert(this.name);
        },
    };

    document.getElementById('sample').onclick = myObj.show;
    </script>

还是用上面例子中的myObj,不同的是,我们不直接调用它的show()函数,而且将其绑定到div的点击事件中去。当我们点击页面上”Click Me”字样时,这个函数就会被调用。那这个时候,this会不会还是指向myObj呢?我们试下,奇怪了,弹出了”undefined”。为什么呢?聪明的朋友们马上反应过来,现在调用show()方法的对象主体已经不再是myObj,而是id为sample的这个div的DOM对象。依此类推,this指针应该指向这个DOM对象喽?我们将函数中打印的内容改为alert(this.id)试试,果然,对话框弹出了”sample”,看来this的确指向了id为sample的DOM对象。这也符合this的定义,指向调用当前函数的对象。大家可以另外再试下Ajax回调中this是指向哪里,再想想为什么。

内部函数的this

再来个更复杂的情形,我们知道函数内部还可以定义内部函数,那么在函数里调用内部函数时,this会是什么情况呢?还是利用上面myObj的例子:

    name = "name of window";
    var myObj = {
        name: "name of myObj",
        show: function() {
            var name = "name in myObj's function show()";
            function innerShow() {
                alert(this.name);
            }
            innerShow();
        },
    };
    myObj.show();

这个例子会有点让人混淆,我们有三个name变量,一个是全局的,一个是属于myObj的,还有一个是myObj.show()函数的局部变量。还有个改动就是myOjb.show()函数里面还定义了一个内部函数innerShow(),并同时调用了这个内部函数。现在我们调用myOjb.show()函数,看看会发生什么。奇怪了,为什么全局的name被打印出来了?这下百思不得姐了。网上很多文章说这是JS的设计缺陷。我个人觉得吧,硬是要解释,也说得过去,毕竟这个内部函数不是被myObj对象调用的,我们并没有看到myObj.innerShow()语句。既然没有别的对象在调用它,那只能是window对象了。

如果我们期望在内部函数被调用时,保留其外部函数的上下文呢?那就要用个小技巧:

    name = "name of window";
    var myObj = {
        name: "name of myObj",
        show: function() {
            var name = "name in myObj's function show()";
            var that = this;    // Reserve "this" of myObj.show()
            function innerShow() {
                alert(that.name);    // Replace "this" by "that"
            }
            innerShow();
        },
    };
    myObj.show();

上面的代码就做了两处改动,都用注释标了出来。首先我们将myObj.show()中的this指针赋值给that变量。这是一个函数内的局部变量,函数外不可见,但是对于内部函数是可见的。而这个that就指向myObj.show()的调用者,即myObj。然后在内部函数innerShow()里,我们用that.name即可获得myObj.name

构造函数

接下来讲两个特殊的函数调用情况,首先是构造函数。在JS对象继承和原型链一文中,我们介绍过,构造函数是用new操作符来调用的。当new被使用时,其实JS做了下面几件事情:

  1. 创建一个新对象,类型是object
  2. 将这个新对象的__proto__属性指向构造函数的原型,即设置原型链
  3. 用这个新对象来调用构造函数
  4. 返回该新对象

从上面的第三和第四点中可以了解到,构造函数的调用者即new操作返回的对象,那构造函数里用到的this,也将指向这个返回的对象。看看下面的例子:

    function Show() {
        this.name = "name in constructor Show()";
    }

    newObj = new Show();
    alert(newObj.name);

new Show()操作返回了对象newObj,那构造函数中的this.name就是指newObj.name。打印出来看看,的确如此吧。

使用call/apply/bind来调用函数

call, applybind方法都是Function原型上的成员函数。由于所有的函数对象都继承于Function的原型(感兴趣的朋友们可以打印出上例中Show.__proto__看看),因此所有的函数对象都可以调用call, applybind方法。这些方法有什么用呢?

先来看下callapply,这两个很相似。call方法的用法如下:

    name = "name of window";
    var myObj = {
        name: "name of myObj",
    }

    function show(x, y) {
        alert(this.name);
        return x + y
    }

    show.call(window, 1, 2);
    show.call(myObj, 1, 2);

使用show.call()方法时,第一个参数就是指定调用show(x, y)函数的对象,第二个参数将是show(x, y)函数的第一个参数x,第三个参数则是show(x, y)函数的第二个参数y。换句话说,show.call(window, 1, 2)等同于window.show(1, 2),而show.call(myObj, 1, 2)则等同于myObj.show(1, 2)。不管在对象windowmyObj上有没有定义过show(x, y)函数,都可以成功调用。结论,函数上的call方法,可以用来指定该函数的调用者。这样,函数里的this,即指向调用者指针,就会指向call方法的第一个参数。我们运行下上面的例子看看,是不是这样?

然后说下apply方法,它同call方法的功能完全一样,只不过它把函数的参数放在一个数组中,而不像call方法,将函数的参数从第二个参数开始一一列出来,所以在参数比较多时apply方法看上去更简洁些。我们将上面例子中的call改为apply,代码如下:

    numArray = [1, 2];
    show.apply(window, numArray);
    show.apply(myObj, numArray);

运行下看结果,是不是同call方法完全一样?show函数中this指针指向的是apply方法的第一个参数。

最后聊下bind方法,这是在IE9+, Firefox4+, Safari5.1+, 以及Chrome7+中才支持的,属于ECMAScript5标准。它可以将已有的函数对象复制一份出来,并同时绑定这个新函数对象的调用者。还是看例子吧:

    name = "name of window";
    var myObj = {
        name: "name of myObj",
    }

    function show(x, y) {
        alert(this.name);
        return x + y
    }

    myShow = show.bind(myObj);
    show(1, 2);
    myShow(1, 2);

依然是前面的show(x, y)函数和myObj对象。当调用show(1, 2)时,我们一开始就聊过,这时this指向window对象。然而,如果我们用show.bind(myObj)来创建一个新的函数对象myShow时,myShow的功能将同show()函数一模一样,唯一的区别,就是它的调用者,将被强制绑定为myObj。所以,即使你在全局调用myShow(1, 2),它的调用者依然是myObj,而不是window对象。你可以试下。

bind()方法除了可以绑定调用者之外,还可以绑定参数。比如上面show(x, y)函数有两个参数,我们可以绑定其中第一个参数(也可以两个都绑定)。方法如下:

    myShow2 = show.bind(myObj, 1);
    myShow2(2);

此时,对于myShow2函数,除了调用者被绑定为myObj对象外,其第一个参数也被绑定为整数1。调用myShow2(2)就等同于调用myObj.show(1, 2)

call, applybind方法的官方解释,就是用来改变函数的上下文执行环境”Execution Context”。简而言之,就是改变函数中this指针的指向。callapply会立即执行函数,而bind可以保存起来稍后执行。

说到这里,大家对this指针的指向应该已经很清楚了吧。其实从某种角度说,在后端开发语言中,this指针也是指向调用当前函数的对象的。只不过,在Java这样的语言里,类中的成员函数一定是被该类所实例化的对象来调用的,你没法把一个函数赋值成另一个对象的属性。而Javascript可以,在一个对象中定义的函数可以由任何一个其他对象来调用,所以this指针的指向只有在运行时才能确定。