どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。
前回のシリーズでは、第2章に相当する考察をお届けしました。
引き続き、第3章をお送りします。
目次と日程
第3章は「関数の扱い」です。いよいよJavaScriptの真髄に迫る感じです。
- 第1章 JavaScriptに慣れ親しむ
- 第2章 変数のスコープ
- 第3章 関数の扱い
- 第4章 オブジェクトとプロトタイプ
- 第5章 配列とディクショナリ
- 第6章 ライブラリとAPI設計
- 第7章 並行処理
LT大会は2021年9月26日です。がんばりましょう。
今日はさっそく1回目で、「関数、メソッド、コンストラクタの、呼び出しの違いを理解する」 をお届けします。
テキスト第3章「関数の扱い」の項目18に対応しています。
今日のアジェンダ
- 関数の利用パターン1(呼び出し)
- 関数の利用パターン2(thisを使ったグローバル関数)
- 関数の利用パターン3(コンストラクタ)
わたしもぼんやり思っていたことですが、他の言語と比べて、JavaScriptはなんでもかんでも関数に仕事をさせています。
たとえばVBAでは、戻り値を返すプロシージャのことを「関数」と呼び、処理をするだけのプロシージャとは区別しています。
ほかにも、Javaのクラスである、コンストラクタ(インスタンスを生成する処理)、メソッドなどは、関数とは別の処理です。
JavaScriptの関数は、具体的にどんな仕事をしているのでしょうか。
「そんなのあたりまえじゃないか」と思うことが多いと思いますが、その先に待ってるJavaScriptの重要な仕様と戦うための基礎固めです。
関数の利用パターン1(呼び出し)
JavaScrtiptのメソッドは、関数であるオブジェクトのプロパティにすぎません。
メソッド定義も、プロパティに無名関数が代入されていることの、糖衣構文(シンタクティックシュガー)です。
function myFunction3_18_02() {
const obj = {
userName: 'Bob',
hello: function () {
return `hello, ${this.userName}`;
},
greeting() {
return `How are you, ${this.userName}`;
}
};
console.log(obj);
// { userName: 'Bob',
// hello: [Function: hello],
// greeting: [Function: greeting] }
}
メソッドHelloから、objのプロパティ[userName]にアクセスするために、thisを付けました。
thisは、書かれている場所で、オブジェクト自分自身を表しますが、このように、オブジェクトをコピーすると、興味深い結果が返ります。
つまり、書かれている場所ではなく、呼び出された場所、とも言えそうです。
function myFunction3_18_03() {
const obj = {
hello() {
return `hello, ${this.userName}`;
},
userName: 'Bob'
};
const obj2 = {
hello: obj.hello,
userName: 'Tom'
}
console.log(obj2.hello()); // hello, Tom
}
これは、関数の呼び出し式(Call Expression)という仕組みからきています。
メソッドを呼び出すと、Call Expressionがthisの結合を決めています 。
thisに結合される値は、レシーバ(receiver)とも呼ばれます。値[Tom]単体ではなく、オブジェクト[obj2]がレシーバです。
ルックアップという言葉はスコープチェーンの動きと同様に、名前結合をするために参照先を探しにいく とイメージしてください。
obj.hello()を実行したときの流れをみてみましょう。
- objのhelloプロパティをルックアップする → オブジェクト[obj]のなかに、プロパティ[hello]があるかどうか探しにいく
- thisがあるので、レシーバとしてthisをオブジェクト[obj]に結合する
- レシーバを呼び出す
同様に、obj2.hello()を実行したときの流れをみてみましょう。
- obj2のhelloプロパティをルックアップする → オブジェクト[obj2]のなかに、プロパティ[hello]があるかどうか探しにいく
- プロパティ[hello]には、値[obj.hello]が定義されている
- obj.hello(戻り値に[
hello, ${this.userName}
]をもつ関数)を返す - thisがあるので、レシーバとしてthisをオブジェクト[obj2]に結合する
- レシーバを呼び出す
オブジェクトのメソッドを呼び出すと、オブジェクトのプロパティをルックアップし、そのときのオブジェクトがメソッドのレシーバとして使われます。
レシーバとして結合したオブジェクトに、結合する相手がいなかったときの、悲しいundefinedを理解しましょう。
function myFunction3_18_04() {
const obj = {
hello() {
return `hello, ${this.userName}`;
},
userName: 'Bob'
};
const obj2 = {
hello: obj.hello,
// userName: 'Tom'
}
console.log(obj2.hello()); // hello, undefined
}
関数の利用パターン2(thisを使ったグローバル関数)
このthisのはたらきを利用して、どこからでも呼び出せる関数を定義し、thisを置いてみます。
共通する処理を関数化するということで、便利かもしれません。
ただし、最終行のように関数単体で呼び出すと、undefinedが返ってしまうので、少々問題ありですね。
function myFunction3_18_05() {
function hello() {
return `hello, ${this.userName}`;
}
const obj1 = {
hello: hello,
userName: 'Bob'
}
const obj2 = {
hello: hello,
userName: 'Tom'
}
console.log(obj1.hello()); // hello, Bob
console.log(obj2.hello()); // hello, Tom
console.log(hello()); //hello, undefined
}
先ほど、単体で呼び出した関数では、レシーバにグローバルオブジェクトが結合されます。
グローバルオブジェクトにuserNameがあれば、結合しますが、このような書き方は、グローバル汚染につながり、決してよくありません。
function myFunction3_18_06() {
//宣言をしないことで、グローバルオブジェクトのプロパティに格納しています
userName = 'John';
function hello() {
return `hello, ${this.userName}`;
}
console.log(hello()); //hello, John
}
‘use strict’モードでは、thisのデフォルトの結合先がundefinedとなります。
function myFunction3_18_07() {
'use strict'
//宣言をしないことで、グローバルオブジェクトのプロパティに格納しています
userName = 'John';
function hello() {
return `hello, ${this.userName}`;
}
console.log(hello()); //ReferenceError: userName is not defined
}
use strictのリストに追加しておきましょう。
strictモードで効く主な厳格
- 暗黙的なグローバル変数の禁止
- 代入不可なプロパティへの代入の禁止
- 削除できないプロパティの削除の禁止
- 関数の引数名の重複の禁止
- 幾つかの識別子は予約語にするため使用禁止(staticとか)
- 8進数表記の禁止
- eval 変数、arguments 変数の宣言禁止
- with 禁止
- ブロックスコープ内の関数定義
- thisのデフォルト結合はundefined ← New
関数の利用パターン3(コンストラクタ)
JavaScriptのコンストラクタは、メソッドや関数と同じように、functionキーワードで定義できます。
new演算子によってインスタンスを生成するクラスは、V8以前の講座で習いましたね。
function myFunction3_18_08() {
function User(name, passwordHash) {
this.name = name;
this.passwordHash = passwordHash;
}
const name = 'Ivy';
const passwordHash = '0e0101';
const u = new User(name, passwordHash);
console.log(u); //{ name: 'Ivy', passwordHash: '0e0101' }
}
コンストラクタが実行されると、できたてホヤホヤのオブジェクトをthisとして渡します。
変数[u]には、Userから生成されたオブジェクトが格納されていますが、元々thisとして書かれていた場所には、できたてホヤホヤのオブジェクトが結合されています。
クラス構文が登場してから、JavaScriptのクラスと関数の関係性が見え辛くなりましたが、もとはこのような書き方をしていました。
そんなクラス構文も、functionキーワードで記述していた関数のただの糖衣構文であり、JavaScriptがクラスベースになったわけではありません。
JavaScriptはプロトタイプベースですが、あとでたっぷり仲良くなりましょう。
まとめ
以上で、「関数、メソッド、コンストラクタの、呼び出しの違いを理解する」をお届けしました。
thisは仲良くなりづらいものです。
しかし、このように関数と一緒に考えると、少し理解が深まった気がしますね。
むかし習ったことの復習もでてきて、楽しかったです。
次回は、「高階関数を快適に使えるようにしよう」 をお届けします。
このシリーズの目次
- [EffectiveJavaScript輪読会3]関数、メソッド、コンストラクタの、呼び出しの違いを理解する
- [EffectiveJavaScript輪読会3]高階関数を快適に使えるようにしよう
- [EffectiveJavaScript輪読会3]カスタムレシーバ付きでメソッドを呼び出すにはcallを使おう
- [EffectiveJavaScript輪読会3]いくつでも引数をとれる関数を呼び出すにはapplyを使おう
- [EffectiveJavaScript輪読会3]可変長引数関数を作るには、argumentsを使う
- [EffectiveJavaScript輪読会3]argumentsオブジェクトを書き換えない
- [EffectiveJavaScript輪読会3]argumentsへのリファレンスは変数に保存する