どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。
前回のシリーズでは、第3章終わりと、第4章始めの考察をお届けしました。
引き続き、第4章をお送りします。
目次と日程
第4章後半は「オブジェクトとプロトタイプ」です。クラスベースではなく、プロトタイプベースを採用した、JavaScriptのアイデンティティを追及する日々です。
- 第1章 JavaScriptに慣れ親しむ
- 第2章 変数のスコープ
- 第3章 関数の扱い
- 第4章 オブジェクトとプロトタイプ
- 第5章 配列とディクショナリ
- 第6章 ライブラリとAPI設計
- 第7章 並行処理
LT大会は2021年10月31日です。がんばりましょう。
今日はさっそく1回目で、「newに依存しないコンストラクタの作り方」 をお届けします。
テキスト第4章「オブジェクトとプロトタイプ」の項目33に対応しています。
今日のアジェンダ
- new演算子を付けわすれると
- new演算子を忘れてもいいコードにする
- 可変長引数に対応する
new演算子を付けわすれると
おさらいもかねて、コンストラクタであるUser関数を定義して、呼び出します。
コンストラクタ関数は、new演算子で呼び出すことを前提としています。
作成したオブジェクト(インスタンス)に、名前とパスワードハッシュをプロパティとして格納します。
コンストラクタ関数Userのレシーバは、新しく生成されたオブジェクトです。
function myFunction5_33_01() {
/**
* コンストラクタ
* @param {string} name 名前
* @param {string} passwordHash ハッシュ
*/
function User(name, passwordHash) {
this.name = name;
this.passwordHash = passwordHash;
}
const u = new User('tsujike', 'hogehoge');
console.log(u); //{ name: 'tsujike', passwordHash: 'hogehoge' }
console.log(u.name); //tsujike
}
しかし、コンストラクタ関数に、new演算子をつけ忘れると、レシーバがグローバルオブジェクトになります。
これは、単に関数Userが実行され、関数User内に記述されているthisが、グローバルオブジェクトを示すからです。
グローバルオブジェクトに、nameとpasswodHashというプロパティを追加してしまう、最悪なグローバル汚染になります。
function myFunction5_33_02() {
/**
* コンストラクタ
* @param {string} name 名前
* @param {string} passwordHash ハッシュ
*/
function User(name, passwordHash) {
this.name = name;
this.passwordHash = passwordHash;
}
const u = User('tsujike', 'hogehoge');
console.log(u); //undefined
console.log(this.name); //tsujike
}
use strictモードでは、グローバルオブジェクトへの結合を禁止する(undefinedを結合する)はずですが、new演算子を付けないコンストラクタ関数の呼び出しは、例外をはくようです。
function myFunction5_33_03() {
'use strict'
/**
* コンストラクタ
* @param {string} name 名前
* @param {string} passwordHash ハッシュ
*/
function User(name, passwordHash) {
this.name = name;
this.passwordHash = passwordHash;
}
const u = User('tsujike', 'hogehoge');
//TypeError: Cannot set property 'name' of undefined
}
new演算子を忘れてもいいコードにする
まぁ、コンストラクタ関数を呼び出すときは、new演算子を忘れないでねという話です。
しかし、忘れることもあるので、new演算子を忘れても動くようにしてみよう、というのが書籍の狙いです。
方法として、「レシーバの値が、Userのインスタンスとして正しいものかチェックする」というアプローチです。
一致しなければ、newしようというテクニックです。
function myFunction5_33_04() {
/**
* コンストラクタ
* @param {string} name 名前
* @param {string} passwordHash ハッシュ
*/
function User(name, passwordHash) {
if (!(this instanceof User)) {
return new User(name, passwordHash);
}
this.name = name;
this.passwordHash = passwordHash;
}
const u = User('tsujike', 'hogehoge');
console.log(u); //undefined
console.log(this.name); //undefined
console.log(u.name); //tsujike
}
このような書き方をすれば、newをつけても(コンストラクタとして呼び出しても)つけ忘れても(普通の関数として呼び出しても)、必ずUser.prototypeを継承したオブジェクトになります。
function myFunction5_33_05() {
/**
* コンストラクタ
* @param {string} name 名前
* @param {string} passwordHash ハッシュ
*/
function User(name, passwordHash) {
if (!(this instanceof User)) {
return new User(name, passwordHash);
}
this.name = name;
this.passwordHash = passwordHash;
}
const funcU = User('tsujike', 'hogehoge');
const constructorU = new User('tsujike', 'hogehoge');
console.log(funcU instanceof User); //true
console.log(constructorU instanceof User); //true
}
可変長引数に対応する
この、newを忘れてもいいよverのコンストラクタ関数は、可変長引数に対応できません。(returnに記述しているnew演算子付きの関数呼び出しの引数の話です)
レシーバを指定するapplyメソッドに代わるものが無いからです。
なので、ES5から実装された、Object.create()メソッドを使うテクニックがあります。
function myFunction5_33_06() {
/**
* コンストラクタ
* @param {string} name 名前
* @param {string} passwordHash ハッシュ
*/
function User(name, passwordHash) {
const self = this instanceof User
? this
: Object.create(User.prototype);
self.name = name;
self.passwordHash = passwordHash;
return self;
}
const funcU = User('tsujike', 'hogehoge');
const constructorU = new User('tsujike', 'hogehoge');
console.log(funcU); //{ name: 'tsujike', passwordHash: 'hogehoge' }
console.log(constructorU); //{ name: 'tsujike', passwordHash: 'hogehoge' }
}
書籍では、このように書かれていました。
コンストラクタはnewを忘れてもいいような書き方で待ち受けておこう new演算子で呼ばれるべきなら、ドキュメントなどで注意を促そう
が、しかし、クラス構文で書くことと、「new演算子は必ず付ける!」という、コーディングガイドラインでいいと感じました。。。
まとめ
以上で、「newに依存しないコンストラクタの作り方」をお届けしました。
次回は、「メソッドをプロトタイプに格納しよう」「プライベートデータの格納にはクロージャを使おう」 の2本をお届けします。