三谈类型问题:ECMAScript为什么错了?
【一、历史】
首先讲一个小历史,这个我以前写过一篇文章在《程序员》上。当年,有一家叫Nombas的公司,发布了一个名为C--的语言,后来做成了一个“能嵌入网页的脚本”。在2001年前后,Nombas官方网页对这一个产品介绍中,使用了“history of scripting”这样的标题,讲述的,却主要是c--这个语言的历史。然而,令人混乱的是:C--这个语言在1998年前后开始按照ECMAScript标准来实现,也就是说,它也是一种JavaScript语言;并且,Nombas公司事实上也是ECMAScript早期的标准委员会成员之一;再并且,一些介绍早期JavaScript历史的材料均根据上述信息,援引Nombas的“history of scripting”这篇文章,进而将JavaScript作为一种脚本语言的源头指向了c--。
后来之后来,很多书籍与网站,写文章又制作图片什么的,把这一切板上钉钉地记入了“历史”。
可是JavaScript之父Eich从来没有承认过这件事情,Nombas公司以及它的老板(C--的发明者)之后也没有再解释过这件事情。
我谈及这段历史,是想说明:一个象“history of scripting”这样令人误解的标题,带来了多大的恶果。而ECMAScript在类型问题上也是如此,单单说它在行文上的、标题上的措辞,是完全“正确无误的”。但是被错误地援引和解释之后,一些“似是而非”的观点就出来了。要说明这一切,我们先得说了解一个事实:即,ECMAScript是一份学术性的、规范性质的手册。因此,有它自己的一套语言组织逻辑和抽象概念在里面——甚至有些只是出现在教材中的概念。因此,不要拿我们一般口头交流用的概念往上去套,在引用一些其它语言中的概念的时候,也要小心。
ECMAScript是不是错的?我记得以前说过,错误的标准也是标准,在它“被纠正”之前,它就是严格正确的。呵呵,这显得有点强辞夺理,但的确如此。而我在本文中所指它的“错误”,而令人误解的这一部分类型描述,而非严格的正确与错误的判决。
【二、类型】
什么是类型?argb同学强调type是指“类型”,而class是指“类别”。这样来讲是很不正确的。首先,在专业的用词中,type与types是不同的。在讲数据类型的时候,我们称一些数据类型的时候,会说它是types,例如:
Types are further subclassified into ECMAScript language types and specification types.
这段文字的意思是说:类型可以分为“ECMAScript 语言(定义的)类型”和“特定类型”两种。注意这里的三个“类型”都用了types。这说明它是一种分类方案。一堆东西分成几堆,每一堆都不是个体——或即使是1个,但仍然是一个分类中的类属概念。
而当讲述单一类型的时候,就用的是type,例如“Object Type”。
我们也要留意,在上面的文字中,ECMAScript所说的“特定类型(specification types)”指的是这份手册中的特殊性。这些类型是用于描述其它类型的,以对ECMAScript这个语言加以进一步实现的。换言之,这些特定类型只出现在“当前的这本ECMAScript手册”中,它不是这个语言语法的一部分,而是用于称述这个语言的实现的。所以,在specification types里面会有所谓的“Property Descriptor”,但在一个具体的JavaScript的语言里面就没这个(在实现ES5的JavaScript语言中会有这个,但与这个规范中讲的并不一致)。
ECMAScript用“语言类型”和“特定类型”这样的分类方法,来描述了“实现一个ECMAScript”所需要的类型支持。但这并不是“一个JavaScript的类型系统”。并不是这样讲的。就好象说,我们讲:世界就人与非人构成。但“人与非人”却并不是世界的类型特性。进一步的讲,“人”包括了“男人、女人、巨人”。这样讲也并没有什么错,但这样没法子考察“巨人”与“男人、女人”的关系。我是说“没法考察”,而不是说“有或没有关系”。
ECMAScript的描述中就存在这样的问题。它说:“语言类型”包括Undefined, Null, Boolean, String, Number 和 Object。这一说法是为了将这些类型与后面的“实现一门语言的过程分别开来。它没法解释:
- Undefined与Null的类型关系
- Function是不是类型
等等这样的问题。所以这段关于“语言类型(ECMAScript language types)”的“定义”尽管放在规范中,作为第8章的最前面的概要介绍,但事实上作不得准。因为ECMAScript本质上是介绍语言的实现与实现中的机制的,而不强调具体的类型的分类法。比如说,整份ECMAScript就没有明确地文字来说明:引用类型与值类型。也同样的原因,第8章中“定义”了Undefined、Null等等,也定义了“Property Attributes”这样在JavaScript中“看不到的”类型。
所以,结论:ECMAScript规范第8章,这一整章是“实现一个JavaScript语言”所需要的类型定义与分类,而不是“JavaScript语言本身的类型”。
【三、定义】
ECMAScript到底有没有讨论这个语言的类型问题的地方呢???
没有。
在第4章中的“Definitions”小节。这是一个对语言“Language Overview”中的定义。这个定义对于“语言”来说,有着真正的有参考性——权威,但不强制的参考性。问题在于,这个部分关于types就直接指向了第8章。因而前面提到的问题一样无解。再则,我们举个例子来看,对于Undefined和Null来说,它们的定义有“type/value”两个,而对于String和Number来说,它们的定义就有“object/type/value”三个。这潜在地说明了它们的不同,但对于function来说,就几乎毫无解释,只是说它是Object,并且是Function()的一个实例。这些解释过于模糊,潜在着一些暗示而又没有确定的含义。
就如果本文开篇中提到的“历史”一样。模糊、暗示以及不确定的含义,使得ECMAScript在类型系统的解释上,存有极大的隐患。
【四、类型系统】
一个语言的类型系统,是使用这个语言编程中的关键信息。
一些高级语言使用“简单类型和复合类型”这样的分类法,例如字符(char)就是简单类型,而字符串(string)就是复合类型。然而仍然是字符串,在不同的语言中也理解得不一样,例如C里面,它就是一个指针;而Pascal里面,既可能是一个字符数组,也可能是一个带隐含字段的结构体。不过总的来说,这一种分类方法有着它明确的依据:一个类型中的值,是不是由其它类型的值构成的。如果是,例如数组、字符串和结构体,就是复合类型;如果不是,例如字符、数字或布尔型,就是简单类型了。
另外一种分类方法是考虑数据的使用方法的,就是引用和传值的问题。如果一个数据按值传递,就是值类型;如果按引用传递,就是引用类型。是否是引用的判断方法很简单,例如:
===
A = <一个数据>;
B = A
===
如果这样的结果中,B和A指向同一数据存储,则它们数据是引用类型的,A和B称为该数据的不同引用;如果它们指向不同的数据存储,则它们的数据是值类型的,A和B的值相等。严格意义上来说,“值相等”是一个语言令人混淆的地方,因为这种情况下,A和B指向不同的存储,因此只能比较存储中的二进制数据是否一致,这样导致的问题有两种:
其一、例如:空字符串和数字0在存储上是一样的。因此从引擎角度上来说,他们是相等的,但自然语言概念中,它们是不同的;
其二、例如:字符串可能占用了非常非常大的存储,两个字符串是否相等就需要逐字节比较,因此效率很低。
所以,引用有引用的问题,值有值的问题。在JavaScript中,”String值“也称为“primitive value”类型;它是值类型,但是却是按引用类型的方式来传值的。
那么JavaScript中的string到底是值类型还是引用类型呢?到底primitive value是值类型还是引用类型呢?到底Null是值还是引用呢?
【五、JavaScript中可能的分类依据】
在前面的讨论中,我的意思是说:
- ECMAScript中并没有提供明确的分类方案,它的类型说明的目的是指向语言的实现,而非语言的使用的;
- 现实中其它语言的分类法,对于JavaScript有一定的水土不服,会存在描述不清楚的地方。
那么我们有或没有其它方法来分类JavaScript中的类型呢?
我想,这种分类方法要与ECMAScript不冲突,也与现实中JavaScript语言的应用不冲突。而对此,我能想得出来的分类方法,就是从typeof这个关键字入手,首先将“基本的类型系统”分出来。这个基本的类型系统是指:一个JavaScript语言必然理解也必然实现的类型。typeof关键字在ECMAScript 5th中被淡化了,但它是伴随这门语言的创生而来,真正深刻地解释了这门语言的原始设计与语言思想。是对这门语言在类型系统上最基本的约束。
我对typeof的价值和作用看得很重。它独一无二地解释了所有可能出现在JavaScript中的数据,绝无混淆。它的结果就只有六种:undefined、string、number、boolean、object和function。其中三种string、number、boolean是这门语言最初的应用环境,即网页中所需的、所能展现的。也是这门语言作为“函数式”特性所能运算的。因为前者,它们作为值,是可以显示于网页;因为后者,它们作为值,是可以计算求值的。关于这一点,我在书中专门列了一个非常大的表,来证明“所有JavaScript的运算,最终都可以表达为这三种值”。——当然这一说法有些不准确。我的意思是说,有些运算是没结果的,例如函数如果没返回,那就是undefined了。但是,同样是在这个问题上,我们也要注意到undefined对于“网页展现”来说没意义,也就是说,它是不应该展示在网页上面的,它只是我们在语言中的一个运算结果,而不是浏览器用户在UI交互上的必须。
undefined、string、number、boolean这四种是值类型,是从这里来的。作为值类型,他们既用于网页(或其它情况下的交互,包括跨语言与平台的接口)展示,也用于计算求值;其中undefined是表明无值。而相应的,object与function是引用类型,而且function与object在类型型别上存在本质上不同。
ECMAScript中说function是一个有[[call]]性质的可调用对象,这是从语言的实现角度来讲的。但如果问一下,在COM中,JScript如何让一个ActiveXObject上的方法可以被调用呢?这个问题ECMAScript就答不出来了。这种情况下,这个ActiveXObject的方法用typeof来检测,到底应该是object呢?还是function呢?这恐怕也是一个大问题。
但根本上来说,一个变量是函数或仅是对象,在于它能否执行。所以如果有变量foo,用typeof检测的结果是function,你就可以唯一判断它可以执行函数调用运算foo()。接下来,它是否支持foo.callee、foo.apply等等,却不好说。这取决于foo instanceof Function是否是真。因为上面的这些性质是Function()的。
在JavaScript的应用中,一定要当心这一点,也就是说,typeof(foo)的返回值,只能决定foo()运算的有效性,其它的含义要从对象系统中去找。
【六、第二套类型系统:对象】
我们讨论上述的六种类型——我一般称为基本类型系统,本质上来说它是“过程式语言”和“函数式语言”这两种语言特性所需要的类型系统。如果整个系统中没有object,那么它其实也很不错,也能用于网页展示,也能处理复杂的计算。而object这一类型,以及由此而来的,整个的Object()类型系统,是一个独立的存在。按照Java的设计,我们绝对可以用OOP来实现全部的开发过程,只要最后输入输出设备能够识别String、Boolean和Number这三种类型就行了。但显然的,我们的输出设备——例如键盘/显示器不能识别,所以Java里面一切都可以OOP,但到了设备交互、网络转输的最核心部分,仍然要做对象到值的转换。同样的,如同前面所讨论过的内容,在函数调用等接口上面,也存在传值、传对象(引用)的问题。
这些看起来扯得远了,事实上却是JavaScript语言设计的一些基础,一些基本的考量。
JavaScript是一种混合范型的语言,在面向对象这一特性上,是从typeof(x) == 'object'这一分支上来的,其它复杂的设计我们就不讲了。只说类型系统而言,这一切都从这里开始考虑,便不会有什么混淆。而Null,因为typeof(null)=='object',所以它是对象;并且(null instaneof Object) == false,表明它不是Object的子类实例。这其实很合逻辑,因为在JavaScript的应用环境中,例如我们常常讲的COM/ActiveX中,就会存在一个变量“是对象”,但“不是Object()及其子类构建”的。
所以,再一次回到typeof上面来。如果一个typeof(x)为true,其实只表明它是对象,可以使用对象存取运算符(.和[]),以及可以使用for..in、with等等语句,但并不一定表明x.toString()就成功,或者x.constructor属性就存在。因为typeof只决定了它是对象,并不决定它是Object()的子类实例。
这里再提及argb的一个问题。就是class这个名词的使用。在JavaScript中用到了Construcor而没有Class,因为它是原型继承的而不是类继承的。但事实上class这个概念还是存在的,例如在instanceof这个运算中,就需要引入“实例构建自类”的概念。所以class仍然应当作为OOP系统中的专用名词,而不能拿来讨论“分类”。进一步的,我们也可以使用“子类”或“子类类型”这样的概念,这些在OOP系统中,都是有明确的含义的。
在对象系统中,string、number、boolean、object和function这些“基本类型系统”中的类型,都有对应的“对象类型”。因此我通常在用string时,表明它就是一个基本类型,是值类型;如果使用String,就强调它是对象类型中的字符串对象,是引用类型;如果使用String(),就强调这是一个构造器,是一个函数。一般来说,我在这样使用的时候是不太容易混淆的。
【其它】
1、两套类型系统是“创举”吗?
其实不是。最早一些JavaScript书籍在讲类型系统的时候,都是从typeof开始讲的。
2、为什么要有多套类型系统?
因为JavaScript是多范型的。
3、为什么JavaScript是这样的?
这其实很好。只是一些从应用角度讨论JavaScript的书,把这些问题讲乱了。
4、JavaScript中除了六种基本类型,还会出现其它“基本类型”吗?
会。一些具体的JavaScript的实现中,typeof可能会返回其它的值,例如JScript中就可能返回unknow。
5、ECMAScript正确吗?
它正确地描述了JavaScript的实现过程,以及在这个实现过程中的类型规范。但是对于类型在语言定义上的规范,却没有明显地提及。
6、什么是primitive value?又什么是primitive types?
你可以这样理解:原始值(primitive value)是指一个语言在它存在之前应该存在的、可以用于实现该语言的值;原始类型(primitive types)是不管这个语言自身对外表达的类型若何,但是在实现该语言是所必须的那些原始值的类型。
^^. 这个解释有点粗暴了。其实我的意思是想强调:千万不要用一个实现规范却讨论语言设计。
7、JavaScript的语源真的与C--没关系吗?
真的。没什么关系。我回头再发那篇考证文章出来。现在,我要去吃饭。已经13:28了,楼下的KFC都要打烊了……