抗美日 發表於 2022-9-19 13:02:00

深入浅出 JavaScript 中的 this

<p>笔者最近在看 你不知道的JavaScript上卷,里面关于 <code>this</code> 的讲解个人觉得非常精彩。<code>JavaScript</code> 中的 <code>this</code> 算是一个核心的概念,有一些同学会对其有点模糊和小恐惧,究其原因,现在对 <code>this</code> 讨论的文章很多,让我们觉得 <code>this</code> 无规律可寻,就像一个幽灵一样</p>
<p>如果你还没弄懂 <code>this</code>,或者对它比较模糊,这篇文章就是专门为你准备的,如果你相对比较熟悉了,那你也可以当做复习巩固你的知识点</p>
<p>本篇文章,算是一篇读书笔记,当然也加上了很多我的个人理解,我觉得肯定对大家有所帮助</p>
<h2 id="执行上下文">执行上下文</h2>
<p>在理解 <code>this</code> 之前,我们先来看下什么是执行上下文</p>
<p>简而言之,执行上下文是评估和执行 <code>JavaScript</code> 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行</p>
<p>JavaScript 中有三种执行上下文类型</p>
<ul>
<li>全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 <code>window</code> 对象(浏览器的情况下),并且设置 <code>this</code> 的值等于这个全局对象。一个程序中只会有一个全局执行上下文</li>
<li>函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个</li>
<li><code>eval</code> 函数执行上下文 — 执行在 <code>eval</code> 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 <code>eval</code>,所以在这里我不会讨论它</li>
</ul>
<p>这里我们先得出一个结论,<strong>非严格模式和严格模式中 this 都是指向顶层对象(浏览器中是window)</strong></p>
<pre><code class="language-js">console.log(this === window); // true
'use strict'
console.log(this === window); // true
this.name = 'vnues';
console.log(this.name); // vnues
</code></pre>
<p>后面我们的讨论更多的是针对函数执行上下文</p>
<h2 id="this-到底是什么为什么要用-this">this 到底是什么?为什么要用 this</h2>
<p><code>this</code> 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调 用时的各种条件</p>
<p>牢记:<strong><code>this</code> 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式</strong></p>
<p>当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包 含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。<code>this</code> 就是记录的 其中一个属性,会在函数执行的过程中用到</p>
<p>看个实例,理解为什么要用 <code>this</code>,有时候,我们需要实现类似如下的代码:</p>
<pre><code class="language-js">function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = "Hello, I'm " + identify(context);
console.log(greeting);
}
var me = {
name: "Kyle"
};
speak(me); //hello, 我是 KYLE
</code></pre>
<p>这段代码的问题,在于需要显示传递上下文对象,如果代码越来越复杂,这种方式会让你的代码看起来很混乱,用 <code>this</code> 则更加的优雅</p>
<pre><code class="language-js">var me = {
name: "Kyle"
};

function identify() {
return this.name.toUpperCase();
}
function speak() {
var greeting = "Hello, I'm " + identify.call(this);
console.log(greeting);
}
speak.call(me); // Hello, 我是 KYLE
</code></pre>
<h2 id="this-的四种绑定规则">this 的四种绑定规则</h2>
<p>下面我们来看在函数上下文中的绑定规则,有以下四种</p>
<ul>
<li>默认绑定</li>
<li>隐式绑定</li>
<li>显式绑定</li>
<li><code>new</code> 绑定</li>
</ul>
<h3 id="默认绑定">默认绑定</h3>
<p>最常用的函数调用类型:独立函数调用,这个也是优先级最低的一个,此事 <code>this</code> 指向全局对象。注意:如果使用严格模式(<code>strict mode</code>),那么全局对象将无法使用默认绑定,因此 <code>this</code> 会绑定 到 <code>undefined</code>,如下所示</p>
<pre><code class="language-js">var a = 2;//变量声明到全局对象中
function foo() {
console.log(this.a);   // 输出 a
}

function bar() {
'use strict';
console.log(this); // undefined
}
foo();
bar();
</code></pre>
<h3 id="隐式绑定">隐式绑定</h3>
<p>还可以我们开头说的:<strong><code>this</code> 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式</strong></p>
<p>先来看一个例子:</p>
<pre><code class="language-js">function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
obj.foo(); // 2
</code></pre>
<p>当调用 <code>obj.foo()</code> 的时候,<code>this</code> 指向 obj 对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 <code>this</code> 绑定到这个上下文对象。因为调 用 <code>foo() </code>时 <code>this</code> 被绑定到 obj,因此 this.a 和 obj.a 是一样的</p>
<p>记住:<strong>对象属性引用链中只有最顶层或者说最后一层会影响调用位置</strong></p>
<pre><code class="language-js">    function foo() {
      console.log(this.a);
    }
    var obj2 = {
      a: 42,
      foo: foo
    };
    var obj1 = {
      a: 2,
      obj2: obj2
    };
    obj1.obj2.foo(); // 42
</code></pre>
<p><strong>间接引用</strong></p>
<p>另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这 种情况下,调用这个函数会应用默认绑定规则</p>
<pre><code>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
</code></pre>
<p>另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这 种情况下,调用这个函数会应用默认绑定规则</p>
<p>赋值表达式 <code>p.foo = o.foo</code> 的返回值是目标函数的引用,因此调用位置是 <code>foo() </code>而不是 <code>p.foo()</code> 或者 <code>o.foo()</code>。根据我们之前说过的,这里会应用默认绑定</p>
<h3 id="显示绑定">显示绑定</h3>
<p>在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把 <code>this</code> 间接(隐式)绑定到这个对象上。 那么如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么<br>
做呢?</p>
<p><code>Javascript</code> 中提供了 <code>apply</code> 、<code>call</code> 和 <code>bind</code> 方法可以让我们实现</p>
<p>不同之处在于,<code>call()</code> 和 <code>apply()</code> 是立即执行函数,并且接受的参数的形式不同:</p>
<ul>
<li><code>call(this, arg1, arg2, ...)</code></li>
<li><code>apply(this, )</code></li>
</ul>
<p>而 <code>bind()</code> 则是创建一个新的包装函数,并且返回,而不是立刻执行</p>
<ul>
<li><code>bind(this, arg1, arg2, ...)</code></li>
</ul>
<p>看如下的例子:</p>
<pre><code class="language-js">function foo(b) {
    console.log(this.a + '' + b);
}
var obj = {
    a: 2,
    foo: foo
};
var a = 1;
foo('Gopal'); // 1Gopal
obj.foo('Gopal'); // 2Gopal
foo.call(obj, 'Gopal'); // 2Gopal
foo.apply(obj, ['Gopal']); // 2Gopal
let bar = foo.bind(obj, 'Gopal');
bar(); // 2Gopal
</code></pre>
<p><strong>被忽略的 this</strong></p>
<p>如果你把 <code>null</code> 或者 <code>undefined</code> 作为 <code>this</code> 的绑定对象传入 <code>call</code>、<code>apply</code> 或者 <code>bind</code>,这些值在调用时会被忽略,实际应用的是默认绑定规则</p>
<pre><code class="language-js">function foo() {
console.log(this.a);
}
var a = 2;
foo.call(null); // 2
</code></pre>
<p>利用这个用法使用 <code>apply(..)</code> 来“展开”一个数组,并当作参数传入一个函数。<br>
类似地,<code>bind(..)</code> 可以对参数进行柯里化(预先设置一些参数)</p>
<pre><code class="language-js">function foo(a, b) {
    console.log("a:" + a + ", b:" + b);
}
// 把数组“展开”成参数
foo.apply(null, ); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind(null, 2);
bar(3); // a:2, b:3
</code></pre>
<h3 id="new绑定">new绑定</h3>
<p>当我们使用构造函数 <code>new</code> 一个实例的时候,这个实例的 <code>this</code> 指向是什么呢?</p>
<p>我们先来看下使用 <code>new</code> 来调用函数,或者说发生构造函数调用时,会执行什么操作,如下:</p>
<ul>
<li>创建(或者说构造)一个全新的对象</li>
<li>这个新对象会被执行[[原型]]连接,将对象(实例)的 <code>__proto__</code> 和构造函数的 <code>prototype</code> 绑定</li>
<li>这个新对象会绑定到函数调用的 <code>this</code></li>
<li>如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象</li>
</ul>
<p>原理实现类似如下:</p>
<pre><code class="language-js">function create (ctr) {
    // 创建一个空对象
    let obj = new Object()
    // 链接到构造函数的原型对象中
    let Con = [].shift.call(arguments)
    obj.__proto__ = Con.prototype
    // 绑定this
    let result = Con.apply(obj, arguments);
    // 如果返回是一个对象,则直接返回这个对象,否则返回实例
    return typeof result === 'object'? result : obj;
}
</code></pre>
<p>注意:<code>let result = Con.apply(obj, arguments);</code> 实际上就是指的是新对象会绑定到函数调用的 <code>this</code></p>
<pre><code>function Foo(a) {
    this.a = a;
}
var bar = new Foo(2);
console.log(bar.a); // 2
</code></pre>
<h2 id="特殊情况箭头函数">特殊情况——箭头函数</h2>
<p>我们之前介绍的四条规则已经可以包含所有正常的函数。但是 ES6 中介绍了一种无法使用 这些规则的特殊函数类型:箭头函数</p>
<p>箭头函数不使用 <code>this</code> 的四种标准规则,而是根据定义时候的外层(函数或者全局)作用域来决 定 <code>this</code>。也就是说箭头函数不会创建自己的 <code>this</code>,它只会从自己的作用域链的上一层继承 <code>this</code></p>
<pre><code class="language-js">function foo() {
// 返回一个箭头函数
// this 继承自 foo()
return (a) =&gt; {
    console.log(this.a);
}
};

var obj1 = {
a: 2
};
var obj2 = {
a: 3
};
var bar = foo.call(obj1);
bar.call(obj2); // 2, 不是 3 !
</code></pre>
<p><code>foo()</code> 内部创建的箭头函数会捕获调用时 <code>foo()</code> 的 <code>this</code>。由于 <code>foo()</code> 的 <code>this</code> 绑定到 <code>obj1</code>, <code>bar</code>(引用箭头函数)的 <code>this</code> 也会绑定到 <code>obj1</code>,箭头函数的绑定无法被修改。(<code>new</code> 也不 行!)</p>
<h2 id="总结this-优先级">总结——this 优先级</h2>
<p>判断是否为箭头函数,是则按照箭头函数的规则</p>
<p>否则如果要判断一个运行中函数的 <code>this</code> 绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断 <code>this</code> 的绑定对象</p>
<ol>
<li>由 <code>new</code> 调用?绑定到新创建的对象</li>
<li>由 <code>call</code> 或者 <code>apply</code>(或者 <code>bind</code>)调用?绑定到指定的对象</li>
<li>由上下文对象调用?绑定到那个上下文对象</li>
<li>默认:在严格模式下绑定到 <code>undefined</code>,否则绑定到全局对象</li>
</ol>
<p>如下图所示:</p>
<p><img src="//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5d199f99f90d4a1093a4a6253d5991a4~tplv-k3u1fbpfcp-zoom-1.image"></p>
<h2 id="参考">参考</h2>
<ul>
<li>
<p>[译] 理解 JavaScript 中的执行上下文和执行栈</p>
</li>
<li>
<p>你不知道的JavaScript上卷</p>
</li>
</ul><br><br>
来源:https://www.cnblogs.com/gopal/p/16707377.html
頁: [1]
查看完整版本: 深入浅出 JavaScript 中的 this