2

关于this

this是JavaScript的一个关键字,自动定义在所有函数中,难点在于this的指向。

this的指向在函数调用时进行绑定,它的context取决于函数调用时的各种条件,与函数定义位置无关

1 this的作用

this可以使不同的context对象重复使用已经存在、声明的函数,无需针对每个对象编写不同版本的函数

function identify() {
    return this.name.toUpperCase();
}
function speak() {
    var greeting = "Hello, i'm " + identify.call( this );    
    console.log( greeting );
}

var me = {name: "Kyle"};
var you = {name: "Reader"};

identify.call( me );         // KYLE
identify.call( you );       // READER

speak.call( me );              // "Hello, i'm KYLE"
speak.call( you );              // "Hello, i'm READER"

2 误解

  • this并不是指向函数本身

  • 在任何情况下,this都不指向函数的词法作用域

3 this是什么?

this是在函数被调用时发生绑定,其指向取决于函数调用的位置。

当一个函数被调用是,会创建一个执行上下文(context)。其中包含函数的调用位置(调用栈)、函数的调用方式和传入的参数等信息。thiscontext中的一个属性

4 this解析

4.1 函数调用位置

函数在程序代码中被调用的位置,清楚调用位置才能明确this的指向

确定函数的调用位置:最关键是分析调用栈(为达到当前指向位置所调用的所有函数)。分析调用栈,可以得出真正的调用位置

function baz() {
    // 当前调用栈是:baz
    // 所以当前调用位置时全局作用域

    console.log('baz');
    bar(); // <-- bar的调用位置
}

function bar() {
    // 当前调用栈是:baz --> bar
    // bar的调用位置在baz中

    console.log('bar');
    foo(); // <-- bar的调用位置
}

function foo() {
    // 当前调用栈是:baz --> bar --> foo
    // foo的调用位置在bar中

    console.log('foo');
}
baz();   //  --> baz的调用位置

4.2 this的绑定规则

在分析清楚调用位置后,根据this绑定的四条规则决定绑定对象。四条规则分别对应四种不同的函数调用方式

总共有四条绑定规则,其优先级是:默认绑定 < 隐式绑定 < 显式绑定 < new绑定

  • 默认绑定:作为独立调用的函数

  • 隐式绑定:作为对象的方法调用的函数

  • 显式绑定(硬绑定):使用call()apply()bind()方法,强制将对象绑定到函数的this

  • new绑定:

4.2.1 默认绑定

默认绑定指将函数作为独立的函数来调用,默认绑定将this绑定到全局对象

分析隐式绑定时,一个对象内部包含一个指向函数的属性,并且通过对象的属性间接引用函数,将this间接绑定到该对象上

function foo() {
    console.log(this.a);
}
var a = 2;
foo();  // 2
  • a在全局作用域中声明,是全局对象的一个属性;

  • foo()使用不带任何修饰的函数进行调用,只能使用默认绑定规则;此时,非严格模式下this指向全局对象,所有this.a被解析为全局变量a

  • 在严格模式中,this不能绑定到全局对象,this只能绑定到undefined

    function foo() {
        'use strict';
        console.log(this.a);
    }
    var a = 2;
    foo();  // TypeError: this is undefined
4.2.2 隐式绑定

判断函数的调用位置是否有上下文对象,隐式绑定将this绑定到调用方法的上下文对象上。

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
obj.foo();  // 2   foo()的调用位置包含上下文对象obj,this隐式绑定到obj对象
  • 对象属性引用链中,只有最后一层会影响调用位置

    function foo() {
        console.log(this.a);
    }
    var obj = {
        a: 2,
        obj2: obj2
    };
    
    var obj2 = {
        a: 42,
        foo: foo
    
    obj.obj2.foo();  // 42   实际是通过obj对象的obj2属性对象来调用foo()函数,this指向obj2
隐式丢失

被隐式绑定的函数会丢失绑定对象,然后应用默认绑定,非严格模式下将this绑定到全局对象。

  • 隐式绑定丢失发生在将隐式绑定的函数赋值给另外的变量,通过改变了来调用函数

    function foo() {
        console.log(this.a);
    }
    var obj = {
        a: 2,
        foo: foo
    };
    var bar = obj.foo;   //  函数别名,传递引用
    var  a = "global a";
    bar();  // "global a",函数的调用位置,bar()其实是一个不带任何修饰的函数调用,所以应用默认的绑定规则
  • 在函数中将回调函数作为参数传入时,参数传递是一种隐式赋值(传递引用),所以应用默认绑定规则

    function foo() {
        console.log(this.a);
    }
    function doFoo(fn) {
        // fn是obj.foo函数本身的一个引用
    
        fn();   // fn的调用位置
    }
    var obj = {
        a: 2,
        foo: foo
    };
    var bar = obj.foo;   //  函数别名,传递引用
    var  a = "global a";
    doFoo(obj.foo);  // "global a",传入的函数被隐式赋值,应用默认绑定规则
    
    setTimeout(obj.foo, 100);   //"global a",使用语言本身的内置函数时道理相同
4.2.3 显式绑定

在JavaScript中,函数是对象,每个函数对象都定义有call()apply()bind()方法,bind()在ES5中。

call()apply()方法:

  • 第一个方法是一个对象,将该对象绑定到this

  • 可以直接指定绑定的对象,称为显示绑定

  • call()apply()区别在于其他参数

    function foo() {
        console.log(this.a);
    }
    var obj = {
        a: 2,
        foo: foo
    };
    foo.call(obj);   // 2  通过foo.call(obj);,在调用foo()时强制将其this绑定到obj对象

显式绑定仍然会有绑定丢失问题:可以使用显示绑定的一个变形来解决这个问题;

硬绑定
  • 创建一个函数bar(),在内部调用foo.call(obj),强制将foothis绑定到obj对象上,无论怎样调用bar()函数,都会手动在obj对象上调用foo,因此foothis指向不会改变

    function foo() {
        console.log(this.a);
    }
    var obj = {
        a: 2,
        foo: foo
    };
    var bar = function() {
        foo.call(obj);   
    }
    
    bar();   //  2
    setTimeout( bar, 100 ); // 2
    // 硬绑定的bar不能再修改它的this指向
    bar.call(window);   // 2
硬绑定的应用场景
  • 创建一个包裹函数,传入所有的参数,并返回接收到的所有值

    function foo(sth) {
        return this.a + sth;
    }
    var obj = {
        a: 2
    };
    var bar = function() {
        // 将arguments传入需要调用的函数
        return foo.apply(obj, arguments);   
    }
    
    bar(3);   //  2 + 3 = 5
  • 创建一个可以重复使用的函数

    function foo(sth) {
        return this.a + sth;
    }
    var obj = {
        a: 2
    };
    // 简单的辅助绑定函数
    function bind(fn, obj) {
        return function() {
            return fn.apply(obj, arguments);
        }
    }
    
    var bar = bind(foo, obj);
    bar(3);   //  2 + 3 = 5
  • 硬绑定是一种非常常见的模式,ES5提供内置Function.prototype.bind方法:返回一个硬绑定的新函数,bind(obj)将参数obj设置为this的上下文,并调用原始函数。

    function foo(sth) {
        return this.a + sth;
    }
    var obj = {
        a: 2
    };
    
    var bar = foo.bind(obj);
    bar(3);   //  2 + 3 = 5
4.2.4 new绑定

JavaScript中的new机制与传统面向对象语言不同。JavaScript中构造函数只是使用new操作符调用的函数,使用new操作符调用函数时:

  1. 创建一个全新对象;

  2. 新对象被执行__proto__链接

  3. 新创建的对象被绑定到函数调用时的this

  4. 如果函数没有返回其他对象,new表达式中的函数调用自动返回新创建的对象

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

4.3 优先级

判断this的指向:找到函数的调用位置,并根据优先级判断应用的规则,默认绑定的优先级最低

  • 显示绑定的优先级高于隐式绑定:在判断时优先考虑显示绑定

    function foo(a) {
        this.a = a;
    }
    
    var obj1 = {
        a: 2;
        foo: foo
    };
    var objb = {
        a: 4;
        foo: foo
    };
    
    obj1.foo();  // 2
    obj2.foo();  // 4
    
    obj1.foo.call(obj2);   // 4
    obj2.foo.call(obj1);   // 2
  • new绑定的优先级高于隐式绑定:

  • new绑定的优先级的高于显示绑定:

    • bar被硬绑定到obj1对象上,但是new bar(3)并未将obj1.a修改为4;

    • new bar(3)修改了调用bar()中的this,得到一个新对象

      function foo(a) {
          this.a = a;
      }
      obj1 = {};
      var bar = foo.bind(obj1);
      bar(2);
      console.log(obj1.a);   // 2
      
      var baz = new bar(4);
      console.log(obj1.a);  // 2
      console.log(baz.a);  // 4

4.4 根据优先级判断函数调用位置应用的规则

  1. 函数是否在new中调用?如果是,this绑定新创建的对象

  2. 函数是否通过call()apply()显示绑定?或者bind()硬绑定?如果是,this指向绑定的对象。

  3. 函数是否在某个上下文对象中调用?如果是,this指向那个上下文对象

  4. 如果不是上述三种情况,使用默认绑定。严格模式下绑定到undefined,非严格模式下绑定到全局对象

4.5 绑定例外

4.5.1 被忽略的this

nullundefined作为this的绑定对象传入call()apply()bind()方法,在调用时被忽略,实际应用默认绑定规则。

使用null来忽略this绑定可能产生副作用:安全的做法是传入一个特殊对象,将this绑定到这个对象不会产生任何副作用。Object.create(null)

5 this词法

ES6中的箭头函数不能使用上述4种规则,而是根据外层(函数或全局)作用域来绝对this。箭头函数常用于回调函数中。

function foo() {
    // 返回一个箭头函数
    return (a) => {
        // this继承自foo()
        console.log(this.a);
    }
}
var obj = {
    a: 2
};

var obj2 = {
    a: 42
};

var bar = foo.call(obj1);  
bar.call(obj2);   // 2, 不是42

Kyxy
316 声望10 粉丝