令人崩溃的 JavaScript
在JavaScript风头还不盛行的时候,我的一位博学的大学老师曾经说过:“相较于Java,C#这些高级(编程)语言,JavaScript可以称得上是超级语言,最终凡是能够使用JavaScript实现的事物,都会被JavaScript所实现”。不过我想展示的是以下JavaScript代码和其运行结果。
console.log(typeof NaN) // 输出 'number'
console.log(typeof null) // 输出 'object'
console.log(99999999999999999999) // 输出 100000000000000000000
console.log(0.5 + 0.1 == 0.6) // 输出 true
console.log(0.1 + 0.2 == 0.3) // 输出 false
console.log(Math.max()) // 输出 -Infinity
console.log(Math.min()) // 输出 Infinity
console.log([]+[]) // 输出空字符串 ''
console.log([]+{}) // 输出 '[object Object]'
{}+[] // 运行结果为 0
console.log(true + true + true) // 输出 3
console.log(true - true) // 输出 0
console.log(true == 1) // 输出 true
console.log(true === 1) // 输出 false
console.log((!+[]+[]+![]).length) // 输出 9
console.log(9 + '1') // 输出 '91'
console.log(91 - '1') // 输出 90
console.log([] == 0) // 输出 true
console.log(010 - 03) // 输出 5
console.log([,,,].length) // 输出 3
console.log("㶷" === "𤈎") // 输出 false
console.log("㶷".length) // 输出 1, 这个"㶷"和下面的"𤈎"在编码上不是一个字
console.log("𤈎".length) // 输出 2
console.log("👩❤️💋👩".length) // 输出 11
console.log("👩❤️💋👩".split()) // 输出 ['👩❤️💋👩']
console.log("👩❤️💋👩".slice(0,2)) // 输出 '👩'
console.log([..."👩❤️💋👩"]) // 输出 ['👩', '', '❤', '️', '', '💋', '', '👩']
上述代码主要来源于一张“Thanks for inventing JavaScript” 的图片,图片展示了JavaScript的创造者布兰登·艾克(Brendan Eich)对JavaScript初学者意味深长的微笑。上述代码中展示的丑陋的弱类型造成的类型转换和字符编码怪异行为只是JavaScript令人崩溃的一角。布兰登·艾克仅用十天完成了,也是这十天的工作,造就了JavaScript后来难以改观以及不断在改观的一系列怪异行为。
JavaScript 诞生
最初,在最早流行的图形接口网页浏览器上,只能加载静态形式的HTML网页,不支持网页的任何动态行为。网景公司的创始人马克·安德森认为HTML需要一种胶水语言,让网页设计师和兼职程序员可以很容易地使用它来组装图片和插件之类的组件,且代码可以直接编写在网页标记中。公司最初的计划是把Java语言或者Scheme语言嵌入到Netscape Navigator浏览器中,不过经过一系列的讨论,公司采纳了由布兰登·艾克设计的新的语言,这就是最初的JavaScript。艾克在1995年5月仅花了十天时间就把原型设计出来了。最初命名为Mocha,1995年9月在Netscape Navigator 2.0的Beta版中改名为LiveScript,同年12月在Netscape Navigator 2.0 Beta 3中部署时又被重命名为JavaScript。之所以采用JavaScript这个名字,是因为当时Java语言热度正当时,网景公司为了蹭热度将其命名为JavaScript。但从技术角度来看,JavaScript和Java是两套不相干的高级编程语言。
伴随着JavaScript的支持,Netscape Navigator浏览器在市场占有上大获全胜,随后,微软对Netscape Navigator解释器进行了逆向工程,创建了JScript,推出Internet Explorer浏览器。这时浏览器端开发语言并没有语言标准化微软在JScript中加入了不少的专属对象。1996年11月,网景正式向ECMA(欧洲计算机制造商协会)提交语言标准。1997年6月,ECMA以JavaScript语言为基础制定了ECMAScript标准规范ECMA-262。JavaScript成为了ECMAScript最著名的实现之一。那时,除了JavaScript,ECMAScript规范还有其余的语言实现,比较知名的就是ActionScript和微软的JScript。
之后,随着行业标准协会和各大浏览器厂商的不断努力下,语言标准一代代丰富,浏览器特性一次次加强,JavaScript逐渐走向成熟,慢慢成为如今的模样。目前,我们可以暂时为JavaScript下一个如此的定义:
JavaScript是一门基于原型,且函数作为头等公民的多范式高级“解释型”编程语言,它支持面向对象程序设计、指令式编程和函数式编程。它提供方法来操控文本、数组、日期以及正则表达式等。不支持I/O,比如网络、存储和图形等,但这些都可以由它的宿主环境提供支持。它由Ecma通过ECMAScript实现语言的标准化,被世界主流浏览器支持。
这个定义来源于维基百科。我们在这里把“解释型”三个字加上引号,因为自从谷歌在Chrome浏览器上推出V8 JavaScript引擎以来,引入了及时编译(英语:Just-in-time compilation,缩写为JIT)机制,大大提高了JavaScript引擎的运行速度。对于JavaScript的JIT机制,有机会我们再专门讨论。ECMAScript标准怎么一代代发展的呢?我们也再单开一篇。
一般来说,完整的JavaScript包括以下几个部分:
- ECMAScript,描述了该语言的语法和基本对象;
- 文档对象模型(英语:Document Object Model,缩写DOM),描述处理网页内容的方法和接口;
- 浏览器对象模型(英语:Browser Object Model,缩写BOM),描述与浏览器进行交互的方法和接口。
正是JavaScript这样的诞生和发展的方式,以及JavaScript面对的问题场景,决定了JavaScript开发语言的设计特点,包括它的缺陷。
JavaScript 特点
根据JavaScript面对的问题领域(Web前端脚本)、设计的历史背景以及发展脉络,造就了JavaScript具备以下一些特点。
- 是一种“解释性”脚本语言(代码不进行预编译)。在V8 JavaScript 引擎出现后这一状况已改变;
- 万物皆对象。JavaScript 变量中除了七种原始类型数据,其余都可视为对象,包括数组和函数。而其中原始类型亦可封箱为对象类型。所有对象类型拥有同一个原型'Object'。
- 函数是头等公民
- 弱类型语言及动态化
- 主要用来向HTML页面添加交互行为;
- 可以直接嵌入HTML页面,但写成单独的js文件有利于结构和行为的分离。
- 事件驱动
- 跨平台。JavaScript运行在浏览器环境下或者在服务端V8 JavaScript引擎环境下,具备平台无关性。
- 自动化的内存分配和回收
- 服务端的异步IO操作
这些特点大部分是设计者有意为之,它们造就了JavaScript的强大和社区的活跃,同时也带来了很多的语言设计问题。
JavaScript 的缺陷
在最初JavaScript产生的时候,网景公司决策层希望这个网页脚本语言是面向对象的,需要“看上去与Java足够相似”。而它的作者布兰登·艾克的主要方向和兴趣是函数式编程,他一直思考的是Scheme语言作为网页脚本语言的可能性。艾克仅用10天就完成了JavaScript的初版设计并向公司交差。他的设计思路大体是这样的:
(1)借鉴C语言的基本语法;
(2)借鉴Java语言的数据类型和内存管理;
(3)借鉴Scheme语言,将函数提升到"第一等公民"(first class)的地位;
(4)借鉴Self语言,引入原型(prototype)机制来扩展对象。
最终,JavaScript语言成了函数式编程和面向对象编程两种风格的混合简化物。这造成了一系列的问题,除了文前提到的弱类型造成的类型转换问题和字符编码问题,JavaScript还存在其它一些大大小小的设计问题。下面我们简述一些。
❗弱类型造成的变量类型隐式转换问题
- 二元运算符
+
会把两个操作数转换为字符串,除非两个操作数都为数字类型; - 二元操作符
-
会把两个操作数转换为数字类型; - 一元操作符,包括
+
和-
,都会把操作数转换为数字。 ==
运算导致的隐式转换问题
❗var
的作用域问题
var
声明的变量会使变量的作用域提升,这给开发人员带来困扰。
❗原型机制中__proto__
性能陷阱
JavaScript使用原型机制管理引用类型,引用类型(对象)的扩展也主要是基于原型机制。原型模式有其好的一面,减少了类型的种类,提高了开发人员对不同对象的管理效率,但原型模式也有其有限制的一面。
❗this
的动态作用域问题
在JavaScript中,this指针不是函数声明时定义的,而是函数运行时绑定的,它的值取决于函数如何被调用,并且不能够被更改。JavaScript运行在严格模式或者非严格模式时,this的值也略有不同。
❗数组与object
纠缠不清的关系
数组也属于对象,JavaScript中数组相比较其他语言的数组,可以说它不是真正的数组,只是一个类似于列表的高阶对象。这造成了一些问题,例如for in
操作遍历的是数组的下标,而非数组的值,这一点很反直觉。另外数组对象的length
属性是可修改的,并且length
的修改回影响数组元素的值。
❗虚假的OO编程
ES6之前JavaScript是没有class
的定义的,ES6中的class
的本质也是JavaScript中的Function
。JavaScript能够完成对象的封装,能够基于原型机制支持面向对象编程范式,原型机制和类机制在工作上的确有很大不同,说JavaScript面向对象编程是虚假的是编程是一种带着“类信仰”的偏执。在JavaScript中,Function
也是对象,凡是对象,都可以基于原型机制进行扩展,开发人员也是基于此来实现构造Function
的继承机制。
❗单线程运行模式造成的回调地狱
JavaScript一般来说是单线程的。为了解决并发和异步问题,开发人员不得不使用回调函数,但是当回调函数食用过多的时候,会极大影响代码可读性和逻辑,一不小心也会触发函数中this
指向问题,这种情况被称为回调地狱。直到Promise对象、Generator 函数、async 函数的出现,回调地狱的问题才得到改观。
❗字符编码问题
在JavaScript语言出现的时候,还没有UTF-16编码,因此只能采用UCS-2这种编码方案。造成所有字符在这门语言中都是2个字节,如果是4个字节的字符,会当作两个双字节的字符处理。后续ECMAScript不得不想方设法去修复这一问题。
❗不支持命名空间,难以模块化
JavaScript设计之初主要用作前端脚本语言,运行时不仅包含很多全局变量,对变量作用域控制问题也不那么严肃,并没有考虑模块问题。最初,开发人员通过立即执行函数(英语:Immediately invoked function expression,缩写IIFE)和闭包机制解决JavaScript模块化问题,直到CommonJS、AMD、CMD、ES Modules 的出现。
❗同时拥有null
和undefined
null
属于对象(object)的一种,意思是该对象为空;undefined则是一种数据类型,表示未定义,两者非常容易混淆。typeof null === "object"
,这是一个相当反直觉的特性。null
可能是被错误地标记为对象,但是修复它会导致很多现有JavaScript程序崩溃,所以一直保留至今。
❗怪异的NaN
NaN
(Not a number)是一种数字,表示超出了解释器的极限。虽然JavaScript对于NaN
的实现完全符合IEEE 754标准的实现,但是请看下面代码及其表现。
console.log(NaN === NaN); // 输出 false
console.log(NaN !== NaN); // 输出 true
console.log(1 + NaN); // 输出 NaN
console.log(typeof NaN); //输出 "number"
做NaN
判断的时候请使用Number.isNaN()
。
❗自动插入分号
对于压缩的JavaScript代码,其有一个机制就是会尝试自动插入分号来修正有缺损的程序。这个机制容易产生错误。 请看下面代码
function foo() {
return
{
value: 1
};
}
console.log(typeof foo()); // 输出 "undefined"
你可能会认为它的输出结果是"object",但是结果却是"undefined"。原因是解释器自动在return语句后面加上了分号。上面代码其实等价于如下代码。
function foo() {
return;
{
value: 1
};
}
console.log(typeof foo()); // 输出 "undefined"
最后
这些JavaScript缺陷伴随着JavaScript特点的造就而造就,随着JavaScript的历史发展而变化,笔者在这里列举它们并非要证明JavaScript是一门糟糕的编程语言。在这里列举它们,是为了更好地理解JavaScript。JavaScript很强大,但只有开发人员更好的理解它,才能发挥出它的威力,编写更多给世界带来幸福感的代码。后续,笔者会围绕JavaScript这些特点和缺陷,展开对JavaScript的剖析和探讨,在这个过程中,期望笔者和读者一道,在前端编程上,更进一步。