2022年1月1日 JavaScript (JS Primer) メソッドとコールバック関数とArrow Function
コールバック関数はcallback()のようにただの関数として呼び出されます。
つまり、コールバック関数として呼び出すとき、この関数にはベースオブジェクトはありません。 そのためcallback関数のthisはundefinedとなります。
// `callback`関数を受け取り呼び出す関数 const callCallback = (callback) => { // `callback`を呼び出す実装 }; const obj = { method() { callCallback(function() { // ここでの `this` は`callCallback`の実装に依存する // `callback()`のように単純に呼び出されるなら`this`は`undefined`になる // `Function#call`などを使って特定のオブジェクトを指定するかもしれない // この問題を回避するために`const that = this`のような一時変数を使う }); } };
- Arrow Functionにおける
this
は呼び出し方の影響を受けません。 つまり、コールバック関数がどのように呼ばれるかという実装についてを考えることなくthis
を扱えます。
const Prefixer = { prefix: "pre", prefixArray(strings) { return strings.map((str) => { // `Prefixer.prefixArray()` と呼び出されたとき // `this`は常に`Prefixer`を参照する return this.prefix + "-" + str; }); } }; const prefixedStrings = Prefixer.prefixArray(["a", "b", "c"]); console.log(prefixedStrings); // => ["pre-a", "pre-b", "pre-c"]
メモ
- JavaScriptでは、関数を定義するためにfunctionキーワードを使います。 functionからはじまる文は関数宣言と呼びます。
- 関数宣言はfunctionから始まります。
- 関数名に()をつけることで、関数としてまとめた処理を呼び出します。
- 関数はオブジェクトのため、変数に代入できます。その場合も()を付けることで呼び出すことができます。
- 関数が値として扱える(変数に代入したりできる)ことをファーストクラス(ファンクション)と呼びます。
- ArrowFunctionは無名関数にのみ使用できます。
- コールバック関数にはベースオブジェクトがありません。
- コールバック関数に
this
を定義する場合はArrow Functionを使用しなければなりません。
Arrow Functionはthis
をbindできない
Arrow Functionで定義した関数ではthisを持つことが出来ないためcall
、apply
、bind
を使ったthis
の指定は単に無視されます。
これまで学んだようにArrowFunctionにthisが渡す暗黙的な値はありません。そのArrowFunctionに対して call
apply
bind
で this
の値を指定する事はできないのは当然ですね。
const fn = () => { return this; }; // Scriptコンテキストの場合、スクリプト直下のArrow Functionの`this`はグローバルオブジェクト console.log(fn()); // グローバルオブジェクト // callで`this`を`{}`にしようとしても、`this`は変わらない console.log(fn.call({})); // グローバルオブジェクト
- Arrow Functionのthisが参照する「自身の外側のスコープにあるもっとも近い関数のthisの値」はcallメソッドで変更できます。
const obj = { method() { const arrowFunction = () => { return this; }; return arrowFunction(); } }; // 通常の`this`は`obj.method`の`this`と同じ console.log(obj.method()); // => obj // `obj.method`の`this`を変更すれば、Arrow Functionの`this`も変更される console.log(obj.method.call("THAT")); // => "THAT"
ここではArrowFunctionに対してcall
を使っていないところがポイントです。obj.method
に対してcall
を使っているのでthis
の値を指定する事ができています。
まとめ
thisは状況によって異なる値を参照する性質を持ったキーワードです。
実行コンテキストやstrict modeなどによって結果が異なり、混乱の元となります。 そのため、メソッドではない通常の関数においては
this
を使うべきではありません。メソッドの中でコールバック関数を使う際は、静的に
this
の値が決まるようにArrowFunctionで書きましょう。this
が指し示すのはベースオブジェクトである事は揺るぎないです。
ES2015の仕様編集者であるAllen Wirfs-Brock氏もただの関数においては
this
を使うべきではないと述べている。
https://twitter.com/awbjs/status/938272440085446657
参考
JS primer メソッドとコールバック関数とArrow Function
2021年12月31日 JavaScript (JS Primer) 関数とthis
Arrow Functionとthis
- Arrow Functionで定義された関数やメソッドにおける
this
がどの値を参照するかは定義時(静的)に決まります。 - 一方、Arrow Functionではないfunctionキーワードの関数においては、
this
は呼び出し元に依存するため関数の実行時(動的)にthisの値が決まります。
Arrow Function内では、暗黙的な引数は受け付けられないため、thisが定義されません。
このときのthisは外側のスコープ(関数)のthisを参照します。
thisはECMAScriptのキーワードであるため、thisという変数を定義できません。
通常の変数と同様にthisがどの値を参照するかは静的(定義時)に決定されます。(静的スコープ)
つまり、Arrow Functionにおけるthisは「Arrow Function自身の外側のスコープに定義されたもっとも近い関数のthisの値」となります。
以下の例はfn関数の外側には関数がないためトップレベルのthisを参照して返していることを示しています。厳密等価演算子でthisと合致させていることで証明されています。
const fn = () => { // この関数の外側には関数は存在しない // トップレベルの`this`と同じ値 return this; }; console.log(fn() === this); fn() 出力結果 true [object Window] // thisのトップレベルであるwindowを参照
thisの値は、実行コンテキストが"Script"ならばグローバルオブジェクト(window)となり、"Module"ならばundefinedとなります。
以下のouter関数はthisを返すArrow Functionをreturnします。
この場合、outer関数のthisを参照するためundefinedとなります。
(outerは関数宣言であるため)
"use strict"; function outer() { // Arrow Functionで定義した関数を返す return () => { // この関数の外側には`outer`関数が存在する // `outer`関数に`this`を書いた場合と同じ return this; }; } // `outer`関数の返り値はArrow Functionにて定義された関数 const innerArrowFunction = outer(); console.log(innerArrowFunction()); // => undefined
復習
- 関数呼び出し時に呼び出し元のスコープの変数を参照する仕組みを動的スコープといいます。
- オブジェクトのプロパティが関数である場合、それをメソッドといいます。
Arrow Functionにthisを使えるのはメソッドだからという基本的なところをもう一度復習しました。
また、一つ上のコードブロックで結果がundefinedになるのはfunction宣言のスコープにthisが参照しにいっているからというのも以下の表から復習できました。
名前 | 関数 | メソッド |
---|---|---|
関数宣言( function fn(){} ) | ⭕️ | ❌ |
関数式( const fn = function(){} ) | ⭕️ | ⭕️ |
Arrow Function( const fn = () => {} ) | ⭕️ | ⭕️ |
メソッドの短縮記法( const obj = { method(){} ) | ❌ | ⭕️ |
参考
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を使うのがもっとも簡潔です。
参考
2021年12月27日 JavaScript (JS Primer) 関数とthis
thisが問題となるパターン
thisは所属するオブジェクトを直接書く代わりとして利用出来ますが、thisには大きく分けて2つの問題があります。
問題1: thisを含むメソッドを変数に代入した場合
問題2: コールバック関数とthis
本日は、1つ目の問題に触れていきます。
問題1: thisを含むメソッドを変数に代入した場合
thisは呼び出し時に定義されるため、基本的に呼び出し時にメソッドでなければundefinedになってしまいます。
以下の例ではpersonというベースオブジェクトから別のプロパティであるfullNameを参照するsayNameメソッドをsay変数に代入しています。
say変数を関数として呼び出す場合、実行時点でthisが定義されるためundefinedとなり、fullNameを参照出来ずにエラーが返っています。
"use strict"; const person = { fullName: "Brendan Eich", sayName: function() { // `this`は呼び出し元によって異なる return this.fullName; } }; // `sayName`メソッドは`person`オブジェクトに所属する // `this`は`person`オブジェクトとなる console.log(person.sayName()); // => "Brendan Eich" // `person.sayName`を`say`変数に代入する const say = person.sayName; // 代入したメソッドを関数として呼ぶ // この`say`関数はどのオブジェクトにも所属していない // `this`はundefinedとなるため例外を投げる say(); // => TypeError: Cannot read property 'fullName' of undefined
問題1の対処法
上記の問題の対処法はcall、apply、bindメソッドが挙げられます。
いずれも関数オブジェクトに用意されているメソッドで、this
を指定して関数を実行することが出来ます。
【問題1の対処法その1】 callメソッド
関数.call(thisの値, ...関数の引数);
callメソッドは第一引数にthisとしたい値を指定し、第二引数には呼び出す関数の引数を指定します。
以下のsay関数はpersonオブジェクトをthisで参照して実行するためにcallメソッドを使用している例です。
"use strict"; function say(message) { return `${message} ${this.fullName}!`; } const person = { fullName: "Brendan Eich" }; // `this`を`person`にして`say`関数を呼びだす console.log(say.call(person, "こんにちは")); // => "こんにちは Brendan Eich!" // `say`関数をそのまま呼び出すと`this`は`undefined`となるため例外が発生 say("こんにちは"); // => TypeError: Cannot read property 'fullName' of undefined
【問題1の対処法その2】 applyメソッド
- applyメソッドは第一引数にthisとする値を指定し、第二引数に関数の引数を配列として渡します。
やっていること自体はcallメソッドと同じで第二引数を配列を指定するかの違いがあるのみです。
以下の例では第二引数で指定した配列は、自動的に展開されてsay関数の仮引数messageに入ります。
"use strict"; function say(message, konbanha) { return `${konbanha} ${this.fullName}!`; } const person = { fullName: "Brendan Eich" }; // `this`を`person`にして`say`関数を呼びだす // callとは異なり引数を配列として渡す console.log(say.apply(person, ["こんにちは", "こんばんは"])); // => "こんにちは Brendan Eich!" // `say`関数をそのまま呼び出すと`this`は`undefined`となるため例外が発生 say("こんにちは"); // => TypeError: Cannot read property 'fullName' of undefined
また、どちらのメソッドもthis
の値が不要な場合はnull
を渡すのが一般的です。
【問題1の対処法その3】 bindメソッド
thisの値を束縛(bind)した新しい関数を作成します。
関数.bind(thisの値, ...関数の引数); // => thisや引数がbindされた関数
function say(message) { return `${message} ${this.fullName}!`; } const person = { fullName: "Brendan Eich" }; // `this`を`person`に束縛した`say`関数をラップした関数を作る const sayPerson = say.bind(person, "こんにちは"); console.log(sayPerson()); // => "こんにちは Brendan Eich!"
基本的にはメソッドを呼び出す事でこの問題を回避した方がよく、その上でどうしてもthis
を固定したいときにcall
apply
bind
メソッドを使うと良いでしょう。
参考
2021年12月25日 JavaScript (JS Primer) 関数とthis
関数宣言や関数式におけるthis
関数宣言・・・fuction yano () {}
関数式・・・ const yano = function() {}
- 関数宣言や関数式ではベースオブジェクトがないためthisはundefinedとなります。
- 関数の中に関数を定義して呼び出す場合も同じです。
strict modeではない状況でthis
がundefined
の場合は、this
がグローバルオブジェクトを参照するように変換される
([object Window]が返されます。)
注意点
strict modeではない状況でthisがundefinedの場合は、thisがグローバルオブジェクトを参照するように変換される問題があります。
strict modeは、このような意図しにくい動作を防止するために導入されています。 しかしながら、strict modeのメソッド以外の関数におけるthisはundefinedとなるため使い道がありません。
そのため、メソッド以外でthisを使う必要はありません。
メソッドにおけるthis
メソッドでは所属しているオブジェクトがベースオブジェクトとなりthisの参照先となります。
(JavaScriptではオブジェクトのプロパティとして指定される関数のことをメソッドと呼びます。)
以下の例ではメソッドの中で同じオブジェクトに所属している別のメソッドにthisでアクセスしています。
const person = { fullName: "Brendan Eich", sayName: function() { // `person.fullName`と書いているのと同じ return this.fullName; } }; // `person.fullName`を出力する console.log(person.sayName()); // => "Brendan Eich"
thisはペースオブジェクトであるpersonを参照するのでここでのthis.fullName;
と person.fullName
は同義です。
したがってthisの部分はベースオブジェクトであるpersonからも参照可能です。
オブジェクトは何重にもネストできますが、thisはベースオブジェクトを参照するというルールは同じです。
以下はオブジェクトのプロパティがネストした場合、ドット記法などでみた場合、一番左のobj1ではなく、メソッドから見てひとつ左のobj3となることを示しています。
ネストしたプロパティにアクセスするには、ドット記法もしくはブラケット記法でチェーンして書く必要があるので、ここでobj3にアクセスするにはobj1.obj2.obj3
となるわけです。
const obj1 = { obj2: { obj3: { method() { return this; } } } }; // `obj1.obj2.obj3.method`メソッドの`this`は`obj3`を参照 console.log(obj1.obj2.obj3.method() === obj1.obj2.obj3); // => true
覚えておきたい
JavaScriptではオブジェクトのプロパティとして指定される関数のことをメソッドと呼びます。
参考
2021年12月24日 JavaScript (JS Primer) 関数とthis
基本的にはメソッドの中で利用しますが、thisは読み取り専用のグローバル変数のようなものでどこにでも書けます。
thisの参照先は主に次の条件によって変化します。
- 実行コンテキストにおけるthis
- コンストラクタにおけるthis
- 関数とメソッドにおけるthis
- Arrow Functionにおけるthis
thisが実際に使われるのはメソッドにおいてです。
thisはトップレベルのスコープに書かれたグローバルオブジェクトを参照します。
実行コンテキストがscriptの場合はthisが指すのはwindowになります。 実行コンテキストがmoduleの場合はthisが指すのはundefinedになります。
- Windowオブジェクトとは、JavaScriptのオブジェクト階層最上位に位置するオブジェクトで、すべてのオブジェクトの親となります。
実行コンテキスト | thisが指す対象 |
---|---|
script | window |
module | undefined |
moduleで単純にグローバルオブジェクトを参照したい場合は、thisではなくglobalThisを使います。
// ブラウザでは`window`オブジェクト、Node.jsでは`global`オブジェクトを参照する
console.log(globalThis);
関数とメソッド
JavaScriptではオブジェクトのプロパティが関数である場合にそれをメソッドと呼びます。
const obj = { method1: function() { }, method2: () => { } };
- オブジェクトのプロパティはキーとバリューの組み合わせです。
短縮記法でこのように書けます。
const obj = { // メソッドの短縮記法で定義したメソッド method() { } }; };
メソッドの呼び出し方はこのようになります。
const obj = { // メソッドの定義 method() { } }; // メソッド呼び出し obj.method();
メソッドを短縮記法書いた場合はそれは関数には部類されず"メソッド"とされます。 thisは、アロー関数の場合にのみthisの挙動が異なるようです。
Arrow Function以外の関数におけるthis
- Arrow Function以外の関数(メソッドも含む)におけるthisの値は実行時に暗黙的に渡される引数のようなものです。
- 値自体は実行時に決まります。
- 関数におけるthisの参照先はベースオブジェクトとなります。
- ベースオブジェクトとは「メソッドを呼ぶ際に、そのメソッドのドット演算子またはブラケット演算子のひとつ左にあるオブジェクト」のことを言います。
- ベースオブジェクトがない場合のthisはundefinedとなります。
// 疑似的な`this`の値の仕組み // 関数は引数として暗黙的に`this`の値を受け取るイメージ function fn(暗黙的に渡されるthisの値, 仮引数) { console.log(this); // => 暗黙的に渡されるthisの値 } // 暗黙的に`this`の値を引数として渡しているイメージ fn(暗黙的に渡すthisの値, 引数);
function宣言ではベースオブジェクトがないためthisはundefinedになります。
Arrow Function以外の関数では、関数の定義だけを見てthisの値が何かということは決定できない点に注意が必要です。
参考
2021年12月23日 JavaScript (JS Primer) 関数とスコープ
クロージャーについての理解が曖昧だったのでJS primer以外の記事も読んでみました。
function User(){ // private member let name = "Jason"; // public member this.getName = function(){ return name; }; } let user01 = new User(); // 直接プライベートメンバにはアクセス出来ない console.log(user01.name); // -> undefined // パブリックメソッド経由でプライベートメンバにアクセスする console.log(user01.getName()); // -> "Jason"
- コード内のプライベートメンバ(ここではname)には、外側から参照できない」
- 「コード内のプライベートメンバ(ここではname)を参照するには、パブリックメソッド(ここではgetName)を経由するしかない」
上記の2点が重要でクロージャーを理解するのに必要な知識でした。 JS primerでは関数に状態を持たせる手段としてで書かれていましたがクロージャーこの記事を読んで理解を深められたので引用しました。
- getNameに代入されている関数は外側の変数nameを参照し続けています。
- クロージャーとは静的スコープとメモリ管理の仕組みを利用して、関数内から特定の変数を参照し続けることで関数が状態を持てる仕組みのことを言います。
ここまで数日にあたりクロージャーと向き合ってきましたが、どの記事の記述にもあるように言葉で伝えるのは難しいという事です。しかし、理解してしまえばそこまで難しくはないので根気よく向き合うと良いかもしれません。
クロージャーの用途
これまでまとめてきた中で高階関数に関してだけ記載してこなかったのでここでは高階関数のみをまとめていきます。
高階関数の一部分としてクロージャーを使う場合のコード例に100の値も追加して記述しました。こうして使い回しが効く関数にできるのもクロージャーの用途です。
function greaterThan(n) { return function(m) { return m > n; }; } // 5より大きな値かを判定する関数を作成する const greaterThan5 = greaterThan(5); const greaterThan100 = greaterThan(100); console.log(greaterThan100(101)); // => true console.log(greaterThan5(4)); // => false console.log(greaterThan5(5)); // => false console.log(greaterThan5(6)); // => true
- greaterThan()に引数を渡して実行した結果を変数に代入します。 nに引数に指定した数字が入ります。
- console.log()を実行した際は引数がfunctionに渡って実行されています。