より安全なJavaScriptを書くために、あったらいいよねという機能

こんな記事があった。

My ECMAScript 7 wishlist | NCZOnline

大雑把にいうと、制限されたgetterがほしいという意見に記事のほとんどが割かれてる。

JavaScriptデバッグ中、一番つらいものの一つに、未定義値にアクセスしたときにundefinedが代入されており、その結果が次のアクセスにならないとわからないという点だと思う。

o = {
  a: () => 1,
  b: () => 2,
  c: () => 3,
  d: () => 4 
}

f = o.e // ここでエラーにならない
// 30行ぐらいのコードがあって忘れるとする
f() // エラー

これが辛い。これを回避するためにどんな仕様が必要か。

というわけで、自分がほしいものはなんだろうと考えてみた。(注意:この記事は上の記事の翻訳記事ではない)

僕自身があんまりES harmonyの議論追ってないので、もしかしたら現状でもできるようになるのはあるかも。こうやったらできるよ!ってのがあったら教えて下さい。

厳格なgetter

undefinedアクセスを厳格に禁止するオプションがあると型を意識した構造体を定義しやすくなる。 RubyPythonはそもそもアクセス出来ないけど、JavaScriptのすべてのオブジェクトは連想配列なので、代入できてしまう。

今の仕様でも Object.seal, Object.freeze と strict mode を組み合わせてメンバに対する書き換えを禁止できるけど、未定義値に対するgetterは阻止できない。

こんなことがしたい。

a = {};
Object.defineProperty(a, undefined, {get: (key) => throw key+' is not defined'});
a.foo; // Error: foo is not defined

(現状のこのコードだとa.undefined に対する定義になってしまう)

現時点でも今初期化時に指定したメンバしか書き換えできなくする方法はある、が、どうも見難くなる。

coffeeのコード例

'use strict'
class StrictPropertyClass
  properties: []
  constructor: (obj) ->
    obj ?= @properties
    if obj
      for key, val of obj
        @[key] = val
    else
      for key in obj
        @[key] = null
    Object.seal @
class A extends StrictPropertyClass
  properties: ['a', 'b', 'c']

class B extends StrictPropertyClass
  constructor: ->
    super({a:1, b: 2})

たとえばこれで初期化時に列挙したメンバしか定義できなくなるクラスが作れるが、毎回継承しないといけないし、記法を制限してしまう。

最初の記事だとこんなのできたらいいよねという提案をしている。

class Person {
    constructor(name) {
        this.name = name;
        Object.seal(this);
        Object.preventUndeclaredGet(this);
    }
}

あと Object.deepPreventExtensions(), Object.deepSeal(), Object.deepFreeze() がほしいとのこと。そうだよね感ある。

未定義値getterの定義

上の続きだけど、JavaScriptでテストでMockを作りたい場合、事前に構造を知らないと現状定義できない。 要はrspecのNullObject的なのがやりたい。

class Mock {
  constructor(){
    this.referenceCount = {}
    this.objects = {}
    Object.defineProperty(this, undefined, {
      get:(key) =>{
        if(this.refenceCount[key])
          this.refenceCount[key]++;
          return this.objects[key];
        else
          this.refenceCount[key] = 1;
          return this.objects[key] = new Mock;
      }
    });
  }
}
a = new Mock()
a.b.c.d.e

getterで再帰で自分自身と同じオブジェクトを返却しつつ、参照カウントをとれる。これはかなり雑な擬似コードだけど。

既存のundefinedに対しても定義できたら、あらゆるものがMock化できる。

Object.observe(window, undefined, () => new Mock)

とはいえ、これは過激すぎて、Mockのやりすぎは良くないんだけど現状が厳しすぎるので、何らかの緩和策がほしい。require先を解析してMockを生成するfacebook/jest みたいなのが生まれた例もあるし。

デストラクタ

デストラクタでイベントフックするというより、デバッグ時にちゃんと死んだか管理したいモチベーションがある。

WeakMap#keys をとれてもいいのかなぁと思ったが、時間を置いて WeakMap#keys の状態を比較してデストラクタ発火させるの、その行為自体が参照を得ることになるので、デストラクタの実装には使えない。

今自分が仕事で使ってるChaplin.jsは、 コントローラが死ぬときに子供のViewとModelのdisposeを読んで回るような設計になってるんだけど、すべてmediatorへ登録しておかないといけないし、専用のdispose関数を定義しておかないといけない。だるい。

これに関しては開発者オプションでもいい。

Object.observeDeep

現状の仕様だと直下のプロパティまでしか監視できない。

a = {
  b: {
    c: 1
  }
}
Object.observeDeep(a, () => console.log 'changed');
a.b.c = 2 // changed

これができたら嬉しい。ただ、あったらいいなとは思うが、内部実装のオブジェクト間のメッセージパッシングがぼろぼろになる気はする。

今の仕様でも専用のsetter作ってアクセスを限定することで実現できるが、結局それdirty checkやってたのと同じではという気はする。Angular2.0がたぶんそうなる。

MVCライブラリでビューモデルの値の監視を実装するの、未だに結構難しい。(というかそもそもChrome35でしかObject.observeまだ入ってないわけで)

スコープの参照制限

コールバックまみれの時にC++ラムダ式みたいに参照を制限したい時がある。

a = 1
b = 2
setTimeout([a]() => b) // b is not defined

GCに優しく、というか、もっと意味的なコンテキストを明示したいときがある。シンボルが混ざってわけがわからなくなることがある。

参照の実体のdelete

foo = {};
obj = {};
obj.foo = foo;
delete obj.foo;

このコードだと obj.foo での foo への参照は消えてるんだけど、fooインスタンスは生きてる。これがトップレベルのスコープだとした場合、GCさせるには delete foo だとか foo = nullするしかない。スコープにキャッチされてる可能性があるので、これで本当に死ぬかはわからない。

delete obj.foo*;

とかしたい。この記法がいいかはともかく。(実体参照の記法いれると全体的に泥沼化するので、何らかの制限は必要だと思う)

今でも一応こんな関数を書くことはできる

deleteDeep = (obj) ->
  if obj instanceof obj
    for own key in obj
      v = obj[key]
      if v instanceof Object
        killObject(v)
      delete obj[k]
    Object.freeze(obj)
  null

とはいえ、これも先に述べたundefined getterの禁止と組み合わせないとあまり意味が無い。

というか明示的に殺したい時ってどこの参照であろうが問答無用で殺したい時なので、そこで出る例外は受け入れたいというか、そこで出るのはいわゆるバグなので…

Array強化

es5のmapとreduceでだいぶ楽になったんだけど、まだちょっと足りないと思う。

  • Array#find
  • Array#first
  • Array#last _ Array#flatten

ここらへんまではデフォルトで入ってほしい。firstは[0]でいいとして、現状のlastはだるすぎる。lastItem = list[list.length-1]と書きたくない。そういえば今度coffeeだとpythonのlist[-1] が入るらしい。

findがないのは単に辛い。毎回自分で書いてる。

async/await

はい。まあ、yieldというかcoの糖衣構文になるんだけど…

switch強化

もうこれはただの願望だけど、なんかもうちょっと賢くなってほしいよね!パターンマッチとかね!

現場からは以上です。

追記