Help us understand the problem. What is going on with this article?

JavaScriptで知っておいて損はないこと

More than 3 years have passed since last update.

この記事はOthloTech Advent Calendar 2016の6日目の記事です。

othlotech-logo-1.png

こんにちは! 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)

なので、基本的に配列の操作は添字で指定するより、あらかじめ提供されているpoppush等の標準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の挙動
parseInt('7')    // 7
parseInt('7km')  // 7

となります。また、文字列の頭が0xだと16進数とみなされたりするので、知っておいて損はないと思います。第二引数に基数を指定できるので、特別なことがない限り明示的に書くといいでしょう。

parseIntの挙動2
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.jsbignumber.jsmath.jsなどのライブラリを頼って頑張って下さい(´・ω・`)

この問題はJavaScriptに限らずほとんどの言語で言えることなので知っておいて損はないですね。

NaNとかいう奴

NaNは、上記のIEEE 754で定義されたNot a Numberの頭文字をとった数値を持たない特別な値です。なのになぜか

numberやん
typeof NaN  // 'number'

「いやいやNumberじゃん(*-∀-)つ ナンデヤネン!」

ってなりますよね。ちなみに、NaNは前述のparseIntなどで数字の形式になっていない文字列を変換するときに生成されます。

NaNはいつできるか
parseInt('hogehoge', 10)  // NaN
Number({})                // NaN

これを判定するのに、JavaScriptではisNaN関数を提供しているのですが、これはparseIntなどのメソッドを使用したときに引数が数値に変換可能かどうかの判定をしているようなので、以下のような動作をします。

isNaN関数の挙動
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を返す性質があるため、他の型とは違って以下のような動作をします。

NaNだけおかしい
0 === 0           // => true
'hoge' === 'hoge' // => true
null === null     // => true

NaN === NaN       // => false

なので、Effective JavaScriptではこの性質を利用して、以下のようなイディオムを紹介していたようです。

NaNであるかを判定するメソッド
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に対して文法や機能が追加されていて、簡単な例だとconstletが使用できます。

varの重複定義
var x = 100;
var x = 200;
console.log(x);  // 200

このように上書きできてしまいます。これを必ず開発を進めていく上でバグを生むのですが、let/constを使うと

letを重複定義
let x = 100;
let x = 200;  // SyntaxError: Identifier 'x' has already been declared
constを重複定義
const x = 100;
const x = 200; // SyntaxError: Identifier 'x' has already been declared

というように重複定義がエラーになったり、他にも変数のスコープについても、let/constの場合は関数以外の構文内もプライベートなスコープ空間とみなして、構文の外を汚染しません。

このように、いろいろとメリットがあるので、余力があれば導入しましょう。

ES6の導入

ただし、ES6には少し問題があります。ES6には

  • let, constキーワードによる変数宣言
  • classキーワードによるクラス宣言
  • 関数の引数のデフォルトパラメータ
  • アロー関数 ...

などいろいろと新しい文法が追加されているのですが、全てのブラウザ上でそのコードがそのまま動くわけではないので、GulpWebpack, Babelなどを用いて、どのブラウザでも動くJSに変換する必要があります。

この辺りを参考にして頑張ってみてください!笑

ES6の構文などのまとめは以下のリンクが詳しいので、興味を持った方は見てみてください。

後半は少し雑になってしまいましたが、これを機にOthloTechという東海の学生テック団体の存在を知っていただけると嬉しいです!!
あと、悪いコードは撲滅しましょうσ(゚∀゚)

pokohide
上京したての新卒Webえんじにあ💻 Ruby(Rails), Node.js, React.jsとか書いてます
https://note.mu/pokohide
timee
日本の労働力の減少を若者の働き方改革で解決します。好きな時に好きなだけ働けるプラットフォームタイミーを作り、人々が好きなことをできる世界を実現します。
https://timee.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away