Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript this指向总结 #9

Open
FridaS opened this issue Mar 9, 2018 · 0 comments
Open

JavaScript this指向总结 #9

FridaS opened this issue Mar 9, 2018 · 0 comments

Comments

@FridaS
Copy link
Owner

FridaS commented Mar 9, 2018

JavaScript this指向总结

this是在运行时绑定的、而不是在定义时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

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

绑定规则

判断this的指向,我们需要先找到函数的调用位置,然后判断应用下面四条规则中的哪一条:

  1. 默认绑定
    function foo () {
        console.log(this.a);
    }
    var a = 2;
    foo(); // 2

foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定规则、this指向全局对象。

注意:如果foo在严格模式下,默认绑定将绑定到undefined、而不是全局对象。(只要foo不在严格模式下,此处this就可以利用默认规则绑定到全局对象;只有foo是严格模式时、此处this绑定到undefined)

  1. 隐式绑定
    function foo () {
        console.log(this.a);
    }
    var obj = {
        a: 2,
        foo: foo
    };
    obj.foo(); // 2

调用位置会使用obj上下文来引用函数,因此可以说函数被调用时obj对象“拥有”或者“包含”foo函数(实际上foo不属于obj对象,它只是被当做引用属性添加到obj中)。

当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为调用foo()时this被绑定到obj,因此this.a和obj.a是一样的。

对象属性引用链中,只有最后一个调用者(直接调用者)会影响调用位置:

    function foo () {
        console.log(this.a);
    }
    var obj2 = {
        a: 42,
        foo: foo
    };
    var obj1 = {
        a: 2,
        obj2: obj2
    };
    obj1.obj2.foo(); // 42

隐式丢失

有时隐式绑定的函数会丢失绑定对象、改用默认绑定:

    function foo () {
        console.log(this.a);
    }
    var obj = {
        a: 2,
        foo: foo
    };
    var bar = obj.foo; // 函数别名
    var a = 1;
    bar(); // 1

虽然bar是obj.foo的一个引用,但实际上它引用的是foo函数本身,因此此时的bar()是一个不带任何修饰的函数调用,因此应用了默认绑定规则。

传入回调函数时也会隐式丢失绑定对象:

    function foo () {
        console.log(this.a);
    }
    function doFoo (fn) {
        // fn其实引用的是foo
        fn(); //调用位置
    }
    var obj = {
        a: 2,
        foo: foo
    };
    var a = 1;
    doFoo(obj.foo); // 1
    setTimeout(obj.foo, 100); // 1

JavaScript环境中内置的setTimeout()函数实现和下面的代码类似:

    function setTimeout (fn, delay) {
        // 等待delay毫秒
        fn(); // 调用位置
    }

所以回调函数(如setTimeout里的回调函数)丢失this绑定是非常常见的。

  1. 显示绑定
    利用call()和apply()方法:
    function foo () {
        console.log(this.a);
    }
    var obj = {
        a: 2
    };
    foo.call(obj); // 2

通过foo.call(obj);,可以在调用foo时强制把它的this绑定到obj上。

如果你传入一个原始值(字符串类型,布尔类型或者数字类型)来当做this的绑定对象,这个原始值会被转换成它的对象形式(也就是new String(),new Boolean()或者new Number()),这通常被称为“装箱”。

但是显示绑定仍无法解决我们之前遇到的丢失绑定的问题,有2个方法可以解决:

  • 硬绑定
    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); // 2

bar函数内强制把foo的this绑定到了obj,无论之后如何调用函数bar,它总会在obj上调用foo。这种绑定是一种显式绑定,因此我们称之为硬绑定。

ES5种提供的Function.prototype.bind也是一种硬绑定。

Function.prototype.bind = function (obj) {
    if (typeof this !== "function") {
        throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
    }
    var args = Array.prototype.slice.call(arguments, 1), // 截取参数
          self = this, // 保存对象上下文
          F = function () {},
          fBound = function () {
              return self.apply(
                  this instanceof F && obj // 1
                  ? this
                  : obj || window,
                  args.concat(Array.prototype.slice.call(arguments)); // 将bind方法的参数和fBound方法的参数合并
              );
          }
    F.prototype = this.prototype;
    fBound.prototype = new F(); // 2
    return fBound;
};

注意:绑定函数作为构造函数时,其this需继承自原函数,并且需要继承原函数的原型链方法,上述代码中的1和2处就是做这两件事。

  • API调用的”上下文“
    第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为”上下文“(context),其作用和bind一样,确保你的回调函数使用指定的this。
      function foo (el) {
          console.log(el, this.id);
      }
      var obj = {
          id: 'awesome'
      };
      // 调用foo时把this绑定到obj
      [1,2,3].forEach(foo, obj); // 1 awesome 2 awesome 3 awesome

这些函数实际上就是通过call或者apply实现了显示绑定。

  1. new绑定
    使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:
    1. 创建(或者说构造)一个全新的对象;
    2. 这个新对象会被执行[[prototype]]连接;
    3. 这个新对象会绑定到函数调用的this;
    4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
       function foo (a) {
           this.a = a;
       }
       var bar = new foo(2);
       bar(); // 2 

绑定规则的优先级

判断函数在某个位置应用的是哪条规则,可以按照下面的顺序来进行判断:

  1. 函数是否在new中调用(new绑定)?如果是,this绑定的是新创建的对象;
  2. 函数是否通过call、apply(显示绑定)或者硬绑定(显示绑定的变种)调用?如果是,this绑定的是指定的对象;
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是,this绑定的是哪个上下文对象;
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,绑定到undefined;否则绑定到全局对象。

但是,有几个例外情况不适用上述4个规则:

  1. 把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则:
    function foo () {
        console.log(this.a);
    }
    var a=2;
    foo.call(null); // 2
  1. 间接引用
    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()而不是p.foo()或o.foo(),所以这里用了默认绑定。

  1. 软绑定
    一些封装函数,以实现可以给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显示绑定修改this的能力。

this词法

上述的4条规则适用于普通正常函数,但是ES6给出了一种无法使用这些规则的特殊函数类型:箭头函数。箭头函数的this是根据外层(函数或者全局)作用域来决定的。

箭头函数的绑定无法被修改(new也不行),它本质上是用this的词法形式(根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的this绑定,这其实和我们常写的self = this;机制一样)替代this机制(取决于调用位置和条件)。

参考:《你不知道的JavaScript(上)》

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant