easy going tech

エンジニアリング、プログラミング学習のアウトプット

JavaScriptでString.prototype.reverseを作る

はじめに

こちらの記事ではJavaScriptにおけるStringオブジェクトを拡張してreverseメソッドを追加する過程をまとめています。 JavaScript以外の言語では文字列クラス(オブジェクト)のビルトインメソッドとしてreverseが備わっているものもありますが、それをJavaScriptにも実装してみようという、ちょっとした遊び心から生まれた記事です。

尚、JavaScriptではビルトインオブジェクトのprototypeを改変することはプロトタイプ汚染と呼ばれ(モンキーパッチと呼ばれることもある)、あまり好まれません。

今回はあくまでも学習の一環で実装していますのでご容赦ください。

概要

JavaScriptではStringオブジェクトにreverseメソッドが存在しないため、以下を実行するとエラーとなります。

'hoge!'.reverse()
// => Uncaught TypeError: "hoge".reverse is not a function

そして今回は以下の結果が得られるようにプログラムを作ります。

'hoge!'.reverse()
// => !egoh

ただしここで1つ制約を設けます。 Array.prototype.reverseは使用してはいけないという制約です。

そもそもこの記事を書こうと思ったきっかけはこちらの記事を読んで、「JavaScriptで同じ事をするには🤔」と考えたのがはじまりです。reverseを使わないという制約がないと大変簡単な実装となってしまうため、この制約を設けています。

Array.prototype.reverseを使用した場合

'hoge!'.split('').reverse().join('')
// => !egoh

これで終了です😅

そもそもprototypeって?

prototypeについてまとめるにはかなりの根気がいるためここでは簡単にまとめます。 詳しく学びたい方は下記を参考にしていただければと思います。

オブジェクトのプロトタイプ - ウェブ開発を学ぶ | MDN

さて、今回の実装に必要最低限な説明のため、Stringオブジェクトと文字列を使ってprototypeの中身を確かめてみます。 まずはお使いのブラウザでdeveloper toolを開きコンソールにString.prototypeと打ってみてください。

そうすると以下のような結果が得られるはずです。

この結果からprototypeの中身には文字列に使用できるメソッドが格納されていることがわかります。(slice、repeatなど) つまりprototypeとはそのオブジェクトで使用できるメソッドやプロパティが格納されたオブジェクトであると言えます。

次にdeveloper toolのコンソールに'hoge'.prototypeと打ってみましょう。 これはundefinedが返ってきます。ここで、先の「prototypeとはそのオブジェクトで使用できるメソッドやプロパティが格納されたオブジェクト」という説明が正しい場合、String.prototypeと同様の結果が得られないのはおかしいと思いませんか?なぜなら'hoge'という文字列に対してもsplicerepeatメソッドは使用できるため、矛盾が生じています。

ではなぜ、文字列がStringオブジェクトのメソッドを使用できるのかというと、JavaScriptではプリミティブな値に対してプロパティアクセスを行った場合、ラッパーオブジェクトへ自動変換されるという仕組みがあるためです。

ラッパーオブジェクト · JavaScript Primer #jsprimer

つまり、文字列へプロパティアクセスがあった場合はStringオブジェクトのprototypeが参照されているということになります。

この仕組みから、String.prototypeにreverseメソッドを追加してあげることで'hoge!'.reverse()が実現できることがわかります。具体的には下記のようにします。

String.prototype.reverse = function () {
  // ここに処理を実装
}

実装

まずはいきなりString.prototypeに実装するのではなく、「受け取った文字列を反転して返す関数(Array.prototype.reverseは使用禁止)」を作ります。 尚、筆者は普段TypeScriptを使用しているためTypeScriptのコードも記載します。但し、TypeScriptの場合はString.prototype.reverse =とした時点でコンパイルエラーが発生するため、関数の実装のみとして、Stringオブジェクトの拡張は割愛します。

Javascript

function reverse(string) {
  const chars = [...string]
  const reversedChars = []

  for (let i = 0; i < string.length; i += 1) {
    reversedChars.push(chars.pop())
  }

  return reversedChars.join('')
}

TypeScript

function reverse(string: string) {
  const chars = [...string]
  const reversedChars: string[] = []

  for (let i = 0; i < string.length; i += 1) {
    const char = chars.pop()
    if (char) reversedChars.push(char)
  }

  return reversedChars.join('')
}

実装のポイントは以下です。

  • const chars = [...string]として受け取った文字列を1文字づつ配列に展開(...スプレッド演算子本当に便利)
  • chars.pop()で後ろから順番に要素を取得して新しい配列に追加

Array.prototype.popについては配列の一番後ろの要素から要素を1つ削除することに注目されがちですが、実は返り値がその削除された要素になります。

console.log([1,2,3,4,5].pop())
// => 5

この特性を利用して、ループ内で配列の後ろから一個づつ要素を取得して新しい配列に追加しています。 TypeScriptの場合はpopの返り値は対象となる配列の要素の型とundefinedのユニオン型になるため(今回の場合はstring | undefined)、if (char)とすることでpush時のコンパイルエラーを防いでいます。

そして注意点としては以下です。

  • for文の条件式をi < chars.lengthとせずにstring.lengthとする

ループ内でcharsに対してpopを呼び出している(charsの要素数が減少していく)ため、i < chars.lengthとしてしまうと期待通りに繰り返しが行われません。

次にこれをString.prototype.reverseに代入していきます。

String.prototype.reverse = function () {
  const chars = [...this]
  const reversedChars = []

  for (let i = 0; i < this.length; i += 1) {
    reversedChars.push(chars.pop())
  }

  return reversedChars.join('')
}

先に実装した関数との変更点は以下です。

  • 引数を削除
  • 関数内で使用していた引数stringを全てthisに変更

このthisは具体的にはレシーバーとなる文字列を指します。 prototypeメソッド内でthisを呼び出した場合はレシーバーとなるオブジェクトが渡ってくるためです。

以上で実装が完了です。

最後のコードをdeveloper tool内で実行した後にお好きな文字列に対してreverse()を呼び出してあげましょう。

期待通りの結果が得られることでしょう。

まとめ

ひょんなことから取り組んだ問題ではありましたが、付随して以下の様々な事を学べました。

  • Array.prototype.popの挙動
  • prototype
  • this
  • ラッパーオブジェクト

Webアプリケーション開発には直接関係のない部分ですが、JavaScriptの機構を知っておくことは必要なことかと思いますので、今後も折りを見て学習していきたいと思います。