JavaScriptの配列
以前、JavaScriptのthisについて学びました。thisは最初からよくわからない物という扱いでしたが、解っているつもりになっていて調べ出すと以外に奥が深いのは配列(Array)です。そこでこれについての情報を整理しました。
また、関連するSymbol.IteratorやArrayBuffer、Uint8Arrayについても触れています。
基本
- []
JavaScriptの配列は[]で初期化します。初期化時に値をセットすることもできます。JavaScriptの配列は型を区別しませんので、ひとつの配列の中に、数値、文字列、boolean、子配列、オブジェクト等を織り交ぜて生成することができます。
let arr1 = []; let arr2 = [1,2,3,4]; let arr3 =['a',5,null,undefined,true,[6,7],{ data:'bcd'}];
- push
psuhメソッドで、配列の最後に値を追加することができます。メソッドの引数には,で区切って複数の値を指定することができます。戻り値として追加後の配列のlengthが返ります。
arr1.push(11); arr2.push('e','f','g');
- pop
popメソッドで、配列の最後の値を取り除くことができます。戻り値として取り除いた値が返りますが、もともと何もなかった場合はundefinedが返ります。
arr1.pop();
- unshift
unshiftメソッドで、配列の最初に値を追加することができます。メソッドの引数には,で区切って複数の値を指定することができます。戻り値として追加後の配列のlengthが返ります。
arr1.unshift(12); arr2.unshift('h','i','j');
- shift
shiftメソッドで、配列の最初の値を取り除くことができます。
arr1.shift();
- indexOf
配列の値を引数に指定して、インデックスを得るメソッドです。
配列中に存在しない場合は-1を返します。配列の中に含まれるかをboolean値で判定するメソッドにincludesがありますが、こちらをチェックすることもできます。
arr2.indexOf('z');
- join
配列を文字列として扱い引数に渡した文字で区切って1行の文字列として返します。Stringオブジェクトのsplitメソッドの逆の事を行うイメージです。引数に何も設定されていない場合,(半角コンマ)が区切り文字に利用されます。区切りが不要な場合は空文字('')をセットします。
- filter
引数に条件となる関数を指定して、関数内でtrueを返したもののみで構成される新しい配列を作成します。
arr1.filter(v=> v > 2);
ちなみに「v=> v > 2」の部分を従来の関数に変換すると次のようになります。アロー関数では引数がひとつの際に引数のカッコを省略でき、一行で戻り値があらわせる場合は{}とreturnの文字を省略できます。
配列の値が2を超える際にtrueが返るので、2を超える値が新しい配列となり返ります。
arr1.filter(function(v) { return v > 2; });
- find
filterと同様の関数を渡し、最初にtrueとなった値を返して終了します。
- findeIndex
findの返る値がインデックスになったバージョンです。
- map
引数に変換用の関数を指定して、元の配列のそれぞれの値をその関数に通した新たな配列を作成します。
arr1.map(v=> v + 100);
- slice
開始インデックスと終了位置を指定して新しい配列を作成します。
終了が省略された場合はその位置から最後までになります。
開始、終了ともにマイナスを指定すると後ろからの位置を示しますが、開始位置を指定する場合は最後の文字の位置が-1、終了位置を指定する場合は最後の文字は指定できず(開始位置だけの省略形を使うことで代替できます)その最後から2番目の文字を示すのが-1となります。
また、全引数を省略するとそれぞれの値をシャロ―コピーします。シャロ―コピーに関しては後述します。
arr1.slice();
- splice
配列の場所を指定した削除や挿入に使います。
第1引数に渡したインデックスから、第2引数渡した数の要素を削除します。第3引数移行に値があった場合はそれらをその場所に挿入します。
arr1.splice(1,1);//[1]を削除 arr1.splice(1,0,'i');//[1]の後に文字列'i'を挿入 arr1.splice(1,0,'j','k');//[1]の後に文字列'j'と'k'を挿入 arr1.splice(1,0,['l','m']);//[1]の後に配列['l','m']を挿入
- reverse
配列を逆転させます。
- sort
配列をソートします。sort用の関数を引数に渡すことで、オリジナルの順位づけもできます。
戻り値はコピーされた値ではなく元の値を置き換えた配列です。なので戻り値を受けなくてもソートの結果は得られます。
function mySort(a,b) { if (condA) { //-1を返すと aがbより小さいと判断されます return -1; } else if(condB) { //1を返すと aがbより大きいと判断されます return 1; } else { //0を返すとaとbは同じ値だと判断されます。 return 0; } } let newArr = arr1.sort(mySort); console.log(newArr===arr1);//true
- values
Array Iterator オブジェクトを返します。Iterator(イテレーター)については後述しますが、値を抽出する順に処理する際に用います。
const arr4=[1,2,3]; const it = arr4.values(); for (const v of it) { console.log(v); // 1 2 3 }
- forEach
各要素を引数に渡した関数に通します。関数の引数に設定できる要素は、currentValue(現在値)、index、元の配列です。forEachの第2引数としてthisの値に設定するオブジェクトも指定できます。
PHPの構文に馴染みのある人にとっての"foreach"はこちらより「for...of」文の方が近いかもしれません。
- concat
ふたつ以上の配列をまとめて別の新たな配列にします。
また変数が配列かどうかは、Array.isArray(param);を使うことでboolean値での判定が可能です。
少し変わった記法
配列関連の記法で筆者が何をしているかよくわからなかったものに次のようなものがありました。
- ...array
配列の前に...がついた書き方でスプレッド構文というそうです。
残余引数(可変長の引数)を受け取ることができる関数の定義や、実際に関数に渡す際に利用します。配列を,区切りで展開した形で渡すのと同じ意味になります。
let arr5=[1,3,5]; const multiply =(...args)=> { if (args.length == 0) { return 0; } let ret = args[0]; for (let i = 1; i < args.length; i++) { ret *= args[i] } return ret; } console.log(multiply(...arr5));//15 console.log(multiply(arr5[0],arr5[1],arr5[2]));//これと同じ意味
- let [param] = array;
定義の際に[](角カッコ)で囲った変数に、初期値として配列を渡すと最初の値だけが変数にコピーされます。
配列だけではなくイテラブル値なら初期値として渡すことができます。なので文字列をわたすと最初の1字だけとなります。
Iterator(イテレーター)
先ほどからイテレーターやイテラブルといった言葉がでてきましたが、イテレーターは反復動作の方法を保持しているオブジェクトです。for ...of文などでの出力順を決定します。
配列にはデフォルトのイテレーターが存在するため、何も設定せずにfor ...of文を使うことができます。
const arr6=[10,20,30];
for (const v of arr6) {
console.log(v); // 10 20 30
}
オブジェクトにはデフォルトではイテレーターは存在しないためfor ...of文は使えませんが、実装することにより利用可能になります。
その際Symbolを使うので先にそれについて説明しておきます。Symbolは、ひとつのプリミティブ(stringやnumberといった基本的な値として扱われる)データ型です。
通常使う場合はlet symkey = Symbol("debug-name");とすることで変数symkeyにSymbolが定義されます。この後、別に定義したオブジェクトに対してobj[symkey]=value;するとオブジェクトのsymkeyプロパティに対してvalueを設定できます。通常はSymbolでなく数値か文字列をセットすると思いますが、Symbolを定義して使うことによって他とは絶対に重複しない値を指定したことと同じになります。
Symbol.iteratorはそのようなSymbolのうち自動に値が設定されているもので、オブジェクト(配列を含む)のデフォルトのイテレーターを指定する際に利用します。たとえば、arr7という配列があった際、arr7[Symbol.iterator]にはデフォルトのイテレーター関数がセットされています。
なので配列に対して[Symbol.iterator]プロパティの値を変更することで、イテレーターを変更することができます。おそらく実際のものとは違うと思いますが、デフォルトで動くのと同じ挙動になるようにイテレーターオブジェクトを実装すると次のようになります。
const arr7=[3,2,1,4,5,6];
arr7[Symbol.iterator] = function() {
return {
current: 0,
last: arr7.length,
next() {
if(this.current < this.last) {
let v = arr7[this.current];
this.current++;
return { done: false, value: v };
} else {
return { done: true };
}
}
}
}
for (const v of arr7) {
console.log(v);
}
イテレーターは、nextで次の値を取りに行き、値が格納されているvalueプロパティと終了か否かを示すdoneプロパティを返します。
iteratorにはジェネレーター関数を渡すこともできます。ジェネレーター関数はyieldで値を返した後そこで一時停止する関数です。再び呼ばれるとその地点から再開し、関数の終わりに達するとイテレーターでオブジェクトのdoneにtrueを設定したのと同じ扱いになります。
ジェネレーター関数の定義ではfunctionの後に*を付けます。また、アロー関数内ではyieldキーワードは使えません。
通常とは逆順で値を取り出すイテレーターをセットしてみます。
const arr8=[3,2,1,4,5,6];
arr8[Symbol.iterator] = function* () {
let wk = arr8.slice();//wkに配列をコピー
while(true) {
let v = wk.pop();
if (typeof v === "undefined") {
break;
}
yield v;
}
}
for (const v of arr8) {
console.log(v);
}
ちなみに、オブジェクトではデフォルトでfor ...ofはは使えないという話がありましたが、for ...inを使うとイテレーターを設定しなくても、オブジェクトのプロパティ名でループを作ることができます。この場合出力順は保証されていません。また、for ...inは配列でも使うことができこの時キーにはプロパティ名の代わりにインデックスが入ります。
const obj={ a: 1, b: 2, c:3 };
for (const key in obj) {
console.log(obj[key]); // 1 2 3(順序不定)
}
配列のコピー
配列のコピーは基本的にシャロ―コピー(Shallow Copy)です。
例えば、arr9 = arr10とした時、参照している場所は一緒になるのでarr9[0]を変更するとarr10[0]の値も変わり、その逆もしかりです。
ただしsliceなどで抽出する際は、配列のそれぞれの要素に対してシャロ―コピーが行われるので、その値がプリミティブ値(単純な数値や文字列)だった場合は独立して存在します。
sortやreverseは戻り値として配列を受け取れますが、新たな配列を作成することはなく元の配列内での入替になるので、プリミティブ値でも共通の値になります。
let arr11 = ['ab',['cd','ef']];
let arr12 = arr11;
let arr13 = arr11.slice();
let arr14 = arr11.map(v=>v);
arr11[0]='lm';
arr11[1][0]='no';
console.log(arr11);//['lm',['no','ef']]
console.log(arr12);//['lm',['no','ef']]
console.log(arr13);//['ab',['no','ef']]
console.log(arr14);//['ab',['no','ef']]
let arr15 = [3,5,4,2,1];
let arr16 = arr5.sort();
let arr17 = arr5.reverse();
console.log(arr15);//[5,4,3,2,1]
console.log(arr16);//[5,4,3,2,1]
console.log(arr17);//[5,4,3,2,1]
arr17[0]=6;
console.log(arr15);//[6,4,3,2,1]
console.log(arr16);//[6,4,3,2,1]
console.log(arr17);//[6,4,3,2,1]
ArrayBuffer
ArrayBufferはバイナリデータ用の入れ物で、「バイト配列」と呼ばれたりします。1バイト単位のデータが配列になって格納されています。Arrayとついていますが先のArrayとは違うオブジェクトで、このオブジェクトの中身を直接操作をすることはできません。
用意されているインスタンスメソッドは指定された範囲をコピーしてArrayBufferを返すsliceしかありません。またインスタンスプロパティも長さが入るbyteLengthしかありません。
ArrayBufferデータを操作したい場合は、DataViewやUint8Array等のオブジェクトを使います。
Uint8Array
ArrayBufferのデータ操作をする前にUint8Arrayについて触れておいた方が理解しやすいと思うので先に説明しておきます。
Unit8Arrayは型付配列と呼ばれるものです。
この記事の最初に、「JavaScriptの配列は型を区別しない」と書きましたが、こちらは型を区別する配列になります。Unit8は8bitのUnsined Intの意味で、符号なしの8ビットの整数の配列であることを表しています。
この配列の仲間には次のようなものがあります。
- Int8Array
- Uint16Array
- Int16Array
- Uint32Array
- Int32Array
- Float32Array
- Float64Array
- BigInt64Array
ArrayBufferは1バイト単位、つまり8ビットなのでUint8Arrayと組み合わさることが多いですが、たとえばInt16Arrayを使ってArrayBufferを操作することもできます。
Int16では、256という値は0x01と0x00の2バイトのセットで保持されています。算数の世界では大きい桁の方を左側に書きますが、バイト配列化された世界はそれが統一されていません。リトルエンディアンなら小さい桁の方から並べて0x00,0x01とし、ビッグエンディアンなら大きい桁の方から並べて0x01,0x00とします。Int16では2バイトなので順番は、バイトが増えると他にも並べ方があります。これらはバイトオーダーと呼ばれることもあります。
Int16Arrayを使ってArrayBufferに256と258をセットします。
let ab1 = new ArrayBuffer(4);
let i16 = new Int16Array(ab1,0,2);
i16[0]=256;
i16[1]=258;
console.log(ab1);
ArrayBufferをconsole.logから覗くと、ちゃんと値がセットされていることが確認できます。
これをUnit8Arrayとして読みだしてみると、リトルエンディアン環境では、256(0000 0001)と258(0002 0001)の上位バイトと下位バイトが分割され(0 1 2 1)と表示されます。
またこれらのオブジェクトのbufferプロパティには参照しているArrayBufferが入ります。次の例では、i16.bufferは参照しているab1を返しています。
...(上記コードからの続き)
let i8 = new Uint8Array(i16.buffer,0,4);//i16.bufferをab1としても同じ
for (let i = 0; i <i8.length; i++) {
console.log(i8[i]); // 0 1 2 1
}
ちなみに別記事では、このUint8Array使ってセキュアでランダムな文字列を生成しました。よろしければ合わせてお読みください。
DataView
DataViewを使うと先の型付配列より柔軟にArrayBufferを扱えます。
オフセット(開始位置)を指定して、UInt16やInt32など任意の値をエンディアンの指定と共にセットしたり読みだしたりすることができます。set時の引数は(位置,値)、get時の引数は(位置)となります。8ビット以外のメソッドでは続くオプション引数としてリトルエンディアンか否かのboolean値をセットできます。
let ab2 = new ArrayBuffer(16);
let dv1 = new DataView(ab2,2,12);//3バイト目から12バイト取得
//切り取った位置の確認
console.log(dv1.byteOffset);//2
//切り取ってもbufferの参照は全体
console.log(dv1.buffer.byteLength);//16
//ab2の3バイト目からInt16の258の値をリトルエンディアンで書き込む
dv1.setInt16(0,258,true); //0002 0001
//書き込んだ値をビッグエンディアンで読み込んでみる
dv1.getInt16(0,false);//256 * 2 + 1 = 513
ArrayBufferの使い道
このように操作するのが大変なArrayBufferですが、どういう場合に使われているかというと、Webブラウザでファイルを生成してダウンロードするような場合に使われています。
ArrayBufferはファイル同様の扱いのBlobオブジェクトを生成する際に渡せる引数です。
たとえば、ASCIIコードを使って「test!」という文字列のテキストを作成してダウンロードさせる場合はブラウザのScriptで次のように書けます。
<script>
let ab3 = new ArrayBuffer(5);
let dv2 = new DataView(ab3);
dv2.setInt8(0,116);//t
dv2.setInt8(1,101);//e
dv2.setInt8(2,115);//s
dv2.setInt8(3,116);//t
dv2.setInt8(4,33);//!
//ArrayBufferを使ってblobオブジェクトを作成します
//ダウンロード用にMIMETYPEをoctet-streamにします
let blob = new Blob([ab3],{type: 'application/octet-stream'});
//a要素を使ってダウンロードさせます
let elA = document.createElement('a');
//blobオブジェクトをセット
elA.href=window.URL.createObjectURL(blob);
//ダウンロードファイル名セット
elA.download = 'sample.txt';
//ダウンロード開始
elA.click();
</script>
ちなみに、まだ実験的な機能としての扱いなのですが、Encoding APIのTextEncoderとTextDecoderを使うと、UTF-8の文字列をUint8Arrayにエンコード、デコードできますので文字列を扱う場合は便利です。
//文字列をArrayBufferに転写
let ui8Array = (new TextEncoder()).encode('テスト 文字列');
let ab4 = new ArrayBuffer(ui8Array.byteLength);
let wkUi8 = new Uint8Array(ab4);
for (let i = 0; i < ui8Array.byteLength; i++) {
wkUi8[i]=ui8Array[i];
}
console.log(ab4);
//Uint8Arrayから文字をデコード
let str = (new TextDecoder()).decode(ui8Array);
console.log(str);//テスト 文字列