DOM オブジェクトとメモリリーク2005年12月04日 20時21分

IE でのメモリリーク

ちょこちょこと紹介されているので知っている人も多いと思うが、IE には DOM ノードに絡んだメモリリークの問題がある。これに関しては Microsoft 自身の記事である「Understanding and Solving Internet Explorer Leak Patterns」に詳しいが、簡単にいえば DOM ノードオブジェクトに関する循環参照を作ると、IE を終了させるまでそのオブジェクトが解放されないというものだ。記事によればメモリリークには以下のようなパターンがあるという。

1. 単純な循環参照

ある DOM ノードオブジェクトのプロパティをたどっていくと自分自身に行き着く場合。以下のようなパターンが考えられる。

element.property == element
element1.property1 == element2, element2.property2 == element1

2. クロージャにまつわる循環参照

クロージャはそのクロージャが定義されたスコープへの参照を持っている。下の例でいうならば、element.onclick にセットされているクロージャは、addClickAction 関数の引数である element への参照を持っているということだ。また、element のほうはというと onclick イベントにクロージャが結び付けられているため、element →クロージャ→ element という循環参照ができてしまっている。

function addClickAction(element)
{
  element.onclick = function () { doSomething; };
}

3. 文書ツリーに属していない要素

これは循環参照とは異なる問題だが、IE では文書ツリーに属していない要素 (createElement() で作成したあとどこにも挿入されていない要素) に対してイベントハンドラを結びつけることもメモリリークを招くらしい。もっともこの場合は先ほどと異なり、リークするメモリはほんの少し (たった数バイト) だそうだ。解決策としては要素を文書ツリーに追加してからイベントハンドラを結びつけることが挙げられる。

解決策

そして実際の対策であるが、1 は DOM ノードオブジェクトに直接 DOM ノードオブジェクトを結び付けないようにすればいいし、3 はよほど大量の作業をしない限りは影響が出ないだろうからいいとして (なくすにこしたことはないが) 、問題は 2 だ。JavaScript ではクロージャを使うことで柔軟な表現が可能になるので、それが使えないとなると困ってしまう。

そこで他の人がどうしているかを見ると、prototype.js ではイベントを結びつけるたびにその内容を記録し、文書が破棄されるとき (unload イベント発生時) に removeEventListener または detachEvent でそれらを解除するという方法をとっている。また、「Leak Free Javascript Closures」(「Collection & Copy」経由) では、クロージャを直接要素に結び付けるのでなく、そのクロージャを実行する関数を作りそれを要素に結びつけるようにしている。クロージャを実行する関数は実際のクロージャを取得するためのハッシュキーしか持っていないので参照が途切れるというわけだ。

なお、一見メモリリークを引き起こしそうだが実はそうでないパターンもある。以下の例がそうだ。

function addActions()
{
  var links = document.getElementsByTagName("a");
  for (var i = 0; i < links.length; i++)
    links[i].onclick = function () { alert("Hello!"); };
}

クロージャが a 要素全体への参照を持っているように思えるが、links の内容は動的 (あとから a 要素が追加 / 削除された場合、それは links にも反映される) であり、links が直接特定の a 要素への参照を持っているわけではないので循環参照にはならないと考えられる。

メモリリークが起こるわけ

さて、そもそもなぜメモリリークが起こるのかといえば、JavaScript / JScript と DOM ノードオブジェクトではガベージコレクタの仕組みが異なるからである。JavaScript / JScript では循環参照があってもメモリを解放してくれるのだが、DOM ノードオブジェクトは参照カウンタにより管理されているので循環参照があるとメモリが解放されずに残ってしまうのである。

そして、DOM ノードオブジェクトを参照カウンタで管理しているのは何も IE だけではない。Gecko (Firefox) も DOM ノードオブジェクトは XPCOM コンポーネントとして実装されているので、その管理は参照カウンタで行われている。ということは同じ問題は Gecko にもありそうだし、事実あったようだ。(修正されたのが最近なので Firefox 1.5 では修正されていない。)

その他のブラウザの実装がどうなっているかは知らないが、同じような実装である可能性は否定できないので、クロージャやイベントハンドラなどを組み合わせて使う場合はメモリリークに気をつけたほうがいいかもしれない。

とはいっても書いてる本人もよくわかってない部分が多いので (特に GC 周り) どしどし突っ込んでもらいたい次第。

それにしても MS の文書にはタスクマネージャを確認してなどと書いてあるけどそれでいいのだろうか? 中野さんはタスクマネージャを見ただけではメモリリークと断言できないといってるのだが。

参考

(本文中で触れたものは除く)

基底クラスのコンストラクタを呼び出す2005年12月05日 00時06分

【JavaScript】多重に派生されたクラスのコンストラクタで,基底クラスのコンストラクタを呼び出す方法」(Graviness Blog) にて基底クラスのコンストラクタを呼び出すという話題が出ていた。優乃さんは this から基底クラスのコンストラクタを呼び出したいようだが、JavaScript では this が何をさすかは文脈により異なってくる (「ECMAScript における this の意味」(Noncommutative Field) を参照)。なので個人的には基底クラスに関する操作はインスタンス (this) よりもクラス (コンストラクタ関数) から行ったほうがいいと思う。クラス名に依存したくないなら arguments.callee を用いればよい。

function Animal() {} // 基底クラス

function Dog() {
  this.__super__(); // this が何であるか (this.__super__ が何をさすか) は不定
}

function Cat() {
  Cat._superClass.apply(this); // クラス名に依存
}

function Swallow() {
  arguments.callee._superClass.apply(this); // クラス名に非依存、でもちょっと長い?
}

しかし実際に書き比べてみるとやはり this から呼び出せたほうが見通しがいいわけで。this から呼び出す際の問題は基底クラスのコンストラクタ内でも this.__super__ が変わらず、それ以上上のクラスのコンストラクタを呼び出せないこと。ならば基底クラスのコンストラクタを呼び出すときだけ __super__ を書き換えてやればいいのではないか。こんな感じで。

function inherit(subClass, superClass) {
  var Temp = new Function();
  Temp.prototype = superClass.prototype;
  subClass.prototype = new Temp;
  subClass.prototype.constructor = subClass;
  subClass.prototype.__super__ = function () {
    var originalSuper = this.__super__;
    this.__super__ = superClass.prototype.__super__ || null;

    superClass.apply(this, arguments);

    if (this.constructor == subClass)
      delete this.__super__;
    else
      this.__super__ = originalSuper;
  };
}

個人的に Class.prototype.constructor == Class というのは常に成り立っていてほしいのでそのように修正。基底クラスのコンストラクタ内で this.method() としても SuperClass#method ではなく SubClass#method が呼び出されるなどの不具合はあるが、コンストラクタ関数だけで完結する処理ならこれでうまく行くと思う。

function SuperSuperClass() {
  alert("SuperSuperClass Constructor");

  this.p = "supersuperclass";
}

function SuperClass() {
  alert("SuperClass Constructor");
  this.__super__(); // SuperSuperClass のコンストラクタを呼び出す.

  this.q = "superclass";
}

function SubClass() {
  alert("SubClass Constructor");
  this.__super__(); // SuperClass のコンストラクタを呼び出す.

  this.r = "subclass";
}

// 継承
inherit(SuperClass, SuperSuperClass);
inherit(SubClass, SuperClass);

var a = new SubClass();
alert(a.p); // "supersuperclass"
alert(a.q); // "superclass"
alert(a.r); // "subclass"

基底クラスコンストラクタ呼び出しの落とし穴2005年12月09日 13時14分

基底クラスのコンストラクタを呼び出す話に関連して。「Graviness Blog」と「JavaScriptっぽい。」で示された解決策には少し問題がある。inherit(SubClass, SuperClass) としたとき、SuperClass 内での this の参照先が SubClass のインスタンス (SubClass 内での this) ではなくなってしまうのだ。「Graviness Blog」でのやり方では SubClass.prototype 、「JavaScriptっぽい。」でのやり方では SuperClass.prototype になってしまう。つまり SuperClass 内で this にセットした値は SubClass のインスタンス間で共有されるということだ。

例えば以下のようにすると okaka の名前が変わってしまう。これはちょっとまずいと思う。

// Graviness Blog
function inherit(subClass, superClass) {
  var Temp = new Function();
  Temp.prototype = superClass.prototype;
  subClass.prototype = new Temp;
  subClass.prototype.__super__ = superClass;
  subClass.prototype.constructor = subClass;
}

function Animal(name) {
  this.name = name;
}

function Cat(name, coat) {
  Cat.prototype.__super__(name);
  this.coat = coat;
}

inherit(Cat, Animal);

var okaka = new Cat("Okaka", "calico");
alert(okaka.name); // "Okaka"
var shiro = new Cat("Shiro", "white");
alert(okaka.name); // "Shiro"
// JavaScriptっぽい。
function inherit(subClass, superClass) {
  var Temp = new Function();
  Temp.prototype = superClass.prototype;
  subClass.prototype = new Temp;
  subClass.prototype.constructor = subClass;
  subClass.prototype.superclass = superClass.prototype;
}

function Animal(name) {
  this.name = name;
}

function Cat(name, coat) {
  this.superclass.constructor(name);
  this.coat = coat;
}

inherit(Cat, Animal);

var okaka = new Cat("Okaka", "calico");
alert(okaka.name); // "Okaka"
var shiro = new Cat("Shiro", "white");
alert(okaka.name); // "Shiro"

Function Expression Statements2005年12月10日 21時32分

もじら組フォーラムで書いたことを再掲。以下 JavaScript といった場合 JScript などは含まないものとする。

JavaScript の function

ECMAScript 3 で function キーワードが使われる構文には FunctionDeclarationFunctionExpression があり、以下のように定義されている。

FunctionDeclaration
function Identifier ( FormalParameterListopt ) { FunctionBody }
FunctionExpression
function Identifieropt ( FormalParameterListopt ) { FunctionBody }
FunctionBody
SourceElements
Program
SourceElements
SourceElements
SourceElement
SourceElements SourceElement
SourceElement
Statement
FunctionDeclaration

これをみればわかるとおり、FunctionDeclaration はプログラム直下か関数本体の直下にしか置けない。下の例でいうと (1) は問題ないが (2) は ECMAScript としては正しくないということだ。(2) の場合 gFunctionExpression とみなされるのではと思うかもしれないが、ExpressionStatementfunction キーワードで開始することはできないのでそれも成り立たない。

// (1)
function f() {
  function g() {}
}

// (2)
function f() {
  do {
    function g() {}
  } while (false);
}

しかし、JavaScript ではバージョン 1.5 から Function Expression Statements という機能が追加され、通常の文が置ける場所ならどこでも関数を定義できるようになった (すなわち JavaScript では (2) も正しいコードである) 。ここではこれを仮に FunctionStatement と呼ぶことにする。

FunctionStatement
function Identifieropt ( FormalParameterListopt ) { FunctionBody }

FunctionDeclarationFunctionStatement の最大の違いは処理されるタイミングである。FunctionDeclarationコンパイル時に処理されるその宣言が含まれる関数 (宣言がトップレベルにある場合はプログラム全体) の実行に先立って処理されるFunctionStatement は実行時に処理される。また、FunctionExpression との違いとして、FunctionExpressionIdentifierFunctionBody 内でのみ有効だが、FunctionStatement では文の評価後にその Identifier を使って関数を参照できるようになることが挙げられる。

以上の特徴から、if 文を使った関数定義の振り分けなどもできる。

// (3)
var condition = true;

if (condition) {
  function f() {
    return true;
  }
} else {
  function f() {
    return false;
  }
}

alert(f()); // JavaScript (Firefox など) では true

また、FunctionStatement が実行時に処理されるということは、その文の評価前はその関数を参照できないということでもある。以下のコードは JavaScript では例外が投げられる。

// (4)
try {
  f();
  function f() {
    alert("Succeed!");
  }
} catch (e) {
  alert(e); // ReferenceError: f is not defined
}

なお、FunctionStatementIdentifier が存在する場合、FunctionStatement の文としての返り値は空値 (empty) になるが、Identifier が存在しない場合はその関数オブジェクトが返る。ただし Firefox 1.0 では Identifier を省略できない (省略するとシンタックスエラーになる) 。

JScript の function

だが、Function Expression Statements はあくまでも JavaScript だけの機能であり、JScript などでは実装されていない。しかし、JScript や Opera の ECMAScript 実装でも任意の場所に function 文を置くことができる。ただし、それが処理されるのはコンパイル時であり、JavaScript のような動的な関数定義はできない。それが証拠に IE や Opera で上記 (3) のコードを実行してみると false と表示される。

さらに、JScript の場合は、FunctionExpression さえもコンパイル時に処理し、その Identifier を変数オブジェクトに登録してしまう。結果以下のコードで "Hello" と表示されてしまうのである。これは明らかに ECMAScript に違反している。

// (5)
alert(f()); // "Hello"
var g = function f() { return "Hello"; };
alert(f == g); // false

JavaScript で引数束縛2005年12月13日 18時14分

引数束縛 (カリー化) の話。まずは「JavaScriptでカリー化」(檜山正幸のキマイラ飼育記)。タイトルを見てどこかで聞いたような話だなと思ったら「関数の変形」(Effecttive JavaScript - Dynamic Scripting) だった。だが、前者は文字列に戻してから評価というのが力技っぽくて個人的に好きでないし、後者は汎用的過ぎていささかわかりにくい。そこで今回は先頭の引数から束縛していくというのに的を絞ってみたいと思う。

まず第 1 引数のみを束縛する場合。Function#apply を使えば引数を配列として渡せるので、束縛された値と後から渡された引数とを連結してやればいいのではないか。

function curry(func)
{
  return function (first) {
    return function () {
      var args = Array.prototype.concat.apply([first], arguments);
      return func.apply(this, args);
    };
  };
}

function sum(x, y) { return x + y; }
function mean3(a, b, c) { return (a + b + c) / 3; }

alert( curry(sum)(10)(15) );               // 25
alert( curry(mean3)(10)(20, 30) );         // 20
alert( curry(curry1(sum))(10)()(20) );     // 30
alert( curry(curry1(mean3)(10))(20)(30) ); // 20

それなりにいい感じである。檜山さんが動かないといっていた最後のパターンもちゃんと計算できているようだ。では調子に乗って束縛する引数の数を可変にしてみよう。

function curry(func)
{
  return function () {
    var length = arguments.length;
    var heads = new Array(length);
    for (var i = 0; i < length; i++)
      heads[i] = arguments[i];

    return function () {
      var args = Array.prototype.concat.apply(heads, arguments);
      return func.apply(this, args);
    };
  };
}

alert( curry(sum)(10, 20)() );    // 30
alert( curry(mean3)(5, 15)(10) ); // 10

これでうまくいくと思ったら落とし穴が。引数に配列があるとそれを展開してしまうのだ。

alert( sum(1, [2, 4]) );        // 12,4
alert( curry(sum)(1)([2, 4]) ); // 3
// sum は二つしか引数をとらないので最後の 4 は無視されている。

そこで後から受け取る引数も配列に変換することに。

function curry(func)
{
  return function () {
    var length = arguments.length;
    var heads = new Array(length);
    for (var i = 0; i < length; i++)
      heads[i] = arguments[i];

    return function () {
      var length = arguments.length;
      var tails = new Array(length);
      for (var i = 0; i < length; i++)
        tails[i] = arguments[i];

      var args = heads.concat(tails);
      return func.apply(this, args);
    };
  };
}

alert( sum(1, [2, 4]) );        // 12,4
alert( curry(sum)(1)([2, 4]) ); // 12,4

さすがにコードが長くなってきたので似たような処理をまとめる。prototype.js にも同名のメソッドがあるが Array.from を定義。

Array.from = function (array) {
  var length = array.length;
  var result = new Array(length);
  for (var i = 0; i < length; i++)
    result[i] = array[i];
  return result;
};

function curry(func)
{
  return function () {
    var heads = Array.from(arguments);
    return function () {
      var tails = Array.from(arguments);
      var args = heads.concat(tails);
      return func.apply(this, args);
    };
  };
}

これでかなりすっきりした。カリー化された関数に対して、複数回の部分適用ができるようにしたものを「JavaScript でカリー化、再び」に書いた。ついでに Function.prototype に結びつければ個人的にはさらにすっきり。以下で定義してある curry メソッドがしていることは、その名に反してカリー化ではなく部分適用である。

Function.prototype.curry = function () {
  var heads = Array.from(arguments);
  var self = this;
  return function () {
    return self.apply(this, heads.concat(Array.from(arguments)));
  };
};

alert( sum.curry(10)(15) );             // 25
alert( mean3.curry(10)(20, 30) );       // 20
alert( sum.curry(10).curry()(20) );     // 30
alert( mean3.curry(10).curry(20)(30) ); // 20
alert( sum.curry(10, 20)() );           // 30

ななしさんからのヒントにより Array.from を使わなくてもよくなった。

Function.prototype.curry = function () {
  var args = arguments;
  var self = this;
  return function () {
    Array.prototype.push.apply(args, arguments);
    return self.apply(this, args);
  };
};

ななしさんnak2k さんのコメントにより Array.from を使わなくてもよくなった。

Function.prototype.curry = function () {
  var args = arguments;
  var self = this;
  return function () {
    Array.prototype.unshift.apply(arguments, args);
    return self.apply(this, arguments);
  };
};