はじめに

こんにちは!

Uniforce株式会社のシバです。

普段の業務では TypeScript を使用しています。

これまでmap関数をよく使ってきましたが、「この関数の内部では何が起こっているのか?」と気になっていました。
そこで、map関数の仕組みを深掘りし、記事にまとめてみました。

map関数とは?

map関数は、配列の各要素に対して コールバック関数を適用し、新しい配列を生成 するメソッドです。

特徴

  • 元の配列を変更せずに、新しい配列を作成する
  • 各要素を変換し、変換後の値を新しい配列に格納する

基本的な動作

const numbers: number[] = [1, 2, 3, 4, 5];
const doubled: number[] = numbers.map((n: number) => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

処理の流れ

  1. numbers の各要素を順番に取り出す
  2. 取り出した要素にコールバック関数を適用
  3. 変換後の値を新しい配列に格納

普段mapを使う分にはこれで十分ですが、今回は内部の仕組みをより深く理解していきます。

mapのシグネチャ

まずはTypeScriptのmapのシグネチャ(関数と引数の型と返り値の型をわかりやすく書いたもの)はこのようになっています。
これをしっかり理解できるように見ていきます。

map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];

ここで<U>が何なのか初めは全く分からなかったのですが、これはジェネリクスというもので

戻り値の型なのですが、柔軟に型を変えることが出来るようになっています。

ちなみにUはUでなくても自由に設定できるのですが、Uが使われることが多いみたいです。

map関数でのジェネリクスの使われ方

U が string の場合

const numbers: number[] = [1, 2, 3];

const stringNumbers: string[] = numbers.map((n: number): string => `Number: ${n}`);
  • T = number
  • U = string
  • 結果: string[] の配列が返る

U がオブジェクトの場合

const numbers: number[] = [1, 2, 3];
const objects = numbers.map((n: number): { id: number } => ({ id: n }));
  • T = number
  • U = { id: number }
  • 結果: { id: number }[] の配列が返る

このコールバック関数が配列の各要素に適用され、新しい配列が作成されます。

コールバック関数の挙動

const numbers: number[] = [10, 20, 30];

const result = numbers.map((value, index, array) => {

console.log(`要素: ${value}, インデックス: ${index}, 配列全体: ${array}`);
return value * 2;
});

console.log(result);

コンソールログの出力結果

1回目 (value = 10, index = 0)

10 * 2 = 20 が新しい配列の最初の要素になる。

要素: 10, インデックス: 0, 配列全体: 10,20,30

2回目 (value = 20, index = 1)

20 * 2 = 40 が新しい配列の2番目の要素になる。

要素: 20, インデックス: 1, 配列全体: 10,20,30

3回目 (value = 30, index = 2)

30 * 2 = 60 が新しい配列の3番目の要素になる。

要素: 30, インデックス: 2, 配列全体: 10,20,30

最終結果の出力

[20, 40, 60]

map の第二引数 (thisArg) の役割

map の第二引数 thisArg を指定すると、コールバック関数内の this の参照を変更できます。

thisArg を渡さない場合(エラー)

// map のコールバック関数内では this は undefined になり、エラー になる
class Multiplier {
factor = 2;

multiply(numbers: number[]): number[] {
return numbers.map(function (n) {
return n * this.factor; // `this` は `undefined`
});
}
}

const m = new Multiplier();
console.log(m.multiply([1, 2, 3])); // エラー: `this.factor` が `undefined`

解決策: thisArg を渡す場合

// thisArg に this を渡すことで、this が Multiplier のインスタンスを指すようになる
// その結果、this.factor が正しく参照され、期待通りの計算ができる
class Multiplier {
factor = 2;

multiply(numbers: number[]): number[] {
return numbers.map(function (n) {
return n * this.factor;
}, this); // `thisArg` を渡して `this` を `Multiplier` インスタンスにする
}
}

const m = new Multiplier();
console.log(m.multiply([1, 2, 3])); // [2, 4, 6]

map の内部実装

map は JavaScript の標準仕様 ECMAScript (Ecma Internationalという団体によって標準化されている仕様)に基づいて実装されていて

ブラウザによって変わりますが実際の処理は、Google Chrome では V8 エンジン によって実行されています。

  • ECMAScript 仕様の準拠
    • map の動作は、ECMAScript の公式仕様に明確に定義されています。
    • 各 JavaScript エンジンは、この仕様に沿って map を実装しています。
  • 内部処理の最適化
    • V8 エンジン ではmap の処理を効率化するために、Torqueという言語 や C++ を用いて最適化が行われているようです。間違っていたらすみません….。
  • 実際のコードの確認方法
    • map の具体的な実装を知りたい場合は、V8エンジン のソースコード を確認する必要があります。
    • V8 エンジン のリポジトリは Google の GitHub に公開されています。

まとめ

今回の記事では、map関数について、基本的な使い方を詳しく見てきました。

mapをより理解するために

  • ECMAScript の仕様書(map関数はArray.prototype.mapを読むと、より詳しいmapの定義が分かる。他にも色んな関数があるので興味のある方はぜひ読んでみて下さい。
  • JavaScript エンジンの仕組みに興味がある人は、V8 の内部実装をチェックすることが出来る。

最後に

mapはとても便利なメソッドで、日常の開発でもよく使います。今回の記事を作成する中でmap関数の理解が少し深まったと思います。

次回はECMAScript の仕様書(map関数はArray.prototype.map の章)を読んで記事にしてみたいと思います。

ここまで読んでいただきありがとうございました。