写点什么

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:113509

评论

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

netty系列之:在netty中使用TCP协议请求DNS服务器

程序那些事

Java Netty 程序那些事 5月月更

如何用Apifox 发送接口请求?

Liam

开发者 测试 后端 开发 Postman

Fastjson官方再次披露高危漏洞,包括rocketmq、jeecg-boot等近15%的github开源项目受影响

墨菲安全

安全 idea插件 Fastjson 依赖漏洞检测 墨菲安全

是开自助洗车店还是传统洗车店好?

共享电单车厂家

自助洗车加盟 开自助洗车店 传统洗车店

集成底座内外网访问配置说明

agileai

k8s 集成底座 企业服务总线 身份管理平台 主数据平台

更全、更精准,美创科技实现Caché数据库M语言精细化审计

美创科技

cache 数据安全 数据库审计

TreeMap源码分析-新增

zarmnosaj

5月月更

互联网通信安全之终端数据保护

融云 RongCloud

网络攻击盯上民生领域,应对DDoS和APT攻击,如何有效防御?

郑州埃文科技

IP地址 网络资产保护 网络攻击防御

为应用赋能!博云容器云产品族正式发布

BoCloud博云

容器 云原生 容器云

重磅首发!火线安全发布《云安全攻防技术期刊》

火线安全

安全 云安全

网页在线帮助中心的搭建策略

小炮

帮助中心

fastposter v2.8.2 发布 电商海报生成器

物有本末

小区适合投放自助洗车机吗?

共享电单车厂家

自助洗车加盟 小区投放自助洗车机

数字人民币智慧学生证来了,对于特定群体硬钱包或大有可为

CECBC

以区块链技术推进应急管理体系现代化

CECBC

高危!Fastjson反序列化远程代码执行漏洞风险通告,请尽快升级

葡萄城技术团队

json 安全 Fastjson

自助手动洗车设备洗车怎么样?

共享电单车厂家

自助洗车加盟 自助洗车机洗车 自助手动洗车设备

等保二级和等保三级的三大区别讲解-行云管家

行云管家

网络安全 等保 等级保护 等保2.0 等保二级

前端监控的搭建步骤,别再一头雾水了!

杨成功

架构 大前端 5月月更

《Mybatis 手撸专栏》第9章:细化XML语句构建器,完善静态SQL解析

小傅哥

源码分析 小傅哥 源码学习 手写Mybatis 源码实践

2021年证券类APP更新迭代监测专题分析(中)发布

易观分析

证券

YARN Federation技术解析及应用

移动云大数据

YARN

智能汽车领域的开源软件供应链安全检测工具分享

墨菲安全

idea插件 工具分享 开源安全 墨菲安全 软件供应链

避免惊群以及负载均衡的原理与具体实现

C++后台开发

nginx 负载均衡 后端开发 C++后台开发 惊群

从流量交换到共享联盟,开放银行如何助力金融数字转型?

CECBC

固定资产投资管理系统解决方案

低代码小观

资产管理 CRM系统 客户关系管理系统 企业设备管理 设备巡检管理系统

MBTI 剧透人生,你的天选职业是什么?(免费测)

融云 RongCloud

模块二

Geek_2ce415

数据库厂家有哪些?排名怎么样?

行云管家

数据库 IT运维 运维审计 数据库审计

uniapp 如何将输入值转成大写

CRMEB

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