ECMAScript 历史与展望
刚立下这个题目时,我也被我自已震惊了。是的,这是一个相当庞大并且需要丰富经验的人才能完成的题目,我也不知道那来的勇气。但因为我一直好奇JavaScript的一些奇怪的设计是怎么诞生的,想借此机会可以让自已边写边学习。所以斗胆立下该题目,想让大伙跟我一起走进这个既让大家欢喜,又让人怨恨的ECMAScript世界。
为什么要学习一门语言的历史
在我们写代码的时候,特别是前端,经常会遇到一些奇怪的语言问题,例如为什么 JavaScript 不区分整型和双精度,为什么 typeof null 是对象而不是 'null' 类型等等。这些问题,就涉及到语言的历史问题,如果你是一个阅历丰富的工程师,很快就能回答前面的问题:JavaScript的数字类型是参考Python的设计,所以没有整型(整型也是float)。typeof null 是对象是真的设计错误,只是现在已无法挽回。学习一门语言的历史,除了满足我们的好奇心,更重要的是理解语言的本质,而不是只其然,而不知所以然。
当一名工程师使用一门语言的时间长了,就会发现越来越多的问题,自然就会产生一些对该语言改进的想法,如果把这种改进的想法贡献给社区,那么你也会成为推动该语言发展的一分子。我们除了不断地从该门语言中获取利益之外,我们也要回馈该语言的社区,以达到共同进步的最终目的。
JavaScript与ECMAScript
在前端工作中,我们接触最多的就是JavaScript,但很多人不知道ECMAScript。JavaScript在设计完成之后,迅速受到推广使用,但由于不同的浏览器对JavaScript的实现不一样,程序员要么非常艰难地实现全部或部分兼容,要么放弃另外一个浏览器的兼容。为了解决这个问题,Netscape(Mozilla的前身)就向ECMA Internationl提交JavaScript的标准化议案,也就出现了ECMAScript。
ECMAScript是一种“建议性的标准”,没有强制性,所以浏览器可以实现所有要求,也可以部分实现(例如IE浏览器),也可以除标准外出现一些自已的扩展特性。所以他们之间并没不是谁包含谁的关系,只是标准与实现之间的关系。例如 除JavaScript 外,ActionScript、JScript等,都是 ECMAScript 的实现。
ECMA International 不仅只为 JavaScript 定标准,他还对JSON、XML、C#、C++等内容进行标准化工作。如上图,除ECMA-262 就是 JavaScript 的标准方案外,还有 ECMA-402 是多语语 API 的标准方案,ECMA-404 是 JSON 的标准方案,而 ECMA-414 是这些标准的父集,某些功能必须要在这个标准下讨论,因为会涉及多方面的内容,例如模块化。
ECMAScript 1.0
JavaScript的初稿完成得有点着急,只花了十天就完成了,所以Netscape向ECMA International 提交ECMAScript 1.0 方案时(1997年),里面遗留了很多设计上的缺陷,而这些缺陷有的可以随着版本的叠代改修正,有些因为使用范围太广而永远也不可能改正了,例如经典的 typeof null 问题,有些因为设计问题也永远无法修正,例如非严格模式下不能使用let修饰符。
ECMAScript 2.0
时隔一年,1998年,ECMA 发布了 ECMAScript 2.0,主要是为了满足 ISO/IEC 16262 标准的要求。
ECMAScript 3.0
到了1999年,ECMAScript 发布历史上最为重要的版本:ECMAScript 3.0,这个版本引进了很多重要的功能和改进:正则表达式,更好的字符串处理,新的状态控制符,try/catch 错误控制等等。特别是 try/catch的引进,让浏览网页变得十分的稳定。由于该版本覆盖范围太广,Internet Explorer 从 5.5 到 8 都对此进行实现,导致以后想对此版本的修改变得非常困难,也导致 ECMAScript 4.0 的失败。
ECMAScript 4.0
第四版并没有发布,最终在2003年被遗弃,其关键的问题是改动过大,争论太多,浏览器厂商不能接受,特别是当时市场霸主 Internet Explorer 的否决,导致 ES4 并没有在浏览器上实现过。但最后也不是毫无用处,其中有部分提议放到了ECMAScript 5.0 ,有些语言实现了该版本,例如:ActionScript。
ECMAScript 5.0
ECMAScript 5.0 ,就是我们熟悉的 ES5 ,代号为 Harmony(和谐),意指 ECMAScript 4.0 版本的一个反醒。由于IE6长达十几年的统治,ES5 也是近几年才逐渐被开发员熟悉,加上一些转译器的出现,例如Babel,才让新的版本更加容易被接受。ES5 带来很多非常重要的功能和改进,是个里程碑式的版本,特别是严格模式的引入,让 JavaScript 拥有了更多的想象力。
严格模式
我们知道,JavaScript 是一门非常松散的语言,设计之初并不是给资深的专业程序员使用,而是设计师和兼职程序员,所以理论上可以容下各种不规范的错误,不至于页面崩溃,例如使用未定义的变量、变量重复声明等。但这种设计随着网页的复杂性增加和多人协作开发,导致非常多的问题,于是就很有必要限制这些不规范的代码,所以就有了 'use strict' ,称为严格模式。这种模式下去掉了许多为人诟病
基础的元编程
元编程就是编写能够使程序拥有自已调理自已程序的能力,简单地理解就是底层编程。在ES5之前,JavaScript 提供什么,你就用什么,例如:1+1=2,那1+1就必须等于2。但有了元编程能力之后,你可以自定义 JavaScript 的一些行为。例如:
'use strict';
let person = {
set firstName(firstName){
this.first = firstName;
},
set lastName(lastName){
this.last = lastName;
},
get fullName(){
return this.first + this.last;
}
}
person.firstName = 'James';
person.lastName = 'Yin';
console.log(person.fullName);
ES5 只提供比较初级的元编程能力,但仅这些能力足够让人惊艳,特别对那些需要对底层进行开发的框架,例如:Vue框架的高效率离不开元编程的出现,最为典型的就是Getter/Setter的使用。还有一些像Object.seal、Object.freeze之类的函数,对于框架开发者来说都是非常重要的功能。
JSON的支持
JSON.parse 和 JSON.stringify 这两个对 JSON 转换的函数,大家可能想不到竟然到2009年的ES5才正式列为标准,在此之前服务器端与前端的数据传输敢直混乱至极,不过当时AJAX也不太流行,加上jQuery的帮助,好像也没多大问题。
ECMAScript 5.1
主要是满足第三版ISO/IEC 16262:2011的标准要求。
ECMAScript 6.0 (ECMAScript 2015)
历时六年之久,终于在 2015 年发布最为关键的 ES6 版本,此版本可以说彻底改变了多年来 JavaScript 给大家的印象,不仅使代码更加优美规范,功能更加强大,甚至更容易使用了。其中最关键的更新是:Promise/A+ 引入,区块作用域,模块语法,类等等。
区块作用域
在以前,为了不污染别的代码,我们会使用一个叫 IIFE (Immediately-Invoked Function Expression) 东西,像下面这样:
(function () { // open IIFE
// var tmp = ···;
// ···
}()); // close IIFE
这是由于ES6之前,变量都是以函数作为作用域划分,在 ES6 你就可以简单地使用区块作用域来划分了:
'use strict';
{ // open block
let tmp = ···;
···
} // close block
console.log(tmp); // ReferenceError
Promise/A+ 和 Generator
对于深受“回调金字塔”伤害的前端工程师来说,现在终于有了完美又优雅的解决办法,就是Promise 和 Generator,特别是 Promise 配合 ECMAScript 2017 的 async/await 食用,敢直爽得不要不要的。
// before
dosomthing(callback1(callback2(callback3())));
// after
dosomthing()
.then(callback1)
.then(callback2)
.then(callback3);
箭头函数
箭头函数与以前的函数最大的不同就是 this 的指向,以前的函数 this 就是指当前函数:
function UiComponent() {
var _this = this; // (A)
var button = document.getElementById('myButton');
button.addEventListener('click', function () {
console.log('CLICK');
_this.handleClick(); // (B)
});
}
UiComponent.prototype.handleClick = function () {
···
};
而箭头函数 this 指向的是父函数:
function UiComponent() {
var button = document.getElementById('myButton');
button.addEventListener('click', () => {
console.log('CLICK');
this.handleClick(); // (A)
});
}
class 语法
JavaScript 是一门基于原型的编程语言,与我们常见的面向对象编程语言有很大的区别,但 JavaScript 仍可以作为一门面向对象的语言来看待,只是写法上会有些不同,例如,我们定义一个对象时使用的是 function 表达式:
function Obj () {
this.firstName = 'James';
}
var obj = new Obj();
console.log(obj.firstName); // 'James'
这种写法非常不直观,遇上类似继承、静态变量语法的需求,这种写法就更为复杂,因此 ES6 推出了 class 语法,为的是减轻面向对象编程的压力。
class People {
constructor(){
this.type = 'Human';
}
static speak(){
console.log('I speak English.');
}
}
class Man extend People{
constructor(){
super();
this.sex = 'male';
}
}
var man = new Man();
console.log(man.type); // Human
console.log(man.sex); // male
Man.speak(); // I speak English
man.speak(); // Type error
ES6 模块语法
CommonJS 的模块语法虽然并不复杂,只是写起来比较啰嗦,例如多输出:
//------ lib.js ------
var sqrt = Math.sqrt;
function square(x) {
return x * x;
}
function diag(x, y) {
return sqrt(square(x) + square(y));
}
module.exports = {
sqrt: sqrt,
square: square,
diag: diag,
};
//------ main1.js ------
var square = require('lib').square;
var diag = require('lib').diag;
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5
ES6 的模块语法简化了这写法:
//------ lib.js ------
export const sqrt = Math.sqrt;
export function square(x) {
return x * x;
}
export function diag(x, y) {
return sqrt(square(x) + square(y));
}
//------ main1.js ------
import { square, diag } from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5
Proxy 对象
ES5 提供元编程的能力还相对较弱,所以 ES6 提出更为强大的 Proxy 对象,以弥补元编程能力不足。Proxy 对象用于自定义对象的基础行为,例如属性的查找、属性附值、函数的执行等等。Proxy 可以让你拥有完全控制对象底层行为的能力,可以创造功能更为强大的对象。下面是一段用来验证输入类型是否正确的代码:
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// The default behavior to store the value
obj[prop] = value;
// Indicate success
return true;
}
};
let person = new Proxy({}, validator);
person.age = 100;
console.log(person.age); // 100
person.age = 'young'; // Throws an exception
person.age = 300; // Throws an exception
ECMAScript 2016
ES6 足足用了六年时间才正式发布,这对于日新月异的互联网来说这跨度实在太长了,加上前端这几年高速发展,行业比较混乱,急需标准规范。于是从 ES6 开始,把每年六月定为 ECMAScript 版本发布固定时期,为了方便记忆,版本名称从以前的顺序号改为年份,所以 ECMAScript 2016 并不建议叫 ES7 哦。
由于折腾的时间比较少,产出也比较少:一个是 Array.prototype.includes,类似 Array.prototype.indexOf 的东西。另一个是求幕语法**
,就是 Math.pow ,用法很简单:6 ** 2 === 36
。
ECMAScript 2017
在刚过去的六月,ECMA International 发布了预料之中的 ECMAScript 2017,其中有一个我们非常熟悉的东西:async/await 语法,这语法在发布之前就已经流行开来了。我们知道,JavaScript 是一门单线程异步处理的语言,处理同步/异步逻辑非常频繁,因此该功能极大地便利了开发者。
另外一个重要的更新是:共享内存。对于需要大量计算的程序来说,例如游戏,往往需要多生成几个 JavaScript 线程来辅助计算,但 JavaScript 处理线程之间共享数据是件很麻烦的事,而且效率也很低。所以就有:SharedArrayBuffer 和 Atomics 这两个对象来帮助开发者处理这桩麻烦事。但目前只能共享整数,而且逻辑较为复杂,所以要谨审使用。
其他诸如字符串补全、数据对象的新函数,新的语法规则等等,都极大方便了前端开发,这里就不一一展开了。
迎接 ECMAScript 的未来
ECMAScript 这几年逐渐稳定下来,预期未来不会有翻天覆地的变化,对于 JavaScript 本身的缺陷也会得到更多的“补丁”,但为了兼容性,不会出现大幅度的改变。
如何学习ECMAScript
在学习上,如果你是精力过剩的学霸,当然把所有 stage-0 阶段(初稿,随时会改版本)的新功能全部都学下来,如果你像我一样,只是一名又懒又有拖延症的学渣,我的建议是等功能提到 stage-4 阶段再学,因为这个阶段的功能是不会再改了,加上每年提出来的改进并不会太多,学习压力并不会太大。
如何无缝地向上升级
如果要使用新的函数和对象,我们可以使用专用的 polyfill 来解决,例如我们想用 ECMAScript 2016 的 Arrary.prototype.includes 函数,只要在你的代码之前执行下面这段代码就可以:
// https://tc39.github.io/ecma262/#sec-array.prototype.includes
if (!Array.prototype.includes) {
Object.defineProperty(Array.prototype, 'includes', {
value: function(searchElement, fromIndex) {
// 1. Let O be ? ToObject(this value).
if (this == null) {
throw new TypeError('"this" is null or not defined');
}
var o = Object(this);
// 2. Let len be ? ToLength(? Get(O, "length")).
var len = o.length >>> 0;
// 3. If len is 0, return false.
if (len === 0) {
return false;
}
// 4. Let n be ? ToInteger(fromIndex).
// (If fromIndex is undefined, this step produces the value 0.)
var n = fromIndex | 0;
// 5. If n ≥ 0, then
// a. Let k be n.
// 6. Else n < 0,
// a. Let k be len + n.
// b. If k < 0, let k be 0.
var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
function sameValueZero(x, y) {
return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y));
}
// 7. Repeat, while k < len
while (k < len) {
// a. Let elementK be the result of ? Get(O, ! ToString(k)).
// b. If SameValueZero(searchElement, elementK) is true, return true.
// c. Increase k by 1.
if (sameValueZero(o[k], searchElement)) {
return true;
}
k++;
}
// 8. Return false
return false;
}
});
}
由于这种打补丁的方式会增加 js 文件的体积,而且会稍微有些性能上的差异,如果不是使用太频繁,我建议是用兼容性更好的旧版本即可,例如:indexOf。
但有些语法上的差异并不能通过打补丁来实现兼容,例如箭头函数,这时候就需要转译器的帮助,我们比较熟悉的 Babel 就是其中之一:
[1,2,3].map(n => n + 1);
转译之后:
"use strict";
[1, 2, 3].map(function (n) {
return n + 1;
});
使用新语法可以大大提高代码的可读性,无论是多人团队开发还是个人开发,都是非常有益的,只要性能满足产品的要求,我的建议是新语法能用就用。另外,由于 babel 可以用旧语法来表达新语法,所以有兴趣学习一些底层原理的同学,可以到 Babel REPL 在线把新语法转译成旧语法,可以有一个更深层次的理解。
Vanilla JS
在以前,由于浏览器实现 JavaScript 的标准相当混乱,因此产生了像 jQuery 这种框架,除了简化函数,更重要的是实现对各浏览器的兼容。在后来,Angular、React、Vue这些框架的出现,人们好像忘记了写代码是可以不用框架的。有个喜欢抓弄人的工程师为了嘲笑那部分一直坚持使用框架的工程师(还有那些喜欢框架的老板们),弄了一个叫 Vanilla JS的网架。向那部分坚持使用框架的工程师推荐一个叫 VanillaJS 的“框架”,实际上 VanillaJS 就是无框架实现的 JS 代码。
无框架代码无论体积、性能、可维护性、可读性等等各方面都拥有无与论比的优越性,我个人极力推荐无框架开发。