深入理解JavaScript之this全面解析

    在之前的章节里我们知道,this  是在函数运行时绑定的,它只与函数在哪被调用有关系

  1.1  调用位置

   在理解  this  的绑定之前,我们先理解  this  的调用位置,

  调用位置就是函数在代码中被调用的位置(而不是声明位置)

   通常来说,寻找调用位置就是寻找“函数被调用的位置”,看起来很简单,但是某些编程模式会隐藏真正的调用位置。

   寻找调用位置,实际上就是寻找分析调用栈(就是为了到达当前执行的函数位置所调用的所有函数)

  我们所关心的调用位置就在当前正在执行的函数的前一个调用中

  

  下面看个例子


     function baz() {
         //当前函数位置是baz
         //调用栈:全局-->baz
         //调用位置是当前函数位置的上一个位置,全局作用域调用了baz(),所以调用位置是全局作用域
         console.log("baz");
         bar();       //bar函数被调用
     }
     function bar() {
         //当前函数位置是bar
         //调用栈:全局-->baz-->bar
        //调用位置是当前位置的上一个位置,baz()调用了bar(),所以调用位置是baz()
         console.log("bar");
         foo();      //foo函数被调用
     }
     function foo() {
         //当前函数位置是foo
         //调用栈:全局-->baz-->bar-->foo
         //调用位置是当前位置的上一个位置,bar()调用了foo(),所以调用位置是bar()
         console.log("foo");
     }
     baz();         //baz被调用  
    

  一般而言,都是从全局作用域中逐一推断出调用栈的位置。

 

   可以将调用栈想象成一个函数调用链,就像我们在上面代码里面分析的那样,但是这种方法非常麻烦并且容易出错,,另一个查看调用栈的方法是通过浏览器的调试工具。当今绝大多数浏览器都内置了开发者工具,其中包括JavaScript调试器。

   就本例来说,你可以在foo(...)中设置一个断点,或者直接在foo(...)中第一行代码中插入一个debugger语句,在运行代码时,调试器会在那个位置暂停,同时会显示当前位置的函数调用列表,这就是你的调用栈,然后找到栈中第二个元素,这就是真正的调用位置。

  下面我们就chrome浏览器为例,看看如何设置断点,并查看调用栈。

​​​​​​​1、用chrome打开需要调试的JS页面

2、按下F12,打开“开发者工具”

3、点击开发者工具中的"sources"打开需要调试的JS页面

4、鼠标单击,代码行号就能设置断点了,如下图我们在19行处的bar(...)函数中设置了断点,程序运行到19行处便会停止。

5、点击  "call stack"  查看当前设置断点函数的调用栈(注意是当前函数的上一个函数才是此函数的调用位置)如下图,我们在bar(...)处设置了断点,"call stack"中显示了三个调用栈,从下到上依次调用,有一个蓝色图标的是当前设置了断点的函数,上一个函数baz(...)才是调用bar(...)的调用位置

 

1.2   绑定规则 

    我们接下来看看,在函数的执行过程中调用位置是如何决定  this  的绑定对象的。

   在JavaScript中有四个  this  的绑定规则,但是,应用哪一条规则要看  this函数  的调用位置。

1.2.1  默认绑定

   首先要介绍的是最常用的函数调用类型:独立函数调用。可以把这条规则看作是当其他规则无法应用时才会应用的一条规则。

看看下面的代码

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

  你应该注意到的一件事是

声明在全局作用域中的变量(此例中是  var a=2)是全局对象的一个同名属性

  接下来我们看到当调用了  foo(...)  函数时,this.a  被解释成了全局变量a  。这是为什么?  因为在本例中,函数调用时应用了  this  的默认绑定,因此  this  指向了全局对象。

  那么我们如何知道此处是应用了  this  的默认绑定规则呢?

   我们首先来看看,foo函数的调用栈,通过分析我们可以得出,是全局对象调用了foo(...)函数,代码中的foo(...)是直接使用不带任何修饰的函数引用进行调用的。因此只能使用默认绑定,而不能使用其他规则

如果使用严格模式"strict mode",那么全局对象将无法应用默认绑定,  this会绑定到"undefined"

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

默认绑定规则:

非严格模式下-----------------------------------------发生独立函数调用(函数直接使用不带任何修饰的函数引用进行调用)且调用位置是全局作用域时,this  绑定全局对象。

严格模式(strict more)下----------------------------this 与函数调用位置无关,不适用默认绑定规则

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

注意:通常来说JavaScript代码中要么非严格,要么严格,不提倡有一些严格,有一些不严格,但是在引用类库中,可能会遇到,非严格与严格并存的情况,这个时候要注意到这种兼容性的小细节

1.2.2  隐式绑定

  另一条需要考虑的是,调用位置是否存在上下文对象。或者说   this函数  是否被某个对象拥有或者包含。这个说法不够严谨。

  我们举个例子来说明

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

    首先要注意的是,foo(...)的声明方式,及其是如何被添加为obj的引用属性的。但是无论是直接在obj中定义还是先定义再添加为引用属性,都改变不了foo(...)严格来说不属于obj对象。

  然而调用位置会使用  obj  的上下文来引用函数,因此你可以说函数被调用时  obj  对象包含或者拥有它。

   无论你怎么称呼这个模式,当foo(...)函数被调用时,它的落脚点确实指向  obj  对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的  this  绑定到这个上下文对象。因为调用foo(...)时  this  被绑定到obj,因此this.a和obj.a是一样的。

  那假如它有多个上下文对象呢?比如这个函数处在一个对象属性引用链中

对象属性引用链只有最顶层或者说最后一层会调用位置

看下面的例子

 function foo() {
           console.log(this.a);
       }

       var obj2={
           a:3,
           foo:foo
       };

       var obj1={
           a:2,
           obj2:obj2
       };

       obj1.obj2.foo();   //结果为3,对象属性引用链

  foo被obj2调用,但是引用方式却是通过对象属性引用链来实现的,"obj1.obj2.foo"。按照我们的隐式绑定规则来看,this  只会绑定最后一层的对象,于是此例中  this 与obj2  进行了绑定,输出的为  obj2  中的a

 

存在的问题:隐式丢失---------被隐式绑定的函数会丢失绑定对象,它会应用默认绑定从而把this绑定到全局对象(非严格模式)或者undefined(严格模式)上

看下面的一段代码


      function foo() {
          console.log(this.a);
      }
      var obj={
          a:2,
          foo:foo
      };
      var bar=obj.foo;   //函数别名,将foo()赋给了bar()
      var a=3;
    bar();        //3,此处bar()没有添加别的修饰词,是独立函数调用,所以应用默认绑定。发生了隐式丢失
    

   我们刚开始时让foo()函数隐式绑定obj,但是当"var bar=obj.foo"时,bar  就已经算obj.foo的另一种引用了,bar引用的是foo()函数的本身,那么此时的"bar()"其实是一种不带任何修饰的函数调用,因此发生了默认绑定。输出全局对象a的值

  另一种更加隐蔽的隐式丢失发生在:传递参数期间也会发生隐式丢失----------即回调函数传入参数时


      function foo() {
          console.log(this.a);
      }
      var obj={
          a:2,
          foo:foo
      };
      function doFoo(fn) {       //foo被当成参数传入
          fn();                    //foo被调用
      }
      var a=3;
      doFoo(obj.foo);     //传入的是obj.foo,参数传递中发生了隐式丢失
    

  这里同样发生了隐式丢失,且是在参数传递的过程中发生了隐式丢失,doFoo(obj.foo)传入的同样是foo()函数本身,而且在调用时,是不带有任何装饰(上下文对象)的调用,因此  this  应用默认绑定。

  如果把函传入语言的内置函数而不是你定义的函数,比如传入setTimeout(),会怎样?

  结果也是一样的。

  function foo() {
          console.log(this.a);
      }
      var obj={
          a:2,
          foo:foo
      };
      var a=3;
      setTimeout(obj.foo,0);    //setTimeout传入的是foo函数本身,且是不带任何装饰的调用,因此是默认绑定

setTimeout()函数和以下的伪代码类似

   function  setTimeout(fn,delay){
       .....//delay延时
           fn();   //调用fn
}

  就如同这个函数被定义在之前的代码里执行的一样。

实际上使用回调函数时,丢失this的绑定是非常常见的,你无法控制回调函数的执行方式,因此就没有办法控制会影响绑定的调用位置,但是可以通过固定this解决这个问题。

1.2.3   显示绑定

  在之前我们讲到了隐式绑定,在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把这个  this  间接(隐式)绑定到这个对象上

  既然有了隐式绑定,那么就有显示绑定,就像当我们不想在对象内部包含函数引用(在对象内部,创建此函数属性),想要在某个对象上强制调用该函数,这就是隐式绑定。

  JavaScript中的“所有”函数都有一些有用的特性(原型),可以用来解决这个问题,具体点来说是使用我们之前接触过的"call(...)"---强制绑定this对象,以及"apply(...)"方法。

  严格来说,JavaScript的宿主环境有时会提供一些非常特别的函数,它们并没有这两个方法(就是某一些函数用不了这两个方法),但是这种函数非常罕见,JavaScript大多数函数都可以使用"call(...)"和"apply(...)"方法

  

  接下来我们看看这两个函数是如何工作的

  函数.方法名(对象)

  它们第一个参数是对象,它们会把这个对象绑定到this,接着在调用函数时指定这个this。如此便将此函数的this和对象绑定到了一起,这就是显示绑定的由来

  call与apply的作用相同,区别在于,两者传入的参数不同

  看下面的代码

function foo() {
          console.log(this.a);
      }
      var obj={
          a:2
      };
      var   a=3;
      foo.call(obj);       //2,call将foo函数中的this显示绑定到obj对象

 通过foo.call(...)我们在调用foo的时候强制把它的this绑定到obj上。

  如果你正在call(...)中传入的不是一个对象,而是一个原始值(字符串类型、布尔类型或者数字类型)来当做this的绑定对象,这个对象会转化成它的对象形式(也就是new String(...)、new Boolean(...)或者new Number(...))-------------------原始值对象转化为它的对象形式称之为“装箱”

  尽管显式绑定很牛逼!!!但是在我们不清楚绑定对象时,仍然无法解决我们的隐式绑定丢失的问题

但是显式绑定的一个变种可以解决这个问题

1  硬绑定

  思考以下代码

 function foo() {
          console.log(this.a);
      }
      var obj={
          a:2
      };
      var bar=function () {
          foo.call(obj);     //在bar()中使用call方法将obj对象练到了一起
      }
      bar();  //2
        setTimeout(bar(),10) //2
          bar.call(window);  //2

  在bar(...)函数中将foo函数显式绑定在obj中,无论bar(...)在哪被调用,它的内部都在使foo(...)显式绑定  obj  对象。因此无论如何调用bar(...),它绑定的对象都在obj中。这就是硬绑定

硬绑定:隐式丢失的主要原因是在隐式绑定的过程中,我们this的对象发生了改变,那么我们只需要将this在调用之前提前绑定到合适的对象就行。

硬绑定典型的应用场景

  • 创建包裹函数:传入所有的参数并返回接收到的所有值
function foo(something) {
         console.log(this.a,something);         //输出,输入的参数
         return this.a+something;           //返回接收到的参数,值为a+参数
     }
     var obj={
         a:2
     };
     var bar=function () {       //创建包裹函数
        return foo.apply(obj,arguments);
     };
     var b=bar(3);   //2  3
     console.log(b);  //5(2+3)

  所谓包裹函数其实就是封装函数,把该函数重要的内容封装起来,只需要知道怎么用即可,用户接触不到核心的函数(foo(...)以及"obj")。

  如上图中包裹函数为bar(...),用户真正接触到的只有bar(...)而没有foo(...)以及"obj",用户只需要知道bar(...)的用法,传入的参数即可。

  • 创建i可以重复使用的辅助函数:

     function foo(something) {
         console.log(this.a,something);         //输出,输入的参数
         return this.a+something;           //返回接收到的参数,值为a+参数
     }
    function bind(fn,obj) {      //创建了捆绑函数,使传入的函数fn与对象obj实现硬绑定
        return function () {
            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,它的用法和用法跟我们上个代码中创建的捆绑函数"bind(...)"类似


     function foo(something) {
         console.log(this.a,something);         //输出,输入的参数
         return this.a+something;           //返回接收到的参数,值为a+参数
     }
    var obj={
         a:2
    };
     var bar=foo.bind(obj);     //函数.bind(this的绑定对象)
     var b=bar(3);     //2  3
     console.log(b);    //5
    

bind(...)会返回一个硬编码的新函数,它会把参数(obj)设置为this的上下文对象并调用原始函数(foo)。

  bind(...)还有一个非常重要的作用便是实现柯里化:bind(...)能把除了第一个参数(用于绑定this)之外的参数传递给下层的函数

2   API调用的上下文

  第三方类库的许多函数,以及JavaScript语言和宿主环境中有许多新的内置函数,都提供了一个可选的参数,通常称之为“上下文”,其作用和bind(...)一样,确保你的回调函数使用指定的this。

  举例来说:

   function foo(el,id) {
            console.log(el,this.id);
        };
        var obj={
            id:"awesome"
        };
        [1,2,3].forEach(foo,obj);   //传入的参数.forEach(函数,对象);
          //1  awesome
          //2  awesome
          //3  awesome

1.2.4  new绑定

  这是最后一条规则“new绑定规则”,在讲解这条规则之前,首先让我们澄清一个非常常见的关于javascript中函数和对象的误解。

  在传统的面向对象的语言中,“构造函数”是类中的一些特殊方法,使用  new  初始化类时会调用类中的构造函数。通常的形式是这样的:

                                                    something = new  Myclass(...);

  在就JavaScript中也有一个  new  操作符,使用方法看起来也跟面向对象的语言一样,学过面向对象语言的开发者会认为这个机制跟面向对象中的机制一样,然而两者天差地别。

  为什么呢???

首先我们先理解一下JavaScript中的构造函数

  • 在JavaScript中构造函数只是一个普通的函数
  • 它们不属于某个类,也不会实例某个类
  • 在使用  new  操作会调用的普通函数

举例说明:

ES5.1中是这么描述Number(...)作为构造函数的

15.7.2  Numbar构造函数

   当  Numbar  对象在  new  表达式中被调用时,它是一个构造函数:它会初始化新创建的对象

  所以包括内置对象函数(比如Numbar(...))在内的所有函数都可以用new来调用,这种函数称为构造函数调用

  也就是说:在JavaScript中没有所谓的“构造函数”,只有对于函数的“构造调用”

我们来分析一下使用  new  调用函数----发生函数“构造调用”的过程

  1. 创建(构造)一个全新的对象
  2. 这个对象会被执行“原型”连接
  3. 这个对象会被绑定到函数调用的this
  4. 如果函数中没有返回其他对象,那么  new  表达式中的函数调用会自动返回这个新对象。

 举例说明使用  new  调用函数的过程。

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

  使用  new  来调用foo(...)函数时,我们会构造一个新对象并把它绑定到foo(...)中调用的  this  上去---------------这就是new绑定

  若还有不明白的  new  可在https://www.cnblogs.com/faith3/p/6209741.html中参考

 

1.3  优先级

  我们已经介绍完了四条this绑定的规则:

  1. 默认绑定----------不带有任何修饰的独立函数调用
  2. 隐式绑定----------带有上下文对象的调用(方法调用)
  3. 显式绑定----------call、apply以及API显式调用(间接调用)
  4. new绑定-----------new构造调用

  那么如果符合多条应用规则,我们该用哪条规则呢?

  首先我们不用考虑“默认绑定”,这肯定是在后的了,只有在不符合其余三条规则下,才会应用“默认规则”

  接下来我们比较下“隐式绑定”与“显式绑定”的优先级,谁更高呢?

  看代码:


   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
    

  我们创建了foo(...)函数,其中有  this  ,“obj1”和“obj2”对象,他们中的"a"值分别为2以及3。我们首先应用了隐式绑定,可以看出输出结果正确,接下来我们用上隐式绑定"带有上下文的引用"再用显式绑定"call方法调用",很明显“obj1”输出的是“obj2”的值,这正是显式调用比隐式调用优先级高的最有利证据。

  显式绑定比隐式绑定优先级高

接下来我们需要搞清楚,new绑定和隐式绑定,哪个优先级更高。

看接下来的代码


  function foo(something) {
      this.a=something;        //将穿传入的参数变成a属性
  }
  var obj1={
      foo:foo
  };
  var obj2={};

     obj1.foo(2);
     console.log(obj1.a);         //2

     obj1.foo.call(obj2,3);         //显式绑定
        console.log(obj2.a);       //3

        var bar=new obj1.foo(4);
        console.log(obj1.a);       //2    隐式绑定
        console.log(bar.a);           //4        new绑定将隐式绑定的a改变了


    

     从中可以看出

           new绑定比隐式绑定优先级更高。

  那么new绑定与显式绑定,谁的优先级更高呢?

new与call/apply无法同时使用,因此无法通过new  foo.call(obj1)来直接进行测试,但是我们可以使用硬绑定来测试它们的优先级

  在看接下来的代码时,让我们来回忆,硬绑定是如何工作的?Function.prototype.bind(...)会创建一个新的包装函数,这个函数会忽略它当前的  this  绑定(无论绑定的对象是什么),并把我们提供的对象绑定到  this  上。

  接下来我们通过代码来看看,new和硬绑定谁的优先级更高。

function foo(something) {
      this.a=something;        //将穿传入的参数变成a属性
  }
  var obj1={};
        var bar=foo.bind(obj1);        //在bar中我们将obj1与foo对象强制绑定到了一起
        bar(2);
        console.log(obj1.a);    //2

//按道理来讲,使用bar作为构建调用时,obj1也应该和foo绑定到一起
        var baz=new bar(3);        
        console.log(obj1.a);     //2  obj1.a的值没有改变,证明硬绑定被解开了
        console.log(baz.a);    //3   证明产生的新对象将硬绑定的this解开指向了新对象,否则obj1.a=3

    出乎意料!bar被硬绑定到obj1上,但是new bar(3)并没有想我们预计(new绑定的优先级比硬绑定的优先级低)的那样改变obj1.a的值为3。相反new绑定修改了硬绑定(到obj1的)调用bar(...)中的this,因为使用了new绑定,我们得到一个名字为baz的新对象,修改了baz.a=3。这也就意味着

                         new绑定比硬绑定优先级别更高

  new看起来无法修改硬绑定的this,然而事实却不一样,在JavaScript中,会首先判断硬绑定函数是否被new调用,如果是的话就会使用新创建的this替换硬绑定的this。

  那么为什么要在new中使用硬绑定呢?这是因为在new中使用bind(...)函数可以将传入的参数,除了对象之外,传递给下一层的函数。

举例:

function foo(p1,p2) {
     this.val=p1+p2;
 }
 //之所以用null,是因为此处我们不用管this硬绑定的对象
 //反正不管是什么,this的值都会被修改
 var bar=foo.bind(null,"p1");
 var baz=new bar("p2");
   baz.val;   //p1p2

1.4   this的引用顺序

现在我们可以根据之前的结论来判断this的绑定顺序了

①函数是否在new中调用(new绑定),是的话this绑定的是新创建的对象。

    var baz=new foo()

②函数是否通过call、apply或者硬绑定(在一个函数A中,对象obj使用call或者是apply永远与this函数绑定到了一起,无论何时在哪调用函数A,this指向的都是对象obj)调用?如果是的话,this绑定的是指定的对象。

  var bar=foo.call(obj1);

③函数是否在某个上下文对象中被调用(隐式绑定)?是的话,this指向这个上下文对象

  var bar=obj1.foo();

④如果都不是的话,就是用默认绑定,在严格模式下,绑定"undefined",否者就绑定全局对象。

  var bar=foo();

1.5  例外的绑定

  规则总有例外,在这里也一样,在某些场景下的this绑定可能不按照我们之前的优先级来,而是直接绑定了默认规则。

1.5.1  被忽略的this

  在你把"undefined"或者"null"作为this的绑定对象传入"call(...)"、"apply(...)"以及"bind(...)"时,这些值会被忽略,而this会应用默认绑定。

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

  foo.apply(null);        //2
  foo.apply(undefined);    //2

  那么什么情况下,你需要往显式绑定方法中传入"null"呢?

  一种常见的做法是使用apply(...)来“展开”一个数组,并当作参数来传入下一个函数,类似的,bind(...)可以对参数进行柯里化(预先放置一个参数)。

柯里化:把接受多个参数的函数变为只接受开始的第一个参数的函数,并且返回接受余下的参数且返回结果的新函数。通俗点解释是:接受一个单一可以预期的参数,返回一个正确结果,http://www.cnblogs.com/pigtail/p/3447660.html参考这篇文章

  下面我们来看看,如何用apply(....)“展开”一个数组

 function foo(a,b) {
      console.log("a:"+a+"b:"+b);
  }
  foo.apply(null,[2,3]);   //a:2 b:3    把数组展开成参数

如何用bind(...)实现柯里化

 function foo(a,b) {
      console.log("a:"+a+"b:"+b);
  }
  var bar=foo.bind(null,2);    //a=2;
    bar(3);      //a:2  b:3

  如上我们可以看到,原本传入的foo的参数为"a""b"但是在使用bind(...),以及传入的绑定对象是"null"之后我们在foo.bind()中就不需要将"a""b"的值完全输入。

  这也就相当于变相的缩小了函数的使用范围,例:原本foo函数可以用于"a""b"值为任意值,但是在"var bar=foo.bind(null,2)"之后,我们使用"bar"只能用在'a=2"的场合----------------这就是柯里化

  这两种方法都需要传入一个参数作为绑定对象,当我们不关心函数绑定的this=时,你仍然需要传入一个参数才能使用这个函数,那么“null”是一个正确的选择

  注意!!!尽管这种做法有时候很实用,但是在this已经实现绑定时,忽略此this会导致,this绑定

结果出错(绑定成全局对象)

   

更安全的this

   那么有没有别的更安全的做法忽略掉这次的this呢?

  其实有种做法比传入"null"还要安全,这就是"DMZ"

 这种做法是传入一个特殊的对象,把this绑定到这个对象不会对你的程序起到任何的副作用。这个对象是"DMZ"-------一个非空的委托对象。

如果我们将"DMZ"作为对象传入this以达到忽略this的目的,那么,任何对this的使用都会被限制在这个空对象里,对外界没有任何影响

  在JavaScript中创建这个空对象最简单的方法是:object.create(null)。这个与{}很像,单不同的是它不会创建object.prototype这个委托,它比{}更空。

function foo(a,b) {
     console.log("a:"+a+"b:"+b);
 }
 var ø=Object.create(null);   //创建DMZ空对象
  foo.apply(ø,[1,2]);         //利用DMZ空对象传入参数到this函数中,对参数进行展开
       //利用bind进行柯里化
    var bar=foo.bind(ø,2);
    bar(3);

  尽管我们可以将"DMZ"对象名更改为我们喜欢的名字,但是仍然是建议大家使用ø

1.5.2  间接引用

  另外一个经常容易犯的错误便是,你可能有意无意的创建一个函数的“间接引用”,在这种情况下this会默认绑定。

 IIFE中赋值

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

 赋值表达式p.foo=o.foo的返回值是目标函数的引用,因此调用位置是foo()而不是o.foo()或者p.foo()。因此这里使用默认绑定

函数赋值

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

  注意这里!!!默认绑定输出的是不是"undefined"并不取决于调用this的位置,而取决于this函数体(这里是foo函数)是否为严格模式。

1.5.3  软绑定

  之前我们有提到硬绑定,强制使this指向一个我们希望指向的对象,隐式绑定、显式绑定都无法改变它(除了new绑定以外)。这样尽管很好,但是却牺牲了函数的灵活性,使用硬绑定就无法使用显式绑定或者隐式绑定修改this指向的对象,只能通过改变硬绑定函数内的函数体。

  如果给默认绑定对象指定一个全局对象和"undefined"以外的值,就可以实现和硬绑定同样的效果,同时隐式绑定和显式绑定还可以修改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;
};
}

  除了软绑定之外,softBind()的其他原理与ES5中的bind(...)类似。

  1. 对指定函数进行封装
  2. 检查使用的this是否绑定到全局对象或者是"undefined"
  3. 是的话就把默认对象obj绑定到this上
  4. 不是的话,就不会修改this
  5. 此函数支持柯里化

softBind()例子

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身上。

1.6  this词法

  之前我们介绍了this绑定的四种规则,然而在JavaScript中并不是所有的函数体都遵循着这四个规则,其中箭头函数便是如此。

  箭头函数并不是用function关键字定义的,而是使用被称为“胖箭头”的操作符=>定义的函数(使用胖箭头代替function声明),胖箭头不适用this的四条规则,胖箭头函数的this是根据外层作用域来决定的

function foo() {
      return (a)=>{
          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,胖箭头的this无法修改,即使是new绑定也不行。

胖箭头一般用于回调函数中,例如事件处理器或者定时器

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

  利用箭头函数可以像硬绑定bind(...)那样,确保this绑定到指定对象,不同的是它是利用作用域。而我们在写代码时,尽量使用一种设计模式-----要么用四条规则绑定,要么使用胖箭头作用域绑定。

总结:

  this在JavaScript的绑定对象,根据以下步骤查找

①new调用?绑定创建的新对象---------------构造函数调用

②由call()、apply()、bind()调用?绑定到指定对象------------间接调用

③由上下文对象调用?绑定到上下文对象-------------------------方法调用

④函数直接使用不带任何修饰的引用?绑定到全局对象--------独立调用

 

能解决隐式丢失的只有:硬绑定(bind(...))

当使用间接引用时,很容易发生隐式丢失,要仔细看,一般发生了隐式丢失,都会应用默认绑定。

  当想要安全的忽略掉this绑定时,那么久传入一个"DMZ"对象,创建方法:

var ø=Object.create(null)。

  ES6中的胖箭头函数不在四条this绑定规则之内,胖箭头函数一旦绑定之后new绑定都无法改变绑定对象,它的this绑定只于上一层的作用域有关,这是用作用域影响this绑定。这与ES6之前的"self=this"一样。

 

猜你喜欢

转载自blog.csdn.net/qq_41889956/article/details/83386111