Block Rockin’ Codes

back with another one of those block rockin' codes

Node で使える ECMA Script 5 の新機能

追記

11/9/24
Gistのリンクを本家Wikiに貼ってみました。
11/9/24
log 関数を修正しました。
11/7/10
JSON.stringify の第二引数 replacer について、補足しました。
11/7/14
os0x さんの指摘を反映しました。
  • String.trimRight、trimLeft は ECMA Script 5 非標準です。
  • JSON.stringify の第3引数には"\t"などの文字列も渡せます。
  • JSON.parse の第2引数 reviver について補足しました。
  • Array.prototype.forEach の第2引数 について補足しました。
  • "use strict" 時の Object.freeze 等の挙動について補足しました。
  • 「ECMA5 というのはちょっとおかしな略し方」について補足しました。
  • タイトルを修正しました。(旧「Node で使える ECMA5 の新機能」)

追記「ECMA5 というのはちょっとおかしな略し方」について

コメントにいただいたものを引用。

ところで、元記事からしてそうですが、ECMA5というのはちょっとおかしな略し方だと思います。
ECMAは欧州電子計算機工業会(European Computer Manufacturers Association)の頭文字で、その5という意味になってしまうかと。

まさしくその通りですね。しかし自分は普段 ECMA5 と言ってしまっていました。。
"ECMA Script 5" もし略すなら "ES5" が妥当ですね。
この記事はタイトルも含めて ECMA Script 5 に訂正しました。

本文

ECMA Script 5 で新しく実装されるいくつかの機能は、ブラウザでの実装がまばらなため、 CSJS では導入が難しいかもしれません。
しかし、 Node では、環境が一つなため、実装されているものは思う存分使うことができます。


今回は、Node の JS エンジンである V8 で実装されている ECMA Script 5 を紹介します。(自分でも一度使い方をまとめて書いておきたかった。)


内容はほぼ ECMA 5 Mozilla Features Implemented in V8 · joyent/node Wiki · GitHub の訳+補足になっていますが、順番はこの Wiki とは違います。
まず最初に一番重要(と個人的に思っている)な Object 関係の API を紹介し、
その後 Array, Date, String, JSON 等の API を解説します。


本文内のサンプルは、REPL で手軽に試せるものは REPL を、長くなるものはソースに書いて実行としています。全てのサンプルを(REPLにしてるのも含めて)一つのファイルにまとめたものを gist に上げました。自分のメモ用ですが、ご自由にどうぞ。

The sample usage of ECMA 5 Features Implemented in V8


バージョンは v0.5.0(unstable) と v0.4.9(stable) で確認してます。


ちなみに途中確認に使ってる log() は以下のようになっています。(console.log が長いってだけす)

var log = console.log;

Object

オブジェクトの定義方法が拡張されました。
まずオブジェクトリテラルには、 Setter, Getter を定義することができます。

var obj = {
  _str: 'default',
  get str() { // getter
    return this._str + '!';
  },
  set str(val) { // setter
    this._str = val.trim();
  }
};
log(obj.str); // "default!"
obj.str = ' asdf ';
log(obj.str); // "asdf!"

Property Descriptor Defaults

プロパティの定義には、プロパティディスクリプタを用いて属性の詳細を細かくコントロールできるようになります。
プロパティディスクリプタのメンバとそのデフォルトは以下の通り。

プロパティ 意味 デフォルト
value プロパティの値 undefined
get Getter undefined
set Setter undefined
writable 書き込み可能か false
enumerable 列挙可能か(for in で出るか) false
configurable プロパティディスクリプタを変更可能か false
Object.defineProperty(obj, prop, desc)

第一引数のオブジェクトに、第二引数のプロパティを、第三引数のプロパティディスクリプタを用いて定義します。

var obj = {};
Object.defineProperty(obj, 'num', {
  value: 10,
  writable: false,
  enumerable: false,
  configurable: false
});
log(obj.num); // 10
for (var i in obj) {
  log(i); // 出力されない
}
obj.num = 20;
log(obj.num); // 10 のまま
Object.defineProperties(obj, props)

第一引数のオブジェクトに新規プロパティを追加するか既存のプロパティを更新します。一度に複数のプロパティを操作することができます。

var obj = {};
Object.defineProperties(obj, {
  num: {
    value: 4,
    writable: false,
    enumerable: false,
    configurable: false
  },
  root: {
    get: function() {
      return Math.pow(this.num, 0.5);
    }
  }
});
log(obj.num); // 4
log(obj.root); // 2
Object.create(proto, props)

まず、第一引数をプロトタイプに持つオブジェクトを生成することができます。

// x, y を継承するオブジェクトを生成
var obj = Object.create({x: 10, y: 20});
log(obj.x); // 10
log(obj.y); // 20

この時、null を指定すると、何も継承しないオブジェクトを生成することもできます。
JS ではオブジェクトリテラル '{}' や 'new Object()' で生成したオブジェクトは、 Object.prototype を継承しているため、Object.create() に null を指定して生成したオブジェクトが最もピュア?(何も継承してない)なオブジェクトということになります。
こうしたオブジェクトは、Object.create() を使わないと、生成することができません。

var obj = Object.create(null);
log(obj); // 何も継承していない

var obj = Object.create(Object.prototype);
log(obj); // {} や new Object() と同じ

第二引数を指定することで、同時にプロパティを定義することもできます。

var obj = Object.create({x: 10, y: 20}, {
  z: {
    value: 30,
    writable: true
  },
  sum: {
    get: function() {
      return this.x + this.y + this.z;
    }
  }
});
log(obj.x); // 10
log(obj.y); // 20
log(obj.z); // 30
log(obj.sum); // 60
Object.keys(obj)

引数のオブジェクトの ownProperties かつ enumerable なプロパティのリストを取得します。

function o() {
  this.a = 1;
}
log(Object.keys(new o())); // [ 'a' ]
function p() {
  this.b = 2;
}
p.prototype = new o();
log(Object.keys(new p())); // [ 'b' ]

Object.create() を使ってるとこんな感じ。

var obj = Object.create({a: 10, b: 20}, {
  x: {
    value: 30,
    enumerable: true
  },
  y: {
    value: 40,
    enumerable: false
  }
});
log(Object.keys(obj)); // [ 'x' ]
Object.getOwnPropertyNames(obj)

引数に渡されたオブジェクトの ownProperty の名前の配列を返します。これには enumerable でないプロパティも含まれます。

var obj = Object.create({a: 10, b: 20}, {
  x: {
    value: 30,
    enumerable: true
  },
  y: {
    value: 40,
    enumerable: false
  }
});
log(Object.getOwnPropertyNames(obj)); // [ 'y', 'x' ]
Object.getPrototypeOf(obj)

第一引数に渡されたオブジェクトの prototype を返します。

var obj = Object.create({a: 10, b: 20}, {
  x: {
    value: 30,
    enumerable: true
  },
  y: {
    value: 40,
    enumerable: false
  }
});
log(Object.getPrototypeOf(obj)); // { a: 10, b: 20 }
Object.getOwnPropertyDescriptor(obj, property)

第一引数のオブジェクトが持つ、第二引数のプロパティのプロパティディスクリプタを取得します。

var obj = Object.create({a: 10, b: 20}, {
  x: {
    value: 30,
    enumerable: true
  },
  y: {
    value: 40,
    enumerable: false
  }
});
log(Object.getOwnPropertyDescriptor(obj, 'x'));
// { value: 30,
//   writable: false,
//   enumerable: true,
//   configurable: false }
Object.preventExtensions(obj)

引数のオブジェクトに新しいプロパティがこれ以上追加されることを防ぎます。

var obj = {a: 10, b: 20};
Object.preventExtensions(obj);
obj.x = 30; // TypeError: Can't add property x, object is not extensible
Object.isExtensible(obj)

引数のオブジェクトに対して、 Object.preventExtensions() が呼ばれているかを確認します。
呼ばれている場合は、拡張不可能なので、 false を返します。

var obj = {a: 10, b: 20};
log(Object.isExtensible(obj)); // true
Object.preventExtensions(obj);
log(Object.isExtensible(obj)); // false
Object.seal(obj)

引数のオブジェクトに対するプロパティの追加、削除、プロパティディスクリプタの変更を防ぎます。ただしプロパティの値は変更できます。

var obj = {a: 10, b: 20};
Object.seal(obj);
obj.a = 30;
log(obj.a); // 30
obj.x = 30; // TypeError: Can't add property x, object is not extensible
Object.isSealed(obj)

引数のオブジェクトに対して、 Object.seal() が呼ばれているかを判定します。

var obj = {a: 10, b: 20};
Object.seal(obj);
log(Object.isSealed(obj)); // true
Object.freeze(obj)

Object.seal() と同じですが、プロパティの値の変更も防ぎます。プロパティが完全に変更不可能になります。(変更しても反映されないだけで、エラーにはならないようです。)
通所は変更しても反映されないだけですが、 Strict Mode の場合はエラーになります。

var obj = {a: 10, b: 20};
Object.freeze(obj);
obj.a = 30; // (strict mode の場合) TypeError: Cannot assign to read only property 'a' of #<Object>
log(obj.a); // (strict mode じゃない場合) 10
Object.isFrozen(obj)

引数のオブジェクトに対して Object.freeze() が呼ばれているかを判定します。

var obj = {a: 10, b: 20};
Object.freeze(obj);
log(Object.isFrozen(obj)); // true

Object.prototype

isPrototypeOf(obj)

(ECMA 3, 5) このメソッドを呼び出されたオブジェクトが、引数に渡されたオブジェクトのプロトタイプだった場合、 true を返します。

var proto = {a: 10, b: 20};
var obj = Object.create(proto);
log(proto.isPrototypeOf(obj)); // true
非標準メソッド

Object.defineProperty() を用いて、既存のオブジェクトにもアクセサを設定することが出来ますが、ECMA Script 5 以前では、 __defineGetter__ 等を用いてこれらを行っていました。
Node でもこれらのメソッドが使用できます。しかし、メソッド名の通り非標準なので、使用には注意してください。紹介だけします。

  • __defineGetter__(name, callback)
  • __defineSetter__(name, callback)
  • __lookupGetter__(name)
  • __lookupSetter__(name)

Function.prototype

bind(thisArg[, arg1[, argN]])

まず関数内の 'this' に引数に取った thisArg を束縛します。

var f = function() {
  return this.a + this.b;
};
log(f()); // NaN
var g = f.bind({ a: 10, b: 20 });
log(g()); // 30


オプションで、第二引数以降に渡した値を、その関数の引数とすることができます。

var f = function(c) {
  return this.a + this.b + c;
};
log(f()); // NaN
var g = f.bind({ a: 10, b: 20 }, 30);
log(g()); // 60

これを用いると、カリー化が簡単に実現できます。

var f = function(c, d) {
  return this.a + this.b + c + d;
};
log(f()); // NaN
var g = f.bind({ a: 10, b: 20 }, 30);
log(g(40)); // 100

簡単に説明すると、引数を二つ取るもともとの関数 f(c, d) から、一つの引数を事前に割り当てた、引数を一つとる新しい関数 g(d) を作ったということ。
詳しくは「カリー化」とか「部分適用」なんかでググってください。

JSON

JSON.stringify(obj [, replacer [, space]])

引数に取る JSON を文字列にシリアライズします。

> JSON.stringify({ a: 1 });
'{"a":1}'

実は第二、第三引数もあります。
説明はこちらから引用させていただきます。 http://d.hatena.ne.jp/Syunpei/20090907/1252301929

replacerは省略可能で、function(key, value)と言うシグネチャの関数オブジェクトを渡します。JS→文字列の変換ルーチンを独自に提供できます。
spaceは、結果の文字列を人間が読みやすくするための、インデントの数を指定します。

replacer を自分で定義することは、まず無いでしょう。。
replacer も使いどころはあるようです。ごめんなさい。



@Constellation さんの gist からサンプルを引用させていただきます。(この記事のサンプルにも追記しました。)

log(JSON.stringify([1,2,3], function replacer(key, value) {
  if (!Array.isArray(value)) {
    return value;
  }
  var len = value.length;
  var result = { length: len };
  for (var i = 0; i < len; ++i) {
    result[i] = value[i];
  }
  return result;
}));
// {"0":1,"1":2,"2":3,"length":3}

他にも こんな使い方 もあるようです。


space は出力に数値で指定した深さのインデントを施します。数値だけではなく '\t' などの文字列も渡せます。例えば、以下のように使えます。

> log(JSON.stringify({ a: 1, b: 2}, null, 2));
{
  "a": 1,
  "b": 2
}
> log(JSON.stringify({ a: 1, b: 2}, null, 4));
{
    "a": 1,
    "b": 2
}
> log(JSON.stringify({ a: 1, b: 2}, null, '\t'));
{
        "a": 1,
        "b": 2
}
JSON.parse(string [, reviver])

JSON バリッドな文字列を JSON オブジェクトにパースします。

> var s = JSON.stringify({ a: 1, b: 0 });
> JSON.parse(s);
{ a: 1, b: 0 }

第二引数の reviver は JSON.stringify の replacer と同様、パースのロジックを自分で定義することができます。以下は、値が 0 か 1 だったらそれぞれを真偽値に変更する例です(こちら を参考にさせて頂きました)。

var s = JSON.stringify({ a: 1, b: 0 });
log(
JSON.parse(s, function(key, value) {
  if (value === 0)
    value = false;
  else if (value === 1) {
    value = true;
  }
  return value; // { a: true, b: false }
})
);

String.prototype

trim(), trimRight(), trimLeft()

文字列の左・右・両端からホワイトスペースを除去します。(ECMA Script 5 では trimRight, trimLeft は非標準です。)

> ' abc '.trim();
'abc'

> ' abc '.trimRight();
' abc'

> ' abc '.trimLeft();
' abc'

Array

Array.isArray(array)

対象のオブジェクトが配列かどうかを検査し、配列なら true を返します。

> Array.isArray([]);
true

Array.prototype

indexOf(value)

配列内に指定した値が有った場合、その最初の添字を返します。無い場合は -1 を返します。

> ([1, 2, 2, 3]).indexOf(2);
1
lastIndexOf(value)

配列内に指定した値が有った場合、その最後の添字を返します。無い場合は -1 を返します。

> ([1, 2, 2, 3]).lastIndexOf(2);
2
filter(callback)

全ての要素にコールバック関数を実行し、 true になる値からなる新しい配列を返します。

> ([1, 2, 3, 4]).filter(function(x) {
... return x > 2;
... });
[ 3, 4 ]
forEach(callback[, thisObject])

全ての要素に対してコールバック関数を実行します。

> ([1, 2, 3]).forEach(function(x) {
... log(x * 2);
... });
2
4
6


第二引数で、コールバック内での this の値を指定することができます。

> ([1, 2, 3]).forEach(function(x) {
... this.log(x * 2);
... }, console);
2
4
6
every(callback)

全ての要素にコールバック関数を実行し、全ての要素がそのコールバック関数の条件を満たしたら true を返します。

> ([1, 2, 3]).every(function(x) {
... return x > 0;
... });
true
> ([1, 2, 3]).every(function(x) {
... return x > 1;
... });
false
> 
some(callback)

全ての要素にコールバック関数を実行し、少なくとも一つがそのコールバック関数の条件を満たしたら true を返します。

> ([1, 2, 3]).some(function(x) {
... return x > 2;
... });
true
> ([1, 2, 3]).some(function(x) {
... return x > 3;
... });
false
map(callback)

全ての要素に対してコールバック関数を実行した結果からなる、新しい配列を返します。

> ([1, 2, 3]).map(function(x) {
... return x * 2;
... });
[ 2, 4, 6 ]
reduce(callback[, initialValue])

配列の先頭から順に二つの要素を取り出し、コールバック関数を適用して一つの値に集約して行きます。引数にはコールバック関数以外にも初期値を渡すことができます。

> (['a', 'b', 'c']).reduce(function(x, y) {
... return x + y;
... });
'abc'
> (['a', 'b', 'c']).reduce(function(x, y) {
... return x + y;
... }, '*');
'*abc'
reduceRight(callback[, initialValue])

配列の最後から順に二つの要素を取り出し、コールバック関数を適用して一つの値に集約して行きます。引数にはコールバック関数以外にも初期値を渡すことができます。

> (['a', 'b', 'c']).reduceRight(function(x, y) {
... return x + y;
... });
'cba'
> (['a', 'b', 'c']).reduceRight(function(x, y) {
... return x + y;
... }, '*');
'*cba'

Date

Date.now()

現在時刻を数値(Unix Time)で返します。

> Date.now();
1310205452263

Date.prototype

toISOString()
> (new Date()).toISOString();
'2011-06-30T23:01:48.320Z'

実装されてない機能

あとがき

ECMA Script 5 の機能が使えると便利な面が多くあります。ブラウザの互換性を気にせずに使えるのは JS 好きには結構嬉しいですね。
これらを使えば、コードの書き方はもちろん、ライブラリの設計方法等も変わっていくでしょう。そういうときの参考になればと思います。

また、現時点で ECMA Script 5 について最も詳細に記述されているのは、JavaScript The Definitive Guide 第6版 (サイ本6版)でしょう。
この本には、ブラウザで使う場合のことも書かれていて(例えば Object.create() を自分で実装するとか)、とても参考になります。



なにか間違いを見つけたら教えてください。