揭开 this & that 之迷

futureyue 7年前
   <p><img src="https://simg.open-open.com/show/2c4f855a9a1add5afc1fce04b88f1882.png"></p>    <p>新手在入门 JavaScript 的过程中,一定会踩很多关于 this 的坑,出现问题的本质就是 this 指针的指向和自己想的不一样。笔者在入门学习的过程中,也踩了很多坑,于是便写下本篇文章记录自己“踩坑”历程。</p>    <h3>一. this 在哪里?</h3>    <p>在上篇 <a href="/misc/goto?guid=4959750414102756442" rel="nofollow,noindex">《从 JavaScript 作用域说开去》</a> 分析中,我们知道,在 Execution Context 中有一个属性是 this,这里的 this 就是我们所说的 this 。this 与上下文中可执行代码的类型有直接关系, <em>* this 的值在进入执行上下文时确定,并且在执行上下文运行期间永久不变。</em> *</p>    <p>this 到底取何值?this 的取值是动态的,是在函数真正被调用执行的时候确定的,函数定义的时候确定不了。因为 this 的取值是执行上下文环境的一部分,每次调用函数,都会产生一个新的执行上下文环境。</p>    <p>所以 this 的作用就是用来指明执行上下文是在哪个上下文中被触发的对象。令人迷惑的地方就在这里,同一个函数,当在不同的上下文进行调用的时候,this 的值就可能会不同。也就是说,this 的值就是函数调用表达式(也就是函数被调用的方式)的 caller。</p>    <h3>二. this & that 具体值得是谁?</h3>    <p>目前接触的有以下14种情况,笔者打算一一列举出来,以后如果遇到了更多的情况,还会继续增加。</p>    <p>既然 this 是执行上下文确定的,那么从执行上下文的种类进行分类,可以分为3种:</p>    <p><img src="https://simg.open-open.com/show/d19a60dbf4ffcd5465a3d41d07516790.png"></p>    <p>那么接下来我们就从 Global Execution Context 全局执行上下文,Function Execution Context 函数执行上下文,Eval Execution Context Eval执行上下文 这三类,具体谈谈 this 究竟指的是谁。</p>    <p>(一). 全局执行上下文</p>    <p>1. 非严格模式下的函数调用</p>    <p>这是函数的最通常用法,属于全局性调用,因此 this 就代表全局对象 Global。</p>    <pre>  <code class="language-javascript">var name = 'halfrost';    function test() {        console.log(this); // window      console.log(this.name); // halfrost  }                                                                                                                                                                                                                                                                                                                                test();</code></pre>    <p>在全局上下文(Global Context)中,this 总是 global object,在浏览器中就是 window 对象。</p>    <p>2. 严格模式下的函数调用</p>    <p>严格模式由 <a href="/misc/goto?guid=4959750414192703791" rel="nofollow,noindex">ECMAScript 5.1</a> 引进,用来限制 JavaScript 的一些异常处理,提供更好的安全性和更强壮的错误检查机制。使用严格模式,只需要将 'use strict' 置于函数体的顶部。这样就可以将上下文环境中的 this 转为 undefined。这样执行上下文环境不再是全局对象,与非严格模式刚好相反。</p>    <p>在严格模式下,情况并不是仅仅是 undefined 这么简单,有可能严格模式夹杂着非严格模式。</p>    <p>先看严格模式的情况:</p>    <pre>  <code class="language-javascript">'use strict';    function test() {      console.log(this); //undefined  };  test();</code></pre>    <p>上面的这个情况比较好理解,还有一种情况也是严格模式下的:</p>    <pre>  <code class="language-javascript">function execute() {          'use strict'; // 开启严格模式        function test() {          // 内部函数也是严格模式          console.log(this); // undefined        }        // 在严格模式下调用 test()        // this 在 test() 下是 undefined        test(); // undefined      }      execute();</code></pre>    <p>如果严格模式在外层,那么在执行作用域内部声明的函数,它会继承严格模式。</p>    <p>接下来就看看严格模式和非严格模式混合的情况。</p>    <pre>  <code class="language-javascript">function nonStrict() {          // 非严格模式        console.log(this); // window      }      function strict() {          'use strict';        // 严格模式        console.log(this); // undefined      }</code></pre>    <p>这种情况就比较简单了,各个模式下分别判断就可以了。</p>    <p>(二).函数执行上下文</p>    <p>3. 函数调用</p>    <p>当通过正常的方式调用一个函数的时候,this 的值就会被设置为 global object(浏览器中的 window 对象)。</p>    <p>严格模式和非严格模式的情况和上述全局执行上下文的情况一致,严格模式对应的 undefined ,非严格模式对应的 window 这里就不再赘述了。</p>    <p>4. 方法作为对象的属性被调用</p>    <pre>  <code class="language-javascript">var person = {        name: "halfrost",      func: function () {          console.log(this + ":" + this.name);      }  };    person.func(); // halfrost</code></pre>    <p>在这个例子里面的 this 调用的是函数的调用者 person,所以会输出 person.name 。</p>    <p>当然如果函数的调用者是一个全局对象的话,那么这里的 this 指向又会发生变化。</p>    <pre>  <code class="language-javascript">var name = "YDZ";    var person = {        name: "halfrost",      func: function () {          console.log(this + ":" + this.name);      }  };    temp = person.func;    temp(); // YDZ</code></pre>    <p>在上面这个例子里面,由于函数被赋值到了另一个变量中,并没有作为 person 的一个属性被调用,那么 this 的值就是 window。</p>    <p>上述现象其实可以描述为,“ <strong>从一个类中提取方式时丢失了 this 对象</strong> ”。针对这个现象可以再举一个例子:</p>    <pre>  <code class="language-javascript">var counter = {          count: 0,        inc: function() {            this.count ++;        }  }      var func = counter.inc;    func();    counter.count;   // 输出0,会发现func函数根本不起作用</code></pre>    <p>这里我们虽然把 counter.inc 函数提取出来了,但是函数里面的 this 变成了全局对象了,所以 func() 函数执行的结果是 window.count++。然而 window.count 根本不存在,且值是 undefined,对 undefined 操作,得到的结果只能是 NaN。</p>    <p>验证一下,我们打印全局的 count:</p>    <pre>  <code class="language-javascript">count  // 输出是 NaN</code></pre>    <p>那么这种情况我们应该如何解决呢?如果就是想提取出一个有用的方法给其他类使用呢?这个时候的正确做法是使用 bind 函数。</p>    <pre>  <code class="language-javascript">var func2 = counter.inc.bind(counter);    func2();    counter.count; // 输出是1,函数生效了!</code></pre>    <p>5. 构造函数的调用</p>    <p>所谓构造函数就是用来 new 对象的函数。严格的来说,所有的函数都可以 new 一个对象,但是有些函数的定义是为了 new 一个对象,而有些函数则不是。另外注意,构造函数的函数名第一个字母大写(规则约定)。例如:Object、Array、Function等。</p>    <pre>  <code class="language-javascript">function person() {        this.name = "halfrost";      this.age = 18;      console.log(this);  }    var ydz = new person();  // person {name: "halfrost", age: 18}    console.log(ydz.name, ydz.age); // halfrost 18</code></pre>    <p>如果是构造函数被调用的话,this 其实指向的是 new 出来的那个对象。</p>    <p>如果不是被当做构造函数调用的话,情况有所区别:</p>    <pre>  <code class="language-javascript">function person() {        this.name = "halfrost";      this.age = 18;      console.log(this);  }    person(); // Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}</code></pre>    <p>如果不是被当做构造函数调用的话,那就变成了普通函数调用的情况,那么这里的 this 就是 window。</p>    <p>构造函数里面如果还定义了 prototype,this 会指向什么呢?</p>    <pre>  <code class="language-javascript">function person() {        this.name = "halfrost";      this.age = 18;      console.log(this);  }    person.prototype.getName = function() {        console.log(this.name); // person {name: "halfrost", age: 18} "halfrost"  }    var ydz = new person();  // person {name: "halfrost", age: 18}    ydz.getName();</code></pre>    <p>在 person.prototype.getName 函数中,this 指向的是 ydz 对象。因此可以通过 this.name 获取 ydz.name 的值。</p>    <p>其实,不仅仅是构造函数的 prototype,即便是在整个原型链中,this 代表的也都是当前对象的值。</p>    <p>6. 内部函数 / 匿名函数 的调用</p>    <p>如果在一个对象的属性是一个方法,这个方法里面又定义了内部函数和匿名函数,那么它们的 this 又是怎么样的呢?</p>    <pre>  <code class="language-javascript">var context = "global";    var test = {        context: "inside",      method: function () {            console.log(this + ":" +this.context);            function f() {              var context = "function";              console.log(this + ":" +this.context);           };          f();             (function(){              var context = "function";              console.log(this + ":" +this.context);           })();      }  };    test.method();    // [object Object]:object  // [object Window]:global  // [object Window]:global</code></pre>    <p>从输出可以看出,内部函数和匿名函数里面的 this 都是指向外面的 window。</p>    <p>7. call() / apply() / bind() 的方式调用</p>    <p>this 本身是不可变的,但是 JavaScript 中提供了 call() / apply() / bind() 三个函数来在函数调用时设置 this 的值。</p>    <p>这三个函数的原型如下:</p>    <pre>  <code class="language-javascript">// Sets obj1 as the value of this inside fun() and calls fun() passing elements of argsArray as its arguments.  fun.apply(obj1 [, argsArray])    // Sets obj1 as the value of this inside fun() and calls fun() passing arg1, arg2, arg3, ... as its arguments.  fun.call(obj1 [, arg1 [, arg2 [,arg3 [, ...]]]])    // Returns the reference to the function fun with this inside fun() bound to obj1 and parameters of fun bound to the parameters specified arg1, arg2, arg3, ....  fun.bind(obj1 [, arg1 [, arg2 [,arg3 [, ...]]]])</code></pre>    <p>在这3个函数里面,this 都是对应的第一个参数。</p>    <pre>  <code class="language-javascript">var rabbit = { name: 'White Rabbit' };        function concatName(string) {          console.log(this === rabbit); // => true        return string + this.name;      }      // 间接调用      concatName.call(rabbit, 'Hello ');  // => 'Hello White Rabbit'        concatName.apply(rabbit, ['Bye ']); // => 'Bye White Rabbit'</code></pre>    <p>apply() 和 call() 能够强制改变函数执行时的当前对象,让 this 指向其他对象。apply() 和 call() 的区别在于,apply() 的入参是一个数组,call() 的入参是一个参数列表。</p>    <p>apply() 和 call(),它俩都立即执行了函数,而 bind() 函数返回了一个新的函数,它允许创建预先设置好 this 的函数 ,并可以延后调用。</p>    <pre>  <code class="language-javascript">function multiply(number) {          'use strict';        return this * number;      }      // 创建绑定函数,绑定上下文2      var double = multiply.bind(2);        // 调用间接调用      double(3);  // => 6        double(10); // => 20</code></pre>    <p>bind() 函数实质其实是实现了,原始绑定函数共享相同的代码和作用域,但是在执行时拥有不同的上下文环境。</p>    <p>bind() 函数创建了一个永恒的上下文链并不可修改。一个绑定函数即使使用 call() 或者 apply()传入其他不同的上下文环境,也不会更改它之前连接的上下文环境,重新绑定也不会起任何作用。</p>    <p>只有在构造器调用时,绑定函数可以改变上下文,然而这并不是特别推荐的做法。</p>    <pre>  <code class="language-javascript">function getThis() {          'use strict';        return this;      }      var one = getThis.bind(1);        // 绑定函数调用      one(); // => 1        // 使用 .apply() 和 .call() 绑定函数      one.call(2);  // => 1        one.apply(2); // => 1        // 重新绑定      one.bind(2)(); // => 1        // 利用构造器方式调用绑定函数      new one(); // => Object</code></pre>    <p>只有 new one() 时可以改变绑定函数的上下文环境,其他类型的调用结果是 this 永远指向 1。</p>    <p>8. setTimeout、setInterval 中的 this</p>    <p>《 javascript 高级程序设计》中写到:“超时调用的代码需要调用 window 对象的 setTimeout 方法”。setTimeout/setInterval 执行的时候,this 默认指向 window 对象,除非手动改变 this 的指向。</p>    <pre>  <code class="language-javascript">var name = 'halfrost';    function Person(){        this.name = 'YDZ';      this.sayName=function(){          console.log(this); // window          console.log(this.name); // halfrost          };      setTimeout(this.sayName, 10);      }  var person=new Person();</code></pre>    <p>上面这个例子如果想改变 this 的指向,可是使用 apply/call 等,也可以使用 that 保存 this。</p>    <p>值得注意的是: setTimeout 中的回调函数在严格模式下也指向 window 而不是 undefined !</p>    <pre>  <code class="language-javascript">'use strict';    function test() {      console.log(this);  //window  }  setTimeout(test, 0);</code></pre>    <p>因为 setTimeout 的回调函数如果没有指定的 this ,会做一个隐式的操作,将全局上下文注入进去,不管是在严格还是非严格模式下。</p>    <p>9. DOM event</p>    <p>当一个函数被当作event handler的时候,this会被设置为触发事件的页面元素(element)。</p>    <pre>  <code class="language-javascript">var body = document.getElementsByTagName("body")[0];    body.addEventListener("click", function(){        console.log(this);  });  // <body>…</body></code></pre>    <p>10. in-line 的方式调用</p>    <p>当代码通过 in-line handler 执行的时候,this 同样指向拥有该 handler 的页面元素。</p>    <p>看下面的代码:</p>    <pre>  <code class="language-javascript">document.write('<button onclick="console.log(this)">Show this</button>');    // <button onclick="console.log(this)">Show this</button>  document.write('<button onclick="(function(){console.log(this);})()">Show this</button>');    // window</code></pre>    <p>在第一行代码中,正如上面 in-line handler 所描述的,this 将指向 "button" 这个 element。但是,对于第二行代码中的匿名函数,是一个上下文无关(context-less)的函数,所以 this 会被默认的设置为 window。</p>    <p>前面我们已经介绍过了 bind 函数,所以,通过下面的修改就能改变上面例子中第二行代码的行为:</p>    <pre>  <code class="language-javascript">document.write('<button onclick="((function(){console.log(this);}).bind(this))()">Show this</button>');    // <button onclick="((function(){console.log(this);}).bind(this))()">Show this</button></code></pre>    <p>11. this & that</p>    <p>在 JavaScript 中,经常会存在嵌套函数,这是因为函数可以作为参数,并可以在合适的时候通过函数表达式创建。这会引发一些问题,如果一个方法包含一个普通函数,而你又想在后者的内部访问到前者,方法中的 this 会被普通函数的 this 覆盖,比如下面的例子:</p>    <pre>  <code class="language-javascript">var person = {           name: 'halfrost',         friends: [ 'AA', 'BB'],         loop: function() {              'use strict';               this.friends.forEach(                   function(friend) {   // (1)                      console.log(this.name + ' knows ' + friend);   // (2)                   }            );        }  };</code></pre>    <p>上述这个例子中,假设(1)处的函数想要在(2)这一行访问到 loop 方法里面的 this,该怎么做呢?</p>    <p>如果直接去调用 loop 方法是不行的,会发现报了下面这个错误。</p>    <pre>  <code class="language-javascript">person.loop();    // Uncaught TypeError: Cannot read property 'name' of undefined</code></pre>    <p>因为(1)处的函数拥有自己的 this,是没有办法在里面调用外面一层的 this 的。那怎么办呢?</p>    <p>解决办法有3种:</p>    <p>(1) that = this 我们可以把外层的 this 保存一份,一般会使用 that ,self,me,这些变量名暂存 this。</p>    <pre>  <code class="language-javascript">var person = {           name: 'halfrost',         friends: [ 'AA', 'BB'],         loop: function() {              'use strict';               var that = this;               this.friends.forEach(                   function(friend) {   // (1)                      console.log(that.name + ' knows ' + friend);   // (2)                   }            );        }  };    person.loop();    // halfrost knows AA  // halfrost knows BB</code></pre>    <p>这样就可以正确的输出想要的答案了。</p>    <p>(2) bind()</p>    <p>借助 bind() 函数,直接给回调函数的 this 绑定一个固定值,即函数的 this:</p>    <pre>  <code class="language-javascript">var person = {           name: 'halfrost',         friends: [ 'AA', 'BB'],         loop: function() {              'use strict';               var that = this;               this.friends.forEach(                   function(friend) {   // (1)                      console.log(this.name + ' knows ' + friend);   // (2)                   }.bind(this)            );        }  };    person.loop();    // halfrost knows AA  // halfrost knows BB</code></pre>    <p>(3) forEach() 的 thisValue</p>    <p>这种方法特定于 forEach()中,因为在这个方法的回调函数里面提供了第二个参数,我们可以利用这个参数,让它来为我们提供 this:</p>    <pre>  <code class="language-javascript">var person = {           name: 'halfrost',         friends: [ 'AA', 'BB'],         loop: function() {              'use strict';               var that = this;               this.friends.forEach(                   function(friend) {   // (1)                      console.log(this.name + ' knows ' + friend);   // (2)                   }, this );        }  };    person.loop();    // halfrost knows AA  // halfrost knows BB</code></pre>    <p>12. 箭头函数</p>    <p>箭头函数是 ES6 增加的新用法。</p>    <pre>  <code class="language-javascript">var numbers = [1, 2];        (function() {          var get = () => {          console.log(this === numbers); // => true          return this;        };        console.log(this === numbers); // => true        get(); // => [1, 2]        // 箭头函数使用 .apply() 和 .call()        get.call([0]);  // => [1, 2]        get.apply([0]); // => [1, 2]        // Bind        get.bind([0])(); // => [1, 2]      }).call(numbers);</code></pre>    <p>从上面的例子可以看出:</p>    <ol>     <li>箭头函数里面的 this 对象就是定义时候所在的对象,而不是使用时所在的对象。</li>     <li> <p>箭头函数不能被用来当做构造函数,于是也不能使用 new 命令。否则会报错 TypeError: get is not a constructor 。</p> <p>this 指向的固化,并不是因为箭头函数内部有绑定 this 的机制,实际原因是箭头函数根本就没有自己的 this ,导致内部的 this 就是外层代码块的 this。正因为它没有 this,所以也就不能作为构造函数了。</p> </li>     <li>箭头函数也不能使用 arguments 对象,因为 arguments 对象在箭头函数体内不存在,如果要使用,可以用 rest 参数代替。同样的,super,new.target 在箭头函数里面也是不存在的。所以,arguments、super、new.target 这3个变量在箭头函数里面都不存在。</li>     <li> <p>箭头函数里面也不能使用 yield 命令,因此箭头函数也不能用作 Generator 函数。</p> </li>     <li> <p>由于箭头函数没有自己的 this ,当然就不能用 call()、apply()、bind() 这些方法去改变 this 的指向。</p> </li>    </ol>    <p>13. 函数绑定</p>    <p>虽然在 ES6 中引入了箭头函数可以绑定 this 对象,大大减少了显示绑定 this 对象的写法(call、apply、bind)。鉴于箭头函数有上述说到的4个缺点(不能当做构造函数,不能使用 arguments 对象,不能使用 yield 命令,不能使用call、apply、bind),所以在 ES7 中又提出了函数绑定运算符。用来取代 call、apply、bind 的调用。</p>    <p>函数绑定运算符是并排的双冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动的将左边的对象作为上下文环境(即 this 对象)绑定到右边的函数上。</p>    <pre>  <code class="language-javascript">foo::bar  // 等同于 bar.bind(foo)    foo::bar(...arguments) // 等同于 bar.apply(foo,arguments)</code></pre>    <p>(三). Eval执行上下文</p>    <p>14. Eval 函数</p>    <p>Eval 函数比较特殊,this 指向就是当前作用域的对象。</p>    <pre>  <code class="language-javascript">var name = 'halfrost';    var person = {        name: 'YDZ',      getName: function(){          eval("console.log(this.name)");      }  }  person.getName();  // YDZ    var getName=person.getName;    getName();  // halfrost</code></pre>    <p>这里的结果和方法作为对象的属性被调用的结果是一样的。</p>    <p>Reference:</p>    <p>《ECMAScript 6 Primer》</p>    <p>《javascript 高级程序设计》</p>    <p><a href="/misc/goto?guid=4959750414277380950" rel="nofollow,noindex">JavaScript This 之谜(译文)</a></p>    <p> </p>    <p>来自:https://halfrost.com/javascript_this/</p>    <p> </p>