Javascript this 解析

2016/12/19 javascriptthis

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

  1. 对象方法调用模式

    window.name = 'window';
    var object = {
        name: 'object',
        run: function() {
            console.log(this.name);
        }
    };
    object.run();   // 'object'
    

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

  2. 函数调用模式

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

    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:

    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'
    

    如果这么写:

    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 与即将被创建的新对象绑定。

    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 值没有变化,变化的是其余参数都直接传递给函数

    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的第一个参数,无论这个函数是如何被调用的。

    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 元素的事件处理函数

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

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

    <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 直接指定的是当前对象

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

    或:

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

    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 丢失

    window.name = 'window';
    var object = {
        name: 'object',
        run: function() {
            console.log(this.name);
        }
    };
    
    setTimeout(object.run, 100);    // 'window'
    

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

# 参考资料

上次更新: 2021/8/7 上午3:33:06