term タグ別の記事一覧

GW引き篭もりチャレンジ:React+Electronでアプリを作ってみた総括

 5連休も最終日となりましたが、あっという間でしたね。ということで本日は5日間分のReactアプリ作成レポートをまとめておりました。普段の仕事と変わらないことをやっていたような気もしますが、何もできない連休だったので仕方がないとも言えます。

 Reactのフル機能に触ったわけではないですが、さすがによくできているなと感じました。GUIアプリケーションの構築においてはコンポーネントごとにクラス化するというのが定番のアプローチなのですが、それをうまくJavaScriptの流儀に落とし込めていると思います。開発環境が簡単に構築でき、またコマンド1つで最終的なHTMLやminifyされたJavaScriptコードを出力できるのも便利でした。ただ、そのバックエンドはブラックボックス化されている(Webpackの設定ファイルなどはまったく触れない)ので、ここから外れた使い方をしようとすると面倒なのかもしれません。

 そして、React+Electronの開発が以外に簡単でリリース版パッケージの作成も以外に容易にできるというのはちょっと驚きでした。今回はWebアプリを作ってそこからElectronアプリを作るという流れで作ったために若干無駄な作業が発生してはいますが、最初からElectronアプリを作ることを想定すればもっと迅速な開発ができるのではないかと思いました。

 なお連休は終わりますが、この電子書籍管理アプリについては普通に機能を追加していきたいのでゆったりと開発を続ける予定です。まずはタグやタイトルの管理機能を実装したいですね。

Reactでアプリを作ってみる(5日目) - ダブルクリックでファイルを開く

 本来はアプリ内で電子書籍ファイルを開けると良いのですが、ちゃんとした電子書籍ビューアを作るのは正直大変なので、とりあえずサムネイル画像をダブルクリックすると外部アプリケーションでそのファイルを開くようにしてみます。

Node.jsプログラムからファイルを指定したアプリで開く

 Node.jsではchild_processモジュールのexec()やexec()関数を利用することでファイルを実行できる。ただしmacOS環境では/usr/bin/openコマンドを経由してファイルを開く方が楽である。このコマンドでは、次のように指定することで指定したアプリで指定したファイルを開くことができる。

open <ファイル> -a <アプリの絶対パス>

 また、「-a <アプリの絶対パス>」を指定しないと、そのファイルに紐づけられているデフォルトのアプリでファイルが開く。当然ながらこれはmacOS環境でしか動作しないので、Linux/Windows環境では普通にアプリケーションのバイナリファイルに引数として開きたいファイルを与えて実行すれば良いと思われる

 これを利用して、ebmgr.jsに次のようにopenBook()メソッドを追加した。

function openBook(vpath) {
  // check given path
  const realPath = vpathToRealPath(vpath);
  if (!realPath) {
    return Promise.reject();
  }

  const openCmd = '/usr/bin/open';
  const args = [ realPath ];
  const opts = {};
  const viewer = getViewer(vpath);

  if (viewer && viewer.length) {
    args.push('-a');
    args.push(viewer);
  }

  return new Promise((resolve, reject) => {
    child_process.execFile(openCmd, args, opts,
                           (error, stdout, stderr) => {
                             if (error) {
                               reject(error);
                               return;
                             }
                             resolve();
                           });
  });
};

 getViewer()は電子書籍ファイルの拡張子を元に開くアプリのパスを返す関数で、今回の実行は単にconfigファイルのviewersパラメータを参照して対応する値を返すだけ。

function getViewer(vpath) {
  const ext = path.extname(vpath).toLowerCase();
  const viewers = config.viewers;
  return viewers[ext];
}

 config.jsonではこんな感じに指定している。この場合、ZIP形式ファイルは「/Applications/cooViewer.app」で開かれることになる。

  "viewers": {
    ".zip": "/Applications/cooViewer.app",
    ".cbz": "/Applications/cooViewer.app"
  }

 このメソッドをReactアプリから呼び出せるよう、ipc-client/jsにopenBook()メソッドを追加。

  openBook(path) {
    return this.sendRequest('openBook', path)
      .then(result => {
        return result;
      });
  }

 なお今回Webアプリ版の方はこのメソッドを実装しない。OpenAPIクライアント(openapi-client.js)でこれを実行すると必ず失敗する。

  openBook(path) {
    return Promise.reject("openBook() is not implemented");
  }

Reactでのダブルクリックの扱い

 Webアプリで要素のダブルクリックに対しアクションを取るにはdblclickイベントを使うのだが、ReactではonDoubleClickという属性を定義することでイベントハンドラを定義する。このイベントはThumbnailオブジェクトで発生させたいので、このクラス内にイベントハンドラを記述する。

 まずはrender()で返すHTMLのimgタグ内でこの属性を定義する。

  render() {
    if (this.state.thumbnail) {
      const b64Data = this.state.thumbnail;
      return (
          <div className="Thumbnail">
          <img className="thumbnail" alt={this.props.item.title} src={b64Data}
               onDoubleClick={() => this.onDoubleClickThumbnail()}/>
          </div>
      );
    } else {
      return (
          <div className="Thumbnail">loading...</div>
      );
    }
  }

 ここではイベントハンドラとしてonDoubleClickThumbnail()を実行する無名関数を指定している。onDoubleClickThumbnail()では次のようにclient経由でopenBookメソッドを実行している。

  onDoubleClickThumbnail() {
    const client = getClient();
    client.openBook(this.props.item.path).catch(error => {
      console.log(error);
    });
  }

 これでサムネイルのダブルクリックに反応してその電子書籍ファイルがconfig.json内で指定したアプリケーションで開かれるようになる。また、非Electron環境ではアプリケーションは開かず、コンソールにエラーメッセージが表示される。

環境に応じてElectronアプリのロード元を変更する

 デバッグ時にいちいちコードを変更してElectronアプリ内でのindex.htmlのロード元を変更するのは面倒臭い。そのため、Electronアプリのmain.js内で「EBM_MODE」環境変数を参照し、この値が「development」ならReactの開発サーバーから取得し、そうでなければローカルファイルを直接読み込むように挙動が変わるよう条件分岐を入れた。


  if (process.env.EBM_MODE == 'development') {
    win.loadURL('http://localhost:3333/index.html');
    win.webContents.openDevTools();
  } else {
    win.loadFile('./public/index.html');
  }

 ついでにdevelopmentモードの場合DevToolsを開くようにもしている。

 5日目はここまで。作業時間は1時間半ほど。

Reactでアプリを作ってみる(4日目) - Electronを使ったアプリ化

 サムネイル画像表示ができたので、本日はこのアプリをアプリフレームワークElectronを使ってスタンドアロンで動作するアプリケーション化してみます。

WebアプリをElectronアプリ化する際の要件

 Electronでは、一般的なWebアプリでは実現できないローカルファイルシステムへのアクセスが可能になる。これを利用することで、Webサーバーを使わずにそのアプリ単独で完結するアプリを開発できる。

 ただ、Electron内で表示される画面はあくまでHTMLベースのものなので、画像を表示させたい場合はURLでそのコンテンツの場所を指定する必要がある。今回のサムネイル画像のような動的に生成した画像を表示させたい場合、選択肢としてはelectronのプロセス内でWebサーバーを立ち上げてそこにHTTPでリクエストを投げて画像を取得するか、どこかにファイルとして保存してそのパスをURLとして指定する、もしくはdata:スキーマのURLを使うという3通りがある。今回は別途Webサーバーを立ち上げたくはないので、data:スキーマを利用することにする。

 また、Electron内で実行するコードからはNode.jsの任意のモジュールが利用できるため、電子書籍ファイル一覧の取得やサムネイルの取得はこれらの処理を実装したebmgr.jsを直接importして利用すれば良いだろうと思っていたのだが、今回使用したcreate-react-appベースの開発環境ではfsなどのモジュールが利用できないように細工がされている模様(https://github.com/facebook/create-react-app/issues/3074)。しょうがないので、これらの処理はElectronのメインプロセスで実行することにする。

データアクセスのためのクラス実装

 Electronではアプリ全体を管理するプロセスと画面表示に関連するプロセスが分離されており、前者をメインプロセス、後者をレンダープロセスと呼ぶ。レンダープロセスとメインプロセスはIPCで簡単に通信ができるので、IPC経由でebmgr.jsに実装されている関数を呼び出せるプロクシクラス(ipc-client.js)を実装し、クライアントからはこのプロクシクラス経由で各種関数を呼び出す設計とした。

 なお、IPCを利用するにはelectronモジュールを読み込む必要があるのだが、非Electron環境ではその動作は失敗する。そのため、次のように条件分岐を入れた上でモジュールを読み込ませる必要がある。

let ipcRenderer;
if (window.require) {
  const electron = window.require('electron');
  ipcRenderer = electron.ipcRenderer;
}

 IPC周りのコードは次のように実装した。メインプロセスに対し、「FunctionCall」というメッセージとともにメソッド名、リクエストID、パラメータを送信すると、その処理結果が「<メソッド名>_<リクエストID>」というメッセージとともに非同期に返される、という流れとなっている。返されるメッセージに対するイベントハンドラは、一回限りのハンドラを登録するonce()メソッドで登録している。

let requestId = 0;
const timeoutMilSec = 30000; // 30 seconds

const channelSend = 'FunctionCall';

export default class IpcClient {
  sendRequest(method, params) {
    requestId++;
    return new Promise((resolve, reject) => {
      ipcRenderer.send(channelSend, method, requestId, params);
      const listener = (event, result, error) => {
        if (error) {
          reject(error);
          return;
        }
        resolve(result);
      };
      const channelRecv = `${method}_${requestId}`;
      ipcRenderer.once(channelRecv, listener);
      setTimeout(() => {
        ipcRenderer.removeListener(channelRecv, listener);
        reject({message: "response too late",
                name: 'TimeOutError'});
      }, timeoutMilSec);
    });
  }

  getBooks() {
    return this.sendRequest('getBooks');
  }

  getBookThumbnail(path) {
    return this.sendRequest('getBookThumbnail', path)
      .then(result => {
        result.data = new Blob([result.data]);
        return result;
      });
  }
}

 この場合、何らかの不具合で結果が返ってこないと永遠にそのハンドラが残り続けてメモリリークが発生するので、タイムアウト時間(30秒)を設定してこの時間内に結果が返ってこない場合失敗したとみなしてイベントハンドラを削除するようにしている。

 また、 getBookThumbnail()内で受け取った画像データをBlobに変換しているが、これはswagger-client経由で同じ処理を実行した場合の動作に合わせるために行なっている。

 メインプロセス側のコードは次のようにした。

const channelSend = 'FunctionCall';
ipcMain.on(channelSend, (event, method, requestId, params) => {
  const channelRecv = `${method}_${requestId}`;
  if (method == 'getBooks') {
    ebmgr.getBooks().then(
      result => {
        event.reply(channelRecv, result);
      },
      error => {
        event.reply(channelRecv, undefined, error);
      }
    );
    return;
  } else if (method == 'getBookThumbnail') {
    ebmgr.getBookThumbnail(params).then(
      result => {
        event.reply(channelRecv, result);
      },
      error => {
        event.reply(channelRecv, undefined, error);
      }
    );
    return;
  }
  event.reply(channelRecv, undefined, { message: "method not found",
                                        name: "InvalidMethodError" });
});

 あわせて、Electron環境ではIPC経由、Web環境ではOpenAPI経由で同一の関数にアクセスできるよう、OpenAPI経由での呼び出しについても同じインターフェイスを持つクラス(OaClient)を実装し、Electron環境かどうかをチェックして適切なクラスのインスタンスを返すgetClient()関数をclient.jsというファイルに実装した。

import OaClient from './openapi-client';
import IpcClient from './ipc-client';

export default function getClient() {
  if (isElectron()) {
    return new IpcClient();
  } else {
    return new OaClient();
  }
}

 これで、getClient()経由でクライアントオブジェクトを取得し、それに対しgetBooks()やgetBookThumbnail()を呼び出すことで、Electron/Webブラウザどちらの環境でも同じコードで処理を記述できるようになる。

 electronのメインプロセスのコード(main.js)はドキュメントのサンプルコード(https://www.electronjs.org/docs/tutorial/first-app#electron-development-in-a-nutshell)ほぼそのままだが、IPC関連のコードを追加しているのと、loadFileの部分を次のように変更している。

win.loadURL('http://localhost:3333/');

 こうすることで、index.htmlファイルをReactのテストサーバーから直接取得できるようになり、開発効率が上がる。

画像をdata:URL経由で表示させる

 まずReactアプリのコンポーネントとして、画像を表示するThumbnailコンポーネントを作成する。Reactのコンポーネントはその属性として任意のオブジェクトを受け取れるようになっており、渡されたオブジェクトはconstructor()の引数として渡される。これを引数にsuper()を実行することで、this.propというプロパティでその値にアクセスできるようになる。

class Thumbnail extends Component {
  constructor (props) {
    super(props);
    this.state = { thumbnail: null,
                 };
  }

 ここでは、「item」という属性として電子書籍ファイルの情報(パス、タイトル)を受け取ることを想定し、そのパスを与えてgetBookThumbnail()メソッドを実行することでサムネイル画像を取得する。この処理は非同期で実行されるので、componentDidMount()メソッド内で実行する。画像を取得できたら、FileReaderオブジェクトを使ってそれをdata: URLに変換してstate.thumbnailに格納し、これをsrc属性に指定したimg要素を出力させる。

  componentDidMount() {
    const client = getClient();
    client.getBookThumbnail(this.props.item.path).then(
      result => {
        //console.log(result);
        const reader = new FileReader();
        reader.readAsDataURL(result.data);
        reader.addEventListener('load', event => {
          this.setState({thumbnail: reader.result});
        });
      }
    );
  }

  render() {
    if (this.state.thumbnail) {
      const b64Data = this.state.thumbnail;
      return (
          <div className="Thumbnail">
          <img className="thumbnail" alt={this.props.item.title} src={b64Data} />
          </div>
      );
    } else {
      return (
          <div className="Thumbnail">loading...</div>
      );
    }
  }
}

 ThumbnailGridコンポーネントでは、このThumbnailコンポーネントを利用する形にrender()を修正した。

render() {
  const makeThumb = x => {
    const encodedPath = encodeURIComponent(x.path);
    return (
        <li key={x.title}>
        <Thumbnail item={x} />
        </li>
    );
  };
  const listItems = this.state.items.map(makeThumb);
  return (
      <div className="ThumbnailGrid">
      <ul>{listItems}</ul>
      </div>
  );
}

Electronアプリの起動

 Electronを利用したコードは、electronコマンド経由で実行する必要がある。毎回手で打ち込むのは面倒なので、Makefileを作成した。

run:
	./node_modules/.bin/electron main.js

 これで試しにアプリを実行したところ、サムネイル変換処理が終わるまでアプリが固まることが判明。ghostscriptの実行を同期的に行なっているのが原因と思われる。ということで、ebmgr.js内でexportしている関数はすべてPromiseを返す非同期関数に書き換える。

function getZipThumbnail(vpath) {
  const realPath = vpathToRealPath(vpath);
  const zip = new AdmZip(realPath);
  const rex = /(\.jpeg|\.jpg)$/;

  for (const entry of zip.getEntries()) {
    if (!entry.isDirectory && rex.test(entry.entryName)) {
      console.log(entry.entryName);
      const data = zip.readFile(entry);
      return Promise.resolve({ contentType: 'image/jpeg',
                               data: data });
    }
  }
  return Promise.reject();
}

 ZIPファイルの処理をするAdmZipは非同期処理をサポートしていないようなので単にPromiseでラップした結果を返すだけ。PDFの処理をしているghostscript4jsは非同期に処理を実行するexecuteメソッドを提供しているので、それをPromiseでラップしている。

  const gsCmd = [ "-sDEVICE=jpeg",
                  "-o",
                  destination,
                  "-sDEVICE=jpeg",
                  "-r72",
                  `-dFirstPage=${page}`,
                  `-dLastPage=${page}`,
                   "-q",
                  realPath];

  return new Promise((resolve, reject) => {
    gs.execute(gsCmd, err => {
      if (err) {
        reject(err);
        return;
      }
      resolve(destination);
    });
  });

 getPdfThumbnail()はエラー処理をサボっているので、async funtion化でPromiseを返すようにした。Node.jsではfs.promises以下にPromiseを返すメソッドが用意されているのでこれを活用している。

async function getPdfThumbnail(vpath, page) {
  const pathname = await makeTempFile(vpath, ".jpeg");
  await generatePdfThumbnail(vpath, pathname, page);

  const data = await fs.promises.readFile(pathname);
  await fs.promises.unlink(pathname);

  return { contentType: 'image/jpeg',
           data: data };
}

 これらに対応して、サーバー側でこれらを呼び出しているコードも書き換えておく。

 さて、相変わらず処理は重いものの、これでアプリがサムネイルのロード環境まで固まる現象は解決。ただ、すべての画像が一斉に表示されるという別の問題に気付く。とはいえ現時点ではクリティカルな問題ではないのでToDOに入れて一旦放置。

スタンドアロンなElectronアプリ化

 この状態では、Electronアプリ内で表示するHTMLファイルはAPIサーバー経由で取得しているので、これを静的なHTMLに切り替えることでアプリがスタンドアロンで動作するように変更する。まず、create-react-appでビルドしたアセットディレクトリ(build)のシンボリックリンクをelectronディレクトリ内に「public」という名前で作成し、この中のindex.htmlをElectronアプリ側のmain.js内で次のようにロードさせてみる

  win.loadFile('./public/index.html');

 すると、動かない。これは、create-react-appで作成した環境でビルドして出力されたindex.htmlファイルではJSやCSS、画像などの各種リソースがすべて「/」から始まる絶対パスで書かれているため。ググったところ、これはReactアプリのpackage.jsonに「“homepage”: “."」を追加することで解決できるとあったので(https://github.com/facebook/create-react-app/blob/1d03579f518d2d5dfd3e5678184dd4a7d8544774/docusaurus/docs/deployment.md)、これを試したところ無事解決。

 これでWebサーバーレスでアプリを実行できるようになったので、これらをアプリとしてパッケージングしてみる。基本的にはドキュメント(https://www.electronjs.org/docs/tutorial/application-distribution)で書かれている通りの作業をすることになる。

 これは定期的に実行することになるので、Makefileを書く。

APP_DEST=release
ELECTRON_PREBUILT=electron/electron/dist
ELECTRON_DIR=electron
RES_DIR=$(APP_DEST)/Electron.app/Contents/Resources/app
REACT_DIR=react-app

dist: $(APP_DEST)
        rsync -a $(ELECTRON_PREBUILT)/ $(APP_DEST)
        rsync -a --exclude='*~' \
                 --exclude='ebmgr.js' \
                 --exclude='Makefile' \
                 --exclude='electron' \
                 --exclude='public' \
                 $(ELECTRON_DIR)/ $(RES_DIR)/
        cp -a ebmgr.js config.json $(RES_DIR)/
        rsync -a $(REACT_DIR)/build/ $(RES_DIR)/public/

$(APP_DEST):
        mkdir -p $(APP_DEST)

 また、Electronアプリ内でghostscript4jsやadm-zipモジュールを利用できるよう、package.jsonファイルに依存性を追加しておく。

  "dependencies": {
    "adm-zip": "^0.4.14",
    "electron": "^8.2.5",
    "ghostscript4js": "^3.2.1"
  },

 これでElectronのアイコンをダブルクリックすると起動するアプリ化完了。

 デバッグ中なのでDevToolsが表示されているが、これはElectronアプリのmain.js内の次のコードを削除すれば表示されなくなる。

  win.webContents.openDevTools();

 本日はここまで。作業時間はちょっと使ってしまって5時間ほど。

Reactでアプリを作ってみる(3日目) - コンテンツの動的な表示

 GWなのでReactで電子書籍管理アプリを作ってみようという話の3日目です。

PDFからのサムネイル画像の作成と読み込み

 2日目の課題として残っていたサムネイル画像をどうやって扱うか問題だが、とりあえずテンポラリディレクトリ上に一時的にサムネイル画像ファイルを生成し、その直後にそのファイルを読み込んだ後削除することで、ローカルファイルシステム上にサムネイルを保持させない方針にしてみる。この時ファイル名に気をつけないと、実行のタイミングによっては一時的に作った画像ファイル名が衝突して意図しないものにすり替わるというトラブルが発生する可能性があるため、作成する画像ファイル名は電子書籍ファイルのパス名のハッシュを用いて生成するようにする。

function makeTempFile(vpath, ext) {
  const tmpdir = os.tmpdir();
  let pathname = path.join(tmpdir, getHash(vpath, 'sha256') + ext);

  let done = false;
  let n = 0;
  while (!done) {
    try {
      fs.writeFileSync(pathname, "", {flag: 'wx'});
      done = true;
    }
    catch (e) {
      pathname = path.join(tmpdir, getHash(vpath + String(n), 'sha256') + ext);
      n++;
    }
  }
  return pathname;
}

 ファイル名の重複を確実に回避するため、空ファイルを作成しておいて、その後Ghostscriptでそのファイルを上書きするという方針にしている。ファイル作成時に「wx」オプションを指定しているので、同名のファイルが存在した場合ファイル作成が失敗する→それを検知して別のファイル名を作成して再度ファイル作成を試みる、を繰り返すというアルゴリズムになっている。

 また、変換処理の実行時にメッセージが表示されてログが汚れるので「-q」オプションを追加して進捗メッセージは非表示にする。オプション配列の後ろの方にこのオプションをつけないとなぜか有効に動かない模様でちょっとハマる。

  const gsCmd = [ "-sDEVICE=jpeg",
                  "-o",
                  destination,
                  "-sDEVICE=jpeg",
                  "-r72",
                  `-dFirstPage=${page}`,
                  `-dLastPage=${page}`,
                   "-q",
                  realPath];

 なお、ここまでスルーしていたが、このアプリ内では電子書籍のパスを仮想パス(vpath)で扱っている。これは設定ファイルで指定した電子書籍ファイルが格納されているディレクトリの絶対パスをMD5でハッシュ化し、そのハッシュ+個別のファイルの相対パスに相当する。うっかりミスでファイルシステム内全体にアクセスできるようになってしまう可能性を減らすためにこのような仕様にしている。

 最終的にPDFファイルのサムネイルを取得する関数は次のような形となった。将来的にJPEG以外の形式でサムネイルを作成する可能性も考慮して一緒にcontentTypeも返している。

function getPdfThumbnail(vpath, page) {
  const pathname = makeTempFile(vpath, ".jpeg");
  generatePdfThumbnail(vpath, pathname, page);

  const data = fs.readFileSync(pathname);
  fs.unlinkSync(pathname);

  return { contentType: 'image/jpeg',
           data: data };
}

OpenAPIのサービス仕様定義の修正とAPIサーバーの修正

 続いては、電子書籍ファイルの仮想パスを指定すると、それに対応したサムネイル画像を返すAPIをAPサーバーに追加する。まずはサービス仕様ファイルにこのAPIを記述する。これは指定された電子書籍ファイルが見つかればimage/jpeg形式で対応する画像を返し、そうでない場合は404を返すというもの。

paths:
  /books/{vpath}/thumbnail:
    get:
      x-swagger-router-controller: Default
      description: Returns a thumbnail of the book
      operationId: getBookThumbnail
      parameters:
        - in: path
          name: vpath
          schema:
            type: string
          required: true
          description: virtual path of the book
      responses:
        "404":
          description: A book with the given vpath was not found
        "200":
          description: A thumbnail of book
          content:
            image/jpeg:
              schema:
                type: string
                format: binary

 この定義ファイルをSwagger Editorに読み込ませて再度サーバーコードを生成してみたのだが、ロジックを毎回再生成されるディレクトリツリーから分離した場所に記述するのが面倒そうな感じだったため、すでに実装しているサーバーコードに手動で追加したパスに対する処理を定義する。といっても編集が必要なのはservice/DefaultService.jsとcontrollers/Default.js、api/openapi.yaml(サービス仕様定義ファイル)の3つのみ。最後のサービス仕様定義ファイルについてはマスターとしている親ディレクトリのもののシンボリックに置き換えておいた。

 まず、service/DefaultService.js内にロジックに相当するコードを記述。このコードは既存コードを参考にしてPromiseを返すようにしている。

/**
 * Returns a thumbnail of the book
 *
 * vpath String virtual path of the book
 * returns byte[]
 **/
exports.getBookThumbnail = function(vpath) {
  return new Promise(function(resolve, reject) {
    const thumb = ebmgr.getThumbnail(vpath);
    if (thumb) {
      resolve(thumb);
    } else {
      resolve();
    }
  });
}

 続いていわゆるコントローラに相当する処理をcontrollers/Default.jsに書く。ここにはHTTPヘッダやレスポンスを返すというWebサーバー的に必要な処理を記述する。このサーバーはexpressjsを使っているので、expressjsの流儀に従ってレスポンス/リクエストオブジェクト経由でレスポンスを返す処理を記述すればOK。

module.exports.getBookThumbnail = function getBookThumbnail (req, res, next, vpath) {
  Default.getBookThumbnail(vpath)
    .then(function (thumb) {
      if (!thumb) {
        // resource not found
        res.writeHead(404);
        res.end();
        return;
      }
      res.writeHead(200, {'Content-Type': thumb.contentType });
      res.end(thumb.data);
    })
    .catch(function (response) {
      res.writeHead(500);
      res.end();
    });
};

 これでAPIサーバーを起動して/docs以下にアクセスしてSwaggerでテストする。問題なく動作したので続いてクライアント側のコードを記述する。

Reactアプリのコード更新

 OpenAPIクライアント経由でgetBooksメソッドを実行して取得した電子書籍リストでは、pathプロパティでその電子書籍ファイルの仮想パスが与えられている。API仕様では/api/v1/books/{URIエンコードされた仮想パス}/thumbnailというパスに対しGETメソッドを投げるとサムネイル画像がimage/jpegというContent-Typeで返ってくるようになっているので、ThumbnailGrid.jsファイル内のrender()メソッドが返すHTML内のimgタグのsrc属性でこのURLを指定する。Reactのテンプレート(JSX)内で文字列連結をする場合、{}の中でJavaScriptコードとして連結処理を記述しなければならないのにちょっとハマる。

render() {
  const makeThumb = x => {
    const encodedPath = encodeURIComponent(x.path);
    return (
        <li key={x.title}>
        <img class="thumbnail" src={"/api/v1/books/" + encodedPath + "/thumbnail"} />
        </li>
    );
  };
  const listItems = this.state.items.map(makeThumb);
  return (
      <div className="ThumbnailGrid">
      <ul>{listItems}</ul>
      </div>
  );
}

 これでReactのテストサーバーを起動してテスト。ちゃんとサムネイルは表示される。

 ただ、画像が表示されるまでちょっと待たされる感じになっている。ログを見るとこんな感じ。

GET /api/v1/books/0f276cdfc8a2c99c988ef7b88141f377%2FOP%E3%82%A2%E3%83%B3%E3%83%95%E3%82%9A%2F6_1-4_Analog_Filteres.pdf/thumbnail 200 136.203 ms - -
   **** Error reading a content stream. The page may be incomplete.
               Output may be incorrect.

   **** Error: File has unbalanced q/Q operators (too many Q's)
               Output may be incorrect.
   **** Error: File did not complete the page properly and may be damaged.
               Output may be incorrect.
GET /api/v1/books/0f276cdfc8a2c99c988ef7b88141f377%2FOP%E3%82%A2%E3%83%B3%E3%83%95%E3%82%9A%2F6_5-8_Analog_Filteres.pdf/thumbnail 200 202.495 ms - -

 ここから、サムネイル生成処理には1つあたりおおむね数百ミリ秒ほどかかっていることが分かる。さすがにちょっとUI的に重いが、とりあえずここの部分の最適化はToDoに突っ込んで置いてまたの機会に。ghostscriptのエラーメッセージについては、画像自体はちゃんと表示されているので現時点では無視。

ZIP形式の電子書籍ファイルからのサムネイル画像取得

 ZIP形式の電子書籍ファイルについても、同じようにサムネイル画像を表示したい。アルゴリズム的にはZIPファイル内のファイルをスキャンして最初に見つかったjpegファイルを取り出せばOKだろう。モジュールとしてはJSZip(https://www.npmjs.com/package/jszip)とADM-ZIP(https://www.npmjs.com/package/adm-zip)というものが見つかった。利用者数はどちらも十分に多く、どっちを選択しても問題なさそうだが、ADM-ZIPはほかに依存するモジュールがない(Dependenciesが0)という点が気に入ったのでそちらを選択。

 サムネイル取得関連のメソッドを整理し、ZIP用とPDF用に分割。すんなりとZIPファイル内から最初の画像ファイルを取得するコードを実装できた(コード全文)。

function getZipThumbnail(vpath) {
  const realPath = vpathToRealPath(vpath);
  const zip = new AdmZip(realPath);
  const rex = /(\.jpeg|\.jpg)$/;

  for (const entry of zip.getEntries()) {
    if (!entry.isDirectory && rex.test(entry.entryName)) {
      console.log(entry.entryName);
      const data = zip.readFile(entry);
      return { contentType: 'image/jpeg',
               data: data };
    }
  }
}

 getThumbnail()関数では拡張子に応じてgetPdfThumbnail()もしくはgetZipThumbnail()を呼び出すことで適切にサムネイルを生成できるようにする。

exports.getThumbnail = getThumbnail;
function getThumbnail(vpath, page) {
  // check given path
  const realPath = vpathToRealPath(vpath);
  if (!realPath) {
    return undefined;
  }

  const ext = path.extname(realPath).toLowerCase();
  if (ext == ".pdf") {
    page = page || 1;
    return getPdfThumbnail(vpath, page);
  }
  if (ext == ".zip" || ext == ".cbz") {
    return getZipThumbnail(vpath);
  }
};

 これでZIP圧縮形式の電子書籍ファイルのサムネイルもとりあえず表示できるようになった。

開発サーバーを使わないアプリ実行(Reactアプリのデプロイ)

 まずはサムネイル画像表示までを実現できたので、ここでReactの開発サーバー外でクライアントを動作できることを確認する。Reactアプリのディレクトリで「npm run build」コマンドを実行すると、アセットがビルドされてリリース用のコードが作成され、buildディレクトリ内にそれら一式が出力される。このディレクトリ内のファイルをWebサーバーで公開することで、開発用サーバーを使わずにアプリを実行できる。

 今回はAPIサーバーのディレクトリ内にReactアプリディレクトリ内のbuildディレクトリへのシンボリックリンクを「public」という名前で作成する。続いてAPIサーバーのindex.jsを修正し、EBM_MODE環境変数の値によってプロキシの有効/無効を切り替えるように変更。これでEBM_MODE環境変数がdevelopment以外の場合、buildディレクトリ内のコンテンツが/以下で公開されるようになる。

// check running environment
const mode = process.env.EBM_MODE || 'development';

if (mode == 'development') {
  // add routes for React
  app.use(/^\/(?!(docs|api)\/).*/, createProxyMiddleware({ target: 'http://localhost:3000', changeOrigin: true }));
} else {
  app.use(express.static('public'));
}

 動作チェックをすると、問題なく動く。Reactアプリはデプロイが面倒そうな先入観があったのだが、意外に簡単だった。ただしnpm run buildコマンドはそれなりに実行に時間がかかるので頻繁に実行することは想定しないほうが良い感じ。

 ということで本日はここまで。作業時間は4時間ほど。

Reactでアプリを作ってみる(2日目) - PDFからのサムネイル生成

 前から触ってみたかったReactを使って、前から欲しいと思っていた電子書籍管理アプリを作ってみたレポート2日目です。今日はサムネイル画像生成の話でReactは全然関係ありません。

 電子書籍ファイルのサムネイルを表示させるために、表紙(つまり電子書籍の1ページ目)の画像の生成が必要となる。PDFファイルの場合、なんらかのツールを使って変換処理が必要となる。

 Node.js向けにはpdf-image(https://www.npmjs.com/package/pdf-image)やpdf2pic(https://www.npmjs.com/package/pdf2pic)というPDFを扱えるモジュールがあるが、どちらもimagemagick(graphicsmagick)を使うようだ。しかし、imagemagickはインストールが面倒臭い上に脆弱性が度々発見されていたりするため、あまり使いたくない。結局PDFのラスタライズ機能だけがあれば良い訳で、調べたところGhostscriptライブラリ(libgs)のJavaScriptバインディングであるghostscript4js(https://github.com/NickNaso/ghostscript4js)というものがあり、これを使えば直接GhostscriptをNode.jsアプリから呼び出してPDFを画像化できるようだ。

 なお、GhostscriptのmacOS向けバイナリは公式には公開されていないのでインストールが若干面倒。これについては別記事で書いた。

 実際にこれを使ってPDFからJPEG形式のサムネイル画像ファイルを生成するgenerateThumbnail()というメソッドを実装してみた(コード全文はこちら)。とりあえずこれでサムネイル画像の生成は可能になった。

const gs = require('ghostscript4js');

exports.generateThumbnail = function generateThumbnail(vpath, page) {
  page = page || 1;
  const realPath = _vpathToRealPath(vpath);
  const target = path.parse(realPath);
  const outputPath = path.join(target.dir, target.name + '.jpeg');

  const gsCmd = [ "-sDEVICE=jpeg",
                  "-o",
                  outputPath,
                  "-sDEVICE=jpeg",
                  "-r72",
                  `-dFirstPage=${page}`,
                  `-dLastPage=${page}`,
                  realPath];

  gs.executeSync(gsCmd);
  return outputPath;
};

 ghostscript4jsのドキュメントには書いていないが、gs.executeCmdメソッドに渡すGhostscriptのオプションは単なる文字列だけでなく、このように文字列の配列形式で与えることも可能。パス名にスペースを含むファイル名を処理する場合はこのように配列形式で与えないとうまく処理できないようだ。また、"-sDEVICE=jpeg"が複数回登場しているが、なぜかこのように重複して指定しないとうまくファイルが生成されなかった。

 さて、この実装だと電子書籍ファイルと同じディレクトリにサムネイル画像を生成することになる。そのあたりの仕様をどうするか検討する必要がある。

 2日目は(Ghostscriptのビルドでちょっと時間を食ってしまったので)ここまで。作業時間は3時間ほど。

GW引き篭もりチャレンジ:Reactでアプリを作ってみる(1日目)

 世間は5連休ですが、特に出かけることもできず、日曜大工や電子工作の材料も入手ができずと普段休日にやっていることをほぼ封じられている毎日です。しょうがないので、前から触ってみたかったReactを使って、前から欲しいと思っていた電子書籍管理アプリを作ってみたレポートです。

 仕事ではないので、特に事前調査などをせずに行き当たりばったりでコードを書いていきます。また、このレポート記事も(仕事ではないので)Twitter並みに雑にやった作業を書いていきます(ちなみに、記事を書いているのはリアルタイムではありません。メモ書きはリアルタイムでやっていますが、整理・校正は5日目にまとめてやっています)。

 さて、作成するアプリの要件は次のとおり。

  • いわゆる「自炊」で作成した電子書籍ファイルを管理する
  • 電子書籍ファイルの形式はPDFと画像が入ったフォルダをZIP形式で圧縮したものの2種類
  • 指定したディレクトリ内の電子書籍ファイルの表紙(1ページ目)のサムネイルをタイル状に表示する
  • サムネイルをダブルクリックするとビューアが起動してそのファイルを開く

 将来的にメタデータによる管理などもできると良いのだが、まずは動くものを実装することと、Reactの全体像やアーキテクチャを掴むことを目標にする。そのため、パフォーマンスについてはあまり気にしない。ただ、ある程度正しいアーキテクチャというものは意識してコードを書いていく。

開発環境の構築&Reactの概要を掴む

 まずはReactのチュートリアル(https://reactjs.org/tutorial/tutorial.html)をざっと眺めるとともに、このチュートリアル内の「Setup Option 2: Local Development Environment」を参照して「npx create-react-app <アプリ名>」コマンドで開発環境を作る。Reactの特徴は、表示するコンポーネントをクラスとして定義して、そこにHTMLのテンプレートを直接書き込む点にあるようだ。今回は「ebmgr」という名前でアプリを作る。

$ npx create-react-app ebmgr

 ちなみにnpxコマンドはNode.jsのパッケージマネージャであるnpmコマンドに同梱されているもので、指定されたパッケージのインストールとそこに含まれているコマンドの実行を同時に行うというもの。

 create-react-appで作成されたディレクトリ内のsrcディレクトリ内にソースコードが、publicディレクトリ内に開発用Webによって公開されるindex.htmlや画像、CSSなどが配置されている。

$ ls
README.md		node_modules		package-lock.json	package.json		public			src

$ ls public/
favicon.ico	index.html	logo192.png	logo512.png	manifest.json	robots.txt

$ ls src/
App.css			App.test.js		index.js		serviceWorker.js
App.js			index.css		logo.svg		setupTests.js

 とりあえずチュートリアルの指示に従って「npm start」コマンドを実行してみると、Webブラウザで作成したアプリケーションの雛形が動いた。index.html内にはJavaScriptファイルやCSSファイルを読み込むようなタグは入っていないので、それらはテストサーバーが動的に挿入している模様。

 また、srcディレクトリ以下のApp.jsを見てみると、そこにブラウザで表示されているアプリケーションのHTMLコードが直接記述されている。これを変更して保存すると、リアルタイムでブラウザの画面がそれに追従して更新される。便利。

import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

 ちなみにこのようにJavaScript中に直接HTMLが書き込まれているフォーマットをJSXと呼ぶらしい。

 JSXファイル内ではSVGファイルやCSSファイルをimport文でインポートできるようだ。また、テンプレート内での変数展開は「{」「}」で行える模様。一般的なテンプレートエンジンはforやwhileのようなループ機構を持っているが、JSXはそういった機構は持っていないようなので、たとえばArrayで格納されているテキストを順に表示したいといった場合、単純にJavaScriptコードで次のように展開してやれば良い。

function App() {
  const items = ["foo", "bar", "hoge"];
  const listItems = this.state.items.map(x => <li key={x}>{x}</li>);
  return (
      <div className="ThumbnailGrid">
      <ul>{listItems}</ul>
      </div>
  );
}

 このApp.jsというのは「App」というコンポーネントを定義しているだけで、ページ全体のレンダリングはindex.jsによって行われている模様。コードをみると、このReactDOM.render()メソッドで表示する内容や、HTML内でそれを挿入するDOM要素を指定している。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

 まずは電子書籍のサムネイル画像をグリッド表示させたいので、App.jsをコピーして「ThumbnailGrid.js」というファイルを作る。

import React from 'react';

function ThumbnailGrid() {
  const items = ["foo", "bar", "hoge"];
  const listItems = this.state.items.map(x => <li key={x}>{x}</li>);
  return (
      <div className="ThumbnailGrid">
      <ul>{listItems}</ul>
      </div>
  );
}

export default ThumbnailGrid;

 index.jsを編集し、このThumbnailGrid.jsをインポートした上でと記述されていたものをに差し替えてみる。無事動作。ついでに今回ServiceWorkerは使わないのでそれ関連のコードを削除。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import ThumbnailGrid from './ThumbnailGrid';

ReactDOM.render(
  <React.StrictMode>
    <ThumbnailGrid />
  </React.StrictMode>,
  document.getElementById('root')
);

Reactアプリに表示する情報を取得するためのAPIをOpenAPIで実装する

 ReactではNode.jsのモジュールをインポートできるようだが、さすがにfsなどのモジュールは使えないようだ。そのため、ローカルファイルシステム上にあるファイルの情報などを表示するにはREST APIなどを使ってサーバーと別途非同期通信を行う必要がある。create-react-appで作成した環境では「npm start」コマンドを実行すると開発用サーバーが立ち上がるが、この開発用サーバーはReactアプリを提供する機能しかないため、別途APIを提供するWebサーバーを用意する必要がある。

 最近ではREST APIといえばOpenAPI(https://www.openapis.org)ですよね、ということでOpenAPI Specification(https://swagger.io/specification/)を見ながら電子書籍データのリソースを定義した最低限のサービス仕様ファイルを作成する。ここでは「/books」にGETリクエストを投げることで、タイトルとファイルパスを含む配列を返すというAPIを定義した。

openapi: 3.0.5
info:
  title: ebook manager (ebmgr) App
  description: ebook manager API
  version: 0.1.0
servers:
- url: http://localhost:3333/api/v1
  description: Local server
paths:
  /books:
    get:
      description: Returns all books list
      operationId: getBooks
      responses:
        "200":
          description: A list of books
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Book'
                x-content-type: application/json
      x-swagger-router-controller: Default
components:
  schemas:
    Book:
      type: object
      properties:
        title:
          type: string
          description: title of book
          example: some interesting title
        path:
          type: string
          description: virtualized path of book
          example: /foo/bar/baz
      example:
        title: some interesting title
        path: /foo/bar/baz

 なお、あとでツールを使ってここからサーバーコードを生成するのだが、ここで定義している「operationId」という要素がメソッド名にマッピングされる。省略すると勝手にダサい名前をつけられる可能性があるので気をつけましょう。

 Swagger Editor(https://editor.swagger.io)に作成したサービス仕様ファイルをアップロードすると、ここで作成したスキーマを検証したり、「Generate Server」や「Generate Client」メニューからサーバー/クライアントコードをダウンロードできるようになる。今回はNode.jsでサーバーも書くので「nodejs-server」を選択。するとzip圧縮されたサーバーコードがダウンロードされる。

 api-serverというディレクトリを作ってダウンロードしたzipファイルを展開して、そこで「npm start」コマンドを実行するとindex.jsファイルが実行されAPIサーバーが立ち上がる。なおこのサーバーはexpressを利用して実装されているようだ。標準では3000番ポートで待受をするが、これはReactの開発サーバーとバッティングするので3333番ポートに変更している。

const path = require('path');
const http = require('http');
const oas3Tools = require('oas3-tools');

const serverPort = 3333;

// OAS3 API server
const options = {
  controllers: path.join(__dirname, './controllers')
};
const oasDefinition = path.join(__dirname, 'api/openapi.yaml');
const expressAppConfig = oas3Tools.expressAppConfig(oasDefinition, options);
expressAppConfig.addValidator();
const app = expressAppConfig.getApp();

http.createServer(app).listen(serverPort, function () {
    console.log('Your server is listening on port %d (http://localhost:%d)', serverPort, serverPort);
    console.log('Swagger-ui is available on http://localhost:%d/docs', serverPort);
});

 サービス仕様に記述したパスに対応する処理はservice/DefaultServices.jsに書けば良いようだ。将来的にこのapi-server以下のコードは書き直すことがあるかもしれないので、実際のロジックはこのディレクトリ外のファイル(../../ebmgr.js)に記述する。

 まず必要なのは指定したディレクトリ内にある電子書籍ファイルの情報を返すコードなので、getContents()というメソッドを実装する(コード全文はこちら)。

function getContents() {
  const targetDirs = config.contentDirectories;
  var results = [];
  for (const dir of targetDirs) {
    const r = searchContents(dir);
    results = results.concat(r);
  }
  return results;
}

function searchContents(dirname) {
  const dirnameHash = getHash(dirname);
  const r = [];
  _searchContents(dirname, dirnameHash, r);
  return r;

  function _searchContents(dirname, dirnameHash, results) {
    const dir = fs.readdirSync(dirname, {withFileTypes: true});
    const exts = config.targetExtentions;
    for (const item of dir) {
      if (item.isDirectory()) {
        _searchContents(path.join(dirname, item.name),
                        path.join(dirnameHash, item.name),
                        results);
        continue;
      }
      for (const ext of exts) {
        if (item.name.endsWith(ext)) {
          const metadata = {};
          metadata.title = path.basename(item.name, ext);
          metadata.path = path.join(dirnameHash, item.name);
          results.push(metadata);
          continue;
        }
      }
    }
  }

}

exports.getContents = getContents;

 これを、service/DefaultService.js内で呼び出す。

const ebmgr = require('../../ebmgr.js');
/**
 * Returns all books list
 *
 * returns List
 **/
exports.getBooks = function() {
  return new Promise(function(resolve, reject) {
    const items = ebmgr.getContents();
    resolve(items);
  });
}

 Swagger Editorから生成されたサーバーコードは、デフォルトでSwagger的な機能が有効になるように設定されており、api-serverディレクトリで「npm start」(もしくは「node index.js」)コマンドでサーバーを起動し、Webブラウザでそのサーバーの/docs/以下にアクセスすることで、リソース一覧を確認したり、フォームで引数を指定してリソースにアクセスすることができる。これで試したところ、とりあえず目的のデータは得られているようだ。

 ついでにAPIサーバー(今回は3333番ポートを使用)からReactの開発サーバー(3000番ポートを使用)にアクセスできるようAPIサーバーにリバースプロクシを設定。これでこのAPIサーバーに対する/docs/以下と/api/以下以外のアクセスはすべてReactの開発サーバー(localhost:3000)に転送される(コード全文)。

const { createProxyMiddleware } = require('http-proxy-middleware');

const app = expressAppConfig.getApp();

// add routes for React 
app.use(/^\/(?!(docs|api)\/).*/, createProxyMiddleware({ target: 'http://localhost:3000', changeOrigin: true }));

 続いてはReactアプリ側でAPIを呼び出すコードの実装。Swagger Editorにはクライアントコードを生成する機能もあるのだが、「Generate Client」-「javascript」でダウンロードしたコードをReact内のコードから呼び出そうとしたらWebpackのエラーで動かない。雑にググったところ、「swagger-client」というものを使えという話があったのでこちらをReactアプリのディレクトリ内でnpmコマンドを実行してインストール。

$ npm i swagger-client

 今回は「ThumbnailGrid」コンポーネント内で電子書籍ファイルのサムネイル画像を表示させたいので、このコンポーネントを定義しているThumbnailGrid.js内でswagger-clientをimportする。

import React, {Component} from 'react';
import SwaggerClient from 'swagger-client';

 あとはThumbnailGrid()メソッド内でこのクライアントを使ってgetBooks APIを叩いてデータを取得し、それをHTMLとしてレンダリングすれば良いのだが、このクライアントは結果を非同期で(Promiseを使って)返す仕組みになっている。つまり単純にメソッドの返り値として取得したデータを返すことはできない。ということでググると、コンポーネントをReact.Componentクラスの派生クラスとして定義することで色々非同期に処理ができるようになるようだ。

 コンポーネントのライフサイクルなどはVue.jsでも登場した概念でほぼ同じ。最初にconstructor()が→render()→copmponentDidMount()という順序で実行される。コンポーネント固有のデータはconstructor()内で定義したstateオブジェクトで管理するようだ。ということで、copmponentDidMount()内でswagger-client経由で電子書籍データのgetを実行することにする。

 swagger-clientは初期化時にサービス仕様定義ファイルを与えるようになっているが、こちらはAPIサーバーの/api-docsからGETできる。

componentDidMount() {
  const client = new SwaggerClient('http://localhost:3333/api-docs');
  client.then(c => {
    c.apis.default.getBooks().then(results => {
      this.setState({ items: results.body,
                      loading: false,
                    });
    });
  });
}

 ReactではsetState()メソッドでstateを更新するようになっており、これに応じてrender()メソッドが実行されてコンポーネントの再レンダリングが実行される。UIライブラリではよくある設計ですね。

 ということで、render()メソッドにstateオブジェクトを元にコンテンツをレンダリングする処理を記述する。

render() {
  const listItems = this.state.items.map(x => <li key={x.title}>{x.title}</li>);
  return (
      <div className="ThumbnailGrid">
      <ul>{listItems}</ul>
      </div>
  );
}

 また、constructor()でstateの初期化をしておく。

class ThumbnailGrid extends Component {
  constructor () {
    super();
    this.state = { items: [],
                   loading: true,
                 };
  }

 これでReactの開発サーバーとAPIサーバーを動かして、APIサーバーにアクセスするととりあえずファイルのタイトル(ファイル名)一覧が表示される。

 1日目はここまで。本日の作業時間は4時間ほど。

ECMAScript 2015(ES6)でのクラス定義におけるclassキーワードとfunctionキーワードの違い

 ECMAScript 2015では、新たにクラスを定義するためのclassキーワードが導入されている。MDNで提供されているドキュメントの「Classes」ページでは、「ECMAScript 6 で導入されたクラス構文は、既存のプロトタイプによるオブジェクト指向の糖衣構文です」と記述されている。しかし、厳密にチェックするとclassキーワードで定義したクラスとfunctionキーワードで定義したクラスは(実用上問題ないレベルで)挙動が微妙に異なる。その違いを検証してみよう。なお、検証に使用した環境はNode.js v6.2.1である。

$ node -v
v6.2.1

宣言方法の違い

 functionキーワードを使ってクラスを定義する場合、次のようにコンストラクタとしてクラス名を持つ関数を定義するのが一般的だ。

> function VectorFunction(x, y) {
... this.x = x;
... this.y = y;
... }
undefined

 いっぽうclassキーワードを利用する場合、次のようにconstructor関数を使ってクラスのコンストラクタを定義する。

> class VectorClass {
... constructor(x, y) {
..... this.x = x;
..... this.y = y;
..... }
... }
[Function: VectorClass]

 classキーワードを利用した場合でも、作成されるのはあくまでFunctionオブジェクト(=関数)である。

newキーワードなしで実行した際の挙動の違い

 これらのクラスのインスタンスを作成するには、この関数をnewキーワード付きで実行する。

> v1 = new VectorFunction(3, 4)
VectorFunction { x: 3, y: 4 }
> v2 = new VectorClass(3, 4)
VectorClass { x: 3, y: 4 }

 この挙動はどちらの場合も同じだ。しかし、newキーワード無しでクラスを関数として実行した場合、functionキーワードを使って定義したクラスとclassキーワードを使って定義したクラスは挙動が異なる。

 functionキーワードを使って定義したクラスは、newキーワード無しで関数として実行すると意図しない結果を生み出すことがある。なぜなら、newキーワード無しで関数を実行した場合、this変数にglobalオブジェクト(strict modeの場合はundefined)が渡されるからだ。

 たとえば今回のケースでは、非strict modeの場合グローバルオブジェクトのプロパティが勝手に変更されてしまう。

> x = 0
0
> y = 0
0
> v1 = VectorFunction(3, 4)
undefined
> x
3
> y
4

 いっぽう、classキーワードで定義したクラスはそのまま実行することはできず、TypeErrorが発生する。

> v2 = VectorClass(3, 4)
TypeError: Class constructor VectorClass cannot be invoked without 'new'
    at repl:1:6
    at REPLServer.defaultEval (repl.js:272:27)
    at bound (domain.js:280:14)
    at REPLServer.runBound [as eval] (domain.js:293:12)
    at REPLServer.<anonymous> (repl.js:441:10)
    at emitOne (events.js:101:20)
    at REPLServer.emit (events.js:188:7)
    at REPLServer.Interface._onLine (readline.js:224:10)
    at REPLServer.Interface._line (readline.js:566:8)
    at REPLServer.Interface._ttyWrite (readline.js:843:14)

 ECMA-262の「14.5.14 Runtime Semantics: ClassDefinitionEvaluation」および「9.2.9 MakeClassConstructor(F)」で説明されているが、classキーワードでクラスを定義した場合、生成されたFunctionオブジェクトの「FunctionKind」内部スロットに「classConstructor」という値がセットされる。この内部スロットはFunctionオブジェクトの種別を格納するもので、「normal」および「classConstructor」、「generator」のいずれかを持つ(「9.2 ECMAScript Function Objects」)。FunctionKind内部スロットの値がclassConstructorの場合、関数を実行する際に実行される「Call」内部メソッドの実行時にTypeError例外が送出される(「9.2.1 [[Call]] ( thisArgument, argumentsList)」)。なお、FunctionKind内部スロットの値がclassConstructorになるのは、classキーワードでクラスを定義した場合のみである。

所有するプロパティの違い

 classキーワードで定義したクラスは、functionキーワードで定義したクラスと異なり「caller」および「arguments」プロパティを持たない。

> Object.getOwnPropertyNames(VectorFunction)
[ 'length', 'name', 'arguments', 'caller', 'prototype' ]
> Object.getOwnPropertyNames(VectorClass)
[ 'length', 'name', 'prototype' ]

 また、classキーワードで定義したクラスのprototypeプロパティはwritableではないが、functionキーワードで定義したクラスのprototypeプロパティはwritableである。

> Object.getOwnPropertyDescriptor(VectorFunction, "prototype")
{ value: VectorFunction {},
  writable: true,
  enumerable: false,
  configurable: false }
> Object.getOwnPropertyDescriptor(VectorClass, "prototype")
{ value: VectorClass {},
  writable: false,
  enumerable: false,
  configurable: false }

 もちろん、どちらの場合もprototypeオブジェクトにプロパティ/メソッドを追加することは可能だ。

> VectorFunction.prototype.norm = function () {
... return Math.sqrt(this.x * this.x + this.y * this.y);
... }
[Function]
> VectorClass.prototype.norm = function () {
... return Math.sqrt(this.x * this.x + this.y * this.y);
... }
[Function]

 このようにして定義したメソッドは、どちらも同じように実行することが可能だ。

> v1 = new VectorFunction(3, 4)
VectorFunction { x: 3, y: 4 }
> v1.norm()
5
> v2 = new VectorClass(3, 4)
VectorClass { x: 3, y: 4 }
> v2.norm()
5

functionキーワードを使ったクラス宣言でしかできないこと

 classキーワードを使ってクラスを宣言すると、そのクラス自体を関数として実行することができなくなる。多くの場合でこのこと自体にデメリットはないが、意図的に(newキーワードなしに)クラスを関数として実行したいというケースも考えられる。たとえば次の例は、strict modeではnewキーワード無しにクラスを関数として実行した場合にthis変数がundefinedになることを利用し、newキーワード付きで実行した場合とnewキーワード無しで実行した場合とで異なる処理を行わせるものだ。

$ node --use_strict
> function Vector(x, y) {
... if (this === undefined) {
..... return new Vector(x, y);
..... }
... this.x = x;
... this.y = y;
... }
undefined
> Vector(3, 4)
Vector { x: 3, y: 4 }
> new Vector(3, 4)
Vector { x: 3, y: 4 }

 このようにして定義したVectorクラスは、newキーワードがあっても無くとも同様にVectorクラスのインスタンスを返す。

ECMAScript 2015の仕様書を読む(その22)

 ECMAScript 2015の仕様書(ECMA-262)を読んでいます。今回は付録について(付録A、D、Eについては詳細な内容は割愛)。

付録A

 文法まとめ。本編で説明されている文法がここでまとめられている。とりあえずここを見れば記述可能な文法が分かる。

付録B

 Webブラウザ向けの追加機能について。本来推奨されないが、Webブラウザ上で実行されるコードの互換性を保つために規定されているもので、Webブラウザ以外のECMAScript実行環境では実装しなくてもよい。また、これらはstrict modeでは利用できない。

 規定されているのは下記。

  • 数値リテラルにおける0で始まる8進数表記(本来であれば8進数表記は0Oで始まる必要がある)
  • 8進数表記を使ったエスケープシーケンス(バックスラッシュに続けて[0-8]もしくは[0-3][0-8]、[0-4][0-8]、[0-3][0-8][0-8])
  • HTML風コメント(を利用するもの)
  • Unicodeの基本多言語面(BMP)に関連する正規表現パターン
  • グローバルオブジェクトのescape(string)およびunescape(string)プロパティ
  • Object.prototype.proto
  • String.prototype.substr(start, length)
  • String.prototypeのanchor、big、blink、bold、fixed、fontcolor、fontsize、italics、link、small、strike、sub、supメソッド
  • Date.prototypeのgetYear、setYear、toGMTStringメソッド
  • RegExp.prototypeのcompileメソッド
  • オブジェクトの初期化時における__proto__プロパティの利用
  • ラベルを使った関数定義
  • ブロックレベルでの関数定義
  • ifステートメント内での関数定義
  • catchブロック内での重複した変数定義

 なお、escapeプロパティは[A-Za-z0-9@*_+-./]をURLエンコードする。

付録C

 ECMAScriptにおけるstrict modeについて。strict modeでは非strct modeと下記が異なる。

  • 予約語にimplements、interface、let、private、protected、public、static、yieldが追加されている
  • 付録Cで定義されているWebブラウザ向けの互換性維持のための文法や機能が利用できない
  • 宣言されていない識別子や参照先が存在しないリファレンスがグローバルオブジェクトのプロパティとして生成されない
  • 各種構文でeval、argumentsを左手側に置くことはできない
  • 関数内で引数が格納されるオブジェクト(argumentsオブジェクト)のcallerやcalleeプロパティ
  • argumentsオブジェクトに格納されている値と引数として渡される値は別
  • argumentsオブジェクトは変更不可
  • 「識別子 eval」や「識別子 arguments」はSyntaxErrorとなる
  • eval内ではそれを呼び出した環境の変数や関数のインスタンスを生成できない
  • thisの値がnullやundefinedだった場合、これらがグローバルオブジェクトに置き換えられることはく、またプリミティブ値が対応するラッパーオブジェクトに変換されることはない
  • 値や関数引数、関数名に対するdelete演算子の利用がSyntaxErrorに
  • Configurableでないプロパティに対するdelete演算子の利用がSyntaxErrorに
  • with文は利用できない
  • try〜catch文でcatchを構成する識別子がevalやargumentsだった場合、SyntaxErrorに
  • 関数定義の歳の引数パラメータで同じものを2つ以上使うとSyntaxErrorに
  • 関数のcallerやargumentsプロパティについてこの仕様書で規定されている以外の拡張をすべきではない

付録D

 過去のバージョンのECMAScriptから挙動が修正された点について。

付録E

 過去のバージョンのECMAScriptとは互換性のない変更について。

ECMAScript 2015の仕様書を読む(その21)

 ECMAScript 2015の仕様書(ECMA-262)を読んでいます。今回はリフレクションやプロキシ、モジュール名前空間に関連するオブジェクトに関する第26章。

第26章第1節

 Reflectオブジェクトについて。Reflectオブジェクトは内部オブジェクトとして定義されているオブジェクトで、グローバルオブジェクトのReflectプロパティの初期値になっている。関数オブジェクトではないので直接このオブジェクトを実行することはできないし、newキーワードを使って直接コンストラクタを呼び出すこともできない。

 Reflectオブジェクトは以下のプロパティ/メソッドを持つ。これらはObjectと同名のものもあるが、targetがオブジェクトでない場合必ずTypeErrorを発生させる点が異なる。

Reflect.apply(target, thisArgument, argumentsList)	target関数をargumentsListを引数として与えて実行する。このときthisにはthisArgumentが渡される
Reflect.construct(target, argumentsList [, newTarget])	targetを元に新たにオブジェクトを作成する。このときargumentsListを引数としてコンストラクタが実行される。newTargetがあ場合、これをコンストラクタとして使う。
Reflect.defineProperty(target, propertyKey, attributes)	targetにpropertyKeyという名前でプロパティを追加し、値にattributesをセットする
Reflect.deleteProperty(target, propertyKey)	targetのproprtyKeyプロパティを削除する
Reflect.enumerate(target)	targetのenumerableなプロパティに帯するiteratorを返す
Reflect.get(target, proertyKey [, receiver])	tagetのpropertyKeyプロパティを取得する。receiverが指定された場合、これをthisとして使用する
Reflect.getOwnPropertyDescriptor(target, propertyKey)	targetのpropertyKeyのPropertyDescriptorを返す
Reflect.getPrototypeOf(target)	targetのprototypeを返す
Reflect.has(target, propertyKey)	targetがpropertyKeyプロパティを持っていたらtrueを返す
Reflect.isExtensible(target)	targetがextensibleならtrue
Reflect.ownKeys(target)	targetが独自に持つプロパティキーの配列を返す
Reflect.preventExtensions(target)	targetの拡張を禁止する
Reflect.set(target, propertyKey, V [, receiver])	targetのpropertyKeyプロパティにVをセットする。receiverが与えられた場合、これをthisとする。
Reflect.setPrototypeOf(target, proto)	targetのプロトタイプをprotoに設定する

第26章第2節

 Proxyオブジェクトについて。Proxyオブジェクトは、あるオブジェクトについて、これを変更することなくそのプロパティやメソッドに対する挙動を追加・変更するために使われるオブジェクトである。

 ProxyオブジェクトはグローバルオブジェクトのProxyプロパティの初期値。Proxyコンストラクタは(target, handler)という引数を取り、targetをベースとした新たなproxyオブジェクトを作成する。このオブジェクトのプロパティにアクセスしたり、メソッドを実行しようとするとhandler関数が実行される。なおProxyオブジェクトは通常の関数としての実行は不可で、newキーワード無しで実行するとTypeErrorとなる。

 また、ProxyオブジェクトはProxy.revocable(target, handler)というメソッドを持つ。このメソッドは取り消し可能な(proxyを無効にできる)proxyオブジェクトを返す。このオブジェクトはproxy自体を格納するproxyプロパティおよびproxyを無効にするrevokeメソッドを持つ。

第26章第3節

 モジュール名前空間オブジェクト(Module Namespace Object)について。このオブジェクトはimportキーワードを使ってモジュールをロードした際に生成される。@@toStringTagプロパティおよび@@iteratorメソッドを持ち、前者は"Module"を返す。後者はモジュールの名前空間に登録されているキーに対するiteratorを返す。

ECMAScript 2015の仕様書を読む(その20)

 ECMAScript 2015の仕様書(ECMA-262)を読んでいます。今回はイテレータやジェネレータ、Promise関連のオブジェクトに関する第25章。

第25章1節

 イテレーションについて。iterate可能なオブジェクトは、@@iteratorプロパティでそのオブジェクトに対応するiteratorを取得できる。iteratorはnextメソッドを持ち、このメソッドを実行するとIteratorResultオブジェクトを返す。このオブジェクトは次のメソッドを持つ。これらはすべてIteratorResultオブジェクトを返す。

next

 また、オプションで次のプロパティを持つ。これらはすべてIteratorResultオブジェクトを返す。

return
throw

 returnは、それ以上nextメソッドを呼び出さないことをiteratorに通知する際に実行される。また、throwはiteratorに何らかのエラーが発生したことを通知するために使われる。throwは引数にexceptionオブジェクトなどのエラー内容を格納するオブジェクトを与えることができる。

 IteratorResultオブジェクトは次のプロパティを持つ。

done	iterateが完了するとtrue
value	対応する値を格納する

第25章2節

 GeneratorFunctionについて。GeneratorFunctionはFunctionオブジェクトの派生オブジェクトであり、Functionオブジェクトのコンストラクタから派生した独自のコンストラクタと、Iteratorのプロトタイプから派生した独自のprototypeを持つ。GeneratorFunctionはグローバルオブジェクトではないが、下記のコードを用いて取得できる。

Object.getPrototypeOf(function*(){}).constructor

 GeneratorFunctionは関数として実行できる。newキーワードを付けても付けなくても同じ挙動を行う。サブクラス化も可能だが、その際はコンストラクタ内でsuperキーワードを使ってGeneratorFunctionオブジェクトを生成する必要がある。また、GeneratorFunctionオブジェクトはfunction*キーワードで生成できる。

 GeneratorFunctionは次のような形式で実行できる。

GeneratorFunction(p1, p2, ..., pn, body)

 bodyは実行コード、p1〜pnはそれに与える引数の識別子である。これらは通常は文字列として与えられる。

また、lengthおよびprototypeというプロパティを持つ。lengthの値は1である。GeneratorFunction.prototypeは次のプロパティを持つ。

GeneratorFunction.prototype.constructor	内部オブジェクト
GeneratorFunction.prototype.prototype	内部オブジェクト
GeneratorFunction.prototype[@@toStringTag]	"GeneratorFunction"

 また、GeneratorFunctionインスタンスはlengthおよびname、prototypeプロパティを持つ。

第25章3節

 ジェネレータオブジェクトについて。このオブジェクトはジェネレータ関数のインスタンスであり、IteratorとIterableインターフェイスの両方の性質を持つ。このオブジェクトはジェネレータオブジェクトのprototypeではなく、インスタンスを作成したジェネレータ関数のprototypeを引き継ぐ。つまり、GeneratorオブジェクトのprototypeオブジェクトはGeneratorFunction.prototypeである。

 Generator.prototypeは次のプロパティ/メソッドを持つ。

Generator.prototype.constructor コンストラクタ
Generator.prototype.next(value)	ジェネレータ関数の状態をvalueにする
Generator.prototype.return(value)	valueを返して処理を終了させる
Generator.prototype.throw(exception)	ジェネレータ関数で例外を発生させる
Generator.prototype[@@toStringTag]	"Generator"

 ジェネレータ関数内では、yieldキーワードの戻り値を使っているケースがある。この場合、次にジェネレータ関数が呼び出された場合にその戻り値が関数内で続く処理に渡されるわけだが、nextメソッドを利用することでその値を変更できる。

第25章4節

 Promiseオブジェクトについて。Promiseオブジェクトは遅延・非同期処理を行うためのオブジェクト。内部的に「Resolve」と「Reject」の2つの関数オブジェクトを保持し、またfulfilled、rejected、pendingの3つのステータスのどれか1つを持つ。

 内部で非同期処理を行っている場合など、それが終了する時点では結果が分からない関数でPromiseオブジェクトを返り値として使用することが想定されている。このオブジェクトは処理が完了するとfulfilledもしくはrejectedのステータスとなり、ステータスを監視することで並列処理が完了したのか、またその結果がどうなったのかを知ることができる。

 Promiseオブジェクトは関数としては実行できない。newキーワード付きでコンストラクタとしての実行のみが許可される。Promiseオブジェクトのコンストラクタはサブクラス化可能であるが、その場合はsuperキーワードを使ってコンストラクタ内でPromiseオブジェクトを生成する必要がある。

 PromiseのコンストラクタはPromise(executor)という形で呼び出される。executorは関数オブジェクトであり、遅延・非同期処理が完了した際に呼び出される。また、resolveとrejectという2つの引数を持つ。いずれも関数オブジェクトで、executor関数内で処理が成功したのか、それとも失敗したのかを通知するために呼び出すことができる。また、executor関数の終了(returnの実行)は遅延・非同期処理の完了を意味するのでは無く、その処理が受け付けられたというのを示すだけである。

 引数として与えられるresolve関数は、1つの引数を取る。executor関数内で対応するPromiseオブジェクトの解決を行いたい際にこの関数が呼び出され、引数には遅延・非同期処理が進められる際に得られた値が渡される。

 引数として与えられるreject関数も1つの引数を取る。executor関数内で対応するPromiseオブジェクトがrejectされ、成功しないことが分かった際にこの関数が呼び出される。引数にはrejectされたことを示す値(一般的にはErrorオブジェクト)が渡される。

 Promiseオブジェクトは次のメソッド/プロパティを持つ。

Promise.all(iterable)	与えられたPromiseのすべてが成功した際に成功となる新たなPromiseオブジェクトを返す
Promise.prototype	内部オブジェクト
Promise.race(iterable)	与えられたPromiseのうち、最初に完了したPromiseと同じ結果を返す新たなPromiseオブジェクトを返す
Promise.reject(r)	与えられた引数によって失敗状態となる新たなPromiseオブジェクトを返す
Promise.resolve(x)	与えられた引数によって成功状態となる新たなPromiseオブジェクトを返す
Promise[@@species]	getter

 また、Promise.prototypeは下記のメソッド/プロパティを持つ。

Promise.prototype.catch(onRejected)	Promiseが失敗したときに実行されるコールバック関数を設定する
Promise.prototype.constructor	コンストラクタ
Promise.then(onFulfilled, onRejected)	成功した際および失敗した際に実行されるコールバック関数を設定する
Promise.prototype[@@toStringTag]	"Promise"

ECMAScript 2015の仕様書を読む(その19)

 ECMAScript 2015の仕様書(ECMA-262)を読んでいます。今回はArrayBufferおよびDataView、JSONオブジェクトについて規定する第24章。

第24章1節

 ArrayBufferオブジェクトについて。ArrayBufferは、処理系内部に確保された、任意の値を格納できるメモリバッファをラップするオブジェクトとなる。このオブジェクトは内部的にはデータを格納するメモリバッファと、そのバッファの長さを保持する。

 ArrayBufferコンストラクタはサブクラス化可能にデザインされている。その場合、サブクラスのコンストラクタ内ではsuperキーワードを使ってWeakMapコンストラクタを呼び出してインスタンスの初期化を行う必要がある。

 ArrayBufferをnewキーワード付きで関数として実行すると、ArrayBufferのインスタンスを生成できる。この際、引数にはバッファの長さを指定できる。newキーワード無しで実行するとTypeErrorとなる。

 ArrayBufferオブジェクトは下記のメソッド/プロパティを持つ。

ArrayBuffer.isView(arg)	argがViewedArrayBufferであればtrueを返す
ArrayBuffer.prototype	内部オブジェクト
ArrayBuffer[@@species]	getter。

 また、ArrayBuffer.prototypeは次のメソッド/プロパティを持つ。

ArrayBuffer.prototype.byteLength	getter。対応するバッファのバイト数を返す
ArrayBuffer.prototype.constructor	コンストラクタ
ArrayBuffer.prototype.slice(start, end)	対応するバッファのstartバイトからendバイトを新たに作成したArrayBufferにコピーして返す
ArrayBuffer.prototype[@@toStringTag]	"ArrayBuffer"

第24章2節

 DataViewオブジェクトについて。DataViewオブジェクトは対応するArrayBufferに格納されたデータにアクセスするためのメソッドを提供する。データへのアクセスはすべてNumber形式で行い、またコンストラクタで指定したbyteOffsetおよびbyteLengthの範囲を超える範囲へはアクセスできない。 

 ArrayBufferコンストラクタはサブクラス化可能にデザインされている。その場合、サブクラスのコンストラクタ内ではsuperキーワードを使ってWeakMapコンストラクタを呼び出してインスタンスの初期化を行う必要がある。

 DataViewオブジェクトをnewキーワード付きで関数として実行すると、DataViewオブジェクトのインスタンスを生成できる。引数はDataView(buffer [, byteOffset [, byteLength]])の最大3つ。bufferは対応付けるArrayBuffer(もしくはその派生オブジェクト)。byteOffset、ByteLengthはそれぞれバッファを参照する際のオフセットと、最大長さ。

 DataViewオブジェクトはprototypeプロパティのみを持つ。また、DataView.prototypeは次のメソッド/プロパティを持つ。

DataView.prototype.buffer	getter。対応付けられたArrayBufferを返す
DataView.prototype.byteLength	getter。参照できる最大バイト数を返す
DataView.prototype.byteOffset	getter。バッファを参照する際のオフセットを返す
DataView.prototype.constructor	コンストラクタ
DataView.prototype.getFloat32(byteOffset [, littleEndian])	対応付けられたバッファのbyteOffsetバイト目からのデータをFloat32として扱い、それをNumber形式に変換してデータを取り出す。littleEndianがtrueならリトルエンディアン、falseならビッグエンディアンで取り出す。以下、取り出す形式が異なるのみで同じ処理を行う
DataView.prototype.getFloat64(byteOffset [, littleEndian])	
DataView.prototype.getInt8(byteOffset)	
DataView.prototype.getInt16(byteOffset [, littleEndian])	
DataView.prototype.getInt32(byteOffset [, littleEndian])	
DataView.prototype.getUint8(byteOffset)	
DataView.prototype.getUint16(byteOffset [, littleEndian])	
DataView.prototype.getUint32(byteOffset [, littleEndian])	
DataView.prototype.setFloat32(byteOffset, value [, littleEndian])	対応付けられたバッファのbyteOffsetバイト目以降に、valueをFloat32として扱ってバイナリ化したものを書き込む。littleEndianがtrueならリトルエンディアン、falseならビッグエンディアンで取り出す。以下、取り出す形式が異なるのみで同じ処理を行う
DataView.prototype.setFloat64(byteOffset, value [, littleEndian])	
DataView.prototype.setInt8(byteOffset, value)	
DataView.prototype.setInt16(byteOffset, value [, littleEndian])	
DataView.prototype.setInt32(byteOffset, value [, littleEndian])	
DataView.prototype.setUint8(byteOffset, value)	
DataView.prototype.setUint16(byteOffset, value [, littleEndian])	
DataView.prototype.setUint32(byteOffset, value [, littleEndian])	
DataView.prototype[@@toStringTag]	"DataView"

第24章3節

 JSONオブジェクトについて。JSON形式で記述されたテキストベースのデータをオブジェクト化するもの。

 JSONオブジェクトはコンストラクタを持たないため、newキーワード付きでは実行できず、また関数として実行することもできない。

 JSONオブジェクトは次のメソッドを持つ。

JSON.parse(text [, reviver])	textをJSON文字列としてパースしECMAScriptオブジェクト/値を生成する。関数オブジェクトであるreviverが与えられた場合、この関数に対しthisとしてプロパティを含むオブジェクトを、引数にプロパティ名と値を与えて実行してこの値を変換できる
JSON.stringify(value, [, replacer [, space]])	valueをJSON文字列に変換する。関数オブジェクトであるreplacerが与えられた場合、引数にプロパティ名と値を与えて実行してその値を変換できる。spaceがtrueだった場合、変換結果を整形する。
JSON[@@toStringTag]	"JSON"

ECMAScript 2015の仕様書を読む(その18)

 ECMAScript 2015の仕様書(ECMA-262)を読んでいます。今回はWeakMapについて規定する第23章第3節とWeakSetについて規定する第4節。

第23章3節

 WeakMapはキー/値のペアを格納するオブジェクト。Mapとは異なり、キーにはオブジェクトしか使用できない。さらにWeakMapではキーを列挙する手段が提供されていない。WeakMapでは格納するキーに対し弱参照のみを保持する。そのため、キーとして使われたオブジェクトがgarbage collectionで回収された場合、WeakMap内からそのキーおよび対応付けられた値は削除される。

 WeakMapコンストラクタはサブクラス化可能にデザインされている。その場合、サブクラスのコンストラクタ内ではsuperキーワードを使ってWeakMapコンストラクタを呼び出してインスタンスの初期化を行う必要がある。

 WeakMapオブジェクトを関数として実行する場合、newキーワード付きで実行する必要がある。引数にはiterableなオブジェクトを取る。

 WeakMapオブジェクトはprototypeプロパティを持つ。WeakMap.prototypeオブジェクトは下記のメソッド/プロパティを持つ。

WeakMap.prototype.constructor	コンストラクタ
WeakMap.prototype.delete(key)	keyに対応するキー/値のペアを削除する
WeakMap.prototype.get(key)	keyに対応する値を返す
WeakMap.prototype.has(key)	keyに対応するキーがあればtrue
WeakMap.prototype.set(key, value)	keyに対応する値をvalueにセットする
WeakMap.prototype[@@toStringTag]	"WeakMap"

第23章4節

 WeakSetはオブジェクトのセットを格納するオブジェクト。Setとは異なり、オブジェクトしか格納できない。WeakMapと同様、格納されているオブジェクトを列挙する手段は提供されておらず、格納するオブジェクトに対し弱参照のみを保持する。

 WeakSetコンストラクタはサブクラス化可能にデザインされている。その場合、サブクラスのコンストラクタ内ではsuperキーワードを使ってWeakSetコンストラクタを呼び出してインスタンスの初期化を行う必要がある。

 WeakSetオブジェクトを関数として実行する場合、newキーワード付きで実行する必要がある。引数にはiterableなオブジェクトを取る。

 WeakSetオブジェクトはprototypeプロパティを持つ。WeakSet.prototypeオブジェクトは下記のメソッド/プロパティを持つ。

WeakSet.prototype.add(value)	valueをセットに追加する
WeakSet.prototype.constructor	コンストラクタ
WeakSet.prototype.delete(value)	valueに対応する値をセットから削除する
WeakSet.prototype.has(value)	valueがセット内にあればtrue
WeakSet.prototype[@@toStringTag]	"WeakSet"

ECMAScript 2015の仕様書を読む(その16)

 ECMAScript 2015の仕様書(ECMA-262)を読んでいます。今回はTypedArrayなどについて規定する第22章2節。

第22章2節

 TypedArrayはバイナリデータを格納するための配列状オブジェクト。格納可能な要素はInt8/Uint8/Uint8C/Int16/Uint16/Int32/Uint32/Float32/Float64のいずれかで、各要素はすべて同じ型である必要がある。各型ごとに異なる次の関数オブジェクトがコンストラクタとして用意されている。

Int8Array
Uint8Array
Uint8ClampedArray
Int16Array
Uint16Array
Int32Array
Uint32Array
Float32Array
Float64Array

 これらはすべて同じ引数を持ち、また同じprototypeを持つので、以下ではこれらのオブジェクトを%TypedArray%と表記する。

 %TypedArray%は異なる引数を持つ複数の形式で関数として呼び出せる。ただし、これらはすべてnewキーワード付き呼び出す必要がある。newキーワードなしで呼び出すと、TypeErrorを返す。

 コンストラクタに与えられる引数とその際の動作は下記のとおり。

%TypedArray%()	空のTypedArrayを返す
%TypedArray%(length)	指定した数の要素を持つTypedArrayを返す。各要素は0に設定される
%TypedArray%(typedArray)	typedArrayと同じ要素を持つTypedArrayを返す
%TypedArray%(object)	objectを元にTypedArrayを作成して返す
%TypedArray%(buffer [, byteOffset [, length]])	bufferのbtyeOffsetからlengthまでの部分を格納したTypedArrayを作成して返す

 %TypedArray%は下記のメソッド/プロパティを持つ。

%TypedArray%.from(source [, mapfn [, thisArg]])	sourceオブジェクトからTypedArrayを作成して返す。mapfnが指定された場合はこの関数を用いたmap処理が行われたあとに作成されたTypedArrayを返。
%TypedArray%.of(...items)	itemsを要素として持つTypedArrayを作成して返す
%TypedArray%[@@species]	gettter。thisを返す

 %TypedArray%.prototypeは下記のメソッド/プロパティを持つ。なお、とくに説明のないものについてはArray.prototypeの同名メソッド/プロパティと同じ処理を行う。

%TypedArray%.prototype.buffer	getter。格納するデータをbuffer形式で返す
%TypedArray%.prototype.byteLength	getter。格納するバイト数を返す
%TypedArray%.prototype.byteOffset	getter。byteOffsetを返す
%TypedArray%.prototype.constructor	コンストラクタ
%TypedArray%.prototype.copyWithin(target, start [, end])
%TypedArray%.prototype.entries()
%TypedArray%.prototype.every(callbackfn [, thisArg])
%TypedArray%.prototype.fill(value, [, start [, end]])
%TypedArray%.prototype.filter(callbackfn [, thisArg])
%TypedArray%.prototype.find(predicate [, thisArg])
%TypedArray%.prototype.findIndex(predicate [, thisArg])
%TypedArray%.prototype.forEach(callbackfn [, thisArg])
%TypedArray%.prototype.indexOf(searchElement [, fromIndex])
%TypedArray%.prototype.join(separator)
%TypedArray%.prototype.keys()
%TypedArray%.prototype.lastIndexOf(searchElement [, fromIndex])
%TypedArray%.prototype.length	格納する要素数
%TypedArray%.prototype.map(callbackfn [, thisArg])
%TypedArray%.prototype.reduce(callbackfn, [initialValue])
%TypedArray%.prototype.reduceRight(callbackfn, [initialValue])
%TypedArray%.prototype.set(overloaded [, offset])	第一引数からデータを読み込む。型によって処理が異なる
%TypedArray%.prototype.set(array [, offset])	arrayからデータを読み込む
%TypedArray%.prototype.set(typedArray [, offset])	typedArrayからデータを読み込む
%TypedArray%.prototype.slice(start, end)
%TypedArray%.prototype.some(callbackfn [, thisArg])
%TypedArray%.prototype.sort(comparefn)
%TypedArray%.prototype.subarray([begin [, end]])	部分配列を返す。この部分配列はバッファを共有する
%TypedArray%.prototype.toLocaleString([reserved1 [, reserved2]])
%TypedArray%.prototype.toString()
%TypedArray%.prototype.values()
%TypedArray%.prototype[@@iterator]()
%TypedArray%.prototype.[@@toStringTag]  getter。名前を返す

 なお、内部的に基底クラスとしてTypedArrayというものは存在するが、これをECMAScript 2015から直接操作することはできない。

 TypedArray.BYETS_PER_ELEMENTおよびTypedArray.prototype.BYTES_PER_ELEMENTで各要素のバイト数を参照できる。

ECMAScript 2015の仕様書を読む(その15)

 ECMAScript 2015の仕様書(ECMA-262)を読んでいます。今回は配列(Array)などについて規定する第22章第1節。

第22章第1節

 Arrayオブジェクトは特別なオブジェクトであり、例外的なプロパティ名を持つ。Arrayオブジェクトを関数として実行すると、新たなArrayオブジェクトを作成して返す。newキーワード付きの場合も同じ処理を行う。また、Arrayコンストラクタは拡張可能で、そのサブクラスはコンストラクタ内でsuperキーワードを使ってArrayコンストラクタを呼び出してオブジェクトを初期化する必要がある。

 Arrayコンストラクタは引数の数によって処理が変わる。引数が無い場合、空の配列を返す。1つの引数を与えた場合、その引数がNumberでかつその値がUint32に変換したものと等しい場合、長さがその値に等しく、かつ各要素がundefinedの配列を作成して返す。そうでない場合、その値を含む長さ1の配列を返す。2つ以上の引数が与えられた場合、それらを格納する配列を返す。

 Arrayオブジェクトは次のプロパティ/メソッドを持つ。

Array.from(items [, mapfs [, thisArg]])	mapfs関数にitems引数の各要素を与えて実行した結果を格納した配列を返す。また、thisArgが指定された場合、mapfsを実行時にそれをthisとして使用する。items引数はiterableなものであれば何でも良い
Array.isArray(arg)	argがArrayであればtrueを返す
Array.of(...items)	引数を値として格納した配列を返す

 Array.prototypeは内部オブジェクトであり、書き換えは不可能。また、Array[@@species]というアクセサプロパティを持つ。値はundefined。

 Array.prototypeプロパティは下記のプロパティ/メソッドを持つ。なお、thisArg引数はcallbackfnを実行する際にthisとして使用するオブジェクトである。また、startが負の場合、length+startが指定されたものとして処理が行われる。endが指定されなかった場合lengthが、endが負の場合length+endが指定されたものとして処理される。

Array.prototype.concat(...arguments)	引数を値として拝謁の末尾に追加する。引数が配列だった場合、その各要素を追加する
Array.prototype.constructor	コンストラクタ
Array.prototype.copyWithin(target, start [, end])	配列内のstartからendまでをtargetの位置から順に格納する
Array.prototype.entries()	[インデックス, 値]をvalue要素に格納するiteratorを返す
Array.prototype.every(callbackfn [, thisArg])	各要素を引数としてcallbackfnを実行し、すべてがtrueならtrueを返す
Array.prototype.fill(value [, start [, end]])	startからendまでの要素をvalueに設定する
Array.prototype.filter(callbackfn [, thisArg])	各要素を引数としてcallbackfnを実行し、trueを返したもののみを含む配列を作成して返す
Array.prototype.find(predicate [, thisArg])	各要素を引数としてpredicateを実行し、trueを返した場合その値を返す。predicateは要素、インデックス、対象の配列という3つの引数が与えられる
Array.prototype.findIndex(predicate [, thisArg])	findと同様の処理を行う。findとは異なり、インデックスを返す
Array.prototype.forEach(callbackfn [, thisArg])	各要素を引数としてcallbackfnを実行する。undefinedを返す
Array.prototype.indexOf(searchElement [, fromIndex])	searchElementと一致する要素のインデックスを返す。fromIndexが指定された場合、その位置から検索を開始する。比較には===演算子が使われる
Array.prototype.join(separator)	各要素を文字列に変換し、separatorを介して連結したものを返す。separatorがundefinedだった場合、","が使われる
Array.prototype.keys()	各キーをvalue要素に格納するiteratorを返す
Array.prototype.lastIndexOf(searchElement [, fromIndex])	searchElementと一致する最後の要素のインデックスを返す。fromIndexが指定された場合、その位置から検索を開始する。比較には===演算子が使われる
Array.prototype.map(callbackfn [, thisArg])	各要素を引数としてcallbackfnを実行し、その結果を含む配列を返す
Array.prototype.pop()	配列の最後の要素を削除してそれを返す
Array.prototype.push(...items)	引数として与えられた値をそれぞれ配列の最後に追加する
Array.prototype.reduce(callbackfn [, initialValue])	各要素を引数としてcallbackfnを実行する。callbackfnには直前に実行されたcallbackfnの戻り値、配列の要素、配列のインデックス、配列の4つの引数が与えられる。initialValueが指定された場合、それが最初に実行されるcallbackfnの第1引数となる。最後に実行されたcallbackfnの戻り値を返す
Array.prototype.reduceRight(callbackfn, [, initialValue])	reduceを逆方向に実行する
Array.prototype.reverse()	配列の要素の順序を逆にする
Array.prototype.shift()	配列の最初の要素を削除して返す
Array.prototype.slice(start, end)	startからendまでの要素を含む配列を返す
Array.prototype.some(callbackfn [, thisArg])	各要素を引数としてcallbackfnを実行し、いずれかがtrueを返したらtrueを返す
Array.prototype.sort(comparefn)	comparefnを比較関数としてソートを実行する
Array.prototype.splice(start, deleteCount, ...items)	startからdeleteCount個の要素を取り除き、そこにitemsで与えた各要素を追加する
Array.prototype.toLocaleString([reserved1 [, reserved2]])	ECMA-402で定義された地域課文字列を返す
Array.prototype.toString()	配列を文字列に変換する
Array.prototype.unshift(...items)	配列の最初に要素を追加しそのlengthを返す
Array.prototype.values()	各要素をvalue要素に格納するiteratorを返す
Array.prototype[@@iterator]()	valuesと同じ
Array.prototype[@@unscopables]	["copyWithin", "entries", "fill", "find", "findIndex", "includes", "keys", "values"]

 Arrayインスタンスはlengthプロパティを持つ。また、Arrayオブジェクトのiteratorはnext()のほか、[@@toStringTag]要素を持つ。

ECMAScript 2015の仕様書を読む(その14)

 ECMAScript 2015の仕様書(ECMA-262)を読んでいます。今回はテキスト処理関連について規定する第21章のうち、RegExpオブジェクトについて。

第21章2節

 RegExpオブジェクトは正規表現およびそれに関するフラグを格納するオブジェクト。Perl 5の正規表現をベースにしている。ECMAScriptで使える正規表現については詳しくまとめると大変なので省略。また、Perlとは異なり使える文字クラスは\d(数字)、\D(数字以外)、\s(ホワイトスペース)、\S(ホワイトスペース以外)、\w(アルファベットおよび数字、アンダースコア)、\W(アルファベットおよび数字、アンダースコア以外)の6つのみ。また、コントロールシーケンスは\t(HT)、\n(LF)、\v(VT)、\f(FF)、\r(CR)が定義されている。

 RegExpオブジェクトは(pattern, flags)という引数を取る。patternは正規表現リテラルもしくは文字列、flagsがフラグ。

 利用できるフラグはg(global)、i(ignoreCase)、m(multiline)、u(unicode)、y(sticky)。

 RegExpオブジェクトはRegExp[@@species]というアクセサを持つ。通常派生オブジェクトを作る際にこれを使用する。サブクラスを作る際にこの挙動を書き換えることも可能。

 RegExp.prototypeプロパティは下記のプロパティ/メソッドを持つ。

RegExp.prototype.constructor    コンストラクタ。初期値は内部オブジェクト
RegExp.prototype.exec(string)	stringに対しマッチを行う
RegExp.prototype.flags	アクセサ。設定されているフラグを返す
RegExp.prototype.global	アクセサ。gフラグが設定されていればtrue
RegExp.prototype.ignoreCase	アクセサ。iフラグが設定されていればtrue
RegExp.prototype[@@match](string)	String.prototype.matchメソッド内で内部的に呼び出されるマッチ関数
RegExp.prototype.multiline	アクセサ。mフラグが設定されていればtrue
RegExp.prototype[@@replace](string, replaceValue)	String.prototype.replaceメソッド内で内部的に呼び出される置換関数
RegExp.prototype[@@search](string)	String.prototype.searchメソッド内で内部的に呼び出されるマッチ関数
RegExp.prototype.source	アクセサ。正規表現を返す
RegExp.prototype[@@split](string, limit)	String.prototype.splitメソッド内で内部的に呼び出されるマッチ関数
RegExp.prototype.sticky	アクセサ。yフラグが設定されていればtrue
RegExp.prototype.test(S)	Sに対しマッチを行い、マッチしたらtrueを返す
RegExp.prototype.toString()	正規表現を文字列として返す
RegExp.prototype.unicode	アクセサ。uフラグが設定されていればtrue

 RegExpオブジェクトはlastIndexプロパティを持つ。これは、次のマッチ処理の際にどの位置からマッチ処理を行うべきかを格納する。

ECMAScript 2015の仕様書を読む(その13)

 ECMAScript 2015の仕様書(ECMA-262)を読んでいます。今回はテキスト処理関連について規定する第21章のうち、Stringオブジェクトについて。

第21章1節

 Stringオブジェクトのコンストラクタは内部オブジェクトが割り当てられているが、サブクラス化可能になっており、Stringオブジェクトのサブクラスはコンストラクタ内でsuperキーワードを使ってStringコンストラクタを実行しインスタンスを作成しなければならない。

 なお、Stringオブジェクトを関数として実行すると引数を文字列に変換したものを返す。また、newキーワード付きで関数として実行すると、引数を値として格納したStringオブジェクトを返す。

 Stringオブジェクトは次のプロパティ/メソッドを持つ。

String.fromCharCode(...codeUnits)	codeUnitsに相当する文字コードを含む文字列を返す。codeUnitsはUnicodeで指定。
String.fromCodePoint(...codePoints)	codePointsに相当する文字コードを含む文字列を返す
String.prototype	内部オブジェクトを指す。変更不可
String.raw(template, ...substitutions)	テンプレートオブジェクトに対し与えられた引数を使って展開を行う。通常はタグ付けされたテンプレート処理のために利用される

 また、String.prototypeオブジェクトは次のプロパティ/メソッドを持つ。

String.prototype.charAt(pos)	posの位置にある文字を返す
String.prototype.charCodeAt(pos)	posの位置にある文字の文字コードを返す
String.prototype.codePointAt(pos)	posの位置にある文字のコードポイントを返す
String.prototype.concat(...args)	argsを連結した文字列を返す
String.prototype.constructor	コンストラクタ。内部オブジェクト
String.prototype.endsWith(searchString [, endPosition])	文字列がsearchStringで終わっていればtrueを返す。endPositionを指定した場合、文字列がその長さであるかのように処理を行う
String.prototype.(includes)sesarchString [, position])	文字列がsearchStringを含んでいればtrueを返す。positionが指定された場合、その位置から検索を開始する
String.prototype.indexOf(searchString [, position])	文字列中でsearchStringが最初に出現する位置を返す。positionが指定された場合、その位置から検索を開始する
String.prototype.lastIndexOf(searchString [, position])	文字列中でsearchStringが最後に出現する位置を返す。positionが指定された場合、その位置から検索を開始する
String.prototype.localeCompare(tha [, reserved1 [, reserved2]])	ECMA-402で規定されているlocaleCompare処理を行う。
String.prototype.match(regexp)	regexpを使ってマッチ処理を行う
String.prototype.normalize([form])	文字列を正規化したものを返す。formは"NFC"、"NFD"、"NFKC"、"NFKD"が指定可能
String.prototype.repeat(count)	文字列をcount回繰り返したものを返す
String.prototype.replace(searchValue, replaceValue)	serachValueをreplaceValueに置き換える。replaceValueには$$、$&、$`、$'、$n、$nnなどが指定可能
String.prototype.search(regexp)	文字列中でregexpにマッチした位置を返す
String.prototype.slice(start, end)	startの位置からendの位置までの部分文字列を返す。endが負の場合、文字列のlengthにendを足したものがendとして使われる
String.prototype.split(separator, limit)	separatorで文字列を分割する
String.prototype.startsWith(searchString [, position])	文字列がsearchStringで始まっているならtrueを返す。positionが指定されていた場合、その場所から検索を開始する
String.prototype.substring(start, end)	startの位置からendの位置までの部分文字列を返す。endがstartより小さい場合、endとstartは入れ替えられる
String.prototype.toLocaleLowerCase([reseved1 [, reserved2]])	ECMA-402で規定されているtoLocaseLowerCase処理を行う。
String.prototype.toLocaleUpperCase([reseved1 [, reserved2]])	ECMA-402で規定されているtoLocaseUpperCase処理を行う。
String.prototype.toLowerCase()	小文字に変換した文字列を返す
String.prototype.toString()	変換した文字列を返す
String.prototype.toUpperCase()	大文字に変換した文字列を返す
String.prototype.trim()	前後のホワイトスペースをものを返す
String.prototype.valueOf()	変換した文字列を返す
String.prototype[@@iterator]()	文字列のコードポイントを走査するためのiteratorを返す

 また、Stringインスタンスはその文字列の長さを格納するlengthプロパティを持つ。

 Stringはiteratorを使った走査にも対応している。このiteratorは、文字列をインデックス順に返す。

ECMAScript 2015の仕様書を読む(その12)

 ECMAScript 2015の仕様書(ECMA-262)を読んでいます。今回はNumber、Math、Dateオブジェクトについて解説する第20章のうち、Dateオブジェクトについて。

第20章3節

 ECMAScriptでは基準時刻(UTC1970年1月1日)からのミリ秒で時刻を表現する。表現できるのはこの基準時刻±9,007,199,254,740,992(2の53乗)ミリ秒であり、これはおよそ±28万5,616年に相当する。ただし、実際にDateオブジェクトで扱えるのは1970年1月1日正午から±1億日(±8,640,000,000,000,000ミリ秒)。

 なお、1日は8640万ミリ秒。また、閏秒は考慮されないが、閏年は考慮される。月については0〜11(0が1月)の値で表現されるが、日は1〜31の値で表現される。曜日は0〜6(0が日曜日)。そのほか、ECMAScript処理系はタイムゾーンの処理や夏時間なども考慮する。

 ECMAScriptはISO 8601 Extended Formatベースのフォーマット仕様(YYYY-MM-DDTHH:mm:ss.sssZ)をサポートする。なお、年については最大6桁(および±)での表現が可能。

 Dateオブジェクトは、基準時刻からのミリ秒をそのオブジェクトの値として格納する。また、Dateオブジェクトのコンストラクタは内部オブジェクトになっている。このコンストラクタはサブクラス化が可能だが、その場合superキーワードを使ってDataコンストラクタをコンストラクタ内で呼び出す必要がある。

 デフォルトのDateコンストラクタは3つのパターンがある。どの場合もnewキーワードなしで呼ばれた場合、いかなる引数が与えられていても現在時刻に相当する文字列を返す。

 2つ以上の引数を取るケースは以下。この場合、引数で指定した日付時刻に相当するDateオブジェクトを返す。また、引数にNaNがあると「不正な時刻」に相当するDateオブジェクトを返す。

Date(year, month [, date [, hours [, minutes [, seconds [, ms]]]]])

 1つの引数を与えた場合、引数として与えられたオブジェクトが日付時刻値を格納していた場合はその日付時刻に相当するDateオブジェクトを、もし引数が文字列であり、かつ日付時刻としてパースできる場合はその日付時刻に相当するDatetオブジェクトを、そうでない場合は引数を数値に変換し、基準時刻にその数値を足した日付時刻に相当するDateオブジェクトを返す。

 引数が与えられなかった場合、現在の日付時刻に相当するDateオブジェクトを返す。

 Dateオブジェクトは次のメソッドを持つ。

Date.now()	基準時刻から現在時刻までの経過ミリ秒を返す
Date.parse(string)	引数を文字列としてパースし、基準時刻からその時刻までの経過ミリ秒を返す

 また、Dateオブジェクトのprototypeオブジェクトは下記のメソッド/プロパティを持つ。

Date.prototype.constructor	コンストラクタ
Date.prototype.getDate()	そのオブジェクトの日にちを返す(下記、同様)
Date.prototype.getDay()	
Date.prototype.getFullYear()	
Date.prototype.getHours()	
Date.prototype.getMilliseconds()	
Date.prototype.getMinutes()	
Date.prototype.getMonth	
Date.prototype.getSeconds()	
Date.prototype.getTime()	基準時刻からの経過ミリ秒を返す
Date.prototype.getTimezoneOffset()	UTCからローカル時刻へのオフセット(分)を返す
Date.prototype.getUTCDate()	そのオブジェクトのUTCでの日にちを返す(下記、同様)
Date.prototype.getUTCDay()
Date.prototype.getUTCFullYear()
Date.prototype.getUTCHours()
Date.prototype.getUTCMilliseconds()
Date.prototype.getUTCMinutes()
Date.prototype.getUTCMonth()
Date.prototype.getUTCSeconds()
Date.prototype.setDate(date)	そのオブジェクトの日付をdateに設定する(下記、同様)
Date.prototype.setFullYear(year [, month [, date]])
Date.prototype.setHours(hour [, min [, sec [, ms]]])
Date.prototype.setMilliseconds(ms)
Date.prototype.setMinutes(min [, sec [, ms]])
Date.prototype.setMonth(month [, date])	
Date.prototype.setSeconds(sec [, ms])	
Date.prototype.setTime(time)	
Date.prototype.setUTCDate(date)	そのオブジェクトのUTCでの日付をdateに設定する(下記、同様)
Date.prototype.setFUTCullYear(year [, month [, date]])
Date.prototype.setUTCHours(hour [, min [, sec [, ms]]])
Date.prototype.setUTCMilliseconds(ms)
Date.prototype.setUTCMinutes(min [, sec [, ms]])
Date.prototype.setUTCMonth(month [, date])	
Date.prototype.setUTCSeconds(sec [, ms])	
Date.prototype.toDateString()	相当する文字列を返す
Date.prototype.toISOString()	相当する文字列(ISO 8601 Extended Format)を返す
Date.prototype.toJSON(key)	JSONで使われる日付時刻文字列を返す
Date.prototype.toLocalDateString([ reserved1 [, reserved2]])	ローカル日付文字列を返す
Date.prototype.toLocalString([ reserved1 [, reserved2]])	ローカル文字列を返す
Date.prototype.toLocalTimeString([ reserved1 [, reserved2]])	ローカル時刻文字列を返す
Date.prototype.toString()	相当する文字列を返す
Date.prototype.toTimeString()	相当する時刻文字列を返す
Date.prototype.toUTCString()	相当するUTC文字列を返す
Date.prototype.valueOf()	基準時刻からの経過ミリ秒を返す
Date.prototype.toPrimitive[@@toPrimitive](hint)	相当するプリミティブ値を返す。hintは"default"もしくは"number"、"string"のどれか。"default"は"string"と同等。

ECMAScript 2015の仕様書を読む(その11)

 ECMAScript 2015の仕様書(ECMA-262)を読んでいます。今回はNumber、Math、Dateオブジェクトについて解説する第20章のうち、Mathオブジェクトについて。

第20章第2節

 Mathオブジェクトについて。Mathオブジェクトは関数オブジェクトではないため、関数としてもnewキーワード付きでも実行できない。

 Mathオブジェクトは多数のプロパティ/メソッドを持つ。まず、数値演算で使われる次のような定数が定義されている。これらはすべて書き込み禁止で列挙もされない。また、Math[@@toStringTag]のみConofigurableである。

Math.E	ネイピア数(e、exp(1))
Math.LN10	log(10)
Math.LN2	log(2)
Math.LOG10E	log10(e)
Math.LOG2E	log2(e)
Math.PI	円周率
Math.SQRT1_2	√(1/2)
Math.Math.SQRT2	√2
Math[@@toStringTag]	"Math"という文字列

 また、次のような数学関数をメソッドとして持つ。

Math.abs(x)	絶対値
Math.acos(x)	アークコサイン
Math.acosh(x)	逆双曲線コサイン
Math.asin(x)	アークサイン
Math.asinh(x)	逆双曲線サイン
Math.atan(x)    アークタンジェント
Math.atanh(x)   逆双曲線タンジェント
Math.atan2(x, y)	atan(y/x)
Math.cbrt(x)	立方根
Math.ceil(x)    x以上の最小の整数
Math.clz32(x)	xを2進数表示し、その戦闘から続く0の桁数
Math.cos(x)	コサイン
Math.cosh(x)	双曲線コサイン
Math.exp(x)	eのx乗
Math.expm1(x)	eのx乗 - 1
Math.floor(x)	x以下の最小の整数
Math.fround(x)	xに最も近い32ビット浮動小数点数を返す
Math.hypot(value1, value2, value3, ...values)	引数で与えた値の2乗和平方根を返す
Math.imul(x, y)	xとyを整数に変換したものの積を返す
Math.log(x)	eを底とするxの対数
Math.log1p(x)	log(x) + 1
Math.log10(x)	10を底とするxの対数
Math.log2(x)	2を底とする2の対数
Math.max(value1, value2, value3, ...values)	引数の中で最大の数を返す。引数が与えられなかった場合は-Infinityを、NaNが含まれていた場合はNaNを返す
Math.min(value1, value2, value3, ...values)	引数の中で最小の数を返す。引数が与えられなかった場合は-Infinityを、NaNが含まれていた場合はNaNを返す
Math.pow(x, y)	xのy乗を返す
Math.random()	0以上1以下の乱数を返す
Math.round(x)	xに最も近い整数を返す(xの小数点1桁目を四捨五入する)
Math.sign(x)	xが0より大きければ1、0なら0、0より小さければ-1を返す
Math.sin(x)	サイン
Math.sinh(x)	双曲線サイン
Math.sqrt(x)	平方根
Math.tan(x)	タンジェント
Math.tanh(x)	双曲線タンジェント
Math.trunc(x)	xの整数部分を返す

ECMAScript 2015の仕様書を読む(その10)

 ECMAScript 2015の仕様書(ECMA-262)を読んでいます。今回はNumber、Math、Dateオブジェクトについて解説する第20章のうち、Numberオブジェクトについて。

第20章第1節

 Numberオブジェクトについて。Numberオブジェクトは基本型であるNumberに対応するオブジェクト。サブクラスを作って拡張可能となっている。サブクラスを作る場合、コンストラクタ内でsuperキーワードを使ってNumberオブジェクトのコンストラクタを実行することが推奨されている。

 Numberオブジェクトを関数として実行すると、引数を数値化したものを返す。引数が与えられなかった場合は0を返す。また、newキーワード付きで実行されると引数を数値化したものを値として格納する新たなNumberオブジェクトを作成して返す。なお、このコンストラクタは内部オブジェクトを参照している。

 Numberオブジェクトが持つプロパティやメソッドは下記の通り。

Number.EPSILON	「1と1より大きい最小の数の差」を示す定数
Number.isFinite(number)	numberがNaN、Infinity、-Infinity以外の数であればtrueを返す
Number.isInteger(number)	numberが整数であればtrueを返す
Number.isNaN(number)	numberがNaNであればtrueを返す
Number.isSafeInteger(number)	numberが整数で、かつその絶対値がNumber.MAX_SAFE_INTEGER以下であればtrueを返す
Number.MAX_SAFE_INTEGER	2の53乗-1。Number型では整数はこの数+1までしか表現できない
Number.MAX_VALUE	Number形で扱える最大の数。約1.7976931348623157×10の308乗
Number.MIN_SAFE_INTEGER	-(2の53乗-1)。Number型では整数はこの数-1までしか表現できない
Number.MIN_VALUE	Number形で扱える最小の数。約5×10の-324乗
Number.NaN	NaN	
Number.NEGATIVE_INFINITY	-Infinity
Number.paseFloat(string)	parseFloat(string)と同等
Number.parseInt(string, radix)	parseFloat(string, radix)と同等
Number.POSITIVE_INFINITY	Infinity
Number.prototype	初期値は内部オブジェクト

 なお、Number.isNaNはisNaNとは異なり、引数としてNaNが与えられた場合のみtrueを返す。

> isNaN("fo")
true
> Number.isNaN("fo")
false

 また、Number.MAX_SAFE_INTEGER+1を超える整数、もしくはNumber.MIN_SAFE_INTEGER-1よりも小さい整数は次の例のように演算結果が保証されない。

> Number.MAX_SAFE_INTEGER 
9007199254740991
> Number.MAX_SAFE_INTEGER + 1
9007199254740992
> Number.MAX_SAFE_INTEGER + 2
9007199254740992
> Number.MAX_SAFE_INTEGER + 3
9007199254740994

 Number.prototypeの持つプロパティ/メソッドは下記になる。

Number.prototype.constructor	コンストラクタ。初期値は内部オブジェクト
Number.prototype.toExponential(fractionDigits)	指数表記した文字列に変換して返す。このとき、小数点以下はfractionDigitsで指定した桁数となる
Number.prototype.toFixed(fractionDigits)	固定小数点表記した文字列に変換して返す。このとき、小数点以下はfractionDigitsで指定した桁数となる
Number.prototype.toLocaleString([reserved1 [, reserved2]])	ECMA-402規格で定義されたローカリゼーションされた文字列を返す
Number.prototype.toPrecision(precision)	precisionで指定した桁数の文字列に変換して返す
Number.prototype.toString([radix])	文字列に変換して返す。radixが指定された場合、radix進数表記で返す
Number.prototype.valueOf()	値を返す

 なお、数値型はNumberオブジェクトでありNnumber.prototypeを継承する。

ECMAScript 2015の仕様書を読む(その9)

 ECMAScript 2015の仕様書(ECMA-262)を読んでいます。今回は基本オブジェクト(Fundamental Objects)についての第19章のうち、Boolean、Symbol、Errorオブジェクトについて。

第19章第3節

 Booleanオブジェクトおよびそのメソッド、プロパティについて説明されている。

 Booleanオブジェクトは関数として実行すると、引数として与えたオブジェクトの真偽値に対応したtrueもしくはfalseを返す。また、newキーワード付きで実行するとその真偽値に対応したBooleanオブジェクトを返す。

 Booleanオブジェクトはprototypeプロパティのみを持つ。初期値は内部オブジェクトとなる。このプロパティの書き換えや属性変更は行えない。また、Boolean.prototypeは下記のプロパティ/メソッドを持つ。

Boolean.prototype.constructor	コンストラクタ。初期値は内部オブジェクト
Boolean.prototype.toString()	格納されている値に応じて"true"もしくは"false"を返す
Boolean.prototype.valueOf()	格納されている値に応じてtrue/falseを返す

第19章第4節

 Symbolオブジェクトおよびそのメソッド、プロパティについて説明されている。SymbolはECMAScript2015で新たに導入された基本オブジェクトで、オブジェクトのプロパティを参照するためのキーとして使用することが想定されている。

 Symbolオブジェクトをそのまま関数として実行すると、ユニークなSymbolオブジェクトを作成して返す。このとき、作成されたオブジェクトの[[Description]]内部スロットに引数として与えたオブジェクトを文字列に変換したものが格納される。また、newキーワード付きで実行するとTypeError例外を発生させる。

 Symbolオブジェクトは下記のプロパティ/メソッドを持つ。

Symbol.for(key)	GlobalSymbolRegistryを検索し、keyに対応するSybolがあればそれを返す。無ければ、新たなSymbolを作成し、GlobalSymboその[[key]]内部スロットおよび[[symbol]]内部スロットにkeyを文字列に変換したものを設定する。GlobalSymbolRegistryにkeyをキーとしてこのSymbolを登録し、このSymbolを返す
Symbol.hasInstance	@@hasInstanceシンボルが格納されている
Symbol.isConcatSpreadable	@@isConcatSpreadableシンボルが格納されている
Symbol.iterator	@@iteratorシンボルが格納されている
Symbol.keyFor(sym)	GlobalSymbolRegistryを検索し、symシンボルに対応するkeyがあればそれを返す。なければundefinedを返す
Symbol.match	@@matchシンボルが格納されている
Symbol.prototype	初期値では内部オブジェクトが格納されている
Symbol.replace	@@replaceシンボルが格納されている
Symbol.search	@@searchシンボルが格納されている
Symbol.spicies	@@spiciesシンボルが格納されている
Symbol.split	@@splitシンボルが格納されている
Symbol.toPrimitive	@@toPrimitiveシンボルが格納されている
Symbol.toStringTag	@@toStringTagシンボルが格納されている
Symbol.unscopables	@@unscopablesシンボルが格納されている

 なお、GlobalSymbolRegistryはグローバルな内部変数。

 Symbol.prototypeは下記のプロパティ/メソッドを持つ。

Symbol.prototype.constructor	コンストラクタ。初期値は内部オブジェクト
Symbol.prototype.toString()	"Symbol ([[Description]])"という文字列を返す
Symbol.prototype.valueOf()	そのシンボル自体を返す。オブジェクトがSymbolオブジェクトで内場合、[[SymbolData]]内部スロットの値を返す
Symbol.prototype[@@toPrimitive](hint)	Symbolオブジェクトをプリミティブ値に変換したものを返す。hintは"default"もしくは"number"、"string"
Symbol.prototype[@@toStringTag]	初期値は"Symbol"

 [[SymbolData]]内部スロットは、そのSymbolオブジェクトの内容を示す値が格納されているもの。

第19章第5節

 Errorオブジェクトおよびそのメソッド、プロパティについて説明されている。Errorオブジェクトは例外が発生した際にランタイムによって生成されるほか、ユーザー定義例外の基本オブジェクトとしても使われる。

 Errorオブジェクトをそのまま関数として実行すると、新たなErrorオブジェクトを生成して返す。このとき、引数に与えたオブジェクトを文字列化してmeessageプロパティの値に設定する。また、newキーワード付きで実行した場合も同じ挙動を行う。

 なお、Errorオブジェクトのコンストラクタはそのサブクラスを作れるように設計されている。その場合、コンストラクタ内ではsuperキーワードを使ってErrorオブジェクトを作成・初期化することが推奨されている。

 Errorオブジェクトは下記のプロパティ/メソッドを持つ。

Error.prototype	初期値では内部オブジェクトが格納されている

 Error.prototypeは下記のプロパティ/メソッドを持つ。

Error.prototype.constructor	コンストラクタ。初期値は内部オブジェクト
Error.prototype.message	初期値は空の文字列
Error.prototype.name	初期値は"Error"
Error.prototype.toString()	"<name>: <message>"という文字列を返す

 なお、ECMAScript2015ではネイティブエラーオブジェクトとして下記が用意されている。

EvalError
RangeError
ReferenceError
SyntaxError
TypeError
URIError

 これらのオブジェクトもサブクラス化が可能となっている。

ECMAScript 2015の仕様書を読む(その8)

 ECMAScript 2015の仕様書(ECMA-262)を読んでいます。今回は基本オブジェクト(Fundamental Objects)についての第19章のうち、Functionオブジェクトについて。

第19章第2節

 Functionオブジェクトおよびそのメソッド、プロパティについてが説明されている。

 Functionオブジェクトは関数として呼び出すことで、関数オブジェクトを返す。この場合、常にFunctionオブジェクトのコンストラクタとして関数は実行されるため、newキーワードを付けても付けなくても同じ結果となる。

 Functionオブジェクトを関数として実行する場合、以下のような引数を取る。

Function(p1, p2, ..., pn, body)

 p1〜pnはその関数に与える仮引数リスト、bodyは関数本体である。戻り値はFunctionオブジェクトとなる。なお、仮引数リストは文字列で与えられる。このとき、1つの引数に複数の仮引数を入れることが可能。たとえば以下の3つは同じものを返す。

new Function("a", "b", "c", "return a+b+c")
new Function("a, b, c", "return a+b+c")
new Function("a,b", "c", "return a+b+c")

 Functionオブジェクトは以下のプロパティを持つ。

Function.length	1を返す
Funtion.prototype	内部オブジェクトが割り当てられている

 また、Function.protoypeオブジェクトは内部オブジェクトであるため、関数として実行すると常にundefinedを返す。また、下記のプロパティを持つ。

Function.prototype.apply(thisArg, argArray)	thisArgをthis変数に設定し、argArrayを引数として関数を実行する
Function.prototype.bind(thisArg, ...args)	thisArgをthis変数に設定し、argsを引数として呼び出される束縛関数を作成して返す
Function.prototype.call(thisArg, ...args)	thisArgをthis変数に設定し、argを引数として関数を実行する
Function.prototype.constructor	コンストラクタ関数。初期値は内部オブジェクト
Function.prototype.toString()	オブジェクトの内容を示す文字列を返す
Function.prototype[@@hasInstance](V)	v instanceof Fに相当

 なお、Functionオブジェクトのインスタンスはlength、name、prototypeというプロパティを持つ。lengthは関数の引数の数、nameは関数名が格納されている。

ECMAScript 2015の仕様書を読む(その7)

 ECMAScript 2015の仕様書(ECMA-262)を読んでいます。今回は基本オブジェクト(Fundamental Objects)についての第19章のうち、Objectオブジェクトについて。

第19章第1節

 ECMAScript 2015では基本オブジェクトとしてObject、Function、Boolean、Symbol、Errorの5つのオブジェクトが用意されている。どのオブジェクトも関数オブジェクトであり、コンストラクタとして呼び出すことができる。第1節では、Objectオブジェクトについて説明されている。

 Objectオブジェクトをnewキーワードを付けずに関数として呼び出すと、その引数をオブジェクト化したものを返す。たとえば数値を引数に与えた場合、その数値をNumberオブジェクトにしたものを返す。引数がnullやundefinedだった場合は新たに空のオブジェクトを作成して返す。また、newキーワード付きで呼び出すと、その引数で指定したオブジェクトのコンストラクタにその引数を与えて実行した結果を返す。たとえば、「new Object(“hoge”)」は「new String(“hoge”)」と同じ結果となる。

 Objectオブジェクトは次のメソッド/プロパティを持つ。

Object.assign(target, ...sources)	sourcesオブジェクトのenumerableなプロパティを、その添え字をキー、値を値としてtargetオブジェクトにコピーする。戻り値はtargetオブジェクト
Object.create (O[, Properties])	Oオブジェクトを元に新しいオブジェクトを作成する
Object.defineProperties(O, Properties)	Oオブジェクトにプロパティを追加する。そのプロパティがすでに定義されていた場合は指定されたものに更新する。Propertiesはキーとして対象のプロパティ、値としてそのプロパティの情報(ディスクリプタ)を格納したオブジェクトを持つオブジェクト
Object.defineProperty(O, P, Attributes)	OオブジェクトにPプロパティを追加する。Attributesはそのプロパティの情報を格納したオブジェクト(ディスクリプタ)
Object.freeze(O)	Oオブジェクトをフリーズする。フリーズされた(frozen)オブジェクトは変更ができなくなる
Object.getOwnPropertyDescriptor(O, P)	OオブジェクトのPプロパティのディスクリプタを返す
Object.getOwnPropertyNames(O)	Oオブジェクトのプロパティ名一覧を含む配列を返す
Object.getOwnProertySymbols(O)	Oオブジェクトのシンボル一覧を含む配列を返す
Object.getPrototypeOf(O)	Oオブジェクトのプロトタイプを返す
Object.is(value1, value2)	value1とvalue2が同じ値であればtrueを返す
Object.isExtensible(O)	Oオブジェクトが拡張可能であればtrueを返す
Object.isFrozen(O)	Oオブジェクトがフリーズされていればtrueを返す
Object.isSealed(O)	Oオブジェクトがsealed状態であればtrueを返す
Object.keys(O)	Oオブジェクトのプロパティ名一覧を含む配列を返す
Object.preventExtensions(O)	Oオブジェクトの拡張を禁止する。拡張が禁止されたオブジェクトには、新たなプロパティを追加できなくなる
Oject.prototype	オブジェクトのプロトタイプを格納するプロパティ
Object.seal(O)	Oオブジェクトをsealed状態にする。sealed状態のオブジェクトに対しては新たなプロパティを追加できず、またプロパティの設定を変更できなくなる
Object.setProtptypeOf(O, proto)	Oオブジェクトのプロトタイプをprotoオブジェクトに変更する

 オブジェクトのプロパティには「データプロパティ(Data Property)」と「アクセサプロパティ(Accessor Proerty)」がある。Dataプロパティは「Value」「Writable」「Enumerable」「Configurable」という属性を持つ。また、アクセサプロパティは「Get」「Set」「Enumerable」「Configurable」という属性を持つ(6.1.7.1)。これら属性を設定/変更するのがdefinePropertiesやdefineProperty。各属性の説明は下記の通り。

Value	そのプロパティに設定された値
Writable	プロパティへの値の書き込みが可能かどうか(Boolean)
Enumerable	そのプロパティがfor-inループでの列挙対象となるか(Boolean)
Configurable	そのプロパティの削除や属性変更が可能かどうか(Boolean)
Get	プロパティに対応するgetter関数
Set	プロパティに対応するsetter関数

 これらを以下のような形で格納したオブジェクトはプロパティディスクリプタと呼ばれる。

{
  value: 値,
  writable: (true|false),
  enumerable: (true|false),
  ...
}

 オブジェクトには拡張の不可や「frozen」や「sealed」といった「Ingegrity Level」を設定できる。まず拡張の不可だが、拡張が不可である場合オブジェクトに新しいプロパティを追加できなくなる(削除は可能)。また、sealed状態のオブジェクトは拡張不可となり、さらに各プロパティの属性が変更不可能になる(プロパティのconfigurable属性がfakseになる)。データプロパティの値を変更することは可能。いっぽうfrozen状態のオブジェクトは拡張不可で各プロパティの属性は変更できなくなる点はsealedと同じだが、さらにデータプロパティの値変更も不可となる(プロパティのwritable属性がfalseになる)。ただし、setter/getterについては実行可能。

 また、Object.prototypeオブジェクトは次のメソッド/プロパティを持つ。

Object.prototype.constructor	オブジェクトのコンストラクタ。デフォルトはObjectに相当する内部オブジェクト
Object.prototype.hasOwnProperty(V)	Vプロパティを持っていればtrue
Object.prototype.isPrototypeOf(V)	Vオブジェクトのプロトタイプがオブジェクトのプロトタイプと一致すればtrueを返す
Object.prototype.propertyIsEnumerable(V)	Vプロパティが列挙可能であればtrueを返す
Object.prototype.toLocaleString([reserved1[ , reserved2]])	オブジェクトのtoStringメソッドを実行しその結果を返す。現状Array、Number、Date、Typed Arraysのみが固有のtoLocaleStringを持つ
Object.prototype.toString()	オブジェクトが持つ情報を文字列に変換して返す
Object.prototype.valueOf()	オブジェクトのプリミティブ値を返す

ECMAScript 2015の仕様書を読む(その6)

 ECMAScript 2015の仕様書(ECMA-262)を読んでいます。エラー処理および言語仕様の拡張についての第16章、標準組み込み(ビルトイン)オブジェクトについての第17章、グローバルオブジェクトについての第18章。

第16章

 エラー処理についてと言語仕様の拡張について。「early error」という種別のエラーが検出されたら、その時点でスクリプトの評価を止めてそれを通知する。また、モジュール内でのearly errorが検出されたら、その時点でモジュールの評価は中止されモジュールは初期化されない。evalキーワードでスクリプトが実行された際にearly errorが発生した場合、その評価をその時点で終了される。

 このearly errorの発生条件は仕様書内に細かく記載されているが、いわゆる文法エラーがこれに相当する。なお、ECMAScriptの処理系はSyntax ErrorおよびReferenceError以外のエラーについては詳細にそれをレポートするべきである。

 言語仕様の拡張については、16.1で拡張してはいけない仕様についてが説明されている。こちらについては一般的にコードを書く際には知識は不要なので割愛。

第17章

 ECMAScriptの標準ビルトインオブジェクトについて。

  • 標準ビルトインオブジェクトの多くは関数オブジェクト
  • 関数オブジェクトのうち、一部はnew演算子付きで実行することが想定されているコンストラクタ
  • ビルトイン関数には必要な引数よりも少ない引数を与えることができる。その場合、足りない引数にはundefinedが渡される
  • 必要な引数よりも多い引数を与えることもできる。通常それは関数の実行時に評価はされるが、その後は無視される。ただし、このとき実装側でTypeError例外を投げることも許可されている
  • ビルトイン関数オブジェクトはlengthプロパティを持つ。このプロパティには、その関数に与える最大の名前付き引数の数が格納されている

第18章

 グローバルオブジェクトについて。グローバルオブジェクトは、コード実行が開始される前に作成される。これらオブジェクトをnew演算子で作成することはできないし、関数として実行することもできない。ECMAScript 2015ではいくつかのグローバルオブジェクトが規定されているほか、処理系によって独自のグローバルオブジェクトを用意することも許されている。

 ECMAScript 2015で定義されているグローバルオブジェクトは次の通り。まず値を示すオブジェクトが3つ。

  • Infinity
  • NaN
  • undefined

 いくつかの関数オブジェクト。

  • eval(x)
  • isFinite(number)
  • isNaN(number)
  • parseFloat(string)
  • parseInt(string, radix)
  • decodeURI(encodedURI)
  • decodeURIComponent(encodedURIComponent)
  • encodeURI(uri)
  • encodeURIComponent(uriComponent)

 いくつかのグローバルオブジェクトのコンストラクタ。

  • Array()
  • ArrayBuffer()
  • Boolean()
  • DataView()
  • Date()
  • Error()
  • EvalError()
  • Float32Array()
  • Float64Array()
  • Function()
  • Int8Array
  • Int16Array
  • Int32Array
  • Map()
  • Number()
  • Object()
  • Proxy()
  • Promise()
  • RangeError()
  • ReferenceError()
  • RegExp()
  • Set()
  • String()
  • Symbol()
  • SyntaxError()
  • TypeRrror()
  • Uint8Array()
  • Uint8ClampedArray()
  • Uint16Array()
  • Uint32Array()
  • URIError()
  • WeakMap()
  • WeakSet()

 そのほかのグローバルオブジェクト。

  • JSON
  • Math
  • Reflect

 各オブジェクトについての詳細は後の章で説明されている。

ECMAScript 2015の仕様書を読む(その4)

 ECMAScript 2015の仕様書(ECMA-262)を読んでいます。今回は関数とクラスの解説がある第14章。ここではアロー関数やclassキーワード、関数のデフォルト値、可変長引数など多くの新機能が説明されている。

第14章

 関数とクラスについて。14.1では、以前よりあったfunctionキーワードを使った関数定義が説明されている。functionキーワードでの関数定義では、新たにRest Parameterという仕組みが導入された。これは、下記のように記述することで、可変長引数をより簡単に扱える仕組み。

function 関数名(引数1, 引数2, […,] ...残り引数)

 たとえば以下のようにすると、foo関数に与えた3つめ以上の引数は配列restに格納される。

function foo(arg1, arg2, ...rest) {}

 また、引数では新たにInitializerを使用できる。「引数=値」のように指定することで、その引数がundefinedだった場合、指定した値がセットされる。たとえば次の関数では、2つめの引数がundefinedだった場合、arg2には1が代入される。

function foo(arg1, arg2 = 1) {}

 14.2ではアロー(「=>」)を使った関数定義(アロー関数)が説明されている。アローを使った関数定義は、簡単にまとめると以下のようになる。

() => {関数定義}
(引数1) => {関数定義}
(引数1, 引数2, [...]) => {関数定義}
(引数1, 引数2, [...], ...rest) => {関数定義}

 {関数定義}の代わりに戻り値を直接記述することも可能。たとえば次の関数は、与えた引数の和を返す。

(a, b) => a + b

 なお、アロー関数内ではargumentsやsuper、this、new.targetが定義されず、関数を定義したレキシカル環境のそれぞれをそのまま継承する。

 14.3はメソッド定義についての説明。新たにget/setキーワードが導入されている。メソッド定義の際にメソッド名の前に「get」もしくは「set」を付与することで、getter/setterを定義できる。このメソッドに対しプロパティのように値を代入すると、代入した値を引数として定義したsetter関数が実行される。また、プロパティのように参照すると定義したgetter関数が実行され、その戻り値が返される。なお、getter関数には引数は与えられない。

 14.4は新たに実装されたgenerator関数についての説明。メソッド定義時の際にメソッド名の前に「」を付与することで定義できる。generator関数では、yieldキーワードを使って関数の途中で実行を中断し、そ時点で指定した値を返すことが可能。yieldもしくはyieldキーワードで中断された関数を再度呼び出すと、その続きから実行が再開される。

 14.5ではclassキーワードなどを使ったクラス定義が説明されている。classキーワードもECMAScript2015の新機能。次のようなスタイルでクラスを定義できる。

class クラス名 [extends 継承するクラス名] {
  クラスの定義
}

 クラスの定義では、staticキーワードを使ったメソッドの定義が可能。

 14.6では末尾再帰の最適化についての説明。再帰的に実装されているコードをループに展開する流れに書いてある処理系による実装の話なので、JSコードを書く側としてはあまり意識する必要は無いはず。

ECMAScript 2015の仕様書を読む(その3)

 ECMAScript 2015の仕様書(ECMA-262)を読んでいます。今回は第12章と第13章。ここではひたすら定義が並んでいるので斜め読みして、必要に応じて戻ることにする。

第12章

 ECMAScriptの式について。deleteやvoid、typeof、instanceofなどを含む演算子の挙動についても解説されている。新キーワードであるyieldも登場。

 yieldはPythonのyieldと同様の処理を行うもので、関数の実行を中断して戻り値を返すというもの。その関数オブジェクトのnextメソッドを実行することで中断したところから実行を再開できる。

第13章

 文と宣言について。新しいキーワードであるletやconst、classなどが登場する。また、新たな構文であるfor〜of文も登場。

 letとconstはvarと同じく変数定義に使うキーワード。letはレキシカルスコープのみで通用する、いわゆる局所変数を定義するのに使い、constは定数として宣言するのに使う。

 また、新たにfor〜of文も登場している。for〜in文と似ているが、プロパティ名ではなくプロパティの値に対して反復処理を行う点が異なる。

ECMAScript 2015の仕様書を読む(その1)

 ECMAScript 2015の仕様書(ECMA-262)を最近読み始めたので、従来のECMAScript 3/5との差異を中心に各章の要点を軽くまとめていく。

 今回は第1章〜第9章まで。ここではECMAScript処理系の内部実装について説明されており、ECMAScriptを使うだけであれば理解は不要。CでNode.js向けモジュールを実装する場合などは知っておくと良いかもしれない。

第1章

 この文書はECMAScript 2015について定義されているということが書いてある。

第2章

 ECMAScript実装が必要とする要件等が書いてある。

第3章

 関連文書について言及。ECMAScript 2015では国際化機能が実装されており、それは別文書(ECMAScript 2015 Internationalization API Specification)でまとめられている

 また、JSONの仕様はStandard ECMA-404 The JSON Data Interchange Formatでまとめられている。

第4章

 ECMAScriptの概要や歴史が書いてある。オブジェクト指向、プロトタイプベースの敬称といった特徴が説明されている。また、用語についても説明されている。この辺りは以前のECMAScriptから変更はない。

第5章

 このドキュメントにおける表記法が説明されている。

第6章

 ECMAScriptで利用できるデータ型と値の説明。こちらも以前からの変更はなし。数値型は64ビット倍精度浮動小数点とか、2の53乗-2がNaNに割り当てられているとか、Infinity(無限大)が定義されているとかが書かれている。オブジェクトが内部的に実装しているメソッドとかの話もあるが、これらはECMAScriptコードからは隠蔽されているのでECMAScriptコードを書く用途では特に覚えなくてもOK。

第7章

 型変換ルール(ObjectやSymbolはtrueになるとか、Stringは空文字列の場合のみfalseになるとか、文字列を数値に変換するルールとか)やオブジェクト型の判定ルール、2つの値が同一かどうかの比較ルールなどが説明されている。

第8章

 ECMAScriptがどうやってコードを実行するのか、という話。

  • ECMAScriptのレキシカル環境は関数宣言やブロックステートメント、TryステートメントとのCatch節などで開始されるよ
  • どのレキシカル環境にも属さない環境はグローバル環境と呼ばれるよ
  • モジュール環境や関数環境といった環境も存在するよ
  • ECMAScriptのコードは評価前に「Realm」(領域)というものに関連付けられるよ
  • 実行コンテキストというものがあるよ
  • ジョブやジョブキューというものがあるよ
  • 一つの実行コンテキストが終了したら、ジョブキューからジョブを1つ取り出してそこから実行コンテキストを作成するよ

第9章

 オブジェクトの振る舞いの話。すべてのオブジェクトはPrototypeという内部スロットを持ち、これを使ってオブジェクトの継承を実現している。また、Extensibleという内部スロットもあり、これがfalseの場合Prototypeスロットは変更が禁止される。

 関数オブジェクトは「strict function」と「non-strict function」の2種類が存在しうる。どちらもレキシカル環境を内包するほか、基底オブジェクトなのか派生オブジェクトなのかといった情報や、関数の種類(normal、classConstructor、generator)といった情報を内部的に持っている。

 ECMAScriptにはいくつかのビルトイン関数があるが、これらの関数オブジェクトは特殊なビルトイン関数オブジェクトとして定義されている。これらのオブジェクトは通常の関数オブジェクトとは異なり、一部の内部メソッドなどを持たないなどの違いがある。

 特殊なオブジェクトとして、Exoticオブジェクトというものがある。9.4節ではExoticオブジェクトとしてArrayオブジェクトやStringオブジェクト、Intger Indexedオブジェクト、Module Namespaceオブジェクトについて説明されている。Exoticオブジェクトはオブジェクトとしての特性を持ちつつ、特別な特性も実装されている(たとえばArrayオブジェクトであればlengthプロパティを持っていたり、インデックスでのアクセスが可能であったり)。

 さらに、その一部のみがECMAScriptで実装されているProxyオブジェクトというものも存在する。これは、ほかの言語で実装されたコードをECMAScriptコードから呼び出すために使われる。

Node.js 4.xで廃止となったutil.is系メソッドの代替案

Node.jsの最新長期サポート版(LTS)であるv4.3.0では、それまで利用できたutil.isArray()やutil.isRegExp()、util.isDate()などのメソッドがDeprecated(廃止予定)扱いとなっています(v4.x系のドキュメントv0.12.x系のドキュメント)。

ドキュメントには理由が説明されておらず、代替案も掲載されていないのですが、StackOverflowでの情報によると、「後方互換性がなくなるために修正したくないから」だそうです。例としてutil.isObject()に関数オブジェクトを与えると戻り値はfalseになりますが、JavaScript(ECMAScript)の言語仕様上は関数オブジェクトもオブジェクトであり、これは不適切な実装であることが挙げられています。

isArrayとかisStringとかはあっても良いのでは……と思いますが、まあほかのコードで代替可能なのでコアからは削除する、というのは理解できます。ということで代替案ですが、下記のようにisinstanceof演算子で代替できます。

$ node
> [] instanceof Array
true
> [] instanceof Object
true
> [] instanceof String
false

なお面倒臭いことに、instanceof演算子はプリミティブ型(文字列や数値型など、オブジェクトではないもの)に対してはすべてfalseを返します。そのため、プリミティブ型に対してはtypeof演算子の戻り値を使用して判断する必要があります。

> typeof a
'string'
> typeof []
'object'
> typeof 10.1
'number'
> typeof /ABC/
'object'