概述
本文是一个系列,包括:
- JavaScript的元系统 - https://blog.csdn.net/aimingoo/article/details/82014291
- JavaScript中创建原子的几种方法 - https://blog.csdn.net/aimingoo/article/details/82080105
- 元类型系统是对JavaScript内建概念的补充(本文)
JavaScript的类型系统一贯是反人类的,以至于JavaScript之父Eich都会跳出来说“我做错了”。但是这并不是说它的整个类型系统就是不可理解的,相反,它提供了观察这门语言的多个不同角度。
远古JavaScript中的类型系统
在远古时期(我是指JavaScript 1.0)中,JavaScript的世界里还并没有“原型”这个东西,而且undefined
也是一个奇葩的存在。
这时的undefined
是一个概念定义而非值声明,它表明函数或某个运算(例如属性存取)没有返回值。由于还没有===
和!==
运算符,所以undefined
被约定为与null
是等值的。
考察这个阶段,可以发现整个类型系统其实只有如下几种值类型。每种类型都包括一个它们各自表示(逻辑上的)“无值”的成员:
- 字符串值,空串是无值;
- 数字值,NaN是无值(数字值中的0表达非值);
- 布尔值,false是非值(或也可以理解为无值);
接下来就是对象和函数。它们都是引用类型(从最早的JavaScript语言就这样定义了),而且也各有一个表达“无值”的方式:
- 函数(以及表达式运算或值运算等)的结果:undefined
- 对象:null
这基本上就是JavaScript 1.0时代对类型系统的全部理解,以及假设。——之所以称之为假设,是因为这时连typeof
运算符都没有,所我们无法在语言的层面上验证它。
不过有趣的是,这个时代是支持面向对象编程的。JavaScript 1.0中的面向对象是“基于类”的,它有“类构造对象(的实例)”的概念:
obj = new Object;
// OR
function MyObject() {
this.x = 100;
}
obj = new MyObject;
在这时,Object
/MyObject
已经被称为构造器(constructor)了。它采用的是被称为“类抄写”的方式,通过向实例“this
”上添加成员来初始化对象。
这个时代并没有原型继承,也不支持instanceof
运算,因此尽管Object
/MyObject
执行“构造一个对象”的职责,却没有人认得它是“类
”。由于JavaScript默认函数总是可以作new
运算,并且this
可以缺省指向global
,因此它是不是“类”就不要紧,用new运算时都不会出错。
NOTE1:往前追溯20多年,一本名为《结构程序设计( Structured programming)》的书定义了面向对象编程。其中说:如果一个过程产生了比它生存时间更久的实例,那么这个过程就称为类,而这个实例就称为对象。因此,JavaScript 1.0时代的面向对象是古典的、传统的设计,不可思议却又历久弥新。
NOTE2:有关JavaScript 1.0,可以参见这里:https://docs.oracle.com/cd/E19957-01/816-6408-10/object.htm#1193255
概念灾难的源头
从JavaScript 1.1开始,这门语言总算提供了“识别自己的类型”的运算,也就是著名的typeof
。在语义上,它提出的想法是:用undefined
表达语言层面的“没有(无值)”。
由于在JavaScript 1.0中已经为string
、boolean
等各自定义了一个它们在语言层面上的“没有”,因此这一设计也就保留了下来。在1.1中,typeof
返回如下六个值之一:
针对值类型:’undefined’、’string’、’boolean’和’number’
针对引用类型:’object’和’function’
有了undefined
,JavaScript可以自如地表达类型间的运算,例如A + B
。任何函数/过程总应该是有一个静态结果的,因此要么它是值,要么它就是无值(undefined)。——基本想法是:如果算不出结果,那么这个过程应当返回undefined
。
而null
表示的是对象层面的没有,因此它被理解为一个对象,也就是说它的typeof
值仍然是’object’。——就好象说NaN表达数值上的‘无值’,但仍然是数字值。例如:
> typeof null
'object'
但是null
又并不是由对象系统创建出来的,因此它不是Object()
或其子类的实例。因此:
> null instanceof Object
false
对象也会参与跨类型的值计算,因此它也需要一个“对象的值的含义”。于是Object.prototype.valueOf()
就出现了。当JavaScript在值运算中发现操作数(x)是对象时,就会调用x.valueof()
来得到它的值类型数据,并以此为操作数来进行值运算。这个过程很简洁,也很完美。
稍稍差一点的是null
,它是对象,但又不继承自Object()
,所以没有Object.prototype.valueOf()
,而它就必须“被理解”有自己的value
。——但是,如果一个运算数“有自己的value
”,那么它不就是值么?
是的。概念开始混乱了。
JavaScript在1.x版本中确立的类型系统
无论如何,JavaScript在1.x版本中确立了自己对类型系统的理解。
这个类型系统可以完美地概括JavaScript中可能的各种对象和概念理解。其中”值类型”用于进行值运算或表达值运算的结果,而引用类型用于“索引到”一个值。
在这个系统中,null
并不在基本类型系统中,它只是在“对象类型系统”中的、一个特殊对象的字面量表示。
惹祸的ECMA
ECMA开始制定ECMA-262(也就是ECMAScript)标准时,JavaScript已经发展到了v1.2版本,一直到后来发布ECMAScript ed3时(1999.12),它才基本与JavaScript v1.3对齐。随后(2000.07~11月),JavaScript发布了v1.5以及JScript发布了v5.5,才将现实中可用的JavaScript版本与ECMA规范标准对应起来,基本上三者同一了。
然而从ECMAScript ed1开始,它就采用了一种“不同寻常”的类型描述方法。在ECMAScript中,一直将Null
独立作为一个类型来描述,并称之为“六种基本类型(six standard types)”之一。而这,显然是与JavaScript的typeof
的返回值有异的。并且ECMAScript还描述了几种用于实现JavaScript的“内部类型(internal types),其中最重要的就是从ECMAScript ed1就开始包括的完成(Completion)、引用(Reference)和列表(List)。
从ECMAScript ed5开始,“基本类型”与“内部类型”就分别被称为语言类型与规范类型了。
根底上的原因,还在于ECMAScript并不是要描述“JavaScript的语言性质”,而是要描述“JavaScript如何实现”。在比目标语言级别更高的维度上,ECMAScript通过所谓“规范类型”来描述和解释“实现JavaScript”时可能操作的数据以及操作这些数据的方式。——它们被严格的区分为两种:对象,或非对象。其中,function
类型从一开始就是对象,因而不存在语义矛盾。出于在前面讲到的种种理由,处理null
时就比较尴尬了:它在JavaScript中是对象,却用来表示“没有对象(对象的‘无’值)”。
ECMAScript再一次在概念上向typeof
的结果说了不,它约定:null
是值,是非对象类型的。
ECMAScript中的类型系统
所以一直以来,JavaScript不得不按“两种类型系统”来进行语言描述,一种是JavaScript应用环境中的,称为“语言类型(Language types)”;另一种则是ECMAScript中的,称为“规范类型(Specification type)”。而ECMAScript在规范中描述的“语言类型”,还与它在实现中使用typeof()
得到的结果不一致。
在这两种类型系统中,JavaScript语言认为
null
是个是引用类型(Object)的值,而ECMAScript认为null是个原始类型(Primitive types)的值。NOTE3:很不幸的是,在讨论JavaScript的时候,这两种描述都是对的。
在ECMAScript中考察一个值(V)的类型时,是根据其内部操作Type()
的结果值,来确定该值V
是或不是“ECMAScript language value”,或者是更具体的某个类型。
当JavaScript引擎在分析脚本代码时,会将代码解析成为数据或执行逻辑以便后续处理,这(通常)也就对应于ECMAScript规范类型中的记录(Record)和词法环境(Lexical Environment)。而在运行期,任何的JavaScript代码最终都会被理解成“可执行的”语句或表达式,并且当一个操作(op)是表达式时,它总有结果值;而当操作是语句时,它的最终状态是由一个称为“完成记录(Completion Record)”的规范类型来存放的——并用这个记录的[[value]]
字段来存放语句的结果值。
下图完整地展示了四种主要的规范类型的使用。
null仍然是无法解释的
上述由ECMAScript构建的类型系统运转良好——比如我们确实可以按照这样的规范来编写一个JavaScript引擎。然而它仍然无法有效地解释null
值的语言特性。
尤其是在ES5之后。因为ES5开始提供了新的创建对象的语法:
obj = Object.create(null)
ECMAScript无法对“对象create
自null”这样的语义给出合理解释,其根本原因在于它隐藏(至少是有意不讨论)这样的一个事实:Object.prototype
就是一个“创建自null”的对象:
> Object.getPrototypeOf(Object.prototype)
null
ECMAScript禁止用户代码向Object.prototype
置值或改变属性——这很容易理解,它将prototype
创建为一个只读且不可配置的属性就可以了。但是,ECMAScript还同时禁止了用户代码改变Object.prototype
的原型,亦即是:
// 当置为非null值时将触发异常
> Object.setPrototypeOf(Object.oprototype, {})
TypeError: Immutable prototype object '#<Object>' ...
所以Object.prototype
被称为“不可变原型对象(Immutable prototype object)”。——在ECMAScript中目前只有Object.prototype
和模块名字空间的原型会这样(尽管后者并没有明确指出)。
NOTE4:
Object.prototype
是唯一被明确约定为“不可变原型对象(Immutable prototype object)”的,而<aNamespaceOfModule>.prototype
却只是置它的[[SetPrototypeOf]]
内部槽为一个特殊的写方法。这二者的处理方法并不一致,一定程度上是为了强调在“Object.prototype
是什么对象”这个问题上的特殊性。
ECMAScript只说明了“该对象不可变原型”这样的性质,却没有解释“原型为null的对象是什么”这一问题。因为ECMAScript在语义上并没有对应于“创建自null的对象”这样的概念。也正是因此,下面的类声明才显得牵强:
class MyClass extends null {
// ...
}
并且当它没有自有的构造方法(constructor)时,才会出现下面的问题:
extends xxx
决定了缺省情况下由父类来创建实例;但是extends null
表明父类是null;所以,- 创建过程出现异常。
> new MyClass
TypeError: Super constructor null of MyClass ...
Metameta的概念补充
在Metameta(@aimingoo/metameta)中对这一现象给出了自己的解释:
- 当一个对象创建自null时,它是一个原子;且,
- 派生自这种原子的、非Object()及其子类实例的对象,是原子对象。
在这样的解释下,可以看到Object.prototype
本质上来说也是一个原子对象。亦即是说,我们找到了ECMAScript/JavaScript创建自己的对象系统的原始方式。加上JavaScript开放了get/setPrototypeOf()
和有关操作属性描述符的方法,于是我们既得了创建“原子/原子对象”的能力,也得到了在“属性表”这一级别维护这些原子自有成员的能力。
进一步的,由于JavaScript中的'function'
类型事实上也是对象,因此我们既得到了表达静态数据的原子,也得到了支持运算过程的原子。再加上JavaScript还通过eval()
提供了原生的解析代码和操作执行上下文的能力,那么——事实上——我们也就得到了整个的原子计算环境/运行框架。
这也是Metameta提供了一个.from()
工具方法的原因:Metameta致力于扩展JavaScript的内建概念,并试图用为ECMAScript建立一个更为完整的概念集。在这其中,.from()
所体现出来的事实是:JavaScript的对象系统,是以原子为基础的类型系统的一个实现。
如图:
而下图则是更完整的、以元类型为基础来实现的、对象/类型系统的景象:
最后
以上就是朴素往事为你收集整理的元类型系统是对JavaScript内建概念的补充的全部内容,希望文章能够帮你解决元类型系统是对JavaScript内建概念的补充所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复