CSS-in-JS,向Web组件化再迈一大步

简介

CSS-in-JS是什么,看到这个词就能大概猜到是在JavaScript里写CSS,那为什么要在JavaScript里写CSS呢,像之前一样写在css文件里哪里不好么?

在介绍这个概念之前,先来回顾一下在日常编写CSS代码时都有哪些痛点:

  • 全局污染 – CSS的选择器是全局生效的,所以在class名称比较简单时,容易引起全局选择器冲突,导致样式互相影响。
  • 命名混乱 – 因为怕全局污染,所以日常起class名称时会尽量加长,这样不容易重复,但当项目由多人维护时,很容易导致命名风格不统一。
  • 样式重用困难 – 有时虽然知道项目上已有一些相似的样式,但因为怕互相影响,不敢重用。
  • 代码冗余 – 由于样式重用的困难性等问题,导致代码冗余。

进化史介绍

在CSS的进化历史上,出现过各种各样的框架致力于解决以上的问题:

  • SASS, LESS – 提供了变量、简单函数、运算、继承等,扩展性、重用性都有了很大的提升,解决了一些样式重用冗余的问题,但是对于命名混乱问题的效果不大。
  • BEM (.block__element–modifier) – 比较流行的class命名规则,部分解决了命名混乱和全局污染的问题,但class定义起来还是不太方便,比较冗长,而且和第三方库的命名还是有可能冲突。
  • CSS Modules – 模块化CSS,将CSS文件以模块的形式引入到JavaScript里,基本上解决了全局污染、命名混乱、样式重用和冗余的问题,但CSS有嵌套结构的限制(只能一层),也无法方便的在CSS和JavaScript之间共享变量。

可以看一个简单的CSS Modules例子了解一下:

生成的dom结构如下图,基于css文件中的class名称生成了唯一的class名称,样式会定义到生成的class上。

styles打印出来如下图,定义了css中的class名字和生成的唯一class名字的对应关系。

可以看出,以上框架都解决了不少痛点,但也还是各有一些不足,当然CSS-in-JS也并不是完美的解决了所有问题,我们先来详细介绍一下。

流行框架介绍

现在随着组件化概念的流行,对从组件层面维护CSS样式的需求日益增大,CSS-in-JS就是在组件内部使用JavaScript对CSS进行了抽象,可以对其声明和加以维护。这样不仅降低了编写CSS样式带来的风险,也让开发变得更加轻松。它和CSS Modules的区别是不再需要CSS样式文件。

来看一下几个流行的CSS-in-JS框架六个月内的下载趋势

我们来看看几个下载量靠前的框架的风格是什么样的:

styled-components

先来看看下载量最高的styled-component的代码风格:

从上图可以看出,Title和Wrapper都是框架包装好的component,可以直接在react的jsx语法中使用,在包装component的时候还定义了标签分别是h1和section。此段代码产生的html dom如下图所示:

可以看到section和h1上分别生成了唯一的class名称,样式也对应的定义在生成的class上了。

这样就可以解决命名混乱和全局污染的问题。组件相关的代码都在一起,可以统一查看,也可以方便的重用样式。

glamorous

再来看看glamorous,这个框架是PayPal开发的。(前两个logo看下来,恍惚间感觉进了化妆品专柜)。

和styled-component不同的是,glamorous的样式直接以attribute的形式定义在了dom上,之后虽然也为其生成了class名称及样式,但这种以attribute定义的方式对伪类选择符(如 :hover)支持的不好,会带来一些不方便,而且需要再记住一套attributes名称和值与真正的css样式代码的对应关系。

JSS

和上面两个框架类似,jss也是会定义styles对象,并附到component上,最后生成的dom也是会有生成的唯一class名称,并有对应的样式,但样式并不是真正的css语法,而是对象的属性和值,这样也是对伪类选择符支持的不好,而且也需要记住属性和css样式代码之间的对应关系。

Radium

Radium在定义样式对象上看似和其他相似,但在生成dom结构的时候并没有生成唯一的class名称,而是直接把样式放到了style属性上,这样会带来诸如可读性差、CSS权重过大、不支持伪类选择符等问题。

测试

下面再来看一个styled-component提供的基于jest的测试框架:

jest-styled-components

这个框架主要是通过生成Snapshot并比较的方式来保证component样式的每次更改都会被检测到,并且样式是期望的样式。这样就又降低了重构CSS样式带来的风险。

优劣势总结

看了这些框架后,可以发现CSS-in-JS的优势还是挺多的:

  • 因为有了生成的唯一class名称,避免了全局污染的问题
  • 唯一的class名称也解决了命名规则混乱的问题
  • JavaScript和CSS之间可以变量共享,比如一些基础的颜色和尺寸,这样再当需要在JavaScript里计算一些高度的时候,可以取到和dom相关的一些padding,margin数值,统一管理
  • 只生成页面需要用到的代码,缩减了最终包的大小,提升了性能
  • CSS的单元测试增加了样式重构的安全性

但是CSS-in-JS也存在着一些不足和争议:

  • 有些观点觉得JS和CSS的关系没这么近,把CSS写进JS里引入了新的一套依赖,增加了复杂度,新人加入项目后需要学习的东西就更多了,也让学习曲线更加陡了
  • 对前端框架确实有些依赖性,更适合于组件化的框架,如React等
  • Debug的时候需要花更多的功夫才能找到对应的样式代码
  • 覆盖第三方插件样式时会有权重不够的问题
  • Lint工具对于JavaScript内部的CSS代码样式支持的还不够

最后

在ThoughtWorks最新一期的技术雷达(CSS-in-JS | Technology Radar | ThoughtWorks)里,它的等级是Assess,表示的是:“值得追求。重要的是理解如何建立这种能力。企业应该在风险可控的项目中尝试此技术。” 所以最后想说的是,虽然它还是有些不足和争议,在应用之前需要多角度衡量一下对项目的适合度。但它的优点也很多,确确实实解决了很多痛点,而且与web组件化的方向高度一致,希望大家在条件合适的情况下多多尝试,多多反馈,这样也能促进整个CSS编码体验的继续进化~


更多精彩洞见,请关注微信公众号:思特沃克

Share

你不知道的高性能Javascript

想必大家都知道,JavaScrip是全栈开发语言,浏览器,手机,服务器端都可以看到JS的身影。 本文会分享一些高效的JavaScript的最佳实践,提高大家对JS的底层和实现原理的理解。

数据存储

计算机学科中有一个经典问题是通过改变数据存储的位置来获得最佳的读写性能,在JavaScript中,数据存储的位置会对代码性能产生重大影响。 – 能使用{}创建对象就不要使用new Object,能使用[]创建数组就不要使用new Array。JS中字面量的访问速度要高于对象。 – 变量在作用域链中的位置越深,访问所需实践越长。对于这种变量,可以通过缓存使用局部变量保存起来,减少对作用域链访问次数 – 使用点表示法(object.name)和操作符(object[name])操作并没有太多区别,只有Safari会有区别,点始终更快

循环

在JS中常见的循环有下面几种:

for(var i = 0; i < 10; i++) { // do something}  
for(var prop in object) { // for loop object}    
[1,2].forEach(function(value, index, array) { // 基于函数的循环})

毋庸质疑,第一种方式是原生的,性能消耗最低的,速度也最快。第二种方式for-in每次迭代都回产生更多的开销(局部变量),它的速度只有第一种的1/7 第三种方式明显提供了更便利的循环方式,但是他的速度只有普通循环的1/8。所以可以根据自己项目情况,选择合适的循环方式。

事件委托

试想一下页面上每一个A标签添加一个事件,我们会不会给每一个标签都添加一个onClick呢。 当页面中存在大量元素都需要绑定同一个事件处理的时候,这种情况可能会影响性能。每绑定一个事件都加重了页面或者是运行期间的负担。对于一个富前端的应用,交互重的页面上,过多的绑定会占用过多内存。 一个简单优雅的方式就是事件委托。它是基于事件的工作流:逐层捕获,到达目标,逐层冒泡。既然事件存在冒泡机制,那么我们可以通过给外层绑定事件,来处理所有的子元素出发的事件。

document.getElementById('content').onclick = function(e) { 
    e = e || window.event;    
    var target = e.target || e.srcElement;    //如果不是 A标签,我就退出   
    if(target.nodeNmae !=== 'A') { return }   //打印A的链接地址    
    console.log(target.href) }

重绘与重排

浏览器下载完HTMl,CSS,JS后会生成两棵树:DOM树和渲染树。 当Dom的几何属性发生变化时,比如Dom的宽高,或者颜色,position,浏览器需要重新计算元素的几何属性,并且重新构建渲染树,这个过程称之为重绘重排。

bodystyle = document.body.style; 
bodystyle.color = red; 
bodystyle.height = 1000px; 
bodystyke.width = 100%;

上述方式修改三个属性,浏览器会进行三次重排重绘,在某些情况下,减少这种重排可以提高浏览器渲染性能。 推荐方式如下,只进行一次操作,完成三个步骤:

bodystyle = document.body.style; 
bodystyle.cssText 'color:red;height:1000px;width:100%';

JavaScript加载

IE8,Firefox3.5,Chrome2都允许必行下载JavaScript文件。所以<script>不会阻塞其他标签下载。 遗憾的是,JS下载过程依然会阻塞其他资源的下载,比如图片。尽管最新的浏览器通过允许并行下载提高了性能,但是脚本阻塞任然是一个问题。 因此,推荐将所有的<script>标签放在<body>标签的底部,以尽量减少对整个页面渲染的影响,避免用户看到一片空白

JS文件高性能部署

既然大家已经知道多个<script>标签会影响页面渲染速度,那么就不难理解“减少页面渲染所需的HTTP”是网站提速的一条经典法则。 所以,在产品环境下合并所有的JS文件会减少请求数,从而加快页面渲染速度。 除了合并JS文件,我们还可以压缩JS文件。压缩是指将文件中与运行无关的部分进行剥离。剥离内容包括空白字符,和注释。改过程通常可以将文件大小减半。 还有一些压缩工具会将局部变量的长度减小,比如:

var myName = "foo" + "bar";  
//压缩后变成  
var a = "foobar";

缓存JS文件

缓存HTTP组件能极大提高网站回访的用户体验。Web服务器通过“Expires HTTP响应头”来告诉客户端一个资源应该缓存多长时间。当然,缓存也有自己的缺陷: 当应用升级时,你需要确保用户下载到最新的静态内容。这个问题可以通过改变静态资源的文件名来解决。 你可能在产品环境看到浏览器引用jsapplication-20151123201212.js,这种就是以时间戳的方式保存新的JS文件,从而解决缓存不更新问题。

总结

当然,高效的JS不仅仅只有这些可以改进的地方,如果能够减少一些性能的损耗,我们就能更高效的使用JavaScript进行开发了。

Share

JavaScript语言中五种消除分支的方法

最近开始使用JavaScript. 回顾了一下这几天的代码, 发现圈复杂度为1. 30几个函数40多行, 超过两行的函数都很少 (当然那种当做对象来用的函数除外, 只说实际做事的函数. 不要小看这40几行代码, 完成了5个完整的具有用户价值的功能. JavaScript的表达能力不是盖的).

由于JavaScript具备一些函数式编程语言的特征, 写出没有分支没有显式循环的代码也属正常. 但实际上多数代码还是命令式的. 命令式风格也能写出圈复杂度为1的代码, 看看都用到了哪些技巧.

多态

这种技巧在<<重构>>里提过, 跟JavaScript没有多大关系. JavaScript对Duck Typing的支持, 使得多态更容易实现. 略过

Null Object Pattern

这个也跟JavaScript没多大关系. 具体到js, 简单说就是不要出现undefined和null, 总是赋初值. string赋””, 对象赋{ }, 等等, 可以少很多判断

Dispatch Earlier, or “Boolean Parameter Considered Harmful”

这也是一种语言无关的策略. 多说几句. 简单来说就是但凡在函数内部需要根据参数进行判断走不同分支的时候, 总应该存在一个更早的时机可以把执行路径分开, 从而消除判断. 或曰早一点分开也需要判断啊! 这是对的, 但这个判断可以由用户做出, 或者在程序的配置中做出, 而无需运行时逻辑. 举个简单的栗子: 用户点击网页对话框中的”确定”和”取消”时发给server端的请求应该如何设计?

一种是发送请求到如下URL: http://my.domain/some/question?agreed=1 或者 http://my.domain/some/question?agreed=0或者post的话同一个URL不同body. 如果是这种设计, 那server端必然有一个if来判断是确定还是取消. 可用户点击的时候已经做出判断了啊, 在程序中再做一次不是很多余吗?

另一种设计会利用用户的判断, 点击”确定”或”取消”的请求会被发送到不同的URL, 从而被路由到不同的server端的代码. 处理”确定”的代码无需关心用户点击的是不是”确定”, 因为只有用户点击”确定”后请求才会被送到你那里. 类似桌面应用不同的按钮关联不同的事件处理程序.

那会不会有代码重复? 有就抽出来呗.

JavaScript 对象是天然的分发表

只有这一条跟JavaScript有点关系. JavaScript对象就是以string为key的哈希, 而JavaScript中函数可以作为值, 也就是函数可以作为哈希的value. 这让JavaScript对象成了一个天然的分发表, key进去,函数出来, 不用任何显式的if/else/switch. 举个栗子:

即时聊天机器人, 根据用户的输入做不同的动作, 比如输入天气的话, 就去查询最近的天气并返回, 输入餐馆就查询附近的餐馆并返回

var response_generators = {
  weather: function( ) { query_weather_from_weather_service… },
  restaurant: function( ) { get_local_information… },
  help: function( ) { generate_help_menu… }
}

var reply = response_generators[user.input]( );

这种方式可消除显式的分支, 并且代码描述性更强. 但有变化发生时, 依然要打开修改response_generators.

利用underscore之类的类库消除循环, 包括循环中的break和continue

var evens = _.filter([1, 2, 3, 4, 5, 6], function(num){ return num % 2 == 0; });
=> [2, 4, 6]
Share

函数式 JavaScript

javascript

[AD] 如果你想在 2015 jsconf 上听到更多关于 Clojure风格的JavaScript函数式编程 的话, 请到这里给我点赞https://jinshuju.net/f/Dfys92 搜索 “Clojure”

May the Lambda be with you!

1 Preface

这是一本你可能2小时就能看完的小书,但是涵盖了基本所有函数式编程的内容,还包含了一些 ECMAScript 6 定义的函数式新特性, 如箭头函数, 模式匹配等等. 还会介绍函数式一些重要概念在 JavaScript是如何实现即应用, 以及如何以函数式的思想编写 JavaScript 代码.

对果JavaScript的基本概念对你来说:

what_you_talking.gif

可能本书并不适合你, 请先移步 JavaScript Allonge, 但是如果你学习函数式编程时感到:

summarize_in_one_word.gif
Figure 2: I’m not reading the crap, summarize in one word

那么这本书将会对你会有所帮助.

我选用的 JavaScript 函数式库是 Eweda(Ramda 的最初实现,更遵守函数式教条,但由于 javascript 的栈很容易爆,Ramda的实现要更 pratical 一些而且可以用的产品代码中, 千万不要在产品中用 eweda,这里只用eweda做介绍目的)

为什么不用 Underscore/Lodash 请移步第二章

由于会介绍 ECMAScript 6 的新特性, 书中很多写法都是 ECMAScript 6 标准, 只能在实现这些 feature 的浏览器(如 Firefox, 请目前参照各浏览器的实现情况) 里运行. 另外, 大多数的例子源码都会在文章里的 jsbin 链接里.

2 Lambda

为什么讲 lambda, 如果小时候玩过游戏”半条命”,那么你早都见过 lambda 了.

Lambda_reactor_complex_logo.png

我从wikipedia里面粘出来了这么一段定义:

lambda包括一条变换规则(变量替换)和一条函数定义方式,Lambda演算之通用在于,任何一个可计算函数都能用这种形式来表达和求值。因而,它是等价于图灵机的.

好吧, 跟没解释一样. 简单来说lambda其实就是 xy 的映射关系, 但在大部分支持函数式的编程语言中, 它等价于匿名函数. 被称为 lambda 表达式. 因为这些函数只需要用一次, 而且变换不复杂, 完全不需要命名.

parallel-universe.gif

匿名函数在程序中的作用是可以作为参数传给高阶函数1, 或者作为闭包被返回.

但是匿名函数并不是原本的 lambda 算子, 因为匿名函数也可以接受多个参数, 如

multiple(x, y) = x*y

写成简单映射的形式, 把名字去掉

(x,y) -> x*y

这就是 lambda 了吗, 不是, lambda的用意是简化这个映射关系以至不需要名字, 更重要的是只映射一个 x.

什么意思呢? 让我们来分解一下上面的这个映射的过程.

  1. lambda 接受第一个参数 5, 返回另一个 lambda
    (5) -> (y -> 5*y)
    
  2. 该返回的 lambda y -> 5*y 接收 y 并且返回 5*y, 若在用 4 调用该 lambda
4 -> 5*4

因此这里的匿名函数 (x,y)->x*y 看似一个 lambda, 其实是两个 lambda 的结合.

而这种接受一个参数返回另一个接收第二个参数的函数叫柯里化2.

这里我们先忍一忍, 来看下 JavaScript 中的 lambda 表达式.

2.1 箭头函数(arrow function)

来看看越来越函数式の JavaScript

新的草案ECMAScript 6 (虽然说是草案,但你可以看到 Firefox 其实已经实现大部分的 feature)里我们越来越近了, 借助一下transcompiler例如Babel 我们完全可以在项目中开始使用es6了3

看看里面有一行 arrow function,为什么叫箭头函数,还记得前面说lambda是提到的箭头吗。而且如果你之前用过 Haskell(单箭头) 或者Scala(双箭头), 会发现都用的是箭头来表示简单映射关系.

由于 arrow function 只在Firefox 22以上版本实现, 本节的所有代码都可以在Firefox的Console中调试, 其他chrome 什么的都没有实现(完全)4. 另外每节的最后我都会给出完整代码的可执行的 jsbin 链接.

2.1.1 声明一个箭头函数

你可以用两种方式定义一个箭头函数

([param] [, param]) => {
   statements
}
// or
param => expression

单个表达式可以写成一行, 而多行语句则需要 block {} 括起来.

2.1.2 为什么要用箭头函数

看看旧的匿名函数怎么写一个使数组中数字都乘2的函数.

var a = [1,2,3,4,5];
a.map(function(x){ return x*2 });

用箭头函数会变成

a.map(x => x*2);

只是少了 functionreturn 以及 block, 不是吗? 如果觉得差不多, 因为你看惯了 JavaScript 的匿名函数, 你的大脑编译器自动的忽略了,因为他们不需要显示的存在.

map(x => x*2) 要更 make sense, 因为我们需要的匿名函数只需要做一件事情, 我们需要的是 一个函数 f, 可以将给定 x, 映射到 y. 翻译这句话的最简单的方式不就是f = (x => x*2)

2.1.3 Lexical this

如果你觉得这还不足以说服改变匿名函数的写法, 那么想想以前写匿名函数中的经常需要 var self=this 的苦恼.

    var Multipler = function(inc){
      this.inc = inc;
    }
    Multipler.prototype.multiple = function(numbers){
      return numbers.map(function(number){
        return this.inc * number;
      })
    }
    new Multipler(2).multiple([1,2,3,4]) 
// => [NaN, NaN, NaN, NaN]  不 work, 因为 map 里面的 this 指向的是全局变量( window)

    Multipler.prototype.multiple = function(numbers){
      var self = this; // 保持 Multipler 的 this 的缓存
      return numbers.map(function(number){
        return self.inc * number;
      })
    }
    new Multipler(2).multiple([1,2,3,4]) // => [ 2, 4, 6, 8 ]

很怪不是吗, 确实是 Javascript 的一个 bug, 因此经常出现在各种面试题中, 问 this 到底是谁.

which-leela.gif

试试替换成 arrow function

Multipler.prototype.multiple = function(numbers){
  return numbers.map((number) => number*this.inc);
};

console.log(new Multipler(2).multiple([1,2,3,4]));// => [ 2, 4, 6, 8 ]

不需要 var self=this 了是不是很开心☺️现在, arrow function 里面的 this 会自动 capture 外层函数的 this 值.

2.2 JavaScript的匿名函数(anonymous function)

支持匿名函数, 也就意味着函数可以作为一等公民. 可以被当做参数, 也可以被当做返回值.因此, JavaScript 的支持一等函数的函数式语言, 而且定义一个匿名函数式如此简单.

2.2.1 创建一个匿名函数

在JavaScript里创建一个函数是如此的 简单 … 比如:

function(x){
    return x*x;
}// => SyntaxError: function statement requires a name

但是, 为什么报错了这里. 因为创建一个匿名函数需要用表达式(function expression). 表达式是会返回值的:

var a = new Array() // new Array 是表达式, 而这整行叫语句 statement

但为什么说 function statement requires a name. 因为 JavaScript 还有一种创建函数的方法–/function statement/. 而在上面这种写法会被认为是一个 function 语句, 因为并没有期待值. 而 function 语句声明是需要名字的.

简单将这个函数赋给一个变量或当参数传都不会报错, 因为这时他没有歧义,只能是表达式.比如:

var squareA = function(x){
    return x*x;
}

但是这里比较 tricky 的是这下 squareA 其实是一个具名函数了.

console.log(squareA) // => function squareA()

虽然结果是具名函数,但是过程却与下面这种声明的方式不一样.

function squareB(x){
    return x*x;
} // => undefined

squareB 用的是 function statement 直接声明(显然 statement 没有返回), 而squareA 则是先用 function expression 创建一个匿名函数, 然后将返回的函数赋给了名为 squareA 的变量. 因为表达式是有返回的:

console.log(function(x){ return x*x});
// => undefined
// => function ()

第一个 undefinedconsole.log 的返回值, 因此 function() 则是打印出来的 function 表达式创建的匿名函数.

2.2.2 使用匿名函数

JavaScript 的函数是一等函数. 这意味着我们的函数跟值的待遇是一样的,于是它

可以赋给变量:

var square = function(x) {return x*x}

可以当参数, 如刚才见到的:

console.log(function(x){return x*x})

将函数传给了 console.log

可以被返回:

function multiply(x){
    return function(y){
        return x*y;
    }
}
multiply(1)(2) // => 2

3 高阶函数(Higher-order function)

我们已经见识到了匿名函数和箭头函数的用法, 匿名的一等函数到底有什么用呢? 来看看高阶函数的应用.

高阶函数意思是它接收另一个函数作为参数. 为什么叫 高阶: 来看看这个函数 f(x, y) = x(y) 按照 lambda 的简化过程则是

f(x) => (y -> x(y))
(y) => x(y)

可以看出来调用 f 时却又返回了一个函数x.

还记得高等数学里面的导数吗, 两阶以上的导数叫高阶导数. 因为求导一次以后返回的可以求导.

概念是一样的, 如同俄罗斯套娃 当函数执行以后还需执行或者要对参数执行, 因此叫高阶函数.

recursion.png

高阶函数最常见的应用如 map, reduce. 他们都是以传入不同的函数来以不同的方式操作数组元素.

另外 柯里化, 则是每次消费一个参数并返回一个逐步被配置好的函数.

高阶函数的这些应用都是为函数的组合提供灵活性. 在本章结束相信你会很好的体会到函数组合的强大之处.

3.1 Higher-order function

函数在 JavaScript 中是一等公民, 因此在 JavaScript 中, 使用高阶函数是非常方便的.

3.1.1 函数作为参数

假设我现在要对一个数组排序, 用我们熟悉的 sort

[1,3,2,5,4].sort( (x, y) => x - y )

如果我们要逆序的排序, 把减号左右的 xy 呼唤,就这么简单, 但如果我是一个对象数组, 要根据对象的 id 排序:

[{id:1, name:'one'},
 {id:3, name:'three'}, 
 {id:2, name:'two'}, 
 {id:5, name:'five'},  
 {id:4, name:'four'}].sort((x,y) => x.id - y.id)

是不是已经能够感受到高阶函数与匿名函数组合的灵活性.

3.1.2 函数作为返回值

函数的返回值可以不只是值, 同样也可以是一个函数, 来看 Eweda 内部的一个工具函数aliasFor, 他的作用是给函数 E 的一些方法起一些别名:

听起来很怪不是吗, 函数怎么有方法, 实际上 JavaScript 的 function是一个特殊 对象, 试试在 Firefox console 里敲 console.log. 是不是看到了一些方法, 但是 typeof console.log 是 function

var E = () => {}
var aliasFor = oldName => {
    var fn = newName => {
      E[newName] = E[oldName];
      return fn;
    };
    return (fn.is = fn.are = fn.and = fn);
};

这里有两个 return, 一个是 fn 返回自己, 另一个是 aliasFor 也返回 fn, 并且给 fn了几个别名 fn.is fn.are

什么意思呢? fn 返回 fn. 很简单就是 fn() => fn, 那么 fn()()=>fn()=>fn …以此类推, 无论调用 fn 多少次,都最终返回 fn.

1qUCC8s.png

这到底有什么用呢, 由于这里使用了 fn 的副作用(side affect) 来干了一些事情E[newName]=E[oldName], 也就是给 E 的方法起一个别名, 因此每次调用 fn 都会给E 起一个别名. aliasFor 最后返回的是 fn 自己的一些别名, 使得可以 chain 起来更可读一些:

aliasFor('reduce').is('reduceLeft').is('foldl')

另外, 函数作为返回值的重要应用, 柯里化与闭包, 将会在在后面专门介绍. 我们先来看下以函数作为参数的高阶函数的典型应用.

3.2 柯里化 currying

还记得 Haskell Curry吗

curry.png

多巧啊, 人家姓 Curry 名 Haskell, 难怪 Haskell 语言会自动柯里化, 呵呵. 但是不奇怪吗, 为什么要柯里化呢. 为什么如此重要得让 Haskell 会默认自动柯里化所有函数, 不就是返回一个部分配置好的函数吗.

我们来看一个 Haskell 的代码.

max 3 4
(max 3) 4

结果都是4, 这有什么用呢.

这里看不出来, 放到 高阶函数 试试. 什么? 看不懂天书 Haskell, 来看看 JavaScript 吧.

3.2.1 我们来看一个问题

写一个函数, 可以连接字符数组, 如 =f([‘1′,’2’]) => ’12’=

好吧,如果不用柯里化, 怎么写? 啊哈 reduce

var concatArray = function(chars){
  return chars.reduce(function(a, b){
    return a.concat(b);
  });
}
concat(['1','2','3']) // => '123'

很简单,对吧.

现在我要其中所有数字加1, 然后在连接
var concatArray = function(chars, inc){
  return chars.map(function(char){
    return (+char)+inc + '';
  }).reduce(function(a,b){
      return a.concat(b)
  });
}
console.log(concatArray(['1','2','3'], 1))// => '234'
所有数字乘以2, 再重构试试看
var multiple = function(a, b){
  return +a*b + ''
}
var concatArray = function(chars, inc){
  return chars.map(function(char){
    return multiple(char, inc);
  }).reduce(function(a,b){
      return a.concat(b)
  });
}
console.log(concatArray(['1','2','3'], 2)) // => '246'

是不是已经看出问题了呢? 如果我在需要每个数字都减2,是不是很麻烦呢.需要将 map 参数匿名函数中的 multiple 函数换掉. 这样一来 concatArray 就不能同时处理加, 乘和减? 那么怎么能把他提取出来呢? 来对比下柯里化的解法.

3.2.2 柯里化函数接口

var multiple = function(a){
  return function(b){
    return +b*a + ''
  }
}

var plus = function(a){
  return function(b){
    return (+b)+a + ''
  }
}
var concatArray = function(chars, stylishChar){
  return chars.map(stylishChar)
    .reduce(function(a,b){
      return a.concat(b)
  });
}
console.log(concatArray(['1','2','3'], multiple(2)))
console.log(concatArray(['1','2','3'], plus(2)))

有什么不一样呢 1. 处理数组中字符的函数被提取出来, 作为参数传入 2. 提取成柯里化的函数, 部分配置好后传入, 好处显而易见, 这下接口非常通畅 无论是外层调用

concatArray(['1','2','3'], multiple(2))

还是内部的 map 函数

chars.map(stylishChar)

这些接口都清晰了很多, 不是吗

这就是函数式的思想, 用已有的函数组合出新的函数, 而柯里化每消费一个参数, 都会返回一个新的部分配置的函数, 这为函数组合提供了更灵活的手段, 并且使得接口更为流畅.

3.2.3 自动柯里化

在 Haskell 语言中, 函数是会自动柯里化的:

max 3 4

其实就是

(max 3) 4

可以看看 maxmax 3 函数的 类型

ghci> :t max
max :: Ord a => a -> a -> a

看明白了么, Ord a => 表示类型约束为可以比较大小的类型, 因此=max= 的类型可以翻译成: 当给定一个=a=, 会得到=a -> a=, 再看看=max 3=的类型就好理解了

ghci> :t max 3
(Num a, Ord a) => a -> a

左侧表示类型约束 a 可以是 Ord 或者 Num, 意思是 max 3 还是一个函数,如果给定一个Ord 或者 Num 类型的参数 则返回一个 Ord 或者 Num.

现在是不是清晰了, 在 Haskell 中每给定一个参数, 函数如果是多参数的, 该函数还会返回一个处理余下参数的函数. 这就是自动柯里化.

而在 Javascript(以及大多数语言) 中不是的, 如果给定多参函数的部分参数, 函数会默认其他参数是 undefined, 而不会返回处理剩余参数的函数.

function willNotCurry(a, b, c) {
    console.log(a, b, c)
    return a*b-c;
}
willNotCurry(1)
// => NaN
// => 1 undefined undefined

如果使用自动柯里化的库 eweda, 前面的例子简直就完美了

var multiple = curry(function(a, b){
  return +b*a + ''
})
var plus = curry(function(a, b){
  return (+b)+a + ''
})

3.3 函数组合 function composition

通过前面介绍的高阶函数, map, fold 以及柯里化, 其实已经见识到什么是函数组合了. 如之前例子中的 map 就是 由 fold 函数与 reverse 函数组合出来的.

这就是函数式的思想, 不断地用已有函数, 来组合出新的函数.

composition.jpg

如图就是函数组合,来自 Catgory Theory(Funtor 也是从这来的,后面会讲到), 既然从 A到B 有对应的映射f,B到 C有对应的映射g, 那么 (g.f)(x) 也就是 fg 的组合 g(f(x)) 就是 A到 C 的映射。上一章实现的 map 函数就相当于 reverse.fold.

3.3.1 Compose

我们可以用 Eweda 非常方便的 compose 方法来组合函数

var gf = E.compose(f, g)

说到了函数组合, 柯里化, 我想现在终于可以解释清楚为什么在这里选用 Eweda/Ramda 而不是 Underscore 了.

举个例子🌰 如果我现在想要 tasks 列表中所有属性为 completedtrue 的元素, 并按照 id 排序.

underscore 里会这样写:

_(tasks)
    .chain()
    .filter( task => task.completed===true)
    .sortBy( task => task.id)
    .value();

这种方式怎么看都不是函数式, 而是以对象/容器为中心的串联,有些像 jquery 对象的链式调用, 或者我们可以写的函数式一些, 如

_.sortBy(_.filter(tasks, task => task.completed===true), task => task.id)

恩恩, 看起来不错嘛, 但是有谁是这么用 underscore的呢. 一般都会只见过 链式调用才是 underscore 的标准写法。

来对比一下用 Eweda/Ramda 解决的过程 :

compose(sortBy(task=>task.id), filter(task=>task.completed===true))(tasks)

好像没什么区别啊? 不就是用了 compose 吗?

区别大了这, 看见 tasks 是最后当参数传给 E.compose() 的吗? 而不是写死在filter 的参数中. 这意味着在接到需要处理的数据前, 我已经组合好一个新的函数在等待数据, 而不是把数据混杂在中间, 或是保持在一个中间对象中. 而 underscore 的写法导致这一长串 _.sortBy(_.filter()) 其实根本无法重用。

好吧如果你还看不出来这样做的好处. 那么来如果我有一个包含几组 tasks的列表 groupedTasks, 我要按类型选出 completed 为 true 并按 id 排序. 如我现在数据是这个:

groupedTasks = [
  [{completed:false, id:1},{completed:true, id:2}],
  [{completed:false, id:4},{completed:true, id:3}]
]

underscore:

_.map(groupedTasks,
   tasks => _.sortBy(_.filter(tasks, task => task.completed===true), task => task.id))

看见我们又把 _.sortBy(_.filter()) 这一长串原封不动的拷贝到了 map 里。 因为 underscore 一开始就要消费数据,使得很难重用,除非在套在另一个函数里:

function completedAndSorted(tasks){
  return _.sortBy(_.filter(tasks, task => task.completed===true), task => task.id))
}
_.map(groupedTasks, completedAndSorted)

只有这样才能重用已有的一些函数。或者虽然 underscore 也有 _.compose 方法,但是 几乎所有 underscore 的方法都是先消费数据(也就是第一个参数是数据),使得很难放到 compose 方法中,不信可以尝试把 filter 和 sortBy 搁进去,反正我是做不到。

来看看真正的函数组合

var completedAndSorted = compose(sortBy(task=>task.id),
                                 filter(task=>task.completed===true))
map(completedAndSorted, groupedTasks)

看出来思想完全不一样了吧.

由于 Eweda/Ramda 的函数都是自动柯里化,而且数据总是最后一个参数, 因此可以随意组合, 最终将需要处理的数据扔给组合好的函数就好了. 这才是函数式的思想. 先写好一个公式,在把数据扔给 公式。而不是算好一部分再把结果给另一个公式。

ThreeFunctionMachines.jpg

而 underscore 要么是以对象保持中间数据, 用 chaining 的方式对目标应用各种函数(书上会写这是Flow-Base programming,但我觉得其实是 Monad,会在下一章中介绍), 要么用函数嵌套函数, 将目标一层层传递下去.

3.3.2 pipe

类似 compose, eweda/ramda 还有一个方法叫 pipe, pipe 的函数执行方向刚好与 compose 相反. 比如 pipe(f, g), f 会先执行, 然后结果传给 g, 是不是让你想起了 bash 的 pipe

find / | grep porno

实际上就是 pipe(find, grep(porno))(/)

没错,他们都是一个意思. 而且这个函数执行的方向更适合人脑编译(可读)一些.

如果你已经习惯 underscore 的这种写法

_(data)
  .chain()
  .map(data1,fn1)
  .filter(data2, fn2)
  .value()

那么转换成 pipe 是很容易的一件事情,而且更简单明了易于重用和组合。

pipe(
  map(fn1),
  filter(fn2)
)(data)

4 Functor

4.1 Functor

Functor 是 可以被 map over 的类型. 什么叫 map over…

比如 list 就可以说是可以被map over… 那么是不是可枚举类型?

不是的, 来看看 Haskell 中如何解释(其实所有函数式的概念可能用 haskell 是最能说明问题的了).

ghci > :t fmap
fmap :: (a -> b) -> fa -> f b

fmap 又是什么东西, fmap 是 map over Functor 的函数. 这个函数只干一个事情, 可能通过前面解释的一点点 Haskell功夫,你可能能翻译 (a -> b) -> fa -> f b 了把. 给定一个从 ab 的映射函数, 再给定一个 a 的 Functor, 返回一个 b 的 Functor.

虽然个个字都认识, 但怎么就不知道啥意思.

如果我再说一个新词, 你是不是会疯掉了– Lift.

好吧, 把他们都串起来, 你就明白了. 1. 平常我们可以把 ab 的映射可以叫做 map, 映射的方式就是函数了. 2. 那么类似的对于函数或者其他可以做这种 map 操作的类型或一种计算方式, 叫做 Functor. 3. 而这种 map 就叫做 fmap, 给定 a 集合到 b 集合的映射方式(也就是一个函数), 就能找到 对 a 的一种计算(computation, 任何可变换的类型, 这就是 Functor) 的变换 – 对 b 的对应计算方式. 4. 如果该计算是一个函数, 那么这个操作叫做 lifting. 非常形象的, 根据 a 到 b 的映射 lift(举) 到另一个层面上.

lifter.png

虽然 lifting 很形象, 但是还是越说越抽象了, 来举个栗子.

4.2 举个栗子🌰

注意我们还没有实现 Functor, 因此下面的栗子还不能运行在你的 console.

前面说了, Functor 可以是数组, 因为数组可以被 map over

var plus1 = n => n+1;
fmap(plus1, [2, 4, 6, 8])// => [3,5,7,9]

这里,数组 Array 就是 Functor 类型, 而 fmap 把 2 -> 3 的映射方式对 Array [2,4,6,8] 进行了变换, 得到 [3,5,7,9]. 这跟数组的 map 方法一样, 比较好理解.

再试试换一种 Functor 类型, 试试函数

var times2 = m => m*2;
fmap(plus1, times2) // => function(){}
fmap(plus1, times2)(3) // => 7 (3*2+1)

看到 fmap 返回的是一个函数, 因为你 map over 的是一个函数 times2. 还记得 (a -> b) -> f a -> f b 的公式么, 因为现在的 Functor 为 Function 类型, 我们可以把=f=替换成函数也就是 x 到 y 的映射, 因此我们可以将该公式替换为

(a -> b) -> (x -> a) -> (x -> b)

再用我们具体的函数 plus1 替换进去

(n->n*2) -> plus1(n) -> plus1(n*2)

也就是说, 这个 fmap 会把函数 times2 应用到 plus1 的任何结果上.

这不就是函数组合吗 plus1(times2(3)), 确实是的. 但这只是 Functor 的冰山一角, dan在来看看别的Functor

Functor 还可以是别的东西…比如

fmap(plus1, Either(10, 20))

Either也是 Functor, 慢着, Either 是什么类型, 好吧,在解释 Either 之前, 我们先忍一忍, 来先看看 JavaScript 中怎么实现以及使用一个 Functor.

4.3 Functor in JavaScript

首先, 我们用定义一个确定 Functor 类型的函数, 如果没有注册的类型抛出异常.

 var types = function(obj) {
throw new TypeError("fmap called on unregistered type: " + obj);
};

然后实现注册 Functor 的函数.

 Functor = function(type, defs) {
        var oldTypes = types;
        types = (obj) => {
            if (type.prototype.isPrototypeOf(obj)) {
                return defs; // 这是递归的出口, 判断类型, 确定 fmap 的 Functor 实例属于注册的哪一个 Functor
            }
            return oldTypes(obj); //不断递归寻找 types, 这个效率会很低, 因为调用栈上好多闭包, 每个闭包都保持着 type 和 defs
        }
};

这样可以用 Functor 函数注册一个新的 Functor 类型并定义它自己的 fmap 方法(还记得前面说的 Functor 只有一个方法吗). 比如我们要把 Array 变成 Functor

Functor(Array, {
    fmap: (fn, array) => {
        arr.map(x => fn(x))
    }
})

好像快要完成的样子. 现在还差 fmap Functor 类型函数了. 这个函数干两件事情, 找到实例属于哪个 Functor 类型, 并调用他的 fmap 方法.

fmap = eweda.curry((fn, obj) => {
    return types(obj).fmap(f, obj)
})

同样的, 我们很快可以把 Function 也变成 Functor

Functor(Function, {
    fmap: (f, g) => {
        return eweda.compose(f, g);
}})

还记得前面说 fmap 函数像函数组合吗, 呵呵, 我们这里就按函数组合实现.


来总结一下 fmap 和 Functor 到底是什么, fmap 可以将函数应用到 Functor 上, Functor 可以看做是容器或者是带 context 的值. 也就是说如果我们想变换 x 的值, 直接给一个函数映射 x=> x*2 即可. 如果我想变换一个数组, 一个函数, 或者 Either 这种带有 context 的或者说容器里面的值, 总不能直接把这些容器直接给函数吧,这时就需要 fmap 将函数的映射关系应用到容器里面的值. 其实就是打开,调一下函数,完了再包好。

好吧, 通过如何实现和使用一个简单的 Functor, 概念上已经估计可以理解了, 我们回过头来看看 Either 是神马玩意.

完整代码

5 Monad

这个概念好难解释, 你可以理解为一个 Lazy 或者是状态未知的盒子. 听起来像是薛定谔猫(估计点进去你会更晕了). 其实就是的, 在你打开这个盒子之前, 你是不知道里面的猫处在那种状态.

Monad 这个黑盒子, 里面到底卖的神马药,我们要打开喝了才知道.

等等, 不是说好要解释 Either 的吗, 嗯嗯, 这里就是在解释 Either. 上节说 Either 是一个 Functor, 可以被 fmap over. 怎么这里又说道黑盒子了? 好吧, Monad 其实也是 Functor. 还记得我说的 Functor 其实是一个带 context 的盒子吗. 而 fmap 使得往盒子里应用函数变换成为了可能.

5.1 Either

先来看看 Either 这种类型会干什么事情. Either表示要不是左边就是右边的值, 因此我们可以用它来表示薛定谔猫, 要不是活着, 要不死了. Either 还有个方法: either

(a -> c) -> (b -> c) -> Either a b -> c

想必你已经对箭头 -> 非常熟了吧.如果前面几章你都跳过了,我再翻译下好了. 这里表示接收函数 a->c 和函数 b->c, 再接收一个 Either, 如果 Either 的值在左边,则使用函数映射 a->c, 若值在右边,则应用第二个函数映射 b->c.

作为 Monad, 它还必须具备一个方法 ‘>>='(这个符号好眼熟的说, 看看 haskell 的 logo, 你就知道 Monad 是有多重要), 也就是 bind 方法.

haskellwiki_logo.png bind 方法的意思很简单, 就是给这个盒子加一个操作, 比如往盒子在加放射性原子,如果猫活着,就是绿巨猫, 如果猫是死的,那就是绿巨死猫.

Left("cat").bind(cat => 'hulk'+cat)
// => Left "hulkcat"
Right("deadcat").bind(cat => 'hulk' + cat)
// => Right "hulkdeadcat"

这有个毛用啊. 表急… 来看个经典例子 ### 走钢索

皮尔斯决定要辞掉他的工作改行试着走钢索。他对走钢索蛮在行的,不过仍有个小问题。就是鸟会停在他拿的平衡竿上。他们会飞过来停一小会儿,然后再飞走。这样的情况在两边的鸟的数量一样时并不是个太大的问题。但有时候,所有的鸟都会想要停在同一边,皮尔斯就失去了平衡,就会让他从钢索上掉下去。

pierre.png 我们这边假设两边的鸟差异在三个之内的时候,皮尔斯仍能保持平衡。

5.1.1 一般解法

首先看看不用 Monad 怎么解

eweda.installTo(this);
var landLeft = eweda.curry(function(n, pole){
    return [pole[0]+n, pole[1]];
});
var landRight = eweda.curry(function(n, pole){
    return landLeft(n, eweda.reverse(pole));
});
var result = eweda.pipe(landLeft(1), landRight(1), landLeft(2))([0,0]);
console.log(result);
// => [3, 1]

还差一个判断皮尔斯是否掉下来的操作.

var landLeft = eweda.curry(function(n, pole){
    if(pole==='dead') return pole;
    if(Math.abs(pole[0]-pole[1]) > 3)
      return 'dead';
    return [pole[0]+n, pole[1]];
});
var landRight = eweda.curry(function(n, pole){
    if(pole==='dead') return pole;
    return landLeft(n, eweda.reverse(pole));
});
var result = eweda.pipe(landLeft(10), landRight(1), landRight(8))([0,0]);
console.log(result);
// => dead

完整代码


5.1.2 现在来试试用 Either

我们先把皮尔斯放进 Either 盒子里, 这样皮尔斯的状态只有打开 Either 才能看见. 假设 Either Right 是活着, Left 的话皮尔斯挂了.

var land = eweda.curry(function(lr, n, pole){
    pole[lr] = pole[lr] + n;
    if(Math.abs(pole[0]-pole[1]) > 3) {
      return new Left("dead when land " + n + " became " + pole);
    }
    return new Right(pole);
});

var landLeft = land(0)
var landRight = land(1);

现在落鸟后会返回一个 Either, 要不活着, 要不挂了. 打开盒子的函数可以是这样的

var stillAlive = function(x){
    console.log(x)
}
var dead = function(x){
    console.log('皮尔斯' + x);
}
either(dead, stillAlive, landLeft(2, [0,0]))

好吧, 好像有一点点像了, 但是这只落了一次鸟, 如果我要落好几次呢. 这就需要实现 Either 的 >>= bind 方法了, 如果你还记得前面实现的 Functor, 这里非常像 :

var Monad = function(type, defs) {
  for (name in defs){
    type.prototype[name] = defs[name];
  }
  return type;
};
function Left(value){
  this.value = value
}
function Right(value){
  this.value=value;
}

Monad(Right, {
  bind:function(fn){
    return fn(this.value)
  }
})

Monad(Left, {
  bind: function(fn){
    return this;
  }
})

哦, 对了, either:

either = function(left, right, either){
    if(either.constructor.name === 'Right')
        return right(either.value)
    else
        return left(either.value)
}

我们来试试工作不工作.

var walkInLine = new Right([0,0]);
eitherDeadOrNot = walkInLine.bind(landLeft(2))
    .bind(landRight(5))
either(dead, stillAlive, eitherDeadOrNot)
// => [2,5]
eitherDeadOrNot = walkInLine.bind(landLeft(2))
  .bind(landRight(5))
  .bind(landLeft(3))
  .bind(landLeft(10)
  .bind(landRight(10)))

either(dead, stillAlive, eitherDeadOrNot)
// => "皮尔斯dead when land 10 became 15,5"

完整代码

5.2 到底有什么用呢, Monad

我们来总结下两种做法有什么区别: 1. 一般做法每次都会检查查尔斯挂了没挂, 也就是重复获得之前操作的 context 2. Monad 不对异常做处理, 只是不停地往盒子里加操作. 你可以看到对错误的处理推到了最后取值的 either.

  1. Monad 互相传递的只是盒子, 而一般写法会把异常往下传如 =”dead”=,

这样导致后面的操作都得先判断这个异常.

由于是用 JavaScript, pole 不限定类型, 所以这里单纯的用字符串代表 pole 的异常状态. 但如果换成强类型的 Java, 可能实现就没这么简单了.

看来已经优势已经逐步明显了呢, Monad 里面保留了值的 context, 也就是我们对这个 Monad 可以集中在单独的本次如何操作value, 而不用关心 context.

还有一个 Monad 叫做 Maybe, 实际上皮尔斯的🌰用 Maybe 更为合适, 因为 Maybe 有两种状态, 一种是有值 Just, 一种是没东西 Nothing, 可以自己实现试试.

5.3 Monad 在 JavaScript 中的应用

你知道 ES6有个新的 类型 Promise 吗, 如果不知道, 想必也听过 jQuery 的 $.ajax吧, 但如果你没听过 promise, 说明你没有认真看过他的返回值:

var aPromise = $.ajax({
    url: "https://api.github.com/users/jcouyang/gists"
    dataType: 'jsonp'
    })
aPromise /***
=> Object { state: .Deferred/r.state(),
    always: .Deferred/r.always(),
    then: .Deferred/r.then(),
    promise: .Deferred/r.promise(),
    pipe: .Deferred/r.then(),
    done: b.Callbacks/p.add(),
    fail: b.Callbacks/p.add(),
    progress: b.Callbacks/p.add() }
***/

我们看到返回了好多 Deferred 类型的玩意, 我们来试试这玩意有什么用

anotherPromise = aPromise.then(_ => _.data.forEach(y=> console.log(y.description)))
/* =>
Object { state: .Deferred/r.state(),
    always: .Deferred/r.always(),
    then: .Deferred/r.then(),
    promise: .Deferred/r.promise(),
    pipe: .Deferred/r.then(),
    done: b.Callbacks/p.add(),
    fail: b.Callbacks/p.add(),
    progress: b.Callbacks/p.add() }

"connect cisco anyconnect in terminal"
"为什么要柯里化(curry)"
"批量获取人人影视下载链接"
......
*/

看见没有, 他又返回了同样一个东西, 而且传给 then 的函数可以操作这个对象里面的值. 这个对象其实就是 Promise 了. 为什么说这是 Monad 呢, 来试试再写一次 走钢丝:

这里我们用的是 ES6 的 Promise, 而不用 jQuery Defered, 记得用 firefox 哦. 另外 eweda 可以这样装

var ewd = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
            ewd.src = 'https://rawgit.com/CrossEye/eweda/master/eweda.js';
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(ewd);
eweda.installTo(this);
var land = eweda.curry(function(lr, n, pole){
    pole[lr] = pole[lr] + n;
    if(Math.abs(pole[0]-pole[1]) > 3) {
      return new Promise((resovle,reject)=>reject("dead when land " + n + " became " + pole));
    }
    return new Promise((resolve,reject)=>resolve(pole));
});

var landLeft = land(0)
var landRight = land(1);

Promise.all([0,0])
.then(landLeft(2), _=>_)
.then(landRight(3), _=>_) // => Array [ 2, 3 ]
.then(landLeft(10), _=>_)
.then(landRight(10), _=>_)
.then(_=>console.log(_),_=>console.log(_))
// => "dead when land 10 became 12,3"

这下是不承认 Promise 就是 Monad 了. 原来我们早已在使用这个神秘的 Monad, 再想想 Promise,也没有那么抽象和神秘了.

Footnotes:

1

第二章会详细解释高阶函数和闭包.

2

柯里化会在第二章详细讨论.

3

可以看看es6比较有意思的新特性 http://blog.oyanglul.us/javascript/essential-ecmascript6.html

4

Chrome有一个 feature toggle 可以打开部分 es6 功能 chrome://flags/#enable-javascript-harmony

本文转自:http://blog.oyanglul.us/javascript/functional-javascript.html

Share