いいえ、絶対に使ってはいけないわけではありません。delete
と言う物がわかっているかと言うことです。
delete
は失敗しても何もせずにfalse
を返すだけの時がある
delete
は失敗するときがあります。対象がローカル変数や関数宣言を用いた関数、構成不可(non-configurable)プロパティの場合です。このとき、非厳格モードでは何もせずにfalse
を返します。しかし、ほとんどの場合はdelete
の戻り値のチェックをしていません。厳格モードではエラーになるからわかるとは言え、非厳格モードではそれに気付かないまま、削除される物として扱ってしまう可能性があります。
delete
してもすぐにメモリが解放されるわけでは無い
どこか別の言語のdelete
と勘違いしていなければ、delete
がメモリの解放を意味するのでは無い事はわかっているはずです。GCを搭載した言語において、オブジェクトのメモリ解放は非決定的です。delete
したからといってそのプロパティが示していたオブジェクトが解放されるわけではありません。まして、他に参照しているものが存在する限り、解放すべきではありませんし、実際に解放はされません。
現代的なブラウザであればM&S方式(およびそれを発展させた)GCを持つため、循環参照はもはやプログラミングにおいて気にする物ではありません。もし、メモリ解放を意味するためにdelete
を使うのであればそれは意味が無いことです。
Arrayに対するdelete
の動作は欠番にするだけ
果たしてそんな人はいるのかと思うのかも知れませんが、Arrayに対してdelete
した場合、番号が詰められると勘違いする場合があります。実際は番号は詰められず、欠番となるだけです。そればかりか、最後の番号を消しても、length
の大きさは変わりません。
const a = [2, 3, 5, 7, 11, 13];
delete a[5];
delete a[2];
delete a[0];
console.log(Object.keys(a)); // => ['1', '3', '4']
console.log(a.length); // => 6
console.log(a[2]); // => undefined
Arrayの要素を削除したい場合は、shift
、splice
、pop
を使用してください。
const a = [2, 3, 5, 7, 11, 13];
a.pop();
a.splice(2, 1);
a.shift();
console.log(Object.keys(a)); // => ['0', '1', '2']
console.log(a.length); // => 3
console.log(a[2]); // => 11
Objectのプロパティを削除はどうすべきか?
連想配列でのキー削除
JavaScriptのObjectは連想配列(言語によっては辞書とも)としても使用できます。連想配列として使うとき、_キーを_削除する場合はdelete
を使うしかありません。
const dic = {
'林檎': '赤色',
'蜜柑': '黄色',
'桃': '桃色'
}
delete dic['林檎'];
for (const [key, value] of Object.entries(dic)) {
console.log(`${key}の色は${value}です。`)
}
しかし、Objectをこのように連想配列として使うこと自体がよくありません。キーには文字列してか使用できない等の連想配列としては制限があるのに、プロトタイプチェーンやObject.defineProperty()
で特殊なプロパティを作れたりと余計な機能があります。連想配列を扱うのであれば、Mapを使用すべきです。
const dic = new Map([
['林檎', '赤色'],
['蜜柑', '黄色'],
['桃', '桃色']
]);
dic.delete('林檎');
for (const [key, value] of dic) {
console.log(`${key}の色は${value}です。`)
}
レコードでのプロパティ削除
もう一つのObjectの使い方はレコードとして使うことです。レコード?なんか聞き慣れていない言葉ですが構造体と言えばわかる人はわかるでしょう。構造体もレコードの一種であり、レコードは構造体を使って実現される場合があります。
連想配列のような使い方で無ければ、Objectのプロパティの名前とその値の型は固定化されているはずです。何気なくそうしているように思っているかも知れませんが、それがレコードとしてObjectを使っていると言うことです。
const checkColor = fruit => {
if ('color' in fruit) {
console.log(`${fruit.name}の色は${fruit.color}です。`);
} else {
console.log(`${fruit.name}の色はわかりません。`);
}
};
const checkSugar = fruit => {
if ('sugar' in fruit) {
console.log(`${fruit.name}の糖度は${fruit.sugar}です。`);
} else {
console.log(`${fruit.name}の糖度はわかりません。`);
}
};
const apple = {
name: '林檎',
color: '赤色',
sugar: 15
};
delete apple.color;
checkColor(apple);
checkSugar(apple);
しかし、この場合は削除する必要はありませんし、ましてや、存在チェックにin
演算子を使うべきではありません。undefined値またはnull値を存在しないというフラグに見立てて、undefined
(ローカル変数として使われている恐れあればvoid 0
)またはnull
を代入すべきです。
const checkColor = fruit => {
if (fruit.color != null) {
console.log(`${fruit.name}の色は${fruit.color}です。`);
} else {
console.log(`${fruit.name}の色はわかりません。`);
}
};
const checkSugar = fruit => {
if (fruit.sugar != null) {
console.log(`${fruit.name}の糖度は${fruit.sugar}です。`);
} else {
console.log(`${fruit.name}の糖度はわかりません。`);
}
};
const apple = {
name: '林檎',
color: '赤色',
sugar: 15
};
apple.color = void 0;
checkColor(apple);
checkSugar(apple);
さらにはimmutableとして扱うことを検討すべきです。
const checkColor = fruit => {
if (fruit.color != null) {
console.log(`${fruit.name}の色は${fruit.color}です。`);
} else {
console.log(`${fruit.name}の色はわかりません。`);
}
};
const checkSugar = fruit => {
if (fruit.sugar != null) {
console.log(`${fruit.name}の糖度は${fruit.sugar}です。`);
} else {
console.log(`${fruit.name}の糖度はわかりません。`);
}
};
const apple = {
name: '林檎',
color: '赤色',
sugar: 15
};
const noColorApple = Object.assign({}, apple, {color: void 0});
checkColor(noColorApple);
checkSugar(noColorApple);
Objectの直属のインスタンスでは無く、何かのインスタンスとしてのオブジェクトも拡張されたレコードの一種と見なすことができます。このように私達は既にレコードの使い方を知っています。単に意識していないだけです。意識することがあれば、それはTypeScriptやPureScriptなど静的型付けにおいて型を書いたときでしょう。
interface Fruit {
name: string;
color?: string;
sugar?: number;
}
const checkColor = (fruit: Fruit) => {
if (fruit.color != null) {
console.log(`${fruit.name}の色は${fruit.color}です。`);
} else {
console.log(`${fruit.name}の色はわかりません。`);
}
};
const checkSugar = (fruit: Fruit) => {
if (fruit.sugar != null) {
console.log(`${fruit.name}の糖度は${fruit.sugar}です。`);
} else {
console.log(`${fruit.name}の糖度はわかりません。`);
}
};
const apple: Fruit = {
name: "林檎",
color: "赤色",
sugar: 15
};
apple.color = undefined;
checkColor(apple);
checkSugar(apple);
重要なのは、レコードにおいて、それぞれのプロパティはプロパティとしては存在する物と見なすことです。しかしその値が意味のある値であるかは別であり、値が存在しない事が許される場合(nullableと言われる)は、undefined値やnull値であることが値が存在しない事を意味するとすることです。JavaScriptでは存在しないプロパティへのアクセスはundefined値を返すため、予めundefined値が入っている場合と動作は同じであり、区別する必要はありませんし、区別すべきではありません。もし、in
演算子やObject.prototype.hasOwnProperty()
を使った場合、undefined値やnull値が入ったプロパティを拾ってきてしまいます。しかし、そのプロパティが実際に何かの情報を持っているわけでは無いため、役に立つことはありません。レコードにおいて、プロパティの存在チェックはx.y != null
のよう形で行うべきであって、プロパティそのものの存在チェックは意味をなさないということです。
さて、では、なぜdelete
は使うべきでは無いのでしょうか?
delete
を使ってもundefined
やnull
を代入してもさほど変わりません。いや、動作が変わってはいけません。しかし、意識の違いがあります。delete
はプロパティそのものを削除することですが、代入はプロパティへの設定です。危惧すべきは、削除するなら、チェックもin
演算子などを使って、プロパティそのものがあるかどうかで判断すればいいと短絡的に捉えてしまうことです。ましてや、削除したはずなのにundefined値を返す意味がわからないかも知れませんし、undefined値を代入していると思う人までいるかも知れません。それなら初めから代入した方が、意味がわかりやすいと言うことです。
なんか、余り積極的な理由では無かったかと思うかも知れません。ただ、私が言いたいのは、レコードとして考えたとき、「削除する」という選択肢は本来あり得ないと言うことです。他の言語でのレコードや構造体では、いらないからプロパティそのものを削除などと言うことはできません。他言語を学ぶときに戸惑わないようにするという意味もあるかと思っています。
delete
してプロトタイプチェーンのプロパティを復活させるのはありか?
プロパティの使い方の一つとして、上書きしてしまったプロトタイプチェーンのプロパティを元に戻すというのがあります。
class Fruit {
constructor({name, color, sugar}) {
this.name = name;
this.color = color;
this.sugar = sugar;
}
checkColor() {
if (this.color != null) {
console.log(`${this.name}の色は${this.color}です。`);
} else {
console.log(`${this.name}の色はわかりません。`);
}
}
}
const apple = new Fruit({
name: '林檎',
color: '赤色',
sugar: 15
});
apple.checkColor();
apple.checkColor = function() {
console.log(`${this.name}の色は教えない。`)
}; // アロー演算子は使えない
apple.checkColor();
delete apple.checkColor;
apple.checkColor();
JavaScriptではメソッドもプロパティであることに注意してください。この場合、apple.checkColor = apple.__proto__.checkColor;
という書き方もできますが、delete
の方がわかりやすいでしょう。この場合は使ってもかまわないと思います。
しかし、果たしてこの方法はどれだけ需要があるのでしょうか?まず、特定のメソッドを上書きするのであれば継承を使ってサブクラスを作るべきです。特定のオブジェクトのみ動作を変えることはクラス名と動作の不一致を起こして、混乱の元です。ましてや、元に戻すなどと言う動作をすることは一体、今、どの動作になっているか容易に判別できないことを意味します。決していい設計とは言えません。
もし、このような書き方が必要になった場合は、設計から見直すべきです。このまま進めば、ほとんどの場合はロジックが複雑化して、いずれ破綻するでしょう。あなたが中級者、いや、上級者であって、そのような複雑なロジックを問題なく考えられるというのであれば、きっと設計から見直して、パフォーマンスと言った何かしらの理由が無ければ、そのような方法は取らないはずです。