标签 Javascript 下的文章

我刚写了一篇关于 Jupyter Notebooks 的文章,它是一个有趣的交互式写 Python 代码的方式。这让我想起我最近学习了 Glitch,这个我同样喜爱!我构建了一个小的程序来用于关闭转发 twitter。因此有了这篇文章!

Glitch 是一个简单的构建 Javascript web 程序的方式(javascript 后端、javascript 前端)。

关于 glitch 有趣的地方有:

  1. 你在他们的网站输入 Javascript 代码
  2. 只要输入了任何代码,它会自动用你的新代码重载你的网站。你甚至不必保存!它会自动保存。

所以这就像 Heroku,但更神奇!像这样的编码(你输入代码,代码立即在公共网络上运行)对我而言感觉很有趣

这有点像用 ssh 登录服务器,编辑服务器上的 PHP/HTML 代码,它立即就可用了,而这也是我所喜爱的方式。虽然现在我们有了“更好的部署实践”,而不是“编辑代码,让它立即出现在互联网上”,但我们并不是在谈论严肃的开发实践,而是在讨论编写微型程序的乐趣。

Glitch 有很棒的示例应用程序

Glitch 似乎是学习编程的好方式!

比如,这有一个太空侵略者游戏(由 Mary Rose Cook 编写):https://space-invaders.glitch.me/。我喜欢的是我只需要点击几下。

  1. 点击 “remix this”
  2. 开始编辑代码使箱子变成橘色而不是黑色
  3. 制作我自己太空侵略者游戏!我的在这:http://julias-space-invaders.glitch.me/。(我只做了很小的更改使其变成橘色,没什么神奇的)

他们有大量的示例程序,你可以从中启动 - 例如机器人游戏等等。

实际有用的非常好的程序:tweetstorms

我学习 Glitch 的方式是从这个程序开始的:https://tweetstorms.glitch.me/,它会向你展示给定用户的推特云。

比如,你可以在 https://tweetstorms.glitch.me/sarahmei 看到 @sarahmei 的推特云(她发布了很多好的 tweetstorm!)。

我的 Glitch 程序: 关闭转推

当我了解到 Glitch 的时候,我想关闭在 Twitter 上关注的所有人的转推(我知道可以在 Tweetdeck 中做这件事),而且手动做这件事是一件很痛苦的事 - 我一次只能设置一个人。所以我写了一个 Glitch 程序来为我做!

我喜欢我不必设置一个本地开发环境,我可以直接开始输入然后开始!

Glitch 只支持 Javascript,我不是非常了解 Javascript(我之前从没写过一个 Node 程序),所以代码不是很好。但是编写它很愉快 - 能够输入并立即看到我的代码运行是令人愉快的。这是我的项目:https://turn-off-retweets.glitch.me/

就是这些!

使用 Glitch 感觉真的很有趣和民主。通常情况下,如果我想 fork 某人的 Web 项目,并做出更改,我不会这样做 - 我必须 fork,找一个托管,设置本地开发环境或者 Heroku 或其他,安装依赖项等。我认为像安装 node.js 依赖关系这样的任务在过去很有趣,就像“我正在学习新东西很酷”,但现在我觉得它们很乏味。

所以我喜欢只需点击 “remix this!” 并立即在互联网上能有我的版本。


via: https://jvns.ca/blog/2017/11/13/glitch--write-small-web-projects-easily/

作者:Julia Evans 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

探索函数式编程,通过它让你的程序更具有可读性和易于调试

当 Brendan Eich 在 1995 年创造 JavaScript 时,他原本打算将 Scheme 移植到浏览器里 。Scheme 作为 Lisp 的方言,是一种函数式编程语言。而当 Eich 被告知新的语言应该是一种可以与 Java 相比的脚本语言后,他最终确立了一种拥有 C 风格语法的语言(也和 Java 一样),但将函数视作一等公民。而 Java 直到版本 8 才从技术上将函数视为一等公民,虽然你可以用匿名类来模拟它。这个特性允许 JavaScript 通过函数式范式编程。

JavaScript 是一个多范式语言,允许你自由地混合和使用面向对象式、过程式和函数式的编程范式。最近,函数式编程越来越火热。在诸如 AngularReact 这样的框架中,通过使用不可变数据结构可以切实提高性能。不可变是函数式编程的核心原则,它以及纯函数使得编写和调试程序变得更加容易。使用函数来代替程序的循环可以提高程序的可读性并使它更加优雅。总之,函数式编程拥有很多优点。

什么不是函数式编程

在讨论什么是函数式编程前,让我们先排除那些不属于函数式编程的东西。实际上它们是你需要丢弃的语言组件(再见,老朋友):

  • 循环:

    • while
    • do...while
    • for
    • for...of
    • for...in
  • var 或者 let 来声明变量
  • 没有返回值的函数
  • 改变对象的属性 (比如: o.x = 5;)
  • 改变数组本身的方法:

    • copyWithin
    • fill
    • pop
    • push
    • reverse
    • shift
    • sort
    • splice
    • unshift
  • 改变映射本身的方法:

    • clear
    • delete
    • set
  • 改变集合本身的方法:

    • add
    • clear
    • delete

脱离这些特性应该如何编写程序呢?这是我们将在后面探索的问题。

纯函数

你的程序中包含函数不一定意味着你正在进行函数式编程。函数式范式将 纯函数 pure function 非纯函数 impure function 区分开。鼓励你编写纯函数。纯函数必须满足下面的两个属性:

  • 引用透明:函数在传入相同的参数后永远返回相同的返回值。这意味着该函数不依赖于任何可变状态。
  • 无副作用:函数不能导致任何副作用。副作用可能包括 I/O(比如向终端或者日志文件写入),改变一个不可变的对象,对变量重新赋值等等。

我们来看一些例子。首先,multiply 就是一个纯函数的例子,它在传入相同的参数后永远返回相同的返回值,并且不会导致副作用。

function multiply(a, b) {
  return a * b;
}

下面是非纯函数的例子。canRide 函数依赖捕获的 heightRequirement 变量。被捕获的变量不一定导致一个函数是非纯函数,除非它是一个可变的变量(或者可以被重新赋值)。这种情况下使用 let 来声明这个变量,意味着可以对它重新赋值。multiply 函数是非纯函数,因为它会导致在 console 上输出。

let heightRequirement = 46;

// Impure because it relies on a mutable (reassignable) variable.
function canRide(height) {
  return height >= heightRequirement;
}

// Impure because it causes a side-effect by logging to the console.
function multiply(a, b) {
  console.log('Arguments: ', a, b);
  return a * b;
}

下面的列表包含着 JavaScript 内置的非纯函数。你可以指出它们不满足两个属性中的哪个吗?

  • console.log
  • element.addEventListener
  • Math.random
  • Date.now
  • $.ajax (这里 $ 代表你使用的 Ajax 库)

理想的程序中所有的函数都是纯函数,但是从上面的函数列表可以看出,任何有意义的程序都将包含非纯函数。大多时候我们需要进行 AJAX 调用,检查当前日期或者获取一个随机数。一个好的经验法则是遵循 80/20 规则:函数中有 80% 应该是纯函数,剩下的 20% 的必要性将不可避免地是非纯函数。

使用纯函数有几个优点:

  • 它们很容易导出和调试,因为它们不依赖于可变的状态。
  • 返回值可以被缓存或者“记忆”来避免以后重复计算。
  • 它们很容易测试,因为没有需要模拟(mock)的依赖(比如日志,AJAX,数据库等等)。

你编写或者使用的函数返回空(换句话说它没有返回值),那代表它是非纯函数。

不变性

让我们回到捕获变量的概念上。来看看 canRide 函数。我们认为它是一个非纯函数,因为 heightRequirement 变量可以被重新赋值。下面是一个构造出来的例子来说明如何用不可预测的值来对它重新赋值。

let heightRequirement = 46;

function canRide(height) {
  return height >= heightRequirement;
}

// Every half second, set heightRequirement to a random number between 0 and 200.
setInterval(() => heightRequirement = Math.floor(Math.random() * 201), 500);

const mySonsHeight = 47;

// Every half second, check if my son can ride.
// Sometimes it will be true and sometimes it will be false.
setInterval(() => console.log(canRide(mySonsHeight)), 500);

我要再次强调被捕获的变量不一定会使函数成为非纯函数。我们可以通过只是简单地改变 heightRequirement 的声明方式来使 canRide 函数成为纯函数。

const heightRequirement = 46;

function canRide(height) {
  return height >= heightRequirement;
}

通过用 const 来声明变量意味着它不能被再次赋值。如果尝试对它重新赋值,运行时引擎将抛出错误;那么,如果用对象来代替数字来存储所有的“常量”怎么样?

const constants = {
  heightRequirement: 46,
  // ... other constants go here
};

function canRide(height) {
  return height >= constants.heightRequirement;
}

我们用了 const ,所以这个变量不能被重新赋值,但是还有一个问题:这个对象可以被改变。下面的代码展示了,为了真正使其不可变,你不仅需要防止它被重新赋值,你也需要不可变的数据结构。JavaScript 语言提供了 Object.freeze 方法来阻止对象被改变。

'use strict';

// CASE 1: 对象的属性是可变的,并且变量可以被再次赋值。
let o1 = { foo: 'bar' };

// 改变对象的属性
o1.foo = 'something different';

// 对变量再次赋值
o1 = { message: "I'm a completely new object" };


// CASE 2: 对象的属性还是可变的,但是变量不能被再次赋值。
const o2 = { foo: 'baz' };

// 仍然能改变对象
o2.foo = 'Something different, yet again';

// 不能对变量再次赋值
// o2 = { message: 'I will cause an error if you uncomment me' }; // Error!


// CASE 3: 对象的属性是不可变的,但是变量可以被再次赋值。
let o3 = Object.freeze({ foo: "Can't mutate me" });

// 不能改变对象的属性
// o3.foo = 'Come on, uncomment me. I dare ya!'; // Error!

// 还是可以对变量再次赋值
o3 = { message: "I'm some other object, and I'm even mutable -- so take that!" };


// CASE 4: 对象的属性是不可变的,并且变量不能被再次赋值。这是我们想要的!!!!!!!!
const o4 = Object.freeze({ foo: 'never going to change me' });

// 不能改变对象的属性
// o4.foo = 'talk to the hand' // Error!

// 不能对变量再次赋值
// o4 = { message: "ain't gonna happen, sorry" }; // Error

不变性适用于所有的数据结构,包括数组、映射和集合。它意味着不能调用例如 Array.prototype.push 等会导致本身改变的方法,因为它会改变已经存在的数组。可以通过创建一个含有原来元素和新加元素的新数组,而不是将新元素加入一个已经存在的数组。其实所有会导致数组本身被修改的方法都可以通过一个返回修改好的新数组的函数代替。

'use strict';

const a = Object.freeze([4, 5, 6]);

// Instead of: a.push(7, 8, 9);
const b = a.concat(7, 8, 9);

// Instead of: a.pop();
const c = a.slice(0, -1);

// Instead of: a.unshift(1, 2, 3);
const d = [1, 2, 3].concat(a);

// Instead of: a.shift();
const e = a.slice(1);

// Instead of: a.sort(myCompareFunction);
const f = R.sort(myCompareFunction, a); // R = Ramda

// Instead of: a.reverse();
const g = R.reverse(a); // R = Ramda

// 留给读者的练习:
// copyWithin
// fill
// splice

映射集合 也很相似。可以通过返回一个新的修改好的映射或者集合来代替使用会修改其本身的函数。

const map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three']
]);

// Instead of: map.set(4, 'four');
const map2 = new Map([...map, [4, 'four']]);

// Instead of: map.delete(1);
const map3 = new Map([...map].filter(([key]) => key !== 1));

// Instead of: map.clear();
const map4 = new Map();
const set = new Set(['A', 'B', 'C']);

// Instead of: set.add('D');
const set2 = new Set([...set, 'D']);

// Instead of: set.delete('B');
const set3 = new Set([...set].filter(key => key !== 'B'));

// Instead of: set.clear();
const set4 = new Set();

我想提一句如果你在使用 TypeScript(我非常喜欢 TypeScript),你可以用 Readonly<T>ReadonlyArray<T>ReadonlyMap<K, V>ReadonlySet<T> 接口来在编译期检查你是否尝试更改这些对象,有则抛出编译错误。如果在对一个对象字面量或者数组调用 Object.freeze,编译器会自动推断它是只读的。由于映射和集合在其内部表达,所以在这些数据结构上调用 Object.freeze 不起作用。但是你可以轻松地告诉编译器它们是只读的变量。

 title=

TypeScript 只读接口

好,所以我们可以通过创建新的对象来代替修改原来的对象,但是这样不会导致性能损失吗?当然会。确保在你自己的应用中做了性能测试。如果你需要提高性能,可以考虑使用 Immutable.js。Immutable.js 用持久的数据结构 实现了链表堆栈映射集合和其他数据结构。使用了如同 Clojure 和 Scala 这样的函数式语言中相同的技术。

// Use in place of `[]`.
const list1 = Immutable.List(['A', 'B', 'C']);
const list2 = list1.push('D', 'E');

console.log([...list1]); // ['A', 'B', 'C']
console.log([...list2]); // ['A', 'B', 'C', 'D', 'E']


// Use in place of `new Map()`
const map1 = Immutable.Map([
  ['one', 1],
  ['two', 2],
  ['three', 3]
]);
const map2 = map1.set('four', 4);

console.log([...map1]); // [['one', 1], ['two', 2], ['three', 3]]
console.log([...map2]); // [['one', 1], ['two', 2], ['three', 3], ['four', 4]]


// Use in place of `new Set()`
const set1 = Immutable.Set([1, 2, 3, 3, 3, 3, 3, 4]);
const set2 = set1.add(5);

console.log([...set1]); // [1, 2, 3, 4]
console.log([...set2]); // [1, 2, 3, 4, 5]

函数组合

记不记得在中学时我们学过一些像 (f ∘ g)(x) 的东西?你那时可能想,“我什么时候会用到这些?”,好了,现在就用到了。你准备好了吗?f ∘ g读作 “函数 f 和函数 g 组合”。对它的理解有两种等价的方式,如等式所示: (f ∘ g)(x) = f(g(x))。你可以认为 f ∘ g 是一个单独的函数,或者视作将调用函数 g 的结果作为参数传给函数 f。注意这些函数是从右向左依次调用的,先执行 g,接下来执行 f

关于函数组合的几个要点:

  1. 我们可以组合任意数量的函数(不仅限于 2 个)。
  2. 组合函数的一个方式是简单地把一个函数的输出作为下一个函数的输入(比如 f(g(x)))。
// h(x) = x + 1
// number -> number
function h(x) {
  return x + 1;
}

// g(x) = x^2
// number -> number
function g(x) {
  return x * x;
}

// f(x) = convert x to string
// number -> string
function f(x) {
  return x.toString();
}

// y = (f ∘ g ∘ h)(1)
const y = f(g(h(1)));
console.log(y); // '4'

Ramdalodash 之类的库提供了更优雅的方式来组合函数。我们可以在更多的在数学意义上处理函数组合,而不是简单地将一个函数的返回值传递给下一个函数。我们可以创建一个由这些函数组成的单一复合函数(就是 (f ∘ g)(x))。

// h(x) = x + 1
// number -> number
function h(x) {
  return x + 1;
}

// g(x) = x^2
// number -> number
function g(x) {
  return x * x;
}

// f(x) = convert x to string
// number -> string
function f(x) {
  return x.toString();
}

// R = Ramda
// composite = (f ∘ g ∘ h)
const composite = R.compose(f, g, h);

// Execute single function to get the result.
const y = composite(1);
console.log(y); // '4'

好了,我们可以在 JavaScript 中组合函数了。接下来呢?好,如果你已经入门了函数式编程,理想中你的程序将只有函数的组合。代码里没有循环(for, for...of, for...in, while, do),基本没有。你可能觉得那是不可能的。并不是这样。我们下面的两个话题是:递归和高阶函数。

递归

假设你想实现一个计算数字的阶乘的函数。 让我们回顾一下数学中阶乘的定义:

n! = n * (n-1) * (n-2) * ... * 1.

n! 是从 n1 的所有整数的乘积。我们可以编写一个循环轻松地计算出结果。

function iterativeFactorial(n) {
  let product = 1;
  for (let i = 1; i <= n; i++) {
    product *= i;
  }
  return product;
}

注意 producti 都在循环中被反复重新赋值。这是解决这个问题的标准过程式方法。如何用函数式的方法解决这个问题呢?我们需要消除循环,确保没有变量被重新赋值。递归是函数式程序员的最有力的工具之一。递归需要我们将整体问题分解为类似整体问题的子问题。

计算阶乘是一个很好的例子,为了计算 n! 我们需要将 n 乘以所有比它小的正整数。它的意思就相当于:

n! = n * (n-1)!

啊哈!我们发现了一个解决 (n-1)! 的子问题,它类似于整个问题 n!。还有一个需要注意的地方就是基础条件。基础条件告诉我们何时停止递归。 如果我们没有基础条件,那么递归将永远持续。 实际上,如果有太多的递归调用,程序会抛出一个堆栈溢出错误。啊哈!

function recursiveFactorial(n) {
  // Base case -- stop the recursion
  if (n === 0) {
    return 1; // 0! is defined to be 1.
  }
  return n * recursiveFactorial(n - 1);
}

然后我们来计算 recursiveFactorial(20000) 因为……,为什么不呢?当我们这样做的时候,我们得到了这个结果:

 title=

堆栈溢出错误

这里发生了什么?我们得到一个堆栈溢出错误!这不是无穷的递归导致的。我们已经处理了基础条件(n === 0 的情况)。那是因为浏览器的堆栈大小是有限的,而我们的代码使用了越过了这个大小的堆栈。每次对 recursiveFactorial 的调用导致了新的帧被压入堆栈中,就像一个盒子压在另一个盒子上。每当 recursiveFactorial 被调用,一个新的盒子被放在最上面。下图展示了在计算 recursiveFactorial(3) 时堆栈的样子。注意在真实的堆栈中,堆栈顶部的帧将存储在执行完成后应该返回的内存地址,但是我选择用变量 r 来表示返回值,因为 JavaScript 开发者一般不需要考虑内存地址。

 title=")

递归计算 3! 的堆栈(三次乘法)

你可能会想象当计算 n = 20000 时堆栈会更高。我们可以做些什么优化它吗?当然可以。作为 ES2015 (又名 ES6) 标准的一部分,有一个优化用来解决这个问题。它被称作 尾调用优化 proper tail calls optimization (PTC)。当递归函数做的最后一件事是调用自己并返回结果的时候,它使得浏览器删除或者忽略堆栈帧。实际上,这个优化对于相互递归函数也是有效的,但是为了简单起见,我们还是来看单一递归函数。

你可能会注意到,在递归函数调用之后,还要进行一次额外的计算(n * r)。那意味着浏览器不能通过 PTC 来优化递归;然而,我们可以通过重写函数使最后一步变成递归调用以便优化。一个窍门是将中间结果(在这里是 product)作为参数传递给函数。

'use strict';

// Optimized for tail call optimization.
function factorial(n, product = 1) {
  if (n === 0) {
    return product;
  }
  return factorial(n - 1, product * n)
}

让我们来看看优化后的计算 factorial(3) 时的堆栈。如下图所示,堆栈不会增长到超过两层。原因是我们把必要的信息都传到了递归函数中(比如 product)。所以,在 product 被更新后,浏览器可以丢弃掉堆栈中原先的帧。你可以在图中看到每次最上面的帧下沉变成了底部的帧,原先底部的帧被丢弃,因为不再需要它了。

 title= using PTC")

递归计算 3! 的堆栈(三次乘法)使用 PTC

现在选一个浏览器运行吧,假设你在使用 Safari,你会得到 Infinity(它是比在 JavaScript 中能表达的最大值更大的数)。但是我们没有得到堆栈溢出错误,那很不错!现在在其他的浏览器中呢怎么样呢?Safari 可能现在乃至将来是实现 PTC 的唯一一个浏览器。看看下面的兼容性表格:

 title=

PTC 兼容性

其他浏览器提出了一种被称作 语法级尾调用 syntactic tail calls (STC)的竞争标准。“语法级”意味着你需要用新的语法来标识你想要执行尾递归优化的函数。即使浏览器还没有广泛支持,但是把你的递归函数写成支持尾递归优化的样子还是一个好主意。

高阶函数

我们已经知道 JavaScript 将函数视作一等公民,可以把函数像其他值一样传递。所以,把一个函数传给另一个函数也很常见。我们也可以让函数返回一个函数。就是它!我们有高阶函数。你可能已经很熟悉几个在 Array.prototype 中的高阶函数。比如 filtermapreduce 就在其中。对高阶函数的一种理解是:它是接受(一般会调用)一个回调函数参数的函数。让我们来看看一些内置的高阶函数的例子:

const vehicles = [
  { make: 'Honda', model: 'CR-V', type: 'suv', price: 24045 },
  { make: 'Honda', model: 'Accord', type: 'sedan', price: 22455 },
  { make: 'Mazda', model: 'Mazda 6', type: 'sedan', price: 24195 },
  { make: 'Mazda', model: 'CX-9', type: 'suv', price: 31520 },
  { make: 'Toyota', model: '4Runner', type: 'suv', price: 34210 },
  { make: 'Toyota', model: 'Sequoia', type: 'suv', price: 45560 },
  { make: 'Toyota', model: 'Tacoma', type: 'truck', price: 24320 },
  { make: 'Ford', model: 'F-150', type: 'truck', price: 27110 },
  { make: 'Ford', model: 'Fusion', type: 'sedan', price: 22120 },
  { make: 'Ford', model: 'Explorer', type: 'suv', price: 31660 }
];

const averageSUVPrice = vehicles
  .filter(v => v.type === 'suv')
  .map(v => v.price)
  .reduce((sum, price, i, array) => sum + price / array.length, 0);

console.log(averageSUVPrice); // 33399

注意我们在一个数组对象上调用其方法,这是面向对象编程的特性。如果我们想要更函数式一些,我们可以用 Rmmda 或者 lodash/fp 提供的函数。注意如果我们使用 R.compose 的话,需要倒转函数的顺序,因为它从右向左依次调用函数(从底向上);然而,如果我们想从左向右调用函数就像上面的例子,我们可以用 R.pipe。下面两个例子用了 Rmmda。注意 Rmmda 有一个 mean 函数用来代替 reduce

const vehicles = [
  { make: 'Honda', model: 'CR-V', type: 'suv', price: 24045 },
  { make: 'Honda', model: 'Accord', type: 'sedan', price: 22455 },
  { make: 'Mazda', model: 'Mazda 6', type: 'sedan', price: 24195 },
  { make: 'Mazda', model: 'CX-9', type: 'suv', price: 31520 },
  { make: 'Toyota', model: '4Runner', type: 'suv', price: 34210 },
  { make: 'Toyota', model: 'Sequoia', type: 'suv', price: 45560 },
  { make: 'Toyota', model: 'Tacoma', type: 'truck', price: 24320 },
  { make: 'Ford', model: 'F-150', type: 'truck', price: 27110 },
  { make: 'Ford', model: 'Fusion', type: 'sedan', price: 22120 },
  { make: 'Ford', model: 'Explorer', type: 'suv', price: 31660 }
];

// Using `pipe` executes the functions from top-to-bottom. 
const averageSUVPrice1 = R.pipe(
  R.filter(v => v.type === 'suv'),
  R.map(v => v.price),
  R.mean
)(vehicles);

console.log(averageSUVPrice1); // 33399

// Using `compose` executes the functions from bottom-to-top.
const averageSUVPrice2 = R.compose(
  R.mean,
  R.map(v => v.price),
  R.filter(v => v.type === 'suv')
)(vehicles);

console.log(averageSUVPrice2); // 33399

使用函数式方法的优点是清楚地分开了数据(vehicles)和逻辑(函数 filtermapreduce)。面向对象的代码相比之下把数据和函数用以方法的对象的形式混合在了一起。

柯里化

不规范地说, 柯里化 currying 是把一个接受 n 个参数的函数变成 n 个每个接受单个参数的函数的过程。函数的 arity 是它接受参数的个数。接受一个参数的函数是 unary,两个的是 binary,三个的是 ternaryn 个的是 n-ary。那么,我们可以把柯里化定义成将一个 n-ary 函数转换成 nunary 函数的过程。让我们通过简单的例子开始,一个计算两个向量点积的函数。回忆一下线性代数,两个向量 [a, b, c][x, y, z] 的点积是 ax + by + cz

function dot(vector1, vector2) {
  return vector1.reduce((sum, element, index) => sum += element * vector2[index], 0);
}

const v1 = [1, 3, -5];
const v2 = [4, -2, -1];

console.log(dot(v1, v2)); // 1(4) + 3(-2) + (-5)(-1) = 4 - 6 + 5 = 3

dot 函数是 binary,因为它接受两个参数;然而我们可以将它手动转换成两个 unary 函数,就像下面的例子。注意 curriedDot 是一个 unary 函数,它接受一个向量并返回另一个接受第二个向量的 unary 函数。

function curriedDot(vector1) {
  return function(vector2) {
    return vector1.reduce((sum, element, index) => sum += element * vector2[index], 0);
  }
}

// Taking the dot product of any vector with [1, 1, 1]
// is equivalent to summing up the elements of the other vector.
const sumElements = curriedDot([1, 1, 1]);

console.log(sumElements([1, 3, -5])); // -1
console.log(sumElements([4, -2, -1])); // 1

很幸运,我们不需要把每一个函数都手动转换成柯里化以后的形式。Ramdalodash 等库可以为我们做这些工作。实际上,它们是柯里化的混合形式。你既可以每次传递一个参数,也可以像原来一样一次传递所有参数。

function dot(vector1, vector2) {
  return vector1.reduce((sum, element, index) => sum += element * vector2[index], 0);
}

const v1 = [1, 3, -5];
const v2 = [4, -2, -1];

// Use Ramda to do the currying for us!
const curriedDot = R.curry(dot);

const sumElements = curriedDot([1, 1, 1]);

console.log(sumElements(v1)); // -1
console.log(sumElements(v2)); // 1

// This works! You can still call the curried function with two arguments.
console.log(curriedDot(v1, v2)); // 3

Ramda 和 lodash 都允许你“跳过”一些变量之后再指定它们。它们使用置位符来做这些工作。因为点积的计算可以交换两项。传入向量的顺序不影响结果。让我们换一个例子来阐述如何使用一个置位符。Ramda 使用双下划线作为其置位符。

const giveMe3 = R.curry(function(item1, item2, item3) {
  return `
    1: ${item1}
    2: ${item2}
    3: ${item3}
  `;
});

const giveMe2 = giveMe3(R.__, R.__, 'French Hens');   // Specify the third argument.
const giveMe1 = giveMe2('Partridge in a Pear Tree');  // This will go in the first slot.
const result = giveMe1('Turtle Doves');               // Finally fill in the second argument.

console.log(result);
// 1: Partridge in a Pear Tree
// 2: Turtle Doves
// 3: French Hens

在我们结束探讨柯里化之前最后的议题是 偏函数应用 partial application 。偏函数应用和柯里化经常同时出场,尽管它们实际上是不同的概念。一个柯里化的函数还是柯里化的函数,即使没有给它任何参数。偏函数应用,另一方面是仅仅给一个函数传递部分参数而不是所有参数。柯里化是偏函数应用常用的方法之一,但是不是唯一的。

JavaScript 拥有一个内置机制可以不依靠柯里化来做偏函数应用。那就是 function.prototype.bind 方法。这个方法的一个特殊之处在于,它要求你将 this 作为第一个参数传入。 如果你不进行面向对象编程,那么你可以通过传入 null 来忽略 this

1function giveMe3(item1, item2, item3) {
  return `
    1: ${item1}
    2: ${item2}
    3: ${item3}
  `;
}

const giveMe2 = giveMe3.bind(null, 'rock');
const giveMe1 = giveMe2.bind(null, 'paper');
const result = giveMe1('scissors');

console.log(result);
// 1: rock
// 2: paper
// 3: scissors

总结

我希望你享受探索 JavaScript 中函数式编程的过程。对一些人来说,它可能是一个全新的编程范式,但我希望你能尝试它。你会发现你的程序更易于阅读和调试。不变性还将允许你优化 Angular 和 React 的性能。

这篇文章基于 Matt 在 OpenWest 的演讲 JavaScript the Good-er Parts. OpenWest 在 6/12-15 ,2017 在 Salt Lake City, Utah 举行。


作者简介:

Matt Banz - Matt 于 2008 年五月在犹他大学获得了数学学位毕业。一个月后他得到了一份 web 开发者的工作,他从那时起就爱上了它!在 2013 年,他在北卡罗莱纳州立大学获得了计算机科学硕士学位。他在 LDS 商学院和戴维斯学区社区教育计划教授 Web 课程。他现在是就职于 Motorola Solutions 公司的高级前端开发者。


via: https://opensource.com/article/17/6/functional-javascript

作者:Matt Banz 译者:trnhoe 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

可以很公平地说,JavaScript 是当下软件工程中最重要的技术。对于那些深入接触过编程语言、编译器和虚拟机的人来说,这仍然有点令人惊讶,因为在语言设计者们看来,JavaScript 不是十分优雅;在编译器工程师们看来,它没有多少可优化的地方;甚至还没有一个伟大的标准库。这取决于你和谁吐槽,JavaScript 的缺点你花上数周都枚举不完,而你总会找到一些你从所未知的奇怪的东西。尽管这看起来明显困难重重,不过 JavaScript 还是成为了当今 web 的核心,并且还(通过 Node.js)成为服务器端和云端的主导技术,甚至还开辟了进军物联网领域的道路。

那么问题来了,为什么 JavaScript 如此受欢迎?或者说如此成功?我知道没有一个很好的答案。如今我们有许多使用 JavaScript 的好理由,或许最重要的是围绕其构建的庞大的生态系统,以及现今大量可用的资源。但所有这一切实际上是发展到一定程度的后果。为什么 JavaScript 变得流行起来了?嗯,你或许会说,这是 web 多年来的通用语了。但是在很长一段时间里,人们极其讨厌 JavaScript。回顾过去,似乎第一波 JavaScript 浪潮爆发在上个年代的后半段。那个时候 JavaScript 引擎加速了各种不同的任务的执行,很自然的,这可能让很多人对 JavaScript 刮目相看。

回到过去那些日子,这些加速使用了现在所谓的传统 JavaScript 基准进行测试——从苹果的 SunSpider 基准(JavaScript 微基准之母)到 Mozilla 的 Kraken 基准 和谷歌的 V8 基准。后来,V8 基准被 Octane 基准 取代,而苹果发布了新的 JetStream 基准。这些传统的 JavaScript 基准测试驱动了无数人的努力,使 JavaScript 的性能达到了本世纪初没人能预料到的水平。据报道其性能加速达到了 1000 倍,一夜之间在网站使用 <script> 标签不再是与魔鬼共舞,做客户端不再仅仅是可能的了,甚至是被鼓励的。

性能测试,JS 基准的简史

(来源: Advanced JS performance with V8 and Web Assembly, Chrome Developer Summit 2016, @s3ththompson。)

现在是 2016 年,所有(相关的)JavaScript 引擎的性能都达到了一个令人难以置信的水平,web 应用像原生应用一样快(或者能够像原生应用一样快)。引擎配有复杂的优化编译器,通过收集之前的关于类型/形状的反馈来推测某些操作(例如属性访问、二进制操作、比较、调用等),生成高度优化的机器代码的短序列。大多数优化是由 SunSpider 或 Kraken 等微基准以及 Octane 和 JetStream 等静态测试套件驱动的。由于有像 asm.jsEmscripten 这样的 JavaScript 技术,我们甚至可以将大型 C++ 应用程序编译成 JavaScript,并在你的浏览器上运行,而无需下载或安装任何东西。例如,现在你可以在 web 上玩 AngryBots,无需沙盒,而过去的 web 游戏需要安装一堆诸如 Adobe Flash 或 Chrome PNaCl 的特殊插件。

这些成就绝大多数都要归功于这些微基准和静态性能测试套件的出现,以及与这些传统的 JavaScript 基准间的竞争的结果。你可以对 SunSpider 表示不满,但很显然,没有 SunSpider,JavaScript 的性能可能达不到今天的高度。好吧,赞美到此为止。现在看看另一方面,所有的静态性能测试——无论是 微基准 micro-benchmark 还是大型应用的 宏基准 macro-benchmark ,都注定要随着时间的推移变成噩梦!为什么?因为在开始摆弄它之前,基准只能教你这么多。一旦达到某个阔值以上(或以下),那么有益于特定基准的优化的一般适用性将呈指数级下降。例如,我们将 Octane 作为现实世界中 web 应用性能的代表,并且在相当长的一段时间里,它可能做得很不错,但是现在,Octane 与现实场景中的时间分布是截然不同的,因此即使眼下再优化 Octane 乃至超越自身,可能在现实世界中还是得不到任何显著的改进(无论是通用 web 还是 Node.js 的工作负载)。

基准与现实世界的时间分布对比

(来源:Real-World JavaScript Performance,BlinkOn 6 conference,@tverwaes)

由于传统 JavaScript 基准(包括最新版的 JetStream 和 Octane)可能已经背离其有用性变得越来越远,我们开始在 2016 年初寻找新的方法来测量现实场景的性能,为 V8 和 Chrome 添加了大量新的性能追踪钩子。我们还特意添加一些机制来查看我们在浏览 web 时的时间究竟开销在哪里,例如,是脚本执行、垃圾回收、编译,还是什么地方?而这些调查的结果非常有趣和令人惊讶。从上面的幻灯片可以看出,运行 Octane 花费了 70% 以上的时间去执行 JavaScript 和垃圾回收,而浏览 web 的时候,通常执行 JavaScript 花费的时间不到 30%,垃圾回收占用的时间永远不会超过 5%。在 Octane 中并没有体现出它花费了大量时间来解析和编译。因此,将更多的时间用在优化 JavaScript 执行上将提高你的 Octane 跑分,但不会对加载 youtube.com 有任何积极的影响。事实上,花费更多的时间来优化 JavaScript 执行甚至可能有损你现实场景的性能,因为编译器需要更多的时间,或者你需要跟踪更多的反馈,最终在编译、垃圾回收和 运行时桶 Runtime bucket 等方面开销了更多的时间。

测速表

还有另外一组基准测试用于测量浏览器整体性能(包括 JavaScript 和 DOM 性能),最新推出的是 Speedometer 基准。该基准试图通过运行一个用不同的主流 web 框架实现的简单的 TodoMVC 应用(现在看来有点过时了,不过新版本正在研发中)以捕获更真实的现实场景的性能。上述幻灯片中的各种测试 (Angular、Ember、React、Vanilla、Flight 和 Backbone)挨着放在 Octane 之后,你可以看到,此时此刻这些测试似乎更好地代表了现实世界的性能指标。但是请注意,这些数据收集在本文撰写将近 6 个月以前,而且我们优化了更多的现实场景模式(例如我们正在重构垃圾回收系统以显著地降低开销,并且 解析器也正在重新设计)。还要注意的是,虽然这看起来像是只和浏览器相关,但我们有非常强有力的证据表明传统的峰值性能基准也不能很好的代表现实场景中 Node.js 应用性能。

Speedometer 和 Octane 对比

(来源: Real-World JavaScript Performance, BlinkOn 6 conference, @tverwaes.)

所有这一切可能已经路人皆知了,因此我将用本文剩下的部分强调一些具体案例,它们对关于我为什么认为这不仅有用,而且必须停止关注某一阈值的静态峰值性能基准测试对于 JavaScript 社区的健康是很关键的。让我通过一些例子说明 JavaScript 引擎怎样来玩弄基准的。

臭名昭著的 SunSpider 案例

一篇关于传统 JavaScript 基准测试的博客如果没有指出 SunSpider 那个明显的问题是不完整的。让我们从性能测试的最佳实践开始,它在现实场景中不是很适用:bitops-bitwise-and.js 性能测试

bitops-bitwise-and.js

有一些算法需要进行快速的 AND 位运算,特别是从 C/C++ 转译成 JavaScript 的地方,所以快速执行该操作确实有点意义。然而,现实场景中的网页可能不关心引擎在循环中执行 AND 位运算是否比另一个引擎快两倍。但是再盯着这段代码几秒钟后,你可能会注意到在第一次循环迭代之后 bitwiseAndValue 将变成 0,并且在接下来的 599999 次迭代中将保持为 0。所以一旦你让此获得了好的性能,比如在差不多的硬件上所有测试均低于 5ms,在经过尝试之后你会意识到,只有循环的第一次是必要的,而剩余的迭代只是在浪费时间(例如 loop peeling 后面的死代码),那你现在就可以开始玩弄这个基准测试了。这需要 JavaScript 中的一些机制来执行这种转换,即你需要检查 bitwiseAndValue 是全局对象的常规属性还是在执行脚本之前不存在,全局对象或者它的原型上必须没有拦截器。但如果你真的想要赢得这个基准测试,并且你愿意全力以赴,那么你可以在不到 1ms 的时间内完成这个测试。然而,这种优化将局限于这种特殊情况,并且测试的轻微修改可能不再触发它。

好吧,那么 bitops-bitwise-and.js 测试彻底肯定是微基准最失败的案例。让我们继续转移到 SunSpider 中更逼真的场景——string-tagcloud.js 测试,它基本上是运行一个较早版本的 json.js polyfill。该测试可以说看起来比位运算测试更合理,但是花点时间查看基准的配置之后立刻会发现:大量的时间浪费在一条 eval 表达式(高达 20% 的总执行时间被用于解析和编译,再加上实际执行编译后代码的 10% 的时间)。

string-tagcloud.js

仔细看看,这个 eval 只执行了一次,并传递一个 JSON 格式的字符串,它包含一个由 2501 个含有 tagpopularity 属性的对象组成的数组:

([
  {
    "tag": "titillation",
    "popularity": 4294967296
  },
  {
    "tag": "foamless",
    "popularity": 1257718401
  },
  {
    "tag": "snarler",
    "popularity": 613166183
  },
  {
    "tag": "multangularness",
    "popularity": 368304452任何
  },
  {
    "tag": "Fesapo unventurous",
    "popularity": 248026512
  },
  {
    "tag": "esthesioblast",
    "popularity": 179556755
  },
  {
    "tag": "echeneidoid",
    "popularity": 136641578
  },
  {
    "tag": "embryoctony",
    "popularity": 107852576
  },
  ...
])

显然,解析这些对象字面量,为其生成本地代码,然后执行该代码的成本很高。将输入的字符串解析为 JSON 并生成适当的对象图的开销将更加低廉。所以,加快这个基准测试的一个小把戏就是模拟 eval,并尝试总是将数据首先作为 JSON 解析,如果以 JSON 方式读取失败,才回退进行真实的解析、编译、执行(尽管需要一些额外的黑魔法来跳过括号)。早在 2007 年,这甚至不算是一个坏点子,因为没有 JSON.parse,不过在 2017 年这只是 JavaScript 引擎的技术债,可能会让 eval 的合法使用遥遥无期。

--- string-tagcloud.js.ORIG     2016-12-14 09:00:52.869887104 +0100
+++ string-tagcloud.js  2016-12-14 09:01:01.033944051 +0100
@@ -198,7 +198,7 @@
                     replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(:?[eE][+\-]?\d+)?/g, ']').
                     replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {

-                j = eval('(' + this + ')');
+                j = JSON.parse(this);

                 return typeof filter === 'function' ? walk('', j) : j;
             }

事实上,将基准测试更新到现代 JavaScript 会立刻会性能暴增,正如今天的 V8 LKGR 从 36ms 降到了 26ms,性能足足提升了 30%!

$ node string-tagcloud.js.ORIG
Time (string-tagcloud): 36 ms.
$ node string-tagcloud.js
Time (string-tagcloud): 26 ms.
$ node -v
v8.0.0-pre
$

这是静态基准和性能测试套件常见的一个问题。今天,没有人会正儿八经地用 eval 解析 JSON 数据(不仅是因为性能问题,还出于严重的安全性考虑),而是坚持为最近五年写的代码使用 JSON.parse。事实上,使用 eval 解析 JSON 可能会被视作产品级代码的的一个漏洞!所以引擎作者致力于新代码的性能所作的努力并没有反映在这个古老的基准中,相反地,而是使得 eval 不必要地更智能复杂化,从而赢得 string-tagcloud.js 测试。

好吧,让我们看看另一个例子:3d-cube.js。这个基准测试做了很多矩阵运算,即便是最聪明的编译器对此也无可奈何,只能说执行而已。基本上,该基准测试花了大量的时间执行 Loop 函数及其调用的函数。

3d-cube.js

一个有趣的发现是:RotateXRotateYRotateZ 函数总是调用相同的常量参数 Phi

3d-cube.js

这意味着我们基本上总是为 Math.sinMath.cos 计算相同的值,每次执行都要计算 204 次。只有 3 个不同的输入值:

  • 0.017453292519943295
  • 0.05235987755982989
  • 0.08726646259971647

显然,你可以在这里做的一件事情就是通过缓存以前的计算值来避免重复计算相同的正弦值和余弦值。事实上,这是 V8 以前的做法,而其它引擎例如 SpiderMonkey 目前仍然在这样做。我们从 V8 中删除了所谓的 超载缓存 transcendental cache ,因为缓存的开销在实际的工作负载中是不可忽视的,你不可能总是在一行代码中计算相同的值,这在其它地方倒不稀奇。当我们在 2013 和 2014 年移除这个特定的基准优化时,我们对 SunSpider 基准产生了强烈的冲击,但我们完全相信,为基准而优化并没有任何意义,并同时以这种方式批判了现实场景中的使用案例。

3d-cube 基准

(来源:arewefastyet.com

显然,处理恒定正弦/余弦输入的更好的方法是一个内联的启发式算法,它试图平衡内联因素与其它不同的因素,例如在调用位置优先选择内联,其中 常量叠算 constant folding 可以是有益的,例如在 RotateXRotateYRotateZ 调用位置的案例中。但是出于各种原因,这对于 Crankshaft 编译器并不可行。使用 IgnitionTurboFan 倒是一个明智的选择,我们已经在开发更好的内联启发式算法

垃圾回收(GC)是有害的

除了这些非常具体的测试问题,SunSpider 基准测试还有一个根本性的问题:总体执行时间。目前 V8 在适当的英特尔硬件上运行整个基准测试大概只需要 200ms(使用默认配置)。 次垃圾回收 minor GC 在 1ms 到 25ms 之间(取决于新空间中的存活对象和旧空间的碎片),而 主垃圾回收 major GC 暂停的话可以轻松减掉 30ms(甚至不考虑增量标记的开销),这超过了 SunSpider 套件总体执行时间的 10%!因此,任何不想因垃圾回收循环而造成减速 10-20% 的引擎,必须用某种方式确保它在运行 SunSpider 时不会触发垃圾回收。

driver-TEMPLATE.html

就实现而言,有不同的方案,不过就我所知,没有一个在现实场景中产生了任何积极的影响。V8 使用了一个相当简单的技巧:由于每个 SunSpider 套件都运行在一个新的 <iframe> 中,这对应于 V8 中一个新的本地上下文,我们只需检测快速的 <iframe> 创建和处理(所有的 SunSpider 测试每个花费的时间小于 50ms),在这种情况下,在处理和创建之间执行垃圾回收,以确保我们在实际运行测试的时候不会触发垃圾回收。这个技巧运行的很好,在 99.9% 的案例中没有与实际用途冲突;除了时不时的你可能会受到打击,不管出于什么原因,如果你做的事情让你看起来像是 V8 的 SunSpider 测试驱动程序,你就可能被强制的垃圾回收打击到,这有可能对你的应用导致负面影响。所以谨记一点:不要让你的应用看起来像 SunSpider!

我可以继续展示更多 SunSpider 示例,但我不认为这非常有用。到目前为止,应该清楚的是,为刷新 SunSpider 评分而做的进一步优化在现实场景中没有带来任何好处。事实上,世界可能会因为没有 SunSpider 而更美好,因为引擎可以放弃只是用于 SunSpider 的奇淫技巧,或者甚至可以伤害到现实中的用例。不幸的是,SunSpider 仍然被(科技)媒体大量地用来比较他们眼中的浏览器性能,或者甚至用来比较手机!所以手机制造商和安卓制造商对于让 SunSpider(以及其它现在毫无意义的基准 FWIW) 上的 Chrome 看起来比较体面自然有一定的兴趣。手机制造商通过销售手机来赚钱,所以获得良好的评价对于电话部门甚至整间公司的成功至关重要。其中一些部门甚至在其手机中配置在 SunSpider 中得分较高的旧版 V8,将他们的用户置于各种未修复的安全漏洞之下(在新版中早已被修复),而让用户被最新版本的 V8 带来的任何现实场景的性能优势拒之门外!

Galaxy S7 和 S7 Edge 的评价:三星的高光表现

(来源:www.engadget.com

作为 JavaScript 社区的一员,如果我们真的想认真对待 JavaScript 领域的现实场景的性能,我们需要让各大技术媒体停止使用传统的 JavaScript 基准来比较浏览器或手机。能够在每个浏览器中运行一个基准测试,并比较它的得分自然是好的,但是请使用一个与当今世界相关的基准,例如真实的 web 页面;如果你觉得需要通过浏览器基准来比较两部手机,请至少考虑使用 Speedometer

轻松一刻

我一直很喜欢这个 Myles Borins 谈话,所以我不得不无耻地向他偷师。现在我们从 SunSpider 的谴责中回过头来,让我们继续检查其它经典基准。

不是那么显眼的 Kraken 案例

Kraken 基准是 Mozilla 于 2010 年 9 月 发布的,据说它包含了现实场景应用的片段/内核,并且与 SunSpider 相比少了一个微基准。我不想在 Kraken 上花太多口舌,因为我认为它不像 SunSpider 和 Octane 一样对 JavaScript 性能有着深远的影响,所以我将强调一个特别的案例——audio-oscillator.js 测试。

audio-oscillator.js

正如你所见,测试调用了 calcOsc 函数 500 次。calcOsc 首先在全局的 sine Oscillator 上调用 generate,然后创建一个新的 Oscillator,调用它的 generate 方法并将其添加到全局的 sine Oscillator 里。没有详细说明测试为什么是这样做的,让我们看看 Oscillator 原型上的 generate 方法。

audio-oscillator-data.js

让我们看看代码,你也许会觉得这里主要是循环中的数组访问或者乘法或者 Math.round 调用,但令人惊讶的是 offset % this.waveTableLength 表达式完全支配了 Oscillator.prototype.generate 的运行。在任何的英特尔机器上的分析器中运行此基准测试显示,超过 20% 的时间占用都属于我们为模数生成的 idiv 指令。然而一个有趣的发现是,Oscillator 实例的 waveTableLength 字段总是包含相同的值——2048,因为它在 Oscillator 构造器中只分配一次。

audio-oscillator-data.js

如果我们知道整数模数运算的右边是 2 的幂,我们显然可以生成更好的代码,完全避免了英特尔上的 idiv 指令。所以我们需要获取一种信息使 this.waveTableLengthOscillator 构造器到 Oscillator.prototype.generate 中的模运算都是 2048。一个显而易见的方法是尝试依赖于将所有内容内嵌到 calcOsc 函数,并让 load/store 消除为我们进行的常量传播,但这对于在 calcOsc 函数之外分配的 sine oscillator 无效。

因此,我们所做的就是添加支持跟踪某些常数值作为模运算符的右侧反馈。这在 V8 中是有意义的,因为我们为诸如 +*% 的二进制操作跟踪类型反馈,这意味着操作者跟踪输入的类型和产生的输出类型(参见最近的圆桌讨论中关于动态语言的快速运算的幻灯片)。当然,用 fullcodegenCrankshaft 挂接起来也是相当容易的,MODBinaryOpIC 也可以跟踪右边已知的 2 的冥。

$ ~/Projects/v8/out/Release/d8 --trace-ic audio-oscillator.js
[...SNIP...]
[BinaryOpIC(MOD:None*None->None) => (MOD:Smi*2048->Smi) @ ~Oscillator.generate+598 at audio-oscillator.js:697]
[...SNIP...]
$

事实上,以默认配置运行的 V8 (带有 Crankshaft 和 fullcodegen)表明 BinaryOpIC 正在为模数的右侧拾取适当的恒定反馈,并正确跟踪左侧始终是一个小整数(以 V8 的话叫做 Smi),我们也总是产生一个小整数结果。 使用 --print-opt-code -code-comments 查看生成的代码,很快就显示出,Crankshaft 利用反馈在 Oscillator.prototype.generate 中为整数模数生成一个有效的代码序列:

[...SNIP...]
                  ;;; <@80,#84> load-named-field
0x133a0bdacc4a   330  8b4343         movl rax,[rbx+0x43]
                  ;;; <@83,#86> compare-numeric-and-branch
0x133a0bdacc4d   333  3d00080000     cmp rax,0x800
0x133a0bdacc52   338  0f85ff000000   jnz 599  (0x133a0bdacd57)
[...SNIP...]
                  ;;; <@90,#94> mod-by-power-of-2-i
0x133a0bdacc5b   347  4585db         testl r11,r11
0x133a0bdacc5e   350  790f           jns 367  (0x133a0bdacc6f)
0x133a0bdacc60   352  41f7db         negl r11
0x133a0bdacc63   355  4181e3ff070000 andl r11,0x7ff
0x133a0bdacc6a   362  41f7db         negl r11
0x133a0bdacc6d   365  eb07           jmp 374  (0x133a0bdacc76)
0x133a0bdacc6f   367  4181e3ff070000 andl r11,0x7ff
[...SNIP...]
                  ;;; <@127,#88> deoptimize
0x133a0bdacd57   599  e81273cdff     call 0x133a0ba8406e
[...SNIP...]

所以你看到我们加载 this.waveTableLengthrbx 持有 this 的引用)的值,检查它仍然是 2048(十六进制的 0x800),如果是这样,就只用适当的掩码 0x7ff(r11 包含循环感应变量 i 的值)执行一个位操作 AND ,而不是使用 idiv 指令(注意保留左侧的符号)。

过度特定的问题

所以这个技巧酷毙了,但正如许多基准关注的技巧都有一个主要的缺点:太过于特定了!一旦右侧发生变化,所有优化过的代码就失去了优化(假设右手始终是不再处理的 2 的冥),任何进一步的优化尝试都必须再次使用 idiv,因为 BinaryOpIC 很可能以 Smi * Smi -> Smi 的形式报告反馈。例如,假设我们实例化另一个 Oscillator,在其上设置不同的 waveTableLength,并为 Oscillator 调用 generate,那么即使我们实际上感兴趣的 Oscillator 不受影响,我们也会损失 20% 的性能(例如,引擎在这里实行非局部惩罚)。

--- audio-oscillator.js.ORIG    2016-12-15 22:01:43.897033156 +0100
+++ audio-oscillator.js 2016-12-15 22:02:26.397326067 +0100
@@ -1931,6 +1931,10 @@
 var frequency = 344.53;
 var sine = new Oscillator(Oscillator.Sine, frequency, 1, bufferSize, sampleRate);

+var unused = new Oscillator(Oscillator.Sine, frequency, 1, bufferSize, sampleRate);
+unused.waveTableLength = 1024;
+unused.generate();
+
 var calcOsc = function() {
   sine.generate();

将原始的 audio-oscillator.js 执行时间与包含额外未使用的 Oscillator 实例与修改的 waveTableLength 的版本进行比较,显示的是预期的结果:

$ ~/Projects/v8/out/Release/d8 audio-oscillator.js.ORIG
Time (audio-oscillator-once): 64 ms.
$ ~/Projects/v8/out/Release/d8 audio-oscillator.js
Time (audio-oscillator-once): 81 ms.
$

这是一个非常可怕的性能悬崖的例子:假设开发人员编写代码库,并使用某些样本输入值进行仔细的调整和优化,性能是体面的。现在,用户读过了性能说明开始使用该库,但不知何故从性能悬崖下降,因为她/他正在以一种稍微不同的方式使用库,即特定的 BinaryOpIC 的某种污染方式的类型反馈,并且遭受 20% 的减速(与该库作者的测量相比),该库的作者和用户都无法解释,这似乎是随机的。

现在这种情况在 JavaScript 领域并不少见,不幸的是,这些悬崖中有几个是不可避免的,因为它们是由于 JavaScript 的性能是基于乐观的假设和猜测。我们已经花了 大量 时间和精力来试图找到避免这些性能悬崖的方法,而仍提供了(几乎)相同的性能。事实证明,尽可能避免 idiv 是很有意义的,即使你不一定知道右边总是一个 2 的幂(通过动态反馈),所以为什么 TurboFan 的做法有异于 Crankshaft 的做法,因为它总是在运行时检查输入是否是 2 的幂,所以一般情况下,对于有符整数模数,优化右手侧的(未知的) 2 的冥看起来像这样(伪代码):

if 0 < rhs then
  msk = rhs - 1
  if rhs & msk != 0 then
    lhs % rhs
  else
    if lhs < 0 then
      -(-lhs & msk)
    else
      lhs & msk
else
  if rhs < -1 then
    lhs % rhs
  else
    zero

这产生更加一致和可预测的性能(使用 TurboFan):

$ ~/Projects/v8/out/Release/d8 --turbo audio-oscillator.js.ORIG
Time (audio-oscillator-once): 69 ms.
$ ~/Projects/v8/out/Release/d8 --turbo audio-oscillator.js
Time (audio-oscillator-once): 69 ms.
$

基准和过度特定化的问题在于基准可以给你提示可以看看哪里以及该怎么做,但它不告诉你应该做到什么程度,不能保护合理优化。例如,所有 JavaScript 引擎都使用基准来防止性能回退,但是运行 Kraken 不能保护我们在 TurboFan 中使用的常规方法,即我们可以将 TurboFan 中的模优化降级到过度特定的版本的 Crankshaft,而基准不会告诉我们性能回退的事实,因为从基准的角度来看这很好!现在你可以扩展基准,也许以上面我们相同的方式,并试图用基准覆盖一切,这是引擎实现者在一定程度上做的事情,但这种方法不能任意缩放。即使基准测试方便,易于用来沟通和竞争,以常识所见你还是需要留下空间,否则过度特定化将支配一切,你会有一个真正的、非常好的可接受的性能,以及巨大的性能悬崖线。

Kraken 测试还有许多其它的问题,不过现在让我们继续讨论过去五年中最有影响力的 JavaScript 基准测试—— Octane 测试。

深入接触 Octane

Octane 基准是 V8 基准的继承者,最初由谷歌于 2012 年中期发布,目前的版本 Octane 2.0 于 2013 年年底发布。这个版本包含 15 个独立测试,其中对于 SplayMandreel,我们用来测试吞吐量和延迟。这些测试范围从 微软 TypeScript 编译器 编译自身到 zlib 测试测量原生的 asm.js 性能,再到 RegExp 引擎的性能测试、光线追踪器、2D 物理引擎等。有关各个基准测试项的详细概述,请参阅说明书。所有这些测试项目都经过仔细的筛选,以反映 JavaScript 性能的方方面面,我们认为这在 2012 年非常重要,或许预计在不久的将来会变得更加重要。

在很大程度上 Octane 在实现其将 JavaScript 性能提高到更高水平的目标方面无比的成功,它在 2012 年和 2013 年引导了良性的竞争,Octane 创造了巨大的业绩和成就。但是现在将近 2017 年了,世界看起来与 2012 年真的迥然不同了。除了通常和经常被引用的批评,Octane 中的大多数项目基本上已经过时(例如,老版本的 TypeScriptzlib 通过老版本的 Emscripten 编译而成,Mandreel 甚至不再可用等等),某种更重要的方式影响了 Octane 的用途:

我们看到大型 web 框架赢得了 web 种族之争,尤其是像 EmberAngularJS 这样的重型框架,它们使用了 JavaScript 执行模式,不过根本没有被 Octane 所反映,并且经常受到(我们)Octane 具体优化的损害。我们还看到 JavaScript 在服务器和工具前端获胜,这意味着有大规模的 JavaScript 应用现在通常运行上数星期,如果不是运行上数年都不会被 Octane 捕获。正如开篇所述,我们有硬数据表明 Octane 的执行和内存配置文件与我们每天在 web 上看到的截然不同。

让我们来看看今天一些玩弄 Octane 基准的具体例子,其中优化不再反映在现实场景。请注意,即使这可能听起来有点负面回顾,它绝对不意味着这样!正如我已经说过好几遍,Octane 是 JavaScript 性能故事中的重要一章,它发挥了至关重要的作用。在过去由 Octane 驱动的 JavaScript 引擎中的所有优化都是善意地添加的,因为 Octane 是现实场景性能的好代理!每个年代都有它的基准,而对于每一个基准都有一段时间你必须要放手!

话虽如此,让我们在路上看这个节目,首先看看 Box2D 测试,它是基于 Box2DWeb (一个最初由 Erin Catto 编写的移植到 JavaScript 的流行的 2D 物理引擎)的。总的来说,很多浮点数学驱动了很多 JavaScript 引擎下很好的优化,但是,事实证明它包含一个可以肆意玩弄基准的漏洞(怪我,我发现了漏洞,并添加在这种情况下的漏洞)。在基准中有一个函数 D.prototype.UpdatePairs,看起来像这样:

D.prototype.UpdatePairs = function(b) {
    var e = this;
    var f = e.m_pairCount = 0,
        m;
    for (f = 0; f < e.m_moveBuffer.length; ++f) {
        m = e.m_moveBuffer[f];
        var r = e.m_tree.GetFatAABB(m);
        e.m_tree.Query(function(t) {
                if (t == m) return true;
                if (e.m_pairCount == e.m_pairBuffer.length) e.m_pairBuffer[e.m_pairCount] = new O;
                var x = e.m_pairBuffer[e.m_pairCount];
                x.proxyA = t < m ? t : m;
                x.proxyB = t >= m ? t : m;
                ++e.m_pairCount;
                return true
            },
            r)
    }
    for (f = e.m_moveBuffer.length = 0; f < e.m_pairCount;) {
        r = e.m_pairBuffer[f];
        var s = e.m_tree.GetUserData(r.proxyA),
            v = e.m_tree.GetUserData(r.proxyB);
        b(s, v);
        for (++f; f < e.m_pairCount;) {
            s = e.m_pairBuffer[f];
            if (s.proxyA != r.proxyA || s.proxyB != r.proxyB) break;
            ++f
        }
    }
};

一些分析显示,在第一个循环中传递给 e.m_tree.Query 的无辜的内部函数花费了大量的时间:

function(t) {
    if (t == m) return true;
    if (e.m_pairCount == e.m_pairBuffer.length) e.m_pairBuffer[e.m_pairCount] = new O;
    var x = e.m_pairBuffer[e.m_pairCount];
    x.proxyA = t < m ? t : m;
    x.proxyB = t >= m ? t : m;
    ++e.m_pairCount;
    return true
}

更准确地说,时间并不是开销在这个函数本身,而是由此触发的操作和内置库函数。结果,我们花费了基准调用的总体执行时间的 4-7% 在 Compare` 运行时函数上,它实现了抽象关系比较的一般情况。

Box2D 比较分析

几乎所有对运行时函数的调用都来自 CompareICStub,它用于内部函数中的两个关系比较:

x.proxyA = t < m ? t : m;
x.proxyB = t >= m ? t : m;

所以这两行无辜的代码要负起 99% 的时间开销的责任!这怎么来的?好吧,与 JavaScript 中的许多东西一样,抽象关系比较 的直观用法不一定是正确的。在这个函数中,tm 都是 L 的实例,它是这个应用的一个中心类,但不会覆盖 Symbol.toPrimitive“toString”“valueOf”Symbol.toStringTag 属性,它们与抽象关系比较相关。所以如果你写 t < m 会发生什么呢?

  1. 调用 ToPrimitive(t, hint Number)。
  2. 运行 OrdinaryToPrimitive(t, "number"),因为这里没有 Symbol.toPrimitive
  3. 执行 t.valueOf(),这会获得 t 自身的值,因为它调用了默认的 Object.prototype.valueOf
  4. 接着执行 t.toString(),这会生成 "[object Object]",因为调用了默认的 Object.prototype.toString,并且没有找到 LSymbol.toStringTag
  5. 调用 ToPrimitive(m, hint Number)。
  6. 运行 OrdinaryToPrimitive(m, "number"),因为这里没有 Symbol.toPrimitive
  7. 执行 m.valueOf(),这会获得 m 自身的值,因为它调用了默认的 Object.prototype.valueOf
  8. 接着执行 m.toString(),这会生成 "[object Object]",因为调用了默认的 Object.prototype.toString,并且没有找到 LSymbol.toStringTag
  9. 执行比较 "[object Object]" < "[object Object]",结果是 false

至于 t >= m 亦复如是,它总会输出 true。所以这里是一个漏洞——使用抽象关系比较这种方法没有意义。而利用它的方法是使编译器常数折叠,即给基准打补丁:

--- octane-box2d.js.ORIG        2016-12-16 07:28:58.442977631 +0100
+++ octane-box2d.js     2016-12-16 07:29:05.615028272 +0100
@@ -2021,8 +2021,8 @@
                     if (t == m) return true;
                     if (e.m_pairCount == e.m_pairBuffer.length) e.m_pairBuffer[e.m_pairCount] = new O;
                     var x = e.m_pairBuffer[e.m_pairCount];
-                    x.proxyA = t < m ? t : m;
-                    x.proxyB = t >= m ? t : m;
+                    x.proxyA = m;
+                    x.proxyB = t;
                     ++e.m_pairCount;
                     return true
                 },

因为这样做会跳过比较以达到 13% 的惊人的性能提升,并且所有的属性查找和内置函数的调用都会被它触发。

$ ~/Projects/v8/out/Release/d8 octane-box2d.js.ORIG
Score (Box2D): 48063
$ ~/Projects/v8/out/Release/d8 octane-box2d.js
Score (Box2D): 55359
$

那么我们是怎么做呢?事实证明,我们已经有一种用于跟踪比较对象的形状的机制,比较发生于 CompareIC,即所谓的已知接收器映射跟踪(其中的映射是 V8 的对象形状+原型),不过这是有限的抽象和严格相等比较。但是我可以很容易地扩展跟踪,并且收集反馈进行抽象的关系比较:

$ ~/Projects/v8/out/Release/d8 --trace-ic octane-box2d.js
[...SNIP...]
[CompareIC in ~+557 at octane-box2d.js:2024 ((UNINITIALIZED+UNINITIALIZED=UNINITIALIZED)->(RECEIVER+RECEIVER=KNOWN_RECEIVER))#LT @ 0x1d5a860493a1]
[CompareIC in ~+649 at octane-box2d.js:2025 ((UNINITIALIZED+UNINITIALIZED=UNINITIALIZED)->(RECEIVER+RECEIVER=KNOWN_RECEIVER))#GTE @ 0x1d5a860496e1]
[...SNIP...]
$

这里基准代码中使用的 CompareIC 告诉我们,对于我们正在查看的函数中的 LT(小于)和 GTE(大于或等于)比较,到目前为止这只能看到 RECEIVERs(接收器,V8 的 JavaScript 对象),并且所有这些接收器具有相同的映射 0x1d5a860493a1,其对应于 L 实例的映射。因此,在优化的代码中,只要我们知道比较的两侧映射的结果都为 0x1d5a860493a1,并且没人混淆 L 的原型链(即 Symbol.toPrimitive"valueOf""toString" 这些方法都是默认的,并且没人赋予过 Symbol.toStringTag 的访问权限),我们可以将这些操作分别常量折叠为 falsetrue。剩下的故事都是关于 Crankshaft 的黑魔法,有很多一部分都是由于初始化的时候忘记正确地检查 Symbol.toStringTag 属性:

Hydrogen 黑魔法

最后,性能在这个特定的基准上有了质的飞跃:

Box2D 加速

我要声明一下,当时我并不相信这个特定的行为总是指向源代码中的漏洞,所以我甚至期望外部代码经常会遇到这种情况,同时也因为我假设 JavaScript 开发人员不会总是关心这些种类的潜在错误。但是,我大错特错了,在此我马上悔改!我不得不承认,这个特殊的优化纯粹是一个基准测试的东西,并不会有助于任何真实代码(除非代码是为了从这个优化中获益而写,不过以后你可以在代码中直接写入 truefalse,而不用再总是使用常量关系比较)。你可能想知道我们为什么在打补丁后又马上回滚了一下。这是我们整个团队投入到 ES2015 实施的非常时期,这才是真正的恶魔之舞,我们需要在没有严格的回归测试的情况下将所有新特性(ES2015 就是个怪兽)纳入传统基准。

关于 Box2D 点到为止了,让我们看看 Mandreel 基准。Mandreel 是一个用来将 C/C++ 代码编译成 JavaScript 的编译器,它并没有用上新一代的 Emscripten 编译器所使用,并且已经被弃用(或多或少已经从互联网消失了)大约三年的 JavaScript 子集 asm.js。然而,Octane 仍然有一个通过 Mandreel 编译的子弹物理引擎MandreelLatency 测试十分有趣,它测试 Mandreel 基准与频繁的时间测量检测点。有一种说法是,由于 Mandreel 强制使用虚拟机编译器,此测试提供了由编译器引入的延迟的指示,并且测量检测点之间的长时间停顿降低了最终得分。这听起来似乎合情合理,确实有一定的意义。然而,像往常一样,供应商找到了在这个基准上作弊的方法。

Mozilla 1162272 漏洞

Mandreel 自带一个重型初始化函数 global_init,光是解析这个函数并为其生成基线代码就花费了不可思议的时间。因为引擎通常在脚本中多次解析各种函数,一个所谓的预解析步骤用来发现脚本内的函数。然后作为函数第一次被调用完整的解析步骤以生成基线代码(或者说字节码)。这在 V8 中被称为懒解析。V8 有一些启发式检测函数,当预解析浪费时间的时候可以立刻调用,不过对于 Mandreel 基准的 global_init 函数就不太清楚了,于是我们将经历这个大家伙“预解析+解析+编译”的长时间停顿。所以我们添加了一个额外的启发式函数以避免 global_init 函数的预解析。

MandreelLatency 基准

由此可见,在检测 global_init 和避免昂贵的预解析步骤我们几乎提升了 2 倍。我们不太确定这是否会对真实用例产生负面影响,不过保证你在预解析大函数的时候将会受益匪浅(因为它们不会立即执行)。

让我们来看看另一个稍有争议的基准测试:splay.js 测试,一个用于处理 伸展树 splay tree (二叉查找树的一种)和练习自动内存管理子系统(也被称为垃圾回收器)的数据操作基准。它自带一个延迟测试,这会引导 Splay 代码通过频繁的测量检测点,检测点之间的长时间停顿表明垃圾回收器的延迟很高。此测试测量延迟暂停的频率,将它们分类到桶中,并以较低的分数惩罚频繁的长暂停。这听起来很棒!没有 GC 停顿,没有垃圾。纸上谈兵到此为止。让我们看看这个基准,以下是整个伸展树业务的核心:

splay.js

这是伸展树结构的核心构造,尽管你可能想看完整的基准,不过这基本上是 SplayLatency 得分的重要来源。怎么回事?实际上,该基准测试是建立巨大的伸展树,尽可能保留所有节点,从而还原它原本的空间。使用像 V8 这样的代数垃圾回收器,如果程序违反了代数假设,会导致极端的时间停顿,从本质上看,将所有东西从新空间撤回到旧空间的开销是非常昂贵的。在旧配置中运行 V8 可以清楚地展示这个问题:

$ out/Release/d8 --trace-gc --noallocation_site_pretenuring octane-splay.js
[20872:0x7f26f24c70d0]       10 ms: Scavenge 2.7 (6.0) -> 2.7 (7.0) MB, 1.1 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]       12 ms: Scavenge 2.7 (7.0) -> 2.7 (8.0) MB, 1.7 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]       14 ms: Scavenge 3.7 (8.0) -> 3.6 (10.0) MB, 0.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]       18 ms: Scavenge 4.8 (10.5) -> 4.7 (11.0) MB, 2.5 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]       22 ms: Scavenge 5.7 (11.0) -> 5.6 (16.0) MB, 2.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]       28 ms: Scavenge 8.7 (16.0) -> 8.6 (17.0) MB, 4.3 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]       35 ms: Scavenge 9.6 (17.0) -> 9.6 (28.0) MB, 6.9 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]       49 ms: Scavenge 16.6 (28.5) -> 16.4 (29.0) MB, 8.2 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]       65 ms: Scavenge 17.5 (29.0) -> 17.5 (52.0) MB, 15.3 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]       93 ms: Scavenge 32.3 (52.5) -> 32.0 (53.5) MB, 17.6 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      126 ms: Scavenge 33.4 (53.5) -> 33.3 (68.0) MB, 31.5 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      151 ms: Scavenge 47.9 (68.0) -> 47.6 (69.5) MB, 15.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      183 ms: Scavenge 49.2 (69.5) -> 49.2 (84.0) MB, 30.9 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      210 ms: Scavenge 63.5 (84.0) -> 62.4 (85.0) MB, 14.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      241 ms: Scavenge 64.7 (85.0) -> 64.6 (99.0) MB, 28.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      268 ms: Scavenge 78.2 (99.0) -> 77.6 (101.0) MB, 16.1 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      298 ms: Scavenge 80.4 (101.0) -> 80.3 (114.5) MB, 28.2 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      324 ms: Scavenge 93.5 (114.5) -> 92.9 (117.0) MB, 16.4 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      354 ms: Scavenge 96.2 (117.0) -> 96.0 (130.0) MB, 27.6 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      383 ms: Scavenge 108.8 (130.0) -> 108.2 (133.0) MB, 16.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      413 ms: Scavenge 111.9 (133.0) -> 111.7 (145.5) MB, 27.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      440 ms: Scavenge 124.1 (145.5) -> 123.5 (149.0) MB, 17.4 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      473 ms: Scavenge 127.6 (149.0) -> 127.4 (161.0) MB, 29.5 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      502 ms: Scavenge 139.4 (161.0) -> 138.8 (165.0) MB, 18.7 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      534 ms: Scavenge 143.3 (165.0) -> 143.1 (176.5) MB, 28.5 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      561 ms: Scavenge 154.7 (176.5) -> 154.2 (181.0) MB, 19.0 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      594 ms: Scavenge 158.9 (181.0) -> 158.7 (192.0) MB, 29.2 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      622 ms: Scavenge 170.0 (192.5) -> 169.5 (197.0) MB, 19.5 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      655 ms: Scavenge 174.6 (197.0) -> 174.3 (208.0) MB, 28.7 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      683 ms: Scavenge 185.4 (208.0) -> 184.9 (212.5) MB, 19.4 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      715 ms: Scavenge 190.2 (213.0) -> 190.0 (223.5) MB, 27.7 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      743 ms: Scavenge 200.7 (223.5) -> 200.3 (228.5) MB, 19.7 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      774 ms: Scavenge 205.8 (228.5) -> 205.6 (239.0) MB, 27.1 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      802 ms: Scavenge 216.1 (239.0) -> 215.7 (244.5) MB, 19.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      833 ms: Scavenge 221.4 (244.5) -> 221.2 (254.5) MB, 26.2 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      861 ms: Scavenge 231.5 (255.0) -> 231.1 (260.5) MB, 19.9 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      892 ms: Scavenge 237.0 (260.5) -> 236.7 (270.5) MB, 26.3 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      920 ms: Scavenge 246.9 (270.5) -> 246.5 (276.0) MB, 20.1 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      951 ms: Scavenge 252.6 (276.0) -> 252.3 (286.0) MB, 25.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      979 ms: Scavenge 262.3 (286.0) -> 261.9 (292.0) MB, 20.3 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1014 ms: Scavenge 268.2 (292.0) -> 267.9 (301.5) MB, 29.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1046 ms: Scavenge 277.7 (302.0) -> 277.3 (308.0) MB, 22.4 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1077 ms: Scavenge 283.8 (308.0) -> 283.5 (317.5) MB, 25.1 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1105 ms: Scavenge 293.1 (317.5) -> 292.7 (323.5) MB, 20.7 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1135 ms: Scavenge 299.3 (323.5) -> 299.0 (333.0) MB, 24.9 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1164 ms: Scavenge 308.6 (333.0) -> 308.1 (339.5) MB, 20.9 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1194 ms: Scavenge 314.9 (339.5) -> 314.6 (349.0) MB, 25.0 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1222 ms: Scavenge 324.0 (349.0) -> 323.6 (355.5) MB, 21.1 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1253 ms: Scavenge 330.4 (355.5) -> 330.1 (364.5) MB, 25.1 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1282 ms: Scavenge 339.4 (364.5) -> 339.0 (371.0) MB, 22.2 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1315 ms: Scavenge 346.0 (371.0) -> 345.6 (380.0) MB, 25.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1413 ms: Mark-sweep 349.9 (380.0) -> 54.2 (305.0) MB, 5.8 / 0.0 ms  (+ 87.5 ms in 73 steps since start of marking, biggest step 8.2 ms, walltime since start of marking 131 ms) finalize incremental marking via stack guard GC in old space requested
[20872:0x7f26f24c70d0]     1457 ms: Scavenge 65.8 (305.0) -> 65.1 (305.0) MB, 31.0 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1489 ms: Scavenge 69.9 (305.0) -> 69.7 (305.0) MB, 27.1 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1523 ms: Scavenge 80.9 (305.0) -> 80.4 (305.0) MB, 22.9 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1553 ms: Scavenge 85.5 (305.0) -> 85.3 (305.0) MB, 24.2 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1581 ms: Scavenge 96.3 (305.0) -> 95.7 (305.0) MB, 18.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1616 ms: Scavenge 101.1 (305.0) -> 100.9 (305.0) MB, 29.2 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1648 ms: Scavenge 111.6 (305.0) -> 111.1 (305.0) MB, 22.5 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1678 ms: Scavenge 116.7 (305.0) -> 116.5 (305.0) MB, 25.0 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1709 ms: Scavenge 127.0 (305.0) -> 126.5 (305.0) MB, 20.7 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1738 ms: Scavenge 132.3 (305.0) -> 132.1 (305.0) MB, 23.9 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1767 ms: Scavenge 142.4 (305.0) -> 141.9 (305.0) MB, 19.6 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1796 ms: Scavenge 147.9 (305.0) -> 147.7 (305.0) MB, 23.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1825 ms: Scavenge 157.8 (305.0) -> 157.3 (305.0) MB, 19.9 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1853 ms: Scavenge 163.5 (305.0) -> 163.2 (305.0) MB, 22.2 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1881 ms: Scavenge 173.2 (305.0) -> 172.7 (305.0) MB, 19.1 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1910 ms: Scavenge 179.1 (305.0) -> 178.8 (305.0) MB, 23.0 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1944 ms: Scavenge 188.6 (305.0) -> 188.1 (305.0) MB, 25.1 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1979 ms: Scavenge 194.7 (305.0) -> 194.4 (305.0) MB, 28.4 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     2011 ms: Scavenge 204.0 (305.0) -> 203.6 (305.0) MB, 23.4 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     2041 ms: Scavenge 210.2 (305.0) -> 209.9 (305.0) MB, 23.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     2074 ms: Scavenge 219.4 (305.0) -> 219.0 (305.0) MB, 24.5 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     2105 ms: Scavenge 225.8 (305.0) -> 225.4 (305.0) MB, 24.7 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     2138 ms: Scavenge 234.8 (305.0) -> 234.4 (305.0) MB, 23.1 / 0.0 ms  allocation failure
[...SNIP...]
$

因此这里关键的发现是直接在旧空间中分配伸展树节点可基本避免在周围复制对象的所有开销,并且将次要 GC 周期的数量减少到最小(从而减少 GC 引起的停顿时间)。我们想出了一种称为 分配场所预占 allocation site pretenuring 的机制,当运行到基线代码时,将尝试动态收集分配场所的反馈,以决定在此分配的对象的确切部分是否存在,如果是,则优化代码以直接在旧空间分配对象——即预占对象。

$ out/Release/d8 --trace-gc octane-splay.js
[20885:0x7ff4d7c220a0]        8 ms: Scavenge 2.7 (6.0) -> 2.6 (7.0) MB, 1.2 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]       10 ms: Scavenge 2.7 (7.0) -> 2.7 (8.0) MB, 1.6 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]       11 ms: Scavenge 3.6 (8.0) -> 3.6 (10.0) MB, 0.9 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]       17 ms: Scavenge 4.8 (10.5) -> 4.7 (11.0) MB, 2.9 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]       20 ms: Scavenge 5.6 (11.0) -> 5.6 (16.0) MB, 2.8 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]       26 ms: Scavenge 8.7 (16.0) -> 8.6 (17.0) MB, 4.5 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]       34 ms: Scavenge 9.6 (17.0) -> 9.5 (28.0) MB, 6.8 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]       48 ms: Scavenge 16.6 (28.5) -> 16.4 (29.0) MB, 8.6 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]       64 ms: Scavenge 17.5 (29.0) -> 17.5 (52.0) MB, 15.2 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]       96 ms: Scavenge 32.3 (52.5) -> 32.0 (53.5) MB, 19.6 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]      153 ms: Scavenge 61.3 (81.5) -> 57.4 (93.5) MB, 27.9 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]      432 ms: Scavenge 339.3 (364.5) -> 326.6 (364.5) MB, 12.7 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]      666 ms: Scavenge 563.7 (592.5) -> 553.3 (595.5) MB, 20.5 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]      825 ms: Mark-sweep 603.9 (644.0) -> 96.0 (528.0) MB, 4.0 / 0.0 ms  (+ 92.5 ms in 51 steps since start of marking, biggest step 4.6 ms, walltime since start of marking 160 ms) finalize incremental marking via stack guard GC in old space requested
[20885:0x7ff4d7c220a0]     1068 ms: Scavenge 374.8 (528.0) -> 362.6 (528.0) MB, 19.1 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]     1304 ms: Mark-sweep 460.1 (528.0) -> 102.5 (444.5) MB, 10.3 / 0.0 ms  (+ 117.1 ms in 59 steps since start of marking, biggest step 7.3 ms, walltime since start of marking 200 ms) finalize incremental marking via stack guard GC in old space requested
[20885:0x7ff4d7c220a0]     1587 ms: Scavenge 374.2 (444.5) -> 361.6 (444.5) MB, 13.6 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]     1828 ms: Mark-sweep 485.2 (520.0) -> 101.5 (519.5) MB, 3.4 / 0.0 ms  (+ 102.8 ms in 58 steps since start of marking, biggest step 4.5 ms, walltime since start of marking 183 ms) finalize incremental marking via stack guard GC in old space requested
[20885:0x7ff4d7c220a0]     2028 ms: Scavenge 371.4 (519.5) -> 358.5 (519.5) MB, 12.1 / 0.0 ms  allocation failure
[...SNIP...]
$

事实上,这完全解决了 SplayLatency 基准的问题,并提高我们的得分至超过 250%!

SplayLatency 基准

正如 SIGPLAN 论文 中所提及的,我们有充分的理由相信,分配场所预占机制可能真的赢得了真实世界应用的欢心,并真正期待看到改进和扩展后的机制,那时将不仅仅是对象和数组字面量。但是不久后我们意识到分配场所预占机制对真实世界应用产生了相当严重的负面影响。我们实际上听到很多负面报道,包括 Ember.js 开发者和用户的唇枪舌战,虽然不仅是因为分配场所预占机制,不过它是事故的罪魁祸首。

分配场所预占机制的基本问题数之不尽,这在今天的应用中非常常见(主要是由于框架,同时还有其它原因),假设你的对象工厂最初是用于创建构成你的对象模型和视图的长周期对象的,它将你的工厂方法中的分配场所转换为永久状态,并且从工厂分配的所有内容都立即转到旧空间。现在初始设置完成后,你的应用开始工作,作为其中的一部分,从工厂分配临时对象会污染旧空间,最终导致开销昂贵的垃圾回收周期以及其它负面的副作用,例如过早触发增量标记。

我们开始重新考虑基准驱动的工作,并开始寻找现实场景驱动的替代方案,这导致了 Orinoco 的诞生,它的目标是逐步改进垃圾回收器;这个努力的一部分是一个称为“ 统一堆 unified heap ”的项目,如果页面中所有内容基本都存在,它将尝试避免复制对象。也就是说站在更高的层面看:如果新空间充满活动对象,只需将所有新空间页面标记为属于旧空间,然后从空白页面创建一个新空间。这可能不会在 SplayLatency 基准测试中得到相同的分数,但是这对于真实用例更友好,它可以自动适配具体的用例。我们还考虑 并发标记 concurrent marking ,将标记工作卸载到单独的线程,从而进一步减少增量标记对延迟和吞吐量的负面影响。

轻松一刻

喘口气。

好吧,我想这足以强调我的观点了。我可以继续指出更多的例子,其中 Octane 驱动的改进后来变成了一个坏主意,也许改天我会接着写下去。但是今天就到此为止了吧。

结论

我希望现在应该清楚为什么基准测试通常是一个好主意,但是只对某个特定的级别有用,一旦你跨越了 有用竞争 useful competition 的界限,你就会开始浪费你们工程师的时间,甚至开始损害到你的真实世界的性能!如果我们认真考虑 web 的性能,我们需要根据真实世界的性能来测评浏览器,而不是它们玩弄一个四年前的基准的能力。我们需要开始教育(技术)媒体,可能这没用,但至少请忽略他们。

2016 年 10 月浏览器基准之战: Chrome、Firefox 和 Edge 的决战

没人害怕竞争,但是玩弄可能已经坏掉的基准不像是在合理使用工程时间。我们可以尽更大的努力,并把 JavaScript 提高到更高的水平。让我们开展有意义的性能测试,以便为最终用户和开发者带来有意思的领域竞争。此外,让我们再对运行在 Node.js( V8 或 ChakraCore)中的服务器端和工具端代码做一些有意义的改进!

结束语:不要用传统的 JavaScript 基准来比较手机。这是真正最没用的事情,因为 JavaScript 的性能通常取决于软件,而不一定是硬件,并且 Chrome 每 6 周发布一个新版本,所以你在三月份的测试结果到了四月份就已经毫不相关了。如果为手机中的浏览器做个排名不可避免,那么至少请使用一个现代健全的浏览器基准来测试,至少这个基准要知道人们会用浏览器来干什么,比如 Speedometer 基准

感谢你花时间阅读!


作者简介:

我是 Benedikt Meurer,住在 Ottobrunn(德国巴伐利亚州慕尼黑东南部的一个市镇)的一名软件工程师。我于 2007 年在锡根大学获得应用计算机科学与电气工程的文凭,打那以后的 5 年里我在编译器和软件分析领域担任研究员(2007 至 2008 年间还研究过微系统设计)。2013 年我加入了谷歌的慕尼黑办公室,我的工作目标主要是 V8 JavaScript 引擎,目前是 JavaScript 执行性能优化团队的一名技术领导。


via: http://benediktmeurer.de/2016/12/16/the-truth-about-traditional-javascript-benchmarks

作者:Benedikt Meurer 译者:OneNewLife 校对:OneNewLife, wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

下文是 James Henry(@MrJamesHenry)所提交的内容。我是 ESLint 核心团队的一员,也是 TypeScript 布道师。我正在和 Todd 在 UltimateAngular 平台上合作发布 Angular 和 TypeScript 的精品课程。

本文的主旨是为了介绍我们是如何看待 TypeScript 的以及它在加强 JavaScript 开发中所起的作用。

我们也将尽可能地给出那些类型和编译方面的那些时髦词汇的准确定义。

TypeScript 强大之处远远不止这些,本篇文章无法涵盖,想要了解更多请阅读官方文档,或者学习 UltimateAngular 上的 TypeScript 课程 ,从初学者成为一位 TypeScript 高手。

背景

TypeScript 是个出乎意料强大的工具,而且它真的很容易掌握。

然而,TypeScript 可能比 JavaScript 要更为复杂一些,因为 TypeScript 可能向我们同时引入了一系列以前没有考虑过的 JavaScript 程序相关的技术概念。

每当我们谈论到类型、编译器等这些概念的时候,你会发现很快会变的不知所云起来。

这篇文章就是一篇为了解答你需要知道的许许多多不知所云的概念,来帮助你 TypeScript 快速入门的教程,可以让你轻松自如的应对这些概念。

关键知识的掌握

在 Web 浏览器中运行我们的代码这件事或许使我们对它是如何工作的产生一些误解,“它不用经过编译,是吗?”,“我敢肯定这里面是没有类型的...”

更有意思的是,上述的说法既是正确的也是不正确的,这取决于上下文环境和我们是如何定义这些概念的。

首先,我们要作的是明确这些。

JavaScript 是解释型语言还是编译型语言?

传统意义上,程序员经常将自己的程序编译之后运行出结果就认为这种语言是编译型语言。

从初学者的角度来说,编译的过程就是将我们自己编辑好的高级语言程序转换成机器实际运行的格式。

就像 Go 语言,可以使用 go build 的命令行工具编译 .go 的文件,将其编译成代码的低级形式,它可以直接执行、运行。

# We manually compile our .go file into something we can run
# using the command line tool "go build"
go build ultimate-angular.go
# ...then we execute it!
./ultimate-angular

作为一个 JavaScript 程序员(这一刻,请先忽略我们对新一代构建工具和模块加载程序的热爱),我们在日常的 JavaScript 开发中并没有编译的这一基本步骤,

我们写一些 JavaScript 代码,把它放在浏览器的 <script> 标签中,它就能运行了(或者在服务端环境运行,比如:node.js)。

好吧,因此 JavaScript 没有进行过编译,那它一定是解释型语言了,是吗?

实际上,我们能够确定的一点是,JavaScript 不是我们自己编译的,现在让我们简单的回顾一个简单的解释型语言的例子,再来谈 JavaScript 的编译问题。

解释型计算机语言的执行的过程就像人们看书一样,从上到下、一行一行的阅读。

我们所熟知的解释型语言的典型例子是 bash 脚本。我们终端中的 bash 解释器逐行读取我们的命令并且执行它。

现在我们回到 JavaScript 是解释执行还是编译执行的讨论中,我们要将逐行读取和逐行执行程序分开理解(对“解释型”的简单理解),不要混在一起。

以此代码为例:

hello();
function hello(){
    console.log("Hello")
}

这是真正意义上 JavaScript 输出 Hello 单词的程序代码,但是,在 hello() 在我们定义它之前就已经使用了这个函数,这是简单的逐行执行办不到的,因为 hello() 在第一行没有任何意义的,直到我们在第二行声明了它。

像这样在 JavaScript 是存在的,因为我们的代码实际上在执行之前就被所谓的“JavaScript 引擎”或者是“特定的编译环境”编译过,这个编译的过程取决于具体的实现(比如,使用 V8 引擎的 node.js 和 Chome 就和使用 SpiderMonkey 的 FireFox 就有所不同)。

在这里,我们不会在进一步的讲解编译型执行和解释型执行微妙之处(这里的定义已经很好了)。

请务必记住,我们编写的 JavaScript 代码已经不是我们的用户实际执行的代码了,即使是我们简单地将其放在 HTML 中的 <script> ,也是不一样的。

运行时间 VS 编译时间

现在我们已经正确理解了编译和运行是两个不同的阶段,那“ 运行阶段 Run Time ”和“ 编译阶段 Compile Time ”理解起来也就容易多了。

编译阶段,就是我们在我们的编辑器或者 IDE 当中的代码转换成其它格式的代码的阶段。

运行阶段,就是我们程序实际执行的阶段,例如:上面的 hello() 函数就执行在“运行阶段”。

TypeScript 编译器

现在我们了解了程序的生命周期中的关键阶段,接下来我们可以介绍 TypeScript 编译器了。

TypeScript 编译器是帮助我们编写代码的关键。比如,我们不需将 JavaScript 代码包含到 <script> 标签当中,只需要通过 TypeScript 编译器传递它,就可以在运行程序之前得到改进程序的建议。

我们可以将这个新的步骤作为我们自己的个人“编译阶段”,这将在我们的程序抵达 JavaScript 主引擎之前,确保我们的程序是以我们预期的方式编写的。

它与上面 Go 语言的实例类似,但是 TypeScript 编译器只是基于我们编写程序的方式提供提示信息,并不会将其转换成低级的可执行文件,它只会生成纯 JavaScript 代码。

# One option for passing our source .ts file through the TypeScript
# compiler is to use the command line tool "tsc"
tsc ultimate-angular.ts

# ...this will produce a .js file of the same name
# i.e. ultimate-angular.js

官方文档中,有许多关于将 TypeScript 编译器以各种方式融入到你的现有工作流程中的文章。这些已经超出本文范围。

动态类型与静态类型

就像对比编译程序与解释程序一样,动态类型与静态类型的对比在现有的资料中也是极其模棱两可的。

让我们先回顾一下我们在 JavaScript 中对于类型的理解。

我们的代码如下:

var name = 'James';
var sum = 1 + 2;

我们如何给别人描述这段代码?

“我们声明了一个变量 name,它被分配了一个 “James” 的字符串,然后我们又申请了一个变量 sum,它被分配了一个数字 1 和数字 2 的求和的数值结果。”

即使在这样一个简单的程序中,我们也使用了两个 JavaScript 的基本类型:StringNumber

就像上面我们讲编译一样,我们不会陷入编程语言类型的学术细节当中,关键是要理解在 JavaScript 中类型表示的是什么,并扩展到 TypeScript 的类型的理解上。

从每夜拜读的最新 ECMAScript 规范中我们可以学到(LOL, JK - “wat’s an ECMA?”),它大量引用了 JavaScript 的类型及其用法。

直接引自官方规范:

ECMAScript 语言的类型取决于使用 ECMAScript 语言的 ECMAScript 程序员所直接操作的值。

ECMAScript 语言的类型有 Undefined、Null、Boolean、String、Symbol、Number 和 Object。

我们可以看到,JavaScript 语言有 7 种正式类型,其中我们在我们现在程序中使用了 6 种(Symbol 首次在 ES2015 中引入,也就是 ES6)。

现在我们来深入一点看上面的 JavaScript 代码中的 “name 和 sum”。

我们可以把我们当前被分配了字符串“James”的变量 name 重新赋值为我们的第二个变量 sum 的当前值,目前是数字 3。

var name = 'James';
var sum = 1 + 2;

name = sum;

name 变量开始“存有”一个字符串,但现在它“存有”一个数字。这凸显了 JavaScript 中变量和类型的基本特性:

“James” 值一直是字符串类型,而 name 变量可以分配任何类型的值。和 sum 赋值的情况相同,1 是一个数字类型,sum 变量可以分配任何可能的值。

在 JavaScript 中,值是具有类型的,而变量是可以随时保存任何类型的值。

这也恰好是一个“动态类型语言”的定义。

相比之下,我们可以将“静态类型语言”视为我们可以(也必须)将类型信息与特定变量相关联的语言:

var name: string = ‘James’;

在这段代码中,我们能够更好地显式声明我们对变量 name 的意图,我们希望它总是用作一个字符串。

你猜怎么着?我们刚刚看到我们的第一个 TypeScript 程序。

当我们 反思reflect我们自己的代码(非编程方面的双关语“反射”)时,我们可以得出的结论,即使我们使用动态语言(如 JavaScript),在几乎所有的情况下,当我们初次定义变量和函数参数时,我们应该有非常明确的使用意图。如果这些变量和参数被重新赋值为与我们原先赋值不同类型的值,那么有可能某些东西并不是我们预期的那样工作的。

作为 JavaScript 开发者,TypeScript 的静态类型注释给我们的一个巨大的帮助,它能够清楚地表达我们对变量的意图。

这种改进不仅有益于 TypeScript 编译器,还可以让我们的同事和将来的自己明白我们的代码。代码是用来读的。

TypeScript 在我们的 JavaScript 工作流程中的作用

我们已经开始看到“为什么经常说 TypeScript 只是 JavaScript + 静态类型”的说法了。: string 对于我们的 name 变量就是我们所谓的“类型注释”,在编译时被使用(换句话说,当我们让代码通过 TypeScript 编译器时),以确保其余的代码符合我们原来的意图。

我们再来看看我们的程序,并添加显式注释,这次是我们的 sum 变量:

var name: string = 'James';
var sum: number = 1 + 2;

name = sum;

如果我们使用 TypeScript 编译器编译这个代码,我们现在就会收到一个在 name = sum 这行的错误: Type 'number' is not assignable to type 'string',我们的这种“偷渡”被警告,我们执行的代码可能有问题。

重要的是,如果我们想要继续执行,我们可以选择忽略 TypeScript 编译器的错误,因为它只是在将 JavaScript 代码发送给我们的用户之前给我们反馈的工具。

TypeScript 编译器为我们输出的最终 JavaScript 代码将与上述原始源代码完全相同:

var name = 'James';
var sum = 1 + 2;

name = sum;

类型注释全部为我们自动删除了,现在我们可以运行我们的代码了。

注意:在此示例中,即使我们没有提供显式类型注释的 : string: number ,TypeScript 编译器也可以为我们提供完全相同的错误 。

TypeScript 通常能够从我们使用它的方式推断变量的类型!

我们的源文件是我们的文档,TypeScript 是我们的拼写检查

对于 TypeScript 与我们的源代码的关系来说,一个很好的类比,就是拼写检查与我们在 Microsoft Word 中写的文档的关系。

这两个例子有三个关键的共同点:

  1. 它能告诉我们写的东西的客观的、直接的错误:

    • 拼写检查:“我们已经写了字典中不存在的字”
    • TypeScript:“我们引用了一个符号(例如一个变量),它没有在我们的程序中声明”
  2. 它可以提醒我们写的可能是错误的:

    • 拼写检查:“该工具无法完全推断特定语句的含义,并建议重写”
    • TypeScript:“该工具不能完全推断特定变量的类型,并警告不要这样使用它”
  3. 我们的来源可以用于其原始目的,无论工具是否存在错误:

    • 拼写检查:“即使您的文档有很多拼写错误,您仍然可以打印出来,并把它当成文档使用”
    • TypeScript:“即使您的源代码具有 TypeScript 错误,它仍然会生成您可以执行的 JavaScript 代码”

TypeScript 是一种可以启用其它工具的工具

TypeScript 编译器由几个不同的部分或阶段组成。我们将通过查看这些部分之一 The Parser(语法分析程序)来结束这篇文章,除了 TypeScript 已经为我们做的以外,它为我们提供了在其上构建其它开发工具的机会。

编译过程的“解析器步骤”的结果是所谓的抽象语法树,简称为 AST。

什么是抽象语法树(AST)?

我们以普通文本形式编写我们的程序,因为这是我们人类与计算机交互的最好方式,让它们能够做我们想要的东西。我们并不是很擅长于手工编写复杂的数据结构!

然而,不管在哪种情况下,普通文本在编译器里面实际上是一个非常棘手的事情。它可能包含程序运作不必要的东西,例如空格,或者可能存在有歧义的部分。

因此,我们希望将我们的程序转换成数据结构,将数据结构全部映射为我们所使用的所谓“标记”,并将其插入到我们的程序中。

这个数据结构正是 AST!

AST 可以通过多种不同的方式表示,我使用 JSON 来看一看。

我们从这个极其简单的基本源代码来看:

var a = 1;

TypeScript 编译器的 Parser(语法分析程序)阶段的(简化后的)输出将是以下 AST:

{
  "pos": 0,
  "end": 10,
  "kind": 256,
  "text": "var a = 1;",
  "statements": [
    {
      "pos": 0,
      "end": 10,
      "kind": 200,
      "declarationList": {
        "pos": 0,
        "end": 9,
        "kind": 219,
        "declarations": [
          {
            "pos": 3,
            "end": 9,
            "kind": 218,
            "name": {
              "pos": 3,
              "end": 5,
              "text": "a"
            },
            "initializer": {
              "pos": 7,
              "end": 9,
              "kind": 8,
              "text": "1"
            }
          }
        ]
      }
    }
  ]
}

我们的 AST 中的对象称为节点。

示例:在 VS Code 中重命名符号

在内部,TypeScript 编译器将使用 Parser 生成的 AST 来提供一些非常重要的事情,例如,发生在编译程序时的类型检查。

但它不止于此!

我们可以使用 AST 在 TypeScript 之上开发自己的工具,如代码美化工具、代码格式化工具和分析工具。

建立在这个 AST 代码之上的工具的一个很好的例子是: 语言服务器 Language Server

深入了解语言服务器的工作原理超出了本文的范围,但是当我们编写程序时,它能为我们提供一个绝对重量级别功能,就是“重命名符号”。

假设我们有以下源代码:

// The name of the author is James
var first_name = 'James';
console.log(first_name);

经过代码审查和对完美的适当追求,我们决定应该改换我们的变量命名惯例;使用驼峰式命名方式,而不是我们当前正在使用这种蛇式命名。

在我们的代码编辑器中,我们一直以来可以选择多个相同的文本,并使用多个光标来一次更改它们。

Manually select matches

当我们把程序也视作文本这样继续操作时,我们已经陷入了一个典型的陷阱中。

那个注释中我们不想修改的“name”单词,在我们的手动匹配中却被误选中了。我们可以看到在现实世界的应用程序中这样更改代码是有多危险。

正如我们在上面学到的那样,像 TypeScript 这样的东西在幕后生成一个 AST 的时候,与我们的程序不再像普通文本那样可以交互,每个标记在 AST 中都有自己的位置,而且它有很清晰的映射关系。

当我们右键单击我们的 first_name 变量时,我们可以在 VS Code 中直接“重命名符号”(TypeScript 语言服务器插件也可用于其他编辑器)。

Rename Symbol Example

非常好!现在我们的 first_name 变量是唯一需要改变的东西,如果需要的话,这个改变甚至会发生在我们项目中的多个文件中(与导出和导入的值一样)!

总结

哦,我们在这篇文章中已经讲了很多的内容。

我们把有关学术方面的规避开,围绕编译器和类型还有很多专业术语给出了通俗的定义。

我们对比了编译语言与解释语言、运行阶段与编译阶段、动态类型与静态类型,以及抽象语法树(AST)如何为我们的程序构建工具提供了更为优化的方法。

重要的是,我们提供了 TypeScript 作为我们 JavaScript 开发工具的一种思路,以及如何在其上构建更棒的工具,比如说作为重构代码的一种方式的重命名符号。

快来 UltimateAngular 平台上学习从初学者到 TypeScript 高手的课程吧,开启你的学习之旅!


via: https://toddmotto.com/typescript-the-missing-introduction

作者:James Henry 译者:MonkeyDEcho 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

一个有价值的命题

我们遇到了一个临界点。除去少数几个特别的的用例之外,使用原生框架和原生应用开发团队构建、维护移动应用再也没有意义了。

在美国,雇佣 iOS,AndroidJavaScript 开发人员的平均花费

在过去的几年,原生移动应用开发的费用螺旋式上升,无法控制。对没有大量资金的新创业者来说,创建原生应用、MVP 设计架构和原型的难度大大增加。现有的公司需要抓住人才,以便在现有应用上进行迭代开发或者构建一个新的应用。要尽一切努力才能留住最好的人才,与 世界各地的公司 拼尽全力争个

2015 年初,原生方式和混合方式开发 MVP 设计架构的费用对比

这一切对于我们意味着什么?

如果你的公司很大或者有足够多的现金,旧思维是只要你在原生应用开发方面投入足够多的资金,就高枕无忧。但事实不再如此。

Facebook 是你最不会想到的在人才战中失败的公司(因为他们没有失败),它也遇到了原生应用方面金钱无法解决的问题。他们的移动应用庞大而又复杂,他们发现编译它竟然需要 15 分钟。这意味着哪怕是极小的用户界面改动,比如移动几个点,测试起来都要花费几个小时(甚至几天)。

除了冗长的编译时间,应用的每一个小改动在测试时都需要在两个完全不同的环境(IOS 和 Android)实施,开发团队需要使用两种语言和框架工作,这趟水更浑了。

Facebook 对这个问题的解决方案是 React Native

能不能抛弃移动应用,仅面向 Web 呢?

一些人认为移动应用的末日已到。尽管我很欣赏、尊重 Eric Elliott 和他的工作,但我们还是通过考察一些近期的数据,进而讨论一下某些相反的观点:

人们在移动应用上花费的时间(2016年4月)

人们使用 APP 的时间占使用手机总时长的 90%

目前世界上有 25 亿人在使用移动手机。这个数字增长到 50 亿的速度会比我们想象的还要快。 在正常情况下,丢掉 45 亿人的生意,或者抛弃有 45 亿人使用的应用程序是绝对荒唐且行不通的。

老问题是原生移动应用的开发成本对大多数公司来说太高了。然而,面向 web 的开发成本也在增加。在美国,JavaScript 开发者的平均工资已达到 $97,000.00

伴随着复杂性的增加以及对高质量 web 开发的需求暴涨,雇佣一个 JavaScript 开发者的平均价格直逼原生应用开发者。论证 web 开发更便宜已经没用了。

那混合开发呢?

混合应用是将 HTML5 应用内嵌在原生应用的容器里,并且提供实现原生平台特性所需的权限。Cordova 和 PhoneGap 就是典型的例子。

如果你想构建一个 MVP 设计架构、一个产品原型,或者不担心对原生应用的模仿的用户体验,那么混合应用会很适合你。但谨记如果你最后想把它转为原生应用,整个项目都得重写。

此领域有很多创新的东西,我最喜欢的当属 Ionic Framework。混合开发正变得越来越好,但还不如原生开发那么流畅自然。

有很多公司,包括最严峻的初创公司,也包括大中规模的公司,混合应用在质量上的表现似乎没有满足客户的要求,给人的感觉是活糙、不够专业。

听说应用商店里的前 100 名都不是混合应用,我没有证据支持这一观点。如果说有百分之零到百分之五是混合应用,我就不怀疑了。

我们最大的错误是在 HTML5 身上下了太多的赌注 — 马克·扎克伯格 

解决方案

如果你紧跟移动开发动向,那么你绝对听说过像 NativeScriptReact Native 这样的项目。

通过这些项目,使用由 JavaScript 写成的基本 UI 组成块,像常规 iOS 和 Android 应用那样,就可以构建出高质量的原生移动应用。

你可以仅用一位工程师,也可以用一个专业的工程师团队,通过 React Native 使用 现有代码库 或者 底层技术 进行跨平台移动应用开发、原生桌面开发,甚至还有 web 开发。把你的应用发布到 APP Store 上、 Play Store 上,还有 Web 上。如此可以在保证不丧失原生应用性能和质量的同时,使成本仅占传统开发的一小部分。

通过 React Native 进行跨平台开发时重复使用其中 90% 的代码也不是没有的事,这个范围通常是 80% 到 90%。

如果你的团队使用 React Native, 既可以消除团队之间的分歧,也可以让 UI 和 API 的设计更一致,还可以加快开发速度。

在编译时不需要使用 React Native,在保存时应用可以实时更新,也加快了开发速度。

React Native 还可以使用 Code PushAppHub 这样的工具来远程更新你的 JavaScript 代码。这意味着你可以向用户实时推送更新、新特性,快速修复 bug,绕过打包、发布这些工作,绕过 App Store、Google Play Store 的审核,省去了耗时 2 到 7 天的过程(App Store 一直是整个过程的痛点)。混合应用的这些优势原生应用不可能比得上。

如果这个领域的创新力能像刚发行时那样保持,将来你甚至可以为 Apple WatchApple TV,和 Tizen 这样的平台开发应用。

NativeScript 依然是个相当年轻的框架驱动,Angular 版本 2,上个月刚刚发布测试版。但只要它保持良好的市场份额,未来就很有前途。

你可能还不知道世界上一些最能创新、最大的科技公司在这类技术上下了很大的赌注,特别是 React Native

我供职过的多家企业以及世界 500 强公司目前都在转移至 React Native。

在产品中特别注意使用 React Native

看下面的例子,这是一个使用 React Native 技术的著名应用列表

Facebook

Facebook 公司的 React Native 应用

Facebook 的两款应用 Ads ManagerFacebook Groups 都在使用 React Native 技术,并且将会应用到实现动态消息的框架上

Facebook 也会投入大量的资金创立和维护像 React Native 这样的开源项目,而且开源项目的开发者最近已经创建很多了不起的项目,这是很了不起的工作,像我以及全世界的业务每天都从中享受诸多好处。

Instagram

Instagram

Instagram 应用的一部分已经使用了 React Native 技术。

Airbnb

Airbnb

Airbnb 的很多东西正用 React Native 重写。(来自 Leland Richardson

超过 90% 的 Airbnb 旅行平台都是用 React Native 写的。(来自 spikebrehm

Vogue

Vogue 是 2016 年度十佳应用之一

Vogue 这么突出不仅仅因为它也用 React Native 写成,而是因为它被苹果公司评为年度十佳应用之一

沃尔玛

Walmart Labs

查看这篇 Keerti文章来了解沃尔玛是怎样看待 React Native 的优势的。

微软

微软在 React Native 身上下的赌注很大。

它早已发布多个开源工具,包括 Code PushReact Native VS Code,以及 React Native Windows,旨在帮助开发者向 React Native 领域转移。

微软考虑的是那些已经使用 React Native 为 iOS 和 Android 开发应用的开发者,他们可以重用高达 90% 的代码,不用花费太多额外的时间和成本就可将应用发布到 Windows 上。

微软对 React Native 生态的贡献十分广泛,过去几年在开源界的表现很抢眼。

结论

移动应用界面设计和移动应用开发要进行范式转变,下一步就是 React Native 以及与其相似的技术。

公司

如果你的公司正想着削减成本、加快开发速度,而又不想在应用质量和性能上妥协,这是最适合使用 React Native 的时候,它能提高你的净利润。

开发者

如果你是一个开发者,想进入一个将来会快速发展的领域,我强烈推荐你把 React Native 列入你的学习清单。

如果了解 JavaScript,你会入门很快,工具我首推 Exponent,其他的就看你怎么想了。使用 Exponent 开发者可以轻松的编译、测试和发布跨 Windows 和 MacOS 两个平台的 React Native 应用。

如果已经是一位原生应用开发者,你更会受益匪浅。因为在需要时你能够胜任深入研究原生应用边缘的工作。虽然不会经常用到,但在团队需要时这可是十分宝贵的能力。

我花了很多时间来学习、指导别人 React Native。因为它让我十分激动,而且使用这个框架创作应用也是我一个平淡的小乐趣。

感谢阅读。


作者简介:

教授和构建 React Native 应用的软件开发专家


via: https://hackernoon.com/the-cost-of-native-mobile-app-development-is-too-damn-high-4d258025033a

作者:Nader Dabit 译者:fuowang 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

JavaScript 即未来趋势所在。

Javascript 得到了众多的技术领导者的拥护和支持,其中一位就是 WordPress 的作者 Matt Mullenweg , 他表示 WordPress 开发者 应该学习 JavaScript , 这也清晰地向 WordPress 社区传达了 JavaScript 在未来的重要性。 同时,这一观点也被普遍接受。向着更先进的技术靠拢与过渡也同时保证了 WordPress 在未来的挑战中不会落于人后。

JavaScript 同时也是众多站在开源立场的技术中的佼佼者。与现在所流行的观点相反,JavaScript 不是一个工程,而是一个由其核心团队共同制定和维护的开放标准。ECMAScript , 这是另一个和 JavaScript 相关的名字, 它虽然不是开源的,但它也有一个开放的标准。

当你在浏览 GitHub 的时候你就可以发现 JavaScript 在当今有多么流行了。而且就仓库的数量 而言,JavaScript 绝对位于所有的编程语言当中最顶尖的那一层次。 同时,在 Livecoding.tv 上你也能看出 JavaScript 有多么突出,这里的用户发布的关于 JavaScript 的视频的数量比其他的话题多得多。在写这篇文章的时候(2016 年底),Livecoding.tv 上已经有 45,919 个 用户原创的 JavaScript 视频教程

热门的开源 JavaScript 框架和库

回归到主题, 庞大的社区是 JavaScript 的一个得天独厚的优势,同时这也推动了 JavaScript 的蓬勃发展。这里有数以百千计的成熟的 JavaScript 框架和库供开发者使用,同时这些最优秀的框架和库都是开源的。对当前的 JavaScript 开发者来说,能够使用这些优秀的框架和库来进行快速开发已经是必须技能了。当今的市场需要快速开发,但是,重复造轮子是没有必要的。不论你是一个 JavaScript 新手还是一个资深的 JavaScript 开发者,使用框架和库都能极大提高你的工作效率。

好了,让我们开始吧!

1. Angular.js

Angular.js 是目前最热门的 JavaScript 框架之一。它用于开发者构建复杂的 web 应用。Angular.js 背后的思想是它的单页应用 model。同时它 也支持 MVC 架构。在 Angular.js 中 ,开发者可以在前端中使用 JavaScript 代码,并从字面上扩展 HTML 词汇。

Angular.js 自 2009 年出现以来已经有了很大的改进。Angular 1 当前的稳定版本是 1.5.8/1.2.30 。你也可以试一试 Angular 2 ,相对于 Angular 1 来说它有了重大的改进,但这个新版本仍未在全球范围内被普遍使用。

在 Angular.js 中,数据绑定是完成工作的一个重要概念。在用户与接口的交互中,当交互完成,view 就会自动更新,随即新值与 model 交互以确保一切都是同步的。底层的逻辑在 model 中执行完成后,DOM 也会随即更新。

2. Backbone.js

复杂 web 应用并不适用于所有场景。一些较简单的 web 应用框架例如 Backbone.js 就非常适合学习 web app 开发。Backbone.js 是一个简单的框架,可以快速方便地构建简单的 web 应用。和 Angular.js 一样,Backbone.js 也支持 MVC 。Backbone.js 还有一些其它关键特性如路由,RESTful API 支持,适当的状态管理等等。你甚至还可以用 Backbone.js 来构建单页应用。

当前的稳定版本是 1.3.3,可以在 GitHub 中找到。

3. D3.js

D3.js 是一个优秀的 JavaScript 库,它允许开发者创建具有数据处理功能的富 web 页面。D3.js 使用 SVG、HTML 和 CSS 来实现这一切功能。使用 D3.js ,你可以更轻松地将数据绑定到 DOM 及启用数据驱动事件。使用 D3.js ,你还可以创建高质量的数据驱动的 web 页面来提供一个更易于理解的视觉效果来呈现数据。查看示例 : LCF 符号哈密顿图 ,由 D3.js 强力驱动。

4. React.js

React.js 是一个使用起来很有趣的 JavaScript 框架。和其它的 JavaScript 框架不同,React.js 志在构建一个高可扩展的前端用户界面。React.js 出现于 2013 年,它采用了 BSD 开源协议。它因其能够开发复杂且漂亮的用户界面所带来的优势而迅速发展壮大。

React.js 背后的核心思想是虚拟 DOM 。虚拟 DOM 在客户端和服务端之间扮演着一个中间人的角色并带来了显著的性能提升。虚拟 DOM 的改变和服务器端 DOM 一样,只需要更新所需的元素,相对于传统的 UI 渲染来说极大提升了渲染速度。

你还可以使用 Recat 来实现 meterial 风格的设计,使你能够开发具有无与伦比的性能的 web 应用。

5. jQuery

jQuery 是一个非常流行的 JavaScript 库,它拥有众多特性例如事件处理、动画等。当你在做一个 web 项目的时候,你不会想要把时间浪费在为一些简单的功能写代码上。jQuery 为减少你的工作量提供了一些易于使用的 API 。这些 API 在所有的常见的浏览器中都能够使用。使用 jQuery, 你可以无缝地控制 DOM 以及 Ajax 这样在近几年来拥有大量需求的任务。使用 jQuery,开发者不必担心一些低级的交互,同时可以使他们的 web 应用的开发更加容易与迅速。

jQuery 同时便于分离 HTML 和 JavaScript 代码,使开发者能够编写简洁而跨浏览器兼容的代码。并且使用 jQuery 创建的 web 应用在将来也易于改善和扩展。

6. Ember.js

Ember.js 是一个 Angular.js 和 React.js 的功能混合体。当你在浏览社区的时候你能明显地感受到 Ember.js 的热门程度。Ember.js 的新特性也不断地在添加。它在数据同步方面与 Angular.js 很像。 双向的数据交换可以确保应用的快速性和可扩展性。同时,它还能够帮助开发者创建一些前端元素。

和 React.js 的相似之处在于,Ember.js 提供了同样的服务器端虚拟 DOM 以确保高性能和高可扩展。同时, Ember.js 提倡简化代码,提供了丰富的 API。Ember.js 还有非常优秀的社区。

7. Polymer.js

如果你曾想过创建你自己的 HTML5 元素,那么你可以使用 Polymer.js 来做这些事。 Polymer 主要集中于通过给 web 开发者提供创建自己的标签的功能来提供扩展功能。例如,你可以创建一个和 HTML5 中的

Polymer 在 2013 年被 Google 引入并以 三句版 BSD 协议发布。

8. Three.js

Three.js 又是另一个 JavaScript 库,主要用于 3D 效果开发。如果你在做游戏开发的动画效果,那么你可以利用 Three.js 的优势。Three.js 在底层中使用 WebGL 使 Three.js 可以轻松地被用于在屏幕上渲染 3D 物体。举一个比较知名的使用 Three.js 的例子就是 HexGLA,这是一个未来派赛车游戏。

9. PhantomJS

使用 JavaScript 工作就意味着和不同的浏览器打交道,同时,当提及浏览器的时候,就不得不讨论资源管理。在 PhantomJS 中,由于有 Headless WebKit 的支持,所以你可以随时监测你的 web 应用。Headless WebKit 是 Chrome 和 Safari 使用的渲染引擎中的一部分。

这整个过程是自动化的,你所需要做的只是使用这个 API 来构建你的 web 应用。

10. BabylonJS

BabylonJS 与 Three.js 不相伯仲, 提供了创建平滑而强大的 3D web 应用的 JavaScript API。它是开源的,且基于 JavaScript 和 WebGL 。创建一个简单的 3D 物体,比如一个球体是非常简单的,你只需要写短短几行代码。通过这个库提供的 文档,你可以很好地掌握它的内容。 同时 BabylonJS 的主页上也提供了一些优秀的 demo 来当作参考。在其官网上你可以找到这些 Demo。

11. Boba.js

Web 应用总是有一个共通的需求,那就是分析。如果你还在苦于将数据的分析与统计插入到 JavaScript 的 web 应用中,那么你可以试一下 Boba.js。Boba.js 可以帮助你将分析的数据插入到你的 web 应用中并且支持旧的 ga.js 。你甚至可以把数据指标和 Boba.js 集成在一起,只需要依赖 jQuery 即可。

12. Underscore.js

Underscore.js 解决了 “当我面对一个空白 HTML 页面并希望即刻开始工作,我需要什么” 这个问题。当你刚开始一个项目,你可能会感到失落或者重复一系列你在之前项目中常做的步骤。 为了简化开启一个项目的过程和给你起个头,Underscore.js 这个 JavaScript 库给你提供了一系列的方法。例如,你可以使用你在之前项目中常用的 Backbone.js 中的 suspender 或者 jQuery 的一些方法。

一些实用的帮助例如 “filter” 和 “invoke the map” 可以给你起个好头,以助于你尽可能快的投入到工作中。 Underscore.js 同时还自带了一个套件来简化你的测试工作。

13. Meteor.js

Meteor.js 是一个快速构建 JavaScript 应用的框架。它是开源的且它能够用于构建桌面应用、移动应用和 web 应用。Meteor.js 是一个全栈的框架同时允许多平台的端到端开发。 你可以使用 Meteor.js 来实现前端和后端功能,同时它也能密切监视应用的性能。Meteor.js 的社区非常庞大,所以它会有不断的新特性更新或者是 bug 修复。Meteor.js 也是模块化的,同时它能配合一些其它的优秀的 API 使用。

14. Knockout.js

Knockout.js 在这些库中可能是最被低估的一个。它是一个基于 MIT 开源协议的开源 JavaScript 框架。作者是 Steve Sanderson。它基于 MVVM 模式。

值得注意的是: Node.js

Node.js 是一个强有力的 JavaScript 运行时环境。它可以被用于使用真实世界数据来构建快速且可扩展的应用。它既不是一个框架也不是一个库,而是一个基于 Google Chrome 的 V8 引擎的运行时环境。你可以用 Node.js 来创建多元化的 JavaScript 应用,包括单页应用、即时 web 应用等等。从技术层面上来讲,由于它的事件驱动式架构,所以 Node.js 支持异步 I/O 。这种做法使得它成为开发高可扩展应用的一个极好的解决方案的选择。查看 Node.js在 livecoding.tv 上的视频。

总结

JavaScript 是 web 开发中的通用语言。它之所以快速发展不仅仅是因为它所提供的内容,更多的是因为它的庞大的开源社区的支持。以上提到的框架和库对任何一个 JavaScript 开发者来说都是必须知道的。它们都提供了一些途径来探索 JavaScript 和前端开发。上面提及的大部分框架和库频繁地在 Livecoding.tv 上出现,其大部分来自对 JavaScript 及其相关技术感兴趣的软件工程师。


via: https://opensource.com/article/16/11/15-javascript-frameworks-libraries

作者:Dr. Michael J. Garbade 译者:chenxinlong 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出