6時だョ!!全員集合!!

Rails・JavaScrictを中心にアウトプットします。

2021年12月30日 JavaScript (JS Primer) 関数とthis

thisが問題となるパターン

thisは所属するオブジェクトを直接書く代わりとして利用出来ますが、thisには大きく分けて2つの問題があります。

問題1: thisを含むメソッドを変数に代入した場合
問題2: コールバック関数とthis

本日は、2つ目の問題に触れていきます。

問題2: コールバック関数とthis

コールバック関数をfunctionを用いて定義した場合、コールバック関数は無名関数であり、callback()のように呼び出している状態になります。なのでコールバック関数のベースオブジェクトはundefinedになってしまいます。

"use strict";
// strict modeを明示しているのは、thisがグローバルオブジェクトに暗黙的に変換されるのを防止するため
const Prefixer = {
    prefix: "pre",
    /**
     * `strings`配列の各要素にprefixをつける
     */
    prefixArray(strings) {
        return strings.map(function(str) {
            // コールバック関数における`this`は`undefined`となる(strict mode)
            // そのため`this.prefix`は`undefined.prefix`となり例外が発生する
            return this.prefix + "-" + str;
        });
    }
};
// `prefixArray`メソッドにおける`this`は`Prefixer`
Prefixer.prefixArray(["a", "b", "c"]); // => TypeError: Cannot read property 'prefix' of undefined

直接無名関数としてコールバック関数を定義せず一度変数に代入してから呼び出しても結果は同じです。

"use strict";
// strict modeを明示しているのは、thisがグローバルオブジェクトに暗黙的に変換されるのを防止するため
const Prefixer = {
    prefix: "pre",
    prefixArray(strings) {
        // コールバック関数は`callback()`のように呼び出される
        // そのためコールバック関数における`this`は`undefined`となる(strict mode)
        const callback = function(str) {
            return this.prefix + "-" + str;
        };
        return strings.map(callback);
    }
};
// `prefixArray`メソッドにおける`this`は`Prefixer`
Prefixer.prefixArray(["a", "b", "c"]); // => TypeError: Cannot read property 'prefix' of undefined

対処法: thisを一時変数へ代入する

"use strict";
const Prefixer = {
    prefix: "pre",
    prefixArray(strings) {
        // `that`は`prefixArray`メソッド呼び出しにおける`this`となる
        // つまり`that`は`Prefixer`オブジェクトを参照する
        const that = this;
        return strings.map(function(str) {
            // `this`ではなく`that`を参照する
            return that.prefix + "-" + str;
        });
    }
};
// `prefixArray`メソッドにおける`this`は`Prefixer`
const prefixedStrings = Prefixer.prefixArray(["a", "b", "c"]);
console.log(prefixedStrings); // => ["pre-a", "pre-b", "pre-c"]

thisがペースオブジェクトを参照できるスコープでconst that = this と代入することで、その後のコールバック関数内でthatを使用したときにPrefixerを呼び出せます。 また代入する以外にも以下のように第二引数にthisとなる値を渡すことでも解決出来ます。

"use strict";
const Prefixer = {
    prefix: "pre",
    prefixArray(strings) {
        // `Array#map`メソッドは第二引数に`this`となる値を渡せる
        return strings.map(function(str) {
            // `this`が第二引数の値と同じになる
            // つまり`prefixArray`メソッドと同じ`this`となる
            return this.prefix + "-" + str;
        }, this);
    }
};
// `prefixArray`メソッドにおける`this`は`Prefixer`
const prefixedStrings = Prefixer.prefixArray(["a", "b", "c"]);
console.log(prefixedStrings); // => ["pre-a", "pre-b", "pre-c"]

この書き方ができるのはArray#mapメソッドなどはthisとなる値を引数として渡せる仕組みを持っているからです。
こうする事でthisを呼び出しているスコープがコールバック関数内ではなくなるため、thisはPrefixerを参照できます。

そもそもメソッド呼び出しとその中でのコールバック関数におけるthisが変わってしまうのが問題でした。
ES2015ではthisを変えずにコールバック関数を定義する方法として、Arrow Functionが導入されました。

対処法: Arrow Functionでコールバック関数を扱う

通常の関数とメソッドは呼び出し時に暗黙的にthisの値を受け取り、関数内のthisはその値を参照します。

対してArrow Function暗黙的なthisの値を受け取らずにスコープチェーンの仕組みと同様に外側の関数を探索します。

"use strict";
const Prefixer = {
    prefix: "pre",
    prefixArray(strings) {
        return strings.map(str => 
            // Arrow Function自体は`this`を持たない
            // `this`は外側の`prefixArray`関数が持つ`this`を参照する
            // そのため`this.prefix`は"pre"となる
             this.prefix + "-" + str
        );
    }
};
// このとき、`prefixArray`のベースオブジェクトは`Prefixer`となる
// つまり、`prefixArray`メソッド内の`this`は`Prefixer`を参照する
const prefixedStrings = Prefixer.prefixArray(["a", "b", "c"]);
console.log(prefixedStrings); // => ["pre-a", "pre-b", "pre-c"]
  • Arrow Functionはこの暗黙的なthisの値を受け取りません。
  • 暗黙的なthisを受け取らないので外側の関数に値を探しにいきます。
  • Arrow Functionで定義したコールバック関数は呼び出し方には関係なく、常に外側の関数のthisをそのまま利用します。

まとめ   

コールバック関数内でのthisの対処法としてthisを代入する方法を紹介しましたが、 ES2015からはArrow Functionを使うのがもっとも簡潔です。

参考

JS primer 関数とthis