你不知道的javascript(this)

说明:本系列是本人读完《你不知道的javascript》的总结和感想。有兴趣的读者可以去看原书籍。

一. 关于this

消除对this的误解

在解释this之前,我们需要先消除一些关于this的错误认识。

  • 指向自身

      人们常常容易把this理解指向函数自身,现在我们来看一个例子。

    我们想要记录以下函数foo被调用的次数,思考以下代码:
    function foo(num){
        console.log("foo :" + num);
        //记录foo被调用的次数
        this.count++;
    }
    foo.count = 0;
    var i;
    for(var i = 0;i<10;i++){
        if( i > 5){
            foo(i);
        }
    }
    //foo :6
    //foo :7
    //foo :8
    //foo :9        
    console.log( foo.count );  //0        
    
    console.log语句输出四次,可点foo()函数调用了四次,
    而foo.count仍然为0,说明foo()函数中的this并非指向
    foo函数。
    
    console.log( window.count );  //NAN
    这里其实this引用的是全局,生成一个window对象下的全局
    变量,值为NAN。
    
  • this的作用域

      第二种常见的误解,this指向函数的作用域。这个观点在某种情况下是正确的,但在其他情况下确实错误的。
      ths和词法作用域的查找是无法实现的。
    function foo(){
        var a = 2;
        this.bar();
    }
    function bar(){
        console.log("bar 引用成功。")
        console.log(this.a);
    }
    foo();
    输出结果:
    // bar 引用成功。
    // undefined
    

  这段代码的看起来好像是我们故意写的错误,但其实诠释的是this的误导性。
  《你不知道的javascript》一书中说this.bar(),无法调用bar函数,我们在bar函数中加入console.log(“bar 引用成功”);而实际中会输出“bar 引用成功”,说明bar调用成功。 
  可能的原因是现在的浏览器引擎已经修补了这个错误。 
  this.a 输出为undefined,这里代码尝试将foo和bar的作用与连接,是错误的做法,无法实现。

this到底是什么?

  当一个函数被调用时,会创建一个活动记录(也称为执行上下问)。这个记录会包含函数在哪里被调用(调用栈),函数的调用方法,传入的参数等信息。this就是记录的其中一个属性,会在函数执行的过程中用到

二. this全面解析

绑定规则

  • 默认绑定

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

    我们调用foo()函数,this.a被解析成 全局变量中的a,为什么?因为在这个例子中,函数调用的时候,this指向了全局对象。
    this.a 即 window.a;

    总结来说:
      默认绑定就是没有进行任何修饰的调用,谁调用this绑定谁。

    function foo(){
        "use strict";
    
        console.log(this.a);
    }
    var a = 2;
    foo();  
    //TypeError: Cannot read property 'a' of undefined
    

    在严格模式下,全局对象无法使用默认绑定。所以报错。

  • 隐式绑定

       一个需要考虑的规则是 调用位置是否有上下文对象,或者说某个对象拥有或者包含。
      思考以下代码:
    function foo(){
        console.log(this.a);
    }
    var obj = {
        a:2,
        foo:foo
    }
    obj.foo();  //2
    
    这里会使用obj上下文引用函数,即函数被调用时obj对象“拥有”或者“包含”。当foo()被调用的时候,隐式绑定规则会把函数调用中的this绑定到这个对象。因此foo()的this绑定到obj对象上,因此输入2。
  • 隐式丢失:(隐式绑定的函数会)
    看下面的代码:

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

    这里输出的global,说明bar()调用的时候,this绑定的是全局对象, 这里造成的原因其实是 bar 赋值得到是 foo函数的一个引用。所以bar调用的时候不带任何的修饰,因此应用了默认绑定。

    function foo(){
        console.log(this.a);
    }
    function dofoo(fn){
        fu();
    }
    var obj = {
        a:2,
        foo:foo
    }
    var a = "global"
    dofoo(obj.foo);   //global
    

    这里同样输出的是global,参数传递是一种隐式赋值,所以和上个例子一样。

    除去上面的情况,调用回调函数也可能会修改this。在一些流行的javascript库中,会把回调函数的this强制绑定到触发事件的DOM元素上。所以使用的时候要注意小心。

  • 显式绑定

    call()和apply()方法的第一参数为对象。它们会把这个对象绑定到this,接着在调用函数事指定这个this。我们称之为显式绑定。

可惜显式绑定不可以解决上面说的绑定丢失,但是显式绑定的一个变形(硬绑定)可以解决。

硬绑定
function foo(){
    console.log(this.a);
}
var obj = {
    a:2,
}
var bar = function(){
    foo.call( obj );
}
bar();  //2
setTimeout( bar , 100); //2
//硬绑定的bar 不可能在外部改变它的this了
bar.call(window);

这里我们创建了bar函数,并在它的内部手动调用了foo.call(obj),因此强制把foo的this值绑定到了 obj。无论如何调用函数bar函数,总会手动在obj调用foo。

创建一个辅助绑定函数,参数为要被绑定的函数和对象。  
函数返回被绑定的函数。
function bind(fn,obj){
    return function(){
        return fn.apply(obj,arguments);
    }
}        

由于硬绑定是一种非常常用的模式,所以在ES5中提供内置的方法,Function.prototype.bind,用法如下,

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

bind(…)会返回一个硬编码的新函数,它会把参数设置为this的上下问并调用原始函数。

  • new绑定

      首先声明以下,javascript中new的机制和面向类的语言完全不同。
      在javascript中的“构造函数”,只是一些使用new操作符被调用的函数,它们并不会属于某个类,也不会实例化一个类,只是一个被new操作符调用的普通函数。
      使用new来调用那个函数,或者说发生构造函数调用时,会执行下面的操作:
    1. 创建(或者说构造)一个全新的对象。
    2. 这个新对象会被执行[[原型]]连接。
    3. 这个新函数会绑定到函数被调用的this。
    4. 如果函数没有返回其他对象,那么new表达式的函数调用会自动返回这个新对象。
    

我们会构造一个对象,并把它绑定到 函数 调用中的this上,这就叫new绑定。

  • 优先级

    这里不详细讲解测试优先级的过程,直接说结果,有兴趣的读者可以可尝试,测试需要注意的是
    1. new和call/apply无法一起使用,无法通过new foo.call(obj)来直接测试。
    2.  判断硬绑定函数是否被new调用,是的话使用新this替代硬绑定的this。
    
    **优先级:new绑定 > 显示绑定 > 隐式绑定 > 默认绑定

绑定例外

  • 被忽略的this

      如果 null 或者 undefined 作为this的绑定对象传入 apply,call或者bind,这些值在函数调用的时候被忽略,实际应用的是默认绑定规则。
    function foo(){
        console.log(this.a);
    }
    var a = 2;
    foo.call( null );  //2
    

    一种常见的做法就是使用apply(..)来展开一个数组,类似地,bind(..)可以对参数进行柯里化。

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

function foo(a,b){
    console.log("a: " + a +",b: " + b)
}
// 把数组展开
foo.apply(null,[2,3]);  //a: 2,b: 3
// 使用bind(..)进行柯里化
var bar = foo.bind(null,2);
bar(3);  //a: 2,b: 3

  然而总使用null可能会产生一个副作用,如果某个函数确实使用了this,而默认绑定则会把this绑定到全局对象,这将导致错误。

  更好的做法是传入一个空对象,使用Object.create(null),会创建一个比{}更空的对象。

function foo(a,b){
    console.log("a: " + a +",b: " + b)
}
// 我们的DMZ对象
var nullObj = Object.create(null);
// 把数组展开
foo.apply(nullObj,[2,3]);  //a: 2,b: 3
// 使用bind(..)进行柯里化
var bar = foo.bind(nullObj,2);
bar(3);  //a: 2,b: 3
  • 间接引用

      你可能有意或者无意创建一个函数的“间接引用”,这种情况下,函数的调用应用默认绑定。

    function foo(){
        console.log(this.a);
    }
    var a = 2;
    var o = {
        a:3,
        foo:foo
    };
    var p = {
        a:4
    };
    o.foo();  //3
    (p.foo = o.foo)();  //2
    

    这里(p.foo = o.foo)这个赋值操作返回的是 foo 的引用,应用默认绑定,调用时绑定到全局对象,所以为this.a 为 2.

三.this词法

  前面介绍的四条规则可以包含所有正常的函数。但是 ES6 中出现的一种无法使用这些规则的特殊函数类型:箭头函数。 ==>