写在前面的话 之前在工作群里看到一个排行榜:
在羡慕美国同行的薪水超高的同时,也奇怪为何 CoffeeScript 的生命力超过了自己之前的想象。
JavaScript 本是 Brendan Eich 在10天内做完的急就章之作,在设计之初,即带有大量的大意设计和缺陷。可以说,即使到了 1.5 时代,JavaScript 作为一门现代语言,依然要提防 Douglas Crockford 在《JavaScript:The Good Parts》中提出的种种陷阱。历年来,各路框架作者和超集语言作者,都不断在 JavaScript 上做出各种各样炫目的模式用法和衍生方言,足见其可提高空间之大。包括 CoffeeScript/TypeScript/Dart/Elm 等解决方案的出现,其实就是在倡导使用 Pre-JavaScript 的语言编写抽象逻辑,然后编译成原生 JavaScript 运行。
CoffeeScript 即是 JavaScript 1.5 时代的 Pre-JavaScript 语言中的佼佼者。其设计的语法和句法利用了 Ruby 和 Python 的优点,然而又能去除 JavaScript 中容易产生二义性的部分,可以认为是一种变换写法的 JavaScript 语言子集,也就是美化过的“The Good Parts”。CoffeeScript 的定位,本来是一门 little language,它的目的不是取代 JavaScript,而是用更好的风格来编写 JavaScript,因此其最终目标也是编译成 JavaScript。因此,很多人都把它当做下一代 JavaScript 标准出现以前的过渡用法。
2015年以来,ES6.0以及后来的 ES2015等标准的制定以及现代浏览器对原生语法支持的逐步实现,使得大部分众望所归的语言特性都可以在原生 JavaScript 中找到。旧的 CoffeeScript(CoffeeScript 1) 编译的结果已然不兼容新的ES2015的发展方向,CoffeeScript 作为一个过渡时期的产物,似乎已然完成了它的历史使命。
但 CoffeeScript 的发展并没有停止。CofffeeScript 紧随现代 JavaScript 推出了 CoffeeScript 2。这一版本的 CoffeeScript 不仅保留了大部分上一版本 Ruby/Python 风格的优美语法,也大量兼容了 ES2015 的新特性(除了 import/export 这个在前后端实行起来经常需要转义和 polyfill 的特性以外),成为了一门更加现代的 little langugage。
可能有读者会问,既然已经有了 ES2015,为什么还要再来一门编程语言呢?笔者认为,不同的编程语言,其实是不同的思考和设计工具。虽说图灵完备的语言总是等价的,但通过另一个角度来对问题和解建模,可以更有效地提高自己对原本掌握的语言的理解。因此,了解 CoffeeScript 的设计和使用理念,一定能对使用原生 JavaScript 编程有所脾益。
本文基本上是 coffeescript.org 在2.0版本后文档的摘译和简化。每一个小的知识点会配上若干的代码块,两个相连的代码块总是代表一段 CoffeeScript 代码和它编译生成的 JavaScript 代码。阅读本文需要一定的 JavaScript 基础。
简明教程 CoffeeScript 是什么? CoffeeScript 是一门编译成 Javascript 的小语言。 在 Java 风格的笨拙锈色之下,JavaScript 有着一颗华丽的心。CoffeeScript 试图用简明的方式,把 JavaScript 中的精粹部分表现出来。
CoffeeScript 的黄金法则是:“它只是 JavaScript”。代码会被一对一地编译成对等的 JS,不会有运行时解释。可以在 CoffeeScript 中无缝地使用任何现存的 JavaScript 库(反之亦然)。编译输出是可读,美化打印过的,而且趋向于和等价的手写 JavaScript 跑得一样快。
安装和试用 coffee 最新版本:2.1.0
1 2 3 4 5 npm install --save-dev coffeescript npm install --global coffeescript
假设我们有一个 test.coffee 的文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 number = 42 opposite = true number = -42 if oppositesquare = (x) -> x * x list = [1 , 2 , 3 , 4 , 5 ] math = root: Math .sqrt square: square cube: (x) -> x * square xrace = (winner, runners...) -> print winner, runners alert "I knew it!" if elvis? cubes = (math.cube num for num in list)
运行coffee -c test.coffee
就会得到一个test.js 文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 (function ( ) { var cubes, list, math, num, number, opposite, race, square; number = 42 ; opposite = true ; if (opposite) { number = -42 ; } square = function (x ) { return x * x; }; list = [1 , 2 , 3 , 4 , 5 ]; math = { root : Math .sqrt , square : square, cube : function (x ) { return x * square (x); } }; race = function (winner, ...runners ) { return print (winner, runners); }; if (typeof elvis !== "undefined" && elvis !== null ) { alert ("I knew it!" ); } cubes = (function ( ) { var i, len, results; results = []; for (i = 0 , len = list.length ; i < len; i++) { num = list[i]; results.push (math.cube (num)); } return results; })(); }).call (this );
缺省的 coffee 编译结果为了最大限度地保证不污染顶层的变量,总是会把文件编译成一个立即执行的函数,使得所有变量的声明和使用局限在一个小作用域里面。当然,它也有个 bare 模式可以去掉这种立即执行函数,如果使用import
和export
的功能,也可以自动进入 bare 模式。这个模式的细节很繁琐,请读者自行查阅文档。
普通函数 1 2 3 4 5 6 7 8 9 square = (x) -> x * xcube = (x) -> square(x) * xfill = (container, liquid = "coffee" ) -> "Filling the #{container} with #{liquid} ..." a = ->
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 var cube, square; square = function (x ) { return x * x; }; cube = function (x ) { return square (x) * x; };var fill; fill = function (container, liquid = "coffee" ) { return `Filling the ${container} with ${liquid} ...` ; }; var a; a = function ( ) {};
使用->
生成 function 函数。带默认值的参数,同样是 ES2015 的内容。
字符串 1 2 3 4 author = "Wittgenstein" quote = "A picture is a fact. -- #{ author } " sentence = "#{ 22 / 7 } is a decent approximation of π"
1 2 3 4 5 6 7 var author, quote, sentence; author = "Wittgenstein" ; quote = `A picture is a fact. -- ${author} ` ; sentence = `${22 / 7 } is a decent approximation of π` ;
CoffeeScript 的字符串同 JavaScript 一样,可以用"
和'
分界。用”引用的字符串,可以用#{}
来进行内插(甚至可以在对象的key 里执行内插)。用’引用的字符串是字面量。
双引号的多行字符串可以自动被编译器连接起来,当然,所有的缩进都失效了:
1 2 3 4 5 6 mobyDick = "Call me Ishmael. Some years ago -- never mind how long precisely -- having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world..."
1 2 3 var mobyDick; mobyDick = "Call me Ishmael. Some years ago -- never mind how long precisely -- having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world..." ;
使用三引号"""
和三个单引号'''
,还可以生成保留格式(特别是缩进)的块 字符串:
1 2 3 4 5 6 7 8 9 10 11 html = """ <strong> cup of coffeescript </strong> """ html = ''' <strong> cup of coffeescript </strong> '''
1 2 3 4 5 6 7 var html; html = "<strong>\n cup of coffeescript\n</strong>" ;var html; html = '<strong>\n cup of coffeescript\n</strong>' ;
用双引号制造出来的块字符串,也一样支持字符串内插功能。
对象和数组 CoffeeScript 支持 JavaScript 格式的对象和数组字面量,也支持更简明的无逗号的、依靠换行的字面量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 song = ["do" , "re" , "mi" , "fa" , "so" ] singers = {Jagger: "Rock" , Elvis: "Roll" } bitlist = [ 1 , 0 , 1 0 , 0 , 1 1 , 1 , 0 ] kids = brother: name: "Max" age: 11 sister: name: "Ida" age: 9
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 var bitlist, kids, singers, song; song = ["do" , "re" , "mi" , "fa" , "so" ]; singers = { Jagger : "Rock" , Elvis : "Roll" }; bitlist = [1 , 0 , 1 , 0 , 0 , 1 , 1 , 1 , 0 ]; kids = { brother : { name : "Max" , age : 11 }, sister : { name : "Ida" , age : 9 } };
CoffeeScript 也支持 ES2015的字面量语法:
1 2 3 4 name = "Michelangelo" mask = "orange" weapon = "nunchuks" turtle = {name, mask, weapon}
1 2 3 4 5 6 7 8 9 var mask, name, output, turtle, weapon; name = "Michelangelo" ; mask = "orange" ; weapon = "nunchuks" ; turtle = {name, mask, weapon};
注释 聪明的读者可能已经发现了注释应该怎么写,但这里还是要总结下。CoffeeScript 里的注释其实是脚本语言的注释的应用:
1 2 3 4 5 6 7 sayFortune = (fortune) -> console.log fortune
1 2 3 4 5 6 7 8 9 10 var sayFortune; sayFortune = function (fortune ) { return console .log (fortune); };
值得注意的是,###
支持了类型注解。
词法作用域和变量安全 聪明的读者可能也发现了,在 CoffeeScript 中,是不需要手写var
这样的关键字的。实际上,ES2015为了解决过去的 JavaScript 中不存在块级作用域这样的问题,专门提出的const
和let
解决方案,CoffeeScript 也不支持。在有多层变量的时候,CoffeeScript 会自动地推导变量的作用域 ,保证内层的变量绝不会污染任何外层变量。每个变量的实际作用域,会被限制在它首次被声明的地方。JavaScript 无意之中忘加var
关键字而污染全局变量的情况便不复存在了。
1 2 3 4 5 outer = 1 changeNumbers = -> inner = -1 outer = 10 inner = changeNumbers()
1 2 3 4 5 6 7 8 9 10 11 var changeNumbers, inner, outer; outer = 1 ; changeNumbers = function ( ) { var inner; inner = -1 ; return outer = 10 ; }; inner = changeNumbers ();
outer
因为在外部作用域里已经声明过了,所以不需要重复声明。而inner
只是在内部作用域里被使用,所以还额外声明了一个函数内的 inner
来专门隔离它的作用域。外部专门声明的var inner
其实就是一个编译器为了谨慎做的声明顶部上推,在这个编译的例子里不影响任何语义。
因为无法使用var
关键字,所以实际上我们是无法遮蔽(shadow)住外部变量的,只能在内部作用域引用外部变量。
If,Else,Unless, 与条件赋值 if/else 里可以不用写小括号和大括号,使用 python 式的缩进定界,用户也可以使用单行 if 和 unless。
1 2 3 4 5 6 7 8 9 10 mood = greatlyImproved if singingif happy and knowsIt clapsHands() chaChaCha()else showIt() date = if friday then sue else jill
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var date, mood;if (singing) { mood = greatlyImproved; }if (happy && knowsIt) { clapsHands (); chaChaCha (); } else { showIt (); } date = friday ? sue : jill;
不定参数语法 Java 程序员里面可能都习惯了类似 … 语法的不定参数语法来了。在 CoffeeScript 中它被称为 Splats 参数。ES2015吸收了这一语法,做出了 rest 参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 gold = silver = rest = "unknown" awardMedals = (first, second, others...) -> gold = first silver = second rest = others contenders = [ "Michael Phelps" "Liu Xiang" "Yao Ming" "Allyson Felix" "Shawn Johnson" "Roman Sebrle" "Guo Jingjing" "Tyson Gay" "Asafa Powell" "Usain Bolt" ] awardMedals contenders... alert """ Gold: #{gold} Silver: #{silver} The Field: #{rest.join ', ' } """ popular = ['pepperoni' , 'sausage' , 'cheese' ] unwanted = ['anchovies' , 'olives' ] all = [popular..., unwanted..., 'mushrooms' ] user = name: 'Werner Heisenberg' occupation: 'theoretical physicist' currentUser = { user..., status: 'Uncertain' }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 var awardMedals, contenders, gold, rest, silver; gold = silver = rest = "unknown" ; awardMedals = function (first, second, ...others ) { gold = first; silver = second; return rest = others; }; contenders = ["Michael Phelps" , "Liu Xiang" , "Yao Ming" , "Allyson Felix" , "Shawn Johnson" , "Roman Sebrle" , "Guo Jingjing" , "Tyson Gay" , "Asafa Powell" , "Usain Bolt" ];awardMedals (...contenders);alert (`Gold: ${gold} \nSilver: ${silver} \nThe Field: ${rest.join(', ' )} ` ); # 特殊的数组省略语法var all, popular, unwanted; popular = ['pepperoni' , 'sausage' , 'cheese' ]; unwanted = ['anchovies' , 'olives' ]; all = [...popular, ...unwanted, 'mushrooms' ];var currentUser, user, _extends = Object .assign || function (target ) { for (var i = 1 ; i < arguments .length ; i++) { var source = arguments [i]; for (var key in source) { if (Object .prototype .hasOwnProperty .call (source, key)) { target[key] = source[key]; } } } return target; }; user = { name : 'Werner Heisenberg' , occupation : 'theoretical physicist' }; currentUser = _extends ({}, user, { status : 'Uncertain' });
循环和表处理 CoffeeScript 的 for 循环可以处理数组、对象和 range,有点类似 Python。而且,它的 for 循环是有返回值的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 eat = (food) -> "#{food} eaten." eat food for food in ['toast' , 'cheese' , 'wine' ] courses = ['greens' , 'caviar' , 'truffles' , 'roast' , 'cake' ]menu = (i, dish) -> "Menu Item #{i} : #{dish} " menu i + 1 , dish for dish, i in courses foods = ['broccoli' , 'spinach' , 'chocolate' ] eat food for food in foods when food isnt 'chocolate' countdown = (num for num in [10. .1 ]) yearsOld = max: 10 , ida: 9 , tim: 11 ages = for child, age of yearsOld "#{child} is #{age} "
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 var courses, dish, eat, food, foods, i, j, k, l, len, len1, len2, menu, ref; eat = function (food ) { return `${food} eaten.` ; }; ref = ['toast' , 'cheese' , 'wine' ];for (j = 0 , len = ref.length ; j < len; j++) { food = ref[j]; eat (food); } courses = ['greens' , 'caviar' , 'truffles' , 'roast' , 'cake' ]; menu = function (i, dish ) { return `Menu Item ${i} : ${dish} ` ; };for (i = k = 0 , len1 = courses.length ; k < len1; i = ++k) { dish = courses[i]; menu (i + 1 , dish); } foods = ['broccoli' , 'spinach' , 'chocolate' ];for (l = 0 , len2 = foods.length ; l < len2; l++) { food = foods[l]; if (food !== 'chocolate' ) { eat (food); } }var countdown, num; countdown = (function ( ) { var i, results; results = []; for (num = i = 10 ; i >= 1 ; num = --i) { results.push (num); } return results; })();var age, ages, child, yearsOld; yearsOld = { max : 10 , ida : 9 , tim : 11 }; ages = (function ( ) { var results; results = []; for (child in yearsOld) { age = yearsOld[child]; results.push (`${child} is ${age} ` ); } return results; })();
特别地,CoffeScript 提供一个低级的 while 循环,它同样是带有返回值的。
1 2 3 4 5 6 7 8 9 10 if this.studyingEconomics buy() while supply > demand sell() until supply > demand num = 6 lyrics = while num -= 1 "#{num} little monkeys, jumping on the bed. One fell out and bumped his head."
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 var lyrics, num;if (this .studyingEconomics ) { while (supply > demand) { buy (); } while (!(supply > demand)) { sell (); } } num = 6 ; lyrics = (function ( ) { var results; results = []; while (num -= 1 ) { results.push (`${num} little monkeys, jumping on the bed. One fell out and bumped his head.` ); } return results; })();
JavaScript 老手可能会很习惯匿名函数立即执行的用法,CoffeeScript 用 do 关键字支持把循环和匿名函数立即执行结合起来:
1 2 3 4 5 for filename in list do (filename) -> if filename not in ['.DS_Store' , 'Thumbs.db' , 'ehthumbs.db' ] fs.readFile filename, (err, contents) -> compile filename, contents.toString()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var filename, fn, i, len; fn = function (filename ) { if (filename !== '.DS_Store' && filename !== 'Thumbs.db' && filename !== 'ehthumbs.db' ) { return fs.readFile (filename, function (err, contents ) { return compile (filename, contents.toString ()); }); } };for (i = 0 , len = list.length ; i < len; i++) { filename = list[i]; fn (filename); }
数组分片和 range 分片 CoffeeScript 同样以索引操作符的形式(类 Python 和 C++)支持对数组的 slicing。
1 2 3 4 5 6 7 8 9 10 11 12 13 numbers = [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ] start = numbers[0. .2 ] middle = numbers[3. ..-2 ] end = numbers[-2. .] copy = numbers[..] numbers = [0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ] numbers[3. .6 ] = [-3 , -4 , -5 , -6 ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var copy, end, middle, numbers, start; numbers = [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ]; start = numbers.slice (0 , 3 ); middle = numbers.slice (3 , -2 ); end = numbers.slice (-2 ); copy = numbers.slice (0 );var numbers, ref, splice = [].splice ; numbers = [0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ]; splice.apply (numbers, [3 , 4 ].concat (ref = [-3 , -4 , -5 , -6 ])), ref;
一切皆表达式 表达式特别于普通语句的地方,就是它们总是可以求值的。 诸位读者可能已经注意到,CoffeeScript 中几乎没有 return。在函数中,最后一行表达式,就是整个块的表达式返回值,这点也是 CoffeeScript 从 Ruby 那里吸收来的特性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 grade = (student) -> if student.excellentWork "A+" else if student.okayStuff if student.triedHard then "B" else "B-" else "C" eldest = if 24 > 21 then "Liz" else "Ike" six = (one = 1 ) + (two = 2 ) + (three = 3 ) globals = (name for name of window)[0. ..10 ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 var eldest, grade; grade = function (student ) { if (student.excellentWork ) { return "A+" ; } else if (student.okayStuff ) { if (student.triedHard ) { return "B" ; } else { return "B-" ; } } else { return "C" ; } }; eldest = 24 > 21 ? "Liz" : "Ike" ;var one, six, three, two; six = (one = 1 ) + (two = 2 ) + (three = 3 );var globals, name; globals = ((function ( ) { var results; results = []; for (name in window ) { results.push (name); } return results; })()).slice (0 , 10 );
比较有意思的是,try/catch 也是可以有返回值的:
1 2 3 4 5 6 7 alert( try nonexistent / undefined catch error "And the error is ... #{error} " )
1 2 3 4 5 6 7 8 9 10 var error;alert ((function ( ) { try { return nonexistent / void 0 ; } catch (error1) { error = error1; return `And the error is ... ${error} ` ; } })());
当然,有些语句在 JavaScript 中也是不能当做表达式的,比如break
、continue
和return
,如果你在代码块中使用了它们,CoffeeScript不会试图进行转换。
操作符与别名 ==
操作符经常引起出乎意料的、与其他语言中表现不一致的行为。所以 CoffeeScript 里没有==
,而会试着进行再编译。把==
编译成===
,!=
编译成!==
。另外,它还提供了可读性更好的两个操作符,is
会被编译为===
,isnt 会被编译为isnt
。
除此之外,还可以用not
作取反操作符!
的别名。
对于逻辑操作符,and
会被编译为&&
,or
会被便以为||
。
在 JavaScript 中,有时候条件语句需要另起一行或者在单行内用分号断句,但CoffeeScript 中的 then 就可以帮我们把条件语句(特别是 while、if、switch 的结构中)和被执行语句连起来。
同YAML
中一样,on
和yes
是布尔值true
的同义词,off
和no
是布尔值false
的同义词。
同其他脚本语言一样,unless
是if
的反写。
同 Ruby 一样,@property
可以当做this.property
来用(少写一个字符)。
可以用in
来做数组元素的存在检查,用of
来做 JavaScript 键值对的存在性检查。
在for
循环中,from
关键字会被编译成 ES2015 的of
。
为了简化数学表达式,**
代表幂运算,而//
执行地板除法(同 Python 一样,去掉小数向下取整)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 -7 % 5 == -2 -7 %% 5 == 3 tabs.selectTabAtIndex((tabs.currentIndex - count) %% tabs.length) launch() if ignition is on volume = 10 if band isnt SpinalTap letTheWildRumpusBegin() unless answer is no if car.speed < limit then accelerate() winner = yes if pick in [47 , 92 , 13 ]print inspect "My name is #{@name} "
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 var modulo = function (a, b ) { return (+a % (b = +b) + b) % b; }; -7 % 5 === -2 ; modulo (-7 , 5 ) === 3 ; tabs.selectTabAtIndex (modulo (tabs.currentIndex - count, tabs.length ));var volume, winner;if (ignition === true ) { launch (); }if (band !== SpinalTap ) { volume = 10 ; }if (answer !== false ) { letTheWildRumpusBegin (); }if (car.speed < limit) { accelerate (); }if (pick === 47 || pick === 92 || pick === 13 ) { winner = true ; }print (inspect (`My name is ${this .name} ` ));
存在操作符 在 JavaScript 中确认一个变量是否存在是很困难的。因为if (variable)
不仅会在变量不存在时生效,在变量为0值(0,空字符串,false)时也会生效。CoffeeScript 的?
操作符只在变量为null
和 undefined
的时候返回false
,这很像 Ruby 中的 nil?
(实际上高版本的 ES 也会有一个叫 Null Propagation Operator
的类似特性)。
这样我们就可以做更加安全的条件赋值了(比a = a || value
安全)。
1 2 3 4 5 6 solipsism = true if mind? and not world? speed = 0 speed ?= 15 footprints = yeti ? "bear"
1 2 3 4 5 6 7 8 9 10 11 var footprints, solipsism, speed;if ((typeof mind !== "undefined" && mind !== null ) && (typeof world === "undefined" || world === null )) { solipsism = true ; } speed = 0 ;if (speed == null ) { speed = 15 ; }
注意看,一个?
被翻译成了对undefined
和null
的严格求不等!==
。但是,?
与unless
搭配的时候,就会被翻译成==
。
1 2 3 4 major = 'Computer Science' unless major? signUpForClass 'Introduction to Wines'
1 2 3 4 5 6 7 var major; major = 'Computer Science' ;if (major == null ) { signUpForClass ('Introduction to Wines' ); }
可以用存在操作符来达到其他语言中经常出现的流利调用,流利调用失败用户会得到undefined
而不会出现烦人的空指针(在 JavaScript 中实际上是不能在undefined
上调用特定成员的TypeError
)异常:
1 zip = lottery.drawWinner?().address?.zipcode
1 2 3 var ref, zip; zip = typeof lottery.drawWinner === "function" ? (ref = lottery.drawWinner ().address ) != null ? ref.zipcode : void 0 : void 0 ;
无括号的链式调用 用.
和断行缩进可以像其他脚本语言一样进行无括号链式调用:
1 2 3 4 5 6 $ 'body' .click (e) -> $ '.box' .fadeIn 'fast' .addClass 'show' .css 'background' , 'white'
1 2 3 $('body' ).click (function (e ) { return $('.box' ).fadeIn ('fast' ).addClass ('show' ); }).css ('background' , 'white' );
解构赋值 用过 ES2015 以后版本的读者应该能够理解什么是解构赋值了:
1 2 3 4 theBait = 1000 theSwitch = 0 [theBait, theSwitch] = [theSwitch, theBait]
1 2 3 4 5 6 7 var theBait, theSwitch; theBait = 1000 ; theSwitch = 0 ; [theBait, theSwitch] = [theSwitch, theBait];
解构赋值搭配上多返回值的函数调用也很有用:
1 2 3 4 5 weatherReport = (location) -> [location, 72 , "Mostly Sunny" ] [city, temp, forecast] = weatherReport "Berkeley, CA"
1 2 3 4 5 6 7 8 var city, forecast, temp, weatherReport; weatherReport = function (location ) { return [location, 72 , "Mostly Sunny" ]; }; [city, temp, forecast] = weatherReport ("Berkeley, CA" );
解构赋值对任意深度嵌套的数组和对象也是一个有用的特性:
1 2 3 4 5 6 7 8 9 10 11 12 13 futurists = sculptor: "Umberto Boccioni" painter: "Vladimir Burliuk" poet: name: "F.T. Marinetti" address: [ "Via Roma 42R" "Bellagio, Italy 22021" ] {sculptor} = futurists {poet: {name, address: [street, city]}} = futurists
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 var city, futurists, name, sculptor, street; futurists = { sculptor : "Umberto Boccioni" , painter : "Vladimir Burliuk" , poet : { name : "F.T. Marinetti" , address : ["Via Roma 42R" , "Bellagio, Italy 22021" ] } }; ({sculptor} = futurists); ({ poet : { name, address : [street, city] } } = futurists);
解构赋值也可以搭配 splats 不定参数使用:
1 2 3 tag = "<impossible>" [open, contents..., close] = tag.split("" )
1 2 3 4 5 6 var close, contents, i, open, ref, tag, slice = [].slice ; tag = "<impossible>" ; ref = tag.split ("" ), open = ref[0 ], contents = 3 <= ref.length ? slice.call (ref, 1 , i = ref.length - 1 ) : (i = 1 , []), close = ref[i++];
取数组末尾元素的例子:
1 2 3 4 text = "Every literary critic believes he will outwit history and have the last word" [first, ..., last] = text.split " "
1 2 3 4 5 var first, last, ref, text; text = "Every literary critic believes he will outwit history and have the last word" ; ref = text.split (" " ), first = ref[0 ], last = ref[ref.length - 1 ];
与构造函数结合的例子:
1 2 3 4 5 class Person constructor: (options) -> {@name, @age, @height = 'average' } = options tim = new Person name: 'Tim' , age: 4
1 2 3 4 5 6 7 8 9 10 11 12 13 var Person , tim;Person = class Person { constructor (options ) { ({name : this .name , age : this .age , height : this .height = 'average' } = options); } }; tim = new Person ({ name : 'Tim' , age : 4 });
绑定(胖)函数 用->
定义的函数会直接转化为普通的function
。
JavaScript 中this
指向的值可以被动态绑定(这真是由来已久的弊病)。有经验的读者肯定能理解,我们在传递回调的时候, 原始this
经常会丢失,变成新作用域里的this
。
而使用胖箭头=>
定义的函数,可以把当前上下文的this
绑定到函数里(好像调用了一个隐式的bind
一样)。当我们在 Prototype 和 JQuery 之类的毁掉库里使用函数时,这回变得非常有用。
1 2 3 4 5 6 7 8 Account = (customer, cart) -> @customer = customer @cart = cart $('.shopping_cart' ).on 'click' , (event) => @customer.purchase @cart
1 2 3 4 5 6 7 8 9 10 var Account ;Account = function (customer, cart ) { this .customer = customer; this .cart = cart; return $('.shopping_cart' ).on ('click' , (event ) => { return this .customer .purchase (this .cart ); }); };
如果我们在这里使用->
,函数里的@customer
实际上就会指向undefined。因为$(‘.shopping_cart’) 这个东西指向的是 dom 变量,很可能没有customer
这个属性。
胖函数是 CoffeeScript 里最受欢迎的特性,也被 ES2015 吸收了,使用 CoffeeScript 中的=>
就会被编译成JavaScript 中的=>
。
生成器函数 CoffeeScript 通过yield
关键字支持 ES2015中的generator functions 。CoffeeScript 中没有function*(){}
这样的无意义结构,只要有yield
就够了。
1 2 3 4 5 6 7 8 perfectSquares = -> num = 0 loop num += 1 yield num * num return window.ps or = perfectSquares()
1 2 3 4 5 6 7 8 9 10 var perfectSquares; perfectSquares = function *() { var num; num = 0 ; while (true ) { num += 1 ; yield num * num; } };
yield
当然也可以配合for...from
使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 fibonacci = -> [previous, current] = [1 , 1 ] loop [previous, current] = [current, previous + current] yield current return getFibonacciNumbers = (length) -> results = [1 ] for n from fibonacci() results.push n break if results.length is length results
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var fibonacci, getFibonacciNumbers; fibonacci = function *() { var current, previous; [previous, current] = [1 , 1 ]; while (true ) { [previous, current] = [current, previous + current]; yield current; } }; getFibonacciNumbers = function (length ) { var n, ref, results; results = [1 ]; ref = fibonacci (); for (n of ref) { results.push (n); if (results.length === length) { break ; } } return results; };
异步函数 ES2017 的异步函数 是通过await
支持的。同生成器函数一样,CoffeeScript 中的不需要async
关键字,只要有await
就行了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 sleep = (ms) -> new Promise (resolve) -> window.setTimeout resolve, mssay = (text) -> window.speechSynthesis.cancel() window.speechSynthesis.speak new SpeechSynthesisUtterance textcountdown = (seconds) -> for i in [seconds..1 ] say i await sleep 1000 say "Blastoff!" countdown 3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 var countdown, say, sleep; sleep = function (ms ) { return new Promise (function (resolve ) { return window .setTimeout (resolve, ms); }); }; say = function (text ) { window .speechSynthesis .cancel (); return window .speechSynthesis .speak (new SpeechSynthesisUtterance (text)); }; countdown = async function (seconds ) { var i, j, ref; for (i = j = ref = seconds; ref <= 1 ? j <= 1 : j >= 1 ; i = ref <= 1 ? ++j : --j) { say (i); await sleep (1000 ); } return say ("Blastoff!" ); };countdown (3 );
类 CoffeeScript 1 就提供了class
和extends
关键字,作为原型相关函数的语法糖。ES2015吸收了这两个关键字,CoffeeScript 2 直接把这两个关键字编译成 ES2015的类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Animal constructor: (@name) -> move: (meters) -> alert @name + " moved #{meters} m." class Snake extends Animal move: -> alert "Slithering..." super 5 class Horse extends Animal move: -> alert "Galloping..." super 45 sam = new Snake "Sammy the Python" tom = new Horse "Tommy the Palomino" sam.move() tom.move()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 var Animal , Horse , Snake , sam, tom;Animal = class Animal { constructor (name ) { this .name = name; } move (meters ) { return alert (this .name + ` moved ${meters} m.` ); } };Snake = class Snake extends Animal { move ( ) { alert ("Slithering..." ); return super .move (5 ); } };Horse = class Horse extends Animal { move ( ) { alert ("Galloping..." ); return super .move (45 ); } }; sam = new Snake ("Sammy the Python" ); tom = new Horse ("Tommy the Palomino" ); sam.move (); tom.move ();
可以用@
开头的函数来定义静态方法:
1 2 3 4 5 6 7 8 9 class Teenager @say: (speech) -> words = speech.split ' ' fillers = ['uh' , 'um' , 'like' , 'actually' , 'so' , 'maybe' ] output = [] for word, index in words output.push word output.push fillers[Math .floor(Math .random() * fillers.length)] unless index is words.length - 1 output.join ', '
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 var Teenager ;Teenager = class Teenager { static say (speech ) { var fillers, i, index, len, output, word, words; words = speech.split (' ' ); fillers = ['uh' , 'um' , 'like' , 'actually' , 'so' , 'maybe' ]; output = []; for (index = i = 0 , len = words.length ; i < len; index = ++i) { word = words[index]; output.push (word); if (index !== words.length - 1 ) { output.push (fillers[Math .floor (Math .random () * fillers.length )]); } } return output.join (', ' ); } };
原型式继承 ES2015提供了一个短路操作符给原型链相关的操作:
1 2 String::dasherize = -> this.replace /_/g , "-"
1 2 3 String .prototype .dasherize = function ( ) { return this .replace (/_/g , "-" ); };
Switch/When/Else CoffeeScript 中的switch
不用担心忘记写break
出现错误的跳转:
1 2 3 4 5 6 7 8 9 10 switch day when "Mon" then go work when "Tue" then go relax when "Thu" then go iceFishing when "Fri" , "Sat" if day is bingoDay go bingo go dancing when "Sun" then go church else go work
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 switch (day) { case "Mon" : go (work); break ; case "Tue" : go (relax); break ; case "Thu" : go (iceFishing); break ; case "Fri" : case "Sat" : if (day === bingoDay) { go (bingo); go (dancing); } break ; case "Sun" : go (church); break ; default : go (work); }
在之前读者已经看到了 CoffeeScript 中大量的语句都可以带有返回值,switch
也不例外。以下的例子里面,switch 后面连表达式都没有:
1 2 3 4 5 6 7 8 score = 76 grade = switch when score < 60 then 'F' when score < 70 then 'D' when score < 80 then 'C' when score < 90 then 'B' else 'A'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var grade, score; score = 76 ; grade = (function ( ) { switch (false ) { case !(score < 60 ): return 'F' ; case !(score < 70 ): return 'D' ; case !(score < 80 ): return 'C' ; case !(score < 90 ): return 'B' ; default : return 'A' ; } })();
Try/Catch/Finally CoffeeScript 中的try
/catch
/finally
块可以完全不带大括号:
1 2 3 4 5 6 7 try allHellBreaksLoose() catsAndDogsLivingTogether()catch error print errorfinally cleanUp()
1 2 3 4 5 6 7 8 9 10 11 var error;try { allHellBreaksLoose (); catsAndDogsLivingTogether (); } catch (error1) { error = error1; print (error); } finally { cleanUp (); }
链式比较 CoffeeScript 从 Python 中借来了链式比较 ,这样范围比较的语句就变得更加简单了。
1 2 3 cholesterol = 127 healthy = 200 > cholesterol > 60
1 2 3 4 5 var cholesterol, healthy; cholesterol = 127 ; healthy = (200 > cholesterol && cholesterol > 60 );
块状的正则表达式 我们已经看到了块状(多行)字符串和注释,CoffeeScript 同样支持块状正则表达式。模仿自 Perl 的/x
修饰符,CoffeeScript 使用///
来对多行表达式定界,这样多行的正则比单行的正则就更加可读了:
1 2 3 4 5 6 NUMBER = /// ^ 0b[01]+ | ^ 0o[0-7]+ | ^ 0x[\da-f]+ | ^ \d*\.?\d+ (?:e[+-]?\d+)? /// i
1 2 3 4 5 6 var NUMBER ;NUMBER = /^0b[01]+|^0o[0-7]+|^0x[\da-f]+|^\d*\.?\d+(?:e[+-]?\d+)?/i ;
标签化的模板字面量 CoffeeScript 支持 ES2015中的标签化模板字面量 ,提供了自定义的字符串内插的能力。
1 2 3 4 5 6 7 8 upperCaseExpr = (textParts, expressions...) -> textParts.reduce (text, textPart, i) -> text + expressions[i - 1 ].toUpperCase() + textPartgreet = (name, adjective) -> upperCaseExpr""" Hi #{name} . You look #{adjective} ! """
1 2 3 4 5 6 7 8 9 10 11 var greet, upperCaseExpr; upperCaseExpr = function (textParts, ...expressions ) { return textParts.reduce (function (text, textPart, i ) { return text + expressions[i - 1 ].toUpperCase () + textPart; }); }; greet = function (name, adjective ) { return upperCaseExpr`Hi ${name} . You look ${adjective} !` ; };
模块 CoffeeScript 中的模块和 ES2015中的模块很相似,都是import
和export
语法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import 'local-file.coffee' import 'coffeescript' import _ from 'underscore' import * as underscore from 'underscore' import { now } from 'underscore' import { now as currentTimestamp } from 'underscore' import { first, last } from 'underscore' import utilityBelt, { each } from 'underscore' export default Math export square = (x) -> x * xexport class Mathematics least: (x, y) -> if x < y then x else yexport { sqrt }export { sqrt as squareRoot }export { Mathematics as default , sqrt as squareRoot }export * from 'underscore' export { max, min } from 'underscore'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 import 'local-file.coffee' ;import 'coffeescript' ;import _ from 'underscore' ;import * as underscore from 'underscore' ;import { now } from 'underscore' ;import { now as currentTimestamp } from 'underscore' ;import { first, last } from 'underscore' ;import utilityBelt, { each } from 'underscore' ;export default Math ;export var square = function (x ) { return x * x; };export var Mathematics = class Mathematics { least (x, y ) { if (x < y) { return x; } else { return y; } } };export { sqrt };export { sqrt as squareRoot };export { Mathematics as default , sqrt as squareRoot };export * from 'underscore' ;export { max, min } from 'underscore' ;
嵌入式 JavaScript 在 CoffeeScript 中可以用倒引号(`)直接引用 JavaScript 代码:
1 2 3 4 5 6 7 8 hi = `function ( ) { return [document .title , "Hello JavaScript" ].join (": " ); } ` markdown = `function ( ) { return \ `In Markdown, write code like \\\`this \\\ `\`; } `
1 2 3 4 5 6 7 8 9 10 11 var hi; hi = function ( ) { return [document .title , "Hello JavaScript" ].join (": " ); };var markdown; markdown = function ( ) { return `In Markdown, write code like \`this\`` ; };
用三个倒引号可以很轻松地引用一大块 JavaScript 代码:
1 2 3 4 5 \```function time ( ) { return `The time is ${new Date ().toLocaleTimeString()} ` ; } \ ```
1 2 3 4 function time ( ) { return `The time is ${new Date ().toLocaleTimeString()} ` ; } ;
JSX CoffeeScript 不需要专门的插件或者设置,就可以理解 JSX。
同普通的 JSX 一样,<
和>
指明了XML 元素,由 {
和}
包围的代码会被内插替换。为了避免和大于、小于的比较混淆,类似i < len
的代码必须带空格,不能写作i<len
。编译器有时候能够在你忘记加空格的时候,猜到你的意图,但不要心怀侥幸,还是要尽量自己加空格。
1 2 3 4 5 6 7 8 9 renderStarRating = ({ rating, maxStars }) -> <aside title={"Rating: #{rating} of #{maxStars} stars" }> {for wholeStar in [0. ..Math .floor(rating)] <Star className="wholeStar" key={wholeStar} />} {if rating % 1 isnt 0 <Star className="halfStar" />} {for emptyStar in [Math .ceil(rating)...maxStars] <Star className="emptyStar" key={emptyStar} />} </aside>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 var renderStarRating; renderStarRating = function ({rating, maxStars} ) { var emptyStar, wholeStar; return <aside title ={ `Rating: ${rating } of ${maxStars } stars `}> {(function() { var i, ref, results; results = []; for (wholeStar = i = 0, ref = Math.floor(rating); 0 <= ref ? i < ref : i > ref; wholeStar = 0 <= ref ? ++i : --i) { results.push(<Star className ="wholeStar" key ={wholeStar} /> ); } return results; })()} {(rating % 1 !== 0 ? <Star className ="halfStar" /> : void 0)} {(function() { var i, ref, ref1, results; results = []; for (emptyStar = i = ref = Math.ceil(rating), ref1 = maxStars; ref <= ref1 ? i < ref1 : i > ref1; emptyStar = ref <= ref1 ? ++i : --i) { results.push(<Star className ="emptyStar" key ={emptyStar} /> ); } return results; })()} </aside > ; };
类型注解 Flow 风格的注释式类型注解可以在 CoffeeScript 中提供静态类型检查的能力(TypeScript 的粉丝可能要头痛了):
1 2 3 4 5 6 7 8 9 10 11 fn = (str , obj ) -> str + obj.num
1 2 3 4 5 6 7 8 9 10 11 var fn; fn = function (str, obj ) { return str + obj.num ; };
如果用户已经成功安装了 Flow 和最新的 CoffeeScript ,可以用如下命令进行类型检查:
1 coffee --bare --no-header --compile app.coffee && npm run flow
--bare --no-header
非常重要,因为 Flow 要求文件中的第一行是// @flow
注释。