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

222 阅读7分钟

前言

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

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

为什么要用 this

  • 考虑以下代码:

                      function speak() {
                      var greeting = "Hello, I'm " + identify.call( this );
                      console.log( greeting );
                      }
                      
                      var me = {
                      name: "Kyle"
                      };
                      
                      var you = {
                      name: "Reader"
                      };
                      
                      identify.call( me ); // KYLE
                      identify.call( you ); // READER
                      
                      speak.call( me ); // Hello, 我是 KYLE
                      speak.call( you ); // Hello, 我是 READER" title="" data-bs-original-title="复制" aria-label="复制"></button>
    </div>
    </div><pre class="js hljs language-javascript"><span class="hljs-keyword">function</span> <span class="hljs-title function_">identify</span>(<span class="hljs-params"></span>) {
    <span class="hljs-keyword">return</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">name</span>.<span class="hljs-title function_">toUpperCase</span>();
    }
    
    <span class="hljs-keyword">function</span> <span class="hljs-title function_">speak</span>(<span class="hljs-params"></span>) {
    <span class="hljs-keyword">var</span> greeting = <span class="hljs-string">"Hello, I'm "</span> + identify.<span class="hljs-title function_">call</span>( <span class="hljs-variable language_">this</span> );
    <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>( greeting );
    }
    
    <span class="hljs-keyword">var</span> me = {
    <span class="hljs-attr">name</span>: <span class="hljs-string">"Kyle"</span>
    };
    
    <span class="hljs-keyword">var</span> you = {
    <span class="hljs-attr">name</span>: <span class="hljs-string">"Reader"</span>
    };
    
    identify.<span class="hljs-title function_">call</span>( me ); <span class="hljs-comment">// KYLE</span>
    identify.<span class="hljs-title function_">call</span>( you ); <span class="hljs-comment">// READER</span>
    
    speak.<span class="hljs-title function_">call</span>( me ); <span class="hljs-comment">// Hello, 我是 KYLE</span>
    speak.<span class="hljs-title function_">call</span>( you ); <span class="hljs-comment">// Hello, 我是 READER</span></pre></li><li>这段代码再不同的上下文对象(me 和 you) 中重复使用函数 identify() 和 speak(), 不用针对每个对象编写不同版本的函数。</li><li><p>若不使用 this 如下代码:</p><div class="widget-codetool" style="display: none;">
          <div class="widget-codetool--inner">
                      <button type="button" class="btn btn-dark far fa-copy rounded-0 sflex-center copyCode" data-toggle="tooltip" data-placement="top" data-clipboard-text="function identify(context) {
                      return context.name.toUpperCase();
                      }
                      
                      function speak(context) {
                      var greeting = &quot;Hello, I'm &quot; + identify( context );
                      console.log( greeting );
                      }
                      
                      identify( you ); // READER
                      speak( me ); //hello, 我是 KYLE" title="" data-bs-original-title="复制" aria-label="复制"></button>
    </div>
    </div><pre class="js hljs language-javascript"><span class="hljs-keyword">function</span> <span class="hljs-title function_">identify</span>(<span class="hljs-params">context</span>) {
    <span class="hljs-keyword">return</span> context.<span class="hljs-property">name</span>.<span class="hljs-title function_">toUpperCase</span>();
    }
    
    <span class="hljs-keyword">function</span> <span class="hljs-title function_">speak</span>(<span class="hljs-params">context</span>) {
    <span class="hljs-keyword">var</span> greeting = <span class="hljs-string">"Hello, I'm "</span> + <span class="hljs-title function_">identify</span>( context );
    <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>( greeting );
    }
    
    <span class="hljs-title function_">identify</span>( you ); <span class="hljs-comment">// READER</span>
    <span class="hljs-title function_">speak</span>( me ); <span class="hljs-comment">//hello, 我是 KYLE</span></pre></li></ul><h3 id="item-0-4">消除对 this 的误解</h3><ul><li><p>在解释下 this 到底是如何工作的,首先必需消除对 this 的错误认识。</p><h4>指向自身</h4></li><li><p>为什么需要从函数内部引用函数自身呢?</p><ul><li>最常见的原因是递归。</li></ul></li><li>其实 this 并不像我们所想的那样指向函数本身。</li><li><p>考虑以下代码:</p><div class="widget-codetool" style="display: none;">
          <div class="widget-codetool--inner">
                      <button type="button" class="btn btn-dark far fa-copy rounded-0 sflex-center copyCode" data-toggle="tooltip" data-placement="top" data-clipboard-text="function foo(num) {
                      console.log( &quot;foo: &quot; + num );
                      
                       // 记录 foo 被调用的次数
                       this.count++;
                       }
                       
                       foo.count = 0;
                       
                       var i;
                       
                       for (i=0; i<10; i++) {
                       if (i > 5) {
                           foo( i );
                           }
                           }
                           // foo: 6
                           // foo: 7
                           // foo: 8
                           // foo: 9
                           
                           // foo 被调用了多少次?
                           console.log( foo.count ); // 这里会输出多少次呢?" title="" data-bs-original-title="复制" aria-label="复制"></button>
    </div>
    </div><pre class="js hljs language-javascript"><span class="hljs-keyword">function</span> <span class="hljs-title function_">foo</span>(<span class="hljs-params">num</span>) {
    <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>( <span class="hljs-string">"foo: "</span> + num );
    
     <span class="hljs-comment">// 记录 foo 被调用的次数</span>
     <span class="hljs-variable language_">this</span>.<span class="hljs-property">count</span>++;
     }
     
     foo.<span class="hljs-property">count</span> = <span class="hljs-number">0</span>;
     
     <span class="hljs-keyword">var</span> i;
     
     <span class="hljs-keyword">for</span> (i=<span class="hljs-number">0</span>; i&lt;<span class="hljs-number">10</span>; i++) {
     <span class="hljs-keyword">if</span> (i &gt; <span class="hljs-number">5</span>) {
         <span class="hljs-title function_">foo</span>( i );
         }
         }
         <span class="hljs-comment">// foo: 6</span>
         <span class="hljs-comment">// foo: 7</span>
         <span class="hljs-comment">// foo: 8</span>
         <span class="hljs-comment">// foo: 9</span>
         
         <span class="hljs-comment">// foo 被调用了多少次?</span>
         <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>( foo.<span class="hljs-property">count</span> ); <span class="hljs-comment">// 这里会输出多少次呢?</span></pre></li><li>先思考,后查看<br>&lt;details&gt;<br>&lt;summary&gt;查看答案&lt;/summary&gt;<br>&lt;pre&gt;</li></ul><div class="widget-codetool" style="display: none;">
          <div class="widget-codetool--inner">
                      <button type="button" class="btn btn-dark far fa-copy rounded-0 sflex-center copyCode" data-toggle="tooltip" data-placement="top" data-clipboard-text="function foo(num) {
                          console.log( &quot;foo: &quot; + num );
                          // 记录 foo 被调用的次数
                              this.count++;
                              }
                              foo.count = 0;
                              var i;
                              for (i=0; i<10; i++) {
                                  if (i > 5) {
                                          foo( i );
                                              }
                                              }
                                              // foo: 6
                                              // foo: 7
                                              // foo: 8
                                              // foo: 9
                                              // foo 被调用了多少次?
                                              console.log( foo.count ); // 0
                                              // 为什么会输出 0 呢?
                                              // 从字面意思来看,上面的函数执行了 4 此,理应来说, foo.count 应该是 4 才对。" title="" data-bs-original-title="复制" aria-label="复制"></button>
    </div>
    
    function foo(num) {
        console.log( "foo: " + num );
        // 记录 foo 被调用的次数
            this.count++;
            }
            foo.count = 0;
            var i;
            for (i=0; i<10; i++) {
                if (i > 5) {
                        foo( i );
                            }
                            }
                            // foo: 6
                            // foo: 7
                            // foo: 8
                            // foo: 9
                            // foo 被调用了多少次?
                            console.log( foo.count ); // 0
                            // 为什么会输出 0 呢?
                            // 从字面意思来看,上面的函数执行了 4 此,理应来说, foo.count 应该是 4 才对。

    </pre>
    </details>

    • 当执行 foo.count = 0; 时,的确向函数对象 foo 中添加了一个属性 count, 但是函数内部代码中 this.count 中的 this 并不是指向那个函数对象,虽然属性名相同,跟对象却并不相同,困惑随之产生。
    • 如果你会有 “如果我增加的 count 属性和预期的不一样,那我增加的是那个 count?”疑惑。实际上,如果你读过之前的文章,就会发现这段代码会隐式地创建一个全局变量 count。它的值为 NaN。如果你发现为什么是这么个奇怪的结果,那你肯定会有 “为什么它的值是 NaN, 而不是其他值?” 的疑惑。(原理参考:mp.weixin.qq.com/s/H1gpn0vfm…)
    • 当然也有方法对上述代码进行规避:

                         // 记录 foo 被调用的次数
                         data.count++;
                         }
                         
                         var data = {
                         count: 0
                         };
                         
                         var i;
                         
                         for (i=0; i<10; i++) {
                         if (i > 5) {
                             foo( i );
                             }
                             }
                             // foo: 6
                             // foo: 7
                             // foo: 8
                             // foo: 9
                             
                             // foo 被调用了多少次?
                             console.log( data.count ); // 4" title="" data-bs-original-title="复制" aria-label="复制"></button>
      </div>
      </div><pre class="js hljs language-javascript"><span class="hljs-keyword">function</span> <span class="hljs-title function_">foo</span>(<span class="hljs-params">num</span>) {
      <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>( <span class="hljs-string">"foo: "</span> + num );
      
       <span class="hljs-comment">// 记录 foo 被调用的次数</span>
       data.<span class="hljs-property">count</span>++;
       }
       
       <span class="hljs-keyword">var</span> data = {
       <span class="hljs-attr">count</span>: <span class="hljs-number">0</span>
       };
       
       <span class="hljs-keyword">var</span> i;
       
       <span class="hljs-keyword">for</span> (i=<span class="hljs-number">0</span>; i&lt;<span class="hljs-number">10</span>; i++) {
       <span class="hljs-keyword">if</span> (i &gt; <span class="hljs-number">5</span>) {
           <span class="hljs-title function_">foo</span>( i );
           }
           }
           <span class="hljs-comment">// foo: 6</span>
           <span class="hljs-comment">// foo: 7</span>
           <span class="hljs-comment">// foo: 8</span>
           <span class="hljs-comment">// foo: 9</span>
           
           <span class="hljs-comment">// foo 被调用了多少次?</span>
           <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>( data.<span class="hljs-property">count</span> ); <span class="hljs-comment">// 4</span></pre></li><li>虽然从某种角度来说,解决了问题,但忽略了真正的问题——无法理解 this 的含义和工作原理,上述代码而是返回了舒适区——词法作用域。</li><li>上面提到的如果匿名函数需要引用自身,除了 this 还有已经被废弃的 <code>arguments.callee</code> 来引用当前正在运行的函数对象。</li><li><p>对于上述提到的代码,更进阶的方式就是使用 foo 标识符来替代 this 来引用函数对象,如下代码:</p><div class="widget-codetool" style="display: none;">
            <div class="widget-codetool--inner">
                        <button type="button" class="btn btn-dark far fa-copy rounded-0 sflex-center copyCode" data-toggle="tooltip" data-placement="top" data-clipboard-text="function foo(num) {
                        console.log( &quot;foo: &quot; + num );
                        
                        // 记录 foo 被调用的次数
                        foo.count++;
                        }
                        foo.count=0
                        var i;
                        
                        for (i=0; i<10; i++) {
                        if (i > 5) {
                            foo( i );
                            }
                            }
                            // foo: 6
                            // foo: 7
                            // foo: 8
                            // foo: 9
                            
                            // foo 被调用了多少次?
                            console.log( foo.count ); // 4" title="" data-bs-original-title="复制" aria-label="复制"></button>
      </div>
      </div><pre class="js hljs language-javascript"><span class="hljs-keyword">function</span> <span class="hljs-title function_">foo</span>(<span class="hljs-params">num</span>) {
      <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>( <span class="hljs-string">"foo: "</span> + num );
      
      <span class="hljs-comment">// 记录 foo 被调用的次数</span>
      foo.<span class="hljs-property">count</span>++;
      }
      foo.<span class="hljs-property">count</span>=<span class="hljs-number">0</span>
      <span class="hljs-keyword">var</span> i;
      
      <span class="hljs-keyword">for</span> (i=<span class="hljs-number">0</span>; i&lt;<span class="hljs-number">10</span>; i++) {
      <span class="hljs-keyword">if</span> (i &gt; <span class="hljs-number">5</span>) {
          <span class="hljs-title function_">foo</span>( i );
          }
          }
          <span class="hljs-comment">// foo: 6</span>
          <span class="hljs-comment">// foo: 7</span>
          <span class="hljs-comment">// foo: 8</span>
          <span class="hljs-comment">// foo: 9</span>
          
          <span class="hljs-comment">// foo 被调用了多少次?</span>
          <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>( foo.<span class="hljs-property">count</span> ); <span class="hljs-comment">// 4</span></pre></li><li>这种解决方式依然规避了 this 问题,并且完全依赖于变量 foo 的词法作用域。</li><li><p>更进阶的方式是强制 this 指向 foo 函数对象, 使用 <code>call, bind, apply</code> 关键字来实现。</p><div class="widget-codetool" style="display: none;">
            <div class="widget-codetool--inner">
                        <button type="button" class="btn btn-dark far fa-copy rounded-0 sflex-center copyCode" data-toggle="tooltip" data-placement="top" data-clipboard-text="function foo(num) {
                        console.log( &quot;foo: &quot; + num );
                        
                         // 记录 foo 被调用的次数
                          // 注意,在当前的调用方式下(参见下方代码),this 确实指向 foo
                          this.count++;
                          }
                          
                          foo.count = 0;
                          
                          var i;
                          
                          for (i=0; i<10; i++) {
                          if (i > 5) {
                           // 使用 call(..) 可以确保 this指向函数对象 foo 本身
                               foo.call( foo, i );
                               }
                               }
                               // foo: 6
                               // foo: 7
                               // foo: 8
                               // foo: 9
                               
                               // foo 被调用了多少次?
                               console.log( foo.count ); // 4" title="" data-bs-original-title="复制" aria-label="复制"></button>
      </div>
      </div><pre class="js hljs language-javascript"><span class="hljs-keyword">function</span> <span class="hljs-title function_">foo</span>(<span class="hljs-params">num</span>) {
      <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>( <span class="hljs-string">"foo: "</span> + num );
      
       <span class="hljs-comment">// 记录 foo 被调用的次数</span>
        <span class="hljs-comment">// 注意,在当前的调用方式下(参见下方代码),this 确实指向 foo</span>
        <span class="hljs-variable language_">this</span>.<span class="hljs-property">count</span>++;
        }
        
        foo.<span class="hljs-property">count</span> = <span class="hljs-number">0</span>;
        
        <span class="hljs-keyword">var</span> i;
        
        <span class="hljs-keyword">for</span> (i=<span class="hljs-number">0</span>; i&lt;<span class="hljs-number">10</span>; i++) {
        <span class="hljs-keyword">if</span> (i &gt; <span class="hljs-number">5</span>) {
         <span class="hljs-comment">// 使用 call(..) 可以确保 this指向函数对象 foo 本身</span>
             foo.<span class="hljs-title function_">call</span>( foo, i );
             }
             }
             <span class="hljs-comment">// foo: 6</span>
             <span class="hljs-comment">// foo: 7</span>
             <span class="hljs-comment">// foo: 8</span>
             <span class="hljs-comment">// foo: 9</span>
             
             <span class="hljs-comment">// foo 被调用了多少次?</span>
             <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>( foo.<span class="hljs-property">count</span> ); <span class="hljs-comment">// 4</span></pre></li></ul><h4>它的作用域</h4><ul><li>常见的误解:this 指向函数的作用域,其实在某种情况下是正确的,但在其他情况下是错误的。</li><li>其实,this 在任何情况下都不指向函数的词法作用域。</li><li><p>考虑一下代码:</p><div class="widget-codetool" style="display: none;">
            <div class="widget-codetool--inner">
                        <button type="button" class="btn btn-dark far fa-copy rounded-0 sflex-center copyCode" data-toggle="tooltip" data-placement="top" data-clipboard-text="function foo() {
                        var a = 2;
                        this.bar();
                        }
                        
                        function bar() {
                        console.log( this.a );
                        }
                        
                        foo();
                        
                        // 这段代码你一共能发现几处错误?并且报错后会抛出什么?" title="" data-bs-original-title="复制" aria-label="复制"></button>
      </div>
      </div><pre class="js hljs language-javascript"><span class="hljs-keyword">function</span> <span class="hljs-title function_">foo</span>(<span class="hljs-params"></span>) {
      <span class="hljs-keyword">var</span> a = <span class="hljs-number">2</span>;
      <span class="hljs-variable language_">this</span>.<span class="hljs-title function_">bar</span>();
      }
      
      <span class="hljs-keyword">function</span> <span class="hljs-title function_">bar</span>(<span class="hljs-params"></span>) {
      <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>( <span class="hljs-variable language_">this</span>.<span class="hljs-property">a</span> );
      }
      
      <span class="hljs-title function_">foo</span>();
      
      <span class="hljs-comment">// 这段代码你一共能发现几处错误?并且报错后会抛出什么?</span></pre></li><li>先思考,后查看<br>&lt;details&gt;<br>&lt;summary&gt;查看答案&lt;/summary&gt;<br>&lt;pre&gt;</li></ul><div class="widget-codetool" style="display: none;">
            <div class="widget-codetool--inner">
                        <button type="button" class="btn btn-dark far fa-copy rounded-0 sflex-center copyCode" data-toggle="tooltip" data-placement="top" data-clipboard-text="function foo() {
                            var a = 2;
                                this.bar();
                                    // bar(); // 当前方式会根据词法作用域的规则来查找 bar() 方法,并且调用它
                                    }
                                    function bar() {
                                        console.log(this.a); // ReferenceError: a is not defined
                                            // console.log(a); // 这种方式也不行,因为函数会创建一个块作用域,所以无法通过 bar 的作用域访问到上层 foo 作用域。
                                            }
                                            foo(); // TypeError: this.bar is not a function" title="" data-bs-original-title="复制" aria-label="复制"></button>
      </div>
      
      function foo() {
          var a = 2;
              this.bar();
                  // bar(); // 当前方式会根据词法作用域的规则来查找 bar() 方法,并且调用它
                  }
                  function bar() {
                      console.log(this.a); // ReferenceError: a is not defined
                          // console.log(a); // 这种方式也不行,因为函数会创建一个块作用域,所以无法通过 bar 的作用域访问到上层 foo 作用域。
                          }
                          foo(); // TypeError: this.bar is not a function

      </pre>
      </details>

      • 首先,这段代码试图通过 this.bar() 来引用 bar() 函数。这是绝对不可能成功的,我们之后会解释原因。调用 bar() 最自然的方法是省略前面的 this,直接使用词法引用标识符。
      • 此外,编写这段代码的开发者还试图使用 this 联通 foo() 和 bar() 的词法作用域,从而让bar() 可以访问 foo() 作用域里的变量 a。这是不可能实现的,你不能使用 this 来引用一个词法作用域内部的东西。

      this 到底是什么

      • 说了这么多,那 this 到底是一个什么样的机制呢?

        • 之前我们说过 this 是在运行时进行绑定的,而不是在编写时绑定的,它的上下文取决于函数调用时的各种条件。
        • this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
      • 当一个函数被调用是,会创建一个执行上下文,这个执行上下文汇总会包含函数在哪里被调用(也就是调用栈),函数的调用方法, 传入的参数等信息。而 this 就是这样一个属性,会在函数执行的过程中被用到。

      小结

      • 学习 this 的第一步要明白 this 既不指向函数自身也不指向函数的词法作用域。
      • 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. 公众号回复 加好友,即可添加为好友