Javascript this 解析

在 Javascript 里,函数被调用的时候,除了接受声明时定义的形式参数,每一个函数还接受两个附加的参数: this 和 arguments。随着函数使用场合的不同,this 的值会发生变化。但是有一个总的原则,那就是 this 指的是,调用函数的那个对象。

下面分四种情况,具体讨论 this 的用法:

  1. 对象方法调用模式

    1
    2
    3
    4
    5
    6
    7
    8
    window.name = 'window';
    var object = {
    name: 'object',
    run: function() {
    console.log(this.name);
    }
    };
    object.run(); // 'object'

    分析:当一个函数被保存为对象的一个属性时,我们称它为一个方法。当方法被调用时(通过 . 表达式或 object[fun] 下标表达式),this 绑定到该对象

  2. 函数调用模式

    当一个函数并非一个对象的属性时,那么它就是被当做一个函数来调用的,以此模式调用函数时,this 被绑定到全局对象(ECMAScript6 的箭头函数(注意只是箭头函数)基本纠正了这个设计):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    window.name = 'window';
    var object = {
    name: 'object',
    run: function() {
    innerFun();
    function innerFun() {
    console.log(this.name);
    }
    return function () {
    console.log(this.name);
    }
    }
    };
    object.run()();
    // =>
    // 'window'
    // 'window'

    分析:从上面可以看出,不管外部环境的 this 是不是 window,通过函数调用模式调用的函数,this 指向 window.

    来看看 ES6 的 this:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    window.name = 'window';
    var object = {
    name: 'object',
    run: function() {
    return () => {
    console.log(this.name);
    }
    }
    };
    var o = { name: 'test_o' };
    object.run()(); // 'object'
    object.run().apply(o); // 'object'

    如果这么写:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    window.name = 'window';
    var object = {
    name: 'object',
    run: () => {
    console.log(this.name);
    }
    };
    var o = { name: 'test_o' };
    object.run(); // 'window'
    object.run.apply(o); // 'window'

    分析:箭头函数和普通函数之间有一个重要的差别 - 箭头函数没有自己的 this 值,其 this 值是继承外域的 this 值。所以箭头函数不仅仅是从外观上简化了函数的写法,更解决了普通函数中 this 的 hack 问题。

  3. 构造函数调用模式

    当一个函数被作为一个构造函数来使用(使用new关键字),它的 this 与即将被创建的新对象绑定。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function C(){
    this.a = 37;
    }
    var o = new C();
    console.log(o.a); // 37
    function C2(){
    this.a = 37;
    return { a: 38 };
    }
    o = new C2();
    console.log(o.a); // 38

    注意:当构造器返回的默认值是一个 this 引用的对象时,可以手动设置返回其他的对象,如果返回值不是一个对象,返回 this。

  4. call, apply, bind 调用

    call, apply 方法的用途都是在特定的作用域中调用函数。apply 方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组。其中,第二个参数可以是 Array 的实例,也可以是 arguments 对象。call 方法与 apply 方法的作用相同,它们的区别仅在于接收参数的方式不同。对于 call 方法而言,第一个参数是 this 值没有变化,变化的是其余参数都直接传递给函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var a = 1;
    function test() {
    console.log(this.a);
    }
    var o = {};
    o.a = 2;
    o.fun = test;
    o.fun.apply(o); // 1
    o.fun.call(o); // 1

    ECMAScript 5 引入了 Function.prototype.bind。调用f.bind(someObject)会创建一个与f具有相同函数体和作用域的函数,但是在这个新函数中,this将永久地被绑定到了bind的第一个参数,无论这个函数是如何被调用的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function f(){
    return this.a;
    }
    var g = f.bind({a:"azerty"});
    console.log(g()); // azerty
    var o = { a: 37, f: f, g: g };
    console.log(o.f(), o.g()); // 37, azerty

this 容易误用的情况

  1. 内联式绑定 Dom 元素的事件处理函数

    1
    2
    3
    4
    5
    6
    7
    <script type="text/javascript">
    function hello() {
    console.log(this.tagName);
    }
    </script>
    <input id="btnTest" type="button" value="点击我" onclick="hello()">

    运行结果是 undefined, 此时的 this 指针并不是指向 input 元素,使用内联式绑定 Dom 元素的事件处理函数时,实际 上相当于执行了以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <script type="text/javascript">
    function hello() {
    console.log(this.tagName);
    }
    document.getElementById("btnTest").onclick = function () {
    hello();
    };
    </script>
    <input id="btnTest" type="button" value="点击我">

    这种情况下 hello 函数对象的所有权并没有发生转移,还是属于 window 对象所有。因为还是 window 对象在调用 hello 方法

    解决方案:把 this 作为参数传入函数,或者直接把方法作为对象的一个属性,这样方法中的 this 直接指定的是当前对象

    1
    2
    3
    4
    5
    6
    7
    <script type="text/javascript">
    function hello(el) {
    console.log(el.tagName);
    }
    </script>
    <input id="btnTest" type="button" value="点击我" onclick="hello(this)">

    或:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <script type="text/javascript">
    function hello() {
    console.log(this.tagName);
    }
    document.getElementById("btnTest").onclick = hello;
    </script>
    <input id="btnTest" type="button" value="点击我">
  2. 临时变量导致的 this 指针丢失

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    window.name = 'window';
    var object = {
    name: 'object',
    run: function() {
    console.log(this.name);
    }
    };
    function runTest() {
    var tmpRun = object.run;
    tmpRun();
    }
    runTest(); // 'window'

    此时 object.run 被赋值给了临时变量 tmpRun, 而临时变量是属于 window 对象的(只不过外界不能直接引用,只对 Javascript 引擎可见),于是在 run 函数内部的 this 指针指向的就是 window 对象了。

    解决方案:

    • 不引入临时变量,每次使用均使用 object.run() 进行调用

    • run 函数内部使用 object.name 显式引用 name 属性,而不通过 this 指针隐式引用

    • 使用 apply/call 函数指定 this 指针

  3. 函数传参导致 this 丢失

    1
    2
    3
    4
    5
    6
    7
    8
    9
    window.name = 'window';
    var object = {
    name: 'object',
    run: function() {
    console.log(this.name);
    }
    };
    setTimeout(object.run, 100); // 'window'

其实这个问题和上一个示例中的问题是类似的,都是因为临时变量而导致的问题。当我 们执行函数的时候,如果函数带有参数,那么这个时候 Javascript 引擎会创建一个临时变量, 并将传入的参数复制(注意,Javascript 里面都是值传递的,没有引用传递的概念)给此临时变量。也就是说,整个过程就跟上面我们定义了一个 tmpRun 的临时变量,再将 object.run 赋值给这个临时变量一样。只不过在这个示例中,容易忽视临时变量导致的 bug。

参考资料: