2024年9月24日

演算子

多くの演算子は既に学校で学んでおり、よく知られています。加算 +, 乗算 *, 減算 - などです。

このチャプターでは、単純な演算子から初めて、次に学校の数学ではカバーされないJavaScript 固有の側面に集中していきます。

用語: “単項演算子”、 “二項演算子”、 “オペランド”

次に進む前に一般的な用語を理解しましょう。

  • オペランド – は演算子が適用されるものです。たとえば、 乗算 5 * 2 では、2つのオペランドがあります: 左のオペランドは 5, 右のオペランドは 2 です。“オペランド” は “引数” と呼ばれることもあります。

  • 演算子が単一のオペランドをもつ場合は 単項演算 です。たとえば、負の単項演算 "-" は数値の符号を反転します:

    let x = 1;
    
    x = -x;
    alert( x ); // -1, 負の単項演算が適用されました
  • 演算子が2つのオペランドを持つ場合は 二項演算 です。同じマイナスも二項演算で同様に存在します:

    let x = 1, y = 3;
    alert( y - x ); // 2, 二項演算子マイナスは値を減算します

    正式には、ここでは2つの異なる演算子について話をしています。: 負の単項演算(単一のオペランド, 符号の反転) と二項演算による減算(2つのオペランド、減算)です。

Maths

以下の算術演算子がサポートされています:

  • 加算 +,
  • 減算 -,
  • 乗算 *,
  • 除算 /,
  • 剰余 %,
  • べき乗 **.

最初の4つはそのままです。%** は以下で補足します。

剰余 %

剰余演算子 % は % という表記にも関わらずパーセントとは関係ありません。

a % b の結果は ab で除算したあまりです。

例:

alert( 5 % 2 ); // 1, 5 / 2 のあまり
alert( 8 % 3 ); // 2, 8 / 3 のあまり

べき乗 **

べき乗演算子 a ** ba 自身を b 回かけます。

例:

alert( 2 ** 2 ); // 4  (2 を 2 回)
alert( 2 ** 3 ); // 8  (2 * 2 * 2, 3 回)
alert( 2 ** 4 ); // 16 (2 * 2 * 2 * 2, 4 回)

数学的には、べき乗は非整数値も同様に定義されています。例えば、平方根は 1/2 によるべき乗です:

alert( 4 ** (1/2) ); // 2 (1/2 の累乗は平方根と同じです)
alert( 8 ** (1/3) ); // 2 (1/3 の累乗は立方根と同じです)

文字列の連結、二項演算子 +

では、学校の算数を超えた JavaScript 演算子の特別な機能を見ていきましょう。

通常、プラス演算子 + は数値の合計です。

しかし二項演算子 + が文字列に適用された場合は、お互いの文字を結合します。

let s = "my" + "string";
alert(s); // mystring

一方のオペランドが文字列の場合、他のオペランドも文字列に変換されることに注意してください。

例:

alert( '1' + 2 ); // "12"
alert( 2 + '1' ); // "21"

ご覧の通り、どちらのオペランドが文字列なのかは関係ありません。

こちらはより複雑な例です:

alert(2 + 2 + '1' ); // "41" であり、"221" ではありません

ここで、演算子は順番に動作します。最初の + が2つの数値を合計し 4 を返します。次に、2つ目の + が文字列 1 を足します。なので、4 + '1' = '41' のようになります。

alert('1' + 2 + 2); // "122" であり "14" ではありません

ここで、最初のオペランドは文字列なので、コンパイラは他の2つのオペランドも文字列として扱います。2'1' と連結するので、'1' + 2 = "12" となり、 "12" + 2 = "122" となります。

二項演算子の + はこのような方法で文字列をサポートする唯一の演算子です。他の算術演算子は数値でのみ動作し、常にオペランドを数値に変換します。

例えば、減算と除算です:

alert( 6 - '2' ); // 4, '2' を数値に変換します
alert( '6' / '2' ); // 3, 両方のオペランドを数値に変換します

数値変換 単項演算子 +

プラス + は2つの形で存在します。上で使ったような二項演算の形式と単項演算の形式です。

単項演算子プラス、もしくは言い換えると単一の値に適用されるプラス演算子 + は、数値に対しては何もしません。しかし、オペランドが数値でない場合は数値に変換します。

例:

// 数値の場合、何の影響もありません
let x = 1;
alert( +x ); // 1

let y = -2;
alert( +y ); // -2

// 非数値を数値に変換します
alert( +true ); // 1
alert( +"" );   // 0

これは Number(...) と同じですが、より短い表現です。

文字列から数値への変換が必要なケースは多いです。例えば、HTMLのフォームフィールドから値を取得する場合、それらは通常文字列です。今、それらの合計が欲しい場合はどうなるでしょう?

二項演算子プラスはそれらを文字列として結合します。:

let apples = "2";
let oranges = "3";

alert( apples + oranges ); // "23", 二項演算子プラスは文字列を結合します

数値として扱いたい場合は変換して合計します:

let apples = "2";
let oranges = "3";

// 二項演算子プラスの処理の前に、両方の値が数値に変換されます
alert( +apples + +oranges ); // 5

// 長い書き方
// alert( Number(apples) + Number(oranges) ); // 5

数学者の立場からは、余分なプラスは奇妙に見えるかもしれません。しかし、プログラマの立場からは特殊なことではありません: 単項演算子プラスが最初に適用され、文字列から数値に変換されます。次に二項演算子プラスはそれらを合計します。

なぜ二項演算子プラスの前に単項演算子プラスが適用されるのでしょうか? それは、単項演算子プラスの 優先順位が高いため です。

演算子の優先順位

式が1つ以上の演算子をもつ場合、実行順はそれらの 優先順位 により決められます。言い換えると、演算子の間には暗黙の優先順があります。

学校で学んだように、式 1 + 2 * 2 の場合、私たちは加算の前に乗算をすることを知っています。それがまさに優先順位です。乗算は加算より より高い優先順位 です。

丸括弧はどの優先順位よりも優位に立つので、もとの優先順位が不適当であれば丸括弧を使います。たとえば (1 + 2) * 2 のようにです。

JavaScriptでは多くの演算子があります。どの演算子も対応する優先順位を持っています。より大きな値をもつ演算子は最初に実行されます。同じ優先順の場合、実行順は左から右になります。

優先順位テーブルの抜粋(これを覚えておく必要はありませんが、単項演算子は対応する二項演算子よりも優先順位が高いことに留意してください):

優先順位 名前 符号
15 単項プラス +
15 単項否定 -
14 冪乗 **
13 乗算 *
13 除算 /
12 加算 +
12 減算 -
2 代入 =

ご覧の通り、 “単項演算子プラス” は優先順位 15 で、 “加算” の 12 よりも大きいです(二項演算子プラス)。なので、式 "+apples + +oranges" において、単項演算子プラスは最初に動作し、次に加算が実行されます。

代入

代入 = もまた演算子であることに注意しましょう。 2というとても低い値として優先順位の一覧に並んでいます。

なので x = 2 * 2 + 1 のように変数に代入するとき、計算が最初に行われ、その後 = が評価され、 x に結果が格納されます。

let x = 2 * 2 + 1;

alert( x ); // 5

代入 = は値を返します

= が演算子であり、“魔法の” 言語構造でないという事実は、興味深い意味合いを持っています。

JavaScript のすべての演算子は値を返却します。これは +- では明らかですが、= の場合にも当てはまります。

x = value の呼び出しでは、valuex に書き込み、その値を返却します。

これは、複雑な式の一部に代入を使用した例です:

let a = 1;
let b = 2;

let c = 3 - (a = b + 1);

alert( a ); // 3
alert( c ); // 0

上記の例では、式 (a = b + 1) の結果は a に代入された値(つまり 3)です。その後、以降の評価で利用されています。

面白いですよね? JavaScript ライブラリで時々目にするので、これがどのように動くのかは理解しておく必要があります。

ですが、このようなコードは書かないでください。このようなトリッキーな書き方がコードの動きを明確にしたり、可読性をあげることはありません。

代入のチェーン

もう1つの興味深い特徴は、代入をチェーンする機能です:

let a, b, c;

a = b = c = 2 + 2;

alert( a ); // 4
alert( b ); // 4
alert( c ); // 4

チェーンされた代入は右から左へ評価されます。最初に最も右の式 2 + 2 が評価され、次に左の変数に代入されます。: c, baです。最後にすべての変数は単一の値になります。

改めて言いますが、可読性を上げるためには、このようなコードを複数行に分割する方がよいです:

c = 2 + 2;
b = c;
a = c;

これは読みやすいですね。コードを素早く眺めているときには特に。

インプレース(in-place)修正

変数に演算子を適用したあと、新しい結果を同じ変数に格納したいことは頻繁にあります。

例:

let n = 2;
n = n + 5;
n = n * 2;

この表記は演算子 +=*= を使用して短縮することができます:

let n = 2;
n += 5; // n = 7 (n = n + 5 と同じ)
n *= 2; // n = 14 (n = n * 2 と同じ)

alert( n ); // 14

短縮の “変更と代入” 演算子はすべての算術演算とビット演算子に存在します: /=, -=

このような演算子は通常の代入と同じ優先順位になります。なので、他のほとんどの計算の後に実行されます:

let n = 2;

n *= 3 + 5;

alert( n ); // 16  (最初に右辺が評価されるので n *= 8 と同じです)

インクリメント/デクリメント

数値を1ずつ増減する操作は、最も一般的な数値演算の1つです。

なので、そのための特別な演算子があります:

  • インクリメント ++ 変数を1増加させる:

    let counter = 2;
    counter++;      // counter = counter + 1 と同じですがより短いです
    alert( counter ); // 3
  • デクリメント -- 変数を1減少させる:

    let counter = 2;
    counter--;      // counter = counter - 1 と同じですがより短いです
    alert( counter ); // 1
重要:

インクリメント/デクリメントは変数に対してのみ適用可能です。 それを 5++ のように値に対して使おうとするとエラーになります。

演算子 ++-- は変数の前後両方に配置することができます。

  • 演算子が変数の後にある場合、それは “後置式” と呼ばれます: counter++
  • “前置式” は演算子が変数の前に来るときです: ++counter

結果はどちらも同じです: counter1 増加します。

それらに違いはあるでしょうか?はい、その違いは ++/-- の戻り値を使う場合にだけ現れます。

違いを明確にしましょう。ご存知の通り、すべての演算子は値を返します。インクリメント/デクリメントも例外ではありません。前置式は新しい値を返す一方、後置式は古い値を返します(インクリメント/デクリメントの前)。

違いを例で見てみましょう。

let counter = 1;
let a = ++counter; // (*)

alert(a); // 2

ここで (*) の行の前置呼び出し ++countercounter を増加させ、2 という新しい値を返します。そのため、 alert2 を表示します。

後置式を使いましょう:

let counter = 1;
let a = counter++; // (*) ++counter を counter++ に変更

alert(a); // 1

(*) の行で、 後置counter++counter を増加させますが、 古い 値を返します(増加する前)。そのため、 alert1 を表示します。

要約すると:

  • インクリメント/デクリメントの結果を使わない場合、どちらの形式を使っても違いはありません。:

    let counter = 0;
    counter++;
    ++counter;
    alert( counter ); // 2, 上の行は同じことをします
  • 値の増加に 加えて、すぐに演算子の結果を使いたい場合は前置式が必要になります:

    let counter = 0;
    alert( ++counter ); // 1
  • 増加させるが、以前の値を使いたい場合は後置式が必要です:

    let counter = 0;
    alert( counter++ ); // 0
他の演算子の中でのインクリメント/デクリメント

演算子 ++/-- は同様に式の中でも使うことができます。それらの優先順位は他の算術演算子よりも高いです。

例:

let counter = 1;
alert( 2 * ++counter ); // 4

比較:

let counter = 1;
alert( 2 * counter++ ); // 2, counter++ は "古い" 値を返すからです

技術的には問題ありませんが、このような記法は一般的にコードの可読性を下げます。1行で複数のことを行う – よいことではありません。

コードを読むとき、上から読んでいく “縦の” 目視はこのような counter++ を見逃しやすく、また変数の増加が明白ではありません。

“1行は1アクション” のスタイルが推奨されます:

let counter = 1;
alert( 2 * counter );
counter++;

ビット演算子

ビット演算子は引数を 32ビットの整数値として扱い、それらのバイナリ表現のレベルで処理します。

これらの演算子はJavaScript固有のものではありません。多くのプログラミング言語でサポートされています。

演算子のリスト:

  • AND ( & )
  • OR ( | )
  • XOR ( ^ )
  • NOT ( ~ )
  • LEFT SHIFT ( << )
  • RIGHT SHIFT ( >> )
  • ZERO-FILL RIGHT SHIFT ( >>> )

これらの演算子はめったに使われません。それらを理解するためには、低レベルの数値表現について掘り下げるべきであり、それは現時点では最適ではないでしょう。すぐには必要ないからです。もし興味がある場合は、MDNのビット演算子 の記事を参照してください。実際に必要になったときにそれをするのが現実的でしょう。

カンマ

カンマ演算子 ',' は最もレアで普通ではない演算子の1つです。より短いコード書くために使われることがあるので、何が起こっているのか理解するために知っておく必要があります。

カンマ演算子を使うと複数の式を評価できます。それらの式はカンマ ',' で区切られています。それぞれが評価されますが、最後の結果のみが返却されます。

例:

let a = (1 + 2, 3 + 4);

alert( a ); // 7 (3 + 4 の結果)

ここで、最初の式 1 + 2 は評価され、その結果はどこかへ捨てられます。次に 3 + 4 が評価され、結果として返却されます。

カンマはとても優先順が低いです

カンマ演算子はとても優先順位が低いことに注意してください。 = よりも低いため、上の例では丸括弧が重要です。

それらがない場合: a = 1 + 2, 3 + 4+ を最初に評価し、数値を a = 3, 7 に加算します。次に代入演算子 =a = 3 を割り当てます。そして、カンマのあとの 7 は処理されず、無視されます。

なぜこのような、最後の部分を除いてすべてを捨てる演算子が必要なのでしょうか?

より複雑な構造において、1行で複数のアクションを書くときに使用される場合があります。

例:

// 1行に3つの演算子
for (a = 1, b = 3, c = a * b; a < 10; a++) {
 ...
}

このようなトリックは多くのJavaScriptフレームワークで利用されているため、ここで言及しています。しかし通常それらはコードの可読性を下げます。なので、そのように書く前によく考えるべきです。

タスク

重要性: 5

下のコードが実行されたあと、変数 a, b, c, d はいくつになるでしょう?

let a = 1, b = 1;

let c = ++a; // ?
let d = b++; // ?

答えは次の通りです:

  • a = 2
  • b = 2
  • c = 2
  • d = 1
let a = 1, b = 1;

alert( ++a ); // 2, 前置式は新しい値を返します
alert( b++ ); // 1, 後置式は古い値を返します

alert( a ); // 2, 1回インクリメントされています
alert( b ); // 2, 1回インクリメントされています
重要性: 3

下のコードが実行されたあと、ax はいくつになるでしょう?

let a = 2;

let x = 1 + (a *= 2);

答えは次の通りです:

  • a = 4 (2で掛けられています)
  • x = 5 (1 + 4 と計算されます)
重要性: 5

これらの式の結果はどうなるでしょう?

"" + 1 + 0
"" - 1 + 0
true + false
6 / "3"
"2" * "3"
4 + 5 + "px"
"$" + 4 + 5
"4" - 2
"4px" - 2
7 / 0
"  -9  " + 5
"  -9  " - 5
null + 1
undefined + 1
" \t \n" - 2

よく考え、書き留めてから答えあわせしてみてください。

"" + 1 + 0 = "10" // (1)
"" - 1 + 0 = -1 // (2)
true + false = 1
6 / "3" = 2
"2" * "3" = 6
4 + 5 + "px" = "9px"
"$" + 4 + 5 = "$45"
"4" - 2 = 2
"4px" - 2 = NaN
7 / 0 = Infinity
" -9  " + 5 = " -9  5" // (3)
" -9  " - 5 = -14 // (4)
null + 1 = 1 // (5)
undefined + 1 = NaN // (6)
" \t \n" - 2 = -2 // (7)
  1. 文字列の追加 "" + 1 では 1 を文字列に変換します: "" + 1 = "1", そして "1" + 0 には同じルールが適用されます。
  2. 減算 - (ほとんどの算術演算子と同様)は数値でのみ動作し、空の文字列 ""0 に変換します
  3. 文字列の追加は、数値 5 を文字列に追加します。
  4. 減算は常に数値に変換します。そのため、" -9 " は数値 -9 になります(前後のスペースは無視されます)。
  5. null は数値変換後は 0 になります。
  6. undefined は数値変換後は NaN になります。
  7. 文字列の先頭/末尾のスペースは、文字列が数値に変換される際に削除されます。ここでは文字列全体が \t\n、“通常” のスペース文字から構成されています。したがって、空文字列のときと同様、0 になります。
重要性: 5

ユーザに2つの数字を訪ね、その合計を表示するコードがあります。

これは正しく機能していません。以下の例の出力は 12 です(デフォルトのプロンプトの値の場合)。

なぜでしょうか?修正してください。結果は 3 になるべきです。

let a = prompt("First number?", 1);
let b = prompt("Second number?", 2);

alert(a + b); // 12

理由はプロンプトがユーザ入力を文字列として返すからです。

なので、変数はそれぞれ値 "1""2" になります。

let a = "1"; // prompt("First number?", 1);
let b = "2"; // prompt("Second number?", 2);

alert(a + b); // 12

すべきことは、+ の前に、文字列から数値へ変換することです。例えば、Number() を使用したり、それらの前に + をつけます。

例えば、。prompt の直前:

let a = +prompt("First number?", 1);
let b = +prompt("Second number?", 2);

alert(a + b); // 3

あるいは alert:

let a = prompt("First number?", 1);
let b = prompt("Second number?", 2);

alert(+a + +b); // 3

最新のコードでは、単項と二項の + 両方を使用しています。面白いですね。

チュートリアルマップ