JavaScript の日常的な使用でも、フロントエンドのインタビュープロセスでも、これは非常に高い出現率を示します。これは間違いなく、この重要性を示しています。しかし、これは非常に柔軟であるため、多くの人がこの動作を理解するのが難しいと感じています。この記事では、なぜこれが必要なのかを説明し、その 6 つのルールを要約します。これがあなたの混乱の解決に役立つことを願っています。
はじめに
これは実際にはパラメータに相当します。このパラメータは開発中に手動で渡すことも、JS またはサードパーティによって渡すこともできます。
このパラメータは通常、関数の実行時に「所有者」を指します。このメカニズムにより、関数の設計がより簡潔になり、再利用可能になります。
this は関数の実行時にバインドされます。バインディング ルールは全部で 6 つあります。
●新しいバインディング: new キーワードを使用してオブジェクトを作成すると、作成されたオブジェクトにバインドされます。
●明示的なバインド: call、apply、または binding メソッドを使用して明示的にバインドされる場合、これが最初のパラメーターになります。
●暗黙的バインディング: オブジェクトに対して関数が実行されると、システムはこれを暗黙的にオブジェクトにバインドします。
●デフォルトバインディング: 関数が独立して実行される場合、厳密モードでの this のデフォルトバインディング値は未定義ですが、それ以外の場合はグローバルオブジェクトです。
●アロー関数バインディング: アロー関数を使用する場合、this のバインディング値は、その外側の通常の関数 (またはグローバル オブジェクト自体) の this と等しくなります。
●システムまたはサードパーティのバインド: システムまたはサードパーティが提供するインターフェイスに関数がパラメータとして渡されると、渡された関数内のこれはシステムまたはサードパーティによってバインドされます。
this
このメカニズムの役割は、オブジェクトを暗黙的に渡すエレガントな方法を提供し、関数の設計をより簡潔で再利用可能にすることができます。
クリックすると背景が赤に変わる 2 つのボタンがある次の例を考えてみましょう。
function changeBackgroundColor(ele) { ele.style.backgroundColor = 'red'; } btn1.addEventListener('click',function () { changeBackgroundColor(btn1); }); btn2.addEventListener('click',function () { changeBackgroundColor(btn2); });
ここでは、クリックされた要素を明示的にchangeBackgroundColor関数に渡します。しかし実際には、暗黙的にコンテキストを渡すこの機能を利用して、現在クリックされている要素を関数内で直接取得することができます。次のように:
function changeBackgroundColor() { this.style.backgroundColor = 'red'; } btn1.addEventListener('click',changeBackgroundColor); btn2.addEventListener('click',changeBackgroundColor);
最初の例では、クリックされた要素は ele (この仮パラメータ) に置き換えられます。 2 番目の例では、特別なキーワード this に置き換えられます。これは、本質的にはオブジェクトへの参照であるため、手動で値を転送する必要がなく、より簡単で便利です。
6つのルール
実際に使ってみると、これがどのオブジェクトを指しているのかが一番わかりにくいです。この記事では、6 種類のシナリオを分類し、これに対する 6 つの拘束ルールをまとめます。
1.new binding
newを使用してオブジェクトを作成する場合、クラス内のこれは何を参照しますか?
class Person { constructor(name){ this.name = name; } getThis(){ return this } } const xiaoMing = new Person("小明"); console.log(xiaoMing.getThis() === xiaoMing); // true console.log(xiaoMing.getThis() === Person); // false console.log(xiaoMing.name === "小明"); // true
上記の例では、 Person クラスは ES6 構文を使用して作成されました。 new キーワードを使用してオブジェクトを作成する過程で、これはシステムによって作成されたオブジェクト、つまり xiaoMing に自動的にバインドされます。
ルール 1: new キーワードを使用してオブジェクトを作成すると、これは作成されたオブジェクトにバインドされます。
2. 明示的なバインディング
シナリオ 2、call、apply、bind メソッドを使用して、このパラメーターを明示的にバインドします。
call メソッドに渡される最初のパラメータは、this によって参照されるオブジェクトです。
function foo() { console.log( this === obj ); // true console.log( this.a === 2 ); // true}const obj = { a: 2}; foo.call( obj );
明示的に渡す場合、this が指すオブジェクトは明らかであり、call、apply、または binding メソッドの最初のパラメーターです。
ルール 2: call、apply、または binding メソッドを使用して明示的にバインドされる場合、これが最初のパラメーターになります。
3. 暗黙的なバインディング
暗黙的なバインディングと明示的なバインディングの違いは、明示的なバインディングでは、関数またはメソッドが常に「所有者」であることを指定することです。この「所有者」は、直接呼び出される関数またはメソッド オブジェクトを指します。
例 1
最初に最も単純な例を見てみましょう。
function bar() { console.log( this === obj ); }const obj = { foo: function () { console.log( this === obj ); }, bar: bar }; obj.foo(); // trueobj.bar(); // true
関数 foo はオブジェクト obj に直接ぶら下がっており、関数 bar は外部で定義されてオブジェクト obj にぶら下がっています。関数がどこで定義されているかに関係なく、関数が最終的に呼び出されるとき、その「所有者」は obj になります。したがって、これは、関数が呼び出されたときに「所有者」obj を指します。
例 2
より深く理解するために、新しいオブジェクトに関数を再割り当てする場合を考えてみましょう。次の例を見てみましょう。
function bar() { console.log( this === obj1 ); // false console.log( this === obj2 ); // true}const obj1 = { foo: function () { console.log( this === obj1 ); // false console.log( this === obj2 ); // true }, bar: bar };const obj2 = { foo: obj1.foo, bar: obj1.bar }; obj2.foo(); obj2.bar();
この例では、obj1 の foo メソッドと bar メソッドが obj2 に割り当てられています。関数が呼び出されるとき、「所有者」は obj1 ではなく obj2 になります。したがって、これは obj2 を指します。
例 3
オブジェクトは複数のレベルでネストできます。この場合、関数が実行されるとき、関数の「所有者」は誰になりますか?
const obj1 = { obj2: { foo: function foo() { console.log( this === obj1 ); // false console.log( this === obj1.obj2 ); // true } } }; obj1.obj2.foo()
foo メソッド/関数の直接呼び出し元は obj1 ではなく obj2 であるため、関数の「所有者」は最も近い直接呼び出し元を指します。
例 4
如果一个方法/函数,在它的直接对象上调用执行,又同时执行了 call 方法,那么它是属于隐式绑定还是显式绑定呢?
const obj1 = { a: 1, foo: function () { console.log(this === obj1); // false console.log(this === obj2); // true console.log(this.a === 2); // true } };const obj2 = { a: 2}; obj1.foo.call(obj2); // true
由上,可以看出,如果显式绑定存在,它就不可能属于隐式绑定。
规则三:如果函数是挂在对象上执行的,这个时候系统会隐式的将 this 绑定为函数执行时的“拥有者”。
4.默认绑定
前一小段,讨论了函数作为对象的方法执行时的情况。本小段,要讨论的是,函数独立执行的情况。
在函数直接调用的情况下,this 绑定的行为,称之为默认绑定。
例一
为了简单起见,先讨论在浏览器的非严格模式的下绑定行为。
function foo() { console.log( this === window); // true} foo();
在上面的例子中,系统将 window 默认地绑定到函数的 this 上。
例二
在这里,先介绍一种我们可能会在代码中见到的显式绑定 null 的写法。
function foo() { console.log( this == window ); // true} foo.apply(null);
将例一默认绑定的情况,改为了显式绑定 null 的情况。
在实际开发中,我们可能会用到 apply 方法,并在第一个参数传入 null 值,第二个参数传入数组的方式来传递数组类型的参数。这是一种传统的写法,当然现在可以用 ES6 的写法来代替,但是这不在本文的讨论范围内。
在本例最需要关注的是,this 竟然指向的 window 而不是 null。个人测试的结果是,在函数独立调用时,或者显式调用,传入的值为 null 和 undefined 的情况下,会将 window 默认绑定到 this 上。
在函数多次调用,形成了一个调用栈的情况下,默认绑定的规则也是成立的。
例三
接着,探讨下严格模式下,this 的默认绑定的值。
"use strict"; function foo() { console.log( this === undefined ); } foo(); // true foo.call(undefined); // true foo.call(null); // false
在严格模式下,this 的默认绑定的值为 undefined。
规则四:在函数独立执行的情况下,严格模式 this 的默认绑定值为 undefined,否则默认绑定的值为 window。
5.箭头函数绑定
箭头函数实际上,只是一个语法糖,实际上箭头函数中的 this 实际上是其外层函数(或者 window/global 本身)中的 this。
// ES6 function foo() { setTimeout(() => { console.log(this === obj); // true }, 100); } const obj = { a : 1 } foo.call(obj); // ES5 function foo() { var _this = this; setTimeout(function () { console.log(_this === obj); // true }, 100); } var obj = { a : 1 } foo.call(obj);
规则五:使用箭头函数时,this 的绑定值和其外层的普通函数(或者 window/global 本身) this 绑定值相同。
6.系统或第三方绑定
在 JavaScript 中,函数是第一公民,可以将函数以值的方式,传入任何系统或者第三方提供的函数中。现在讨论,最后一种情况。当将函数作为值,传入系统函数或者第三方函数中时,this 究竟是如何绑定的。
我们在文章一开始提到的,两个按钮例子,系统自动将 this 绑定为点击的按钮。
function changeBackgroundColor() { console.log(this === btn1); // true} btn1.addEventListener('click',changeBackgroundColor);
接着测试系统提供的 setTimeout 接口在浏览器和 node 中绑定行为。
// 浏览器 setTimeout(function () { console.log(this === window); // true },0) // node setTimeout(function () { console.log(this === global); // false console.log(this); // Timeout },0)
很神奇的是,setTimeout 在 node 和浏览器中的绑定行为不一致。如果我们将 node 的中的 this 打印出来,会发现它绑定是一个 Timeout 对象。
如果是第三发提供的接口,情况会更加复杂。因为在其内部,会将什么值绑定到传入的函数的 this 上,事先是不知道的,除非查看文档或者源码。
系统或者第三方,在其内部,可能会使用前面的五种规则一种或多种规则,对传入函数的 this 进行绑定。所以,规则六,实际上一条在由前五条规则上衍生出来的规则。
规则六:调用系统或者第三方提供的接口时,传入函数中的 this 是由系统或者第三方绑定的。