どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。今日は第5回目です。
前回のおさらい
前回は、「暗黙の型変換に注意しよう」をお届けしました。
今回は、「オブジェクトラッパーよりもプリミティブが好ましい」 をお届けします。
テキスト第1章「JavaScriptに慣れ親もう」の項目4に対応しています。
今日のアジェンダ
- JavaScriptのSymbol型
- ラッパーオブジェクト
- ラッパーオブジェクトの暗黙の生成
- 実務を意識するなら
JavaScriptのプリミティブ型とSymbol型
JavaScriptの「プリミティブ」という単語を正確にとらえましょう。
プリミティブとは、オブジェクトではなく、メソッドを持たない値 のことです。
JavaScriptには、6つのプリミティブ型があります。
– 文字列
– 数値
– 真偽値
– undefined
– シンボル(ECMAScript 2016 で追加)
– BitInt(ECMAScript 2020 で追加)
プリミティブ型から生成されるものは、プリミティブ値です。さまざまな生成方法がありますが、それぞれの「型」に対応した「値」があると思ってください。
たとえば、「文字列型に対応した文字列値」といえば、“ダブルクォーテーション” や `テンプレートリテラル` のような値です。
「数値型に対応した数値値」といえば、123 や 0.8 のような値です。数値値(すうちち)というのも変ですので、「数値」と呼びます。
と、いうことで「シンボル型に対応したシンボル値」というものがあります。今後、「シンボル」と呼びます。
シンボルとは
シンボルは、これまでにない特殊な値ですので、イメージできないのも当然です。
しかし、他のプリミティブ値と同様に、生成したり、変数に代入したり、比較したりできます。
また、シンボルは唯一無二の値であり、一度生成したら、二度と同じ値を生成できません。
function myFunction1_4_01() {
//シンボルプリミティブ値を生成するSymbol()関数
console.log(typeof Symbol()); //symbol
//生成と代入
const sym1 = Symbol();
const sym2 = Symbol();
//シンボルの比較
console.log(sym1 === sym1); //true
console.log(sym1 === sym2); //false
//シンボルは唯一無二である
console.log(Symbol() === Symbol()); //false
}
ここで、オブジェクトの復習ですが、オブジェクトに定義したプロパティやメソッドは、いつでも書き換えることができました。
これはあまり良くない方法ですが、組み込みオブジェクトにオリジナルメソッドを定義する こともできます。
例では、文字列の先頭文字を返す「getFirstIndex()メソッド」をStringオブジェクトに追加してみました。
function myFunction1_4_02() {
//オブジェクトにプロパティを定義する
const obj = {};
const prop = 'name';
obj[prop] = 'Tsujike';
console.log(obj.name); //'Tsujike'
//プロパティを書き換える
obj['name'] = 'etau';
console.log(obj.name); //'etau'
//オブジェクトにメソッドを定義する
obj.getYourName = function () { return this.name };
console.log(obj.getYourName());//etau
//メソッドを書き換える
obj.getYourName = function () { return `${this.name}様` };
console.log(obj.getYourName());//etau様
//組み込みオブジェクトStringに、オリジナルメソッドgetFirstIndex()を定義する
String.prototype.getFirstIndex = function () { return this[0] };
console.log('ABC'.getFirstIndex());//A
}
あまり良くない方法だと言われていますが、組み込みオブジェクトにオリジナルメソッドを追加してコーディングしているプログラマ も世界中にたくさんいます。
JavaScriptもそれなりに歴史のあるプログラミング言語です。世界中にオリジナルメソッドが存在し、動いているでしょう。
そんなある日、本家のJavaScript運営者が 「StringオブジェクトにgetFirstIndex()メソッドを追加します」 というアップデートを公開したらどうなるでしょう。
世界中のオリジナルメソッド「getFirstIndex()メソッド」は書き換えられ、大混乱を招きます。
なぜ、メソッドが書き換えられてしまうのでしょうか。それは、「名前が衝突している」 からです。
これこそ、ECMAScript2016でシンボルが導入された、最大の理由です。
プロパティ名の重複を避ける
これまで、プロパティ名には文字列(変数も含む)を使用してきました。
しかし、文字列のばあい、同名のプロパティ(obj[‘name’]のような)を上書きしてしまいます。
ECMAScript2016で導入されたシンボルは、プロパティ名に使うことができます。
シンボルは唯一無二 でしたので、プロパティは重複せず、上書きされません。
function myFunction1_4_03() {
const obj = {};
let name;
//プロパティに文字列
x = 'name';
obj[x] = 'Noriko';
console.log(obj[x]); //'Noriko'
//プロパティ名が重複すると上書きする
y = 'name';
obj[y] = 'Noriaki';
console.log(obj[x]); //'Noriaki'
//プロパティ名にシンボルを使う
n = Symbol();
obj[n] = 'Norisuke';
console.log(obj[n]); //'Norisuke'
//重複せず、上書きされない
m = Symbol();
obj[m] = 'Masanori';
console.log(obj[n]); //'Norisuke'
}
このことは、デバッガでも確認できます。
メソッドも同様に、オブジェクトのプロパティ名へシンボルを使用することで、重複を回避できます。
function myFunction1_4_04() {
//組み込みオブジェクトStringに、オリジナルメソッドgetFirstIndex()を定義する
String.prototype.getFirstIndex = function () { return this[0] };
console.log('ABCD'.getFirstIndex());//A
//突然誰かが、組み込みオブジェクトStringに、同名のオリジナルメソッドgetFirstIndex()を定義すると、上書きされる
const functionName1 = 'getFirstIndex';
String.prototype[functionName1] = function () { return this[1] };
console.log('ABCD'.getFirstIndex()); //B
//なので、文字列型の名前ではなく、シンボルでメソッドを追加定義する
const functionName2 = Symbol();
String.prototype[functionName2] = function () { return this[2] };
console.log('ABCD'[functionName2]()); //C
//Symbolは唯一無二なので、メソッドは上書きされない
const functionName3 = Symbol();
String.prototype[functionName3] = function () { return this[3] };
console.log('ABCD'[functionName2]()); //C
console.log('ABCD'[functionName3]()); //D
}
あとは 「シンボルをどのように列挙するか」 ですが、これは、またの機会にお届けします。
ラッパーオブジェクト
プリミティブ型はメソッドをもたないはずなのに、文字列型に.length()メソッドをぶつけると、文字数を取得できますよね?
これは、文字列型をオブジェクトに一時的に変換し、そのオブジェクトに定義されているメソッドを呼び出すからです。
このように、プリミティブ型を暗黙的にオブジェクトに変換する機能を、「ラッパーオブジェクト」と呼びます。
ラッパーオブジェクトと明示的に生成したオブジェクトの違いを区別しましょう。
function myFunction1_4_05() {
//文字列型をStringオブジェクトに変換するラッパーオブジェクト
let name = 'kenzo';
console.log(name.length); //5
console.log(name[1]); //e
//型の確認と明示的なStringオブジェクトの生成
console.log(typeof name); //String
name = new String('kenzo');
console.log(name); // { '0': 'k', '1': 'e', '2': 'n', '3': 'z', '4': 'o' }
console.log(name.valueOf(), typeof name); //kenzo, object
//オブジェクト同士の比較
console.log('kenzo' === 'kenzo'); //true
console.log(new String('kenzo') === new String('kenzo')); //false
console.log(new String('kenzo') == new String('kenzo')); //false
}
ラッパーオブジェクトの暗黙の生成
ラッパーオブジェクトが生成されると、そのオブジェクトに定義されているprototypeメソッドを使えるようになります。
しかしながら、明示的に生成したオブジェクトはプロパティを保持するにも関わらず、ラッパーオブジェクトでは破棄されます。
これは、ラッパーオブジェクトが、呼び出される都度、暗黙的にオブジェクトを生成しているからです。
function myFunction1_4_05() {
//Stringオブジェクトのメソッド
const name = 'kenzo';
console.log(name.length); //5
console.log(name.toUpperCase()); //KENZO
console.log(name.replace('ken', 'ben')); //benzo
//正式なオブジェクトは、プロパティが残る
const obj = {};
obj.someProperty = 17;
console.log(obj['someProperty']); //17
//呼び出しのさい、最初の'hello'.someProperty = 17は破棄されている
'hello'.someProperty = 17;
console.log('hello'['someProperty']); //undefined
}
実務を意識するなら
JavaScriptの3つのプリミティブ型に対し、あえてnew演算子でオブジェクトを生成する必要はありません。
むしろ、都度暗黙的にオブジェクトを生成しているなら、newすることは厳禁と言えるかもしれません。
実務で使えるSymbolのあてがあるので、いつか時間があるときにTryしてみたいです。
まとめ
以上で、「オブジェクトラッパーよりもプリミティブが好ましい」をお届けしました。
どうせプリミティブ型を考察するなら、V8で登場した Symbol型 を考察してみたかったので、この機会に失礼しました。
次回は、「型が異なるときに==を使わない」 をお届けします。
参考資料
ECMAScript6にシンボルができた理由
【JavaScript】 シンボル(Symbol)とは?使い方を解説します
『GAS初級・中級講座 同時受講のススメ ラッパーオブジェクト』GAS初級4期LT200311 つじけ
Special Thanks
etauさん
このシリーズの目次
[EffectiveJavaScript輪読会]ノンプロ研EffectiveJavaScript輪読会とは
[EffectiveJavaScript輪読会]どのJavaScriptをつかっているのかを意識しよう
[EffectiveJavaScript輪読会]JavaScriptの浮動小数点を理解しよう
[EffectiveJavaScript輪読会]暗黙の型変換に注意しよう
[EffectiveJavaScript輪読会]オブジェクトラッパーよりもプリミティブが好ましい
[EffectiveJavaScript輪読会]型が異なるときに==を使わない
[EffectiveJavaScript輪読会]セミコロン挿入の限度を学ぼう
[EffectiveJavaScript輪読会]文字列は16ビットの符号単位を並べたシーケンスとして考えよう