HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

delete 0:到底是在删除什么?

  • 2019-11-15
  • 本文字数:4848 字

    阅读完需:约 16 分钟

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的性质,以便进一步地处理它。


这就导致了一种新的“引用”类型呼之欲出。(“引用”到底有什么用?它对我们的编程有什么实际的影响呢?我会在《JavaScript 核心原理解析》专栏的 02 讲中详细为你讲解。)

到底在删除什么?

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


一如现在这个问题: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)识别的所有类型都无关。因为,它是一个表达式。


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


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,称为不同环境下绑定的标识符/名字。

分享回顾

《JavaScript 核心原理解析》专栏下一讲中我将给你讲述的,就是这个名字从声明到发现的全过程。在今天分享的内容中,有一些知识点我来带你回顾一下。


  • delete 运算符尝试删除值数据时,会返回 true,用于表示没有错误(Error)。

  • delete 0 的本质是删除一个表达式的值(Result)。

  • delete x 与上述的区别只在于 Result 是一个引用(Reference)。

  • delete 其实只能删除一种引用,即对象的成员(Property)。


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


希望你喜欢我的分享。戳此可以免费试读我的专栏


2019-11-15 18:113476

评论

发布
暂无评论
发现更多内容

Java实战干货|Spring Boot整合MyBatis框架快速实现数据操作

三十而立

Java spring springboot

全网超火Blender零基础教程!从零上手的免费3D软件

Finovy Cloud

blender 3D软件

“AI 上运动,直播更精彩” 百度智能云联手 Pixellot 创新中国大众体育传播新模式

Baidu AICLOUD

视频云 云导播

DevOps|研发效能不是老板工程,是开发者服务

laofo

DevOps cicd 研发效能 持续交付 平台工程

YOWOv2:优秀的实时视频动作检测框架

Zilliz

计算机视觉 构建模型 Milvus

华大北斗芯片亮相纽伦堡国际嵌入式展EW2023

江湖老铁

iOS上架App Store详解(图文)

雪奈椰子

常用MQTT客户端库简介

EMQ映云科技

物联网 IoT mqtt 客户端 企业号 3 月 PK 榜

代码开源!阿里妈妈展示广告Match底层技术架构最新进展

阿里技术

开源

抽丝剥茧还原真相,记一次神奇的崩溃

阿里技术

debug

阿里表哥甩我一份Redis笔记,看完还进不了阿里让我卖豆腐去

做梦都在改BUG

Java 数据库 nosql redis 缓存

这是我见过最好的JVM笔记,拿到阿里offer后我哭了

做梦都在改BUG

Java JVM 虚拟机 垃圾回收

牧云助手:一款面向技术爱好者的远程主机管理工具

百川云开发者

运维 主机管理 终端远程协助

测试人社区——软件测试技术沙龙分享

测试人

人工智能 软件测试 精准测试 工程效能 ChatGPT

焱融科技荣登《2022中国企业数智化创新TOP50》榜单

焱融科技

文件存储 分布式文件存储 数智化 高性能存储 全闪存储

如何通过优化图片、JS等资源加载项来提高网页的加载速度?

兴科Sinco

前端开发 CDN HTTP 网页加速

《深入理解高并发编程:JDK核心技术》-冰河新书上市

冰河

并发编程 多线程 高并发 协程 异步编程

2023年10大主流产品路线图绘制软件盘点

爱吃小舅的鱼

产品经理 产品路线图工具

如何学习分布式系统,分布式是什么,这里有很好的解释,很全

三十而立

Java 分布式

海泰方圆又双叒叕连获两项隐私计算专利

电子信息发烧客

面试官:String字符串的最大长度是多少?

做梦都在改BUG

Java

Kubernetes玄幻问题一览

陆云

#Kubernetes#

Notification(状态栏通知)详解

梦笔生花

android Android Studio Notification

一个小网站的云原生实践

松然聊技术

架构 云原生

自己动手写虚拟机

ScratchLab

虚拟机 kvm

GitHub 润色框架,让你的GitHub不再索然无味

做梦都在改BUG

GitHub

传统企业,如何构建性能测试技术体系

老张

技术 #性能测试

携手共进丨九科信息入选信通院“铸基计划”高质量数字化转型产品及服务全景图,并受邀出席高质量数字转型创新大会

九科Ninetech

强!阿里P9限时开源的实战笔记:SpringBoot2精髓

做梦都在改BUG

Java 面试 微服务 Spring Boot 框架

Copilot 用“粉紫色磨砂UI”和“啊啊啊BGM ”,梦境了这届网友

B Impact

云智一体,深入生命科学

Baidu AICLOUD

基因测序 AI制药 AI for Science

delete 0:到底是在删除什么?_大前端_周爱民_InfoQ精选文章