ES2015(ES6)な時代だからこそ、ES5を改めて調べたJavaScript初級者のメモ

  • 396
    Like
  • 5
    Comment
More than 1 year has passed since last update.

はじめに

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はコンストラクタが呼ばれないので、callapplyを使って明示的に呼び出す。

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.stringifyJSON.parseの事。

var json = JSON.parse('{ "hoge": "piyo" }');
console.log(json); // => { hoge: 'piyo' }

console.log(JSON.stringify(json)); // => {"hoge":"piyo"}

Function.prototype.bind

関数にthisを渡して新しい関数を返す。簡単に言うと、applycallの関数を新しく生成して返す版。ややこしく感じるが、単に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

配列内のすべての要素が、渡した関数で評価され、全て通れば ture、一つでも通らないと 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で楽しもう。