JavaScript の変数と delete 演算子2008年01月09日 07時13分

Kanasan.JS JavaScript 第 5 版読書会 #1 にて delete 演算子の動作が話題に上ったそうです。そこで、それについてちょっとまとめてみようかと思い立ったはいいものの、ずるずると引き伸ばしているうちに年を越してしました。しかし、読書会 #2 の開催も決まり、もうこれ以上引き伸ばしているといつまでたっても書けなさそうなので、いい加減腹をくくって個人的にまとめてみようと思います。

JavaScript の変数

delete 演算子の話に移る前に、変数とは何なのかおさらいしておきましょう。JavaScript において、変数とはプロパティの別名です。といっても、すべてのプロパティを変数というわけではありません。一般には、グローバルオブジェクトと Activation オブジェクトのプロパティを指して変数といい、特に前者をグローバル変数、後者をローカル変数と呼びます。

Activation オブジェクトとは、関数の引数やローカル変数をプロパティとして持つオブジェクトです。Call オブジェクトと呼ばれることもあり、関数を実行するたびに作られます。ある関数が引数 arg とローカル変数 local を持つということは、その関数が呼び出される際に作られる Activation オブジェクトが arg プロパティと local プロパティを持つということに他なりません。関数の再帰呼び出しをする際に、呼び出し元と呼び出し先とで同名のローカル変数の指す値が独立していられるのは、それぞれの呼び出しに対して Activation オブジェクトが別個に作られるからです。

グローバルオブジェクトはグローバルコードにおける this キーワード、または (ブラウザ上で実行しているなら) グローバル変数 window などから参照できますが、Activation オブジェクトを JavaScript のコードから直接参照することはできません。ここで、グローバル変数 window といいましたが、これはもちろんグローバルオブジェクトの window プロパティのことです。ブラウザ上では Window オブジェクトがグローバルオブジェクトとなり、また Window オブジェクトは自分自身を指す window プロパティを持っているからです。

var global = 42;
this.global; // => 42
// グローバルコード (関数の内側ではない
// トップレベルのコード) では、this は
// グローバルオブジェクトを指す。

this.global2 = 12;
global2; // => 12
// グローバルオブジェクトのプロパティはグローバル変数となる。

function f() {
  var local = 36;
  // Activation オブジェクトは JavaScript からは
  // 参照できない。すなわち、x.local がローカル
  // 変数 local と等しくなるような x は存在しない。
}
f();

delete 演算子の対象

さて、それではいよいよ本題、delete 演算子が何をしているかですが、端的にいえば delete 演算子のしていることとはプロパティの削除です。delete 演算子はあるオブジェクトを対象とし、そのオブジェクトのプロパティを削除します。ですが、ここで delete 演算子の対象となるのがどのオブジェクトなのかということには十分注意しなくてはいけません。具体的な例を C++ の delete 演算子とも比べながら見ていきましょう。

// C++
class Object {
public:
  Object *x;
};

int main() {
  Object o;
  o.x = new Object();
  delete o.x;
  return 0;
}
// JavaScript
var o = {};
o.x = new Object();
delete o.x;

どちらもオブジェクト o のプロパティ (メンバ変数) x に新しいオブジェクトを代入した後、それに対して delete 演算子を適用しています。この C++ のコードのうち、delete 演算子の部分は以下のように書き換えられます。

// delete o.x (C++)
o.x->~Object();
::operator delete(o.x);

o.x が指すオブジェクトのデストラクタを呼び出した後、o.x を引数として operator delete 関数を呼び出しています。つまり、この C++ のコードにおいて、delete 演算子の対象となるオブジェクトは o.x が指すオブジェクトであるといえるでしょう。

一方、JavaScript では delete 演算子を使わずに書き換えることはできませんが、概念的には以下のような操作を行っています。

// delete o.x (JavaScript)
o.[[Delete]]("x");

オブジェクト o の [[Delete]] 内部メソッドを、文字列 "x" を引数として呼び出しています。内部メソッドとは、実際の JavaScript コードからは見えない内部的な操作の説明のために導入されたメソッドで、[[Delete]] 内部メソッドは引数として渡された名前のプロパティを削除します。すなわち、JavaScript では delete 演算子の対象となるオブジェクトは o が指すオブジェクトなのです。delete 演算子の実行による変化はオブジェクト o からプロパティ x がなくなるということだけで、o.x が指していたオブジェクトには基本的に何の変化ももたらしません。

もちろん、実際の JavaScript 処理系のほとんどは、プロパティ x が削除された結果、o.x が指していたオブジェクトがどこからも参照されなくなったのなら、そのオブジェクト自体を削除し、メモリ領域を解放するといった操作を行っています。しかし、このことは ECMAScript の仕様として決められたことではなく、極端な話メモリ領域を使い捨てにして、一度作ったオブジェクトは決して削除しない実装があったとしても仕様に違反してはいないはずです。このことからも、JavaScript の delete 演算子が、メモリ領域の解放を目的とした C++ の delete 演算子とはまったくの別物であることがわかるでしょう。

なお、ここではオブジェクト o の x プロパティを指定するのに delete o.x としましたが、これは delete o["x"] と書いても同じことです。

変数に対する delete 演算子

上の内容を言い直すと、JavaScript の delete 演算子とは、あるプロパティが与えられたとき、そのプロパティを持つオブジェクトから、そのプロパティを削除するものだということです。ここで、変数とはプロパティの別名であるといったことを思い出してください。変数に対する delete 演算子の動作も同じように説明できます。以下では、便宜上グローバルオブジェクトを [[Global]] で、実行中の関数の Activation オブジェクトを [[Activation]] で参照できるものとします。

var global1 = 42;
delete global1;
// -> [[Global]].[[Delete]]("global1");

var global2 = 12;

function f() {
  var local = 36;
  delete local;
  // -> [[Activation]].[[Delete]]("local");

  delete global2;
  // -> [[Global]].[[Delete]]("global2");
  // Activation オブジェクトは global2 プロパティを
  // 持っていないので、スコープチェーンをたどって
  // グローバルオブジェクトからプロパティを探す。
  // グローバルオブジェクトでは無事プロパティが
  // 見つかるので、グローバルオブジェクトの
  // [[Delete]] 内部メソッドが呼び出される。
}
f();

削除できるプロパティとできないプロパティ

delete 演算子を使えばどんなプロパティも削除できるというわけではありません。まず、対象となるオブジェクト自身が持つプロパティでないと削除できません。プロパティの探索自体はプロトタイプチェーンをたどって行われますが、そこで見つかったプロパティは削除されないということです。

function C() { this.x = 42; }
C.prototype.x = 12;

var o = new C();
o.x; // => 42

delete o.x;
o.x; // => 12
// o 自身の x プロパティが削除されたので、
// o のプロトタイプチェーンをたどって
// C.prototype が持つ x プロパティの値が返される。

delete o.x;
o.x; // => 12
// o 自身はもはや x プロパティを持っていないので、
// プロパティの削除は行われない。
// プロトタイプチェーン上のオブジェクトが持つ
// x プロパティは削除されない。

また、プロパティは ReadOnly や DontDelete といった属性を持つことがあります。DontDelete 属性を持つプロパティは削除できません。もともと存在しないプロパティを代入によって新たに作った場合 DontDelete 属性は付きませんが、組み込みオブジェクトに最初から存在するプロパティの中には DontDelete 属性を持つものもあります。

var re = /abc/i;

delete re.ignoreCase;
re.ignoreCase; // => true
// RegExp オブジェクトの ignoreCase プロパティは
// DontDelete 属性を持つので削除できない。

re.newProperty = 42;
delete re.newProperty;
re.newProperty; // => undefined
"newProperty" in re; // => false
// 代入により新しく作られたプロパティは
// DontDelete 属性を持たないので削除できる。

変数の属性

繰り返しますが、変数とは特定のオブジェクトのプロパティです。var 文により変数を宣言するということは、特定のオブジェクトにプロパティを追加するということです。この特定のオブジェクトのことを変数オブジェクトと呼び、グローバルコードならグローバルオブジェクト、関数内ならその関数が実行される際に作られる Activation オブジェクトがそれに該当します。

var 文により作られるプロパティは DontDelete 属性を持ちます。つまり、var 文によって宣言された変数は削除できないということです。これは関数宣言によって作成されたプロパティも同じです。

var x = 42;
delete x;
x; // => 42
// var 文により作られた x プロパティは
// DontDelete 属性を持つので削除できない。

y = 12;
delete y;
y; // => ReferenceError: y is not defined
// var 文を使わずに作られたyプロパティは
// DontDelete 属性を持たないので削除できる。

function f() { return 36; }
delete f;
f(); // => 36
// 関数宣言により作られた f プロパティは
// DontDelete 属性を持つので削除できない。

f = 24;
f; // => 24
// f プロパティが指す値を変更することはできる。

しかし、これにはひとつ例外があります。それが eval 関数を使って実行されるコード、eval コード内でのことです。eval コード内での変数オブジェクトは eval 関数を呼び出した時点のものと同じになります。たとえば、グローバルコードで eval 関数を呼び出し、その eval コード内に var 文が含まれていたとき、その var 文により作られるプロパティはグローバルオブジェクトのものとなります。ですが、そのプロパティは DontDelete 属性を持ちません。eval コード中の関数宣言についても同様です。

eval("var x = 42;");
x; // => 42
delete x;
x; // => ReferenceError: x is not defined
// eval コード内の var 文により作られたプロパティは
// DontDelete 属性を持たないので削除できる。

eval("function f() { return 12; }");
f(); // => 12
delete f;
f(); // => ReferenceError: f is not defined
// eval コード内の関数宣言により作られたプロパティは
// DontDelete 属性を持たないので削除できる。

ただし、eval コード中に含まれる関数内では、var 文により作られるプロパティが DontDelete 属性を持ちます。

eval("(function () {" +
     "  var x = 42;" +
     "  delete x;" +
     "  return x;" +
     "})();")
// => 42
// eval コード中に含まれる関数内で var 文により作られた
// プロパティは DontDelete 属性を持つので削除できない。

このように eval コード中で var 文および関数宣言の扱いが変わってくることは、ECMA-262 3rd 10.1.3 変数の実体化および 10.2.2 eval コードにて定められています。なお、ECMAScript 4 ではこの動作が修正され、eval コード中の var 文により作られたプロパティも DontDelete 属性を持つようになる予定です。

delete 演算子の返り値

delete 演算子は単項演算子であり、typeof 演算子や ! 演算子といったほかの単項演算子と同様、返り値を持ち他の式に組み込んで使うこともできます。delete 演算子の返り値は真偽値であり、対象のオブジェクトが指定されたプロパティを持ち、かつそのプロパティが DontDelete 属性を持つときは false、その他の場合は true です。

function C() { this.x = 42; }
C.prototype.y = 12;
var o = new C();

delete o.x; // => true
o.x; // => undefined
"x" in o; // => false
// o の x プロパティは DontDelete
// 属性を持たないので true を返す。

delete o.y; // => true
o.y; // => 12
// o 自身は y プロパティを持たないので true を返す。

delete o.z; // => true
// o は z プロパティを持たないので true を返す。

delete o; // => false
// グローバルオブジェクトの o プロパティは
// DontDelete 属性を持つので false を返す。

delete undefinedProperty; // => true
// undefinedProperty プロパティをもつ
// オブジェクトは存在しないので true を返す。

本来なら delete 演算子をプロパティでない値に適用したときも true が返るはずですが、このような場合に例外を投げる実装もあるようです。

delete 42; // => true
// 42 はプロパティでないので true を返す。
// 例外を投げる実装 (ECMAScript 仕様には違反) もある。

var x = 24;
delete x; // => false
delete x++; // => true
x; // => 25
// 式 x++ を評価した値は 24 という数値であり、
// グローバルオブジェクトの x プロパティ
// ではないので、delete x++ は true を返す。

ちなみに、ここではプロパティかどうかといっていますが、ECMAScript 仕様では Reference 型という内部的なデータ型を説明のために導入し、delete 演算子の被演算子を評価した結果の値が Reference 型の値でなければ true を返すとしています。

コメント

_ javascript学徒 ― 2008年08月18日 13時26分

素晴らしい解説ありがとうございました。C++と全然違いますね

コメントをどうぞ

※メールアドレスとURLの入力は必須ではありません。 入力されたメールアドレスは記事に反映されず、ブログの管理者のみが参照できます。

名前:
メールアドレス:
URL:
コメント:

トラックバック

このエントリのトラックバックURL: http://nanto.asablo.jp/blog/2008/01/09/2552470/tb

_ Copy/Cut/Paste - 2008年03月12日 00時30分

4章 変数
4.2 変数の宣言
varで宣言した変数に初期値を与えない状態では変数は未定義値になる。

varで宣言した変数は永続します。delete演算子で変数を削除しようとすると、エラーになります。
こんなこと考えもしなかった。本当かどうかbread.htmlで試してみた。
var...

_ Sam Lin - 2008年04月04日 22時46分

版权声明:可以任意转载,但转载时必须标明原作者charlee、原始链接http://tech.idv2.com/2008/01/09/javascript-variables-and-delete-o...