この記事はOthloTech Advent Calendar 2016の6日目の記事です。
こんにちは! OthloTechのぽこひでです。最近はJavaScriptを良く書いているので、JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティスでも紹介されてた良いパーツや悪いパーツの中から開発の上で知っておいて損はないことをまとめました。
この記事は、JavaScript初中級者向けの記事です。
JSのオブジェクトは参照渡し
JavaScriptのオブジェクトはコピーではなく参照渡しです。なので、以下のようなことが起きます。
var x, y; x = y = {}; // x, yは同じ空オブジェクトを参照している。
x['hoge'] = 1; // xを更新すると
y['hoge'] // 1
また、同様にJavaScriptでの=
は単なる値渡しではなく、オブジェクトの場合は値のポインタ、つまり参照の値渡しがされます。
var a = {'hoge': 1};
var b = a; // これはオブジェクトがコピーされるのではなく、参照先がコピーされただけ
a['hoge'] = 2; // なので参照先の値を更新すると
b['hoge'] // 2
これを防ぐには、=
を用いたコピーではなく、独自でclone
メソッドなどを定義するのも良いのですが、なかなか面倒くさいのでnode-cloneモジュールを用いたりします。
配列の長さ
JavaScriptの配列にはサイズの上限がなく、length
プロパティは配列内の最も大きな整数値をもつプロパティのプロパティ名 + 1
なので、以下のような動作になります。
var array = [];
array.length // 0
array[100] = true;
array.length // 101(100 + 1)
なので、基本的に配列の操作は添字で指定するより、あらかじめ提供されているpop
やpush
等の標準APIをなるべく使用すべきです。
訂正
JavaScriptの配列のlength
プロパティはUnit32
の範囲と定められているので、整数値であってもその範囲から溢れれば、length
に影響しないことがあります。
var arr = [];
arr[10] = "a";
arr[-1>>>0] = "b"; // lengthは 2^32 (=4294967296) を持てないので、これをlengthで表現できない
console.log(arr.length); // 最初に格納したもののみが適用されて、 lengthは11
new Array((-1>>>0) + 1);
// 因みにArray ConstructorではUint32を超える引数を渡すと例外が飛ぶ
コメントをくださったgaogao_9ありがとうございます。
配列のソート
JavaScriptの配列のsort
メソッドは数値の配列を正しく並び変えることができません。
var array = [32, 12, 5, 2, 7, 10];
array.sort();
// [ 10, 12, 2, 32, 5, 7 ]
JavaScriptの標準の比較関数は、比較の前に変数の型を調べないため、どの要素も文字列と見なしてソートしてしまうので、正しくソートされません。
なので、自分で比較関数を与えてやることで回避できます。比較関数は2つのパラメータを受け取って、それらが等しいなら0を、1番目のパラメータが先なら負の数を、逆なら正の数を返すようにすれば独自でカスタマイズできます。
var array = [32, 12, 5, 2, 7, 10];
array.sort((function(a, b) {
return a - b;
});
// [ 2, 5, 7, 10, 12, 32 ]
もっと独自にオブジェクト配列をこのプロパティでソートしたい!と思ったらJavaScript つい忘れてしまう配列のソート方法とか見てみるといいかもしれないです。
parseInt
parseInt
は文字列を整数に変換するメソッドですが、数字以外の文字が見つかると処理をストップしてしまうので、注意が必要です。なので、
parseInt('7') // 7
parseInt('7km') // 7
となります。また、文字列の頭が0x
だと16進数とみなされたりするので、知っておいて損はないと思います。第二引数に基数を指定できるので、特別なことがない限り明示的に書くといいでしょう。
parseInt('0x12') // 16進数なので、18 (16*1 + 1*2)
parseInt('0x12', 10) // 10進数なので、文字列でストップし 0
浮動小数の誤差
JavaScriptは8バイトのIEEE 754という浮動小数点標準という規格を使用して浮動小数点数を表現していて、それに収まらない場合は丸めなどを行っているため浮動小数点の計算は必ずしも正しいとは限りません。
ちなみに、8バイトなのでJSでは浮動小数点数を32個の[0,1]
で表現していて精度的には10進数桁数でいう7桁程度です。
0.1 + 0.2 // 0.30000000000000004
0.14 * 100 // 14.000000000000002
これを防ぐ方法をいくつか。
数値のスケーリング
浮動小数同士の計算は誤差を含みますが、整数演算は正しく行われるので、10^n
を掛けて整数に一旦直すことで避けれます。
(0.1 * 10 + 0.2 * 10) / 10 // 0.3
(0.14 * 100 * 100) / 100 // 14.000000000000002
しかし、この場合でも整数値にスケールアップするときに小数点の計算をしているので、結果的に誤差が残ってしまいます。
外部ライブラリに頼っちゃう
decimal.js
やbignumber.js
、math.js
などのライブラリを頼って頑張って下さい(´・ω・`)
この問題はJavaScriptに限らずほとんどの言語で言えることなので知っておいて損はないですね。
NaN
とかいう奴
NaN
は、上記のIEEE 754で定義されたNot a Number
の頭文字をとった数値を持たない特別な値です。なのになぜか
typeof NaN // 'number'
「いやいやNumber
じゃん(*-∀-)つ ナンデヤネン!」
ってなりますよね。ちなみに、NaN
は前述のparseInt
などで数字の形式になっていない文字列を変換するときに生成されます。
parseInt('hogehoge', 10) // NaN
Number({}) // NaN
これを判定するのに、JavaScriptではisNaN
関数を提供しているのですが、これはparseInt
などのメソッドを使用したときに引数が数値に変換可能かどうかの判定をしているようなので、以下のような動作をします。
isNaN(NaN) // true
isNaN('hoge') // true
isNaN(undefined) // true
isNaN({}) // true
isNaN(0) // false
isNaN('1.234') // false
isNaN(null) // false
// ちなみに、Number(null) は 0なのでfalseです...
これ以外にもNaN
には自分自身との等号判定でfalse
を返す性質があるため、他の型とは違って以下のような動作をします。
0 === 0 // => true
'hoge' === 'hoge' // => true
null === null // => true
NaN === NaN // => false
なので、Effective JavaScriptではこの性質を利用して、以下のようなイディオムを紹介していたようです。
function isReallyNaN(x) {
return x !== x; // xがNaNであればtrue, それ以外ではfalse
}
===
と ==
基本的には==
でも正常に動作するのですが、これは比較対象が同じデータ型のときのみです。もし、比較対象の型がバラバラであった場合、==
は両者の型を強制的に統一してから判定を行うため以下のようなことが起き得ます。
'' == 0 // true
0 == '' // true
0 == '0' // true
false == 0 // true
この例から、==
演算子は前者と後者のデータ型が違った場合は、前者のデータ型に後者を強制的に変換して判定していることがわかります。このように、==
演算子は予想と反する結果が表示するので、データ型も考慮する===
を使いましょう。
グローバル変数
生のJavaScriptはグローバル変数に依存しています。気をつけましょう。
さいごに
ECMAScript6
を使いましょう!
ECMAScript6
で何ができるかはこのいまからはじめるECMAScript 6のスライドが分かりやすかったです。(以下、略してES6
と表記します)
今、上記で説明したJavaScriptの知っておいて損はないことを知っておいて損はないのですが、ES6
は上記の普通のJavaScriptに対して文法や機能が追加されていて、簡単な例だとconst
やlet
が使用できます。
var x = 100;
var x = 200;
console.log(x); // 200
このように上書きできてしまいます。これを必ず開発を進めていく上でバグを生むのですが、let/const
を使うと
let x = 100;
let x = 200; // SyntaxError: Identifier 'x' has already been declared
const x = 100;
const x = 200; // SyntaxError: Identifier 'x' has already been declared
というように重複定義がエラーになったり、他にも変数のスコープについても、let/const
の場合は関数以外の構文内もプライベートなスコープ空間とみなして、構文の外を汚染しません。
このように、いろいろとメリットがあるので、余力があれば導入しましょう。
ES6
の導入
ただし、ES6
には少し問題があります。ES6
には
-
let
,const
キーワードによる変数宣言 -
class
キーワードによるクラス宣言 - 関数の引数のデフォルトパラメータ
- アロー関数
...
などいろいろと新しい文法が追加されているのですが、全てのブラウザ上でそのコードがそのまま動くわけではないので、Gulp
やWebpack
, Babel
などを用いて、どのブラウザでも動くJSに変換する必要があります。
この辺りを参考にして頑張ってみてください!笑
ES6
の構文などのまとめは以下のリンクが詳しいので、興味を持った方は見てみてください。
後半は少し雑になってしまいましたが、これを機にOthloTechという東海の学生テック団体の存在を知っていただけると嬉しいです!!
あと、悪いコードは撲滅しましょうσ(゚∀゚)