どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。今日は第4回目です。
前回のおさらい
前回は、「JavaScriptの浮動小数点を理解しよう」をお届けしました。
今回は、「暗黙の型変換に注意しよう」 をお届けします。
テキスト第1章「JavaScriptに慣れ親もう」の項目3に対応しています。
今日のアジェンダ
- JavaScriptの自動的型変換プロトコルによる強制
- NaN(Not a Number)
- オブジェクトの型変換
- 強制の真偽性
- 実務を意識するなら
JavaScriptの自動的型変換プロトコルによる強制
JavaScriptには、間違った型を提供すると、エラーになるケースがあります。
関数でないものを呼び出したり、nullのプロパティを設定しようとしたばあいです。
function myFunction1_3_01() {
//実行する際はコメントインアウトしながら
// console.log("hello"(1)); //TypeError: "hello" is not a function
// console.log(null.x); //TypeError: Cannot read property 'x' of null
}
しかしながら、JavaScriptは多くのばあいで型によるエラーを容認し、スクリプトを実行します。
期待される型に合わせ、内部的に値を変換することを 「自動的型変換プロトコルによる強制」 と呼びます。
特に数値型は 優先的に文字列型に変換 され、処理されます。
function myFunction1_3_02() {
console.log(3 + true); //4
//文字列を優先する強制(coerce)
console.log('2' + 3); //'23'
console.log(2 + '3'); //'23'
}
自動的型変換プロトコルのすべてを把握できませんが、+演算子と数値型と文字列型だけを分析すると、左結合を行っている ようにも思えます。
乗算は数値型を優先するなど、仕様としてそのまま(明示的に型変換などをせずに)実務に取り入れるには怖い印象があります。
function myFunction1_3_03() {
//複数の型が混在する演算
console.log(1 + 2 + '3'); //'33'
console.log(1 + '2' + 3); //'123'
//左結合を行っているようである
console.log((1 + 2) + '3'); //'33'
console.log((1 + '2') + 3); //'123'
//乗算は数値型を優先?
console.log('17' * 3); //51
}
前回お伝えした、ビット演算でも、強制はおこなわれます。ビット演算では、浮動小数点に変換され、次に演算を行っているようです。
これは、直感的で便利なようにうつるかもしれません。
function myFunction1_3_04() {
//論理和
console.log('8' & 1); //0
//論理積
console.log('8' | '1'); //9
}
しかしながら、次節では、型の強制にはエラーを隠す作用がある ことをお伝えします。
NaN(Not a Number)
nullやundefinedの処理は、多くのばあいでエラーを発生させますが、型の強制により、エラーを起こさずにのんきに演算を行ってしまいます。
undefinedはNaN(Not a Number)へ変換され、演算を実行します。
NaNとは、Not a Numberと言いながら、数値型です。これはJavaScriptの仕様ではなく、浮動小数点の国際規格が決めていることです。
function myFunction1_3_05() {
//nullはエラーが起きるはずだが、0に変換されてしまう
const n = null;
console.log(n + 1); //1
//undefinedはエラーが起きるはずだが、NaNに変換されてしまう
const u = undefined;
console.log(u + 1); //NaN
//NaNはnumber型である
console.log(typeof NaN); //number
}
そして、JavaScriptは、NaNを「それ自信と等しくない値」として扱います。つまり、NaNがNaNであるか?という条件式が意味をなさなくなります。
これは、isNaN()メソッドで解決できそうに思えます。
function myFunction1_3_06() {
//NaNがNaNであるかを判定できない
const x = NaN;
console.log(x === NaN); //false
console.log(x == NaN); //false
//isNaN()メソッドで判定できる
console.log(isNaN(NaN)); //true
}
しかし、前述したundefinedのような、NaNではないのに型の強制によりNaNに変換されてしまう値は、isNaN()メソッドで判定できません 。
これは、JavaScriptが、NaNを「それ自信と等しくない値」として扱うことを逆手にとり、否定で判定できます。
実務では、関数化して使用するのもアリでしょう。
function myFunction1_3_07(){
//NaN以外の引数は、isNaN()メソッドでは判定できない
console.log(isNaN('foo')); //true
console.log(isNaN(undefined)); //true
console.log(isNaN({})); //true
console.log(isNaN({ valueOf: 'foo' })); //true
//仕様を逆手に取った判定
const a = NaN;
console.log(a !== a); //true
const b = 'foo';
console.log(b !== b); //false
const c = undefined;
console.log(c !== c); //false
const d = {};
console.log(d !== d); //false
const e = { valueOf: 'foo' };
console.log(e !== e); //false
/**
* NaNかどうかを判定する関数
*/
const isReallyNaN = x => x !== x;
console.log(isReallyNaN(NaN)); //true
console.log(isReallyNaN(undefined)); //false
}
オブジェクトの型変換
これまでは、数値や文字列の型変換という強制をみてきました。次はオブジェクトです。
JavaScriptは、オブジェクトにも 「文字列型への型変換の強制」 をする場合があります。
function myFunction1_3_08() {
//リテラルでオブジェクトを生成する2つのグローバルオブジェクト
console.log(JSON, typeof JSON); //{}, object
console.log(Math, typeof Math); //{}, object
//オブジェクトはtoString()メソッドをぶつけると、文字列型に変換されたときの値を確認できる
console.log(Math.toString()); //[object Math]
console.log(JSON.toString()); //[object Math]
//オブジェクトから文字列への強制型変換
console.log('わたしは' + JSON + 'です'); //わたしは[object JSON]です
console.log('わたしは' + Math + 'です'); //わたしは[object Math]です
}
オブジェクトには、toString()のように文字列型を返すメソッドの他に、valueOf()メソッド という、オブジェクトの値を返すメソッドがあります。
普段は、使い道があまりなさそうですが、型変換の強制を制御するために、オブジェクトにvalueOf()メソッドを実装し、戻り値をもたせることができます。
そして、toString()メソッドとvalueOf()メソッドのどちらももつオブジェクトでは、valueOf()メソッドを優先的に呼び出します。
function myFunction1_3_09() {
//valueOf()メソッドはオブジェクトのthis.valueを返す
const x = (123).valueOf();
console.log(x, typeof x); //123 'number'
//数値型から文字列への強制(おさらい)
console.log(2 + '3'); //'23'
//valueOf()メソッドを使った型(値)制御
console.log(2 + { valueOf: function () { return 3; } }); //5
//オブジェクトの型変換強制では、valueOf()メソッドが優先的に呼び出される
const obj = {
toString() {
return 'hoge';
},
valueOf() {
return 17;
}
};
console.log('object:' + obj); //object:17
}
強制の真偽性
ノンプロ研GAS講座で習う「暗黙の型変換」とは、条件式における真偽値への強制 でした。
文字列や数値やオブジェクトとは違って、真偽性の判定では強制メソッドの暗黙的な呼び出しが行われません 。
function myFunction1_3_10() {
//暗黙の型変換でfalseを返す値
if (false || 0 || -0 || '' || NaN || null || undefined)
console.log(true);
} else {
console.log('すべてfalseです');
}
//'すべてfalseです'
}
JavaScriptでは0をFalsyとみなすため(Rubyなどの他のプログラミング言語では0をTruthyとみなす)、引数に0を渡したときの挙動には注意が必要です。
対策として、typeof演算子によるundefinedとの比較 が挙げられます。
実務でデフォルト値を正しく設定するばあいに必要であれば、関数化するといいでしょう。
function myFunction1_3_11() {
/**
* 引数が0かfalsyを判定する関数
*/
const point = (x = 320, y = 240) => {
return { x: x, y: y };
}
console.log(point(0, 0)); //{ x: 0, y: 0 }
console.log(point()); //{ x: 320, y: 240 }
console.log(point(undefined, undefined)); //{ x: 320, y: 240 }
}
実務を意識するなら
実務では、空の配列を判定 することがあるでしょう。
備忘録的なもので考察はいたしませんが、要素に0が格納されているばあいも含む、空の配列を判定する方法は、[] == ” です。
出典:Don’t Make Javascript Equality Look Worse Than It Is by:Craig Gidney
function myFunction1_3_12() {
const values = [[], [''], [0]];
//1.暗黙の型変換では判定できない
for (const value of values) {
if (value) console.log(`${value}をtrueと判定しました`);
}
//をtrueと判定しました, をtrueと判定しました, 0をtrueと判定しました
//2.厳密な型判定でも判定できない
for (const value of values) {
if (value === [''] || value === [] || value === '') {
} else {
console.log(`${value}をfalseと判定しました`);
}
}
//をfalseと判定しました, をfalseと判定しました, 0をfalseと判定しました
//3.falseだと要素に0があると判定できない
for (const value of values) {
if (value == false) console.log(`${value}は空白です`);
}
//は空白です, は空白です, 0は空白です
//4.直感的正しく空判定する
for (const value of values) {
if (value == '') console.log(`${value}は空白です`);
}
//は空白です, は空白です
}
まとめ
以上で、「暗黙の型変換に注意しよう」をお届けしました。
よく「GASはチャラい」と言われますが(あくまで個人的な感想です)、JavaScriptが型にゆるい仕様であることが由来です。
しかしながら、型に寛容な仕様を武器にするか弱点にするかは、結局、仕様を理解するかどうか自分次第なのかなと思いました。
次回は、「オブジェクトラッパーよりもプリミティブが好ましい」 をお届けします。
参考資料
ECMAScript® 2019 Language Specification
Object.prototype.valueOf() MDN
Special Thanks
このシリーズの目次
[EffectiveJavaScript輪読会]ノンプロ研EffectiveJavaScript輪読会とは
[EffectiveJavaScript輪読会]どのJavaScriptをつかっているのかを意識しよう
[EffectiveJavaScript輪読会]JavaScriptの浮動小数点を理解しよう
[EffectiveJavaScript輪読会]暗黙の型変換に注意しよう
[EffectiveJavaScript輪読会]オブジェクトラッパーよりもプリミティブが好ましい
[EffectiveJavaScript輪読会]型が異なるときに==を使わない
[EffectiveJavaScript輪読会]セミコロン挿入の限度を学ぼう
[EffectiveJavaScript輪読会]文字列は16ビットの符号単位を並べたシーケンスとして考えよう