JavaScript の使ったことない演算子を練習する

JavaScript、ECMAScript はちょくちょく新たな演算子が増えている。また、コレまで存在していたものの、個人的に使っていない・使いこなせていない演算子もあるので、今回はそれらを雑多にまとめてみようと思う。全部を扱うのではなく、「使いこなせるようになったら良さそうだな」と個人的に思ったモノをチョイスしている。

以下に検証用コードを書き連ねる。

void function bitwiseNot() {
  console.log('\n~ : Bitwise Not : ビット否定');
  console.log({ '~-2': ~-2, '~-1': ~-1, '~0': ~0, '~1': ~1, '~2': ~2 });
  console.log(`['A', 'B', 'C'].indexOf('D') : `, ['A', 'B', 'C'].indexOf('D'));
  console.log(`~['A', 'B', 'C'].indexOf('D') … 0 = Falsy : `, ~['A', 'B', 'C'].indexOf('D'));
  console.log(`if(~['A', 'B', 'C'].indexOf('D')) … Not Includes`);
}()

void function exponentiation() {
  console.log('\n** : Exponentiation : べき乗');
  console.log('2の3乗 = 2 * 2 * 2 = 8 : 2 ** 3 : ', 2 ** 3);
}()

void function nullishCoalescing() {
  console.log('\n?? : Nullish Coalescing : Null 合体');
  console.log('- || : Logical OR : Evaluate Falsy Values');
  console.log(`  - undefined || 'Default' : `, undefined || 'Default');
  console.log(`  - null      || 'Default' : `, null      || 'Default');
  console.log(`  - 0         || 'Default' : `, 0         || 'Default');
  console.log(`  - ''        || 'Default' : `, ''        || 'Default');
  console.log(`  - NaN       || 'Default' : `, NaN       || 'Default');
  console.log('- ?? : Nullish Coalescing : Evaluate Nullish Values Only');
  console.log(`  - undefined ?? 'Default' : `, undefined ?? 'Default');
  console.log(`  - null      ?? 'Default' : `, null      ?? 'Default');
  console.log(`  - 0         ?? 'Default' : `, 0         ?? 'Default', '(Number 0 Is Valid)');
  console.log(`  - ''        ?? 'Default' : `, ''        ?? 'Default', '(Empty String Is Valid)');
  console.log(`  - NaN       ?? 'Default' : `, NaN       ?? 'Default', '(NaN Is Valid)');
  console.log('- `??` Equals `foo === undefined && foo === null` And `foo == null`');
  console.log(`  - undefined == null ? 'Default' : undefined : `, undefined == null ? 'Default' : undefined);
  console.log(`  - null      == null ? 'Default' : null      : `, null      == null ? 'Default' : null     );
  console.log(`  - 0         == null ? 'Default' : 0         : `, 0         == null ? 'Default' : 0        , '(Number 0 Is Valid)');
  console.log(`  - ''        == null ? 'Default' : ''        : `, ''        == null ? 'Default' : ''       , '(Empty String Is Valid)');
  console.log(`  - NaN       == null ? 'Default' : NaN       : `, NaN       == null ? 'Default' : NaN      , '(NaN Is Valid)');
}()

void function logicalNullishAssignment() {
  console.log('\n??= : Logical Nullish Assignment : Null 合体代入');
  console.log('  (Node.js Is Not Available Yet)');
  const hoge = { foo: 10 };
  console.log('const hoge = { foo: 10 }; : ', hoge);
  //hoge.foo ??= 20;  // Unexpected token '??='
  console.log('hoge.foo ??= 20;          :  { foo: 10 }');
  //hoge.bar ??= 30;
  console.log('hoge.bar ??= 30;          :  { foo: 10, bar: 30 }');
}()

void function optionalChaining() {
  console.log('\n?. : Optional Chaining : オプショナル・チェイニング');
  const first = {
    second: {
      third: 'Third Value',
    },
    array: [1, 2, 3],
    func: (text) => {
      return { result: text };
    }
  };
  console.log(`const first = `, first);
  console.log(`first?.second                : `, first?.second               );
  console.log(`first?.secondNotExist        : `, first?.secondNotExist       );
  console.log(`first?.['second']            : `, first?.['second']           );
  console.log(`first?.['secondNotExist']    : `, first?.['secondNotExist']   );
  console.log(`first?.second.third          : `, first?.second.third         );
  console.log(`first?.second.thirdNotExist  : `, first?.second.thirdNotExist );
  console.log(`first.second?.third          : `, first.second?.third         );
  console.log(`first.secondNotExist?.third  : `, first.secondNotExist?.third );
  console.log(`first?.second?.third         : `, first?.second?.third        );
  console.log(`first?.secondNotExist?.third : `, first?.secondNotExist?.third);
  console.log(`first.array?.[0]             : `, first.array?.[0]            );
  console.log(`first.arrayNotExist?.[0]     : `, first.arrayNotExist?.[0]    );
  console.log(`first.func?.('Func')         : `, first.func?.('Func')        );
  console.log(`first.funcNotExist?.('Func') : `, first.funcNotExist?.('Func'));
}()

void function commaOperator() {
  console.log('\n, : Comma Operator : カンマ演算子');  // 最後 (右端) の式の値が返る
  let x = 1;
  console.log('let x = 1;     : ', x);
  x = (x++, 10);
  console.log('x = (x++, 10); : ', x);
  let y, z;
  console.log(`if(x === 10) y = 3, console.log('  true'), z = 6; else y = 4, console.log('  false'), z = 8;`);
  if(x === 10) y = 3, console.log('  true'), z = 6; else y = 4, console.log('  false'), z = 8;
  console.log(`  y = ${y} , z = ${z}`);
  console.log(`if(x !== 10) y = 3, console.log('  true'), z = 6; else y = 4, console.log('  false'), z = 8;`);
  if(x !== 10) y = 3, console.log('  true'), z = 6; else y = 4, console.log('  false'), z = 8;
  console.log(`  y = ${y} , z = ${z}`);
}()

目次

~ : Bitwise Not : ビット否定

ビット否定演算子 ~ は、よく indexOf() と組み合わせて、配列内に要素が含まれているかどうかをチェックするのに使っていた。現在は includes() 関数があるので使うことは減ったが、仕組みを理解しておく。

「ビット」の概念は JS の中でなかなか使うことがないので今回は説明を省略する。どうして includes() 的な使い方ができるかのまとめだけおさえておく。

はじめに、ビット否定を付けた値がどう解決されるかをいくつか並べてみる。

ココで、-1 のビット否定 ~-10 になることを押さえておく。

続いて、配列と indexOf() の動きを確認する。

const includesA   = ['A', 'B', 'C'].indexOf('A');  // → 0
const includesB   = ['A', 'B', 'C'].indexOf('B');  // → 1
const includesC   = ['A', 'B', 'C'].indexOf('C');  // → 2
const notIncludes = ['A', 'B', 'C'].indexOf('D');  // → -1

配列に含まれていない値の場合は -1 が返る。

それでは、これらの値をビット否定してみる。

const includesA   = ~['A', 'B', 'C'].indexOf('A');  // → -1
const includesB   = ~['A', 'B', 'C'].indexOf('B');  // → -2
const includesC   = ~['A', 'B', 'C'].indexOf('C');  // → -3
const notIncludes = ~['A', 'B', 'C'].indexOf('D');  // → 0 (Falsy)

配列に含まれていない値が、Falsy な 0 に解決された。

ということで、次のように if 文に組み合わせて扱える。

const array = ['A', 'B', 'C'];

if(~array.indexOf('A')) {  // = array.includes('A')
  console.log('配列に A が含まれます');
}

if(!~array.indexOf('D')) {  // = !array.includes('D')
  console.log('配列に D が含まれません');
}

!~ と、ビット否定の結果を反転してやると、

  1. indexOf() の結果が -1 (配列内に含まれない)
  2. -1 がビット否定されて 0 になる
  3. 0 は Falsy な値なので Not 演算子 ! で反転させると true になる

ということで、「配列に○○が含まれない場合」という if 文が書ける。

** : Exponentiation : べき乗

「2の3乗」みたいな、べき乗も演算子で書ける。ES2016 より追加されたらしい。

割った余りを求める「剰余演算子」% もそうだが、べき乗演算子も、演算結果を代入する代入演算子が存在する。

let foo = 2;
foo **= 3;  // foo = 2 ** 3;
console.log(foo);  // → 8

let bar = 5;
bar %= 2;  // bar = 5 % 2
console.log(bar);  // → 1

?? : Nullish Coalescing : Null 合体

コレまで、Or 演算子 || を用いて変数が空の場合にデフォルト値を代入するようなテクニックは存在したが、Or 演算子は Falsy な値 を全て評価してしまうので、0'' (空文字) なども無効な値とされてしまい、場合によっては不都合があった。

コレを回避しようとして厳密に判定しようとすると、次のように書く必要があった。

// undefined・null との厳密等価比較を行う
let foo = (foo === undefined && foo === null) ? 'Default Value' : foo;

// もしくは null との曖昧等価比較で同義
let foo = (foo == null) ? 'Default Value' : foo;

ES2020 になって追加された Null 合体演算子 ?? は、コレを簡単に書けるようになる。

それぞれの演算子での結果を見てみよう。

// || : Logical Or : Evaluate Falsy Values
foo = undefined || 'Default';  // Default
foo = null      || 'Default';  // Default
foo = 0         || 'Default';  // Default
foo = ''        || 'Default';  // Default
foo = NaN       || 'Default';  // Default

// ?? : Nullish Coalescing : Evaluate Nullish Values Only
foo = undefined ?? 'Default';  // Default
foo = null      ?? 'Default';  // Default
foo = 0         ?? 'Default';  // 0
foo = ''        ?? 'Default';  // '' (空文字)
foo = NaN       ?? 'Default';  // NaN

// `== null` も念のため確認 (`foo === undefined && foo === null` と同じ)
foo = undefined == null ? 'Default' : undefined;  // Default
foo = null      == null ? 'Default' : null     ;  // Default
foo = 0         == null ? 'Default' : 0        ;  // 0
foo = ''        == null ? 'Default' : ''       ;  // '' (空文字)
foo = NaN       == null ? 'Default' : NaN      ;  // NaN

??= : Logical Nullish Assignment : Null 合体代入

Node.js v14.16.1 時点ではまだ対応していない (Unexpected token 扱いになる) のだが、Null 合体代入 ??= という代入演算子もある。Chrome v90 では動作した。

const hoge = { foo: 10 };

hoge.foo ??= 20;  // hoge.foo の値は 10 のまま

hoge.bar ??= 30;  // hoge.bar に 30 が代入される
// ↑ は以下と同義
if(hoge.bar == null) {
  hoge.bar = 30;
}

?. : Optional Chaining : オプショナル・チェイニング

ネストしたプロパティにアクセスする際、その存在チェックをしようと思うと、面倒臭いことになる。

const first = {
  second: {
    third: 'Third Value',
  },
  array: [1, 2, 3],
  func: (text) => {
    return { result: text };
  }
};

// 存在チェックした上で表示する
if(first && first.second && first.second.third) {
  console.log(first.second.third);
}

こうした if 文は面倒なので、ES2020 から追加された、オプショナル・チェイニングを使って簡略化する。存在しないプロパティにあたった場合は、その配下へのチェーンが記述されていても undefined と解釈されて、エラーにならずに済む。

console.log( first?.second                );  // { third: 'Third Value' }
console.log( first?.secondNotExist        );  // undefined (エラーにならない)
console.log( first?.['second']            );  // { third: 'Third Value' }
console.log( first?.['secondNotExist']    );  // undefined (エラーにならない)
console.log( first?.second.third          );  // Third Value
console.log( first?.second.thirdNotExist  );  // undefined (エラーにならない)
console.log( first.second?.third          );  // Third Value
console.log( first.secondNotExist?.third  );  // undefined (エラーにならない)
console.log( first?.second?.third         );  // Third Value
console.log( first?.secondNotExist?.third );  // undefined (エラーにならない)
console.log( first.array?.[0]             );  // 1
console.log( first.arrayNotExist?.[0]     );  // undefined (エラーにならない)
console.log( first.func?.('Func')         );  // { result: 'Func' }
console.log( first.funcNotExist?.('Func') );  // undefined (エラーにならない)

配列や、オブジェクトにブラケット記法でアクセスする際は first.array?.[0]first?.['second'] のように ?.[] と書く。

また、関数を呼び出す場合は first?.func() ないしは first.func?.() と書く。?.() で書けることを押さえておこう。

ネストされたプロパティの途中からオプショナル・チェイニングを書くこともできるが、大抵はルート直下のプロパティに書いておけば一番安全だろう。

, : Comma Operator : カンマ演算子

カンマ演算子 , は、左から順に式が評価され、最後の式の評価結果が返される。一般的に使うことは少ないが、コードゴルフにおいてセミコロン ; の代わりに使用し、if 文のブレース {} を省略したりすることがある。

let x = 1;
x = (x++, 10);  // → 10

let y = 0;
let z = 0;
if(x === 10) y = 3, console.log('true'), z = 6; else y = 4, console.log('false'), z = 8;
// → true がコンソール出力される
// → y = 3 , z = 6

if(x !== 10) y = 3, console.log('true'), z = 6; else y = 4, console.log('false'), z = 8;
// → false がコンソール出力される
// → y = 4 , z = 8

以上

Null 合体とオプショナル・チェイニングはこれから覚えておきたい演算子だ。カンマ演算子についても、挙動を理解しておくとコード読解に役立ちそう。