深入理解 JavaScript 中 delete 操作符的删除机制

作者:青柠时间:2026-10-26 20:05:30浏览:557

本文深入解析 JavaScript 中 delete 操作符的工作原理。通过执行上下文、变量对象和激活对象的概念,阐明为何声明的变量和函数具有 DontDelete 特性而无法被删除,而动态添加的属性可以。同时对比了 C++ 的 delete 差异,并解释了 Eval 代码及浏览器控制台环境下的特殊行为。

一、问题的提出

delete 操作符的基本行为

首先观察以下代码示例,需注意这些代码不应在浏览器开发者工具(如 FireBug 或 Chrome Developer Tool)中直接运行,具体原因将在后文说明。



我们可以成功删除对象的属性:


代码如下:

 
var o = { x: 1 };
delete o.x; // true
o.x; // undefined

然而,我们无法删除通过 var 声明的变量:


代码如下:

 
var x = 1;
delete x; // false
x; // 1

同样,通过 function 声明的函数也无法被删除:


代码如下:

 
function x(){}
delete x; // false
typeof x; // "function"

需要留意的是,当 delete 操作符返回 true 时,表示该属性可以被删除;若返回 false,则表示该属性具有不可删除的特性。



执行上下文与变量对象

要深入理解这一机制,必须掌握变量实例化和属性特性这两个核心概念——遗憾的是,许多 JavaScript 书籍对此着墨甚少。理解这些原理并不困难,如果读者不关心其底层运行逻辑,可以选择跳过此部分。



二、代码类型

在 ECMAScript 标准中,存在三种类型的可执行代码:Global code(全局代码)、Function code(函数代码)以及 Eval code(在 Eval 中执行的代码)。


代码如下:

 
var x=1;//Global code
function test(){
var y=2;//Function Code
eval("var z=3");//Eval Code in Function
} 
eval("function evalTest(){}");//Eval Code in Global


三、执行上下文

当 ECMAScript 代码执行时,它始终在特定的上下文中运行。执行上下文是一个抽象实体,有助于我们理解作用域和变量实例化的工作机制。对于上述三种类型的代码,每种都有其对应的执行上下文。当函数执行时,控制权进入函数代码(Function code)的执行上下文;当全局代码执行时,则进入全局代码(Global code)的执行上下文。



由此可见,执行上下文在逻辑上源自一个栈。起始可能是具有自身作用域的全局代码,代码中可能调用一个具有自己作用域的函数,该函数又可能调用另一个函数,依此类推。即使函数递归调用自身,每次调用都会进入一个新的执行上下文。



四、Activation object(激活对象)/Variable object(变量对象)

每个执行上下文内部都包含一个 Variable Object(变量对象)。与执行上下文类似,Variable Object 也是一个抽象实体,用于描述变量实例化的机制。有趣的是,代码中声明的变量和函数实际上是被作为这个变量对象的属性添加进去的。



当进入全局代码的执行上下文时,全局对象被用作变量对象。这也解释了为什么在全局范围内声明的变量或函数变成了全局对象的属性。


代码如下:

 
/* remember that `this` refers to global object when in global scope */
var GLOBAL_OBJECT = this;
var foo = 1;
GLOBAL_OBJECT.foo; // 1
foo === GLOBAL_OBJECT.foo; // true
function bar(){}
typeof GLOBAL_OBJECT.bar; // "function"
GLOBAL_OBJECT.bar === bar; // true

全局变量变成了全局对象的属性,那么在函数代码(Function code)中定义的局部变量又是如何处理的呢?其行为非常相似:它们也成为了变量对象的属性。唯一的区别在于,在函数代码(Function code)中,变量对象不是全局对象,而是所谓的激活对象(Activation object)。每次函数代码(Function code)进入执行作用域时,就会创建一个激活对象(Activation object)。



不仅函数代码(Function code)中的变量和函数成为激活对象的属性,函数的每一个参数(与形参相对应的名称)以及一个特定的 Arguments 对象也是。注意,激活对象是一种内部机制,不会被程序代码直接访问到。


代码如下:

 
(function(foo){
var bar = 2;
function baz(){}
/* 
In abstract terms,
Special `arguments` object becomes a property of containing function's Activation object:
ACTIVATION_OBJECT.arguments; // Arguments object
...as well as argument `foo`:
ACTIVATION_OBJECT.foo; // 1
...as well as variable `bar`:
ACTIVATION_OBJECT.bar; // 2
...as well as function declared locally:
typeof ACTIVATION_OBJECT.baz; // "function"
*/ 
})(1);

最后,在 Eval 代码(Eval code)中声明的变量作为正在调用的上下文的变量对象的属性被创建。Eval 代码(Eval code)只使用它正在被调用的那个执行上下文的变量对象。


代码如下:

 
var GLOBAL_OBJECT = this;
/* `foo` is created as a property of calling context Variable object,
which in this case is a Global object */
eval('var foo = 1;');
GLOBAL_OBJECT.foo; // 1
(function(){
/* `bar` is created as a property of calling context Variable object,
which in this case is an Activation object of containing function */
eval('var bar = 1;');
/* 
In abstract terms,
ACTIVATION_OBJECT.bar; // 1
*/ 
})(); 

五、属性特性

属性特性与 DontDelete

现在变量会怎样已经很清楚(它们成为属性),剩下唯一的需要理解的概念是属性特性。每个属性都有来自下列一组属性中的零个或多个特性——ReadOnly, DontEnum, DontDelete 和 Internal,你可以认为它们是一个标记,一个属性可有可无的特性。为了今天讨论的目的,我们只关心 DontDelete 特性。



当声明的变量和函数成为一个变量对象的属性时——要么是激活对象(Function code),要么是全局对象(Global code),这些创建的属性带有 DontDelete 特性。但是,任何明确的(或隐含的)创建的属性不具有 DontDelete 特性。这就是为什么一些属性能删除,而另一些不能。


代码如下:

 
var GLOBAL_OBJECT = this;
/* `foo` is a property of a Global object.
It is created via variable declaration and so has DontDelete attribute.
This is why it can not be deleted. */
var foo = 1;
delete foo; // false
typeof foo; // "number"
/* `bar` is a property of a Global object.
It is created via function declaration and so has DontDelete attribute.
This is why it can not be deleted either. */
function bar(){}
delete bar; // false
typeof bar; // "function"
/* `baz` is also a property of a Global object.
However, it is created via property assignment and so has no DontDelete attribute.
This is why it can be deleted. */
GLOBAL_OBJECT.baz = 'blah';
delete GLOBAL_OBJECT.baz; // true
typeof GLOBAL_OBJECT.baz; // "undefined"


六、内置属性和DontDelete

一句话:属性中一个独特的特性 (DontDelete) 控制着这个属性是否能被删除。注意,对象的内置属性(即对象的预定义属性)有 DontDelete 特性,因此不能被删除。特定的 Arguments 变量(或者,正如我们现在了解的,激活对象的属性),任何函数实例的 length 属性也拥有 DontDelete 特性。


代码如下:

 
(function(){
/* can't delete `arguments`, since it has DontDelete */
delete arguments; // false
typeof arguments; // "object"
/* can't delete function's `length`; it also has DontDelete */
function f(){}
delete f.length; // false
typeof f.length; // "number"
})(); 

与函数参数相对应的创建的属性也有 DontDelete 特性,因此也不能被删除。


代码如下:

 
(function(foo, bar){
delete foo; // false
foo; // 1
delete bar; // false
bar; // 'blah'
})(1, 'blah');


七、未声明的赋值

简单地就是未声明的赋值在一个全局对象上创建一个可删除的属性。


代码如下:

 
var GLOBAL_OBJECT = this;
/* create global property via variable declaration; property has DontDelete */
var foo = 1;
/* create global property via undeclared assignment; property has no DontDelete */
bar = 2;//可理解为 window.bar=2; 根据上面的第五点是可以删除的
delete foo; // false
typeof foo; // "number"
delete bar; // true
typeof bar; // "undefined"

请注意,DontDelete 特性是在属性创建的过程中确定的,后来的赋值不会修改现有属性已经存在的特性,理解这一点很重要。


代码如下:

 
/* `foo` is created as a property with DontDelete */
function foo(){}
/* Later assignments do not modify attributes. DontDelete is still there! */
foo = 1;
delete foo; // false
typeof foo; // "number"
/* But assigning to a property that doesn't exist,
creates that property with empty attributes (and so without DontDelete) */
this.bar = 1;
delete bar; // true
typeof bar; // "undefined"

八、Eval code

Eval 代码的特殊性

在 Eval 中创建的变量或方法比较特别,没有 DontDelete 特性,也就是说可以删除。



代码如下:

 
eval("var x = 1;");
console.log(x); // 1
delete x;
console.log(typeof x); // undefined
eval("function test(){ var x=1; console.log(delete x);/* false */;return 1;}");
console.log(test()); // 1
delete test;
console.log(typeof test); // undefined

注意,这里说的在 Eval 中创建的变量或方法不包括方法内部的变量或方法,如上面代码中的红色部分,仍然跟之前讲的一致:不能被删除。



九、FireBug的困惑

浏览器控制台环境下的异常

我们看一段在 FireBug 中执行的代码结果:


代码如下:

 
var x=1;
delete x;
console.log(typeof x);//undefined
function y(){
var z=1;
console.log(delete z);//false
} 
y();
delete y;
console.log(typeof y);//undefined

这明明是违反上述规则的,但跟上面第八点对比后发现,这正在代码在 eval 中执行的效果。虽然没有证实,但我猜测 FireBug(Chrome Developer tool)中控制台代码是用 eval 执行的。



所以,当大家在测试 JS 代码时,如果涉及到当前上下文环境时特别要注意。



十、delete操作符删除的对象

与 C++ delete 的区别

C++ 中也有 delete 操作符,它删除的是指针所指向的对象。例如:



代码如下:

 
class Object {
public:
Object *x;
} 
Object o;
o.x = new Object();
delete o.x; // 上一行new的Object对象将被释放

但 Javascript 的 delete 与 C++ 不同,它不会删除 o.x 指向的对象,而是删除 o.x 属性本身。


代码如下:

 
var o = {};
o.x = new Object();
delete o.x; // 上一行new的Object对象依然存在
o.x; // undefined,o的名为x的属性被删除了

在实际的 Javascript 中,delete o.x 之后,Object 对象会由于失去了引用而被垃圾回收, 所以 delete o.x 也就“相当于”删除了 o.x 所指向的对象,但这个动作并不是 ECMAScript 标准, 也就是说,即使某个实现完全不删除 Object 对象,也不算是违反 ECMAScript 标准。



“删除属性而不是删除对象”这一点,可以通过以下的代码来确认。


代码如下:

 
var o = {};
var a = { x: 10 };
o.a = a;
delete o.a; // o.a属性被删除
o.a; // undefined
a.x; // 10, 因为{ x: 10 } 对象依然被 a 引用,所以不会被回收

另外,delete o.x 也可以写作 delete o["x"],两者效果相同。



十一、其他不能被删除的属性

其他不可删除的属性

除了上面说过的内置属性(即预定义属性)不能被删除外,prototype 中声明的属性也不能 delete:


代码如下:

 
function C() { this.x = 42; }
C.prototype.x = 12;
C.prototype.y = 13;
var o = new C();
o.x; // 42, 构造函数中定义的o.x
delete o.x; //true 删除的是自身定义的x
o.x; // 12, prototype中定义的o.x,即使再次执行delete o.x也不会被删除
delete o.y; //true,因为 o自身没有o.y属性,y存在于prototype链中,也就是说对象自身属性和prototype属性是不同的
o.y; //13


小结



上面说了那么多,希望对大家认识 JavaScript 中的 Delete 有所帮助。由于水平有限,不保证完全正确,如果发现错误欢迎指正。



原文为:


1、http://perfectionkills.com/understanding-delete/(英文)


2、http://nanto.asablo.jp/blog/2008/01/09/2552470(日文)



本文首发http://jscode.cnblogs.com

作者最新文章