十四、当代 ECMAScript——2015/2016 解决方案

我数不清我在本书中提到过多少次即将到来的 JavaScript 版本,请放心,这是一个很大的数字。 语言跟不上应用开发人员的需求,这有点令人沮丧。 在 JavaScript 的新版本中,我们已经讨论过的许多方法都不再需要了。 不过,现在有一些方法可以让下一个版本的 JavaScript 工作。

在本章中,我们将具体研究其中几个:

  • 打印稿
  • BabelJS

TypeScript

这里不缺少编译成 JavaScript 的语言。 CoffeeScript 可能是这些语言中最著名的一个例子,尽管将 Java 编译成 JavaScript 的谷歌 web 工具包也曾经非常流行。 微软在 2012 年发布了一种名为 TypeScript 的语言。 它被设计成 JavaScript 的超集,就像 c++是 C 的超集一样。这意味着所有语法上有效的 JavaScript 代码也是语法上有效的 TypeScript 代码。

微软自己也在一些较大的 web 属性中大量使用 TypeScript。 Office 365 和 Visual Studio Online 都有大量用 TypeScript 编写的代码库。 这些项目实际上比 TypeScript 早很多。 据报道,从 JavaScript 到 TypeScript 的转换非常容易,因为它是 JavaScript 的超集。

TypeScript 的设计目标之一就是让它尽可能地与 ECMAScript-2015 和未来的版本兼容。 这意味着 TypeScript 支持 ECMAScript-2016 的一些特性,尽管肯定不是全部,以及 ECMAScript-2015 的一部分。 ECMAScript-2016 中有两个重要特性是 Typescript 部分支持的:装饰器和 async/await。

装饰师

在前面的章节中,我们探讨了面向方面编程(AOP)。 在 AOP 中,我们用拦截器包装函数。 decorator 提供了一种简单的方法来做到这一点。 假设我们有一个在维斯特洛分派消息的类。 显然,那里没有电话或互联网,所以消息是通过乌鸦发送的。 如果我们能监视这些信息就太好了。 我们的CrowMessenger班看起来如下:

class CrowMessenger {
  @spy
  public SendMessage(message: string) {
    console.log(`Send message is ${message}`);
  }
}
var c = new CrowMessenger();
var r = c.SendMessage("Attack at dawn");

你可以在SendMessage方法上标注@spy。 这只是另一个拦截和包装该函数的函数。 在间谍内部,我们可以访问函数描述符。 正如你在下面的代码中看到的,我们使用描述符并操作它来捕获发送到CrowMessenger类的参数:

function spy(target: any, key: string, descriptor?: any) {
  if(descriptor === undefined) {
    descriptor = Object.getOwnPropertyDescriptor(target, key);
  }
  var originalMethod = descriptor.value;

  descriptor.value =  function (...args: any[]) {
    var arguments = args.map(a => JSON.stringify(a)).join();
    var result = originalMethod.apply(this, args);
    console.log(`Message sent was: ${arguments}`);
    return result;
  }
  return descriptor;
}

间谍显然对测试功能非常有用。 我们不仅可以监视这里的值,还可以替换函数的输入和输出。 考虑以下:

descriptor.value =  function (...args: any[]) {
  var arguments = args.map(a => JSON.stringify(a)).join();

var result = "Retreat at once";

  console.log(`Message sent was: ${arguments}`);
  return result;
}

修饰器可以用于 AOP 之外的目的。 例如,您可以将对象的属性标注为可序列化的,并使用这些标注来控制定制的 JSON 序列化。 我怀疑,随着装饰器得到支持,它将变得更加有用和强大。 Angular 2.0 已经在大量使用装饰器了。

Async/Await

在第 7 章、反应式编程中,我们谈到了 JavaScript 编程的回调特性如何使代码变得非常混乱。 没有什么比尝试将一系列异步事件链接在一起更明显的了。 我们很快就陷入了一个代码陷阱,它看起来像下面这样:

$.post("someurl", function(){
  $.post("someotherurl", function(){
    $.get("yetanotherurl", function(){
      navigator.geolocation.getCurrentPosition(function(location){
        ...
      })
    })
  })
})

这段代码不仅难以阅读,而且几乎不可能理解。 async/await 语法是从 c#借来的,允许以一种更简洁的方式编写代码。 在幕后,生成器被用来(或者被滥用,如果你喜欢的话)创建真正的 async/await 的印象。 让我们来看一个例子。 在前面的代码中,我们使用了 geolocation API 来返回客户端的位置。 它是异步的,因为它对用户的机器执行一些 IO 以获得真实世界的位置。 我们的规范要求我们获取用户的位置,将其发送回服务器,然后获取图像:

navigator.geolocation.getCurrentPosition(function(location){
  $.post("/post/url", function(result){
    $.get("/get/url", function(){
   });
  });
});

如果我们现在引入 async/await,这可以变成:

async function getPosition(){
  return await navigator.geolocation.getCurrentPosition();
}
async function postUrl(geoLocationResult){
  return await $.post("/post/url");
}
async function getUrl(postResult){
  return await $.get("/get/url");
}
async function performAction(){
  var position = await getPosition();
  var postResult = await postUrl(position);
  var getResult = await getUrl(postResult);
}

这段代码假设所有的async响应都返回一个承诺,这个承诺是一个包含状态和结果的构造。 目前,大多数async操作不返回承诺,但有库和实用程序将回调转换为承诺。 如您所见,语法比混乱的回调更清晰,更容易理解。

打字

除了我们在上一节提到的 ECMAScript-2016 特性外,TypeScript 还包含了一个相当有趣的类型系统。 JavaScript 最好的部分之一是它是一种动态类型语言。 我们已经多次看到,不被类型所累是如何节省时间和代码的。 TypeScript 中的类型系统允许你使用你认为必要的或多或少的类型。 你可以给变量一个类型,用下面的语法声明它们:

var a_number: number;
var a_string: string;
var an_html_element: HTMLElement;

一旦一个变量被赋了一个类型,TypeScript 编译器不仅会使用它来检查这个变量的使用情况,还会推断出从这个类派生出的其他类型。 例如,考虑以下代码:

var numbers: Array<number> = [];
numbers.push(7);
numbers.push(9);
var unknown = numbers.pop();

在这里,TypeScript 编译器会知道unknown是一个数字。 如果你试图用它作为其他的东西,比如下面的字符串:

console.log(unknown.substr(0,1));

然后编译器将抛出一个错误。 然而,您不需要为任何变量指定类型。 这意味着您可以调优类型检查运行的程度。 虽然听起来很奇怪,但它实际上是引入类型检查的严格性而又不失 JavaScript 灵活性的出色解决方案。 输入仅在编译期间强制执行,一旦代码被编译为 JavaScript,任何与字段相关的输入信息的提示都将消失。 因此,发出的 JavaScript 实际上非常干净。

如果你对类型系统感兴趣,并且知道像逆变这样的单词,并且可以讨论逐步类型的不同级别,那么 TypeScript 的类型系统可能非常值得你花时间去研究一下。

本书中的所有示例最初都是用 TypeScript 编写的,然后被编译成 JavaScript。 这样做是为了提高代码的准确性,并且通常是为了避免我经常把代码弄得一团糟。 我很有偏见,但我认为 TypeScript 确实做得很好,肯定比编写纯 JavaScript 更好。

在 JavaScript 的未来版本中不支持输入。 因此,即使在 JavaScript 的未来版本中会有所有的变化,我仍然相信 TypeScript 在提供编译时类型检查方面有它的一席之地。 在编写 TypeScript 时,类型检查器让我避免犯愚蠢的错误的次数之多,让我一直感到惊讶。

BabelJS

TypeScript 的另一种选择是使用 BabelJS 编译器。 这是一个开源项目 ECMAScript-2015 和超越等效 ECMAScript 5 JavaScript。 ECMAScript-2015 的许多变化都是语法上的细微变化,所以它们实际上可以在 ECMAScript 5 JavaScript 中表示,尽管没有那么简洁或令人愉快。 我们已经在 es5 中使用了类结构。 BabelJS 是用 JavaScript 编写的,这意味着从 ECMAScript-2015 到 ES 5 的编译可以直接在网页上完成。 当然,BabelJS 的源代码使用了 ES 6 结构,这似乎是编译器的趋势,所以必须使用 BabelJS 来编译 BabelJS。

在撰写本文时,BabelJS 支持的 ES6 函数列表非常广泛:

  • 箭头功能
  • 计算属性名
  • 默认参数
  • 解构的任务
  • 迭代器和 for of
  • 发电机的理解
  • 发电机
  • 模块
  • 数字字面值
  • 属性赋值方法
  • 对象初始化器速记
  • 其他参数
  • 传播
  • 模板文字
  • 承诺

BabelJS 是一个多用途的 JavaScript 编译器,所以编译 ES-2015 代码只是它能做的许多事情之一。 有许多插件提供了大量有趣的功能。 例如,“内联环境变量”插件插入编译时变量,允许基于环境进行条件编译。

关于这些特性是如何工作的,已经有相当多的文档可用,所以我们不会一一介绍。

如果你已经安装了 node 和 npm,设置 Babel JS 是一个相当简单的练习:

npm install g babel-cli

这将创建一个 BabelJS 二进制文件,可以像这样编译:

babel  input.js --o output.js

对于大多数用例,您希望使用构建工具(如 Gulp 或 Grunt)进行研究,它们可以一次编译许多文件,并执行任意数量的编译后步骤。

Classes

现在你应该已经厌倦了阅读关于在 JavaScript 中创建类的不同方法。 不幸的是,我是这本书的作者,所以让我们看最后一个例子。 我们将使用前面的城堡示例。

BabelJS 不支持文件中的模块。 相反,文件被视为模块,这允许以一种类似于require.js的方式动态加载模块。 因此,我们将把模块定义从我们的城堡中删除,只保留类。 另一个在 TypeScript 中而不是 es6 中存在的特性是在参数前加上public,使其成为类的公共属性。 相反,我们使用export指令。

一旦我们做了这些更改,源 ES6 文件看起来如下所示:

export class BaseStructure {
  constructor() {
    console.log("Structure built");
  }
}

export class Castle extends BaseStructure {
  constructor(name){
    this.name = name;
    super();
  }
  Build(){
    console.log("Castle built: " + this.name);
  }
}

生成的 es5 JavaScript 如下所示:

"use strict";

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

Object.defineProperty(exports, "__esModule", {
  value: true
});

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeofcall === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var BaseStructure = exports.BaseStructure = function BaseStructure() {
  _classCallCheck(this, BaseStructure);
  console.log("Structure built");
};

var Castle = exports.Castle = function (_BaseStructure) {
  _inherits(Castle, _BaseStructure);
  function Castle(name) {
    _classCallCheck(this, Castle);
    var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Castle).call(this));
    _this.name = name;
    return _this;
  }
  _createClass(Castle, [{
    key: "Build",
    value: function Build() {
      console.log("Castle built: " + this.name);
    }
  }]);
  return Castle;
}(BaseStructure);

很明显,BabelJS 生成的代码不像 TypeScript 生成的代码那么干净。 您可能还注意到,使用了一些辅助函数来处理继承场景。 还有一些提到了"use strict";。 这是一个给 JavaScript 引擎的指令,它应该在严格模式下运行。

严格模式可以防止许多危险的 JavaScript 实践。 例如,在一些 JavaScript 解释器中,不先声明变量就使用变量是合法的:

x = 22;

如果之前没有声明x,会抛出一个错误:

var x = 22;

不允许复制对象中的属性,也不允许重复声明参数。 还有许多其他的实践,"use strict";将作为错误处理。 我喜欢将"use strict";视为类似于将所有警告视为错误。 它可能不像 GCC 中的–Werror那样完整,但在新的 JavaScript 代码基上使用严格模式仍然是一个好主意。 BabelJS 只是为您强制执行。

默认参数

es6 中不是一个巨大的特性,但真正的特点是引入了默认参数。 在 JavaScript 中,调用函数总是可以不指定所有参数。 简单地从左到右填充参数,直到没有更多的值,并且所有剩余的参数都是未定义的。

默认参数允许为未填写的参数设置一个非 undefined 的值:

function CreateFeast(meat, drink = "wine"){
  console.log("The meat is: " + meat);
  console.log("The drink is: " + drink);
}
CreateFeast("Boar", "Beer");
CreateFeast("Venison");

这将输出以下内容:

The meat is: Boar
The drink is: Beer
The meat is: Venison
The drink is: wine

生成的 JavaScript 代码实际上非常简单:

"use strict";
function CreateFeast(meat) {
  var drink = arguments.length <= 1 || arguments[1] === undefined ? "wine" : arguments[1];
  console.log("The meat is: " + meat);
  console.log("The drink is: " + drink);
}
CreateFeast("Boar", "Beer");
CreateFeast("Venison");

模板文字

从表面上看,模板字面量似乎是 JavaScript 中缺少字符串插值的解决方案。 在某些语言中,例如 Ruby 和 Python,您可以直接将周围代码中的替换注入到字符串中,而不必将它们传递到某种字符串格式化函数中。 例如,在 Ruby 中,你可以这样做:

name= "Stannis";
print "The one true king is ${name}"

这将把${name}参数绑定到周围作用域的名称上。

ES6 支持 JavaScript 中类似的模板字面量:

var name = "Stannis";
console.log(`The one true king is ${name}`);

可能很难看到,但该字符串实际上是由反引号而不是引号包围的。 要绑定到作用域的令牌用${}表示。 在大括号内,你可以放一些复杂的表达式,比如:

var army1Size = 5000;
var army2Size = 3578;
console.log(`The surviving army will be ${army1Size > army2Size ? "Army 1": "Army 2"}`);

BabelJS 编译的版本的这段代码简单地将字符串插入替换为附加字符串:

var army1Size = 5000;
var army2Size = 3578;
console.log(("The surviving army will be " + (army1Size > army2Size ? "Army 1" : "Army 2")));

模板文字还解决了许多其他问题。 在模板字面量中换行是合法的,这意味着您可以使用模板字面量来创建多行字符串。

考虑到多行字符串的思想,模板字面量似乎对构建特定领域的语言很有用:这个主题我们已经见过很多次了。 DSL 可以嵌入到模板文字中,然后从外部插入值。 例如,使用它保存 HTML 字符串(当然是 DSL)并从模型中插入值。 这些工具可能会取代今天使用的一些模板工具。

Block bindings with let

JavaScript 中的变量的作用域很奇怪。 如果你在一个块内定义一个变量,比如在一个if语句内,那么这个变量在块外仍然可用。 例如,请参见以下代码:

if(true)
{
  var outside = 9;
}
console.log(outside);

这段代码将打印9,即使这个变量明显超出了作用域。 如果你假设 JavaScript 像其他 c 语法语言一样支持块级作用域,那么至少它超出了作用域范围。 JavaScript 中的作用域实际上是函数级的。 在代码块中声明的变量(如附在iffor循环语句上的变量)被提升到函数的开头。 这意味着它们在整个函数的范围内。

es6 引入了一个新的关键字let,它将变量作用域限定在块级别。 这种类型的变量非常适合在循环中使用,或者在if语句中维护适当的变量值。 Traceur 实现了对块作用域变量的支持。 然而,由于性能影响,目前这种支持还处于试验阶段。

考虑以下的代码:

if(true)
{
  var outside = 9;
  et inside = 7;
}
console.log(outside);
console.log(inside);

这将编译为以下内容:

var inside$__0;
if (true) {
  var outside = 9;
  inside$__0 = 7;
}
console.log(outside);
console.log(inside);

可以看到,内部变量被重命名后的变量替换了。 一旦出了块,变量就不再被替换。 运行此代码将在console.log方法出现时报告 inside 是未定义的。

生产中

BabelJS 是一个非常强大的工具,用于复制 JavaScript 下一个版本的许多结构和特性。 然而,生成的代码永远不会像支持构造那样高效。 可能值得对生成的代码进行基准测试,以确保它继续满足项目的性能需求。

提示和技巧

在 JavaScript 中有两个出色的函数库用于处理集合:下划线 js 和 Lo-Dash。 与 TypeScript 或 BabelJS 结合使用时,它们有非常令人愉快的语法,并提供了强大的功能。

例如,使用下划线查找集合中满足条件的所有成员如下所示:

_.filter(collection, (item) => item.Id > 3);

该代码将查找 ID 大于3的所有项目。

这两个库都是我首先添加到新项目中的东西。 下划线实际上是绑定在一个 MVVM 框架 backbone.js 上的。

Grunt 和 Gulp 的任务是用来编译用 TypeScript 或 BabelJS 编写的代码的。 当然,在微软的许多开发工具链中,TypeScript 也得到了很好的支持,尽管 BabelJS 目前还没有得到直接支持。

小结

随着 JavaScript 功能的扩展,对第三方框架甚至翻译器的需求开始下降。 语言本身取代了许多工具。 对于像 jQuery 这样的工具来说,最终的结局是它们不再被需要,因为它们已经被吸收到生态系统中。 多年来,网络浏览器的速度一直无法跟上人们欲望变化的速度。

下一个版本的 AngularJS 背后付出了巨大的努力,但为了让新组件与即将到来的 web 组件标准保持一致,人们也付出了巨大的努力。 Web 组件不会完全取代 AngularJS,但 Angular 最终会简单地增强 Web 组件。

当然,不需要任何框架或工具的想法是荒谬的。 总会有解决问题的新方法出现,新的库和框架也会出现。 人们对如何解决问题的看法也会有所不同。 这就是为什么市场上存在各种 MVVM 框架的空间。

如果使用 ES6 构造,使用 JavaScript 会是一种愉快得多的体验。 有几种可能的方法可以做到这一点,其中哪一种最适合您的特定问题需要进一步研究。