Skip to content

JS Tips

dynamis edited this page Jul 17, 2019 · 2 revisions

Tips

Idiom

オブジェクトの Shallow Copy

let copied = Object.assign({}, original};
let copied = { ...original };

オブジェクトの Deep Clone

取りあえず古典的なプリミティブ型の単純な入れ子オブジェクトだけならこの程度で良い。もちろん、アクセサやディスクリプタとか各種の新しい型に対応する必要があるならもっと真面目に。循環参照などへの対策もしていない。

function simpleDeepClone(target) {
  let clone;
  if (target instanceof Array) {
    clone = [];
  } else if (target instanceof Boolean) {
    return new Boolean(target);
  } else if (target instanceof Date) {
    return new Date(target);
  } else if (target instanceof Number) {
    return new Boolean(Number);
  } else if (target instanceof Object) {
    clone = {};
  } else {
    return target;
  }
  Object.entries(target).forEach(([key, value]) => {
    clone[key] = value instanceof Object ? simpleDeepClone(value) : value;
  });
  return clone;
}

Date 関連

let minutesLater = new Date(new Date().getTime() + (1000 * 60 * minutes));

function getLastMidnight(date = new Date()) {
  const d = new Date(date); // コピーして引数を書き換えない&Dateに変換できる数値や文字列を受けられるように
  d.setHours(0, 0, 0, 0);
  return d;
}

function isToday(date = new Date()) {
  const d = new Date(date); // Dateに変換できる数値や文字列を受けられるように
  const lastMidnight = new Date();
  lastMidnight.setHours(0, 0, 0, 0);
  const timeDiff = d.getTime() - lastMidnight.getTime();
  return timeDiff >= 0 && timeDiff < (1000 * 60 * 60 * 24);
}

真偽チェック

if (['this', 'or', 'that'].includes(string)) { ... }

async/await

async function() {
  await doAsyncSomthing()
    .catch() { ... }; // プロミスだから普通に catch できる
}

Lazy Getters

従来の普通の Lazy Getters は strict モードでは getter only プロパティへの代入ができないためエラーになる

{
  get lazyProperty() {
    delete this.lazyProperty;
    return this.lazyProperty = somethingHeavyToLoad();
  }
}

従って class 定義などで使いたい場合はこんな感じ:

class MyPoorClass {
  get lazyProperty() {
    Object.defineProperty(this, 'lazyProperty', {
      value: somethingHeavyToLoad(),
    });
    return this.lazyProperty;
  }
}

Utility Functions

sleep

function sleep(msec) {
  return new Promise(resolve => setTimeout(resolve, msec));
}

addEventListener が callback に処理を戻すまで await する (既に発生済みにイベントを待ち受けない場合に限る)

let event = await new Promise((resolve) => {
    someEventTarget.addEventListener('eventName', resolve, { once: true });
  });

ready (open etc) イベントで callback 実行するイベントリスナを await する (既に ready なら即座に、まだなら待ってから実行したい場合)

let event = await new Promise((resolve) => {
    if (someEventTarget.readyState === 'READY') {
      resolve(someEventObject);
    }
    else {
      someEventTarget.addEventListener('ready', resolve, { once: true });
    }
  });

文字列が Blob で受信されたときに文字列に戻す

/**
 * Convert Blob to String. Please use like:
 * 
 * let str = await readBlobString(blob);
 * @param  {Blob} blob A blob to be converted to string.
 * @returns {Promise<String>} A promise that returns a converted string.
 */
async function readBlobAsString(blob) {
  const reader = new FileReader();
  await new Promise((resolve) => {
    reader.addEventListener('loadend', resolve, { once: true });
    reader.readAsArrayBuffer(blob);
  });
  const buf = reader.result; // contents of blob as a typed array
  // TODO: currently only support ASCII, need to support utf-8...
  return String.fromCharCode.apply(null, new Uint8Array(buf));
}

Fluent で定義した文字列ラベルを読み込む

const defaultFtlFile = './ftl/messages.ftl';
const defaultLocale = 'ja';
/**
 * Generate gettext like function which return UI text lable defined in the FTL file.
 * When prefix speficied, lookup for both the key with/without prefix.
 * @param  {String} ftlfile
 * @param  {String} prefix
 * @param  {String|Array} locales
 */
async function generateGetText(ftlfile = defaultFtlFile, prefix = '', locales = defaultLocale) {
  const response = await fetch(ftlfile);
  const messageSourse = await response.text();

  const messageContext = new Fluent.MessageContext(locales);
  const errors = messageContext.addMessages(messageSourse);
  if (errors.length) {
    // in case the .ftl file have syntax error, show them
    console.log(errors);
  }
  /**
   * Gettext like function which return UI text label defined in the FTL file.
   * @param   {String} key
   * @returns {String}
   */
  // return key => messageContext.getMessage(`${prefix}${key}`);
  return (key) => {
    let message = messageContext.getMessage(`${prefix}${key}`);
    if (!message) {
      message = messageContext.getMessage(key);
    }
    return message;
  };
}

Debug

JavaScript モジュール

  • Firefox 59 時点ではデフォルト無効なので有効化: dom.moduleScripts.enabled = true
  • <script src="..." type="module"> を呼び出し側の方の js 読み込みに使用する。読み込まれるモジュール側は script タグでは読み込まないことに注意。
  • モジュールのパスは js ファイル同士の相対パスでしていするのであって html に対する相対パスではないことに注意。

分割代入

  • 右辺が Object ではないものに対して分割代入しようとすると next() メソッドがないとか Iterable じゃないといった類いのエラーが出ることがあるので要注意

forEach など配列のメソッドと async await

次のように forEach に渡す関数を async にしてその中で await をしてもその関数の呼び出し元が await をせずに呼び出してくるので配列の次の要素の処理までに待ち時間を入れることができないことに注意

// このような書き方では sleep は意味がなく一気に処理される
arr.forEach(async (item) => {
  console.log(item); // 何か要素毎の処理
  await sleep(1000);
});

// async な配列要素処理メソッドは存在しないため要素の処理毎に待ち時間を入れたい場合は for ループ使う
(async () => {
  for (let i=0; i < arr.length; i++) {
    console.log(arr[i]); // 何か要素毎の処理
    await sleep(1000);
  }
})();

非同期処理

コールバック関数を受け取る独自設計をするのは過去の遺物。イベントベースの非同期処理はイベントリスナを利用し、それ以外の非同期処理はプロミスにする。

イベントリスナ

  • イベントリスナの利点、向いている処理
    • 任意の回数任意のタイミングで発生するイベントの処理に適している
    • 一度だけ実行したいとか複数回実行したいとか、同じイベントに色々結びつけたいとか統一的な方法で扱えて便利
  • イベントリスナの欠点、向いていない処理 イベントリスナ登録前に発生したイベントのキャッチができない。
    • 例えば WebSocket の open イベントのようにオブジェクトの準備が整い次第実行したい処理はその準備ができているかを確認して、準備できていたら即時実行、できていなければイベントリスナを登録するという使い方が必要
      if (something.readyState === 'READY') {
        do nextthing();
      } else {
        something.addEventListener('ready', () => {
          do nextthing();
        }, { once: true });
      }
      
    • イベント登録するオブジェクトを何らかのタイミングで破棄、再生性する必要があるときの対応が面倒 (そのオブジェクトに登録したすべてのイベントを再登録する必要があるが登録されているリスナ一覧を取得する API が存在しない)
      • 例えば WebSocket が切断された場合に再接続する場合、以前のインスタンスに登録していたイベントリスナをすべて何処かにイベントリスナに登録したメソッドの一覧を保持しておかないと再登録ができない。初期化処理側ですべて一括するか、配列の WeakMap で持っておくかなど工夫が必要。
    • エセイベントリスナ実装が結構あるので注意が必要。
      • 例えば jQuery の on などは同じイベントリスナを登録するとそれが 2 度呼ばれるなど、重複管理が不適切
      • readyState の実装も含めていないとイベント登録時に準備が済んでいるかどうかに応じた呼び分けが不可能になる

プロミス

  • プロミスの利点、向いている処理
    • 非同期処理と同期処理を統一的に扱いたい場合
    • インデントが減って書きやすい読みやすいことも良いが、何よりもエラーハンドリングを統一的な方法で扱えることが重要
    • 初期化が済んでいるかどうかにかかわらず初期化完了したら実行したい場合などに迷わず使える (then はその時点で既に Promise が resolved になっていても呼ばれる)
    • async/await と組み合わせてコードが非常に書きやすく読みやすくなる
  • プロミスの欠点、向いていない処理
    • うっかりプロミスを返す処理を await などせずに素通ししてしまうと try/catch し損ねる
    • プロミスは一度解決してしまうと再度 then/catch が呼ばれることがない
  • プロミス利用時の注意
    • Promise.all は渡すプロミス全てが解決すればその値のリストが得られるが一つでもリジェクトされれば即実行停止 (fail fast) され他の解決した値を取得できない all or nothing な実装であることに注意

async/await と throw/catch

次のように await せずに anotherAsync を呼ぶとその中で投げられた例外はキャッチされない。これは async 関数の呼び出しは非同期的なものであり、await で呼び出さない限り anotherAsync 内の例外が投げられる前に someAsync 関数の処理を最後まで完了してその返値となる Promise は resolved 状態に移行するため。 someAsync.catch でキャッチしたいのであれば someAsync の実行完了前に例外を投げる必要があり、呼び出し先の非同期関数での例外をキャッチしたいのであれば oneMoreAsync のように await 付きで呼び出す必要がある。

async function anotherAsync() {
  throw new Error("error in anotherAsync");
}
async function oneMoreAsync() {
  throw new Error("error in oneMoreAsync");
}
async function someAsync() {
  anotherAsync();
  await oneMoreAsync()
}
someAsync()
  .catch(e => {
    console.log(e);
  })

同様に、例外を投げる再帰呼び出し async 関数では自己呼び出し時に await を付けなければ再帰呼び出し時の例外をキャッチできなくなる

async function recursiveAsync(c = 0) {
  if (c < 3) {
    // await を付けなければ再帰呼び出し先からの例外をキャッチし損なうので注意
    await recursiveAsync(c + 1);
  } else {
    throw Error(c);
  }
}
recursiveAsync().catch(e => console.log(e));

async/await と throw/catch 利用時の注意

await は Promise が解決されるまで待機して resolved の値を取得するが、reject されてしまった場合は catch しなければそこでコードが実行停止されてしまうし resolved = await asyncFunc() と代入しようとしていた変数 resolved は何も代入されずに undefined となってしまうことに注意。

以下、async 関数または Promise を await する場合の挙動を示すコードサンプル

willResolve = async () => {
  return "resolved";
}
willReject = async () => {
  throw new Error("rejected");
}
willResolveExecutor = (resolve, reject) => {
  resolve("resolved");
}
willRejectExecutor = (resolve, reject) => {
  reject(new Error("rejected"));
}

(async () => {
  let resolved = await willResolve().catch(() => {
    console.log(1, "catched"); // 呼ばれない
  });
  console.info(2, resolved); // 2 resolved
  
  resolved = await willReject().catch(() => {
    console.log(3, "catched"); // 3 catched
  });
  console.info(4, resolved); // 4 undefined

  resolved = await new Promise(willResolveExecutor).catch(() => {
    console.log(5, "catched"); // 呼ばれない
  });
  console.info(6, resolved); // 6 resolved

  resolved = await new Promise(willRejectExecutor).catch(() => {
    console.log(7, "catched"); // 7 catched
  });
  console.info(8, resolved); // 8 undefined

  resolved = await new Promise(willRejectExecutor); // Error: rejected (キャッチされない)
  console.info(9, resolved); // これ以降のコードは実行されない

  alert("never executed"); // 例外キャッチしなかったので実行されない
})();

Cross Origin Fetch

  • 単純な GET/POST リクエストではその応答に Access-Control-Allow-Origin: * レスポンスヘッダが必要。
  • Content-Type を指定するなど「単純・典型的ではない」リクエストをするときはプリフライトとして OPTIONS リクエストが飛び、その応答のレスポンスヘッダに以下のようなヘッダがなければ 400 Bad Request などとなってしまう。
    Access-Control-Allow-Origin: *
    Access-Control-Allow-Methods: GET, POST
    Access-Control-Allow-Headers: Content-Type
    
  • CORS やプリフライトについて:
  • node で CORS 対応サーバ作るなら https://www.npmjs.com/package/cors 使う

WebSocket

  • socket.ReadyState が OPEN になる前に socket.send() とかするとエラーメッセージなしで処理をそこで終了してしまう。readyState 確認して送信する Wrapper を用意しておくと呼び出しタイミングが早すぎても大丈夫:
if (socket.readyState === WebSocket.OPEN) {
  socket.send(JSON.stringify(data));
} else {
  socket.addEventListener('open', () => {
    socket.send(JSON.stringify(data));
  }, { once: true });
}
  • ブラウザ間のメッセージ中継とか自動再接続とかしたくなったら素の WebSocket を使わず socket.io を使って解決する。

WebRTC

  • 確認ダイアログを消したければ: media.navigator.permission.disabled = true
  • Windows Firefox での WebRTC Indicator を消す方法は用意されていない。userChrome.css で制御する:
    #webrtcIndictor {
      display: none !important;
      border: none !important;
    }
    #firefoxButton, #audioVideoButton, #audioVideoPopup, #shareSeparator, #screenShareButton {
      display: none !important;
      width: 0px !important;
      height: 0px !important;
      opacity: 0.5;
      background-color: transparent !important;
      background-image: none !important;
      border: none !important;
    }