はじめに
ECMAScript2015(第6版、通称ES6)が承認され、Babelも登場し、世はまさにES2015時代。なのだけど、JavaScript初級者としてはES5自体をちゃんと把握していなかったりするので、今さらながら調べてみることにした。
間違っている所があれば、ご指摘いただけると大変助かります。
ECMAScript5で追加されたもの
ECMAScript5 compatibility tableにて、ES5で追加された機能がどのブラウザに対応しているかが分かる。また、es5-shimというライブラリが、古いブラウザでES5の一部の機能が実装可能になる。
基本的には、IE9以上/iOS7以上、それ以外はモダンなブラウザであれば大抵対応している。
use strict
スクリプトの先頭、もしくは関数内の先頭に記載することでstrict modeで実行される。自分が書いているものに関しては基本的に書いたほうが良い。strict modeでは、今まで微妙だけど動いているようなコードがエラーとして扱われる。グローバル変数は作成できない。writableがfalseなオブジェクトへの代入は例外が発生する…など。
"use strict";
これから出るコードは、すべてstrict modeを前提とする。
Object.create
いきなり大物感がある…。JavaScriptはプロトタイプベース。というわけで、ES5以前のJavaScriptでは、Class
構文がないため、プロトタイプ・コンストラクタ・new演算子でクラスや継承を表現する。
ES5では、Object.create
が導入され new演算子を使わない書き方ができるようになった。
var camera = {
shutter_sound: "カシャ",
take: function() {
console.log(this.shutter_sound);
}
};
var nikon = Object.create(camera, { shutter_sound: { value: "パシャッ" } });
nikon.take();
Object.createとnew演算子の比較
Object.create
はコンストラクタが呼ばれないので、call
やapply
を使って明示的に呼び出す。
var Camera = function(sound) {
this.shutter_sound = sound || "カシャ";
}
Camera.prototype.take = function() {
console.log(this.shutter_sound);
}
var nikon = Object.create(Camera.prototype);
Camera.call(nikon, "パシャッ");
nikon.take();
var Camera = function(sound) {
this.shutter_sound = sound || "カシャ";
}
Camera.prototype.take = function() {
console.log(this.shutter_sound);
}
var canon = new Camera("バシュ");
canon.take();
Object.create
は、第1引数にプロトタイプオブジェクト、第2引数に追加するプロパティオブジェクトを渡すと、新しいオブジェクトを作成してくれる。第2引数は後述するObject.defineProperties
に対応するもの。正直、ES2015のclass
を使って、まっとうなクラスを実現するほうが良いと思う。こんなメモを書いておいてなんだけど…。
Object.defineProperty / Object.defineProperties
値(value) / 書き込み(writable) / 列挙(enumerable) / 再定義(configurable) / getter(get) / setter(set)を設定したプロパティを定義することができる。Object.defineProperties
で、複数一括で定義することも出来る。
データディスクリプタ
データディスクリプタは、valueとwritableを持つオブジェクト。
var obj = {};
Object.defineProperty(obj, "money", {
value: 100,
writable: false,
enumerable: true,
configurable: false
});
console.log(obj.money); // => 100
obj.money = 500; // => TypeError
アクセサディスクリプタ
アクセサディスクリプタは、getとsetを持つオブジェクト。プロパティに値を代入する際に、データのチェックをしたり出来る。
var obj = {};
(function() {
var kozukai_init = 0;
Object.defineProperty(obj, "kozukai", {
get : function(){
return kozukai_init;
},
set : function(newValue){
kozukai_init = (newValue <= 100) ? newValue : 100;
},
enumerable : true,
configurable : true
});
})();
obj.kozukai = 200;
console.log(obj.kozukai); // => 100 つらい
Object.getPrototypeOf
オブジェクトのプロトタイプを返す。実装依存の__proto__
が標準化されたもの。
function Obj() {}
Obj.prototype.piyo = function() {};
console.log(Object.getPrototypeOf(Obj)); // => [Function: Empty]
console.log(Obj.__proto__); // => [Function: Empty]
var obj = new Obj();
console.log(Object.getPrototypeOf(obj)); // => { piyo: [Function] }
console.log(obj.__proto__); // => { piyo: [Function] }
Object.keys
Object.keys
は、オブジェクトに存在する列挙可能なプロパティの配列を返す。newでオブジェクトを作る場合は for in
+ hasOwnProperty
と変わらないが、Object.create
を使う場合は、enumerableがfalseなため変わってくる。
var obj = Object.create({}, { piyo: { value: "piyo!" } });
obj.hoge = "hoge";
console.log(Object.keys(obj)); // => [ 'hoge' ]
var ps = [];
for (var p in obj) {
if (obj.hasOwnProperty(p)) {
ps.push(p);
}
}
console.log(ps); // => [ 'hoge' ]
以下の例だと、for in
+ hasOwnProperty
の場合は、piyoが列挙されている。enumerableはデフォルトでfalseだが、for in
はプロトタイプチェインのプロパティも列挙される。
var Obj = {
hoge: "hoge",
piyo: "piyo"
}
var obj = Object.create(Obj, { piyo: { value: "piyo!" } });
console.log(Object.keys(obj)); // => []
var ps = [];
for (var p in obj) {
if (obj.hasOwnProperty(p)) {
ps.push(p);
}
}
console.log(ps); // => [ 'piyo' ]
というわけでObject.keys
を使っていく。
Object.getOwnPropertyNames
Object.getOwnPropertyNames
は、Object.keys
とは違いenumerableの可否に関係なくすべてのプロパティの配列を返す。
var obj = Object.create({}, { piyo: { value: "piyo!" } });
obj.hoge = "hoge";
console.log(Object.keys(obj)); // => [ 'hoge' ]
console.log(Object.getOwnPropertyNames(obj)); // => [ 'piyo', 'hoge' ]
var Obj = {
hoge: "hoge",
piyo: "piyo"
}
var obj = Object.create(Obj, { piyo: { value: "piyo!" } });
console.log(Object.keys(obj)); // => []
console.log(Object.getOwnPropertyNames(obj)); // => [ 'piyo' ]
- Object.getOwnPropertyNamesは非列挙も取得するObject.keys
Object.freeze / Object.seal / Object.preventExtensions
いわゆる不変オブジェクトを作ることが出来る。JavaScriptにはprivateプロパティのようなアクセス制限がないため、ES5以前はクロージャーを使うなどして実現してきた。ES5の登場で、標準関数で簡単に実現できる。すごい。
クロージャーで隠蔽
var clojure = function() {
var i = 0;
return function() {
i++;
console.log(i);
};
}
var hoge = clojure();
hoge(); // => 1
freeze / seal / preventExtensionsで不変オブジェクト
freeze -> seal -> preventExtensionsの順に制約が緩くなる。
メソッド | 追加 | 削除 | 変更 |
---|---|---|---|
freeze | × | × | × |
seal | × | × | ◯ |
preventExtensions | × | ◯ | ◯ |
var obj = {
hoge: "hoge",
piyo: "piyo"
};
Object.freeze(obj);
// 追加はエラー
obj.fuga = "fuga"; // => TypeError
// 削除はエラー
delete obj.piyo; // => TypeError
// 変更はエラー
obj.hoge = "change"; // => TypeError
Object.seal(obj);
// 追加はエラー
obj.fuga = "fuga"; // => TypeError
// 削除はエラー
delete obj.piyo; // => TypeError
// 変更は可能
obj.hoge = "change";
Object.preventExtensions(obj);
// 追加はエラー
obj.fuga = "fuga"; // => TypeError
// 削除は可能
delete obj.piyo;
// 変更は可能
obj.hoge = "change";
なお、内部的にはObject.seal
はconfigurableをfalseに、Object.freeze
はwritableをfalseにしているため、Object.defineProperty
でも不変オブジェクトを実現できる。
今までJavaScriptで面倒だった不変オブジェクトが簡単に実現できるため良さそうだが、Arrayオブジェクトに対してfreezeしても、ChromeV8の場合TypeErrorが発生しない。オブジェクト自体は変わらないが、普通に値が返ってくる…。
ES2015のconst
も対応していないブラウザ(IE10以前/Safari)があるため、JavaScriptで定数や不変オブジェクトを作るのは大変。
Object.getOwnPropertyDescriptor
オブジェクトの指定プロパティを返す。
var Obj = {
hoge: "hoge",
piyo: "piyo"
}
var obj = Object.create(Obj, { piyo: { value: "piyo!" } });
console.log(Object.getOwnPropertyDescriptor(obj, "hoge")); // => undefined
console.log(Object.getOwnPropertyDescriptor(obj, "piyo"));
// => {
// value: 'piyo!',
// writable: false,
// enumerable: false,
// configurable: false
// }
Date.prototype.toISOString
YYYY-MM-DDTHH:mm:ss.sssZ
形式で返却する。タイムゾーンはUTCになる。
var today = new Date('12 Sep 2015 10:27 UTC');
console.log(today.toISOString()); // => 2015-09-12T10:27:00.000Z
// ES5以前の文字列メソッド
console.log(today.toString()); // => Sat Sep 12 2015 19:27:00 GMT+0900 (JST)
console.log(today.toDateString()); // => Sat Sep 12 2015
console.log(today.toTimeString()); // => 19:27:00 GMT+0900 (JST)
console.log(today.toLocaleString()); // => Sat Sep 12 2015 19:27:00 GMT+0900 (JST)
console.log(today.toLocaleDateString()); // => Saturday, September 12, 2015
console.log(today.toLocaleTimeString()); // => 19:27:00
Date.now
UTCの1970-0101 00:00:00から現在までのミリ秒を返す。以下の3つは全て同じ。
// ES5
console.log(Date.now()); // => 1442053952064
// ES5以前
var now = new Date();
console.log(now.getTime()); // => 1442053952064
// 四則演算を利用して数値変換する・普通にIntegerにキャストしてもOK
console.log(+new Date()); // => 1442053952064
Array.isArray
オブジェクトが配列であれば true 、でなければ false を返す。今までは非常に面倒くさい配列チェックが普通になった。
// ES5
var arr = [];
console.log(Array.isArray(arr)); // => true
// ES5以前
var arr = [];
console.log(Object.prototype.toString.call(arr) === '[object Array]'); // => true
ネイティブJSON
ES5で策定されたネイティブJSONとは、つまりJSON.stringify
とJSON.parse
の事。
var json = JSON.parse('{ "hoge": "piyo" }');
console.log(json); // => { hoge: 'piyo' }
console.log(JSON.stringify(json)); // => {"hoge":"piyo"}
Function.prototype.bind
関数にthisを渡して新しい関数を返す。簡単に言うと、apply
やcall
の関数を新しく生成して返す版。ややこしく感じるが、単にthisの指す先を指定オブジェクトに変更できるということ。
以下のはJavaScriptで最初にハマるthisの指す先が変わる挙動。ugu()
関数の中で呼ばれているthisはグローバルを指している。fuga
プロパティから直接呼ばれているthisはhoge
オブジェクトを指している。呼ばれ元によってthisの中身が変わる。
var hoge = {
piyo: "piyo",
fuga: function() {
function ugu() {
console.log(this.piyo); // => undefined
}
ugu();
console.log(this.piyo); // => piyo
}
}
hoge.fuga();
Function.prototype.bind
ではこのthisの中身を明示的に指定することができる。call
との違いはその場で実行するか、関数が返ってくるか。
var hoge = {
piyo: "piyo",
fuga: function() {
function ugu() {
console.log(this.piyo); // => piyo
}
ugu.bind(this)();
ugu.call(this); // これと同じ
console.log(this.piyo); // => piyo
}
}
hoge.fuga();
イベントリスナーなどでthisの中身が変わってしまう場合などに役に立つ。よく使うテクニックのthis self = this;
と同じようなことが簡単にできる。
var button = document.getElementById("hoge");
var ButtonSetting = function(button) {
this.name = "hoge";
this.click = function() {
button.addEventListener("click", this.event.bind(this));
};
this.event = function() {
console.log(this.name); // => hoge
};
};
var hoge = new ButtonSetting(button);
hoge.click();
String.prototype.trim
両端の空白を除いた文字列を返す。空白の対象は、正規表現のメタ文字\s
にあたる。半角スペースやタブコード、改行コード、全角スペース(u3000)など。
var text = " hoge ";
console.log(text.trim()); // => hoge
Array.prototype.indexOf / Array.prototype.lastIndexOf
Array.prototype.indexOf
は、配列の中に指定の値を検索し、最初にヒットした要素の添字を返す。Array.prototype.lastIndexOf
は、最後にヒットした要素の添字を返す。ヒットしない場合は-1が返る。比較には厳密な比較(===)が用いられる。
なお、線形探索なので普通にfor..in
しているのと、さほど変わらない。速度を出したい場合は(ソートされているなど前提条件が必要だが)二分探索を用いたほうが速い。
var arr = [1,2,3,4,5,6,5,4,3,2,1];
console.log(arr.indexOf(3)); // => 2
console.log(arr.indexOf(10)); // => -1
console.log(arr.lastIndexOf(3)); // => 8
Array.prototype.every
配列内のすべての要素が、渡した関数で評価され、全て通れば true、一つでも通らないと false が返ってくる。
function isNum(val) {
return (typeof val === 'number') ? true : false;
}
var arr = [1,2,3,4,5];
console.log(arr.every(isNum)); // => true
var arr = [1,2,"3",4,5];
console.log(arr.every(isNum)); // => false
Array.prototype.some
Array.prototype.every
とは違い、1つでも通れば true を返す。
function isNum(val) {
return (typeof val === 'number') ? true : false;
}
var arr = [1,2,3,4,5];
console.log(arr.some(isNum)); // => true
var arr = [1,2,"3",4,5];
console.log(arr.some(isNum)); // => true
Array.prototype.forEach
配列内の要素に対して、渡した関数を実行する。後述するArray.prototype.map
と並んで、ES5でもっとも使う機能じゃないかと思う。
function lists(val, index, array) {
console.log("arr[" + index + "] = " + val);
}
var arr = [1,2,3,4,5];
arr.forEach(lists);
// arr[0] = 1
// arr[1] = 2
// arr[2] = 3
// arr[3] = 4
// arr[4] = 5
Array.prototype.map
配列内の要素に対して、渡した関数を実行し、その結果を配列として返す。
function strUpper(val, index, array) {
return val.toUpperCase();
}
var arr = ['a', 'b', 'c'];
console.log(arr.map(strUpper)); // => [ 'A', 'B', 'C' ]
Array.prototype.filter
配列内の要素に対して、渡した関数を実行し、通ったものからなる配列を返す。Array.prototype.some
の配列が返ってくる版。
function isNum(val) {
return (typeof val === 'number') ? true : false;
}
var arr = [1,2,"3",4,5];
console.log(arr.filter(isNum)); // => [ 1, 2, 4, 5 ]
Array.prototype.reduce / Array.prototype.reduceRight
いわゆる畳み込み。配列の先頭から要素を次々とたたみ込むように、渡した関数を実行し、その結果を返す。また、第2引数に初期値を与えられる。Array.prototype.reduceRight
は逆に右から左に適用していく。
function plus(p, c, index, array) {
return p + c;
}
var arr = [1,2,3,4,5];
console.log(arr.reduce(plus)); // => 15 総和
console.log(arr.reduce(plus, 10)); // => 25 総和 + 10
プロパティへ文字列でアクセス
プロパティへのアクセスをドットではなく、ブラケットの文字列でアクセスできる。知らなかったけどこれES5からなのか。
var hoge = { piyo: "piyo!"};
console.log(hoge.piyo); // = piyo!
console.log(hoge["piyo"]); // = piyo!
つまり、プロパティ名を短縮してアクセスさせるようなこともできる。
var s = "split";
console.log("1,2,3"[s](",")); // => [ '1', '2', '3' ]
その他
- 予約後をプロパティ名として使用可能
- 変数名にゼロ幅スペースを使用可能
- parseIntで0始まりを8進数ではなく10進数として処理
- 第2引数で基数を必ず指定する方針が良いと思う
- undefinedを不変な値へ
ECMAScript5を振り返ってみて
ES5の場合、目玉はやはりArrayのイテレータ系かな。Objectも大きいけどES6の今の時代だと、素直にBabelでclass構文を使ったほうが素直でいいと思った。
結論:古いブラウザ捨てて、ES6をBabelで楽しもう。