一.問題の背景
シナリオは以下の通り:
'use strict';
var F = function() {
this.arr = [1, 2, 3, 4, 5, 6, 7];
var self = this;
Object.defineProperty(self, 'value', {
get: function() {
if (!self._value) {
self._value = self.doStuff();
}
return self._value;
},
set: function(value) {
return self._value = value;
}
})
}
F.prototype.doStuff = function() {
return this.arr.reduce(function(acc, value) {return acc + value}, 0);
};
F のインスタンスは value プロパティを持っていますが、new するときにプロパティ値を初期化したくありません(この値は必ずしも使用されるとは限らず、計算コストが高いため、または new するときにまだ計算できない可能性があるため)。自然に getter を定義して「必要に応じて計算」を実装することを考えます:
var f = new F();
// このとき f には value 属性がありますが、値は何なのかまだ分かりません
// 初めてこの属性にアクセスするときに初期値を計算します(doStuff を通じて)
f.value
var tmpF = new F()
// value 属性にアクセスしなければ、初期値を計算する必要は永遠にありません
これにより、事前に不要な高コスト操作を回避できます。例えば:
-
DOM 照会
-
layout(getComputedStyle()など) -
深さ優先探索
もちろん、直接 getValue() を追加しても望みの効果は得られますが、getter の方が使用側にとって親切で、外部は値が事前に計算されたものなのかその場で計算されたものなのか全く分かりません
delete の奇妙な動作は 2 つの部分に分かれます:
// 1.defineProperty で定義された属性を delete するとエラー
// Uncaught TypeError: Cannot delete property 'value' of #<F>
delete f.value
// 2.プレースホルダー初期値を追加すると、正常に delete できるようになる
// F の value 定義部分を以下に変更
var self = this;
self.value = null; // プレースホルダー、delete エラーを回避
Object.defineProperty(self, 'value', {/*...*/});
二.原因分析
delete エラー
delete 演算子のルールは:delete 成功時は true を返し、それ以外は false を返すことを覚えているでしょう
削除が成功しようがしまいが、エラーになるはずはありません。実際、エラーが出るのは厳密モードを有効にしているためです:
Third, strict mode makes attempts to delete undeletable properties throw (where before the attempt would simply have no effect):
(Strict mode - JavaScript | MDN より引用)
厳密モードでは、削除できないとエラ��になります。しかし、すでに defineProperty() で value 属性を追加したのに、なぜ削除できないのでしょうか?configurable が原因です:
configurable
true if and only if the type of this property descriptor may be changed and if the property may be deleted from the corresponding object.
Defaults to false.
これがデフォルトで false になるとは。調べてみると、他のいくつかもデフォルトで false であることが分かりました:
configurable Defaults to false.
enumerable Defaults to false.
writable Defaults to false.
value, get, set Defaults to undefined.
descriptor を定義すると属性の読み書き方法が変更されるため、!writable はまだ合理的ですが、!enumerable は少し強く、!configurable は行き過ぎです。しかしルールはこうなっているので、奇妙な動作 1 は合理的です
プレースホルダー初期値
属性がすでに存在する場合、defineProperty() は少し譲歩し、元の descriptor の気持ちを考慮すると推測:
var obj = {};
obj.value = null;
var _des = Object.getOwnPropertyDescriptor(obj, 'value');
Object.defineProperty(obj, 'value', {
get: function() {},
set: function() {}
});
var des = Object.getOwnPropertyDescriptor(obj, 'value');
console.log(_des);
console.log(des);
結果は以下の通り:
// _des
{
configurable: true,
enumerable: true,
value: null,
writable: true
}
// des
{
configurable: true,
enumerable: true,
get: (),
set: ()
}
defineProperty() 後、configurable と enumerable は元のまま変わらないことが分かりました。そのため、プレースホルダー値を追加すると削除できるようになります。また writable はなくなりました。getter/setter を定義した後、書き込み可能かどうかは getter/setter の具体的な実装次第で、一目では分からなくなるためです(例えば setter が新しい値を破棄するか、getter が不変の値を返す場合、効果はいずれも書き込み不可です)
三.delete のルール
delete の問題に遭遇したついでに、もう少し詳しく見てみましょう
delete var
一般に delete は var 宣言の変数は削除できず、プロパティは削除できると考えられています。実際は完全に正しいわけではありません。例えば:
var x = 1;
delete x === false
// var 宣言の変数を削除できる
eval('var evalX = 1');
delete evalX === true
// プロパティでも削除できない場合がある
var arr = [];
delete arr.length === false
var F = function() {};
delete F.prototype === false
// DOM、BOM オブジェクトで言うことを聞かないものはさらに多い
少なくとも形式的には、delete は var 宣言の変数を削除できないは正しくありません。evalX が削除できる理由は興味深く、いくつかのことを理解する必要があります:実行環境、変数オブジェクト/アクティベーションオブジェクト、eval 環境の特殊性
実行環境
実行環境は 3 種類あります:Global 環境(script タグで囲まれた環境など)、Function 環境(onclick 属性値の実行環境、関数呼び出しで作成される実行環境など)、eval 環境(eval に渡されたコードの実行環境)
変数オブジェクト/アクティベーションオブジェクト
各実行環境は変数オブジェクトに対応し、ソースコードで宣言された変数と関数は変数オブジェクトの属性として存在します。そのため、グローバル作用域で宣言されたものは global の属性になります。例えば:
var p = 'value';
function f() {}
window.p === p
window.f === f
Function 実行環境の場合、変数オブジェクトは一般に global ではなく、アクティベーションオブジェクトと呼ばれます。Function 実行環境に入るたびにアクティベーションオブジェクトが作成され、関数本体で宣言された変数と関数のほか、各引数および arguments オブジェクトもアクティベーションオブジェクトの属性として存在しますが、直接検証することはできません
注意:変数オブジェクトとアクティベーションオブジェクトは抽象的な内部メカニズムで、変数スコープを維持し、環境を隔離するなどするために使用され、直接アクセスできません。Global 環境で変数オブジェクトが global のように見える場合でも、この global も完全に内部の変数オブジェクトというわけではありません(属性アクセス上で交差があるだけです)
P.S.変数オブジェクトとアクティベーションオブジェクトのような「玄妙」なものはあまり深く追求する必要はありません。それぞれが何か、どのような関係があるかは重要ではなく、その作用を理解すればよいのです
eval 環境の特殊性
eval 実行環境で宣言された属性と関数は、呼び出し環境(つまり上位の実行環境)の変数オブジェクトの属性として存在します。これは他の 2 つの環境と異なる点ですが、もちろん直接検証することもできません(変数オブジェクトに直接アクセスできないため)
変数オブジェクトの属性にはいくつかの内部特徴があります。例えば目に見える configurable, enumerable, writable です(もちろん内部の区分はさらに細かく、削除できるかどうかは configurable の一部に過ぎない可能性があります)
従うルールは:宣言を通じて作成された変数と関数には削除できないという天賦の権利があり、明示的または暗黙的な属性代入を通じて作成された変数と関数にはこの天賦の権利がないということです
組み込みのいくつかのオブジェクト属性にも削除できないという天賦の権利があります。例えば:
var arr = [];
delete arr.length === false
void function(arg) {console.log(delete arg === false);}(1);
属性代入を通じて作成された変数と関数には削除できない天賦の権利がないため、代入を通じて作成された変数と関数は削除できます。例えば:
x = 1;
delete x === true
window.a = 1
delete window.a === true
同様に global に追加されるグローバル変数宣言を通じて作成されたものは削除できません:
var y = 2;
delete window.y === false
作成方法が異なるだけで、作成時に天賦の権利が決定されます
さらに、興味深い試みがあります。eval が直接外側の変数オブジェクトを取得し、eval 環境で宣言されたものには削除できない天賦の権利がないなら、両者を組み合わせれば強制的な削除をオーバーライドできるでしょうか?
var x = 1;
/* Can't delete, `x` has DontDelete */
delete x; // false
typeof x; // "number"
eval('function x(){}');
/* `x` property now references function, and should have no DontDelete */
typeof x; // "function"
delete x; // should be `true`
typeof x; // should be "undefined"
結果はオーバーライド後も削除できません。変数オブジェクト上で宣言方式によって内部的に追加された属性は、descriptor の変更を禁止しているようです。上記の x 値はオーバーライドされましたが、削除できない天賦の権利は残っています
四.まとめ
defineProperty() によって定義された新しい属性は、その descriptor のデフォルトのいくつかの属性がすべて false です。つまり、列挙不可、descriptor 変更不可、削除不可です。例えば:
var obj = {};
Object.defineProperty(obj, 'a', {configurable: true, value: 10});
Object.defineProperty(obj, 'a', {configurable: true, value: 100});
delete obj.a === true
Object.defineProperty(obj, 'b', {value: 11});
delete obj.b === false
// エラー、descriptor の変更を許可しない
// Uncaught TypeError: Cannot redefine property: b
Object.defineProperty(obj, 'b', {value: 110});
また、delete 演算子のシンプルなルールは以下の通り:
-
被演算子が参照でない場合、直接 true を返す
-
変数オブジェクト/アクティベーションオブジェクトにこの属性がない場合、true を返す
-
属性が存在するが、削除できない天賦の権利がある場合、false を返す
-
それ以外の場合、属性を削除し、true を返す
したがって:
delete 1 === true
基本値は最初のステップで true になります。削除したかどうかは分かりませんが
コメントはまだありません