JavaScript

JavaScript

JavaScriptを
学ぶチュートリアル

第2章文字列

文字列(string)」は、おそらくWebで最も重要なデータ構造でしょう。そもそもWebページは最終的に「サーバからブラウザに送信される文字列」でできていますし、文字列操作は他の多くのプログラムでも必要となります。つまりJavaScriptプログラミングで最初に学ぶのは文字列で決まりです。

コラム 2.1. 文字列とバイナリ

Webページが最終的に文字列でできているということは、原理的には「人間が読める文字でできている」「つまりテキストデータである」ということでもあります。Web開発者はそのことを知っているので、「文字列なのだから人間が読めるはずだ」と確信しています。実際、この安心感は大きいと言えます。

では人間が読めないデータ構造とは何でしょう。それは一般に「バイナリデータ(binary data)」と呼ばれます。

典型的なバイナリデータといえば「画像ファイル」「音声ファイル」「動画ファイル」でしょう。試しに、こうしたファイルをテキストエディタで無理やり開くと、文字化けのような意味不明のものが表示されます。人間に読めないというのはそういう意味です。

テキストデータとバイナリデータは対で考えられることが普通です。「テキストデータだから必ずエディタで開けば読める」「バイナリデータだからテキストエディタでは読めない」などです。

しかし厳密に言えば、「文字列(テキストデータ)は人間が読める文字集合(プリンタで印刷できる文字)だけでできている」のに対し、「バイナリデータは、人間が読める文字の集合と、人間が読めない特殊文字(制御記号など)の集合の両方でできている」のです。

つまり原理的には「あらゆるデータはバイナリデータ」なのです(屁理屈のように聞こえるかもしれませんが)。その中で、人間が読める(印刷可能な)文字集合だけでできたデータを、文字列またはテキストデータと呼んでいるに過ぎません。文字列(テキストデータ)は、バイナリデータの部分集合と理解できます。

プログラマーは、こうした違いに敏感です。「テキストデータは印刷可能」と書きましたが、さらに厳密なことを言えば、改行文字\n(Linefeed: LF)のような印刷可能ではない特殊記号もテキストデータに含まれます。

2.1 文字列の基礎

文字列は、文字(character)を特定の順序でひとつながりに並べたものでできています。文字列の例としては、1で紹介した「hello, world」プログラムが挙げられます。それでは、コマンドラインでNodeのセッションを起動し、console.log()を使わず、文字列だけを入力するとどうなるか見てみましょう。

$ node
> "hello, world!"
'hello, world!'

このように使う文字をコードの中にそのまま書いたものは、文字列リテラル(string literal)と呼ばれます。ここでは二重引用符"で囲むことで文字列リテラルを表現しています。NodeのREPLがこの文字列リテラル"hello, world!"を評価すると、評価した文字列をそのまま返します。

細かいことですが、入力した文字列は二重引用符で囲まれていたのに、REPLは一重引用符'で囲んて返していることにお気づきでしょうか。返す文字列を二重引用符で囲むか一重引用符で囲むかといった詳細はシステムに依存する、つまりシステムに任されているので、システムごとに異なっていても気にする必要はありません。たとえば、ブラウザコンソールでは、文字列の戻り値を二重引用符で囲むものもあります。ささいなことですが、この機会にJavaScriptにおける二重引用符と一重引用符の違いについて学ぶことにしましょう。

コラム 2.2. 引用符について

通常の英文で用いる一重引用符とアポストロフィは「本来」別の文字ですが、プログラミングで用いられるASCII文字では慣例で兼用されています。ワードプロセッサなどで用いる英語の文章スタイルでは、一重引用符には‘□’、二重引用符には“□”のように曲線(カーリー)スタイルの記号を使うのが本来正式とされていますが、プログラミングではカーリー引用符を意味を持つ記号として直接用いることはまずありません。

JavaScriptは他の多くの言語と異なり、基本的に二重引用符と一重引用符を同じように扱います。

> "It's not easy being green"
"It's not easy being green"

上のように二重引用符で囲まれた文字列の中にアポストロフィ(')を書いた場合は、そのまま二重引用符の内側全体が文字列として評価されます。

ただし以下の'It's not easy being green'のように、アポストロフィ(')を含む文字列を一重引用符で囲む場合は、アポストロフィの直前にバックスラッシュ(\)を付けて、アポストロフィをエスケープしなければなりません。この場合、評価後に両側の一重引用符が二重引用符に変えられ、エスケープされていた一重引用符のエスケープが除去されて、上と同じ結果が出力されます。Node.jsが気を利かせて、評価後にエスケープなしの文字列にしてくれたと考えるとよいでしょう。

> 'It\'s not easy being green'
"It's not easy being green"

以下のように一重引用符の中でアポストロフィをエスケープせずに同じ文字列を入力すると、REPLは文字列が 「It’」の直後で終わっていると認識し、一重引用符の開きと閉じが対応していない(一重引用符が奇数個存在している)とみなして以下のように構文エラーになります。

> 'It's not easy being green' // 開きと閉じが対応してないのでエラーになる
'It's not easy being green'
    ^
SyntaxError: Unexpected identifier 's'

このときJavaScriptは、'It'の直後にあるナマの's'を「文字列ではない」コードの一部と解釈しました。しかしsという「識別子(identifier)」が定義されていないので、REPLが「sの意味がわからない」というエラーを返したのです。識別子については2.2で詳しく説明します(コラム 2.6)。

一重引用符で囲まれた文字列の中では、一重引用符をエスケープしないといけないということがこれでわかりました。二重引用符の場合もやはりこれと同じように、二重引用符で囲まれた文字列の中では、二重引用符をエスケープしないといけません。

> "Let's write a \"hello, world\" program!"
`Let's write a "hello, world" program!`

二重引用符で囲んだ文字列の中に、アポストロフィとエスケープ済み二重引用符が両方入っています。この場合Node.jsはさらに気を利かせてバッククォート`という引用符で全体を囲み直し、エスケープがない形で返しました。

これらはあくまでNode.jsコンソールの動作である点にご注意ください。ブラウザコンソールでは、このような気を利かせた引用符変換は行わず、入力したとおりに返す場合もあります。しかしどの環境であっても、「文字列を囲むのに使った引用符を文字列の中でも使う場合はエスケープしなければならない」ことは変わりません。

以下のように引用符の内側に何も置かない「空文字列(empty string)」も、重要な文字列の一種です。

> ""
''

空文字列についてはこの後の2.4.2および3.1でも詳しく説明します。

2.1.1 演習問題

  1. JavaScriptでは、Tab文字\t)や改行文字\n)などの、よく使われる特殊文字もサポートしています。これらの文字を、引用符で囲まれた文字列の中に入れて評価してみてください。一重引用符、二重引用符、バッククォートでそれぞれ試してください。使用するコンソールやブラウザの種類によって、動作に違いがあることを確認してみましょう。

2.2 文字列の結合と挿入

文字列操作で重要な2大機能といえば、文字列同士の「結合(concatenation)」機能と、文字列の中で変数の値を表示する「挿入(interpolation1)」機能です。

以下のように+演算子を用いることで、文字列と文字列を結合できます(コラム 2.3)。

$ node
> "foo" + "bar";    // 文字列の結合
'foobar'
コラム 2.3. 文字列の結合について

+演算子で文字列同士を結合する機能は、Rubyを含む多くのプログラミング言語で使われていますが、数学的に細かいことを言えば、足し算は「\( a+b = b+a \)」のような交換法則を満たさなければ本当の意味での足し算とは呼べません2

しかし文字列の結合は、"foo" + "bar""foobar"となり、"bar" + "foo""barfoo"になることからわかるように、明らかに交換法則から外れています。そうした理由もあって、文字列結合に他の方法を使う言語もあります。たとえばPHP言語では文字列結合を「"foo" . "bar"」のようにドット.で表記しています。Go言語では+での文字列結合を明確に禁止して、文字列挿入のみを用います。

一般に、+演算子で文字列を結合するよりも、次に述べる文字列挿入(式展開)の方が洗練されていますし、動作も安定しているのでおすすめです(参考:『Rubyでの文字列連結に「+」ではなく式展開「」を使うべき理由』)。

上のように、"foo"「足す」"bar"が評価されると'foobar'というひとつの文字列になります(『コマンドライン編』でも説明したように、"foo""bar"は、説明するときの仮の名前によく使われます: 日本では"hoge""huga"もよく使われます)。

コラム 2.4では、「//」というJavaScriptのコメント機能も使われていることにご注目ください。REPLセッションで//を付ける必要は普通ありませんが、本チュートリアルでは説明のためにこのように//でコメントを付けることがあります。

コラム 2.4. コメント機能について

JavaScriptでは、//のようにスラッシュを2つ連続で書くとそこから先がコメントとして扱われ、JavaScriptコードを実行するときに無視されます。コメントは、以下のようにコードの作者がコメントを付けるときによく使われます。

  // コンソールに挨拶文を出力する
  console.log("hello, world!");  // コマンド本体

1行目のコメントは、これから行う処理についての説明です。2行目の後半にあるコメントは、その行についての説明です。

コメント機能はコードのデバッグ(5.2)を行うときにもよく使われます。コードの特定の行を何行かまとめて一時的に無効にしたいときに、それらの行を「コメントアウト」するという手法です。元に戻すには、追加したコメント記号//を削除します(コメント解除)。『テキストエディタ編』では、以下のように特定の行をまとめてコメントアウトしたりコメント解除する方法も学びましたね。

  // console.log("foobar");
  // console.log("racecar");
  // console.log("Racecar");

テキストエディタには、手で//を入力したり削除したりしなくても、エディタのショートカット機能を使って一発でコメントアウトしたりコメント解除したりできる機能もあるのが普通です。具体的な方法はエディタやエディタの設定によって異なりますが、macOS用のエディタやIDEでは、行を何行か選択してキーを押しながら/キーを押すとコメントアウトとコメント解除を即座に切り替えられるのが普通です。例の「技術の成熟」精神(コラム 1.1)を発揮して、自分のエディタでどの方法が使えるかを調べてください。

JavaScriptでは、以下のように/* ... */を用いて複数行を一度にコメントアウトする機能も一応使えます(これは元々C言語で使われていた記法です)。

  /* console.log("foobar");
  console.log("racecar");
  console.log("Racecar"); */

なお今どきのテキストエディタは行コメント//をショートカットで簡単にオンオフできるので、著者はこの/* ... */形式のコメントを現場ではめったに使いません。

いずれにしろ、普通はコメント機能をコンソールセッションで使うことはないでしょう。

  $ node
  > 17 + 42   // 数値の足し算
  59

皆さんがコンソールでこうしたコードを入力(またはコピペ)する場合は、もちろんコメント部分を省略しても構いません。省略しなくてもコメント部分は無視されます。

コラム 2.5. コメントは控えめかつ的確に

「コメントをたっぷりつければ読みやすくなる」と勘違いしないようご注意ください。特に不慣れなうちは、コードにやたらめったらコメントを書いて不安を解消しようとする人をよく見かけます。プログラミング教室や自学自習の段階であればそれでもよいと思いますが、仕事の現場でコードを書く場合は、無駄なコメントをたくさん書くと確実に他のプログラマーから嫌がられます。

理由はいくつかあります。まずコメントが多すぎると暑苦しくなって逆に読みづらくなります。ベテランほど、コードの動作を単になぞるだけのコメントを「無駄だ」と考えるものです。「コードを読めばわかることを、わざわざコメントに書かないで欲しい」ということです。

プログラミングでは、自分や他人の書いた大量のコードを「極めて厳密に」読まなければなりませんし、それができなければ務まりません。一般にベテランほど読むのは速くなりますが、特に他人の書いたものを大量に読むのはベテランにとってもしんどい作業なのです。そのため、ベテランはコードをなるべくシンプルかつ素直に書くことを常に心がけますし、コメントも必要なものしか書かないようにしています。なくてもよいような無駄なコメントが書かれていると、読まされる人にとって本当に苦痛なのです。

コードに無駄なコメントをたくさん書くと、それだけで初心者とみなされてしまいます。「コードに余計なことを書かない」「コードを汚さない」ことは、チームで仕事をするときの大事なエチケットです。

特に、GitHubなどのリポジトリには、コードを汚さずにWebでコメントを付ける機能やissue管理機能がありますので、不安や疑問点を解消したい場合は、コメントをコードに書くよりも、そうした機能を使うかSlackなどのチャットで相談する方がよいでしょう(チームによってこのあたりの流儀は異なります)。

もっと深刻な理由もあります。コードの動作を単に説明する「しょうもない」コメントを書いてしまうと、後でコードを書き換えたときにコメントの説明も同時に書き換えないと辻褄が合わなくなってしまいます。そういうコメントが大量に付けられていると、コードとコメントの辻褄を常に合わせなければならず、余分な仕事を増やしてしまいますし、いつの日かコードとコメントのどちらかを更新しそこねてしまう危険も生じます。後から参加する人が、コードとコメントがずれていることに気づかないまま作業すると事故の元になります。

だからこそ、ベテランはよく「コードそのものに語らせろ」と指導します。コードの出来がまずい人は、コメントにいろいろ書いて言い訳しようとしがちですが、「ダメなコードをコメントで言い訳するのではなく、まともなコードを書いてくれ」とベテランから注意されるでしょう。

もちろん、必要なコメントであれば書かなければいけません。コードの動きを一行ずつくどくど説明するだけのコメントはほとんどの場合不要ですが、機能の概要を説明するコメントと、(言い訳ではない)いい意味での「技術的な理由」を具体的に説明するコメントは後々役に立つ可能性が高いでしょう。たとえば「ここでは3つの方法が考えられる: 1はこれこれ、2はこれこれ、3はこれこれ。しかしどの方法にも少しずつデメリットがある。それぞれのデメリットを検討した結果、これこれこういう理由で3を採用した。」といったコメントは、後でそのコードを読む人にとって有用な情報になります。

言い換えると、「What(これは何々である、という概要の情報)」と「Why(技術的な理由)」はよいコメントである可能性が高く、「How(細かな説明)」はよくないコメントの可能性が高いということです。

本チュートリアルでは説明のためにコメントを多めに使っていますが、仕事でコードを書くようになったら「コードを汚さないためにはどうすればよいか」「今書いているコメントは本当にGitに登録する価値があるか」を常に気にかけるようにしましょう。

今度は、文字列の結合を「変数(variable)」について行う方法も見てみましょう。変数という用語について詳しくは『CSS & Design編』およびコラム 2.6をご覧ください。

コラム 2.6. 変数と識別子の違い

プログラミングがまったく初めての人にとって、変数という用語はおそらく馴染みがないでしょう。変数は、コンピュータサイエンスできわめて重要な概念です。

ごく簡単に説明すると、「いろんなものを保存できる箱」があり、その箱に名前を付けたものが変数です。その箱にはさまざまな種類のものを保存できるようになっているので、英語のvariable(「変えることが可能な」「変わる可能性のある」という形容詞)という言葉が名詞として使われているのです。日本語で変種や変異を意味する「バリエーション(variation)」や「バリアント(variant)」にも通じる言葉です。

要するに変数に入れた値は、後から変更できますし、後から変わる可能性があるということです(知らないうちに変わってしまっては困りますが)。そのように理解しましょう。

ちなみに変数と反対の概念は、定数(constant)です。日本語でも「コンスタントに努力する」などと言いますよね。定数もやはり「名前の付いた箱」のたとえで理解できますが、一度入れたものを他のものに変えられない点が変数と異なります。

変数をもう少し身近なものにたとえてみましょう。小学校の教室の後ろに、名札付きの荷物棚がよく置かれています。棚には生徒の名前が表示されていて、そこに着替えや本やかばんなどを置けるようになっています(図 2.1)。変数はひとつひとつの棚であり、変数名は棚に付けた名前、変数の値は棚の中身、ということになります。

JavaScriptの変数名は、「識別子(identifier)」の一種です。識別子と書くと何やら難しそうですが、要するに(特定のものを指す)「名前」のことです。JavaScriptで使う関数や引数やプロパティなどにも名前が付けられますが、そういうものをひっくるめて識別子と呼んでいるに過ぎません。

識別子が名前であると言っても、「日本」や「アメリカ」、あるいは「塩」や「海」のような一般名詞や集合名詞のようなものではありません。「Michael Hartl」のように、ただひとつのものを指す固有の名前が識別子です。

「変数」という用語は、実際には多くの場合「(値の)保存場所」「(変数の)名前」「(変数の)値」をまとめた概念として用いられます。変数が「保存場所」を表すこともあれば、「変数名」を意味することもあり、あるいは変数に入っている「値」を指す可能性もあります。

images/figures/cubby
図 2.1: コンピュータの変数を具体的にたとえる

それでは、JavaScriptでletを用いて、ファーストネーム(名)とラストネーム(姓)を保存する変数をそれぞれ作ってみましょう(リスト 2.1)。今後、特に指定がない限り> で始まるコードはNode.jsコンソールに入力する前提で進めます。

リスト 2.1: letを使って変数に値を代入する
> let firstName = "Michael";
> let lastName  = "Hartl";

上のletというキーワードは、firstNameという変数(識別子)に"Michael"という文字列を=で代入するときと、lastNameという変数(識別子)に"Hartl"という文字列を=で代入するときに使われています。

リスト 2.1で使われているfirstNamelastNameという名前の付け方にご注目ください。このように、「最初は英小文字で始まる単語」「以後の単語は英大文字」「単語と単語の間には何も置かない」命名法をキャメルケース(camelCase)と呼びます(図 2.23)。図でおわかりのように、ラクダのコブのように真ん中で大きくなっていることからキャメルケースという名前が付けられました(ここでいうcaseは、大文字と小文字のどちらを使うかを表す言葉です)。JavaScriptの変数名には、このキャメルケースを用いるのが一般的です。なお、変数ではなくStringのようなオブジェクトプロトタイプ(object prototype)(7)の名前は、冒頭も大文字にするのが普通です。

images/figures/camel
図 2.2: キャメルケースの由来

リスト 2.1のとおりに変数名を2つ定義したので、これらを用いてファーストネームとラストネームを結合できます。英語の名前なので、結合するときは間にスペース文字も1個置きます(リスト 2.2)。

リスト 2.2: 変数に入れられた文字列(とスペース文字)を結合する
> firstName + " " + lastName;
'Michael Hartl'

firstNamelastNameは文字列の入った変数(文字列変数)ですが、間のスペース文字" "は文字列リテラルです。

ところで、リスト 2.1で使われているletは、新しい「モダンなJavaScript」コードの大きな特徴です。

なお、モダンなJavaScriptはES6と呼ばれることもよくありますが、その理由はECMAScript標準のバージョン6で大きく改良されたからです。実際、ES6以降のJavaScriptは、それ以前の古いJavaScriptと事実上別言語であるとすら言われるほど様変わりしています。

「古い」JavaScriptコードのひと目で分かる特徴は、letではなくvarという古いキーワードを使っていることです。varの機能はletほぼ同じように使えるのですが、varにはさまざまな問題が付きまとうため(ここでは詳しく説明しません)、ES6以降はこの問題を解決したlet4.2で学ぶconstを使うのが定番です。

要するに、varを使っているコードは古いコードです。ネットに落ちているコードでvarを見かけたら、「これは古いんだな」と割り引いて読むべきです。そのままコピペしてはいけません。

当然本チュートリアルでも、このletconstを常に使います。しかしJavaScriptの古いコードも未だに世の中で大量に使われていて、そこでは残念ながらvarも使われまくっているので、letvarの両方を知っておくことも重要です(リスト 2.3)。

リスト 2.3: あえて時代遅れのvarで変数に値を代入してみる
var firstName = "Michael";
var lastName  = "Hartl";

リスト 2.3は説明のために表示しています。

2.2.1 バッククォート構文

+による文字列結合を学んだので、もうひとつの文字列挿入方法を学ぶことにしましょう。ES6以降では、以下のようにバッククォート`4を用いた特殊な文字列挿入構文が利用できるようになりました。

> `${firstName} is my first name.`
'Michael is my first name.'

上では、文字列がバッククォート`...`で囲まれており、その中に変数が置かれています。さらにこの変数は、ドル記号$と波かっこ{...}で囲まれています。

このように書くと、${...}の中に置いた変数の値が取り出されて、文字列のその部分が自動的に値と置き換えられます。これが文字列挿入です。ここではfirstNameという変数に入っている"Michael"という文字列が値として使われます5

リスト 2.2のコードをバッククォート構文で書き換えたものをリスト 2.4に示します。

リスト 2.4: 初歩的な文字列結合をバッククォート構文に置き換える
> firstName + " " + lastName;    // スペースも結合する必要がある
'Michael Hartl'
> `${firstName} ${lastName}`;    // 上を文字列挿入に置き換えたもの
'Michael Hartl'

リスト 2.4にある2つの式は同じ結果を出力しますが、一般には下の文字列挿入による方法をおすすめします。上の方法だとスペース" "も結合しなければならないので、あまりかっこよくありません(詳しく知りたい方は『Rubyでの文字列連結に「+」ではなく式展開「」を使うべき理由』もどうぞ)。

2.2.2 演習問題

  1. Node.jsコンソールで、letを用いて変数に値を代入した後、まったく同じ代入をもう一度行うとどうなるかを試してみてください。具体的には、let firstName = "Michael";を実行した後、上矢印キーを押してもう一度同じものを表示し、再実行します。それが終わったらvarでも同じことをやってみてください。具体的には、var lastName = "Hartl";を実行し、上矢印キーを押してもう一度同じものを表示し、再実行します。
  2. 今自分が住んでいる県名をprefectureという変数に代入し、今自分が住んでいる市町村名をcityという変数に代入し、以後の番地をnumberという変数に代入してください。次に、文字列挿入構文を用いて住所を「神奈川県横須賀市桜が丘2000-2」のように組み立てて表示してください。
  3. 上の演習と同じことを、今度は「神奈川県 横須賀市桜が丘 2000-2」のように県名と市町村名と番地の間にTab文字を挿入して行ってください。

2.3 出力

1.3以降で学んだように、JavaScriptでは以下のようにconsole.log()関数を使って文字列を出力します6

> console.log("hello, world!");     // 文字列をコンソールに出力する
hello, world!

少しテクニカルな話をします。このconsole.log()関数で行う処理は、コンピュータサイエンスの世界では副作用(side effect)と呼ばれます。副作用と書くと何やら難しそうですが、要するに「結果を返す以外」の何かを行うことは、すべて副作用なのです。

コンピュータサイエンスにおける理論上の関数は、実行すると「結果を返す」ことがあくまでメインの処理なのです。そして、値を返す以外の処理は概念上すべて「副作用」に分類されているということです。

「副作用」というと薬の副作用のような「望ましくない効き目」を連想する人も多いと思いますが、コンピュータサイエンスにおける副作用は本来、良い・悪いとは無関係であり、単なる分類上の名目に過ぎません。副作用はよくないという主張も根強くありますが、副作用をゼロにするのは非現実的です7

console.log("hello, world!");

たとえば上をコンソールで実行すると、コンソールに結果が出力されますが、この「コンソールに結果を出力する」部分が副作用に相当します。

そしてconsole.log()は、理論上のメインの処理としては「何も返していません」。つまり、返されたものを変数に代入したりできないということです。先ほどからコンソールで何かを実行して出力するたびに、出力の次の行にundefinedというメッセージが表示されますが(図 2.3)、その理由がこれです。

REPLでこのundefinedというメッセージが表示されても、通常は無視して大丈夫です。皆さんに知っておいていただきたいのは、JavaScriptには「値を返す関数」と、「値を返さない、副作用のみの関数」の2種類があるということです。ほとんどは前者の「値を返す関数」で、 副作用がある場合とない場合の両方があります。後者の「値を返さない、副作用のみの関数」はconsole.log()など、ごく少数です。

興味のある方へ: REPLで関数を実行すると、REPLは関数から値が返ってきたときにそれを表示しようと待ち構えます。REPLはそれがどんな関数であるかについては何も関知しません。console.log()は何も返さないので、「この関数は何も返さなかったよ」とREPLが表示したというだけのことです。それがundefinedの意味です。"Michael"のように文字列リテラルだけをREPLに入力した場合はundefinedが表示されませんが、その理由は文字列リテラルは「関数ではない」からです。つまり文字列リテラルを入力しても、REPLは結果を表示しようと待ち構えたりしないので、undefinedが表示されることもないのです。

images/figures/undefined
図 2.3: Node.jsコンソールでundefinedと表示された様子

ところで、他のプログラミング言語にある出力関数はprintprintf(print formatの略)やputs(put string)のように名前が短いのに、JavaScriptではなぜconsole.log()のような冗長な名前にしたのでしょうか?consoleオブジェクト上のlog()メソッドをわざわざ呼び出すといった直感的ではない方法になったのはなぜでしょうか?

その答えは「JavaScriptがもともとブラウザで動かすことしか想定していなかったから」です。RubyやPHPやPythonのような汎用のプログラミング言語として使われるとは当初思ってもみなかったのです。

console.log()という名前にも当時の名残りが見つかります。このconsoleオブジェクトはブラウザコンソールのことで、実行結果をログとしてそこに出力するという意味だったのです。もちろん、ブラウザコンソールでのログ出力は現在でも優れたデバッグ方法です。たとえば、サンプルWebサイトのindexページのHTMLに以下のようなscriptタグ(リスト 2.5)を書いておくと、ブラウザでそのページを開いたときブラウザコンソールにログが出力されます。ブラウザコンソールを開いていない一般ユーザーには見えません。

リスト 2.5: コンソールにログを出力する index.html
<!DOCTYPE html>
<html>
  <head>
    <title>JavaScriptを学ぼう</title>
    <meta charset="utf-8">
    <script>
      alert("hello, world!");
      console.log("This page contains a friendly greeting.");
    </script>
  </head>
  <body>
    <h1>Hello, world!</h1>
    <p>このページにはJavaScriptで書かれたアラートがあります。</p>
  </body>
</html>

indexページを開くと先ほどのアラートボックスが表示され、その後ブラウザコンソールにメッセージが出力されます(図 2.4)。

images/figures/console_log_greeting
図 2.4: indexページのコンソールにログ出力されたメッセージ

最後にちょっとしたコツを紹介します。1.4.1でも簡単に紹介しましたが、console.log()にカンマ区切りで複数の引数を渡すと、以下のように間にスペースが1個ずつ挿入されます。

> console.log(firstName, lastName);
Michael Hartl

これを知っておけば、2.2でやったときのような文字列の結合を、文字列挿入も+記号も使わずに手軽にできます。

なお、日本語の文字列でこの方法を使う場合もスペースが挿入されてしまいます。スペースを挿入したくない場合は通常の文字列挿入を使いましょう。

2.3.1 演習問題

  1. 今度はコンソールではなく、ファイルで作業します。index.htmlファイルの中に、2.2のときのようなfirstName変数とlastName変数を定義し、それらの変数の値をconsole.log()関数でブラウザコンソールに出力するJavaScriptコードを書いてください。

2.4 プロパティ、論理型、制御フロー

JavaScriptでは、ごく一部の例外を除いてあらゆるものが「オブジェクト(object)」になっています。そしてJavaScriptの文字列も、やはりオブジェクトです。文字列がオブジェクトになっているので、文字列から(文字列そのもの以外の)付加的な情報を取り出すことも、1.3.1で学んだドット.記法を用いることもできます。

最初に、文字列の「プロパティ(property)」にアクセスしてみましょう。プロパティは「属性(attribute)」と呼ばれることもあり、意味は同じです。JavaScriptではさまざまな情報がプロパティの形でオブジェクトに追加されます。ここでは、文字列にある文字の個数8を表すlengthプロパティの値をコンソールで表示してみましょう。

$ node
> "badger".length;    // 文字列のlengthプロパティを表示する
6
> "".length           // 空文字列の長さもちゃんとゼロになる
0

なお、Stringオブジェクトにはlengthプロパティしかありません。これについてはMDNのStringページを開いてブラウザの検索機能で「プロパティ」という文字列を検索すれば確かめられます。

lengthプロパティは、長さを比較するときに特に便利です。たとえば以下のように、文字列の長さが特定の数値より大きいか小さいかを調べるときに使えます。ちなみに、上矢印キーを押すたびに入力履歴をさかのぼれるので、入力の一部をいろいろ変えて実行してみたい場合に便利です。

> "badger".length > 3;
true
> "badger".length > 6;
false
> "badger".length >= 6;
true
> "badger".length < 10;
true
> "badger".length == 6;
true

最後の行では、lengthが特定の数値に等しいことをチェックするのに==というダブルイコール記号を使っていることにご注目ください。==記号は他の言語でも比較操作でよく使われていますが、JavaScriptの==にはとんでもない罠が潜んでいます。

> "1" == 1;    // えぇ?!
true

恐ろしいことに、==で比較すると文字列"1"と数値1が等しいと判定されてしまいます。理由は、JavaScriptの==は、文字列を数値に変換してから比較するからです。

JavaScriptにおける==の挙動は、プログラミング言語としては理解し難いものです。他のプログラミング言語経験者がJavaScriptのコードを書くと、知らずにこの罠を踏んで厄介なバグを作り込んでしまうことがよくあります。しかしJavaScriptは最初からこのようになっているため、今さらこの挙動を変えるわけにはいきません。

そういったわけで、JavaScriptのダブルイコール==は勘違いの元になりやすく、危険です。これを避けるには、ダブルイコール==は使わず、以下のように「厳密比較演算子」であるトリプルイコール===)だけを使うのがベストです。

> "1" === 1;    // これなら期待どおりでしょう
false

本チュートリアルでも、両辺が等しいことを比較する場合は===だけを使います。

さて、上の例のような比較を行うと、結果は必ずtruefalseのいずれかの値になります。truefalseという値は「論理型(boolean)」と呼ばれており、偉大な数学者ジョージ・ブール(George Boole)の名前から命名された用語です(図 2.59)。

images/figures/boole
図 2.5: 偉大なるジョージ・ブール先生のご尊顔

論理値は、特にプログラミングの制御フロー(control flow)で有用です。制御フローでは、比較の結果に基づいて処理方法を変えることがよくあります(リスト 2.6)。

リスト 2.6: ifで制御フローを切り替える
> let password = "foo";
> if (password.length < 6) {
    "パスワードが短すぎます";
  }
'パスワードが短すぎます'

さて、リスト 2.6ifの直後の部分が丸かっこ()に囲まれていることと、その後ろに波かっこ{...}が置かれていることにご注目ください10

また、この波かっこの中にあるコードがインデント(indentation: 字下げ)されている点にもご注意ください。JavaScriptに限らず、このようにインデントを追加する習慣は多くの言語で普及しています。インデントがあってもなくてもコードの動作は変わりません。あくまで人間にとって読みやすくするためのものです(コラム 2.7)。

コラム 2.7. コードの書式を整える

本チュートリアルで用いられるコードサンプルは、REPLに入力するものも含めて、JavaScriptコードを読みやすくし、ひと目で理解できるような形で整えられています。コードの書式は実行には影響しませんが、人間にとっての読みやすさに大きく影響するので、書式がばらつかないよう統一することが重要です。

先ほど申し上げたように、プログラマーはコードを読む苦労を少しでも減らすため、コード内で書式がばらつかないことを強く願います。プロジェクト内で書式がばらつくと大変なストレスになるからです。

しかし多くのプログラミング言語は、多少書式が違っていても実行結果が変わらないため、世の中には実にさまざまな書式の流派が存在します。たとえば先ほどの波かっこひとつとっても、以下のようなさまざまな流派があります。

> let password = "foo";
> if (password.length < 6) {
    "パスワードが短すぎます";
  }
> let password = "foo";
> if (password.length < 6) {
    "パスワードが短すぎます";}
> let password = "foo";
> if (password.length < 6)
    {
      "パスワードが短すぎます";
    }

困ったことに、こうした書式はプログラマーによって好みがバラバラなので、どれがスタイルとして優れているかという激しい論争が何度も繰り広げられました。しかし所詮好みでしかないので、優れているという理由をいくら並べても後付けにしかなりません。

なお、Go言語は誕生直後からこうした書式も言語レベルで強制的に揃えるようになっています。これを嫌う人もいますが、Go言語開発者は「書式で喧嘩しなくて済むので助かる」と喜んでいます(個人的にもうまい方法だと思います)。しかし、既に普及している言語で同じことをすれば本当の戦争になってしまうことが目に見えているので、なかなかそうもいきません。

現在は多くの場合「自分の好みはおいといて、そのプロジェクトの書式に合わせる」という現実的な方法に落ち着いているようです。別のプロジェクトに参加するときは書式を変えないといけないのですが、少なくとも「ひとつのプロジェクト内では書式を揃えよう」というのがコンセンサスになりつつあります。

そういったわけで、JavaScriptの書式にも唯一の正解というものはなく、細かくはプロジェクトごとにさまざまです。そこで、一般に通用するであろう大まかなガイドラインを以下に紹介しておきます。

  • インデント(字下げ)は、コードのブロック構造を読みやすくするうえで重要。ブロックを表す開き波かっこ{の直後は改行して、インデントすること(テキストエディタによっては自動整形機能も使えます)。
  • インデントには(Tab文字ではなく)スペース文字を使うこと。エディタのTabエミュレーション機能を使えば、Tabキーを押したときに自動でスペース文字に置き換えてくれます。問題はスペースを何文字に設定するかです。スペース4文字や8文字を好む開発者も多いのですが、著者はRubyでもよく使われている「スペース2文字」が好ましいと考えています。スペース2文字ならインデントであることもすぐわかりますし、場所も取りすぎません。
  • 論理的に意味が区切られる箇所には改行を置く。著者は、特にletconstといった「宣言部」と、その後に続くコード本体の間に、改行を1個置いて1行空けるスタイルを好んでいます。これなら、宣言部がその箇所で終わり、そこからコード本体が始まることがすぐわかります。具体例についてはリスト 4.6を参照。
  • 1行あたり最大80文字までとする。なお、これは画面が狭かった時代の名残り11。現在はPCの画面が随分大きくなったこともあって、このスタイルを守らない開発者も多く、時代遅れとみなされることもあります。しかし「1行80文字まで」を守って書くのはよい訓練にもなりますし、Linuxのlessコマンドでソースコードをさっと表示する場合や、電子書籍にソースコードを掲載する場合など、思わぬ制約に直面したときにも書式が崩れにくくなります。1行80文字は、変数名を考えるときや長いコードを書くときのよい目安にもなり、環境を問わずコードを楽に読めるようにする意味でも守っておくとよいと思います。

本チュートリアルではこの後も、より細かいコード書式設定の例をいくつか紹介する予定です。

著者は、よいコード書式を習慣づけるためにも、基本的にNode.jsのREPLでもファイルと同じように書式を整えるよう普段から心がけています。

なお、実際のREPLでは必ずしも入力したとおりの書式で表示されるとは限りません。たとえば、Node.jsのREPLの実装によっては、開き波かっこ{を入力してReturnキーを押すと、次の行の冒頭にドット3つ...を表示して、ブロックの始まりであることがわかるようにしてくれることもあります(図 2.6)。このような表示の違いについては気にするほどのものではありません。気になる方は、例の「技術の成熟」精神(コラム 1.1)を発揮して、REPLコードのサンプルをREPLを入力すると実際の表示がどう変わるかを調べて適宜調整するとよいでしょう。

images/figures/repl_indentation
図 2.6: REPLではコードの書式が一般的なインデントと違うこともある

さて、続いてはelseを用いて、条件に応じた振る舞いをもうひとつ追加してみましょう。条件がfalseの場合は、elseに続く波かっこ...の内側が実行されます(リスト 2.7)。

リスト 2.7: ifelseによる制御フロー
> password = "foobar";
> if (password.length < 6) {
    "パスワードが短すぎます";
  } else {
    "パスワードの長さは適切です";
  }
'パスワードの長さは適切です'

リスト 2.7では、先ほど定義したpassword変数に新しい値を再代入している、つまりpassword変数を再定義している点にご注意ください。password変数は定義済みなので、再定義にはletを付けません。

password変数の新しい値の長さが6になったので、password.length < 6という条件はfalseになります。この場合、JavaScriptはifの直後のブロック(これをifブランチとも呼びます)内にある文を評価せず、代わりにelseブランチを評価します。その結果、"パスワードの長さは適切です"というメッセージが出力されます。

2.4.1 論理値の反転と組み合わせ

&&(論理AND)」「||(論理OR)」を用いて論理値同士を組み合わせることも、「!(NOT演算子)」を用いて論理値を反転することもできます。

まずは&&演算子からやってみましょう。&&の左右に置かれた論理値が「両方ともtrue」になると、全体がtrueという論理値になります。

たとえば「フレンチフライを食べたいか?」という条件と「ベイクドポテトを食べたいか?」という条件を&&でつなげたとしましょう。この場合は、両方の条件がtrueの場合にのみ、全体がtrueになります。言葉で書けば「フレンチフライとベイクドポテトを両方とも食べたいか?」となります。

今の私がフレンチフライも食べたいしベイクドポテトも食べたいとすると、どちらの条件もtrueになり、全体もtrueになります。

「フレンチフライを食べたいか?」という条件と「ベイクドポテトを食べたいか?」という条件のどちらかが(あるいはどちらも)falseになると、全体もfalseになります。

このような組み合わせを表にしたものを真理値表(truth table)と呼びます。&&演算子の真理値表をリスト 2.8に示します。

リスト 2.8: 2つの条件を&&でつないだ場合の真理値表
> true && true
true
> false && true
false
> true && false
false
> false && false
false

この真理値表をJavaScriptコードに落とし込んだものをリスト 2.9に示します。

リスト 2.9: 条件で&&演算子を使う
> let x = "foo";
> let y = "";
> if (x.length === 0 && y.length === 0) {
    "どちらの文字列も空です";
  } else {
    "少なくともどちらかは空ではありません";
  }
'少なくともどちらかは空ではありません'

リスト 2.9のうち、「y.length」の部分の実際の値は0ですが、「x.length」の部分の実際の値は0ではありません。そのため、2つの条件を&&で組み合わせた結果はfalseになります(この結果は、リスト 2.8と一致します)。したがって、elseのブロックが評価されます。

(x.length === 0 && y.length === 0)をもう少し噛み砕いて説明すると、「xの長さとyの長さは両方ともゼロか?」ということです。同じことをもう少し堅苦しく書くと、「xの長さがゼロである」かつ(AND)「yの長さはゼロである」は正しいか?となります。

このとおりになったとき、つまりxの長さとyの長さが両方ともゼロの場合は、条件全体の評価結果がtrueになります。

xの長さとyの長さのどちらかがゼロでない場合、およびxの長さとyの長さがどちらもゼロでない場合は、条件が満たされないので、条件全体の評価結果がfalseになります。

これが、&&演算子による「論理AND」であり、これが表す論理は「論理積」と呼ばれます。なお、&を2個使っているのは、&が1個だけだと違う意味を表すからです(ビット論理積: 本チュートリアルではこれについて扱いません)。

次は「||(論理OR)」です。||&&のときと異なり、2つの条件の「いずれか片方」または「両方」がtrueの場合に、全体の結果がtrueになります。

||の真理値表をリスト 2.10に示します。

リスト 2.10: 2つの条件を||でつないだ場合の真理値表
> true || true
true
> true || false
true
> false || true
true
> false || false
false

条件に||を使ったJavaScriptコードをリスト 2.11に示します(xyの値は、先ほどの&&のときの値が使われる前提です)。

リスト 2.11: 条件で||演算子を使う
> if (x.length === 0 || y.length === 0) {
    "少なくともどちらかの文字列は空です";
  } else {
    "どちらの文字列も空ではありません";
  }
'少なくともどちらかの文字列は空です'

ここで、リスト 2.10の真理値表をよ〜く見てください。||は論理ORだと申し上げましたが、「2つの条件の片方だけがtrueのときだけtrueではないことにご注意ください!ここが重要です。「2つの条件が両方ともtrueのときにもtrue」なのです。これが「論理和」すなわち論理ORです。

日常語の「〜か〜」「〜または〜」「〜もしくは〜」といったORっぽい言葉は、そのままだと曖昧なのです。たとえば「フレンチフライかベイクドポテトが食べたい」と言う場合、おそらく「フレンチフライかベイクドポテト、どちらかだけ食べたい」という意味で使われるでしょう(図 2.712)。両方食べたいという意味で「AかB」と言う人はあまりいないと思いますが、ゼロとは言い切れません。

繰り返しますが、||が表す論理ORは、日常語と違い、「両方ともtrue」の場合含まれるのです。だからこそ、わざわざ論理ORと呼んで、日常語と違うことを強調しています。

同様に、論理ANDも日常語とは違います。日常語のANDっぽい言葉は、「AおよびBおよびC」「AやBやC」「AとBとC」のような日常語は、要素を並べる(列挙する)ときに「も」使いますし、「最初にAを行い、次にBを行い...」のように句(フレーズ)と句を接続するときに「も」使われます。

しかし論理ANDは、条件と条件の関係を表すときに「しか」使いません。そうしないと曖昧さが生じて混乱するからです。

論理学もコンピュータも、日常語ではない論理ANDや論理ORを前提に厳密にロジックを組み立てています13。どうかお忘れなく。

images/figures/fries
図 2.7: 食べたかったのはフレンチフライだけなのよね

JavaScriptでは、&&||の他に、「否定(negation)」を表す!演算子(NOT演算子)も使えます14!を条件の直前に付けると、truefalseになり、falsetrueになります(リスト 2.12)。要するに、truefalseを反転させる機能です。

リスト 2.12: !を冒頭に付けた場合の真理値表
> !true
false
> !false
true

条件で!を使った場合のコードをリスト 2.13に示します(xの値は、先ほどの||のときの値が使われる前提です)。

リスト 2.13: 条件で!演算子を使う
> if (!(x.length === 0)) {
  "xは空ではありません";
} else {
  "xは空です";
}
'xは空ではありません'

リスト 2.13は有効なJavaScriptです。この場合x.length === 0という条件はfalseになりますが、それが!で反転されるので結果はtrueになります。

> (!(x.length === 0))
true

上では単独のNOT演算子!を使いましたが、実際には!==という「等しくないことを表す」演算子を使う方が一般的です(!だけだと見落としやすいので)。

> if (x.length !== 0) {
  "xは空ではありません";
} else {
  "xは空です";
}
'xは空ではありません'

2.4.2 否定の否定

JavaScriptで論理値を得るために、必ずしも2つの条件を比較しなければならないわけではありません。実を言うと、JavaScriptでは数値や文字列などのあらゆるオブジェクトを(半ば無理やりですが)trueまたはfalseのどちらかに判定できます。

これは、否定のNOT演算子!を2つつなげた!!を使うことで行なえます15!!は「否定の否定」を表すので、以下のようにtrueの「否定の否定」はtrueに、falseの「否定の否定」はfalseになるというわけです16

> !!true
true
> !!false
false

!!truefalseの前につけても何も変わらないので、これだけでは全然面白くありません。しかし!!は、truefalse以外のオブジェクトにつけたときの挙動がポイントなのです。たとえば以下のように、文字列"foo"の直前に!!を付けると、文字列"foo"の論理値はtrueと評価されます。つまり、論理値でない値が、論理値として評価されます。

> !!"foo"
true

理由はおわかりでしょうか?実はJavaScriptでは、「空文字列""の論理値はfalse」「空でない文字列はすべてtrue」と決められているのです(参考17

> !!""
false

リスト 2.9のコードでは、わざわざ文字列の長さを調べて比較していましたが、今紹介した「論理値でないものを論理値として評価する」テクニックを用いれば、「xの否定かつyの否定」という形にすることで、リスト 2.14のようにもっとコンパクトに書き直せます。

リスト 2.14: 論理値でないもの(文字列)を論理値として評価する
> if (!x && !y) {
    "どちらの文字列も空です";
  } else {
    "少なくとも片方の文字列は空ではありません";
  }
'少なくとも片方の文字列は空ではありません'
コラム 2.8. 否定表現に気をつけよう

否定のNOT演算子!は、ロジックを組み立てるうえで非常に重要であることはたしかです。しかし否定をむやみに使うと人間にとって逆に読みにくくなることもあります。

たとえば、先ほどの(!x && !y)という条件はコンパクトですが、意味を知るために読み解かなければなりません。言葉で書き表すと「x(は空)ではない」かつ「y(は空)ではない」ということになります。

一方、この条件を書き直した(x.length > 0 && y.length > 0)は冗長ですが、否定を含まない分人間にとって読みやすくなっています。言葉で書き表すと「xの長さは0よりも大きい」かつ「yの長さは0よりも大きい」ということになります。

一般に、同じロジックでも否定を含まない表現の方が、冗長になる代わりに人間にとって読みやすいものです。これはプロのプログラマーであっても同じで、ベテランは多くの場合、なるべく否定を含まないコードで書こうとします。

もちろん、ロジックを(!x && !y)のようにコンパクトに圧縮できるのはプログラマーにとって快感ですし、実行速度も向上する傾向があります。しかし業務で書くコードは、なるべく「クイズにしない」方がよいと言えます。普段から誰でも素直に読めるコードを書く方が、未来の自分を助けます。

否定でさらに怖いのは「部分否定」です。

日常の話し言葉や書き言葉であっても、否定語は「どこからどこまでを否定するのか」がしばしば曖昧になります。英語で「You can’t win them all.」という慣用句があります。これは「いつも勝てるわけではない」すなわち「世の中思い通りにはならないものだ、諦めが肝心」というニュアンスで使われます。この慣用句の「not」(can’t)の対象「them」に、「all」が付いているので、「すべてというわけではない」という部分否定になります。日本人はこの慣用句を「まったく勝てない」と誤って解釈しがちなので、このパターンはよく試験問題にも使われます。それほど部分否定は間違えやすいのです。

コードの条件も、部分否定を含むと間違えやすくなります。たとえば、(!x && !y)という条件ではなく、(x && !y)のように条件の片方だけ否定演算子が付けられると、一気に難易度が上がってしまいます(クイズとしては面白いので、ご興味があれば(x && !y)の真理値表を自分で書いてみるとよいでしょう)。

余談ですが、論理学では部分否定の曖昧さを回避するために、「(Aが存在する)ではない」のように丸かっこを用いてどこからどこまでが否定の対象なのかを明確にすることもあります。現代の論理学はさらに徹底していて、言葉ではなくもっぱら記号で表すので、まるでプログラミング言語のように見えます。論理学は完成された学問だと思われがちですが、実際には多数の流派が存在し、現在も盛んに研究されていて、決して完成された学問ではありません(なお、コンピュータで用いられているブール論理学は完成しています)。

2.4.3 演習問題

  1. xの値が"foo"yの値が""とすると、x && yという条件の値がどうなるかを確かめて答えてください。次に、このx && y全体に否定の否定!!を用いて論理値を求めるとfalseになることを確かめてください。(ヒント: 式の組み合わせ全体に!!を適用したい場合は、式全体を丸かっこ()で囲みます。)
  2. 上の演習に続けて、x || yという条件の値がどうなるかを確かめて答えてください。次に、この条件を論理値として評価すると値がどうなるかを確かめて答えてください。次に、リスト 2.14の条件をx || yに置き換えて、置き換え前と同じ結果が得られるようにコードを書き直してください。(ヒント: 文字列の順序を入れ替えてみましょう。)

2.5 メソッド

2.4で述べたように、JavaScriptのStringオブジェクトにあるプロパティはlengthだけです。しかしStringオブジェクトでは実にさまざまな「メソッド(method)」がサポートされています18。オブジェクト指向プログラミング言語における文字列(Stringオブジェクトのインスタンス: 以後は文字列インスタンスと呼びます)は、特定のメソッドに「応答」します。1.3.1で見たように、文字列インスタンスにドット.を付けてそのメソッドを呼び出せるということです(インスタンスの意味については7で解説します)。

たとえば、文字列インスタンスはtoLowerCase()というインスタンスメソッドに「応答」します。このインスタンスメソッドは、以下のように大文字の「HONEY BAGDER」という文字列を「honey badger」のようにすべて小文字に変換します19

$ node
> "HONEY BADGER".toLowerCase();
'honey badger'

このtoLowerCase()メソッドは、たとえばメールアドレスをすべて小文字に揃えたい場合などに便利です20

> let username = firstName.toLowerCase();
> `${username}@example.com`;   // サンプルのメールアドレス
'michael@example.com'

ここでひとつご注意いただきたいことがあります。lengthなどの「プロパティ」には丸かっこ()を付けませんでしたが、メソッド名の末尾には、以下のように何も渡さない場合であっても空の丸かっこ()を付けなければなりません。

toLowerCase()

JavaScriptは、名前(正確には識別子)の末尾に丸かっこ()が付けられると、それをメソッド(関数)として認識します。toLowerCase()メソッドは引数を取らないのですが(小文字変換に引数を指定する必要はありません)、メソッドであることを認識させるためには丸かっこ()が必要です。ついでながら、toLowerCase()というメソッド名がcamelCase(キャメルケース)に従って「最初の単語toは小文字、LowerとCaseの頭は大文字」の形で命名されていることにもご注目ください(2.2)。

小文字変換があるなら大文字変換があるでしょう。お察しのとおり、JavaScriptにはそのためのメソッドも用意されています。ここで実際の例を見る前に、大文字変換用のメソッド名がどんな名前かを自分で推測してみてください。ヒントは、小文字は英語で「lower-case」、大文字が「upper-case」であることです(図 2.821)。

letter_case
図 2.8: 昔の活版印刷で大文字の活字を「上のケース」、小文字の活字を「下のケース」に入れていたのが由来です。

さて、皆さんの推測は当たっていたでしょうか?答えを見てみましょう。

> lastName.toUpperCase();
'HARTL'

こういう勘が働くことが、例の「技術の成熟」のあかしです。ただしコラム 1.1でも述べたように、ドキュメントを使いこなすスキルも重要です(技術英語をどしどし読めることも重要です)。特にMDNの公式ドキュメント『Stringオブジェクト』には、文字列操作に便利なインスタンスメソッドがずらりと掲載されています22

それでは、Stringオブジェクトにはどんな便利メソッドがあるか、いくつか見ていくことにしましょう(図 2.9)。

images/figures/javascript_string_methods
図 2.9: JavaScriptの文字列操作メソッドの一部

図 2.9のメソッドを見ると、下の方にこんなコードが書かれていて、その後ろに簡単な説明文があります。

String.prototype.includes(searchString [, position])

このString.prototypeとは何でしょうか?これについては7で解説しますが、実際の答えは「知らなくてもドキュメントを読むには問題ない」です。実を言うと、プロ開発者でもString.prototypeのような部分はどしどし読み飛ばしています。重要でない部分をさっさと読み飛ばせるスキルも、昔からある「技術の成熟」のあかしのひとつです。細部が気になってくると思いますが、学び始めの時期にはそれも当然ですし、実際重要です(最初からまったく気にしない人はむしろ問題です)。慣れてくれば、どこが重要でないかがだんだん見えてくるようになりますので、ご心配なく。

さて、MDNドキュメントのString.prototype.includes(searchString [, position])の部分がリンクになっているので、クリックして下にスクロールすると、豊富なコード例が掲載されています(図 2.10)。

images/figures/string_includes_examples
図 2.10: includes()メソッドのコード例(constと表記されている)

図 2.10のコード例を実際に動かしてみましょう。その際、以下のように置き換えることにします。

  1. constletに置き換える
  2. strsoliloquy(芝居の「独り言」の意)に置き換える。
  3. 文字列を囲む一重引用符を二重引用符に置き換える
  4. シェークスピアの原文どおりになるよう、文字列末尾のピリオドをコロンに置き換えます(図 2.1123
  5. console.logを省略する
  6. コメントを書き換える
images/figures/hamlet
図 2.11: デンマークの王子ハムレット:「生きるべきか、死すべきか、それが問題なのだ。」

以上のように変更したコードをNode.jsのREPLで動かした結果は以下のようになります(リスト 2.15)。

リスト 2.15: includes()すべきかすべきでないか、それが問題なのだ。
> let soliloquy = "To be, or not to be, that is the question:";
> soliloquy.includes("To be");        // "To be"という文字列があるか調べる
true
> soliloquy.includes("question");     // "question"はあるか?
true
> soliloquy.includes("nonexistent");  // この文字列は存在しない
false
> soliloquy.includes("TO BE");        // 大文字小文字は区別される
false
> soliloquy.includes("To be", 1);     // 問題: この「,1」の意味がわかりますか?
false
> soliloquy.includes("o be,", 1);     // (上のヒント)
true

リスト 2.15のコードのうち、最後の2行については本チュートリアルであえて解説していないので、現時点ではおそらく意味がわからないでしょう。これについては演習問題に回します。本セクションの演習問題では、よく使われる文字列操作メソッドを他にもいくつか紹介します。

2.5.1 演習問題

  1. 「badger」という文字列(ただし大文字小文字を区別しない)が「hoNeY BaDGer」という文字列に含まれているかどうかを調べるJavaScriptコードを書いてください。
  2. 先ほどのincludes(string, i)iはどのような意味になるのかを考えて答えてください。(ヒント: JavaScriptでは文字列の先頭にある文字は「0文字目」と数えます。「1文字目」ではないのです。)

2.6 文字列のイテレーション

文字列の章の最後を飾るのは「イテレーション(iteration: 反復処理)」です。イテレーションとは、あるオブジェクトの中にある要素(element)を端から1個ずつ処理することを繰り返す形で、オブジェクト内の要素をすべて処理することです。イテレーションは、コンピュータプログラミングの基本的な手法です。この後の3.55.4でもイテレーションの例を取り上げます。また、68.5ではイテレーションを部分的に行う(つまり端から端まですべて行うことを避ける)方法を学びますが、これを身に付けると開発力がぐんと高まります。

本セクションでは、文字列についてのイテレーション、つまり文字列にある文字を端から1文字(character)ずつ扱う方法を学びます。今後は単に「文字列をイテレートする」とも書きます。

文字列をイテレートするために学ばなければならないことが2つあります。1つ目は、ある文字列の中にある特定の文字にアクセスする手段です。2つ目は、「ループ(loop)」の書き方です。

文字列内の特定の文字にアクセスする方法は、MDNに掲載されている文字列操作メソッドのリストで調べられます。調べてみると以下のような記述が見つかります。

String.prototype.charAt()       index で指定された位置の文字 (UTF-16 コード1個から成ります) を返します。

このリンクをクリックすると、charAt()メソッドそのものの意味と「index」の意味を調べられます。2.5で用いたsoliloquy文字列を使ってcharAt()メソッドを実行する例をリスト 2.16に示します。

リスト 2.16: charAt()メソッドの振る舞いを調べる
> console.log(soliloquy);   // (先ほど入力した文字列が残ってることを確かめる)
To be, or not to be, that is the question:
> soliloquy.charAt(0);
'T'
> soliloquy.charAt(1);
'o'
> soliloquy.charAt(2);
' '

リスト 2.16では、charAt(0)を実行すると文字列の「最初の文字」が返され、charAt(1)を実行するとその次の文字が返される、というふうに進みます。

ここが重要です。このとき、文字列の最初の文字は0番目、その次の文字は1番目、その次の文字は2番目と数えます。そして、この数値を「添字(index)」と呼びます24

「最初の文字が0番目」というのは一見直感に反する感じがしますが、プログラミング言語では、何かを数えるときに「ゼロ始まり(zero-offset: ゼロオフセット)」で考えることが広く行われています。そもそもコンピュータの設計が基本的にゼロオフセットを採用しているので仕方ないとも言えます。

ゼロオフセットを学んだので、いよいよ最初のループの例を見ていきましょう。ここではforループを使います(リスト 2.17)。

forループでは、以下のように添字用の変数iを定義し、指定の最大値に達するまでiの値をカウントアップしながらループを実行します。

リスト 2.17: シンプルなforループ
> for (let i = 0; i < 5; i++) {
  console.log(i);
}
0
1
2
3
4

このようなループの書き方は、JavaScriptはもちろんのこと、「C言語」「C++言語」から「Java言語」「Perl言語」「PHP言語」「Go言語」に至るまで、驚くほど多くの言語で共通しています(構文が微妙に違う言語もありますが)。

リスト 2.17forループをじっくり読んでみましょう。

forループの条件は()の中に3つ書き、条件と条件の間はセミコロン;で区切ります。

1番目の条件let i = 0はループの「開始条件」を表します。具体的には、letキーワードで添字用の変数iを定義し、開始の値を0に設定しています。

コラム 2.9. 添字変数の文字

昔から添字の変数にはiがよく使われます(indexの頭文字)が、これ以外の変数ももちろん使えます。特別な意図がない限り、iを使う方が読む人にとっても「ああ、これは添字なのね」とすぐわかってもらえます。

もともと数学で、未知数にアルファベットの終わりである「x」「y」「z」を使い、定数に「a」「b」「c」から順に使う習慣がありました。そして、それ以外に一時的な「使い捨て」の変数を表すのに「i」「j」「k」「l」「m」「n」という真ん中あたりのアルファベットを順に使う習慣もありました。

iが添字変数に使われるのは、indexの頭文字でもあり、数学でよく使われる上述の一時変数ともマッチしていたからでしょう。

プログラミングでも、一時的な使い捨て変数には「i」「j」「k」「l」「m」「n」の順に使うことがよくあります。

2番目の条件i < 5は、ループの「継続条件」を表します。つまり、iが5より小さい間は、ループを続けるという意味です。iが5より小さい間は、この継続条件はtrueになります。そしてiが5より小さくない場合はfalseになり、ループを終了します。

3番目の条件i++は、添字変数の「増分(increment)」を表します。つまり、ループ本体を下まで実行するたびにiに1を足す(つまりiの値を1増やす)という意味です。

なお、i++という奇妙な記法は、i = i + 1iに1を足す)を略記したものです。ループ構文では「1ずつ足す」「1ずつ減らす」操作が非常によく使われるので、i++という略記が多用されます。増分の反対語は「減分(decrement)」で、勘のよい人ならi+++-に変換して書けば良いと見当がつくでしょう。減分はi = i - 1(iから1を引く、つまりiの値を1減らす)という意味です。

さて、リスト 2.17のコードを見て「全然わからん」「読んでて目がつらい」と感じた方、あなたは正常です。

forループは昔からプログラミングの重要な基本であり、広く使われているのもたしかです。しかし私は、forループをしっかり学んだうえで「できるだけ使わずに済ます」ことが、よいプログラミングのあかしだと考えています。

forループを紹介しておいて、使うなってどういうこと?」と思われるかもしれませんね。間違えないでいただきたいのですが、forループを絶対使ってはいけないという意味ではありません。現代にはforループよりもずっとよい書き方があるので、可能な限りよい書き方を優先して使いましょうと申し上げたいのです。「forループで書くしかない場合にだけ、奥の手としてforループを使う」のが基本の姿勢です。

ではforループよりも優れた書き方とは何でしょうか?ひとつは、5.4で学ぶ「forEachループ」です。

もうひとつのよい方法は、68.5で学ぶ、「関数型プログラミング」を応用する方法です。Mike Vanierというコンピュータ科学者(図 2.12)は私の個人的な友人でもありますが、彼はかのPaul Graham氏に宛ててこんなメールを送ったことがあります。

(forループのような)退屈きわまる繰り返しは、書いていてすぐ飽きてしまいます。(i = 0; i < N; i++)のようなループ構文を1回書くたびに5セントずつもらえたら、今ごろ私は億万長者でしょう。

Mikeがメールに書いたforループは、リスト 2.17とほとんど同じです。違いといえば、letがないことと、継続条件でNが(適当な数字の代わりに)使われていることだけです。

images/figures/mike_vanier
図 2.12: forループをあと数個書けばMike Vanierも億万長者

forループを書かずに済ます方法については6で学びますが、今はリスト 2.17forループをしっかり身につけて、使えるようにしておくことです。

さて、いよいよリスト 2.16リスト 2.17を組み合わせて、ハムレットの有名な独白をイテレートする準備が整いました。

今回使うforループは、継続条件の部分だけを少し変えます。リスト 2.17では、継続条件で指定する上限値がi < 5のように固定されていました。つまり添字変数iが0、1、2、3、4とカウントアップし、5になったらループを終了しますので、ループする回数が常に5回になるということです。

しかしsoliloquy変数に入れるハムレットの独白はそれよりも少し長いので、独白の文字列に含まれる文字を数えておく必要がありそうです。文字数を人力で数えてもよいのですが、せっかくJavaScriptを学んでいるのですから、2.4で学んだlengthプロパティで以下のようにさくっと調べましょう。

> soliloquy.length
42

文字数は42と出ました25。ではforループでこの値を使いましょう。

for (let i = 0; i < 42; i++) {
  console.log(soliloquy.charAt(i));
}

上のコードはもちろんちゃんと動きますし、リスト 2.17で学んだforループの完璧な応用です。

しかしここでこんな疑問が頭をもたげるかもしれません。「42というループの回数をforループに直接書く必要ってあるのかな?」「最初からlengthプロパティをforループの条件に書いてしまえばいいのでは?」

はい、おっしゃるとおりです。ループ回数のナマの上限値をforループに直接書く(これを「ハードコード」と呼びます)べきではありません。この上限値を条件にハードコードしてしまうと、文字列の長さが変わったときに対応できなくなってしまい、汎用性が損なわれてしまうからです。

今回のような場合も、forループの条件にナマの数値ではなく、lengthプロパティを使うのが常套手段です。lengthを使うよう書き換えた結果をリスト 2.18に示します。

リスト 2.18: forループでlengthプロパティを使う
> for (let i = 0; i < soliloquy.length; i++) {
  console.log(soliloquy.charAt(i));
}
T
o

b
e
.
.
.
t
i
o
n
:

前述のとおり、forループはあくまで「最後の手段」に取っておき、通常はできるだけforループを避けるのがベストです。しかしながら、forループはエレガントとは言い難いにもかかわらず、ループを最初に学ぶにはとても向いています。

この後の8でも解説されているように、プログラミングではコードだけを書けばよいのではなく、「テストコード(単にテストとも呼びます)」も書くようにしなければなりません。最初に(コードではなく)テストを書き、次にテストがとにかくパスするように実際のコードを書き、次に「リファクタリング(refactoring)」でコードを改善する、という3つのステップを繰り返す、「TDD(test-driven development: テスト駆動開発)」という強力な手法があります。この2番目のステップでは、最初からかっこよいコードを書こうとせず、「拙くてもダサくてもよいので、わかりやすいコードを書く」必要があります。forループはこういうときによく使われます。

最初にforループで動きを確かめておいて、それからもっとよい方法でループを書くのは、一見遠回りなようでいて、実は非常に効果的です。最初から難しい魔法を繰り出そうとするとハマることがよくあります。最初からホームランを狙って大振りするより、着実な出塁と進塁を目指しましょう26

2.6.1 演習問題

  1. 最初にletを用いて、soliloquyの長さと等しいNという変数を定義してください。次にMike Vanierが書いていたコードを(書き換えずに)そのままforループの条件として使って、実際にJavaScriptで動かせることをNode.jsのREPLで示してください。なお、letを使わなくても一応動きますが、letを省略するのはよくありません
  2. リスト 2.18で使ったcharAt()メソッドの代わりに、soliloquy[i]のような「波かっこ記法」を使っても同じ結果が得られます。実際にNode.jsのREPLでやってみてください。
1. 「補間」「式展開」と訳されることもあります。
2. ついでに申し上げれば、交換法則は行列の積では一般に成り立ちません: \( AB \neq BA \)
3. 画像引用元: Wikimedia(2018-02-19)Copyright © 2009 by Silver Spoon Sokpop、クリエイティブ・コモンズ「表示 - 継承 3.0 非移植 (CC BY-SA 3.0)」 ライセンス条項に基づいて無改変にて利用。
4. 英語ではbacktickとも呼ばれます。
5. Perl言語やPHP言語の経験がある方は、式の中で"Michael $lastName"のように変数にドル記号を付けて書ける構文との違いを調べてみましょう。
6. なお、出力することを英語ではprintと書きますが、これは大昔のコンピュータの端末で実行結果をラインプリンタに出力していた名残りです。
7. 本チュートリアルの範囲を大きく超えますが、世の中には関数型プログラミングという通常のプログラミング言語と大きく異なる分野があり、そこでは副作用を積極的に排除することが目的のひとつになっています: ただし関数型プログラミングは非常に抽象度が高く、また数学的素養を強く求められます。
8. 文字の「サイズ」や「長さ」と呼ぶこともあります。
9. 画像引用元: Wikimedia(2018-02-19)、パブリックドメイン。
10. このように波かっこ{...}を使う記法は、C言語およびC言語の影響を受けた言語でよく見かけます。
11. 昔のターミナル画面は1行あたり80文字までしか表示できませんでした。
12. 画像引用元: Wikimedia(2018-02-19)、Copyright © Popo le Chien、クリエイティブ・コモンズ「表示 - 継承 3.0 非移植 (CC BY-SA 3.0)」ライセンスに基づいて無改変にて利用。
13. 英語の「and」や「or」にも似たような曖昧さがあります。英文でしばしば「and/or」と書くのもさらに曖昧さを助長しています。
14. !は英語圏でしばしば「バン(bang)」と発音されます。おそらく正式の「exclamation mark(感嘆符)」だと長過ぎるのでしょう。
15. 英語圏では「バンバン」と発音されますが、日本だと「ダブルびっくり」などとかわいく呼ぶ人もいます
16. なお、日本語の「二重否定」は、「〜しないわけにはいかない」などのように意味を強める強調のはたらきがありますが、論理値の「否定の否定」にそのような効果はありません。
17. 何をtrueと評価し、何をfalseと評価するかは、実は言語によってかなり異なります(これは混乱の元でもあるのですが)。たとえばRubyでは、空文字列""であっても論理値はtrueと評価されます。
18. 1.3.1で、メソッドはオブジェクトにアタッチされた一種の関数であり、ドット.記法を用いて呼び出せると学んだことを思い出しましょう。
19. 日本語には大文字小文字の概念がないので、日本語の文字列はtoLowerCase()で変換されず、エラーにもなりません
20. もし皆さんがNodeコンソールでセッションを終了してしまった場合、それまでに定義したfirstName変数などは消えてしまいます。定義したものは他のセッションで保持されないので、Nodeコンソールを再度立ち上げても元に戻りません。このような場合は、例の「技術の成熟」精神(1.1)を発揮して解決しましょう。ヒントを申し上げると「セッションは消えますが、コマンド履歴は残っています」。
21. 画像引用元: Wikimedia(2018-02-19)、Copyright © 2013 by Maggie McCain、クリエイティブ・コモンズ「表示 2.0 一般 (CC BY 2.0)」ライセンスに基づいて無改変にて利用。
22. こうした情報は MDN公式Webサイトでももちろん探せますが、実を言うと著者の場合は十中八九「javascript string」でググっています。
23. 画像引用元: Wikimedia(2018-02-19)、パブリックドメイン。
24. なお、indexの複数形は本来indicesなのですが、近年はindexesと書かれることも少しずつ増えてきています。また、日本語では配列に使うindexを「添字」、それ以外(特にデータベース用語)では「インデックス」と呼ぶのが一般的です。
25. 偶然ですが、有名な「生命、宇宙、そして万物についての究極の疑問の答え」と同じ数値です。
26. 余談ですが、現代のプロ野球ではデータ分析が非常に進んでおり、たとえば打者の評価はホームランの数や打率よりも出塁率が最も重要視されています: Wikipedia
前の章
第2章 文字列 JavaScript編
次の章