jeremygo

jeremygo

我是把下一颗珍珠串在绳子上的人

このことを五つのルールから紹介します

記事の始まりに先立ち、まずは問題を解いてみましょう:

window.name = "window";

function User(name) {
  this.name = name;
  this.greet1 = function() {
    console.log(this.name);
  };
  this.greet2 = function() {
    return function() {
      console.log(this.name);
    };
  };
  this.greet3 = function() {
    return () => console.log(this.name);
  };
}

const UserA = new User("UserA");
const UserB = new User("UserB");

UserA.greet1();
UserA.greet1.call(UserB);

UserA.greet2()();
UserA.greet2.call(UserB)();

UserA.greet3()();
UserA.greet3.call(UserB)();

自分で答えを書いてみてもいいですし、答えを確認したい場合はそのまま記事の最後に移動してください。最後に解析も行います。

この記事では、以下の五つのルールから JavaScript における this を紹介します:

  • 暗黙のバインディング
  • 明示的なバインディング
  • new バインディング
  • レキシカルバインディング
  • デフォルトバインディング

暗黙のバインディング#

まずはコードを見てみましょう:

const user = {
    name: 'Jeremy',
    greet () {
        console.log(`私の名前は ${this.name} です`)
    }
}

user オブジェクトの greet メソッドを呼び出してみましょう:

user.greet();  // 私の名前は Jeremy です

user オブジェクトを通じて greet メソッドを呼び出すと、greet 内の thisuser オブジェクトを指していることがわかります。これが暗黙のバインディングのポイントです: 関数がコンテキストオブジェクトを持つ場合、暗黙のバインディングは関数呼び出しの this をそのコンテキストオブジェクトにバインドします。したがって、ここでの this.nameuser.name と同じです。

少し拡張してみましょう:

const user = {
    name: 'Jeremy',
    greet () {
        console.log(`私の名前は ${this.name} です`)
    },
    son: {
        name: 'lap',
        greet () {
            console.log(`私の名前は ${this.name} です`)
        }
    }
}

user.son.greet() を呼び出した結果はあなたの期待通りですか?

では、コードを書き換えてみましょう:

function greet () {
    console.log(`私の名前は ${this.name} です`)
}

const user = {
    name: 'Jeremy'
}

greet を独立した関数に分けました。では、greet 内の thisuser オブジェクトに指すようにするにはどうすればよいでしょうか?

明示的なバインディング#

JavaScript では、すべての関数にはこの機能を実現するためのメソッドがあります(つまり、this の指し示す先を変更することができます)。それが call です:

call() メソッドは、指定された this 値を持つ関数を呼び出し、提供された引数(引数のリスト)をそれぞれ渡します。

したがって、次のように呼び出すことができます:

greet.call(user)

これが 明示的なバインディング の意味です。私たちは明示的に(.call を使用して)this の指し示す先を指定しました。

もし greet にいくつかの引数を渡したい場合、call メソッドの残りの引数を使用する必要があります:

function greet (l1, l2, l3) {
    console.log(`私の名前は ${this.name} で、${l1}、${l2}、${l3} を知っています`)
}

const user = {
    name: 'Jeremy'
}

const languages = ['JavaScript', 'Java', 'PHP']

greet.call(user, languages[0], languages[1], languages[2]) // 私の名前は Jeremy で、JavaScript、Java、PHP を知っています

これらのコードを実際に実行すると、languages 配列の要素を一つずつ渡すのが面倒だと気づくでしょう。この場合、より良い選択肢があります。それが .apply です:

apply() メソッドは、指定された this 値を持つ関数を呼び出し、配列(または [配列のようなオブジェクト](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Indexed_collections#Working_with_array-like_objects))として提供された引数を渡します。

.apply.call の唯一の違いは、引数の渡し方です。したがって、次のように呼び出すことができます:

greet.apply(user, languages) // 私の名前は Jeremy で、JavaScript、Java、PHP を知っています

最後に紹介するのは .bind です:

bind() メソッドは、新しい関数を作成し、呼び出すときに this キーワードを提供された値に設定します。そして、新しい関数を呼び出すときに、指定された引数リストを元の関数の引数の先頭に追加します。

.bind.call と似ていますが、違いは .call は即座に呼び出されるのに対し、.bind は新しい関数を返し、後で呼び出すことができます:

const newFn = greet.bind(user, languages[0], languages[1], languages[2])
newFn() // 私の名前は Jeremy で、JavaScript、Java、PHP を知っています

new バインディング#

新しいコードを見てみましょう:

function User (name) {
    this.name = name
}

const me = new User('Jeremy')
console.log(me.name) // Jeremy

new を使って User を呼び出すと、新しいオブジェクトが構築され、そのオブジェクトが User 呼び出しの this にバインドされます。

JavaScriptnew の使い方は、従来のクラスベースの言語と似ていますが、内部メカニズムは全く異なります。new を使って関数を呼び出すと、次の操作が実行されます:

  • 新しいオブジェクトを作成
  • このオブジェクトをプロトタイプにリンク
  • このオブジェクトを関数呼び出しの this にバインド
  • もしその関数が他のオブジェクトを返さなければ、この新しいオブジェクトを返す

レキシカルバインディング#

上記で紹介した四つのルールは、すべての通常の関数をカバーしていますが、ES6 では特別な関数が導入されました。それがアロー関数です。

アロー関数式 の構文は関数式よりも短く、独自の thisargumentssuper または new.target を持ちません。これらの関数式は、本来匿名関数が必要な場所に適しており、コンストラクタ関数として使用することはできません。

アロー関数は独自の this を持たず、外側の(関数またはグローバル)スコープに基づいて this を決定します。

では、コードを書き換えてみましょう:

const user = {
    name: 'Jeremy',
    languages: ['JavaScript', 'Java', 'PHP'],
    greet () {
        return function () {
            console.log(this.name);
        }
    }
}

greet メソッド内で関数を返しましたが、user.greet()() を呼び出すと、返されるのは undefined です。

この理由は、返された関数を呼び出すときにバインドされたコンテキストオブジェクトがないため(デフォルトで window になります)です。したがって、直接的な解決策として 明示的なバインディング を使用することを考え、コードを次のように変更します:

const user = {
    name: 'Jeremy',
    languages: ['JavaScript', 'Java', 'PHP'],
    greet () {
        return function () {
            console.log(this.name);
        }.bind(this)
    }
}

user.greet()() // Jeremy

では、アロー関数 を使って書き換えた場合はどうなるでしょうか?

const user = {
    name: 'Jeremy',
    languages: ['JavaScript', 'Java', 'PHP'],
    greet () {
        return () => {
            console.log(this.name);
        }
    }
}

user.greet()() // Jeremy

アロー関数の this の探索ルールは、実際には 変数探索 に似ています。ES6 以前から、ほぼ同等のパターンを使用していました:

var user = {
    name: 'Jeremy',
    languages: ['JavaScript', 'Java', 'PHP'],
    greet () {
        var self = this;
        return function () {
            console.log(self.name);
        }
    }
}

user.greet()() // Jeremy

同じ関数やプログラム内で、これら二つのスタイルを混在させない方が良いです。そうしないと、コードがより難しくなり、メンテナンスも大変になります。

デフォルトバインディング#

最後に、このコードを再度見てみましょう:

function greet () {
    console.log(`私の名前は ${this.name} です`)
}

const user = {
    name: 'Jeremy'
}

もし greet を直接呼び出したら、何が起こるでしょうか?

greet() // 私の名前は undefined です

これが最後のルールを引き起こします。もし 暗黙のバインディング(オブジェクト呼び出し)も 明示的なバインディング.call.apply.bind)もなく、new バインディングもない場合、JavaScript はデフォルトで thiswindow オブジェクトに指し示します(したがって、デフォルトバインディングは ウィンドウバインディング とも呼ばれます):

window.name = 'window'

function greet () {
    console.log(`私の名前は ${this.name} です`)
}

const user = {
    name: 'Jeremy'
}

greet() // window

ES5 では、厳密モードを有効にすると、JavaScriptthisundefined に保ちます。

まとめ#

this の指し示す先を判断するためのプロセスをまとめてみましょう:

  • まず、関数がどこで呼び出されているかを確認します。
  • 関数はオブジェクトを通じて呼び出されていますか(. の左側がオブジェクト)?もしそうなら、this はそのオブジェクトを指します。そうでなければ、次に進みます。
  • 関数は .call.apply、または .bind を通じて呼び出されていますか?もしそうなら、this は指定されたコンテキストオブジェクトを指します。そうでなければ、次に進みます。
  • 関数は new キーワードを使って呼び出されていますか?もしそうなら、this は新しく作成されたオブジェクトを指します。そうでなければ、次に進みます。
  • 関数はアロー関数ですか?もしそうなら、this はアロー関数の外側の最初の非アロー関数を指します。そうでなければ、次に進みます。
  • 実行環境は厳密モードですか?もしそうなら、thisundefined です。そうでなければ、次に進みます。
  • thiswindow オブジェクトを指します。

最後に、記事の冒頭の問題に戻り、実行結果を示します:

window.name = "window";

function User(name) {
  this.name = name;
  this.greet1 = function() {
    console.log(this.name);
  };
  this.greet2 = function() {
    return function() {
      console.log(this.name);
    };
  };
  this.greet3 = function() {
    return () => console.log(this.name);
  };
}

const UserA = new User("UserA");
const UserB = new User("UserB");

UserA.greet1();  // UserA
UserA.greet1.call(UserB); // UserB

UserA.greet2()();  // window
UserA.greet2.call(UserB)(); // window

UserA.greet3()();  // UserA
UserA.greet3.call(UserB)();  // UserB

UserAUserB はそれぞれ new で構築されているため、対応する name はそれぞれ UserAUserB です。

  • UserA.greet1() : 最初に greet1UserA によって呼び出されるため、greet1 内の thisUserA を指し、したがって出力は UserA です。
  • UserA.greet1.call(UserB)greet1.call を通じて呼び出され、指定されたオブジェクトは UserB なので、出力は UserB です。
  • UserA.greet2()():最初に greet2UserA によって呼び出され、バインドされたコンテキストオブジェクトのない関数を返すため、出力は window です。
  • User.greet2.call(UserB)():ここで gree2.call を通じて UserB によって呼び出されますが、同様にバインドされたコンテキストオブジェクトのない関数を返すため、出力は依然として window です。
  • UserA.greet3()():ここで返されるのはレキシカルバインディングのアロー関数で、バインドされたコンテキストオブジェクトは UserA なので、出力は UserA です。
  • UserA.gree3.call(UserB)():ここでもアロー関数が返され、バインドされたコンテキストオブジェクトは .call で指定された UserB なので、出力は UserB です。

参考リンク:

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。