追本溯源,ES6 和 ES5 的对比实现

4,612 阅读4分钟

词语连线

让我们先来玩一个游戏,根据 ES5,ES2015, ES2016,ES6,ES2017 这几个词的关系,进行连线。

如果你有点“蒙圈”,不妨先来看看 Javascript 的发展历史。

  • 2011 年,标准化组织 ECMA 发布 ECMAScript 5.1 版
  • 发布后,开始着手制定 ECMAScript 6.0 版本,即 ES6
  • 为了让标准的升级成为常规流程,决定在每年的 6 月份正式发布一次,作为当年的正式版本:
    • 2015 年 6 月发布 《ECMAScript 2015 标准》(简称 ES2015),它是 ES6 的第一个版本:
    • 2016 年 6 月发布 《ECMAScript 2016 标准》(简称 ES2016),进行了小幅修订
    • 2017 年 6 月发布 ES2017 标准

那么上面游戏的答案是:

关系

ES6 与 ES5

作为一个在 JS 高程《JavaScript 高级程序设计》 指引下走进前端的“老”同学,我常常被 ES5 和 ES6 的写法“困扰”,担心写出来的代码不够“新”。时间与经验的沉淀促进语言的“进化”,那么观察一个特性/语法的变迁,或许是一件有意思的事情。

今天我们就来看看,ES6 中一些语法如果用 ES5 该如何实现。

let

ES5 只有全局作用域函数作用域,ES6 通过 let 语法增加了块级作用域,增强了一些场景下的合理性。

ES6

for (let i = 0; i < 3; i++) {
    // 
}
console.log(i); // ReferenceError: i is not defined

ES5

(function() {
    for (var i = 0; i < 3; i++) {
    // 
    }
})();
console.log(i); // ReferenceError: i is not defined

const

ES5 中是没有常量的,通常用大写的命名规范来“约定”常量,却不能保证不被更改。ES6 中新增了 const。

ES6

const a = '123';

try {
  a = '456';
} catch(err) {
  // err: TypeError: Assignment to constant variable
  console.log(a); // '123'
}

ES5

Object.defineProperties(window, {
  a: {
    value: '123',
    writable: false
  }
});

a = '456';
console.log(a); // '123'

Class

与其他新增的语法特性不同,ES6 中的 Class 实质上是 JavaScript 现有的基于原型的继承的语法糖。

声明一个动物类

ES6

class Animal {
  constructor(type, name) {
    this.type = type;
    this.name = name;
  }
  static eat() {
  }
  speak() {
    console.log(this.name + ' say:');
  }
}

ES5

function Animal(type, name) {
    this.type = type;
    this.name = name;
}
Animal.eat = function () {
}
Animal.prototype.speak = function () {
    console.log(this.name + ' say');
}

实例化一个小明同学

var a = new Animal('people', 'xiaoming');
a.speak();

声明猫猫类(继承动物类)

ES6

class Cat extends Animal {
    constructor(name) {
        super('cat', name);
    }
   
    speak() {
        super.speak();
        console.log('meow');
    }
}

ES5

function Cat(name) {
    Animal.call(this, 'cat', name);
}
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

// 静态方法
Cat.eat = Animal.eat;

Cat.prototype.speak = function () {
  Animal.prototype.speak.call(this);
  console.log('meow');
};

实例化一只名为“卷卷”的喵,然后喵一声

var juanjuan = new Cat('juanjuan');
juanjuan.speak(); // juanjuan say: meow

对比一下,ES6 更优美吧~

Module

ES6 import/export

// lib/math.js
export function sum(x, y) {
  return x + y;
}
export var pi = 3.141593;

// 引用
import { pi, sum } from 'lib/math';
console.log('2π = ' + math.sum(math.pi, math.pi));

ES5 require/exports

// lib/math.js
exports.sum = sum;
function sum(x, y) {
  return x + y;
}
var pi = exports.pi = 3.141593;

// 引用
var math = require('lib/math');
console.log('2π = ' + math.sum(math.pi, math.pi));

但是,上面这两种写法并不“等价”。这要从 javascript 的模块体系说起。

javascript 一直没有模块体系,于是社区制定了一些模块加载方案,比较通用的是 CommonJS 和 AMD 两种, 前者用于服务器,后者用于浏览器。(为了兼容还引入了UMD)。

CommonJS 和 AMD 模块都是在运行时确定模块关系。即整体加载模块(模块下的所有方法),生成一个对象,然后在从这个对象上读取方法。这种加载称为“运行时加载”

// CommonJS模块
let { stat, exists, readFile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

然而 ES6 模块不是对象。

// ES6模块
import { stat, exists, readFile } from 'fs';

上面代码的实质是从 fs 模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。

关于模块这部分,其实值得展开研究。(我还没整太明白)

结尾

语法的背后,是语言本身的“特性”,他们的出现是为了解决一些场景的问题。追本溯源是一个很好的学习方式。我们,下期再见~