プリミティブ値

JavaScript で基本となる値です。

数値

数字な値(number)は単に数値を記述するだけです。

const one = 1;
const eleven = 11;
const pi = 3.14;

真偽値

truefalseからなる値です。ある条件が正しければtrue、正しくなければfalseを使います。頭に!を付けるとそれぞれ逆の意味になります。

if (true) {
  console.log('正しい');
}

if (!false) {
  console.log('正しくない');
}

null

今は何もないを表す値ですが、使わなくていいです。
使う場合はそのままnullと書きます。

const nil = null;

undefined

こちらも何もないを表す値です。変数初期化時に値を入れない場合勝手にundeifnedになります。

const undefinedValue;
console.log(undefinedValue); // undefined

文字列

上記は、すべてそのまま書くだけで使えるものでしたが、これは'"(または、バッククオート)で囲む必要があります。これで文字列(string)になります。個人的には'を使うのが好きです。

const hello = 'hello';
const world = "world";

const name = 'nju33';
const helloName = `hello ${name}`;
console.log(helloName); // hello nju33

Undefined

Undefined は対象に値が何も入っていない状態を表します。

変数定義時に値を指定しない場合や、

let foo;
// foo === undefined

引数を要求する関数で引数を渡さなかった場合、

const fn = str => { /* ... */ };
fn(); // str === undefined

などによく見られます。

安全な undefined 確認

実を言うと undefined は予約語として扱われていないので、コードを書く人が好きに値を設定できてしまいます。もしも、誰かが変な値を指定してしまうと今書いているコード部分の=== undefinedで比較している箇所がうまく動かなくなるかもしれません。

そのようにならないようにundefinedの代わりにvoid 式を使うと良いです。void 式は式に関係なく必ずundefinedを返す式です。

// どこか遠い所
const undefined = 'foo';

// ---

const myFunction = () => {
  console.log(undefined); // 'foo' 😱
  // 以下の式が`false`に
  console.log(undefined === void 0);
}

myFunction();

Array メソッド

forEach

単にすべての配列の要素を回したい時に使います。
戻り値はundefinedです。

const result = [1, 2, 3, 4].forEach(num => {
  console.log(num);
});
// 1
// 2
// 3
// 4
console.log(result) // undefined;

map

配列のある要素を、同じ位置で別の値に変換したい時などに使います。
戻り値はなんでもありで、具体的には配列に存在するすべての型の配列です。

const result = [1, 2, 3, 4].map(num => {
  if (num % 2 === 0) {
    return String(100 - num);
  }

  return num;
});
// 1
// '98'
// 3
// '96'
console.log(result) // [1, '98', 3, '96']

reduce

配列の値を使って、1つの値を導き出したい時などに使います。
戻り値はなんでもありで、具体的にはハンドラーが受け取る第1引数の型です。

ハンドラーの中でreturnした値が、次のハンドラーの第1引数に来ます。reduceの第2引数に値を渡すと、それが1周目のaccの値になります。

// 0 + 1 + 2 + 3 + 4
const result = [1, 2, 3, 4].reduce((acc, num) => {
  acc = acc + num
  return acc;
}, 0);

console.log(result); // 10;

some

「1つでもそういう条件の要素があるか?」を調べる時に使います。
戻り値は、1つでも当てはまればtrue、1つも当てはまらなければfalseです。

const result1 = [1, 2, 3, 4].some(num => {
  return num === 2;
});
console.log(result1); // true

const result2 = [1, 2, 3, 4].some(num => {
  return num === 5;
});
console.log(result2); // false

every

「すべての要素でそんお条件が当てはまるか?」を調べる時に使います。
戻り値は、すべて当てはまればtrue、1つでも違うならfalseです。

const result1 = [1, 2, 3, 4].every(num => {
  return num < 10;
});
console.log(result1); // true

const result2 = [1, 2, 3, 4].every(num => {
  return num < 3;
});
console.log(result2); // false

push

配列の最後に値を追加します。
戻り値は「いくつ追加したか」の数値です。

破壊的メソッドで、元の配列を書き換えます。

const arr1 = [1, 2, 3, 4];
const result1 = arr1.push(5);
console.log(result1); // 1
console.log(arr1); // [1, 2, 3, 4, 5]

const arr2 = [1, 2, 3, 4];
const result2 = arr2.push(5, 6);
console.log(result2); // 2
console.log(arr2); // [1, 2, 3, 4, 5, 6]

pop

配列の一番最後の値を取り出します。
戻り値は取り出した値です。

const result = [1, 2, 3, 4].pop();
console.log(result); // 4

unshift

配列の最初に値を追加します。
戻り値は「いくつ追加したか」の数値です。

破壊的メソッドで、元の配列を書き換えます。

const arr1 = [1, 2, 3, 4];
const result1 = arr1.unshift(0);
console.log(result1); // 1
console.log(arr1); // [0, 1, 2, 3, 4]

const arr2 = [1, 2, 3, 4];
const result2 = arr2.unshift(-1, 0);
console.log(result2); // 2
console.log(arr2); // [-1, 0, 1, 2, 3, 4]

shift

配列の一番最初の値を取り出します。
戻り値は取り出した値です。

破壊的メソッドで、元の配列を書き換えます。

const result = [1, 2, 3, 4].shift();
console.log(result); // 1

splice

pushpop,unshift,shift的に使うこともできますが、基本的に、

  • 配列から中間地点にある要素を削除する
  • 配列の中間地点にある要素を追加する

のに使います。戻り値は削除した要素の配列です。

破壊的メソッドで、元の配列を書き換えます。

要素の削除

第1引数は中間地点を表していて、第2引数は「いくつ削除するか」を渡します。以下の場合、インデックス1(2)の位置から要素を1つ削除するという意味になり、2が削除されます。

const arr = [1, 2, 3, 4];
const result = arr.splice(1, 1);
console.log(result); // [2]
console.log(arr); // [1, 3, 4]

要素の追加

第3引数以降には追加したい要素をただ順番に書いていくだけです。以下では第2引数を0にしているので、何も削除せず、インデックス1の位置から101102を追加するという意味になります。

const arr = [1, 2, 3, 4];
const result = arr.splice(1, 0, 101, 102);
console.log(result); // []
console.log(arr); // [1, 101, 102, 2, 3, 4]

要素の削除と追加

もちろんこの2つを組み合わせて使うこともできます。

const arr = [1, 2, 3, 4];
const result = arr.splice(1, 1, 101, 102);
console.log(result); // [2]
console.log(arr); // [1, 101, 102, 3, 4]

find

IEでは使えません。

配列から条件にある要素を見つけて要素を返します。
戻り値は要素があれば要素の型、なければundefinedです。

const result = [1, 2, 3, 4].find(num => {
  return num % 2 === 0;
});
console.log(result); // 2

const result2 = [1, 2, 3, 4].find(num => {
  return num % 5 === 0;
});
console.log(result2); // undefined

findIndex

IEでは使えません。

配列から条件にある要素を見つけて要素のインデックス値を返します。
戻り値は要素があれば要素インデックス値(number)、なければ-1です。

const result = [1, 2, 3, 4].findIndex(num => {
  return num % 2 === 0;
});
console.log(result); // 1

const result2 = [1, 2, 3, 4].findIndex(num => {
  return num % 5 === 0;
});
console.log(result2); // -1

flat

IEEdgeNodeJSでは使えません。

1階層までの配列をトップレベルの配列の要素とすることができます。
戻り値は処理後の配列です。

const result = [1, [2, 3], [[4]]].flat();
console.log(result); // [1, 2, 3, [4]]

flatMap

IEEdgeでは使えません。

mapの結果の後、さらにflatするような処理をします。
戻り値は処理後の配列です。

以下はmap[[1, 99], [2, 98], [3, 97], [4, 96]]となった後にflat[1, 99, 2, 98, 3, 97, 4, 96]となる感じです。

const result = [1, 2, 3, 4].flatMap(num => {
  return [num, 100 - num];
});
console.log(result); // [1, 99, 2, 98, 3, 97, 4, 96]

Promise

いつ完了するか分からない処理を順番に実行するのに使います。例えば、

  • API のレスポンスが返ってきた時
  • 指定秒スリープした時
  • DOM が出来上がった時

など。そして、そのような処理を行えるようにPromiseクラスがあります。
Promiseインスタンスはresolverejectコールバック関数を引数として受け取れる関数を渡して作ります。 resolveは処理が成功した時に呼ぶもので、rejectは処理が失敗した時に呼ぶものです。

インスタンスメソッド

Promise#then

resolveコールバック関数が呼ばれるとPromise#thenメソッドが呼ばれます。このメソッドはresolve(arg)が実行された直後に実行されます。resolveに渡した第一引数がPromise#thenの第一引数の値になります。

const processing = new Promise(resolve => {
  resolve({value: 1});
});

processing.then(obj => {
  console.log(obj); // {value: 1}
});

Promise#catch

reject(arg)が実行された時に呼ばれるのがPromise#catchです。

const processing = new Promise((_, reject) => {
  reject({value: 1});
});

processing.catch(obj => {
  console.log(obj); // {value: 1}
});

実はthrowでも同じようにPromise#catchが呼ばれます。

const processing = new Promise((_, reject) => {
  throw {value: 1};
});

processing.catch(obj => {
  console.log(obj); // {value: 1}
});

注意する点として、catchできるのは「rejectまたはthrowされた非同期関数で終わっている場合」に限ります。なので、以下のように何も返さないPromise#catchなどで握り潰してしまうと、その部分のキャッチした値は完全に捨ててしまうことになります。

const processing = new Promise((_, reject) => {
  throw {value: 1};
})
  .catch(() => undefined);

processing.catch(obj => {
  // 呼ばれない
  console.log(obj); // {value: 1}
});

連続した処理

最初に書いたとおりこれの使う理由には処理を連続で書けるようにする点があります。では、どうするのがいいかという事の個人的な話です。

まず、常にPromise#catchをチェーン毎に1つにします。
処理を連続で書けると言っても、throwするかもしれない処理をそのまま渡すと、Promise#catchが難しくなっていきやすいと感じます。それはPromise#catchからPromise#catch間や親との間の伝達の問題です。
ちゃんとエラー処理を施した非同期関数を別に作り渡すようにすると良いと思います。

別の非同期関数にしてチェインに渡すことで次の非同期関数には、自分自身のPromise#thenの結果だけを渡すことができ、同様に自分へはPromise#thenの結果が渡ってくるようにできます。つまり、自分自身のPromise#catchが後のチェイン上の非同期関数へ影響を与えることがなくなります。

例えばこんな感じです。(new Promise(resolve => resolve())() => Promise.resolve()new Promise((_, reject) => reject())() => Promise.reject()のように置き換えれます)

const childAsyncFn1 = () => Promise.reject("...");
const childAsyncFn2 = () => Promise.reject("...");
const childAsyncFn3 = () => Promise.reject("...");

const mainAsyncFn1 = () => {
  return childAsyncFn1()
    .then(childAsyncFn2);
};

const mainAsyncFn2 = () => {
  return Promise.resolve()
    .then(childAsyncFn3);
};

await mainAsyncFn1()
  .then(mainAsyncFn2)
  .catch(err => {
    // childAsyncFn1 のエラー処理
  });

そしてPromise#catchではできるだけPromise#thenと抽象的に同じ意味の値を返すようにすると良いと思います。最終手段のエラー処理と頭に置いておくと良いんじゃないかなと思います。

const childAsyncFn1 = () => Promise.reject("...");
const childAsyncFn2 = () => Promise.reject("...");
const childAsyncFn3 = () => Promise.reject("...");

const mainAsyncFn1 = () => {
  return childAsyncFn1()
    .then(childAsyncFn2)
    .catch(() => {
      // 後が問題なく動くような抽象的な値を返す
      // これにより必ず `Promise#then` が返される
      return {/*...*/}
    });
};

const mainAsyncFn2 = something => {
  return Promise.resolve(something)
    .then(childAsyncFn3);
};

// mainAsyncFn2 へ渡るようになる
await mainAsyncFn1()
  .then(mainAsyncFn2)
  .catch(err => {
    // もしかしたら childAsyncFn2 のエラー処理
  });

例外処理

起こす

throwを使うと、その部分で「何かしら変な事が起きた」ことが返せます。

throw new Error('エラーメッセージ');

コメント

JDOC コメントを残すと他の人の助けになるかもです。

/**
 * @throws {Error} こんなの時
 */
function foo() {
  /* ... */
}

起こったエラーを扱う

try-catchを使います。
tryブロックにはエラーが起こりそうな処理を書きます。もし起きた時、throwされたErrorcatchブロックの引数で受け取れます。

try {
  foo();
} catch (err) {
  console.log(err);
  // Error: エラーメッセージ
  //   at Object.<anonymous>

  // ここで扱えないなら再度 throw したり
  // throw err;
}

よく使うエラー

最低限Errorにだけはして返すといいと思います。

TypeError

TypeError オブジェクトは、値が期待される型でない場合のエラーを表します。 TypeError - JavaScript | MDN

引数の型などに想定外の型を渡された時などに使います。

/**
 * @throws {TypeError} num が数値じゃない時
 */
const plusOne = num => {
  if (typeof num !== 'number') {
    throw new TypeError('数値を渡してください');
  }

  return num + 1;
};

SyntaxError

構文的に不正なコードを解釈しようとした場合のエラーを表します。 SyntaxError - JavaScript | MDN

渡された文字列をパースする関数などで、想定外の形の文字列を渡された時などに使います。

const parse = str => {
  const re = /* 正規表現 */;
  if (!re.test(str)) {
    throw new SyntaxError('文字列の形が不正です')
  }

  return Foo.parse(str);
}

自分で作る

Errorクラスを継承して自分のErrorクラスを作ることができます。
Error.captureStackTraceはスタックトレースから無駄を減らしてくれます。

class MyError extends Error {
  constructor(message) {
    super(message);
    Error.captureStackTrace(this, MyError);
  }
}

const foo = () => {
  /* ... */
  throw new MyError('エラー!!');
  /* ... */
};

try {
  foo();
} catch (err) {
  console.log(err instanceof MyError); // true
}

非同期 async-await

async-awaitは、「いつ完了するか分からない処理」だけど「順番に実行したい」な事をするために使います。「コールバック地獄」と呼ばれていたような読みづらいコードをシンプルに書けます。

構文

functionの前、アロー関数なら()の前にasyncと書きます。

async function foo() {
  /* ... */
}
const bar = async () => {
  /* ... */
};

そして、中でPromiseを返す関数の実行前にawaitと書けば、処理が終わるまでその場で待ってくれます。これはPromise#thenを使う方法と比べて構文を反対に書いたような形になります。つまり「関数→then→変数」から「変数→await→関数」といった具合です。

// Promise#then
asyncFn1().then(data => {/*...*/});
asyncFn2().then(() => {/*...*/});

// await
const data = await asyncFn1();
await asyncFn2();

以下は1毎にコンソールにテキストを出力する例です。

/**
 * 1秒待つ
 * (
 *    `async`と書くと`Promise`だなとわかるのでオススメしたいです
 *    TypeScript 除く
 *  )
 */
const delay = async () => new Promise(r => setTimeout(r, 1000));

(async () => {
  console.log('すぐ表示');
  await delay();
  console.log('1秒後表示');
  await delay();
  console.log('さらに1秒後');
})();

エラー処理

await文の例外はtry-catchでキャッチすることができます。(エラーでやっているけど単に勉強の為でPromise#rejectでも良い)

class OversleepError extends Error {
  constructor() {
    super('寝坊した');
    Error.captureStackTrace(this, OversleepError);
  }
}

/**
 * 寝る
 */
const sleep = async () => {
  throw new OversleepError();
};

(async () => {
  try {
    await sleep();
  } catch (err) {
    console.log(err instanceof OversleepError); // true
  }
})();

扱うのはPromiseなのでcatchで一旦受け取ってできるだけ処理することももちろんできます。

/* 上の関数定義そのまま */

const eatBreakfast = async () => {
  /* 食べる */
};

(async () => {
  const task = {
    breakfast: true
  };

  await sleep().catch(err => {
    if (err instanceof OversleepError) {
      task.breakfast = false;
      return;
    }
  });

  // 遅刻してない場合、
  // 朝食を食べる
  if (task.breakfast) {
    await eatBreakfast();
  }
})();

分割代入

「ある配列やオブジェクトの1部だけを使うからそれだけ取り出して使いたい」ような時に使えます。

Object

const {取り出したいプロパティ名} = ...とするだけです。

const {a, c} = {a: 1, b: 2, c: 3};
console.log(a); // 1
console.log(c); // 3

万が一同じ名前の変数がスコープ上にあるなら:で別名をつけることができます。

const {a: alias} = {a: 1};
console.log(alias); // 1

また=を使うことで、その値がundefinedの時にデフォルト値が入るようにできます。

const {c = 3} = {a: 1};
console.log(c); // 3

組み合わせると。

const {d: aliasOfD = 4} = {a: 1};
console.log(aliasOfD); // 4

Array

最初の要素だけ取りたい場合は、

const [first] = [1, 2, 3];
console.log(first); // 1

最後だけならカンマで頭の要素を飛び越えて取得します。

const [,,first] = [1, 2, 3];
console.log(first); // 1

またそれ以降の要素をすべて取得したいなら...を使います。

const [,...tail] = [1, 2, 3];
console.log(tail); // [2, 3]

ただ、これは最後の変数値にしか使えません。

const [...head,] = [1, 2, 3];
// これは駄目!

また配列も=でデフォルト値を設定できます。

const [first = 1] = [];
console.log(first); // 1

...の方には設定できません。

const [, ...tail = [2, 3]] = [1];
// SyntaxError: Invalid destructuring assignment target

イベント

EventTarget#addEventListener

EventTarget#addEventListenerは特定のイベントが起きた時に、それに連動する形である処理を実行したい時に使います。よくあるのが要素をクリックしたら「ほげほげ」するというような処理です。それはこのように書かれます。

document.getElementById('button').addEventListener(
  'click',
  () => {
    console.log('clicked');
  }
);

1番目がイベントの名前、そして2番目に連動して実行したい処理(関数)です。ちなみに、addEventListenerには3番目の引数も設定できます。これは仮引数falseが渡っているのでデフォルトでは有効ではない形でイベントが設定されます。

3番目の引数はuseCaptureと書かれたりしていて、子要素で同名のイベントが起きた時に親から連動処理を実行するかを定めるものです。例を用意しました。

外の四角はクリックすると「outer click」、中の四角はクリックで「inner click」とログするようになっています。そして外の四角のイベントはuseCapturetrueに設定しています。この状態で中の四角をクリックするとどうなるかというと、「outer click」から「inner click」の順番で呼ばれます。親の四角のイベントでuseCapturefalseとしているとこれは逆になります。

EventTarget#removeEventListener

EventTarget#removeEventListeneraddEventListenerで設定した連動処理を解除するのに使います。気を付けることは、例えば上記の「clicked」とログするイベントを解除したい時に、

document.getElementById('button').removeEventListener(
  'click',
  () => {
    console.log('clicked');
  }
);

と書いても効かないということです。実は2番目の引数のハンドラーは同じものである必要があります。上記では、その場で定義した(ただし処理は同じんな)新しい関数を渡してしまっているので解除されません。
これを修正するにはハンドラーをちゃんと安定した場所に定義します。

const onClick = () => {
  console.log('clicked');
};

そしてaddEventListener, removeEventListenerで使えば大丈夫です。

const button = document.getElementById('button');
button.addEventListener('click', onClick);
button.removeEventListener('click', onClick);

あるオブジェクトのプロパティの有無を調べる

Object#hasOwnProperty

プロトタイプオブジェクトを辿らずにそのオブジェクトがプロパティを持っているか調べれます。これはオブジェクトのメソッドですが、対象のオブジェクトがhasOwnPropertyを持っている可能性などが可能性は低いですがある為メソッドでの呼び出しは良くありません。以下のようにcallから呼び出すとほぼ確実にメソッドのhasOwnPropertyが呼び出せてより安全です。

Object.prototype.hasOwnProperty.call({foo: 'foo'}, 'foo');
// true
({foo: 'foo', hasOwnProperty: () => false}).hasOwnProperty('foo');
// false

in

プロパティ名 in オブジェクトでも調べることができます。こちらはプロトタイプを遡り調べることができます。

'hasOwnProperty' in Object
// true

もちろん、ただのプロパティの有無も調べられますよ。

'foo' in {foo: 'foo'}
// true

ビットフラグ

権限などの管理はビットフラグを使うと楽そうです。

const EXECUTABLE = 1 << 2;
const WRITABLE = 1 << 1;
const READABLE = 1;

const executes = user => {
  return Boolean(EXECUTABLE & user.permission);
};

const writes = user => {
  return Boolean(WRITABLE & user.permission);
};

const reads = user => {
  return Boolean(READABLE & user.permission);
};

const executableUser = {permission: 0b111};
const writableUser = {permission: 0b011};
const readableUser = {permission: 0b001};

const process = user => {
  switch (true) {
    case executes(user): {
      console.log('execute!');
      break;
    }
    case writes(user): {
      console.log('write!');
      break;
    }
    case reads(user): {
      console.log('read!');
      break;
    }
    default: {
      throw new Error('');
    }
  }
};

[executableUser, writableUser, readableUser].forEach(process);
// execute!\nwrite!\nread!\n

AND

両方が1の場合のみ1になります。0b101 & 0b1000b100(4)です。
上記コードでEXECUTABLE0b100で判断しようとしていて、executableUserpermission: 0b111を持っているのでBoolean(0b100 & 0b111)trueとなります。readableUserでは0b100 & 0b001 === 0b000falseとなるので権限を持っていないことがわかります。

OR

片方が1なら1になります。権限を付与する時などに。0b100 | 0b001 === 0b101

日本時間を取得

JavaScript では時間はnew Date()などで取得できますが、そのマシンのタイムゾーン設定などからうまく日本時間を取得できない場合があります。

JST が選択可能なマシンの場合

Date#toLocaleStringを使えば大丈夫です。日本時間を取得するにはこれに日本のタイムゾーンであるAsia/Tokyoを渡して実行します。

new Date().toLocaleString({ timeZone: 'Asia/Tokyo' })
// "3/19/2019, 3:29:58 PM"

これで日本時間の文字列が取得できます。何か触りたい場合は再度これをnew Date()に渡して使います。

const japanStandardTime = new Date().toLocaleString({ timeZone: 'Asia/Tokyo' });
new Date(japanStandardTime).getHours();
// 15
new Date(new Date().toLocaleString({ timeZone: 'Asia/Tokyo' })).getTime();
// 1552978393000

JST が選択できないマシンの場合

この場合上のように、timeZone: 'Asia/Tokyo'と設定しても UTC になってしまうようです( AWS Lambda 上など)。その場合は、Date#getTimeZoneOffsetを使います。
これは、協定世界時からどれだけ差があるかをで取得できるメソッドです。例えば日本なら9時間で9 * 60(540)分進んでいますから-540という値が得られます。そして、 UTC なマシンでは0を取得します。

new Date().getTimeZoneOffset();
// -540 JST
// 0 UTC

この差を利用することで、以下のようにするとどちらでも日本時間を取得できます。つまり、 JST の方を0として扱い、逆に UTC を540分遅れているという風にして、その分のtimeを現在のtimeに追加してあげるということです。

new Date(Date.now() + ((new Date().getTimezoneOffset() + (9 * 60)) * 60 * 1000));
// どちらで実行しても同じ結果

Symbol.iterator

オブジェクトにこのプロパティを指定するとfor..ofなど反復文で回る要素を自分で制御できます。またSymbol.iteratorにはGeneratorfunction* () {...}を使います。例えば以下では、1, 2, 3と表示されます。

const something = {};
something[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;

    // *yield [1, 2, 3]
    // も同じ結果になる
};

for (const num of something) {
  console.log(num);
}

またスプレッド構文を使うことで配列を取得することもできます。

const arr = [...something];
console.log(arr);
// [1, 2, 3]

し 僕的にはArray::filterなどで1回では絞り込めないような時に使ったりします。

例えば、配列がSlack-APIで取得できるような以下のような構造だとした時に、

const items = [
  {type: '***'},
  {type: '...'},
  {type: 'bot'},
  {type: ',,,'}
];

{type: 'bot'}まで絞り込みたい時などです。ちなみにGeneratorのthisSymbol.iteratorを埋め込んだ要素自身になります。

const untilItemHasTypeIsBot = function*() {
  const len = this.length;
  let i = 0;
  while (i < len) {
    if (this[i].type === "bot") {
      break;
    }
    yield this[i];
    i++;
  }
};

items[Symbol.iterator] = untilItemHasTypeIsBot;
console.log([...items]);
// [ { type: '***' }, { type: '...' } ]

Proxy

Proxyとは

これらは基本的な言語操作(例えば、プロパティ検索、代入、列挙、関数呼び出しなど)に割り込み、動作をカスタマイズできます。
メタプログラミングより

らしいです。自分の理解で説明すると、「プロパティ値に実際にアクセスする前にプロパティ値を好きに弄れる。元の値も返そうと思えば返せる」というような感じです。

Proxy Handler

new Proxy(obj, handler)のように第2引数に渡します。ハンドラには特定のメソッドをいくつか作る必要があります。例えば、

  • 値の取得時get
  • 値の設定時set
  • in構文でhas
  • 関数実行時apply
  • newした時constructor

など。そしてこれらの事をトラップ(trap)と言います。

Reflect について

Reflectを使うとgettersetterなど関数の中のthisをProxytargetのように扱うことができます。

まずはgetsetだけ見ていきます。

getトラップ

const realObj = {
  foo: 'foo',
  get bar() {
    return this._bar;
  }
};

const handler = {
  get(target, propName, receiver) {
    if (target[propName]) {
      return Reflect.get(...arguments);
    }
    return 'default value';
  },
}

const obj = new Proxy(realObj, handler);
console.log(obj.foo, obj.bar); // 'foo' 'default value'

これはrealObjgetトラップを適用してます。obj.fooの時はそのまま値が返りますが、obj.barの時はthis._barという値がないのでdefault valueが返ります。

setトラップ

まずsetトラップは値を入れたか入れないかでbooleanを返すようにします。

const realObj = {
  foo: 0,
  _bar: 0,
  set bar(value) {
    this._bar = value;
  },
  set baz(value) {
    this._baz = value;
  }
};

const handler = {
  set(target, propName, value, receiver) {
    if (Object.prototype.hasOwnProperty.call(target, propName)) {
      Reflect.set(...arguments);
      return true;
    }
    return false;
  },
}

const obj = new Proxy(realObj, handler);
obj.foo = obj.bar = obj.baz = 1;
console.log(obj.foo, obj._bar, obj._baz) // 1 1 undefined

これはfoo_barには1が入りますが、_barは入りません。これはthis._bazが無いためです。

既存の HTML タグを Custom Web Component で上書きできるか

できませんSyntaxError: Failed to execute 'define' on 'CustomElementRegistry': "button" is not a valid custom element nameになります。

Custom Web Component は 「-」 を挟む形じゃないと定義できない

foobarは登録できませんが、x-foox-barは大丈夫です。したがって、既存のHTMLタグはワンワールドの名前の要素しかないので無理だということになります。

カスタム要素の名前にはダッシュ(-)を含める必要があります。つまり、はすべて有効な名前ですが、は無効です。この要件によって、HTML パーサーは、通常の要素とカスタム要素を区別することができます。またこれによって、新しいタグが HTML に追加されたときの前方互換性が保証されます。 https://developers.google.com/web/fundamentals/web-components/customelements

DOM の作成

単一要素

document.createElement(TAG_NAME)で好きなHTML要素を1つ作れます。例えば、document.createElement('div')であれば<div />要素が、document.createElement('a')であれば<a />要素が作られます。

複数要素

document.createRange().createContextualFragment(HTML)を使うとネストした断片ドキュメントを取得できます。断片ドキュメントとは、最初ページには1つのDOMだけしか存在しませんが、そのDOMとは関係ない(影響を与えない)完全に新しい構造のDOMを作ることができ、そのDOMの事を断片ドキュメントと言います。

素の断片ドキュメントはdocument.createContextualFragment()から作成することができ、その場合このドキュメントへ上記の単一要素を追加していく方法でDOMを構築必要があります。ここでdocument.createRange().createContextualFragment(HTML)を使っているのは、使用時に渡したHTMLによってDOMを自動構築してくれる為です。

つまり以下は、

const fragment = document.createDocumentFragment();

const div = document.createElement('div');
div.textContent = 'div';

const span = document.createElement('span');
span.textContent = 'span';

div.appendChild(span);
fragment.appendChild(div);

このように書き換えることができます。

const fragment = document.createRange().createContextualFragment(
  '<div>div<span>span</span></div>'
)

イベントなどを登録する必要がある場合もネストが浅い場合は使ったほうが便利です。querySelectorが使えるのでそれで取得して設定するといいと思います。

 ### 要素を DOM に追加

要素でも断片ドキュメントでもDOMへ要素を追加するメソッドに渡してあげるだけです。

document.body.appendChild(div);

document.body.insertBefore(
  document.createRange().createContextualFragment('<div>aa<span>aaaa</span></div>'),
  document.body.children[0],
);

DOM 追加後の注意

断片ドキュメント内の要素は完全に無くなり、追加後はquerySelectorなどで要素を取得できなくなるので、必要な要素は先に取得するなどするといいと思います。

DOM の取得

getElementById

<div id="foo">...</div>という要素を取得したい場合は、document.getElementById('foo')と実行することでその要素を取得できます。もし、存在しない場合はnullになります。

querySelector と querySelectorAll

CSS のセレクタで要素を取得できます。querySelectorは対象の要素かnullが、querySelectorAllは対象の要素の配列(無ければ空配列)を返します。

document.querySelector('.foo');
document.querySelectorAll('.foo, [data-testid="bar"]');

オブジェクトライクな値をただのオブジェクトに変換

ここで言うオブジェクトライクとは Enumerable とそうじゃないプロパティが混在していたり、 Prototype 継承していたりするオブジェクトのことです。

以下でできます。

Object.assign で オブジェクトライクな値をただのオブジェクトに変換

Enumerable なプロパティだけのオブジェクトを取り出す(新たに作る)ことができます。ちなみに、上は下のシンタックスシュガーで対応していないブラウザもあるので注意です。

{...obj};
Object.assign({}, obj};

JSON 静的メソッドでオブジェクトライクな値をただのオブジェクトに変換

上記との違いは、undefinedFunction値も落とすことができることです。

JSON.parse(JSON.stringify({foo: 'a', bar: undefined, baz: null, fn: () => {}}))
// {foo: "a", baz: null}

ただ data- 属性値を取り出して渡したい

実際にこういうことがありました。上記の方法を使って、data-*属性値を取り出して、そのまま渡すにはこうします。

const dataset = {...element.dataset};
// dataset を使う

ちなみにHTMLElement#datasetプロパティはIE11未満ではサポートされていないので注意ですね。

値の列挙

値を列挙する時は(場合にもよるかもしれませんが)Object.freezeを使うといいかもしれません。これを使うとそのオブジェクトの値は(そのままでは)上書きできないようにできます。

また、これはIE9から使えて問題もないです。

使い方

単に引数に上書きを禁止したいオブジェクトを渡すだけです。また戻り値ではただのオブジェクトの用に使うことができます。

Object.freeze({foo: 123})
// {foo: 123}

// `writable`が`false`に
Object.getOwnPropertyDescriptor(Object.freeze({foo: 123}), 'foo')
// {value: 123, writable: false, enumerable: true, configurable: false}

// `Object.freeze`を使わないと`writable`が`true`
Object.getOwnPropertyDescriptor({foo: 123}, 'foo')
// {value: 123, writable: true, enumerable: true, configurable: true}

上書きしようとしてもエラーにはならない

エラーなどは起きませんが、値は元のままです。

const obj = Object.freeze({foo: 123});
obj.foo = 456;
obj.foo // 123

うっかり上書きする・される心配が減ります。

.js ファイルを非同期で読み込む

一応以下のようにすれば非同期読み込みになります。

<script async src="..."></script>

ですが、これだと100%読み込む事になってしまうので、読み込みたいタイミングで読み込む方法です。

script を動的に作成する

流れは、

  1. <script>タグを作る
  2. 属性を設定
  3. <body>あたりに埋め込む

です。

script タグを作る

document.createElementを使います。scriptを作りたいなら、

const script = document.createElement('script');

です。これで DOM 上にはまだありませんが、<script>タグが作れました。

属性を設定

以下でasyncsrc辺りを設定します。

script.async = 1;
script.src = '...';

body あたりに埋め込む

<script>は DOM に埋め込んでから初めてソースを読み込んでくれるので DOM に追加してあげます。
一番てっとり早いのが<body>タグに埋め込む方法だと思います。これは<body>タグがあればdocument.bodyで取得できます。もし、ない場合は何かの要素にidを振りdocument.getElementByIdなどを使うといいです。

ある要素の下に要素を追加するにはNode#appendChildを使います。以下は追加するコードです。

document.body.appendChild(script);

これでこのような HTML になり読み込みしてくれるはずです。

<body>
  <script async src="..."></script>
</body>

読み込みが終わったタイミングを知る

非同期で読み込むソースが何か別ソースの依存だったり、処理の関係上「Aの読み込みが確実に終わった後にBの処理をしたい」ということは結構あると思います。これには<script>loadイベントを使います。

loadイベントは<script>はソースが読み込み終わった時に発火されます。そのハンドラー関数に続きの処理を書いた関数を設定すれば順番に処理を実行できますね。これは、DOM に追加する前に設定する必要があります。

script.addEventListener('load', () => {
  alert('読み込み完了');
};

document.body.appendChild(script);

script 埋め込みを非同期関数化する

そのソースの読み込み自体を非同期関数にすると何かと便利に扱えます。これはnew Promiseを返しloadのハンドラー関数にそのresolveを指定するだけです。

const loadScriptXxx = () => {
  return new Promise(resolve => {
    /** 上記の内容 */
    script.addEventListener('load', resolve);
    /** 上記の内容 */
  });
};

これでasync-awaitで分かりやすく書けるかもです。

(async () => {
  await loadScriptXxx();
  await loadScriptYyy(); // 例えば Xxx 依存
  await loadScriptZzz(); // 例えば Yyy 依存

  alert('Xxx, Yyy, Zzz 読み込み完了!')
})();

.css ファイルを非同期で読み込む

流れは、

  1. <link>タグを作る
  2. 属性を設定
  3. document.headに埋め込み

<link>タグはdocument.createElementを使って以下のように作れます。

const link = document.createElement('link');

Node としては存在しますが、まだ DOM 上にはない状態です。

属性を設定

.cssの読み込みなのでrelhrefが必要ですね。以下のように設定します。

link.rel = 'stylesheet';
link.href = '...'; // url

document.head に埋め込み

<head>タグが存在するマークアップだとdocument.head<head>タグを取得でき、クエリで取得する必要もない一番てっとり早い方法だと思います。なのでここに上記で作成したlinkを埋め込みます。

ある要素の下に要素を埋め込むにはNode#appendChildを使います。以下はそのコードです。

document.head.appendChild(link);

これでこのような HTML になるはずです。そして、 DOM に追加された瞬間にそのhrefに指定したファイルの取得が始まります。

<head>
  <link rel="stylesheet" href="...">
</head>

読み込み終わりのタイミングを知る

スタイルシートなので何かに依存しないと動かないといったことはあまりないと思いますが、知りたい場合はloadイベントを使います。これは.cssファイルの読み込みが終わったら発火するイベントなので、これを使えば読み終わったらアラート出すみたいなことができます。

link.addEventListener('load', () => {
  alert('読み込み完了');
});

document.head.appendChild(link);

DOM に埋め込む前に定義してください。

関数のメモ化

以下のfoo関数は中で重い処理を行っているものだとします。

foo(1);
foo(1);
foo(1);

上記では3回、引数もまったく同じで状態でfooを実行しているということをしています。もし、fooの結果が、ある引数の時の結果が必ず決まったものになるという関係ならこれは勿体無いことをしていることになります。必ず決まった結果になるのに、丸々重い処理をしているからです。

このような場合は、最初の実行の結果を記録して、次回以降それを返すようにするといいです。

  • 1を渡されて実行された時に初回の実行結果の記録がなければ、関数のメイン処理をそのまま行い、得られた結果を記録
  • 次回以降1を渡されて実行された場合は、前回の結果の記録が残っているので関数のメイン処理は行わず、記録をそのまま返します。

メモ化するにはfoo関数をこのようにします。memoは記録するための Map です。

const memo = new Map();
const foo = num ⇒ {
  if (memo.has(num)) {
    return memo.get(num);
  }

  const result = process(); // 重い処理...

  memo.set(num, result);
  return result;
};

最初は記録があるかのチェックをして、あれば記録を返す処理の部分です。これはnumで数値なのでそのままキーとして使っていますが、これがオブジェクトなら新しい参照のオブジェクト化したり、対象のプロパティを取り出したり、JSON.stringifyで文字列にしたりして正規化します。

記録がない場合はその後の処理を行い結果は、正規化したキーの値としてsetするだけです。

試しに測ってみる

/*
 * [
 *   [98, 24, 34, 1, 0, ...],
 *   [9, 98, 43, 11, 93, ...],
 *   [17, 9, 43, 54, 86, ...],
 *   ...
 * ]
 */

const Benchmark = require('benchmark');

const suite = new Benchmark.Suite();

const randomIndex = () => Math.floor(Math.random() * 99);
const arr = Array.from(Array(100)).map(container => {
  return Array.from(Array(100)).map(() => randomIndex());
});

const nonMemoizeFn = idx => {
  return arr[idx].reduce((acc, num) => {
    acc += num;
    return acc;
  }, 0);
};

const memoize = new Map();
const memoizeFn = idx => {
  if (memoize.has(idx)) {
    return memoize.get(idx);
  }

  const result = nonMemoizeFn(idx);
  memoize.set(idx, result);
  return result;
};

suite
  .add('memoize fn', () => {
    memoizeFn(randomIndex());
  })
  .add('non memoize fn', () => {
    nonMemoizeFn(randomIndex());
  })
  .on('cycle', event => {
    console.log(String(event.target));
  })
  .on('complete', () => {
    console.log('Fastest is ' + this.filter('fastest').map('name'));
  })
  .run({async: true});

結果はこのようになります。

memoize fn x 20,497,190 ops/sec ±1.00% (86 runs sampled)
non memoize fn x 754,918 ops/sec ±1.89% (86 runs sampled)
Fastest is memoize fn

DOM 監視: MutaionObserver

DOM に変更が走るかどうか監視を行えるものにMutationObserverAPI があります。使うにはまずインスタンスを作る必要があります。

const observer = new MutationObserver(callback);

引数にはcallbackが必要です。これは DOM に変更が走った時に発火するコールバック関数です。コールバック発火時この関数にはMutationRecordというオブジェクトが配列渡ってきます。なぜ配列かというとそのタイミングで置きたすべての記録が渡される為です。
MutationRecordにはどういう変更が走ったのか(type)やそれぞれのtypeで役立つ情報などが渡ってきます。

監視を始めるには作ったインスタンスobserverobserveを対象の要素を引数に実行します。

observer.observe(node, {/* ... */});

監視を終えたい時はdisconnectメソッドを実行します。これを実行すると今までにobserveへ渡した要素についてすべて監視が解除されます。

observer.disconnect();

MutationObserverInit

MutationObserver#observeする時、どういったものを監視するのかを指定する必要があります。このオプションで指定できる値は以下の7つです。

  • childList
  • subtree
  • attributes
  • attributeOldValue
  • attributeFilter
  • characterData
  • characterDataOldValue

この中で、attributescharacterDatachildListの内1つは必ずtrueで設定しなければなりません。

ざっと見れば7つと分かりづらいですが、グループ分して以下の3つに分けて考えても良いと思います。

  1. childListと`subTree
  2. attributesattributeOldValueattributeFilter
  3. characterDatacharacterDataOldValue

childList と subTree

要素が追加された、削除されたを監視したい場合はchildListtrueで置きます。例えば自身がelementだとして、element.appendChildelement.removeChildなどが呼ばれた時にコールバックが発火します。MutationRecordへは要素が追加された場合はaddedNodesに、削除された場合はremovedNodesにそれぞれ対象の要素が渡されます。

上記のelementように監視対象が自分自身だけの場合ならこれでも良いですが、子が持つ要素も同じように監視したい場合にsubTreetrueにします。これで何らかの手段である子要素に子要素ができた時にもコールバックを走らせれます。

observer.observe(item, {
  childList: true,
  subtree: true,
});

attributes と attributeOldValue、 attributeFilter

要素の属性が変わるかどうか監視したい場合はattributestrueで置きます。このtypeでコールバックが発火した時のみ、attributeNameに値が入ります。例えばstyle属性を変更して発火したのであればstyleのような感じです。

後の2つはオプショナルです。

attributeOldValuetrueにすると、変更があった際のMutationRecordoldValueに対象の属性の古い値が入ってきます。

attributeFilterは属性を絞り込みたい場合に使います。例えば「value属性は監視したいけど、id属性の監視は要らないや」という場合以下のように設定します。

observer.observe(item, {
  attributes: true,
  attributeOldValue: true,
  attributeFilter: ['id'],
});

characterData と characterDataOldValue

テキストが変わったかどうかを監視したい場合はcharacterDatatrueで置きます。
characterDataOldValueはオプショナルでtrueにすると、attributeOldValueのようにoldValueプロパティに変更前のテキストが入ってきます、

observer.observe(node.childNodes[0], {
  characterData: true,
  characterDataOldValue: true,
});

このように監視した状態で以下のようにテキストを更新するとコールバックが発火します。

node.childNodes[0].nodeValue = 'update text';

実際に動いてる様子はCodeSandboxで見ることができます。

交差監視: IntersectionObserver

IntersectionObserver API を使うと「お、この要素見える位置に入ってきたな」と分かったときに好きな処理を行えます。

これを使うにはまずインスタンスを作ります。

const observer = IntersectionObserver(callback, {/* ... */});

ここで渡したcallbackは(デフォルトで)ブラウザ画面に要素が入ってきたり、出ていったとこに発火されるコールバック関数です。この関数は引数にIntersectionObserverEntryの配列を渡してきます。この値は出たり入ったりした要素と様々なコンテキストを持ったオブジェクトです。
例えばintersectionRatioはコンテキストの1つで、0-1の間(小数点含む)の値で対象の要素は今どれだけ可視領域に入ってるのかが分かります。これは0だとすべて見えてない、1だとすべて見えてるという意味になるからです。

設定

先程デフォルトでと書きましたが、どの要素を基準に出たり入ったりを監視するかは変えることができます。これにはIntersectionObserverの第2引数を使います。第2引数には基準を変える以外の要素も持っています。

root

rootが要素の基準になるものです。これはデフォルトではブラウザのビューポートになってますが、

{
  root: document.getElementById('container')
}

とすると、そのobserverで監視した要素は#containerから出たり入ったりした時にコールバックが走るようになります。

rootMargin

rootMarginroot要素の領域を拡大・縮小化できます。例えばrootMargin: '50px 50px 50px 50px'とした場合、要素の見た目は変わりませんが、上右下左側に50px余分に自分の可視領域だと見なすことができます。つまり、observeしている要素が表示領域まで達するのに実際はあと50pxあったとしてもコールバック関数が発火します。

この値を例えば-50pxとすると逆の意味になります。-50pxと設定されていると実際に見えるようになった領域から更に50pxスクロールしなければコールバックが発火しなくなります。

このオプションはデフォルトで0px 0px 0px 0pxが設定されてます。

threshold

thresholdはどれくらいの頻度でコールバックを呼び出すかを設定します。デフォルトでは0が設定されてます。これは(rootMarginがすべて0の状態で)1pxでも被った時コールバック関数を実行するということになります。

この値を0.5に変えると要素が半分見えた時にコールバック関数が発火するようになり、1だとすべて見えた時…という具合になります。

この値は配列でも設定することができ、[0, 0.5, 1]と設定すれば、入る時は

  1. 1pxでも見えた時
  2. 半分見えた時
  3. 全部見えた時

出る時は

  1. 全部見えなくなった時
  2. 半分見えなくなった時
  3. まったく見えなくなった時

という具合でコールバック関数が呼ばれるようになります。

要素の監視

監視を始めるにはIntersectionObserver#observeを呼びます。この引数には監視対象となる要素を渡します。

observer.observe(element);

監視の必要が無ければ解除しましょう。それにはIntersectionObserver#unobserveまたはIntersectionObserver#disconnectを使います。前者は要素1つに関して監視を解除するメソッドですが、後者はそのオブザーバーの監視対象すべてについて解除します。

observer.observe(element);
observer.disconnect();

マルチスレッド: Web Worker

JavaScript は基本シングルスレッド(UIスレッド)でイベントループを回しつつ処理を実行してます。UIスレッドで重い処理を行うと一時的に画面がフリーズしたり、画面が表示されるまでの速度が遅くなったりの原因になります。

Web Worker を使うと JavaScript コードをUIとは別のスレッドで実行できるので、そういった問題を回避できるかもしれません。ここにフリーズする例しない例があります。このしない例では Web Worker を使用してます。

PromisesetTimeoutなどとの違いは、それらは非同期で実行されますが UIスレッド上で処理されます。Web Worker も非同期ですが別のスレッドで処理されるという違いがあります。

仕組み

Web Worker はイベントで UIスレッドとのデータのやり取りを行います。どちらのスレッドでも使うのは2つのメソッドだけです。

  1. #addEventListener('message', () => { /* ... */ }によりイベントハンドラーを登録し、送られてきたデータを処理
  2. #postMessage(data)でデータを別スレッドへ送信
                      UI                                      Other
                +------------+               +------------------------------------+
                |    ping!   | +-----------> |addEventListener('message', handler)|
                +------------+               +------------------+-----------------+
                                                                |
                                                                |
                                                                |
                                                                |
                                                                |
                                                                v
+--------------------------------------------+          +-------+-----+
|addEventListenerHandleer('message', handler)| <------+ |     pong!   |
+--------------------------------------------+          +-------------+

制限

Web Worker が動くスレッドでは以下のような制限が掛かります。

  • window
  • document
  • あらゆる DOM 要素

Web Worker ファイルを作る

Worker ファイルは拡張子を.worker.jsとするケースが多いようなのでこのルールに則りworkers/ping-pong.worker.jsというファイルを作ります。このファイルの中にはselfというDedicatedWorkerGlobalScopeがグローバル領域で使えるようになっており、これを通してイベントを扱ったり、データを送信したりします。

以下のコードは送られてきたデータのtypeプロパティ値がpingだった場合pongという文字列データを返す Web Worker です

self.addEventListener('message', event => {
  if (event.data.type === 'ping') {
    self.postMessage('pong'); 
  }
});

Worker インスタンス

スレッドは UI スレッドから Worker コンストラクタを通して建てれます。その時 Worker へは Web Worker となる.jsファイルへのパスを渡します。先程のファイルを使うと以下のようになります。

const worker = new Worker('workers/ping-pong.worker.js');

UI スレッド側でも同じ用にイベントを扱うようにしたり、データを送信します。基本図のように UI スレッドから何かしら処理の起点をあげなければ何も始まりません。

worker.addEventListener('message', event => {
  console.log(event.data);
});

worker.postMessage({type: 'ping'});

もしコードが即実行されるようになっていればリロードする度にpongと表示されるはずです。
また上記のような感じで実装したです。この例はクリックしたら…ですがその度pongとコンソールに表示されます。

deno を Mac にインストール

まずdenoをダウンロードしてきます。

curl -L https://deno.land/x/install/install.py | python

次にパスを指定するだけです。

echo export PATH="/Users/nju33/.deno/bin":\$PATH >> $HOME/.bash_profile

fishを使っているなら以下になります。

echo set -gx PATH {$HOME}/.deno/bin $PATH >> $HOME/.config/fish/config.fish

設定ファイルを再読込してdeno -vしてみましょう。

deno: 0.2.8
v8: 7.2.502.16
typescript: 3.2.1

こんな感じ(2019-01)のものが出力されたら完了です。

API のレスポンス JSON をコピー

Chrome だけですが。

レスポンスを触れる箇所で debugger を置く

例えばこんな感じのコードです。とりあえずdataを触りたいのでこの位置にdebuggerを置きます。

ちなみにデモデータはこちらのサイト(jsonplaceholder.typicode.com)。

fetch('https://jsonplaceholder.typicode.com/comments')
  .then(res => res.json())
  .then(data => {
    debugger;
  });

これを Chrome の Devtool で実行するとdebuggerの箇所で止まってdataで詳細が見れると思いますが、ここで Devtool が提供する関数copyを使うことで、その JSON を丸っとコピーできます。今回ならこんな感じでコピーします。

copy(data);

ネットワークタブのレスポンスが文字化けしてうまくコピーできないような時には便利です。