演算子

演算子はよく利用する計算を関数やメソッドではなく、記号として表現したものです。 たとえば、足し算を行う + も演算子の一種で、演算子には多くの種類があります。

演算子は記号単独ではなく、演算の対象が必要になります。 演算子の対象のことを被演算子(オペランド)と呼びます。

次の1 + 2における+演算子をみてみます。

1 + 2;

+演算子は、次のような1つの演算子に対して、2つのオペランドを前後に置いています。 このような2つのオペランドを取る演算子を二項演算子と呼びます。

オペランド1 演算子 オペランド2

また、1つの演算子に対して1つのオペランドをとるものもあります。 たとえば、次のような数値をインクリメントする++演算子は、前後どちらかにオペランドを置きます。

let num = 1;
num++;
// または
++num;

このような1つのオペランドを取る演算子を単項演算子と呼びます。 単項演算子と二項演算子で同じ記号を使うことがあり、そのために呼び方を変えています。

多くのプログラミング言語では同じような演算子を持っています。 この節は、分からない記号が出てきたら確認する時に読むとよいでしょう。

しかし、JavaScriptでは比較演算子において、暗黙的な型変換の問題があります。 そのため、演算子をひととおり見た後に、暗黙的な型変換と明示的な型変換について学んでいきます。

二項演算子

四則演算など基本的な二項演算子を見ていきます。

プラス演算子(+

2つの数値を加算する演算子です。

1 + 1; // => 2

マイナス演算子(-

2つの数値を減算する演算子です。

1 - 1; // => 0

乗算演算子(*

2つの数値を乗算する演算子です。

2 * 8; // => 16

除算演算子(/

2つの数値を除算する演算子です。

8 / 2; // => 4

剰余演算子(%

2つの数値のあまりを求める演算子です。

8 % 3; // => 2

[ES2016] べき乗演算子(**

2つの数値のべき乗を求める演算子です。 左オペランドを右オペランドでべき乗した値を返します。

// べき乗演算子(ES2016)で2の4乗を計算
2 ** 4; // => 16

べき乗演算子は同じ動作をするMath.powメソッドも存在しています。

Math.pow(2, 4); // => 16

べき乗演算子はES2016で後から追加された演算子であるため、関数と演算子それぞれ存在しています。 他の二項演算子は演算子が先に存在していたため、Mathには関数がありません。

単項演算子(算術)

単項演算子は、1つのオペランド受け取り処理する演算子です。

単項プラス演算子(+

単項演算子の+はオペランドを数値に変換します。

明示的に+数値のように数値に対して、単行プラス演算子を付けるケースはほぼ無いでしょう。

+1; // => 1

また、単項プラス演算子は、数値以外も数値へと変換します。

次の例では、数字を数値へ変換しています。 一方、数値に変換できない文字列などはNaNという特殊な値へと変換されます。

+"1"; // => 1
+"文字列"; // => NaN

NaNは"Not-a-Number"の略称で、数値ではないがNumber型の値を表現しています。 NaNはどの値とも(NaN自身に対しても)一致しない特性があり、Number.isNaNメソッドを使うことでNaNの判定を行えます。

// 自分自身とも一致しない
console.log(NaN === NaN); // => false
// Number型である
console.log(typeof NaN); // => "number"
// Number.isNaNでNaNかどうかを判定
console.log(Number.isNaN(NaN)); // => true

しかし、単項演算子の+は文字列から数値への変換に使うべきではありません。 Numberコンストラクタ関数やparseInt関数などの明示的な変換方法が存在するためですが、詳細は暗黙的な型変換の章で解説します。

単項マイナス演算子(-

単項マイナス演算子はマイナスの数値を記述する場合に利用します。

たとえば、マイナスの1という数値を -1 と書くことができるのは、単項マイナス演算子を利用しているからです。

-1; // => -1

また、単項マイナス演算子はマイナスの数値を反転できます。 そのため、"マイナスのマイナスの数値"はプラスの数値となります。

-(-1); // => 1

単項マイナス演算子も文字列などを数値へ変換します。 単項プラス演算子と同様で、単項マイナス演算子で数値への変換を行うべきではありません。

-"1"; // => -1
-"文字列"; // => NaN

インクリメント演算子(++

インクリメント演算子(++)は、オペランドの数値を+1する演算子です。 オペランドの前後どちらかにインクリメント演算子をおくことで、オペランドに対して値を+1します。

let num = 1;
num++;
// 次のようにした場合と結果は同じ
// num = num + 1;

オペランドの前後のどちらかにインクリメント演算子を起きますが、それぞれで評価の順番が異なります。

後置インクリメント演算子(num++)は、次のような順で処理が行われます。 num++が返す値は+1する前の値となります。

  1. numが評価
  2. numに対して+1する

一方、前置インクリメント演算子(++num)は、次のような順で処理が行われます。 そのため、++numが返す値は+1した後の値となります。

let x = 1;
x++; // => 1
console.log(x); // => 2
let y = 1;
++y; // => 2
console.log(y); // => 2

デクリメント演算子(--

デクリメント演算子(--)は、オペランドの数値を-1する演算子です。

let num = 1;
num--;
// 次のようにした場合と結果は同じ
// num = num - 1;

デクリメント演算子は、インクリメント演算子と同様に、オペランドの前後のどちらかに置くことができます。

let x = 1;
x--; // => 1
console.log(x); // => 0
let y = 1;
--y; // => 0
console.log(y); // => 0

比較演算子

比較演算子はオペランド同士の値を比較し、真偽値を返す演算子です。

厳密等価演算子(===

厳密等価演算子は、左右の2つのオペランドを比較します。 同じ型で同じ値である場合に、trueを返します。

1 === 1; // => true
1 === "1"; // => false

また、オペランドがどちらもオブジェクトである時は、 オブジェクトの参照が同じである場合に、trueを返します。

// {} は新しいオブジェクトを作成している
const objA = {}, objB = {};
objA === objB; // => false
// 同じ参照を比較している場合
objA === objA; // => true

厳密不等価演算子(!==

厳密不等価演算子は、左右の2つのオペランドを比較します。 異なる型または異なる値である場合に、trueを返します。

1 !== 1; // => false
1 !== "1"; // => true

等価演算子(==

等価演算子(==)は、2つのオペランドを比較します。 同じ型のオペランドを比較する場合は、厳密等価演算子(===)と同じ処理を行います。

1 == 1; // => true
"str" == "str"; // => true
"JavaScript" == "ECMAScript"; // => false
// オブジェクトは参照が一致しているならtrueを返す
// {} は新しいオブジェクトを作成している
const objA = {}, objB = {};
objA == objB; // => false
objA == objA; // => true

しかし、等価演算子(==)はオペランド同士が異なる型の値であった場合に、 同じ型となるように暗黙的な型変換してから比較を行います。

そのため、次のような見た目からは、結果を予測できない挙動が多く存在します。

// 文字列を数値に変換してから比較
1 == "1"; // => true
// "01"を数値にすると`1`となる
1 == "01"; // => true
// 真偽値を数値に変換してから比較
0 == false; // => true
// nullの比較はfalseを返す
0 == null; // => false
// nullとundefeinedの比較は常にtrueを返す
null == undefined; // => true

意図しない挙動となることがあるため、暗黙的な型変換が行われる等価演算子(==)を使うべきではありません。 代わりに、厳密等価演算子(===)を使い、異なる型を比較したい場合は明示的に型を合わせるべきです。

等価演算子(==)において許容される例外として、nullundefinedの比較があります。 次のように、比較したいオペランドが null または undefined であることを判定したい場合に、 厳密等価演算子では2度比較する必要があります。

const value = getValue();
if (value == null || value === undefined) {
    console.log("valueがnullまたはundefinedである場合の処理");
}
// == で書いた場合
if (value == null) {
    console.log("valueがnullまたはundefinedである場合の処理");
}

しかし、このようなケースにおいても仕組みを理解するまでは、 常に厳密等価演算子(===)を利用することを推奨します。

不等価演算子(!=

不等価演算子(!=)は、2つのオペランドを比較し、等しくないならtrueを返します。

1 != 1; // => false
"str" != "str"; // => false
"JavaScript" != "ECMAScript"; // => true
true != true;// => false
// オブジェクトは参照が一致していないならtrueを返す
const objA = {}, objB = {};
objA != objB; // => true
objA != objA; // => false

不等価演算子も、等価演算子(==)と同様に異なる型のオペランドを比較する際に、暗黙的な型変換を行ってから比較します。

1 != "1"; // => false
0 != false; // => false
0 != null; // => true
null  == undefined; // => true

そのため、不等価演算子(!=)は、利用するべきではありません。 代わりに暗黙的な型変換を行わない厳密不等価演算子(!==)を利用します。

大なり演算子/より大きい(>

大なり演算子は、左オペランドが右オペランドより大きいならば、trueを返します。

42 > 21; // => true
42 > 42; // => false

大なりイコール演算子/以上(>=

大なりイコール演算子は、左オペランドが右オペランドより大きいまたは等しいならば、trueを返します。

42 >= 42; // => true
42 > 42 || 42 === 42; // => true

小なり演算子/より小さい(<

小なり演算子は、左オペランドが右オペランドより小さいならば、trueを返します。

21 < 42;// => true
42 < 42; // => false

小なりイコール演算子/以下(<=

小なりイコール演算子は、左オペランドが右オペランドより小さいまたは等しいならば、trueを返します。

42 <= 42; // => true
42 < 42 || 42 === 42;// => true

ビット演算子

JavaScriptでは、数値は内部的にIEEE 754方式の浮動小数点数として表現されています。 ( データ型とリテラルを参照)

ビット演算子はオペランドを符号付き32bit整数に変換してから演算します。 ビット演算子による演算結果は10進数の数値を返します。

たとえば、9という数値は符号付き32bit整数では次のように表現されます。

0b0000000000000000000000000001001; // => 9
// Number#toStringメソッドを使うことで2進数表記の文字列を取得できる
(9).toString(2); // => "1001"

また、-9という数値は、ビッグエンディアンの2の補数形式で表現されるため、次のようになります。

0b11111111111111111111111111110111; // => 4294967287
// ゼロ桁埋め右シフトをしてからNumber#toStringで2進数表記を取得できる
(-9 >>> 0).toString(2); // => "11111111111111111111111111110111"

ビット論理積(&

論理積演算子(&)はビットごとのAND演算した結果を返します。

15     & 9;      // => 9
0b1111 & 0b1001; // => 0b1001

ビット論理和(|

論理和演算子(|)はビットごとのOR演算した結果を返します。

15     | 9;      // => 15
0b1111 | 0b1001; // => 0b1111

ビット排他的論理和(^

排他的論理和演算子(^)はビットごとのXOR演算した結果を返します。

15     ^ 9;      // => 6
0b1111 ^ 0b1001; // => 0b0110

ビット否定(~

単項演算子の否定演算子(~)はオペランドを反転した値を返します。 これは1の補数と知られている値と同じものです。

~15; // => -16
~0b1111; // => -0b10000

否定演算子(~)はビット演算以外でも使われていることがあります。

JavaScriptのString#indexOf(string)は、文字列中にあるstringの位置を見つけて返すメソッドです。 このindexOfメソッドは、検索対象が見つからない場合に、-1を返します。

const string = "森森本森森";
// 見つかった場合はindex値を返す
string.indexOf("本"); // => 2
// 見つからない場合は-1を返す
string.indexOf("火"); // => -1

否定演算子(~)は1の補数を返すため、~(-1)0となります。

~0; // => -1
~(-1); // => 0

JavaScriptでは0もif文ではfalseとして扱われます。 そのため、~indexOfの結果0となることを利用して書くイディオムが一部では使われています。

const string = "森森本森森";
if (string.indexOf("火") === -1) {
    // 見つからなかった場合の処理
}
// 否定演算子(`~`)で類似表現
if (~string.indexOf("火")) {
    // 見つからなかった場合の処理
}

このイディオムは文字列を検索した結果を真偽値で取得できれば不要となるケースが殆どです。 ES2015以降ではString#includesで真偽値を取得できるため、分かりにくいだけのイディオムとなりつつあります。

const string = "森森本森森";
// `String#includes`は"火"があるならtrueを返す
if (!string.includes("火")) {
    // 見つからなかった場合の処理
}

左シフト演算子(<<

左シフト演算子は、numberbitの数だけ左へシフトします。 左にあふれたビットは破棄され、0のビットを右から詰めます。

number << bit;

次のコードでは、9を2ビット分だけ左へシフトしています。

     9 << 2; // => 36
0b1111 << 2; // => 0b111100

右シフト演算子(>>

右シフト演算子は、numberbitの数だけ右へシフトします。 右にあふれたビットは破棄され、左端のビットのコピーを左から詰めます。

number >> bit;

次のコードでは、-9を2ビット分だけ右へシフトしています。 左端のビットのコピーを使うため、常に符号は維持されます。

(-9) >> 2; // => -3
// 0b11111111111111111111111111110111 >> 2; // => 0b11111111111111111111111111111101

ゼロ埋め右シフト演算子(>>>

ゼロ埋め右シフト演算子は、numberbitの数だけ右へシフトするのは右シフト演算子(>>)と同じです。 右にあふれたビットは破棄され、0のビットを左から詰めます。

次のコードでは、-9を2ビット分だけゼロ埋め右シフトしています。 左端のビットは0となるため、常に正の値となります。

(-9) >>> 2; // => 1073741821
// 0b11111111111111111111111111110111 >>> 2; // => 0b00111111111111111111111111111101

代入演算子(=

代入演算子(=)は変数に対して値を代入します。 詳しくは変数と宣言を参照してください。

const version = 2015;

また、それぞれの二項演算子と代入演算子は組み合わせて利用できます。 +=-=*=/=%=<<=>>=>>>=&=^=|=については、 次のように、演算した結果を代入できます。

let num = 1;
num += 10; // num = num + 10; と同じ
console.log(num); // => 11

[ES2015] 分割代入(Destructuring assignment)

代入演算子は1つの変数に値を代入するものでした。 分割代入を使うことで、配列やオブジェクトの値を複数の変数へ代入できます。

分割代入は、代入演算子(=)を使うのは同じですが、左辺のオペランドが配列リテラルやオブジェクトリテラルとなります。 次のように、右辺の配列の値を、左辺の配列リテラルの対応する変数へ代入します。

const array = [1, 2];
// aには0番目の値、bには1番目の値
// var a = array[0], b = array[1];
const [a, b] = array;
console.log(a); // => 1
console.log(b); // => 2

同様にオブジェクトも分割代入できます。 オブジェクトの場合は、右辺のオブジェクトのプロパティ値を、左辺に対応するプロパティ名へ代入します。

const object = {
    "key": "value"
};
// プロパティ名へ対応する変数へ代入
// var key = object.key;
const { key } = object;
console.log(key); // => "value"

分割代入は、代入演算子のみではなく、関数の仮引数やモジュールでも利用できます。 詳しくは、第n章の分割代入で扱います。

条件(三項)演算子(?:

条件演算子(?:)は三項をとる演算子であるため、三項演算子とも呼ばれます。

条件演算子は条件式を評価した結果がtrueならば、Trueの時処理する式の評価結果を返します。 条件式falseである場合は、Falseの時処理する式の評価結果を返します。

条件式 ? Trueの時処理する式 : Falseの時処理する式;

if文との違いは、条件演算子は式として書くことができるため値を返します。 次のように、条件式の評価結果により"A" または "B" どちらかを返します。

const valueA = true ? "A" : "B";
console.log(value); // => "A";
const valueB = false ? "A" : "B";
console.log(value); // => "B";

条件分岐による値を返せるため、条件によって変数の初期値が違う場合などに使われます。

次の例では、text文字列にprefixとなる文字列を先頭に付ける関数を書いています。 prefixの第二引数を省略したり文字列ではないものが指定された場合に、デフォルトのprefixを使います。 第二引数が省略された場合には、prefixundefinedが入ります。

条件演算子の評価結果は値を返すので、constを使って宣言と同時に代入できます。

function addPrefix(text, prefix) {
    // `prefix`が指定されていない場合は"デフォルト:"を付ける
    const pre = typeof prefix === "string" ? prefix : "デフォルト:";
    return pre + text;
}

addPrefix("文字列"); // => "デフォルト:文字列"
addPrefix("文字列", "カスタム"); // => "カスタム文字列"

if文を使った場合は、宣言と代入を分ける必要があるため、constを使うことができません。

function addPrefix(text, prefix) {
    let pre = "デフォルト:";
    if (typeof prefix === "string") {
        pre = prefix;
    }
    return pre + text;
}

addPrefix("文字列"); // => "デフォルト:文字列"
addPrefix("文字列", "カスタム"); // => "カスタム文字列"

論理演算子

論理演算子は基本的に真偽値を扱う演算子で、AND、OR、NOTを表現できます。

AND演算子(&&

AND演算子(&&)は、左辺の値の評価結果がtrueであるならば、右辺の評価結果を返します。 左辺の評価がtrueではない場合、右辺は評価されません。

このような値が決まった時点でそれ以上評価しないものを短絡評価(ショートサーキット)と呼びます。

const x = true, y = false;
// x -> y の順に評価される
x && y; // => false
// 左辺がfalsyであるなら常にfalse
// xは評価されない
y && x; // => false

AND演算子は、if文とよく組み合わせて利用します。 次のように、valueがString型で かつ 値が"str"である場合という条件をひとつの式として書くことができます。

const value = "str";
if (typeof value === "string" && value === "str") {
    console.log(`${value} is string value`);
}
// if文のネストで書いた場合と結果は同じとなる
if (typeof value === "string") {
    if (value === "str") {
        console.log(`${value} is string value`);
    }
}

このときに、valueがString型でない場合は、その時点でfalseとなります。

短絡評価はif文のネストに比べて短く書くことができます。

しかし、if文が3重4重にネストしているのは不自然なのと同様に、 AND演算子やOR演算子が3つ4つ連続する場合は複雑で読みにくいコードです。 その場合は抽象化ができないかを検討するべきサインとなります。

OR演算子(||

OR演算子(||)は、左辺の値の評価結果がfalseであるならば、右辺の評価結果を返します。 AND演算子(&&)とは逆に、左辺がtrueである場合は、右辺を評価せずtrueを返します。

const x = true, y = false;
// xがtrueなのでyは評価されない
x || y; // => true
// yはfalseなのでxを評価した結果を返す
y || x; // => true

OR演算子は、左辺の評価結果がfalseである場合に、右辺を評価します。 これを言い換えると、左辺の評価結果がfalseである場合に、右辺に定義したデフォルト値を使う。

この特性を理解するためには、評価結果がfalseとなるものはどのようなものがあるかを知る必要があります。

JavaScriptでは、次の値はfalseとして評価されます。 このような値はfalseっぽい値ということで falsy と呼ばれています。

  • false
  • undefined
  • null
  • 0
  • NaN
  • ""(空文字)

これら以外の値は、trueとして評価されます。

このundefinedfalseと評価される特性を利用して、 次のように仮引数のデフォルト値を定義するという書き方ができます。

  • prefixの値がある場合はprefixの値を代入
  • prefixの値が未定義(undefined)である場合は、右辺の値を代入

function addPrefix(text, prefix) {
    const pre = prefix || "デフォルト:";
    return pre + text;
}

addPrefix("文字列"); // => "デフォルト:文字列"
addPrefix("文字列", "カスタム"); // => "カスタム文字列"

しかし、この書き方にはひとつ問題があります。 falsyとして評価される値には、空文字や数値の0が含まれている点が挙げられます。

そのため、次のようにprefixに空文字を指定して、prefixを空文字を期待する場合に、 デフォルト値が使われるという意図しない挙動が発生してしまいます。

function addPrefix(text, prefix) {
    const pre = prefix || "デフォルト:";
    return pre + text;
}

addPrefix("文字列", ""); // => "デフォルト:文字列"

このような意図しない挙動を避けて、デフォルト値を扱うには先ほどの条件演算子を活用する方法などがあります。

しかし、ES2015からは関数の仮引数のデフォルト値を指定できるデフォルト引数が使えます。 そのためデフォルト引数として書ける場合は、次のように書くのがもっともシンプルになります。

function addPrefix(text, prefix = "デフォルト:") {
    return prefix + text;
}

addPrefix("文字列"); // => "デフォルト:文字列"
addPrefix("文字列", "カスタム"); // => "カスタム文字列"

デフォルト引数とは、関数呼び出しの際に指定した引数が省略された時に、引数にデフォルト値を設定する機能です。 これにより、先ほどOR演算子で起きた意図しない挙動を避けることができます。

NOT演算子(!

NOT演算子(!)は、オペランドの評価結果がtrueであるならば、falseを返します。

!false; // => true
!true; // => false

NOT演算子は必ず真偽値を返すため、次のように2つNOT演算子を重ねて真偽値へ変換するという使い方も見かけます。

const string = "";
// 空文字はfalsyな値
!!string; // => false

このようなケースの多くは、比較演算子を使うなどより明示的な方法で、真偽値を得ることができます。 安易に!!による変換に頼るよりは別の方法を探してみるのがいいでしょう。

const string = "";
// 空文字でないことを判定
console.log(string.length > 0); // => false

グループ演算子(()

グループ演算子は複数の二項演算子が組み合わさった場合に、演算子の優先順序を明示できる演算子です。

たとえば、次のようにグループ演算子で囲んだ部分が最初に処理されるため、結果も変化します。

const a = 1;
const b = 2;
const c = 3;
a + b * c; // 7
(a + b) * c; // => 9

演算子の優先順序はECMAScript仕様で定義されていますが、多様な演算子が出てきた場合に見分けるのは難しいです。 グループ演算子はもっとも優先度が高い演算子となります。 そのため、グループ演算子を使い優先順序を明示できます。

次のようなグループを演算子を使わずに書いたコードを見てみましょう。 atrueまたは、bかつctrueであるときに処理されます。

if (a || b && c) {
    // a が true または
    // b かつ c が true
}

ひとつの式に複数の種類の演算子が出てくると読みにくくなる傾向があります。 このような場合にはグループ演算子を使い、結合順を明示して書くようにしましょう。

if (a || (b && c)) {
    // a が true または
    // b かつ c が true
}

文字列結合演算子(+

数値にでてきたプラス演算子(+)は、文字列の結合に利用できます。

プラス演算子は、文字列を結合した文字列を返します。

const value = "文字列" + "結合";
console.log(value); // => "文字列結合"

カンマ演算子(,

カンマ演算子(,)は、カンマ(,)で区切った式を左から順に評価し、 最後の式の評価結果を返します。

次の例では、式1式2式3の順に評価され、式3の評価結果を返します。

式1, 式2, 式3;

これまでに、カンマで区切るという表現は、varによる変数宣言などでも出てきました。 左から順に実行する点ではカンマ演算子の挙動は同じものですが、構文としては似て非なるものです。

const a = 1, b = 2, c = a + b;
console.log(c); // => 3

一般にカンマ演算子を利用する機会は殆どないため、「カンマで区切った式は左から順に評価される」ということだけを知っていれば問題ありません。1

1. カンマ演算子を活用したテクニックとしてindirect callというものがあります。

results matching ""

    No results matching ""