this 的绑定规则 - 上 - 你不知道的 JS

  1. this 的默认绑定
  2. this 的隐式绑定
    1. 隐式绑定的丢失 this
  3. this 的显式绑定
    1. 绑定丢失
    2. 硬绑定
    3. API 调用时的上下文
  4. new 绑定 this
  • 箭头函数
  • 函数内 this 既不指向函数自身,也不指向函数的 词法作用域。

    this 实际上在函数被调用时发生绑定,this 指向什么 ** 完全 ** 取决于函数在哪里,怎样被调用。

    this 的默认绑定

    缺省的规则。
    最常用的函数调用类型:** 独立函数调用 **。

    有一个细节:

    function foo() {
        "use strict";
        console.log( this.a );
    }
    var a = 2;
    foo(); // TypeError: this is undefined

    只要函数的定义是严格模式,那么默认调用时,this 会绑定到全局对象上。

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

    上面的函数定义没有在严格模式,但是执行时是在严格模式,结果 this 依旧可以绑定到全局对象上。

    this 的隐式绑定

    函数的调用位置是否有上下文对象,是否被某个对象包含。this 由此来自动隐式地绑定。

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

    虽然函数 foo 不属于对象 obj ,但是 foo 作为属性添加到了 obj 中,但是函数被调用时,对象 obj 就 “拥有了” 函数 foo。
    当函数引用有上下文 ** 对象 ** 时,** 隐式绑定 ** 规则会把函数调用中的 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
    delete obj2.a;
    obj1.obj2.foo(); // undefined
    obj2.__proto__ = obj1;
    obj1.obj2.foo(); // 2

    代码说明三点:

    1. 首先在最接近函数调用的对象中,obj2 中寻找属性
    2. 没找到,则报 undefined
    3. 若存在继承关系,则会向上寻找

    隐式绑定的丢失 this

    1. 被隐式绑定的函数有时会丢失绑定对象,然后就会引用默认绑定,继而将 this 绑定到全局或 undefined。

      function foo() {
       console.log( this.a );
      }
      var obj = {
       a: 2,
       foo: foo
      };
      var bar = obj.foo; // 函数别名!
      var a = "oops, global"; // a 是全局对象的属性
      bar(); // "oops, global"

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

    2. 更常见的隐式绑定丢失发生在参数传递时:

      function foo() {
       console.log( this.a );
      }
      function doFoo(fn) {
       // fn 其实引用的是 foo
       fn(); // <-- 调用位置!
      }
      var obj = {
       a: 2222,
       foo: foo
      };
      var a = "全聚德"; // a 是全局对象的属性
      doFoo( obj.foo ); // "全聚德"

      参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一
      个例子一样。这种情况放在内置函数里也一样。内置函数不会帮你把函数绑定。JavaScript 环境中内置的 setTimeout() 函数实现和下面的伪代码类似:

      function setTimeout(fn,delay) {
       // 等待 delay 毫秒
       fn(); // <-- 调用位置!
      }
    3. 调用回调函数的函数可能会修改 this。在一些流行的 JavaScript 库中事件处理器常会把回调函数的 this 强制绑定到触发事件的 DOM 元素上。这在一些情况下可能很有用,但是有时它可能会让你感到非常郁闷

    this 的显式绑定

    隐式绑定一般是在对象内部包含指向函数的属性,使用时通过属性间接引用函数,从而隐式绑定 this 到对象上。
    显式绑定就是强制把 this 扔给某个函数。
    使用 callapply 函数 。

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

    通过 foo.call(..),我们可以在调用 foo 时强制把它的 this 绑定到 obj 上。
    call 原始值,会自动 ** 装箱 **。原始值会被转换成它的对象形式(也就是 new String(..)、new Boolean(..) 或者 new Number(..))。
    如图

    从 this 的绑定结果来说,call 和 apply 是一样的。区别是传递的参数

    绑定丢失

    显式绑定没有直接解决丢失绑定问题,因为在函数调用时,作为参数传递的 this 可能没法拿到。而且 call 和 apply 是直接执行函数,在作为参数传递的场景中不能使用。
    怎么解决?

    硬绑定

    就是在外面套一个包裹函数。

    思考下面的代码:

    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.call(obj),因此强制把 foo 的 this 绑定到了 obj。无论之后如何调用函数 bar,它总会手动在 obj 上调用 foo。这种绑定是一种显式的强制绑定,因此我们称之为 ** 硬绑定 **。

    常见用法:

    function foo(something) {
        console.log( this.a, something );
        return this.a + something;
    }
    // 简单的辅助绑定函数,返回一个可执行函数
    function bind(fn, obj) {
        return function() {
            // 这里的 arguments 是内层这个匿名返回函数的 arguments
            return fn.apply( obj, arguments );
    };
    }
    var obj = {
        a:2
    };
    var bar = bind( foo, obj );
    var b = bar( 3 ); // 2 3
    console.log( b ); // 5

    由于硬绑定是一种非常常用的模式,所以 ES5 提供了内置的方法 Function.prototype.bind,它的用法就不说了🙄

    API 调用时的上下文

    第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一
    个可选的参数。
    例如,Array.prototype.forEach

    arr.forEach(callback(currentValue [, index [, array]])[, thisArg])

    callback 函数后的 thisArg 就是用来指定 callback 函数执行时的 this 值。

    let s = "淦";
    function fn(e) {
        console.log(e + this)
    }
    [1,2,3].forEach(fn, s);
    // 输出:
    1 淦
    2 淦
    3 淦

    这里不要用箭头函数,因为箭头函数的 this 在定义时已经固定为了外层的 this(这里固定为了全局对象)。

    new 绑定 this

    在 Java 中,构造函数是一个类的特殊方法,使用 new 初始化时会调用类的构造函数。
    在 JavaScript 中,new 的使用方法看起来和 Java 一样。但是,其机制与 Java 这种面向类的语言完全不同。

    JavaScript 中,构造函数只是使用 new 操作符时被调用的函数。他们并不会属于某个类,更不会实例化一个类。因为 new 后面只是一个普通的函数。

    使用 new 调用函数,会 ** 自动 ** 执行下面的操作:

    1. 构造一个全新对象
    2. 新对象会被连接到一个 [[Prototype]]
    3. 新对象绑定到函数调用的 this
    4. 如果函数没有其他返回对象,那么 new 表达式中的函数会自动返回这个新对象

    箭头函数

    ES6 引入的箭头函数并不是使用 function 关键字定义的,而是使用被称为 “胖箭头” 的操作符 => 定
    义的。
    箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this。

    function foo() {
        // 返回一个箭头函数
        return (a) => {
        //this 继承自 foo()
        console.log( this.a );
    };
    }
    var obj1 = {
        a:2
    };
    var obj2 = {
        a:3
    }
    var bar = foo.call( obj1 );
    bar.call( obj2 ); // 2, 不是 3 !

    foo 函数内部的箭头函数会捕获执行 foo() 的 this,foo.call( obj1 ); 执行完后,箭头函数的 this 就指向 obj1,并且永远不变了。就算是 new 也不行。

    实际上,在 ES6 之前我们就已经在使用一种几乎和箭头函数完全一样的模式。

    function foo() {
        let self = this; 
        // let that = this;
        setTimeout( function(){
            console.log( self.a );
        }, 100 );
    }
    var obj = {
        a: 2
    };
    foo.call( obj ); // 2

    虽然 self = this 和箭头函数看起来都可以取代 bind(..),但是从本质上来说,它们想替代的是 this 机制。
    如果你经常编写 this 风格的代码,但是绝大部分时候都会使用 self = this 或者箭头函数来否定 this 机制,那你或许应当:

    1. 只使用词法作用域并完全抛弃错误 this 风格的代码;
    2. 完全采用 this 风格,在必要时使用 bind(..),尽量避免使用 self = this 和箭头函数。

    转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论。
    我的空间