どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。
前回のおさらい
前回は、「柔軟なインターフェイスのために、構造的な型付けを使う」をお届けしました。
今日は6回目で、「配列と「配列のようなもの」を区別しよう」をお届けします。
テキスト第6章「ライブラリとAPI設計」の項目58に対応しています。
今日のアジェンダ
- 多重定義とは
- 型の判定
- ユーザー定義型
多重定義とは
多重定義(オーバーロード)とは、同名のメソッドを定義することです。
Javaでは、同じメソッド名の中からどのメソッドを呼び出すかは、指定する引数の型や数、順番を元にコンパイラーが判断してくれますが、JavaScriptでは、最後に読まれたメソッドを呼び出します。
function myFunction8_58_01() {
/** クラスBitVector */
class BitVector {
constructor() {
this.bit = [];
}
/** bitを判定するメソッド */
enable(x) {
if (typeof x === 'number') this.enableBit(x);
}
/** bitを判定するメソッド */
enable(x) {
for (let i = 0; i < x.length; i++) {
this.enableBit(x[i]);
}
}
/** bitをプロパティに格納するメソッド */
enableBit(x) {
this.bit.push(x);
}
/** ビット配列に引数があるか返すメソッド */
bitAt(x) {
return this.bit.includes(x) ? 1 : 0;
}
}
const bits = new BitVector();
bits.enable(4);
bits.enable([1, 3, 8, 17]);
console.log(bits.bitAt(4)); // => 0 ←正しくない結果
console.log(bits.bitAt(8)); // => 1
console.log(bits.bitAt(9)); // => 0
}
複数の引数を受け取りたいばあいは、このように、メソッド内で引数の判定を行うといいでしょう。(このようなメソッド、複数のシグニチャを許容するメソッドもまた、多重定義と呼びます)
function myFunction8_58_02(){
/** クラスBitVector */
class BitVector {
constructor() {
this.bit = [];
}
/** bitを格納するメソッド */
enable(x) {
if (typeof x === 'number') {
this.enableBit(x);
} else { //xが配列のようなものと仮定する
for (let i = 0; i < x.length; i++) {
this.enableBit(x[i]);
}
}
}
/** ビットを格納するメソッド */
enableBit(x) {
this.bit.push(x);
}
/** ビット配列に引数があるか返すメソッド */
bitAt(x) {
return this.bit.includes(x) ? 1 : 0;
}
}
const bits = new BitVector();
bits.enable(4);
bits.enable([1, 3, 8, 17]);
console.log(bits.bitAt(4)); // => 1
console.log(bits.bitAt(8)); // => 1
console.log(bits.bitAt(9)); // => 0
}
テキストでは、文字列を追加するクラスも紹介されていました。
このように、複数の引数を受け取ることができるメソッドを、多重定義と呼びます。
function myFunction8_58_03() {
/** クラスStringSet */
class StringSet {
constructor() {
this.string = '';
}
/** 引数を判定するメソッド */
add(x) {
if (typeof x === 'string') {
this.addString(x);
} else if (Array.isArray(x)) { //本当の配列かテスト
x.forEach(s => this.addString(s), this);
} else {
for (const key in x) this.addString(key);
}
}
/** プロパティに文字列があるか判定するメソッド */
contains(word) {
return this.string.includes(word);
}
/** プロパティにstringを追加するメソッド */
addString(x) {
this.string += x;
}
}
const set = new StringSet();
//複数の引数を受け取ることができる=メソッドが多重定義されている
set.add('Hamlet');
set.add(['Rosencrants', 'Guildenstern']);
set.add({ 'Ophelia': 1, 'Polonius': 1, 'Horatio': 1 });
console.log(set.contains('Polonius')); // => true
console.log(set.contains('Guildenstern')); // => true
console.log(set.contains('Falstaff')); // => false
}
型の判定
JavaScriptの型には、プリミティブ型と組み込みオブジェクト型の2種類があります。
typeof演算子で判定できるものや、組み込みオブジェクトのメンバーで型判定できるときは、積極的に使いましょう。
function myFunction8_58_04() {
const string = 'Tom';
console.log(typeof string === 'string'); // => true
const bool = false;
console.log(typeof bool === 'boolean'); // => true
const number = 10;
console.log(Number.isInteger(number)); // => true
const array = [];
console.log(Array.isArray(array)); // => true
const date = new Date();
console.log(date instanceof Date); // => true
//Objectの判定はinstanceof演算子では行わない
const object = [];
console.log(object instanceof Object); // => true
}
項目52の復習ですが、もしES5の環境でないばあいなどは、call()メソッドでも判定できます。
function myFunction8_58_05() {
console.log(Object.prototype.toString.call({})); // => [object Object]
console.log(Object.prototype.toString.call(new Date())); // => [object Date]
console.log(Object.prototype.toString.call('a')); // => [object String]
console.log(Object.prototype.toString.call(/.*/)); // => [object RegExp]
console.log(Object.prototype.toString.call([])); // => [object Array]
console.log(Object.prototype.toString.call(function () { })); // => [object Function]
console.log(Object.prototype.toString.call(JSON)); // => [object JSON]
console.log(Object.prototype.toString.call(SpreadsheetApp)); // => [object Object]
class Test { }
console.log(Object.prototype.toString.call(new Test())); // => [object Object]
}
ユーザー定義型
C++の設計者であるBjarne Stroustrup氏は、「数値型とか文字列型などのデータ型だけが型じゃなくて、データをどう処理するか決めたものも型って言えるんじゃない?」 という考え方をプログラミング言語に取り入れました。
あらかじめ決められたデータ型に対して、いわば、ユーザーが独自に定義する、ユーザー定義型といったところです。
そして、このユーザー定義型に、クラスという名前を付けました。
function myFunction8_58_06(){
/**
* Personクラス
*/
class Person {
constructor() {
this.name = 'Tom';
this.age = 30;
}
/**
* 年齢を倍にして返すメソッド
* @return {number} 倍の年齢
*/
getDoubleTomsAge() { return this.age * 2; }
}
const p = new Person();
console.log(p, p.getDoubleTomsAge()); // { name: 'Tom', age: 30 } , 60
}
ユーザー定義型の判定
では、なにをもってユーザー定義型と言えるのでしょうか。
それは、プロパティの有無です。
JavaScriptのような動的型付けの言語では、さまざまなデータ型を格納できるため、最終的なデータ型は、プログラムを実行してみないとわかりません。
しかし、バグの早期発見や、エラー検出のため、型は早めに判定できたほうが便利です。
なので、データ型ではなく、クラスがもつプロパティで、型を判定しよう、という考え方が**ダックタイピング(または、ダックテスティング)**です。
in演算子は、プロパティが含まれるかどうか、プロトタイプチェーンをさかのぼって参照する演算子です。
クラス動詞でプロパティの重複があるばあいは、完璧に判定できませんが、型を判定する助けになるでしょう。
function myFunction8_58_07(){
/**
* Personクラス
*/
class Person {
constructor() {
this.name = 'Tom';
this.age = 30;
}
/**
* 年齢を倍にして返すメソッド
* @return {number} 倍の年齢
*/
getDoubleTomsAge() { return this.age * 2; }
}
const p = new Person();
//in演算子による型判定
console.log('getDoubleTomsAge' in p); //true
}
まとめ
以上で、「配列と「配列のようなもの」を区別しよう」をお届けしました。
型のお話については、オブジェクト指向のシリーズで、詳しくお届けします。
ダックテスティングは、非推奨とされていますが、その理由は、別のブログで考察したいと思います。
次回は、最終回で 「過剰な型強制を防ごう」 をお届けします。
参考資料
- 【Java入門】オーバーロードの使い方(オーバーライドとの違いも解説)
- Understanding Duck Typing in ECMAScript
- 【Python入門】ダックタイピングをやってみる – ポリモーフィズム
- etau/gas-classes Github
- 改定新版JavaScript本格入門~モダンスタイルによる基礎から現場での応用まで
このシリーズの目次
- [EffectiveJavaScript輪読会8]一貫した規約を維持しよう
- [EffectiveJavaScript輪読会8]undefinedは「値なし」として扱う
- [EffectiveJavaScript輪読会8]オプションオブジェクトで、キーワード付き引数群を受け取ろう
- [EffectiveJavaScript輪読会8]不必要な状態を排除する
- [EffectiveJavaScript輪読会8]柔軟なインターフェイスのために、構造的な型付けを使う
- [EffectiveJavaScript輪読会8]配列と「配列のようなもの」を区別しよう
- [EffectiveJavaScript輪読会8]過剰な型強制を防ごう