この投稿は、 JavaScript Advent Calendar 18日目の記事です。
更新履歴
- こちら をご覧下さい
JavaScript の書き方をアップデートする
JavaScript Good Parts で書かれているような JS の書き方は、古くなりつつある部分も多いです。
正直なところ、自分はあの本が「今でも」良書だとは思っていません。
初学者に勧めることもしません。まんべんなさと普遍性と客観性から「パーフェクト JavaScript」 を勧めています。
その頃と比べると、 JavaScript をとりまく環境は変わりました
JavaScript の進化に合わせて書き方もアップデートしていくべきなので、今回は分かりやすいしきい値として
という前提で、列挙してみます。
たとえば XHR2 や File API に依存したサービスをやる場合など、そもそも古い IE が最初から相手になってない場合もでてきます。
そうでなくても、今サポートしているバージョンは、時間が経てばいずれ切れていくはずです。
古い不必要なイディオムに引っ張られた書き方は修正して、より現状に即した簡潔で妥当なコードに移行してべきだと思っています。
前提
- 主に ES5 と DOM 周りの機能と、それを取り巻くイディオムのアップデートを対象とする
- 実装サポートのソースは以下の二つとする
- http://kangax.github.io/compat-table/es5/
- http://caniuse.com/
- 上記ソースから IE/Chrome/FF/Safari が緑になっている機能を「使えると」して扱う
- 「こう書ける」は 「そう書かないといけない」と同義ではない
- 使えるけれども、飛躍が大きいためすぐに使うべきとは思わない機能については含めない
JS 編
Strict Mode
下記の一行を追加するだけです。
"use strict";
記述することで、 Strict Mode というモードが有効になり、簡単に言うと「変なコード/危ういコード」を書けなくします。
個人的には、これから新しく書く JavaScript で、 Strict Mode を有効にしない積極的な理由は無いと考えています。
Strict Mode の詳細は全部は解説しませんが、中でも知っておくべき挙動を解説します。
まず、いわゆるコンストラクタ関数の new を忘れ普通の関数として呼んだ場合の this です。
(function() {
function A() {
this.p = 1; // window.p = 1;
}
var a = A(); // new 忘れ
})();
これを防ぐために A() が new で呼ばれなかったときのために、代わりに return してあげる実装も行われたりしました。
(ここで例外を上げるのもありだと思います。ネイティブのオブジェクトはそういう実装もあります。)
function A() {
if (!(this instanceof A)) {
return new A(); // or throw Error
}
this.p = 1;
}
しかし、 Strict Mode 下では、コンストラクタ関数を new を忘れて呼び出したとき this が undefined になるため、エラーが発生します。
(function() {
"use strict";
function A() {
this.p = 1; // TypeError: Cannot set property 'p' of undefined
}
var a = A(); // new 忘れ
})();
new を忘れると悲惨だから、そもそも 「new なんか使うな」とか言ってた本もありましたが、自分はそうは思いません。
そして Strict Mode のこの挙動は、 new を忘れてグローバルオブジェクトが汚染されるのを防ぐので、 new をより正しく使う助けになります。
他にも、bad parts とされた with の禁止や、 const, NAN への代入といった誤った処理のエラー扱いなど、細かいところが色々と変わります。詳細は以下などを確認下さい。
見ての通り単なる文字列定義なので、有効になるかどうかは別としてどんなバージョンのブラウザでも書くことはできます。
ただし、書く場所には気をつける必要があります。
全てが自分の管理下にある場合は、全てのコードよりも先頭に一行追加すれば全体が Strict Mode になりますが、他の「非 Strict Mode」なコードと連結するような場合は、問題が出るかもしれません。
このためたとえば JSHint なんかは、グローバルな Strict Mode を警告したりします。
そこで、 Strict Mode は関数レベルでも指定できるため、よくある以下のようなイディオムで関数化した名前空間内で有効にするのがいいでしょう。
(function() {
"use strict";
})();
$(function() {
"use strict";
});
undefined 判定
JavaScript の undefined は、実際はただの変数であるため、代入することが可能でした。
undefined = true; // 相手は死ぬ
例えば、関数の引数の有無を undefined で判定する以下のようなコードは、 undefined が上書きされていると失敗しました。
そこで、 typeof を用いて以下のように調べていました。
function fn(arg) {
if (arg === undefined) {
// 信用できない
}
if (typeof arg === 'undefined') {
// 信用できる
}
}
fn(); // 引数は undefined
このため確実な undefined 、つまり undefined という変数の中に、なにも入っていない、言うなれば本物の undefined が入っている状態(あ、これ怒られる表現だ) を求めて人々は八苦を繰り返してきました。
(function(undefined) {
console.log(undefined); // これが、これこそが undefined だ!
})();
undefined = void 0; // こんなのもあった
しかし、 ES5 以降 undefined は const 扱い(immutable) になったため、前述の代入は実行可能だが実際には代入されなくなります。さらに Strict Mode なら const の代入はエラーになります。
よって、typeof しなくても安心して undefined を用いることができます。
function fn(arg) {
if (arg === undefined) {
// 引数が無ければ 0 にする。
arg = 0;
}
console.log(arg);
}
fn();
補足
しかしコメントで指摘されましたが undefined は予約語ではないので(Strict Mode でも)、ローカル変数として undefined を定義することはできてしまいます。
確かに。
(function () {
var undefined = true;
console.log(undefined); // true
})();
なので、このミスが起こってる可能性を考えるとまだ typeof は必要そうです。
日付の取得
タイミング制御などで、現在の時間を UnixTime で取り、差分を出したりする場合。
まず Date オブジェクトを new してから変換する必要がありました。
new Date().valueOf();
+new Date();
しかし、 Date.now() が入ったのでその必要はありません。
Date.now();
また、これはオブジェクトを new しないため、パフォーマンス的にも有利だそうです。
+new Date を Date.now() に差し替えると200~400% 高速化も
Array
Array.prototype にある以下は全て使えます。
- Array.prototype.indexOf
- Array.prototype.lastIndexOf
- Array.prototype.every
- Array.prototype.some
- Array.prototype.forEach
- Array.prototype.map
- Array.prototype.filter
- Array.prototype.reduce
- Array.prototype.reduceRight
また、これらのメソッドは Array.prototype が拡張されていたとしても影響を受けずに配列の保持する値だけを扱うことができます。
また、非破壊メソッドで処理結果を配列として返すために、メソッドチェインができるので、 関数型な感じで配列を処理できます。
配列を for 文で処理する前に、 map/reduce/forEach あたりを使った書き方を考えてみて下さい。
また Array.isArray() も使えるため、正確な Array の判定も可能です。
String.prototype.trim()
便利
String への添え字アクセス
IE7 までは charAt() を使わないとできませんでしたが、文字列に添え字アクセスができます。
var str = "string";
console.log(str[2]); // r
ただし、バイト単位のようなので、サロゲートペアはうまく扱えない点には注意が必要そうです(取得できなかった)。
あと、 Array になったわけではないので、 Array のメソッドは生えてません。
Object.create()
継承的なことをしたい場合の話です。
例えば Sub extends Super 的なことをやろうとする場合、方法の一つとして Super のインスタンスを、 Sub の prototype に追加することで、プロトタイプチェーンをつなぐ方法があります。
ちなみに、 Sub() の prototype を上書きしてしまっているので、 constructor プロパティは自分で直しておく必要があります。
function Sup() {
console.log('constructor sup');
this.name = 'super';
}
Sup.prototype.getName = function() {
return this.name;
}
function Sub() {
Sup.call(this); // super constructor call
console.log('constructor sub');
this.name = 'sub';
}
// extends
Sub.prototype = new Sup();
Sub.prototype.constructor = Sub;
var sub = new Sub();
console.log(sub.getName()); // sub
しかし、これでは Sup() の new が走るため、 Sup() のコンストラクタ処理が余計に走ってしまいます。
この方法で継承するためには、親クラスのコンストラクタに処理があると、余計な初期化が一回多く走ってしまい不都合です。
よって、 prototype だけコピーして、空のコンストラクタである仮のコンストラクタ関数を作って、そいつをプロトタイプチェーンに挟むという処理が行われてきました。
// extends
// Sub.prototype = new Sup();
var F = function F () {};
F.prototype = Sup.prototype;
Sub.prototype = new F();
Sub.prototype.constructor = Sub;
しかし、今は同等のことを行う Object.create() が入ったので、これを使います。
(上の方法は、 Object.create() の polyfill として使われていたりしました。)
Object.create() を使う場合は、 constructor だけケアしてあげればいいです。
Sub.prototype = Object.create(Sup.prototype);
Sub.prototype.constructor = Sub;
ちなみに、Object.create() は第二引数で PropertyDescriptor をとれます。
そこで、この constructor を一緒に定義して、さらに親がなんだったのかを保存するようにしたのが、 node.js の util.inherits です。
exports.inherits = function(ctor, superCtor) {
ctor.super_ = superCtor;
ctor.prototype = Object.create(superCtor.prototype, {
constructor: {
value: ctor,
enumerable: false,
writable: true,
configurable: true
}
});
};
Object.keys()
enumerable な key が取得できます。うれしいのは prototype をたどらない点ですね。
配列で取れるので、そのあとのメソッドチェインも可能です。
for in と同じなので、オブジェクトがネストしてる場合は一番浅いところだけだし、順番は不定(実装依存)です。
// before
Object.prototype.x = 10; // !!!
var o = { a: 1, b: 2, c: 3 };
for (key in o) {
// console.log(key); // a, b, c, x
if (o.hasOwnProperty(key)) {
console.log(key); // a, b, c
}
}
// after
Object.keys(o).forEach(function(key) {
console.log(key); // a, b, c
});
Array でも同じ。
// before
Array.prototype.x = 10; // !!!
var a = [1, 2, 3];
for (i in a) {
// console.log(i); // 0, 1, 2, x
if (a.hasOwnProperty(i)) {
console.log(i); // 0, 1, 2
}
}
// after
Object.keys(a).forEach(function(key) {
console.log(key); // 0, 1, 2
});
Object.defineProperty については後述。
DOM 編
その jQuery 本当に必要ですか?
querySelector(All) / getElementsByClassName
getElementById, getElementsByTagName に続き、 getElementsByClassName が使えるようになりました。
これらは、単一の ID ・クラス名・タグ名に使えます。
合わせて querySelector / querySelectorAll が普通に使えます。
こっちは、 jQuery のような ID/class/tag の複合検索ができます。
ただし、例えば querySelector を使えば getElementById などはいらないかというとそうではありません。
単発の ID への検索は getElementById の方が速いらしいです。
document.getElementById("#name"); // o
document.querySelector("#name"); // x
document.querySelector("div.wrapper a"); // o
http://caniuse.com/#feat=queryselector
http://caniuse.com/#feat=getelementsbyclassname
addEventListener / removeEventListener
イベントリスナの登録は、以下のように代入で書くことができます。
element.onclick = function() {...}
しかし、これには問題があります。
- コールバックを一個しか登録できない
- 上書きしてしまうと壊れる挙動があり得るため、代入前に調べないといけない
- 同じ DOM にたいして、複数のリスナを登録したい場合はよくある
そこで、どのような DOM へのリスナ追加も、 addEventListener を使うべきです。(attachEvent は忘れましょう)
element.addEventListener('click', function() { .... });
複数のリスナを登録しても全て発火しますし、第三引数で発火フェーズの制御もできます。
ただし、 removeEventListener 時に、消したいリスナの参照を持ってないといけないので、そこは注意が必要です。
(caniuse になかった)
https://developer.mozilla.org/ja/docs/Web/API/EventTarget.addEventListener#Browser_compatibility
DOMContentLoaded
長らく、 DOM に触れる JS の実行は window.onload の後に行われてきました。
しかし、これは DOM の構築後に埋め込む画像データやスタイルシートなど、およそ JavaScript からは触らないだろうコンテンツの取得もすべて終わってから発火します。
代わりに window.DOMContentLoaded を使うことで、 DOM の構築だけが終わった時点で発火されるため、 JS での初期化を少し早く行うことができます。
ちなみに jQuery の $() もこのタイミングを使っており、この段階で実行して失敗するスクリプトで無い限り、こちらがよいでしょう。
document.addEventListener("DOMContentLoaded", function() {
// jQuery の $(function(){}); とほぼ同じ
});
XHR2
XMLHttpRequest は Level2 が始まって機能も API も増えています。(もう Level2 とは言わないらしいです)
古い IE の Polyfill (ActiceX 的なあれ) はもういらないどころか、互換性のために残っている古い書き方から、新しい書き方に移行し、さらのバイナリも扱うことができます。
違いはたくさんありますが、とりあえず以下あたりを見るとどっちを意識して書かれているかわかります。
before
xhr.onreadystatechange = function (e) {
if (this.readyState === 4 && this.status === 200) {
// success
}
}
after
xhr.addEventListener('load', function () {
if (this.status === 200) {
// success
}
}
イベントも増えました。
- onloadstart;
- onprogress;
- onabort;
- onerror;
- onload;
- ontimeout;
- onloadend;
File / FileReader / Blob / Blob URL
この辺も使えるので、 Blob を XHR2 で multipart アップロードとかできますね。
FileSystem API はまだなので、サンドボックス環境でのローカルファイル読み書きはできません。
http://caniuse.com/#feat=fileapi
http://caniuse.com/#feat=filereader
http://caniuse.com/#feat=blobbuilder
http://caniuse.com/#feat=bloburls
CORS
XHR2 は CORS に対応しています。
といっても API が変わるわけではなく、つまりオリジン(schema, host, port)が違う URL への XHR が自動的に CORS として扱われます。
JS のレイヤでは変わる場所はありませんが、サーバでは変更が必要です。
注意点
- Access-Control ヘッダ
- Preflight リクエスト
まず、 Access-Control ヘッダですが、以下の種類があります。
- Access-Control-Allow-Origin
- Access-Control-Expose-Headers
- Access-Control-Max-Age
- Access-Control-Allow-Credentials
- Access-Control-Allow-Methods
- Access-Control-Allow-Headers
最低でも Allow-Origin で、リクエスト元のオリジンが含まれていないと、 XHR が成功しません。
したがってサーバは、必要最小限の範囲からのリクエストを許可する必要があります。(すぐ "*" を設定しようとするのはやめましょう)
他も必要に応じて設定します。
次に、 CORS では、以下の条件にあうリクエストを「シンプルなリクエスト」と定義しています。
要するに、従来の Form Submit で発生するリクエストと同質なものを指しています。
- GET または POST のみ
- POST で Content-Type が application/x-www-form-urlencoded、multipart/form-data、または text/plain のいずれか
- リクエストにカスタムヘッダ (X-Modified など) を設定しない
そして、 XHR2 が発するリクエストが シンプルなリクエストではない 場合に、 Preflight リクエストという
OPTION リクエストが発生し、リクエストの妥当性をサーバにまず問い合わせます。このリクエストの内容はいじれません。
この OPTION リクエストにきちんと応答しなければ CORS は失敗するので、ここも対応が必要です。
詳細は、別の解説に譲ります。
WebSocket
標準で WebSocket 通信が使えるようになります。
ただし、 Socket.IO がもういらないかどうかは別の話です、 WebSocket 未実装ブラウザを救うためのフォールバック提供だけであればいりませんが。
確かに最初はそういう役割も重要なものの一つでしたが、 WebSocket オブジェクトが使えても現実的にその通信が「通じる」とは限りません。
これからも、 WebSocket が通じない様々なネットワーク環境で、リアルタイムな通信を行うために、 Socket.IO のようなライブラリは必要になり続けるでしょう。
Web Storage
いわゆる localStorage/sessionStorage が使えます。
(indexedDB はちょっと怪しいです)
注意点は以下
- オブジェクトは入らないので、 JSON.stringify して入れる
- ストレージの容量は実装依存
- 容量を取得する API はない
- 溢れると例外は出るっぽいけど、そもそも溢れを気にするような使い方はしない方が良い
Web Workers
使えます。(shared web worker はまだ、 service worker はもっとまだ)
注意点は以下
- DOM(window, document etc) にはさわれない
- やりとりが messaging 経由になる
- 起動コストはやっぱりあるので、それがペイする処理に使う
イベントループを止めてしまう可能性があるような CPU ヘビーな処理が基本です。
例えば文字列/数値の演算などを行うのはこちらの方が良いでしょう。
なんでも Worker にすれば良いとは思いませんが。
history API / hash change
hash change は結構前から使えたのですが、 history API (history.pushState) も使えます。
に history API は Pjax をやるときに使われ、 SPA 的なアプリを作る場合は嬉しいでしょう。
使いこなすのは結構難しいと思いますが。
http://caniuse.com/#feat=history
http://caniuse.com/#feat=hashchange
JSON / Base64
使用頻度が高いけど、ライブラリを使う必要があったものが、ネイティブに実装された例です。
ネイティブの方が速いというのもあるでしょうが、 JS の場合はライブラリ分の JS ファイルサイズが減らせるのもメリットの一つです。
json2.js はもういりません。
JSON.stringify({ "a": 10 });
JSON.parse('{ "a": 10 }');
みんながこぞって作ってた、 Base64 も地味に実装されています。
window.btoa("hello") // "aGVsbG8="
window.atob("aGVsbG8=") // hello
(btoa = binary to ascii の意味)
http://caniuse.com/#feat=json
http://caniuse.com/#feat=atob-btoa
TypedArray
バイナリ扱えますよと。 MDN では大丈夫となってますが、 caniuse は Uint8ClampedArray だけ無いよって書かれてますが Uint8Array は使えるので頑張ればいけそう。
https://developer.mozilla.org/en/docs/Web/JavaScript/Typed_arrays
http://caniuse.com/#feat=typedarrays
relative URI
読み込みの話ですが。
CDN などからアセットファイルを読む時に、 scheme を省略すると元のドキュメントと同じ scheme で解釈されます。
これは RFC3986 に定義されていたのでずっと仕様にはあったようです。
IE6 からサポート されていたようですが、
IE8 以下で CSS が二回ダウンロードされるバグがあり。
2010 年くらいになって 認知されはじめ普及した ようです。
<script src="//example.com/script.js"></script>
<img src="//example.com/image.png">
<link rel="//example.com/style.css">
最近は HTTPS 化も広まっており、 HTTP からの更新や HTTP/HTTPS 併用などのケースもあると思うので、直にスキーマを書く必要が無い場合は、省略する方が良いでしょう。
迷いどころ編
使えるんだけど、使わない方がよさそうだったり、使うとハマりそうなので迷っている仕様です。
async / defer
async 属性と defer 属性は、概ね使用することができます。
async は、スクリプトのダウンロードを DOM の構築と並行して行うことができ、
defer は、スクリプトの実行を DOM の構築完了後まで遅延させます。
なのでたとえば、 以下のように記述すると head タグ内などに書いても、以降の DOM 構築をブロックせずにダウンロードし、 document.ready 的なタイミングで実行させることができるのです。
<script src="script.js" async defer></script>
(ちなみにこのサポート範囲では type のデフォルトは "text/javascript" なので書く必要はありません)
ところが、この場合問題になるのが実行順序です。
互いに依存する JS を複数ファイル読み込むような場合は、注意が必要というか、個人的には以下の状況以外では使わないほうがいいと個人的には思っています。
- asset pipeline などを通して完全に 1 ファイルに結合されている
- google analitics などの JS のように、完全に他と独立している
そうでない場合に async/defer を使いこなすのは難しいです。
将来的にモジュールの依存解決が JS レベルで解決すれば話は別ですが、そうでない場合、 完全にコントロールするには Body の下に順番に並べる従来のスタイルが安全ですね。
HTTP2 の push もからむと知見はまた変わりそう。
Object.defineProperty
先の Object.keys や for in 文で必ず hasOwnProperty しないといけない話などは、結局どこかでプロトタイプを拡張した場合に色々困るって言う話がほとんどなんですよね。
でも、 defineProperty で property descriptor というメタ情報を使って numerable 属性が変更できるようになりました。
それで解決という場合も一応あります。
ただ、 property descriptor はメタ情報です。
writable / configurable = false で存在を強制したり、
getter / setter で、普通のプロパティアクセスをフックして色々できてしまいます。
一番怖いのは、ちょっとかじった初心者が、「覚えたてのものを使いたくなる現象」で色々いじくった場合、およそ凶悪なオブジェクトが完成したりします。
基本的には設計をきちんと考えられるライブラリやフレームワークの作者などが、そのメタ情報で統一された正解を保証しながら使うのが良さそうで、アプリ開発などでがんがん使うたぐいのモノでもなさそうなイメージを持っています。
多くの場合、setter / getter の代わりに普通にメソッドを定義したり、 hasOwnProperty で prototype を回避すれば間に合う場合が多い。
Object.seal/freeze/preventExtensions
気持ちはわかるが、使ったことは無い。
どうしても無いとだめなユースケースってなんだろうなぁ。
この手の処理は、やる側はいいけれど、やられた側が結構面倒になったりすることが多いイメージしか無い。
知見求む。
http://kangax.github.io/compat-table/es5/#Object.seal
http://kangax.github.io/compat-table/es5/#Object.freeze
http://kangax.github.io/compat-table/es5/#Object.preventExtensions
bind
Function.prototype.bind() はコンテキストを外から差し込むことができます。
例えばコールバック内の this を保存するパターン。
function Foo() {
this.a = 'a';
}
Fn.prototype.fn = function() {
var _this = this;
setTimeout(function() {
console.log(_this.a);
}, 1000);
}
ちなみに、この _this
の部分は self
や that
派がいる。
以下の問題がある。
-
self
: 実は Window.self があり、被ってる。 -
that
: that ってなんだよ。 -
_this
: TypeScript が吐くコードとか、まあまあ穏健派。 -
thisArg
: bind の仕様 に合わせる派。
bind を使うとこの戦争に終止符を打てる。
Fn.prototype.fn = function() {
setTimeout(function() {
console.log(this.a);
}.bind(this), 1000);
}
個人的には bind() 派です。
ただし、これはきちんと理解していないと無駄にハマったり、乱用すると他人が追いにくいコードになるため、注意した方が良いのも事実。
ということで、 this を _this
に保存する感じがまだいいのかも。