关于javascript:this-之谜揭底从浅入深理解-JavaScript-中的-this-关键字二

35次阅读

共计 7105 个字符,预计需要花费 18 分钟才能阅读完成。

前言

系列首发于公众号『前端进阶圈』,若不想错过更多精彩内容,请“星标”一下,敬请关注公众号最新消息。

this 之谜揭底:从浅入深了解 JavaScript 中的 this 关键字(二)

调用地位

  • 在了解 this 的绑定过程之前,首先要了解 调用地位 调用地位就是函数在代码中被调用的地位(而不是申明的地位)
  • 通常来说,寻找调用地位就是寻找 ” 函数被调用的地位 ”, 最重要的要剖析调用栈(就是为了达到以后执行地位所调用的所有函数)。运行代码时,调用器会在那个地位暂停,同时会在展现以后地位的函数调用列表,这就是调用栈。

绑定规定

  • 函数的调用地位决定了 this 的绑定对象,通常状况下分为以下几种规定:

    默认绑定

  • 最罕用的函数调用类型:独立函数调用。可把这条规定看到是无奈利用其余规定时的默认规定。

    function foo(){console.log(this.a);
    }
    var a = 2;
    foo(); // 2
  • 当调用 foo() 时,this.a 被解析成了全局变量 a。为什么?

    • 因为在上述代码中,函数调用时利用了 this 的默认绑定,因而 this 指向全局对象。(要了解 this,就要先了解调用地位)
  • 如果应用严格模式(strict mode),那全局对象将无奈应用默认绑定,因而 this 会绑定到 undefined。

    function foo(){
    "use strict";
    console.log(this.a);
    }
    var a = 2;
    foo(); // Type: this is undefined
  • 尽管 this 的绑定规定齐全取决于调用地位,然而 只有 foo() 运行在非 strict mode 下时,默认绑定能力绑定到全局对象; 严格模式下与 foo() 的调用地位无关。

    function foo(){console.log(this.a);
    }
    
    var a = 2;
    
    (function (){
    "use strict";
    
    foo(); // 2})
  • 通常状况下,尽量减少在代码中混合应用 strict modenon-strict mode,尽量减少在代码中混合应用 strict mode 和 non-strict mode。

隐式绑定

  • 另一条规定是调用地位是否有上下文对象,或者说是否被某个对象领有或包裹。
  • 思考以下代码:

    function foo() {console.log(this.a); // 2
    }
    
    var obj = {
    a: 2,
    foo: foo
    }
    
    obj.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
  • 隐式失落:在被隐式绑定的函数会失落绑定对象,也就是说它会默认绑定,从而把 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"
  • 还有一种奇怪的形式,就是在传入回调函数时隐式失落

    function foo() {console.log( this.a);
    }
    
    function doFoo(fn) {
     // fn 其实援用的是 foo
    
    fn(); // <-- 调用地位!}
    
    var obj = {
    a: 2,
    foo: foo
    };
    
    var a = "oops, global"; // a 是全局对象的属性
    
    doFoo(obj.foo); // "oops, global"
  • 在咱们传入函数时也会被隐式赋值。
  • 那如果传入的函数不是自定义的函数,而是语言内置的函数呢?后果还是一样的,没有区别

    function foo() {console.log( this.a);
    }
    
    var obj = {
    a: 2,
    foo: foo
    };
    
    var a = "oops, global"; // a 是全局对象的属性
    
    setTimeout(obj.foo, 100); // "oops, global"

显示绑定

  • 那咱们不想在对象外部蕴含函数援用,而是想在某个对象上强制调用函数,该如何操作?

    • 那就必须要应用 call() 和 apply()。第一个参数是一个对象,也就是须要绑定的对象,第二个参数传入的参数,而两者之间的区别就在于第二个参数,call 的第二个参数是一个个参数,而 apply 则是一个参数数组。

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

      new 绑定

  • 在传统的语言中,构造函数时一个非凡办法,应用 new 初始化须要调用的类,通常模式下是 let something = new MyClass();
  • 在应用 new 来调用函数,会主动执行以下操作:

    1. 创立一个新对象
    2. 让新对象的 __proto__(隐式原型) 等于函数的 prototype(显式原型)
    3. 绑定 this, 让新象绑定于函数的 this 指向
    4. 判断返回值,如果返回值不是一个对象,则返回刚新建的新对象。

优先级

  • 如果在某个调用地位利用多条规定该如何?那为了解决此问题,那就引申出了优先级问题。
  • 毫无疑问,默认绑定的优先级是四条规定中最低的,能够先不思考它。
  • 先来看看隐式绑定和显式绑定那个优先级更高?

    function foo() {console.log( this.a);
    }
    
    var obj1 = {
    a: 2,
    foo: foo
    };
    
    var obj2 = {
    a: 3,
    foo: foo
    };
    
    // 隐式绑定
    obj1.foo(); // 2
    obj2.foo(); // 3
    
    // 显式绑定
    obj1.foo.call(obj2); // 3
    obj2.foo.call(obj1); // 2
  • 能够看出,显式绑定的优先级更高,也就是说在判断时该当思考是否能够利用显式绑定。
  • 再来看看 new 绑定和隐式绑定的优先级?

    function foo(something) {this.a = something;}
    
    var obj1 = {foo: foo};
    
    var obj2 = {};
    
    // 隐式绑定
    obj1.foo(2);
    console.log(obj1.a); // 2
    
    obj1.foo.call(obj2, 3);
    console.log(obj2.a); // 3
    
    // new 绑定
    var bar = new obj1.foo(4);
    console.log(obj1.a); // 2
    console.log(bar.a); // 4
  • 能够看出,new 绑定比隐式绑定的优先级更高,但 new 绑定和显式绑定谁的优先级更高呢?
  • new 与 call/apply 无奈一起应用,因而无奈通过 new foo.call(obj1) 来进行测试,但能够通过硬绑定来测试他两的优先级。
  • 硬绑定:Function.prototype.bind(…) 会创立一个新的包装函数,这个函数会疏忽以后的 this 绑定(无论绑定的对象是什么),并把咱们提供的对象绑定到 this 上。
  • 这样看起来硬绑定(也是显式绑定的一种)仿佛比 new 绑定的优先级更高,无奈应用 new 来管制 this 绑定。

    function foo(something) {this.a = something;}
    
    var obj1 = {};
    
    var bar = foo.bind(obj1);
    bar(2);
    console.log(obj1.a); // 2
    
    var baz = new bar(3);
    console.log(obj1.a); // 2
    console.log(baz.a); // 3
  • 出其不意!bar 被硬绑定到 obj1 上,然而 new bar(3) 并没有像咱们预计的那样把 obj1.a 批改为 3。相同,new 批改了硬绑定(到 obj1 的)调用 bar(..) 中的 this。因为应用了 new 绑定,咱们失去了一个名字为 baz 的新对象,并且 baz.a 的值是 3。
  • 硬绑定中的 bind(…) 的性能之一就是能够把除了第一个参数 (第一个参数用于绑定 this) 之外的其余参数传递给上层的函数(这种技术称为 ” 局部利用 ”, 是 ” 柯里化 ” 的一种)。

    function foo(p1,p2) {this.val = p1 + p2;}
    
    // 之所以应用 null 是因为在本例中咱们并不关怀硬绑定的 this 是什么
    // 反正应用 new 时 this 会被批改
    var bar = foo.bind(null, "p1");
    
    var baz = new bar("p2");
    
    baz.val; // p1p2
  • 判断 this

    1. 是否在 new 中调用(new 绑定), this 指向新创建的对象
    2. 是否通过 call、apply(显示绑定),this 指向绑定的对象
    3. 是否在某个对象中调用(隐式绑定),this 指向绑定的上下文对象
    4. 如果都不是,则是默认绑定,在严格模式下,this 指向 undefined, 非严格模式下,this 指向全局对象。
  • 优先级问题

    • 显式绑定:call()、apply()。(硬绑定也是显式绑定的其中一种: bind())
    • new 绑定: new Foo()
    • 隐式绑定: obj.foo();
    • 默认绑定: foo();
  • 排序:显式绑定 > new 绑定 > 隐式绑 定 > 默认绑定

绑定例子

被疏忽的 this

  • 如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被疏忽,理论利用的是默认绑定规定:

    function foo() {console.log( this.a);
    }
    
    var a = 2;
    
    foo.call(null); // 2
  • 那在什么状况下会传入 null 呢?

    • 一种十分常见的做法是应用 apply(..) 来“开展”一个数组,并当作参数传入一个函数。

      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

    • DMZ(demilitarized zone)空委托对象
  • 在 JavaScript 中创立一个空对象最简略的办法都是 Object.create(null)。Object.create(null) 和 {} 很 像,但 是 并 不 会 创 建 Object.prototype 这个委托,所以它比 {}“更空”:

    function foo(a,b) {console.log( "a:" + a + ", b:" + b);
    }
    
    // 咱们的 DMZ 空对象
    var ø = Object.create(null);
    
    // 把数组开展成参数
    foo.apply(ø, [2, 3] ); // a:2, b:3
    
    // 应用 bind(..) 进行柯里化
    var bar = foo.bind(ø, 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() 而不是 p.foo() 或者 o.foo()。依据咱们之前说过的,这里会利用默认绑定。
  • 留神:对于默认绑定来说,决定 this 绑定对象的并不是调用地位是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this 会被绑定到 undefined,否则 this 会被绑定到全局对象。

软绑定

  • 硬绑定这种形式能够把 this 强制绑定到指定的对象(除了应用 new 时),避免函数调用利用默认绑定规定。应用硬绑定会大大降低函数的灵活性,应用硬绑定之后就无奈应用隐式绑定或显示绑定来批改 this。
  • 可通过一种软绑定的办法来实现:

    if (!Function.prototype.softBind) {Function.prototype.softBind = function(obj) {
        var fn = this;
        // 捕捉所有 curried 参数
        var curried = [].slice.call( arguments, 1);
        var bound = function() {
            return fn.apply((!this || this === (window || global)) ?
                    obj : this
                curried.concat.apply(curried, arguments)
            );
        };
        bound.prototype = Object.create(fn.prototype);
        return bound;
    };
    }
  • 实现软绑定性能:

    function foo() {console.log("name:" + this.name);
    }
    
    var obj = {name: "obj"},
    obj2 = {name: "obj2"},
    obj3 = {name: "obj3"};
    
    var fooOBJ = foo.softBind(obj);
    
    fooOBJ(); // name: obj
    
    obj2.foo = foo.softBind(obj);
    obj2.foo(); // name: obj2 <---- 看!!!fooOBJ.call(obj3); // name: obj3 <---- 看!setTimeout(obj2.foo, 10);
    // name: obj   <---- 利用了软绑定
  • 能够看到,软绑定的 foo() 可手动将 this 绑定到 obj2 或 obj3 上,但如果利用默认绑定,则会将 this 绑定到 obj。

    this 词法

  • 在 ES6 中呈现了一种无奈应用这些规定的非凡函数类型:箭头函数
  • 箭头函数不实用 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() 的 this 绑定到 obj1,bar(援用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无奈被批改。(new 也不行!)
  • 在 ES6 之前,咱们也有应用和箭头函数一样的模式,如下代码:

    function foo() {
    var self = this; // this 快照
    setTimeout(function(){console.log( self.a);
    }, 100 );
    }
    
    var obj = {a: 2};
    
    foo.call(obj); // 2
  • 尽管 self = this 和箭头函数看起来都能够取代 bind(..),然而从实质上来说,它们想代替的是 this 机制。

小结

  1. 判断 this 指向

    1. 是否在 new 中调用(new 绑定), this 指向新创建的对象
    2. 是否通过 call、apply(显示绑定),this 指向绑定的对象
    3. 是否在某个对象中调用(隐式绑定),this 指向绑定对象的上下文
    4. 如果都不是,则是默认绑定,在严格模式下,this 指向 undefined, 非严格模式下,this 指向全局对象。
  2. 箭头函数不会应用上述的四条规定,而是依据以后的词法作用域来决定 this 的。箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。与 ES6 之前的 self = this 的机制一样。
  3. 留神:对于默认绑定来说,决定 this 绑定对象的并不是调用地位是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this 会被绑定到 undefined,否则 this 会被绑定到全局对象。

特殊字符形容:

  1. 问题标注 Q:(question)
  2. 答案标注 R:(result)
  3. 注意事项规范:A:(attention matters)
  4. 详情形容标注:D:(detail info)
  5. 总结标注:S:(summary)
  6. 剖析标注:Ana:(analysis)
  7. 提醒标注:T:(tips)

    往期举荐:

  8. 前端面试实录 HTML 篇
  9. 前端面试实录 CSS 篇
  10. JS 如何判断一个元素是否在可视区域内?
  11. Vue2、3 生命周期及作用?
  12. 排序算法:QuickSort
  13. 箭头函数与一般函数的区别?
  14. 这是你了解的 CSS 选择器权重吗?
  15. JS 中 call, apply, bind 概念、用法、区别及实现?
  16. 罕用位运算办法?
  17. Vue 数据监听 Object.definedProperty()办法的实现
  18. 为什么 0.1+ 0.2 != 0.3,如何让其相等?
  19. 聊聊对 this 的了解?
  20. JavaScript 为什么要进行变量晋升,它导致了什么问题?

    最初:

  21. 欢送关注『前端进阶圈』公众号,一起摸索学习前端技术 ……
  22. 公众号回复 加群 或 扫码, 即可退出前端交流学习群,一起高兴摸鱼和学习 ……
  23. 公众号回复 加好友,即可添加为好友

正文完
 0