CategoryResourceRepost/极客时间专栏/JavaScript核心原理解析/从零开始:JavaScript语言是如何构建起来的/01 | delete 0:JavaScript中到底有什么是可以销毁的.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

210 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="01 | delete 0JavaScript中到底有什么是可以销毁的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/86/f5/866ed6d1256f70b5727bea4313c145f5.mp3"></audio>
你好,我是周爱民,感谢你来听我的专栏。
今天是这个系列的第一讲我将从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.x`中`obj.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是只读的会发生什么
希望你喜欢我的分享。