Javascript this 解析

2016/12/19 javascriptthis

在 Javascript 里,函数被调用的时候,除了接受声明时定义的形式参数,每一个函数还接受两个附加的参数: thisarguments。随着函数使用场合的不同,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

ECMAScript5 引入了 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。

# 参考资料

上次更新: 2022/8/6 16:49:01