跳转至

01 delete 0:JavaScript中到底有什么是可以销毁的

你好,我是周爱民,感谢你来听我的专栏。

今天是这个系列的第一讲,我将从JavaScript中最不起眼的、使用率最低的一个运算——delete讲起。

你知道,JavaScript是一门面向对象的语言。它很早就支持了delete运算,这是一个元老级的语言特性。但细追究起来,delete其实是从JavaScript 1.2中才开始有的,与它一同出现的,是对象和数组的字面量语法。

有趣的是,JavaScript中最具恶名的typeof运算其实是在1.1版本中提供的,比delete运算其实还要早。这里提及typeof这个声名狼藉的运算符,主要是因为delete的操作与类型的识别其实是相关的。

习惯中的“引用”

早期的JavaScript在推广时,仍然采用传统的数据类型的分类方法,也就是说,它宣称自己同时支持值类型和引用类型的数据,并且,所谓值类型中的字符串是按照引用来赋值和传递引用(而不是传递值)的。这些都是当时“开发人员的概念集”中已经有的、容易理解的知识,不需要特别解释。

但是什么是引用类型呢?

在这件事上,JavaScript偷了个懒,它强行定义了“Object和Function就是引用类型”。这样一来,引用类型和值类型就给开发人员讲清楚了,对象和函数呢,也就可以理解了:它们按引用来传递和使用。

绝大多数情况下,这样解释起来是行得通的。但是到了delete运算这里,就不行。

因为这样一来,delete 0就是删除一个值,而delete x就既可能是删除一个值,也可能是删除一个引用。然而,当时JavaScript又同时约定:那些在global对象上声明的属性,就“等同于”全局变量。于是,这就带来了第三个问题:delete x还可能是删除一个global对象上的属性。而它在执行这个操作的时候,看起来却像是一个全局变量(的名字)。

这中间有哪些细节的区别呢?

delete这个运算的表面意思,是该运算试图销毁某种东西。然而,delete 0中的0是一个具体的、字面量表示的“值”。一个字面量值“0”如何在现实世界中销毁呢?假定它销毁了,那是不是说,在这个语言当前的运行环境中,就不能使用0这个值了呢?显然,这不合理。

所以,JavaScript认为“所有删除值的delete就直接返回true”,表明该行为过程中没有异常。很不幸,JavaScript 1.2的时代并没有结构化异常处理(即try…catch语句)。所以,通过函数调用中返回true来表明“没有异常”,其实是很常规的做法。

然而,返回值只表明执行过程中没有异常,但实际的执行行为是“什么也没发生”。你显然不可能真的将“0”从执行系统中清理出去。

那么接下来,就还剩下删除变量和删除属性。由于全局变量实际上是通过全局对象的属性来实现的,因此删除变量也就存在识别这两种行为的必要性。例如:

delete x

这行代码究竟是在删除什么呢?出于JavaScript是动态语言这项特性,从根本上来说,我们是没有办法在语法分析期来判断x的性质的。所以现在,需要有一种方法在运行期来标识x的性质,以便进一步地处理它。

这就导致了一种新的“引用”类型呼之欲出。

到底在删除什么?

探索工作往往如此,是所谓“进五退一”,甚至是“进五退四”。在今后的专栏文章中,你往往会看到,我在碰触到一种新东西的时候会竭力向前,但随后又后退好几步,再来讨论一些更基础层面的东西。这是因为如果不把这些基础概念说得清楚明白,那么往前冲的那几步常常就被带偏了方向。

一如现在这个问题:delete 0到底是在删除什么?

对于一门编译型语言来说,所谓“0”,就是上面所述的一个值,它可以是基础值(Primitive values),也可以是数值类型。但如果将这个问题上升到编译之前的、所谓语法分析的阶段,那么“0”就会被称为一个记号(Tokens)。一个记号是没有语义的,记号既可以是语言能识别的,也可以是语言不能识别的。唯有把这二者同时纳入语言范畴,那么这个语言才能识别所谓的“语法错误”。

delete不仅仅是要操作0或x这样的单个记号或标识符(例如变量)。因为这个语法实际起作用的是一个对象的属性,也就是“删除对象的成员”。那么它真正需要的语法其实是:

delete obj.x

只不过因为全局对象的成员可以用全局变量的形式来存取,所以它才有了

delete x

这样的语法语义而已。所以,这正好将你之前所认识的倒转过来,是删除x这个成员,而不是删除x这个值。不过终归有一点是没错的:既然没办法表达异常,而delete 0又不产生异常,那么它自然就该返回true。

然而,如果你理解了delete obj.x,那么就一定会想到:obj.x既不是之前说过的引用类型,也不是之前说过的值类型,它与typeof(x)识别的所有类型都无关。因为,它是一个表达式。

所以,delete这个操作的正式语法设计并不是“删除某个东西”,而是“删除一个表达式的结果”:

delete UnaryExpression

表达式的结果是什么?

在JavaScript中表达式是一个很独特的东西,所有一切表达式运算的终极目的都是为了得到一个值,例如字符串。然后再用另外一些操作将这个值输出出来,例如变成网页中的一个元素(element)。这是JavaScript语言创生的原力,也是它的基础设计。也只是因为有了这种设计,它才变得既像面向对象的,又像函数式语言的样子。

表达式的执行特性,以及表达式与语句的关系等等细节,回头我放在第二阶段的内容中讲给你听。现在我们只需要关注一个要点,表达式计算的结果到底是什么?因为就像上面所说的,这个结果,才是delete这个操作要删除的东西。

在JavaScript中,有两个东西可以被执行并存在执行结果(Result),包括语句和表达式。比如你用eval()来执行一个字符串,那么实际上,你执行的是一个语句,并返回了语句的值;而如果你使用一对括号来强制一个表达式执行,那么这个括号运算得到的,就是这个表达式的值。

表达式的值,在ECMAScript的规范中,称为“引用”。

这是一种称为“规范类型”的东西。

规范中的“引用”

实际上这个概念出现得也很早。从JavaScript 1.3开始,ECMAScript规范就在语言定义的层面,正式地将上述的天坑补起来,推出了上面说到的这个“(真正的)引用类型”。

但是,由于这个时候规范的影响力在开发人员中并不那么大,所以开发人员还是习惯性地将对象和函数称为引用,而其它类型就称为值,并且继续按照传统的理解来解释JavaScript中对数据的处理。

这种情况下,一个引用只是在语法层面上表达“它是对某种语法元素的引用”,而与在执行层面的值处理或引用处理没关系。所以,下面这行简短的语句:

delete 0

实际上是在说:JavaScript将0视为一个表达式,并尝试删除它的求值结果。

所以,现在这里的0,其实不是值(Value)类型的数据,而是一个表达式运算的结果(Result)。而在进一步的删除操作之前,JavaScript需要检测这个Result的类型:

  • 如果它是值,则按照传统的JavaScript的约定返回true;
  • 如果它是一个引用,那么对该引用进行分析,以决定如何操作。

这个检测过程说明,ECMAScript约定:任何表达式计算的结果(Result)要么是一个值,要么是一个引用。并且需要留意的是,在这个描述中,所谓对象,其实也是值。准确地说,是“非引用类型”。例如:

delete {}

那么显然,这里要删除的一对大括号是表示一个字面量的对象,当它被作为表达式执行的时候,结果也是一个值。这也是我常常将所有这类表达式称为“单值表达式”的原因,这里并没有所谓的“引用”。

你可以像下面这样,非常细致而准确地解释这一行代码:单值表达式的运算结果返回那个“对象字面量”的单值。然后,delete运算发现它的操作数是“值/非引用类型”,就直接返回了true。

所以,什么也没有发生。

还会发生什么

那么到底还会发生什么呢?

在JavaScript的内部,所谓“引用”是可以转换为“值”,以便参与值运算的。因为表达式的本质是求值运算,所以引用是不能直接作为最终求值的操作数的。这依赖于一个非常核心的、称为“GetValue()”的内部操作。所谓内部操作,也称为内部抽象操作(internal abstract operations),是ECMAScript描述一个符合规范的引擎在具体实现时应当处理的那些行为。

GetValue()是从一个引用中取出值来的行为。这有什么用呢?比如说下面这行代码:

x = x

我们上面说过,所谓x其实是一个引用。上面的表达式其实是一个赋值表达式,那么“引用x赋值给引用x”有什么意义呢?其实这在语法层面来解释是非常直接的:

所有赋值操作的含义,是将右边的“值”,赋给左边用于包含该值的“引用”。

那么上面的x=x,其实就是被翻译成:

x = GetValue(x)

来执行的。而JavaScript识别两个x的不同的方法,就称为“手性”,即是所谓“左手端(lhs, left hand side)”和“右手端(rhs)”。它本来是用来描述自然语言的语法中,一个修饰词应该是放在它的主体的前面或是后面的。而在程序设计语言中,它用来说明一个记号(Token)是放在了赋值符号(例如“=”号)的左边或是右边。作为一个简单的结论,区别上例中的两个x的方法就是:

如果x放在左边作为lhs,那么它是引用;如果放在右边作为rhs,那么就是值。

所以x=x的语义并不是“x赋给x”,而是“把值x赋给引用x”。

所以,“delete x”归根到底,是在删除一个表达式的、引用类型的结果(Result),而不是在删除x表达式,或者这个删除表达式的值(Value)

是的,在JavaScript中的delete是一个很罕见的、能直接操作“引用”的语法元素。由于这里的“引用”是在ECMAScript规范层面的概念,因此在JavaScript语言中能操作它的语法元素其实非常少。

然而很不幸,delete就是其中之一。

告诉我这些有什么用

等等,我想你一定会问了:神啊,让我知道这些究竟有什么用呢?我永远也不会去执行delete 0这样的操作啊!

是的。但是我接下来要告诉你的事实是:obj.x也是一个引用。对象属性存取是JavaScript的面向对象的基本操作之一,所以本质上我们早就在使用“引用”这个东西了,只不过它太习以为常,所以大家都视而不见。

“属性存取("."运算符)”返回一个关于“x”的引用,然后它可以作为下一个操作符(例如函数调用运算“()”)的左手端来使用,这才有了著名的“对象方法调用”运算:

obj.x()

因为在对象方法调用的时候,函数_x()_是来自于obj.x这个引用的,所以这个引用将obj这个对象传递给x(),这才会让函数_x()_内部通过this来访问到obj。

根本上来说,如果obj.x只是值,或者它作为右手端,那么它就不能“携带”obj这个对象,也就完成不了后续的方法调用操作。

对象存取 + 函数调用 = 方法调用

这是JavaScript通过连续表达式运算来实现新的语义/语法的经典示例。

而所谓“连续运算”其实是函数式运算范式的基本原则。也就是说,obj.x()是在JavaScript中集合了“引用规范类型操作”“函数式”“面向对象”和“动态语言”等多种特性于一体的一个简单语法。

而它对语言的基础特性的依赖,就在于:

  • delete 0中的这个0是一个表达式求值;
  • delete x中的x是一个引用;
  • delete obj.xobj.x是一组表达式连续运算的结果(Result/引用);

于是,我们现在可以解释,当x是全局对象global的属性时,所谓delete x其实只需要返回global.x这个引用就可以了。而当它不是全局对象global的属性时,那么就需要从当前环境中找到一个名为x的引用。找到这两种不同的引用的过程,称为ResolveBinding;而这两种不同的x,称为不同环境下绑定的标识符/名字。

知识回顾

下一讲我将给你讲述的,就是这个名字从声明到发现的全过程。至于现在,这一讲就要告一段落了。今天的内容中,有一些知识点我来带你回顾一下。

  • delete 运算符尝试删除值数据时,会返回true,用于表示没有错误(Error)。
  • delete 0的本质是删除一个表达式的值(Result)。
  • delete x与上述的区别只在于Result是一个引用(Reference)。
  • delete其实只能删除一种引用,即对象的成员(Property)。

所以,只有在delete x等值于delete obj.x时delete才会有执行意义。例如with (obj) ...语句中的delete x,以及全局属性global.x。

思考题

  • delete x中,如果x根本不存在,会发生什么?
  • delete object.x中,如果x是只读的,会发生什么?

希望你喜欢我的分享。

精选留言(15)
  • 海绵薇薇 👍(108) 💬(12)

    老师好,我又来了:-) 1. delete 0 这里的0是一个值(就当前情况),而不是引用是吗? 2. delete x (x不存在) 返回true x 表达式返回的应该是一个引用,并且环境中并没有表示这个引用不能被删除,这个理解对吗? 但是文章中有提到delete只能删除属性这一种引用,糊涂了,估计这里的理解还是有问题。 3. delete null 返回true delete undefined 返回false 为啥啊?不都是值吗? 4. 还想知道昨天提问的1和2两条是不是漏洞百出啊,就想知道个结果😁。

    2019-11-19

  • 海绵薇薇 👍(68) 💬(9)

    hello 老师好,感谢老师之前的回答:) 突然想到,访问不存在的变量x报ReferenceError错误,其实是对x表达式的的Result引用做getValue的时候报的错误,然后为啥typeof x和delete x不报错,因为这两个操作没有求值。

    2019-11-22

  • 潇潇雨歇 👍(33) 💬(4)

    1、如果x根本不存在,delete x什么也不做,返回true 2、如果x只读,delete object.x不能删除掉x属性,返回false;如果在严格模式下,会报错:TypeError: Cannot delete property 'c'

    2019-11-11

  • 潇潇雨歇 👍(25) 💬(7)

    关于delete的知识,大家可以看下MDN的讲解:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/delete 以及这篇深入delete博客:http://perfectionkills.com/understanding-delete/

    2019-11-11

  • SOneDiGo 👍(21) 💬(1)

    想问下老师如何理解用delete处理array element实际上在底层是如何操作的? 例如:array = [1,2,'1'] 为什么 delete array[2] 后数组就成了[1,2,undefined/empty]?

    2019-11-22

  • 海绵薇薇 👍(20) 💬(3)

    感谢老师指点😁 ref:语法上的引用 我又看了几遍文章并根据提供的连接,得出如下结论: 1. var x x = 0 console.log(x) x 表达式返回的是一个ref({referencedName: 'x', base: Environment Record}),然后计算值getValue(ref)得到具体的值,具体的值会分为传统意义上的基本类型和引用类型 2. 衍生出下面的猜想 var obj obj = {a: 1} console.log(obj.a) obj.a 也是一个ref({referencedName: 'a', base: obj}),然后计算值的时候getValue(ref)得到具体的值1 3. 关于表达式的结果Result的疑问。 文中说:表达式的值,在 ECMAScript 的规范中,称为“引用”。(表达式的结果(Result)是引用。) 但是后文说Result可能是引用/值。 这里的值我不能很好的理解。值指的是另一种引用的格式吗?例如链接文档中提到的base其实有很多种值 undefined, Object, a Boolean, a String, a Symbol, a Number。值指的是{base: 0}这种引用吗?如果不是这样的话base的Boolean等基本值类型有啥用啊? 还是说 0 这个表达式的Result就是0这个值? 期待老师的指点😁

    2019-11-18

  • 潇潇雨歇 👍(19) 💬(2)

    看的第三遍。还是要去看看规范加深理解。 如果x根本不存在,delete x操作时,x首先是一个表达式,语义上是一个引用,然后去寻找该引用的result,但是x根本不存在,是找不到的。也就做不了什么,返回ture。 如果obj.x是只读的或者不可配置的,表示他是不能删除的,但是他是实实在在的引用,是可以求值得到Result的,所以返回false。表示不能删除。

    2019-11-16

  • Wiggle Wiggle 👍(16) 💬(1)

    即便 obj.x 是一个 function,当 obj.x 作为右手端时,也会被 GetValue 方法抽取出值来,而这个“值”并不是直觉上的数字或字符串。这里是有恍然大悟的感觉的,“值”和“引用”应当从严格的规范定义层面理解,而不能从直觉上来理解,只要满足定义,那就是“值”/“引用”。

    2019-11-12

  • Ming 👍(16) 💬(4)

    乍一读,云里雾里。翻了文档并做测试,总结如下: delete 操作符用于删除对象的属性,它接收一个表达式,该表达式应返回对象属性的引用。 1. 如果表达式返回的结果是引用: 当该引用是 let 或 const 定义的,delete 执行结果总是 false; 当引用作为对象的属性不存在时,delete 对象的属性,执行结果为 true,表示未处理; 当该引用为 window 对象的属性且是 var 定义的,delete window 对象的属性,执行结果为 false,表示处理失败(获取属性描述符时为不可配置); 如果在全局环境下显示定义一个属性描述符为可配置的全局属性,执行 delete,结果是 true,表示操作成功; 当该引用为非 window 对象的属性且是 var 定义的,delete 非 window 对象的属性,执行结果为 true,表示处理成功(获取属性描述符时为可配置)。 2. 如果表达式返回的结果是值,如数字、字符串等,delete 执行结果为 true,表示未处理。

    2019-11-11

  • 隔夜果酱 👍(14) 💬(1)

    既然delete这么鸡肋,只能删除对象的成员. 那么后来的版本中为什么不进行改进呢? 比如限定其只能用delete obj.x这种语法格式. 或者加入trycatch,对删除value的操作直接报错呢?

    2019-11-11

  • 渭河 👍(13) 💬(3)

    这句话要怎么理解呀 所谓值类型中的字符串是按照引用来赋值和传递引用(而不是传递值)的

    2019-11-21

  • 桃翁 👍(12) 💬(3)

    我突然 明白了 (obj.func=obj.func)()这种方式会丢掉obj里面的this,因为等号右边的obj.func是值,所以得到的仅仅是个函数这个值,而不是引用。 老师我理解得是对的吗?

    2020-03-19

  • 余文郁 👍(12) 💬(5)

    老师,JS是基于对象的语言,不是面象对象的语言吧,感觉第二段这有点不妥,虽然ES6增加了class语法,但只是原型的语法糖而已

    2019-11-11

  • ssala 👍(10) 💬(2)

    关于delete,搜集了一些资料,结合代码测试,我目前是这样理解的:delete为一元操作符,其操作数为一个表达式,如果表达式的求值结果是一个值,那么`delete 值`直接返回true,表示该操作没有异常。如果表达式求值结果是一个引用,那么`delete 引用`则会有如下表现,如果引用是可删除的,则直接删除该引用,返回true,否则返回false。 关于属性/property的可删除特性,参照这篇文章:http://perfectionkills.com/understanding-delete/ 关于引用和值的理解,我用段代码说明,如下:

    var x = {a: 20}
    
    代码中,x是引用,它"指向"执行系统中{a: 20}的一个对象,而{2: 20}则是值,它对应执行系统中内存上的一块区域。x.a是引用,它"指向"执行系统中内存20这个值,而20是值,它也对应执行系统中内存上的一块区域。因此:
    delete x // `false` ,x为表达式,求值结果为global.x,且该属性是用var来声明的,其特性是不可删除
    
    delete 20 // `true`,当执行系统遇到20字面量时,认为其为表达式,对其求值以后得到20这个值,delete 值返回true
    
    delete x.a // `true` x.a 为引用,且可以删除
    
    另外关于delete x,若x不存在,我的解释是:x为表达式,由于未定义,表达式求值结果是未定义的,但是虽然未定义,但求值结果仍然是值,而delete 值就返回true。不知这种解释是否正确?

    2019-11-14

  • blueBean 👍(8) 💬(1)

    表达式的值,在 ECMAScript 的规范中称为“引用”。 ECMAScript 约定:任何表达式计算的结果(Result)要么是一个值,要么是一个引用。 上面这两句话矛盾了吧

    2020-02-21