LoginSignup

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

More than 5 years have passed since last update.

なぜ、deleteを使ってはいけないのですか?

Last updated at Posted at 2017-01-19

いいえ、絶対に使ってはいけないわけではありません。deleteと言う物がわかっているかと言うことです。

deleteは失敗しても何もせずにfalseを返すだけの時がある

deleteは失敗するときがあります。対象がローカル変数や関数宣言を用いた関数、構成不可(non-configurable)プロパティの場合です。このとき、非厳格モードでは何もせずにfalseを返します。しかし、ほとんどの場合はdeleteの戻り値のチェックをしていません。厳格モードではエラーになるからわかるとは言え、非厳格モードではそれに気付かないまま、削除される物として扱ってしまう可能性があります。

deleteしてもすぐにメモリが解放されるわけでは無い

どこか別の言語のdeleteと勘違いしていなければ、deleteがメモリの解放を意味するのでは無い事はわかっているはずです。GCを搭載した言語において、オブジェクトのメモリ解放は非決定的です。deleteしたからといってそのプロパティが示していたオブジェクトが解放されるわけではありません。まして、他に参照しているものが存在する限り、解放すべきではありませんし、実際に解放はされません。

現代的なブラウザであればM&S方式(およびそれを発展させた)GCを持つため、循環参照はもはやプログラミングにおいて気にする物ではありません。もし、メモリ解放を意味するためにdeleteを使うのであればそれは意味が無いことです。

Arrayに対するdeleteの動作は欠番にするだけ

果たしてそんな人はいるのかと思うのかも知れませんが、Arrayに対してdeleteした場合、番号が詰められると勘違いする場合があります。実際は番号は詰められず、欠番となるだけです。そればかりか、最後の番号を消しても、lengthの大きさは変わりません

Arrayでdelete.js
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の要素を削除したい場合は、shiftsplicepopを使用してください。

Arrayの要素削除.js
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を使うしかありません。

連想配列としてのObject.js
const dic = {
  '林檎': '赤色',
  '蜜柑': '黄色',
  '': '桃色'
}
delete dic['林檎'];
for (const [key, value] of Object.entries(dic)) {
  console.log(`${key}の色は${value}です。`)
}

しかし、Objectをこのように連想配列として使うこと自体がよくありません。キーには文字列してか使用できない等の連想配列としては制限があるのに、プロトタイプチェーンやObject.defineProperty()で特殊なプロパティを作れたりと余計な機能があります。連想配列を扱うのであれば、Mapを使用すべきです。

Mapを使用.js
const dic = new Map([
  ['林檎', '赤色'],
  ['蜜柑', '黄色'],
  ['', '桃色']
]);

dic.delete('林檎');
for (const [key, value] of dic) {
  console.log(`${key}の色は${value}です。`)
}

レコードでのプロパティ削除

もう一つのObjectの使い方はレコードとして使うことです。レコード?なんか聞き慣れていない言葉ですが構造体と言えばわかる人はわかるでしょう。構造体もレコードの一種であり、レコードは構造体を使って実現される場合があります。

連想配列のような使い方で無ければ、Objectのプロパティの名前とその値の型は固定化されているはずです。何気なくそうしているように思っているかも知れませんが、それがレコードとしてObjectを使っていると言うことです。

レコードでdelete.js
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を代入すべきです。

レコードでundfined値代入.js
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として扱うことを検討すべきです。

immutableとして扱う.js
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など静的型付けにおいて型を書いたときでしょう。

TypeScriptで書いてみた.ts
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を使ってもundefinednullを代入してもさほど変わりません。いや、動作が変わってはいけません。しかし、意識の違いがあります。deleteはプロパティそのものを削除することですが、代入はプロパティへの設定です。危惧すべきは、削除するなら、チェックもin演算子などを使って、プロパティそのものがあるかどうかで判断すればいいと短絡的に捉えてしまうことです。ましてや、削除したはずなのにundefined値を返す意味がわからないかも知れませんし、undefined値を代入していると思う人までいるかも知れません。それなら初めから代入した方が、意味がわかりやすいと言うことです。

なんか、余り積極的な理由では無かったかと思うかも知れません。ただ、私が言いたいのは、レコードとして考えたとき、「削除する」という選択肢は本来あり得ないと言うことです。他の言語でのレコードや構造体では、いらないからプロパティそのものを削除などと言うことはできません。他言語を学ぶときに戸惑わないようにするという意味もあるかと思っています。

deleteしてプロトタイプチェーンのプロパティを復活させるのはありか?

プロパティの使い方の一つとして、上書きしてしまったプロトタイプチェーンのプロパティを元に戻すというのがあります。

復活のプロパティ.js
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の方がわかりやすいでしょう。この場合は使ってもかまわないと思います。

しかし、果たしてこの方法はどれだけ需要があるのでしょうか?まず、特定のメソッドを上書きするのであれば継承を使ってサブクラスを作るべきです。特定のオブジェクトのみ動作を変えることはクラス名と動作の不一致を起こして、混乱の元です。ましてや、元に戻すなどと言う動作をすることは一体、今、どの動作になっているか容易に判別できないことを意味します。決していい設計とは言えません。

もし、このような書き方が必要になった場合は、設計から見直すべきです。このまま進めば、ほとんどの場合はロジックが複雑化して、いずれ破綻するでしょう。あなたが中級者、いや、上級者であって、そのような複雑なロジックを問題なく考えられるというのであれば、きっと設計から見直して、パフォーマンスと言った何かしらの理由が無ければ、そのような方法は取らないはずです。


JavaScript初級者のためのコーディングガイドに戻る

1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up