無駄と文化

実用的ブログ

指定した範囲の整数配列の作り方 in JavaScript ES6

この記事は原文: Array Number Ranges in JavaScript ES6 を、著作者であるDavid Arvelo氏の許可を受けて翻訳したものです。

f:id:todays_mitsui:20151103132111p:plain

注意
この記事の公開後、ECMAScriptの技術委員会によりリスト内包表記はES6の仕様から削除されました。
この記事はサポート予定の他の機能についても言及しているため、未編集のままにされています。


ここ数日間、私はECMAScript 6について深く掘り下げて学ぶ日々を過ごしました。ES6は、ES5と比較してとても素晴らしく、ES6が近い将来に使えるようになる事によるWebの可能性に興奮を覚えました。

実行環境がどうであれ、あなたがJavaScriptプログラマであれば(パッと見は難しく感じても)ES6の新機能を学ぶことには大きな価値があります。なぜなら、イテレータ, ジェネレータ, クラス定義, ブロックスコープ, リスト内包表記 などの他言語から影響を受けた新機能と、変数の分割代入, Default Parameters, 可変長引数, 配列の展開, 強化されたObjectリテラル, そして 関数のアロー記法などの既存のコードをより簡単に書けて・読みやすくするための糖衣構文によって、JavaScriptをもっともっと直感的に書けるようになるからです。


私はES6のコードを書いてみるなかで昇順に並んだ整数の配列が必要になりました。同時に、ES6にはそういう配列のための新しいコンストラクタがあるんじゃないか?と思ったのです。

たとえば、Pythonならシンプルに、

array = range(1,100)

と、このように書けば、1以上100未満の整数の配列を(または、Python風に云えばリストを)得ることができます。
ES6にはこのようなrange()関数はありませんが、大丈夫。Python風の解法があるじゃないですか。そう、リスト内包表記です。:D


私より詳しい人の良さげなやり方がないかWeb上を探し回っていると、Ariya Hidayat氏の興味深い記事を見つけました。どうやら整数だけでなくあらゆる型のシーケンシャルな配列を作るためにリスト内包表記を使う事ができるようです。彼の場合、アルファベットの配列を作るためにリスト内包表記使っていました。

[for (i of Array.apply(0, Array(26)).map((x, y) => y)) String.fromCharCode(65 + i)].join('');
// => "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

ES5のArray.apply()とES6の.map()やアロー記法などを組み合わせた、冴えたやり方がとても気に入りました。Array(num)という文法を利用して要素が空の配列を作る方法も「なるほど!」です。

しかし、もっと短い書き方で整数配列を作れるかどうか、私はそこに興味があります。ES6のArray.prototype.fill()メソッドとAriya氏の方法を組み合わせて、こんな風に書くのはどうでしょう。

[for (j of Array(100).fill(0).map((v,i) => i)) j]
// => [0, 1, 2, ..., 99]

少しは短く書けましたが、ただ整数配列を取得するだけにしてはまだ冗長ですね。 ちょっと考えて、結局このように書き直しました、

[for (i of Array(100).keys()) i]
// => [0, 1, 2, ..., 99]

いい感じでしょ?
これにはES6で登場した新しいArray APIを使っています。Array.prototype.keys()は、for .. of構文と組み合わせて使うことができるイテレータを返します。イテレータは配列の全ての要素に渡ってキーを返すので、この場合であれば0からarray.length-1までの全ての整数が返されるわけです。最高ですね!


Object.keys([1,2,3])としても同じようなことができますが、Array(10)のような記法と組み合わせて動的に範囲指定した配列を作る場合には思った通りに動作しません。(最初に述べたとおり)Array(num)で作られるのは要素が空の 配列 です。

この方法が素晴らしいのは、整数列の始まりの値値の間隔を変えたくなったらリスト内包表記の最後にあるiの部分をいじるだけで済むことです。または、配列の長さを変えたいときにはArray()の引数で指定してやるだけですね。

[for (i of Array(97).keys()) i+3]
// => [3, 4, 5, ..., 99]

[for (i of Array(100).keys()) i*10]
// => [0, 10, 20, ..., 990]

[for (i of Array(100).keys()) i*10+10]
// => [10, 20, 30, ..., 990, 1000]

まだまだPythonのrange()ほど読みやすくはないけれど、ちゃんと動いていますね。
もし、0からある値までの配列がほしいだけなら、もっと簡単に書くこともできます。

Array.from(Array(100).keys())
// => [0, 1, 2, ..., 99]

ES6で導入されたArray.from()は配列のようなオブジェクトを配列に変換します。
これは、arguments 変数DOM NodeListsを配列に変換したいといった、JavaScriptでよくある要求に応えるエレガントな方法です。

/* _Working with NodeLists_ */

// ES5でのやり方
var slice = [].slice;
var nodes = document.querySelectorAll('p');
var classes = slice.call(nodes).map(function (element) {
  return element.className;
});

// ES6ならこうだぜ、みんな!
var nodes = document.querySelectorAll('p');
var classes = Array.from(nodes).map(element => element.className);


/* _Getting function arguments_ */

// ES5でのやり方
function doSomething () {
  var args = [].slice.call(arguments);
  ...
}

// ES6でのやり方(まぶしく輝いて見える)
function doSomething () {
  var args = Array.from(arguments);
}

// Rest Parametersを使えばもっと簡単に...
function doSomething (...args) {}

ゴイスーですねッ!!あまりに素晴らしいもんで話が脱線しちゃったけど。


範囲を表現する方法をもう一つお教えしましょう。なんと、無限の範囲までも表現できるやり方です。実際には、まぁ、少なくともメモリがオーバーフローするまでは :)
このテクニックにはジェネレータを使っています。私はこの方法をMDN Wikiで見つけたo.Oから学びました。みんな、ありがとう!

// ジェネレータ関数を定義
// 指定した範囲の整数列のイテレータを返す
function* range (begin, end, interval = 1) {
  for (let i = begin; i < end; i += interval) {
    yield i;
  }
}

// 試してみよう!

// 配列変数に格納
var seq = [for (i of range(20,50,5)) i];
// => [20, 25, 30, 35, 40, 45]

// 全体をメモリに格納することなく順次使うことができる
for (i of range(1,7,2)) {
  console.log(i);
}
// => 1
// => 3
// => 5

// こっちの書き方のほうがPythonっぽい :)
var iter = range(12,19,3);

var num;
num = iter.next(); // => { value: 12, done: false }
num = iter.next(); // => { value: 15, done: false }
num = iter.next(); // => { value: 18, done: false }
num = iter.next(); // => { value: undefined, done: true }
num = iter.next(); // => { value: undefined, done: true }

このシンプルさ。そして、(配列を返す通常の関数と比べたとき)ジェネレータが値を返すためにほとんどメモリを消費しないという事実。美しいですね。
範囲の終端としてどれだけ大きな数を渡しても(たとえそれがInfinityであっても!)、あなたがイテレータを意図的に配列に変換しない限り、実際に巨大な配列が作られることはありません。そのため、ユーザーのマシンをクラッシュさせたり、UXを損なうほどに動作が重くなるようなこともありません。
唯一の注意点は、この関数はES6の標準仕様のイテレータfor..of ループと合わせなければ使えない事ですね。

では、楽しんで!