特定のプロパティを除外した新しいオブジェクトを作る(immutableなdelete)
あるオブジェクトから、特定のプロパティのみ除外した新しいオブジェクトを作りたい場合がある。
const p1 = { x: 0, y: 1, z: 2, }; // p1からzプロパティを除外した新しいオブジェクトを作る const p2 = Object.assign({}, p1); delete p2.z; console.log(p2); // { x: 0, y: 1 }
最近の実装(およびtranspile前提の環境)であればECMAScriptのObject Rest Destructuringを使うことで、外部ライブラリやユーティリティ関数を用意しなくても1行で書くことができる。
const { z, ...p2 } = p1; console.log(p2); // { x: 0, y: 1 }
格納されるプロパティのキーや取り除きたいプロパティのキーが動的な場合は、ComputedPropertyNameと組み合わせることになる。
const key = 'z'; const { [key]: _, ...p2 } = p1; console.log(_); // 2 // うーん……
取り除かれたプロパティの値は使わないよという表明で`_
`にしたけど、そもそも余計な定数が定義されないようにしたい。そこでさらに空の分割代入を行う。
const key = 'z'; const { [key]: {}, ...p2 } = p1; // {}の代わりに[]でも可 // 不要な定数は定義されない!
これでよし!と思いきや、key
のプロパティが存在しなかったり、値がundefined
だった場合にエラーが起きてしまう。
const p1 = { x: 0, y: 1, }; const key = 'z'; const { [key]: {}, ...p2 } = p1; // TypeError: p1[(intermediate value)] is undefined
ということでデフォルト値を指定する。
const p1 = { x: 0, y: 1, }; const key = 'z'; const { [key]: {} = {}, ...p2 } = p1; // 問題なし!
これでよし!ただし、プロパティの値がnull
の場合はデフォルト値が採用されず、先ほどと同様にTypeErrorが投げられてしまうので、値にnull
が含まれ得るオブジェクトには使えない。
そのままreturn
する場合などは、IIFEで引数をdestructuringすれば1文で書けるけど、ここまでくるとさすがにやりすぎな感じがする。
switch (action.type) { case 'PAGE_CLOSE': return (({ [action.id]: {} = {}, ...nextState }) => nextState)(state); // ... }改めて考えると、IIFEを使う場合はローカル変数のことを気にする必要はないから、
_
で問題なかった。
switch (action.type) { case 'PAGE_CLOSE': return (({ [action.id]: _, ...nextState }) => nextState)(state); // ... }
書き捨てではない・読まれるコードでは、lodash/fpのomit
などを使おう……。
TypeScriptでもちゃんと型がomitされて幸せ。
とはいえもう型のomitも表現できるようになってるけど。
TypeScriptでReduxのActionを書くときの煩わしさを軽減するためのモジュールを作った
TypeScriptで型の恩恵を受けられるようにReduxアプリケーションのコードを書こうとすると、同じようなことを繰り返し書かなければならないところがある。顕著なのはAction周りだと思う。同じことを思う人が多いのか、既に楽に書くためのモジュールがたくさんある(ts-action, typescript-fsa, typesafe-actions, などなど)けど、Middlewareが必要だったり、Reducerの書き方を変えなければならなかったりしてどれもしっくりこない。
もっとシンプルに解決できないかなと思ってモジュールを作ってみた。実装としてはTypeScriptで14行、提供するのは2つのAPIのみ。
以下、Redux Async Exampleの例を借りて、pureにTypeScriptで書いたコードとこのモジュールを使って書いたコードを比較してみる。
続きを読むホイール操作でスライドをめくるユーザースクリプト
以前作ったslideshare scrollというユーザースクリプトでQiitaのスライドにも対応しようと思ったものの、3年前のコードで無駄が多いので一から書き直した。
createWheelHandler
で前後ボタンの取得を遅延させているのは、Qiitaのスライドが非同期で組み立てられるため。
GitHub上のJSコードでモジュールをリンクで辿れるようにするユーザースクリプト
JavaScriptでgetterとsetterのどちらか一方のみをオーバーライドする
JavaScript でオブジェクトに accessor property を定義したとき、継承先でそのプロパティの getter, setter のどちらか一方のみを override するのは一筋縄にはいかない。
例として、長方形を表す Rectangle
と、正方形を表す Square
を定義することを考える。Square
は Rectangle
を継承する。
switch文によるコールバック地獄回避
url.txtに書かれているURLを取得し、そのURLのリソースの内容を取得し、1秒後にその内容をalert
するという例(のための例)を考える。
まずは普通に書いてみる。
var client = new XMLHttpRequest; client.open('GET', 'url.txt'); client.onload = function (event) { var url = client.response; client.open('GET', url); client.onload = function (event) { setTimeout(function () { alert(client.response); }, 1000); }; client.send(null); }; client.send(null);
次にswitch
文を使って書いてみる。
var step = 0; var client; (function callback(arg) { switch (step++) { case 0: client = new XMLHttpRequest; client.open('GET', 'url.txt'); client.onload = callback; client.send(null); break; case 1: var url = client.response; client.open('GET', url); client.onload = callback; client.send(null); break; case 2: setTimeout(callback, 1000); break; case 3: alert(client.response); } }());
おまじないのswitch
文によってベースとなるインデントが深くなるものの、メインとなるコードは縦に揃う。コードの流れもopen
→send
→open
→send
→setTimeout
となり直観的。
……Duff's deviceのコードを見て思いついただけ。
JavaScriptでカスラムエラーをどう作るか
はじめに:IE 11、Firefox 28、Chrome 34で試している
JavaScriptのErrorオブジェクトにはstack
プロパティにコールスタックを表す文字列がセットされる。これは現行のECMAScript仕様では規定されておらず、実装によって違いがある。
function foo() { bar(); } function bar() { baz(); } function baz() { throw new Error('X'); } (function main() { try { foo(); } catch (error) { console.log(error.stack); } })();
Chromeでのコンソール出力の例。
Error: X at baz (http://localhost/CustomError.html:8:24) at bar (http://localhost/CustomError.html:7:18) at foo (http://localhost/CustomError.html:6:18) at main (http://localhost/CustomError.html:12:9) at http://localhost/CustomError.html:16:3
このように、ネイティブに用意されているエラーオブジェクト用のコンストラクタ(Error
, TypeError
, RangeError
など)を使う場合、stack
プロパティはどの実装でもセットされる。ただし、セットされるタイミングは少し異なり、Firefox・Chromeではオブジェクト作成時にセットされ、IEではthrow
されるときにセットされる。
var e = new Error('X'); console.log(e.stack); // IEではまだセットされていない try { throw e; } catch (e) { console.log(e.stack); // セットされる }
ネイティブに用意されているコンストラクタでは物足りない場合、Error
を継承した独自のエラーコンストラクタを作ることを思いつく。ところが、これだとIEでしかstack
プロパティがセットされない。
function TimeoutError(message) { Error.call(this, message); } TimeoutError.prototype = Object.create(Error.prototype, { constructor: Object.getOwnPropertyDescriptor(TimeoutError.prototype, 'constructor'), name: { value: 'TimeoutError', configurable: true, enumerable: true, writable: true } }); var e = new TimeoutError('session timeout'); try { throw e; } catch (e) { console.log(e.stack); // IEではコールスタックを表す文字列 // Firefoxでは空文字列 // Chromeではundefined }
IEではthrow
されるオブジェクトがError.prototype
を継承していればstack
プロパティがセットされるようだ。Firefox・Chromeはネイティブコンストラクタでのみstack
プロパティがセットされるように見える。
ではどうすればいいだろうか。コンストラクタを用意せず、継承もせず、こんなので十分だと思う。
var createError = (function () { var hasOwnProperty = Object.prototype.hasOwnProperty; return function createError(name, message, properties) { var e = new Error(message); e.name = name; if (properties) for (var key in properties) if (hasOwnProperty.call(properties, key)) e[key] = properties[key]; try { throw e; // for IE } catch (e) { return e; } }; })(); var timeoutError = createError('TimeoutError', 'session timeout'); var fetchError = createError('FetchError', '404 Not Found', { statusCode: 404 });
エラーオブジェクトを扱うときは、instanceof
ではなくname
プロパティからその種類を判別すればよい。ネイティブのエラーオブジェクトも同じように判別できる(ECMAScript仕様にて規定)。
new TypeError().name === 'TypeError'; fetch(url, { timeout: 10 * 1000 }).then(function (text) { // do something }, function (error) { switch (error.name) { case 'TimeoutError': alert('タイムアウトしました'); break; case 'FetchError': if (error.statusCode === 404) { alert('リソースが見つかりません'); break; } alert('リソースを取得できませんでした(' + error.statusCode + ')'); } });
createError
関数の分、コールスタックを一行消費してしまうのが惜しい。IE・Chromeでは、stack
プロパティのコールスタックは10行までのようだ。
(function () { function a() { b(); } function b() { c(); } function c() { d(); } function d() { e(); } function e() { f(); } function f() { g(); } function g() { h(); } function h() { i(); } function i() { j(); } function j() { k(); } function k() { l(); } function l() { throw new Error('HELLO?'); } try { a(); } catch (e) { console.log(e.stack); } })();
Chromeでのコンソール出力の例。
Error: HELLO? at l (http://localhost/CustomError.html:30:24) at k (http://localhost/CustomError.html:29:18) at j (http://localhost/CustomError.html:28:18) at i (http://localhost/CustomError.html:27:18) at h (http://localhost/CustomError.html:26:18) at g (http://localhost/CustomError.html:25:18) at f (http://localhost/CustomError.html:24:18) at e (http://localhost/CustomError.html:23:18) at d (http://localhost/CustomError.html:22:18) at c (http://localhost/CustomError.html:21:18)
ES6のObject.assign
があれば、それで十分かもしれない。
throw Object.assign(new Error('404 Not Found'), { name: 'FetchError', statusCode: 404 });