_
# 点点符号..是虚拟方法,它让使用underscore和lodash变得前所未有地简单。
# 忘掉_()和.value()吧。
console.log [2, 3, 4]..max()
console.log [5, 3, [2, 4], 1]..flatten()..sortBy()
# <>是钻石函数,表示无参数,先不用去管它,运行代码,然后看下一个示例。
# 下个示例会告诉你冒号和等号是什么意思。
]]>
,Windows用户可以在编辑器中自定义快捷键,就像
# 我们在这个编辑器中定义的一样。
# 有的编辑器支持用Tab键插入一段预定义代码,那你就可以定义"="+"Tab"为插入≠。
]]>
q: encodeURIComponent("
select * from yahoo.finance.quotes where symbol in ("AAPL")
")
env: encodeURIComponent("store://datatables.org/alltableswithkeys")
query: "?q=\(q)&env=\(env)&format=json"
console.log "正在查询股价..."
response: web.jsonGet("https://query.yahooapis.com/v1/public/yql\(query)")'wait
quote: response.body.query.results.quote
console.log "苹果公司 买入价: \(quote.Bid), 卖出价: \(quote.Ask)"
# 忘了“恐怖大三角”吧。
# 只要对一个promise加'wait,就会等待,直到取得结果,而'wait所在的函数就会自动
# 成为异步函数。
# 这里没有任何回调,就和同步一样。
# do表示调用函数自身。
]]>
也是无形参的,在<>中可以使用@。
# @很好记,圆圈里面就是个a。从此以后把@读做arg吧。
#
add: <> @0 + @1 # var add = function() {return arguments[0] + arguments[1];};
console.log add(2, 3)
# 觉得undefined也太长了?没问题,Fus提供了void。
console.log void
# 在Fus中,没有长度超过7个字母的关键字。
]]>
x: point.x
y: point.y
Math.sqrt(x * x + y * y)
console.log distance{x: 3, y: 4}
# 函数无需写return,最后一个语句即是返回的值。
# 你无需用var声明一个变量。变量在第一次赋值时自动声明。作用域为函数级。
# 这既简洁又避免了全局污染。如果在JS中忘了写var那可糟了,会变成全局变量,造成
# 全局污染。
# 你可以把下面一行的#去掉,看看它出现了什么错误。
#console.log x
]]>
me.name: name
me._spoken: 0
speak: <>
me._spoken: self + 1
console.log "\(me.name)喵了一声儿。第\(me._spoken)次。"
# 类的实例化,无需使用new,Fus会根据首字母是否大写来判断,如首字母大写,会自动
# 在编译成的JS里加上new。
cat: Cat("大白")
cat.speak()
cat.speak()
console.log cat.name
# 只要类的属性名以_开头,它就自动成为“私有”的。所以cat._spoken是undefined。
console.log cat._spoken
# Fus的类本质上就是JS的类,所以你也完全可以在JS文件中导入Fus的类。
]]>
console.log(i * i)
}
# 第二种弹性对象应用于loop函数,使你能够接近于自定义语法,实现美丽的循环,
# 而其本质却仍是函数传递对象。
# 不信?把loop改成console.log,看看输出结果。
# (由于输出的是转换后的JSON,所以你看不到函数)
# 这两种弹性对象和“批量导入”一起使用,让每个人都接近于拥有扩展这门语言的能力。
]]>
下文既有教程(以正常字体表示),又有对一般开发者不太重要的可能比较枯燥的语言规范(以灰色小楷体表示)。第一次读的时候只要读教程就可以了(教程通常也是语言规范的一部分,只是比较好懂而已)。
FutureScript | JS (ES5) | JS (ES6) | CoffeeScript | |
---|---|---|---|---|
异步函数 | ✓ | |||
简化的函数表示法 | ✓ | ✓ | ✓ | |
函数式if |
✓ | ✓ | ||
简化的if |
✓ | ✓ | ✓ | |
模式匹配 | ✓ | ✓部分 | ||
复杂自我赋值 | ✓ | |||
递归无需命名 | ✓ | |||
类实例化无需new |
✓ | |||
类私有属性 | ✓ | |||
类getter、setter | ✓ | ✓ | ||
类静态构造函数 | ✓ | |||
当前类 | ✓ | |||
管道 | ✓ | |||
虚拟方法 | ✓ | |||
rem 求余 |
✓ | ✓ | ✓ | ✓ |
mod 求余 |
✓ | ✓ | ||
精简instanceof 和typeof |
✓ | |||
导入 | ✓ | ✓ | ||
批量导入 | ✓ | |||
导出 | ✓ | ✓ | ||
字符串插值 | ✓ | ✓ | ✓ | |
函数式try...catch |
✓ | ✓ | ||
弹性对象 | ✓ | |||
自然的枚举 | ✓通过fus-ext | |||
自然的函数式循环 | ✓通过fus-ext | |||
自我调用 | ✓ | ✓ | ||
存在运算符 | ✓ | ✓ | ||
包含运算符 | ✓ | ✓ | ||
幂运算符 | ✓ | ✓ |
近些年,JS的语法越来越复杂,同时也越来越臃肿。
JS | FutureScript |
---|---|
三种符号表示字符串 | 统一用双引号 |
= 赋值 |
统一使用冒号 |
[] 访问数组元素 |
统一使用点号 |
原生循环 | 使用弹性对象实现 |
return |
|
someVar++ |
someVar: self + 1 |
someVar-- |
someVar: self - 1 |
someVar += 2 |
someVar: self + 2 |
someVar -= 2 |
someVar: self - 2 |
someVar *= 2 |
someVar: self * 2 |
someVar /= 2 |
someVar: self / 2 |
<< |
|
>> |
|
>>> |
FutureScript是第一个具有全部这4种属性的语言:
作者把这一组属性称为“未来风格”。
Fus的一些功能,例如弹性对象,此前也从未有语言实现过。
Fus的目标:我们要使比它简短的语言都没它易懂,比它易懂的语言都没它简短。我们探求简短和易懂的最佳平衡点,并让它保持惊人的美丽与统一。
(这章不属于语言规范。由于工具会更新,因此本章可能会过时。)
首先确定你的电脑上安装了Node.js 18或更高的版本。
fus
命令具体使用方法请点击:
https://www.npmjs.com/package/futurescript
我们为文本编辑器提供了语法高亮显示的模块,用于Sublime Text、Atom,应该也可用于WebStrom等支持tmbundle的编辑器。
Atom用户可以直接使用language-futurescript包。你可以继续使用现有的theme或使用这两个为FutureScript专门优化过的theme:4-color和4-color-dark。
其他编辑器的用户(以Sublime Text为例),在菜单中选Preferences,Browse Packages,弹出的窗口就是包含所有语法包的目录。点此下载最新版,然后把tmbundle目录改名为FutureScript,拷贝到该目录中即可。
我们使用的语法名称为“FutureScript h1”,请注意辨别。
编译后生成的JS代码符合ECMAScript 2017规范。
编译器本身也符合ECMAScript 2017规范,并且不依赖其他任何程序。注意,编译器只是命令行工具的一部分,命令行工具是依赖Node.js的。
后缀名为.fus。
Fus文件的外观:由版本行、语句、注释和无语义空白组成。语句是可以嵌套的。版本行和语句是有语义的,注释和无语义空白都是无语义的。
语句不是表达式,也不可以作为表达式。表达式不是语句,但可以作为语句,作为语句时称作表达式语句。有两种语句:表达式语句和命令语句。
类似CoffeeScript,在FutureScript中,缩进是很重要的,它构成了嵌套。缩进可以用<<
与>>
代替。
类似CoffeeScript,多个并列的行可以通过分号;
合并,单行可以通过\
拆分成多行。
空行,是指不含有字符的行。
空白行,是指不含有字符或只含有空白字符的行。
无语义空白,是指空白行,或者行右侧无意义的空白。
下文的所有描述,都是假设代码中的无语义空白已被去除。
大多数语句都是表达式语句,除了这些是命令语句:
给定一条语句,该语句所在级别的最大范围连续内容称作该语句所在的块,块不包括版本行。除了块以外,还有表,块和表合称“容器”。块的子项可以是语句或注释,表的子项不可以是语句。我们说某语句的子块(或子表),是指该语句的一级下属。我们说某块的子语句,也是指该块的一级下属。
这些容器是块:
其他容器则都是表,例如:
当块(除catch块外)只含有单条语句时,也可以写在一行里而不使用缩进,这称为内联块。例如:
注意此时aaa()
仍然是属于then块的。另请注意,这种写法中,块之前必须有相应的关键字或符号,例如上例中的then
不能省略。由此可知,所有的块(除后置if中的块外),语法里都有关键字或符号作为前导,例如->
、then
、try
,当这个前导不处于语句头部且不处于行的头部且后面不是内联块时才可能允许省略(还要看语法是否支持,例如->
就不能省略)。
块的子语句是从上往下执行。除顶级块外所有块都有值,若最后一个子语句是表达式,则为该表达式的值,若不是,则为void
。
对于某一行B,若它的起始部分不属于字符串,它的缩进的效果是什么?依下面的步骤:
1. 强制连接:上一行A以\
结尾吗?若否,则做下一步。若是,则执行:无论缩进如何,该行被解析为犹如被连接到上一行末尾并跳到5,若无法解析则跳到6。(术语解释:这些步骤中的“连接”指的是两行相连,中间插入一空格。第二行的缩进不算入字符。)
2. 并列:上面存在一行A,满足B的缩进等于A的缩进,且A和B之间无其他行或A和B之间的所有行的缩进都大于B的缩进,且A没有被连接或被强制连接(术语解释:“被连接者”是指连接后的右面,“连接者”是指左面)吗?若否,则做下一步。若是,则执行:A的起始部分和B的起始部分被解析为并列关系并跳到5,若无法解析则跳到4。
3. 层级:上面存在一行A,满足B的缩进大于A的缩进,且A和B之间无其他行或A和B之间的所有行的缩进都大于B的缩进,且A没有被连接或被强制连接吗?若否,则做下一步。若是,则执行:A的结尾部分和B的起始部分被解析为层级关系并跳到5,若无法解析则跳到4。
4. 连接:上面存在一行A,满足B的缩进不小于A的缩进,且A和B之间无其他行或A和B之间的所有行的缩进都大于B的缩进吗?若否,则跳到6。若是,则执行:B被解析为犹如被连接到A的末尾所属语句的末尾并跳到5,若无法解析则跳到6。例如:
或:
或:
都等价于:
注意:这种写法我们暂时不支持:
aaa()
.c()
]]>
你需要写一个括号:
aaa()
).c()
]]>
5. 解析成功并退出。
6. 解析失败并退出。
缩进的规则很适合用来表示一些复杂的语句。例如,你可以这样写:
s: @0.toLowerCase()
text.includes(s)
)
"cat" ? "meow"
"dog" ? "woof"
| "unknown"
# speak is "woof"
]]>
和CoffeeScript有一点不同,如下代码:
CoffeeScript编译成a.b(aaa).c()
,但我们还是按常规编译成a.b(aaa.c())
。如果你想要达到CoffeeScript的效果,要加上括号:
通常来说,把一行拆成几行会增加可读性。但有的时候恰恰相反,你会发觉把几行特别短小的代码合并成一行反而更好看,特别是当你有很多这样的“几行”的时候。例如:
>
B: class << bbb: 1 >>
C: class << ccc: 1 >>
D: class << ddd: 1 >>
E: class << eee: 1 >>
]]>
它和这个是等价的:
这时<<
和>>
的威力就体现出来了。<<
表示接下去的代码缩进一级,>>
表示还原。
<<
和>>
必须在1行内结束。也就是说,只能把多行合并成1行,无法把多行合并成2行。
这两个符号和分号、逗号结合使用,可以达到最大程度的合并效果。分号合并行,但对象属性、数组元素、类属性需要用逗号合并。
在>>
后可加分号或逗号,也可不加分号或逗号,例如:
<< x + 1 >>, bbb: 3 >>
if aaa << task1() >> else << task2() >>; commonTask()
]]>
这里的分号和逗号都是可略的。
每个文件的第一行必须是版本行。版本行通常都写成一行,但写成多行也是允许的,但为了方便说明,还是说成“版本行”。版本行的一般语法:
默认值为:
版本号后面的以逗号分隔的项不可重复。版本行的例子:
版本号必须写上。只要你写的版本号不大于安装的FutureScript的版本号,该文件就会按照该版本的规范来运行,这是因为你安装的任何一个版本的FutureScript都包括了(至少我们会尽量包括)所有历史版本的编译器。而这正是FutureScript和其他语言的不同之处。你甚至可以在一个项目中,不同文件使用不同版本。
写上版本号还有个好处,就是你把某个文件交给别人时,他就知道文件是什么版本的,从而可以更好地修改。
本文只描述一个版本。
当你想把版本行写成多行时,版本号后面要加上大括号,大括号必须在第一行。版本号和大括号之间可以有逗号,也可以没有逗号。例如:
当文件的后缀名不是.fus或没有后缀名时,版本号前必须有fus
。当文件的后缀名是.fus时,不必须,然而我们推荐加上fus
,因为对你的文本编辑器有用,方便它判断应该使用哪个语法包,使代码呈现正确的彩色。
激进模式(radical)和兼容模式(compatible)的差异是:激进模式更鼓励单参数的函数。你是否觉得函数的多参数把事情弄复杂了?如果每个函数最多只能有一个参数,这个参数也可以是数组,那不也能实现“多参数”的功能吗?但是这个变化实在太大,所以我们默认不开启激进模式,而是采用兼容模式,你仍然能够使用多参数的全部功能。
激进模式中,@
表示JS的arguments[0]
。兼容模式中,@
表示JS的arguments
。
本文的示例是按照兼容模式写的,在激进模式下有些地方需要改写。
FutureScript代码可以被JS调用。JS代码也可以被FutureScript调用。
capitalized new
是指创建类的实例不用加new
,编译后的JS会自动加入new
,它依据类名的首字母大小写,碰到大写则会加。示例:
这是默认的。
能识别的有两种形式:变量和普通的点号(x.y
,它会判断y
),像x."y"
或x.y'
是不能识别的。例如:
manual new
就是传统办法,创建类的实例需要加new
。
node import
是指导入被编译成node的导入(即require)。node export
是指导出被编译成node的导出(即exports和module.exports)。node modules
是两者都做。es modules
是两者都不做(即按照ES6的规范)。具体这有什么意义,参见“模块”一章。
编译后的代码是strict mode。
使用#
(内联)和###
(格式),基本和CoffeeScript完全一样。但有一点不同,CoffeeScript的###
是统统编译,我们的###
是仅当起始的###
紧接在版本行下面(或两者之间只有空白)并且结尾的###
右边没有任何非空白时才编译,否则编译器自动忽略。为了和JS兼容,注释中的*/
会编译成两个空格。
函数的声明有3种方式:箭头->
、钻石<>
、线--
。<>
和--
是无参数时的简便方法。参数为1个时括号可省略,这有别于CoffeeScript。
x + 1
a: (x) -> x + 1
]]>
如果没有参数,那么必须使用如下一种:
Math.random()
a: -- Math.random()
a: () -> Math.random()
]]>
允许多参数,但必须要有括号:
x * y
]]>
如果是单参数,并且该参数是数组,就可以使用类似“解构赋值”的形式:
x * y
]]>
有了这个,激进模式才成为真正实用的东西,因为你可以用a[1, 2]
来调用它(具体后面再介绍)。其实从用途上来说,多参数能达到的,这个也能达到,而且更纯粹,所以,在激进模式中,我们不鼓励使用多参数(多参数在激进模式中功能不全,无法用@
表示第一个参数之后的参数)。
在<>
函数内部,可以使用@
。@
取代了JS的arguments
(兼容模式下)或arguments[0]
(激进模式下),它是如此简便,在许多情况下,你会喜欢不带参数名。例如,我们可以写:
@0 + @1
console.log add(2, 3) # output 5
]]>
参数可以有默认值:
x * y
b: [x ifvoid: 0, y ifvoid: 0] -> x * y
]]>
有默认值的话,哪怕只有一个参数,外面也必须有括号(或中括号)。
如果是ifvoid
默认,则ifvoid
可以省略,所以上面的例子可简写为:
x * y
b: [x: 0, y: 0] -> x * y
]]>
除了ifvoid
默认以外,还有ifnull
默认,后面会介绍。
下表列出三种函数符号的详细区别:
箭头函数-> |
钻石函数<> |
线函数-- |
|
---|---|---|---|
形参 | 有 | 无 | 无 |
@ |
无 | 有 | 无 |
fun |
有 | 有 | 无 |
功能 | 较多 | 最多,可以模拟其他两个 | 少 |
简洁 | 较简洁 | 通常很简洁,但有时不简洁 | 很简洁 |
例如在@
的使用上,如下代码:
inner: x -> @
]]>
这里的@
不是指inner
函数的参数,而是指outer
函数的参数,因为->
的语法没有定义@
。
fun
是一个关键字,指“本函数”。这通常写在递归里,如果函数名很长,这可以减少输入量,甚至无须为函数命名也可以使用递归。
同理,由于--
的语法没有定义fun
,所以--
里面的fun
指的是外层->
或<>
函数。
内层参数不能和任何外层参数重名。这比CoffeeScript、JS都严格。但不排除在以后版本中放开这个限制。
我们已经知道所有的成员访问都用点号.
,这还有个好处,就是让函数的调用可以省去小括号而直接写上中括号。
函数的调用有4种表示法:空格、小括号、中括号、大括号。
多参数调用一定要加括号,这个和CoffeeScript不同。只有单参数调用是可以不加括号的。当然我们也都知道没有参数时要加上代表零参数的()
。
CoffeeScript里面如果函数调用要传递的参数本身也是函数的话,如果它没有参数,那么可写成这样的形式:
Math.random()
]]>
因为CoffeeScript把它理解成:
Math.random())
]]>
但FutureScript中绝对不能这么写,否则编译器会误以为这是一个参数为abc的函数。必须采用如下一种:
Math.random()
abc -- Math.random()
abc () -> Math.random()
]]>
我们还可以使用'
来达到类似splat的效果:
相当于ES6的
单引号'
称为变体。'
是FutureScript独有的符号,不只这一种形式,后面还会介绍。
可以使用展开符号...
,例如:
y.length
a(0, 0, 0) # returns 2
]]>
展开符号...
也可以用在函数调用中,例如:
目前,展开符号...
的限制是:必须作为最后一个参数,并且不能在数组中使用。以后的版本中可能会取消这个限制。
注意这两个函数调用是不同的:
凡是函数都有一个返回值,值即为函数块的值,所以我们无需使用return
。函数块的值是啥?精确定义在前面讲过,下面再通俗地讲一遍:就是函数的最后一条语句的值,如果最后一条语句不是表达式,那么则是void
。这个规则十分重要,是函数式编程思想的核心。示例:
a: x * x
b: y * y
a + b
calc(2, 3) # returns 13
]]>
不单是函数块,if...then...else...
里面的块,以及所有其他的块(顶级块除外),用的都是这个规则。
不像CoffeeScript,我们没有胖箭头=>
,这是因为我认为JavaScript使this
的含义可变是一种错误的设计,它应该始终指向当前对象。
我们的函数和CoffeeScript的函数的对应关系如下:
用:
、as
。例如:
注意,:
不构成表达式,但as
构成表达式。例如:
a: 1
]]>
执行abc()
返回的值是void
而不是1
。但
1 as a
]]>
执行abc()
返回的值是1
。
变量不用声明也不能声明,这是因为我们的变量都是自动声明的。该机制类似CoffeeScript,但有着显著的区别,主要是在内外层方面:
这两段代码是一样的,都是编译成一个变量:
CoffeeScript:
a = 5
]]>
FutureScript:
但这两段代码不一样:
CoffeeScript:
a = 5
a = 3
]]>
CoffeeScript编译成两个重名变量a
。然而在FutureScript中:
会编译成一个变量a
。
变量自动声明的灵感源于CoffeeScript,我觉得这是CoffeeScript最好的特性之一,也许很多人不同意,觉得这样禁止了在内层声明同名变量,所以是不好的特性,但其实不然。在大型项目中,不同的程序员分别管理不同的文件是合理的,模块具有隔离作用;但是不同的程序员分别管理一个文件的内外层是不合理的,这会引起混乱。既然一个文件的内外层应作为一个整体管理,所以为内层变量取名的时候顺便注意一下外层并不是一件难事,因此也就没有必要支持在内层声明同名变量。而且我们比CoffeeScript做得更彻底。
:
也支持数组解构赋值(不支持嵌套),例如:
冒号的作用就是让左边的拥有右边的值。无论是对象中的冒号还是赋值中的冒号,含义相似。
:
支持对多标识符同时赋值,例如:
只要不是FutureScript关键字,就能作为变量名,哪怕是JS关键字如function
也能作为变量名。但JS关键字不能作为全局变量名来赋值或使用。
如果你想仍然使用Node的require
、exports
和module.exports
,那么可以跳过本章,因为在FutureScript中你仍然可以像往常一样使用这些东西。
我们知道,Node有它自己的模块机制,但问题是该机制并不属于ES6,ES6定义了一套JS原生模块语法。由于FutureScript是基于JS的,所以我们的原生语法是基于ES6的。
一个文件即是一个模块。和模块有关的语法有import
、export
、'export
。
或
都是指导入ES6中的“默认导出”,等同于JS的
当版本行使用默认值的时候,就编译成上面这样的JS。但妙的是,版本行如果有node import
或node modules
,同样的代码,它就会编译成:
你无需修改代码,就可以控制本模块中的所有import
是使用Node机制还是ES6机制,所以现在import
可以代替require
。
其实,由于现在的Node和浏览器对ES6的支持还不是非常完美,你也许不得不用Babel来转换成ES5,而Babel会自动把import
转换成Node机制,所以就算你版本行里不写明,应该也没什么关系(但可能两者有些微小的区别)。
还可以导入所有的“命名导出”:
或
相当于JS的
还可以导入指定的“命名导出”:
或
相当于JS的
不支持在一个语句里同时导入“默认导出”和“命名导出”,有此需求请分两个语句。我认为一个模块既有“默认导出”又有“命名导出”并不好。
注意,其实上面的一些例子利用了all
在用大括号的情况下可省略的特性。如果要写全,应该是:
import
后面只能跟内联普通字符串,并且字符串里不可以有插值(但可以有转义符)。import
不能既有冒号又有as
。这些都是正确的import语句:
这些是错误的import语句:
import
的赋值不是普通的赋值,它是“绑定”的,和ES6一样。所以,语法上有限制,它只能是赋给变量,不能赋给对象属性,也不能在冒号左边出现ifvoid
、ifnull
、'
。
我们使用node经常碰到这种情况:每个文件开头都有一大堆的导入,基本都重复,但又无法省略。当然,你可以建立一个新模块(即新文件),把这堆东西都放里面,再导出,于是每个模块只要导入这个新模块就可以了。但是,它的代价是,多出了点号。例如,本来是:
你建一个all.fus文件,把这三个import
放里面,然后在原文件里写:
但问题是调用时多出了all
,所以似乎也没省多少。
FutureScript具有独特的“批量导入”功能(ES6中也没有该功能)。当有all
但无冒号也无as
时便编译成批量导入,它会导入所有的命名导出(但不包括默认导出)并让它们都成为变量名。于是,我们可以写成:
这个功能有个重要特点:编译器生成变量名时,需要读取被导入的模块,为了保证路径解析的方法和运行时一致,我们使解析器尽可能简单,所以只支持以"./"
、"../"
或"/"
开头的路径。所以,这是一个限制,该功能只适合导入同一个项目中的另一个模块,绝不适合用模块名从别的项目中直接导入(就算支持,这也会造成变量名的不确定性)。所以,项目间的导入,一定要在本项目中有一个“清单”模块作为中间模块。
“批量导入”并不引入全局变量,所以不邪恶。这些变量在别的模块中是不可见的,除非别的模块也批量导入了它们。
如果A批量导入B,如果某个B的命名导出(即A中被定义的变量名)恰好是A所在的FutureScript版本的关键字,那么该变量将不可用,但不会出错。
被普通导入的模块可以是Fus文件或JS文件,但被批量导入的模块必须是Fus文件(版本不限)。
以上介绍的是import语句,import还可以作为表达式,格式是:import "module-name"
,后面不能是as
,例如:
所以import表达式格式比较自由,但只能导入默认导出,并且如果使用到赋值,那就是普通的赋值,不是绑定的。
导出时,我们可以使用4种语法:export
、'export
、export as
、export ... as
。这些都是正确的语法:
@0 * @1
mp: (<> @0 * @1) export as multiply
mp: (<> @0 * @1) as multiply'export
abc export as def
export abc as def
export abc
export: abc # note that this is different from above
export: <> @0 * @1
]]>
这些是错误的语法:
@0 * @1
obj.multiply'export: <> @0 * @1
]]>
注意abc export as def
和export abc as def
相似但有着两个区别。一个区别很明显,就是前者是表达式语句而后者是命令语句。另一个区别很微妙,前者的abc
是作为表达式,后者的abc
是作为变量,这就使得:前者是不绑定的,而后者是绑定的(能享受到ES6模块的变量绑定带来的好处)。
当语句以export:
开头,表示ES6中的“默认导出”。例如:
当版本行使用node export
或node modules
的时候,这条语句会编译成:
文件中这种语句只能出现一次,而且若出现则不能含有其他形式的导出,否则产生编译错误。这是因为如前面提到的,一个模块既有“默认导出”又有“命名导出”不好,虽然出于兼容考虑,FutureScript支持导入其他这种架构的模块,但自己的模块就不要使用这种架构了。
默认导出还可以用这种形式:
所以默认导出其实是一种特殊的命名导出(详情参见ES6)。当你把它命名为default
时即为默认导出。
所以这两个:
两者功能相似,唯一的区别是:前者的abc
是作为变量,所以是绑定的。后者的abc
是作为表达式,所以是不绑定的。(这和ES6的规则相同,ES6的export {... as default}
和export default ...
也有着上述区别。)
导入和导出必须出现在最外层,因为这是基于ES6的。我们是静态导入,无法动态导入,这比Node要严格。当然,我知道有些场合可能需要动态导入,那你完全可以抛弃FutureScript的原生模块语法,而用Node
的require
。
内联字符串:
格式字符串:
Welcome to "FutureScript"!
"
]]>
上例的字符串其实就相当于CoffeeScript的:
Welcome to "FutureScript"!
"""
]]>
你一定觉得奇怪,为什么我们只需用一个引号就能包住一大段文字(文字本身也有引号),秘密在于我们要求在格式字符串内部,任何行的缩进都必须大于引号的缩进,且起始引号和结束引号的缩进必须相同,所以内部有再多引号也没关系,不影响编译器判断。
格式字符串的起始引号后面必须紧接一换行(起始和结束的两个换行不算作字符),且结束引号所在的行中,该引号前面必须全是空白。格式字符串的缩进是从字符串内部除空行外缩进最小的一行的缩进的相同值开始算作字符。
格式字符串内部不可以全是空白(但用转义符表示的空白除外)。例如这些都是非法的:
这个虽然表示一个空格,但是是允许的:
编译器是怎样判断某个引号是内联的还是格式的?看起始引号后面紧接的字符,如果是换行,则为格式,如果是其他字符,则为内联。
我们不用单引号,因为我觉得你要想用单引号,当字符串很长时,你可以用格式"
;当字符串很短时,像'"'
,这其实只增加了微小的可读性,我宁愿写成"\""
。无论哪一种用单引号都没有明显优势,所以,还是别占用宝贵的符号资源为好。而且双引号是和JSON一致的。
也支持字符串插值,就像CoffeeScript,不过我们使用\(...)
。
一个插值不能占据多行。目前不支持插值中含有双引号(即字符串嵌套)。其实就算支持,这也会显得太复杂而不美观,完全可以赋值给新变量,再插值。
上面介绍的都是普通字符串。除了普通字符串外,还有特殊字符串和字符串扩展。什么是字符串扩展?正则表达式就是典型的例子。
正则表达式我们用r"..."
来表示。例如:
内联正则表达式废除了\
的大部分功能,除了2个:\"
、用于行尾连接的\
。注意,JS的正则表达式里的"
和#
可以直接表示,但在这儿必须用\"
和\#
,因为这两个字符有特殊含义。
正则表达式可以有插值,插入的是字符串:
和CoffeeScript相同,正则表达式的插值如果是普通字符串的话,如果想用字面量表示\
字符,会比较繁琐,所以建议仅把插值用于动态插入非符号字符。
还可以表示带有标志的正则表达式:
格式正则表达式的例子:
格式正则表达式的不同点是:空格和换行无意义,支持注释,表示双引号字符更简单。注释的#
左边必须至少有一个空格,这和普通注释不同。
原义字符串是受C#启发。原义字符串用v"..."
来表示,例如:
原义字符串完全废除了\
的功能,所以行末不能用\
来连接,在内联原义字符串中也无法表示双引号字符。
所以总结一下,凡是字符串起始符号"
左边紧接着字母的,可以是表示字符串,也可以表示字符串扩展。
正则表达式引号中间的内容本质上是字符串,这和JS和CoffeeScript都不同。就算是格式正则表达式中的注释,本质上是属于字符串的,只是转换时去掉了而已。注意,转换是运行时而不是编译时,因为可能有插值,所以无法在编译时转换。
还有一点和CoffeeScript不同,就是CoffeeScript中:
这会编译成多行自动连接,连接符是一个空格。但是我们不支持,一个原因是这和我们的格式不兼容,另一个原因是我觉得这只适合西方语言。如果是中文,那加上空格可就错了。所以这个功能不好,应该老老实实用\
来连接,是英文的话,前面加个空格。
下表是不同种类的转义规则(转换时进行的转义不包括在内,如正则表达式语法本身的\
):
内联" | 格式" | 内联v" | 格式v" | 内联r" | 格式r" | 内联js" | 格式js" | |
---|---|---|---|---|---|---|---|---|
\普通转义符 | 有 | 有 | 无 | 无 | 无 | 无 | 无 | 无 |
\\ | 有 | 有 | 无 | 无 | 有 | 无 | 无 | 无 |
\" | 有 | 有但不需 | 无 | 不需且无 | 有 | 不需且无 | 无 | 不需且无 |
\连接 | 有 | 有 | 无 | 无 | 有 | 有 | 无 | 无 |
插值 | \(...) | \(...) | 无 | 无 | #(...) | #(...) | 无 | 无 |
方括号的作用就是表示数组或类似数组的东西(用于解构赋值等),没有其他用途。
点号的作用就是访问对象的成员。由于数组也是对象,所以JS中arr[3]
我们改为arr.3
,更一致。其实数组就是属性名为索引值的对象,不信的话,可以用Object.keys(arr)
来验证一下。JS这点和别的语言不同,但不是糟粕,因为这个概念挺统一的,也没有什么破绽。
当然,在和带小数点的数字同时出现时,会感觉别扭,这时建议加上括号。比较一下:
a.(b)
等价于JS的a[b]
。
数组的表示法和CoffeeScript完全相同。
对象的表示法和CoffeeScript相似,但它必须加上大括号,不能省略。
对象成员访问用的是点号,和CoffeeScript基本相同,但如左边是数字,则左边必须加上括号,例如(1).toString()
。这个限制是为了防止产生歧义,如1.3
,如果没有这个限制就会看上去既像小数又像数组元素访问。当然你可以争辩说其实并没有歧义,因为1
不可能是数组,那如果是1.3.a
呢?感觉怪怪的,不如(1.3).a
直观。所以这一点上我们比JS还严格(JS不允许1.a,但允许1.3.a)。
FutureScript提供了独特的“弹性对象表示法”。支持两种弹性。第一种弹性是省略冒号和true
,例如:
相当于JS的:
第二种弹性是省略冒号和逗号,例如:
console.log(i * i)
}
]]>
相当于JS的:
弹性对象表示法的另一个例子是枚举:
因为它相当于JS的:
上面两个例子分别使用了fus-ext包中的loop函数(第二种弹性)和enum函数(第一种弹性)。弹性对象表示法有着强大的力量,使你可以接近于自定义语法。我们再也不需要在语言层面支持循环和枚举,直接使用外部包的函数,其简洁性达到(甚至超过)许多有原生语法支持的语言。
注意,不可对一个对象同时使用两种弹性。另外,第二种弹性中的任何一行必须存在至少两项(键和值分别算一项)。所以{a b}
是第二种弹性(相当于{a: b}
),但{a}
是第一种弹性(相当于{a: true}
)。
第二种弹性中,如果项数为奇数,那么第一项被解析为值而非键,它的键为空字符串。参见loop
的例子。
第二种弹性中,调用函数时不能用空格,而应该用括号。
无论何种弹性,只要一个键后面没有冒号,那么这个键本身就不能用表达式来表示,例如这个是非法的:
第二种弹性中的关键字只当该项是值时才真正作为关键字。这就是说,如下代码:
是会编译成:
你可以写:
但你如果要把运算符改成or
的话,应该加上括号:
第二种弹性中的键名不可以是not
或as
。
最后请注意,因为本章的例子有外部函数,所以这些例子可能会过时。
有这些字面量:
字面量都是表达式,但和一般表达式的区别是:
我们有6个关键字或符号表示上下文,分别是:
me
Me
super
fun
@
self
@
后面的点号可以省略,除非点号后面跟(
,或者"
、v"
、r"
之类的字符串或字符串扩展。
self
能表示复杂的自我赋值,例如,当要自加到左边而不是右边时:
等号的作用就是判断相等,不用再考虑烦人的=
、==
、===
之间的区别。
/=
或≠
表示不相等,也可写作not =
,中间的空白可有可无。这里的亮点是≠
。如果你用Mac,可以直接通过按“Alt”键加“=”键输入≠
,Mac OS原生支持输入不等号。如果你用Windows,也可以在编辑器中自定义快捷键,所以其实每个人都可以很方便地输入≠
,这大大增强了可读性。
为什么我们不用!=
?因为感叹号目前FutureScript还没用到,我实在不想让一个宝贵的符号只能(或者说,只适合)和等号结合。/=
显得也比!=
更像≠
。
和CoffeeScript一样,我们也支持这种链式比较(目前仅支持3个算元):
但是我们的限制比CoffeeScript多,只有同一个方向的才可以链式比较。像这种就不支持:
c
console.log "success"
]]>
FutureScript和其他语言在逻辑运算方面最大的区别就是not
的优先级。not
的优先级被降低,仅高于and
和or
。这意味着我们可以少加很多括号。你肯定抱怨过在JS中不得不这样写:
但现在我们可以:
但事实上这个例子是非法的,因为我们没有instanceof
关键字,我们是用is
关键字,你可以写not abc is Abc
但稍显难看(所以我们支持写成abc isnt Abc
)。那有没有好看的例子呢?有,像这种也不用加上括号:
再回到刚才说的不等号。其实还可以用not ... =
。这5个功能相同,你看哪个顺眼就用哪个:
不过not ... =
虽然功能相同,但生成的代码还是与其他三个不同的。
用if
、?
、then
、else
、|
。例如:
或
也可以并成一行:
或
甚至
?
和then
等价,|
和else
等价,这条规则不仅限于if
语句,在所有地方通用。下文提到的它们任何一个的规则也适用于它的等价方,例如then
的规则也适用于?
。
条件前置时,if
可写可不写,而then
则当then
块非内联时可写可不写。if
省略时要注意避免出现优先级的问题,例如:
100 then 100 else b
]]>
这里的if
省略的话意思就完全不一样了,除非加个括号。
当then
之后是表示命令语句的关键字时,then
也可省略(这也适用于模式匹配)。例如:
100 throw new Error()
]]>
如果没有else
块,那么则当else
时整个if
表达式的值为void
。
但是要注意,条件前置时无论怎样省略,if
和then
必须至少出现一个。
条件还可以后置,例如:
条件后置时if
绝不能省略。一条语句中只可以含有一个后置的if
,它必须出现在语句的最外层(外面不能有括号),而且if
之前不能是普通的块,只能是内联块。由于后置的if
只能处于语句的最外层,所以如前面是冒号赋值,那么冒号和if
之间的应该属于冒号而不是if
。
还有一点和CoffeeScript不同,就是当碰到分号时:
100 then aaa(); bbb()
]]>
CoffeeScript是把aaa(); bbb()
全作为子语句,这看上去似乎是硬插入一条规则,很不统一,而且我们已有了专门的符号,可以写成:
100 << aaa(); bbb() >>
]]>
所以这条规则在我们这里作废。FutureScript里,分号的左右两边能在不引发语法错误的前提下穿越一切障碍物。
match
可以看成是一个增强版switch
,它称为模式匹配。例如:
上面只是最普通的模式匹配,day
是输入,1、2、3……称作模式。
模式匹配中的“比较”默认是用=
直接比较输入和模式,match
右边还可以不是输入而是比较函数,例如:
statusCode >= @0
600 ? "unsupported"
500 ? "server error"
400 ? "client error"
300 ? "redirect"
200 ? "success"
100 ? "informational"
| "unsupported"
# message is "client error"
]]>
使用激进模式的话,上例中的@0
更是可以简化为@
。
else
可以跟前面的并在一起,此时用or else
,但该项不能有then
。这称为else
模式和其他模式的合并写法,但本质上还是两个模式。在模式匹配中,or
这个关键字若不作为运算符使用,可以简写为逗号,
,所以上面的例子可以写成:
statusCode >= @0
600, | "unsupported"
500 ? "server error"
400 ? "client error"
300 ? "redirect"
200 ? "success"
100 ? "informational"
]]>
用or else
的时候or
还可以省略,所以还可以写成:
statusCode >= @0
600 | "unsupported"
500 ? "server error"
400 ? "client error"
300 ? "redirect"
200 ? "success"
100 ? "informational"
]]>
比较函数的功能非常强大,例如:
match <> pos.x = @0.0 and pos.y = @0.1
[0, 0] ? "king"
[0, 1] ? "pawn"
[4, 5] ? "queen"
| "empty"
piece: chessBoard {x: 4, y: 5} # will be "queen"
]]>
(注:激进模式中@0.0
和@0.1
可以简化为@0
和@1
。)
又如:
text.includes(@0)
"cat" ? "meow"
"dog" ? "woof"
| "unknown"
# speak is "woof"
]]>
当match
后面是函数时,则被识别成比较函数。要直接写出->
或<>
,绝不能用变量替代。
还有or模式和and模式。or模式我们会经常用到,例如:
and模式则不常用,但也有例子:
array.includes(@0)
"a" and "b" and "c" ? "The array includes all of the first 3 letters."
"a" ? "The array includes a."
"b" ? "The array includes b."
"c" ? "The array includes c."
| "The array doesn't include any of the first 3 letters."
]]>
由此可知,or和and作为运算符在模式中使用是有限制的,它作为运算符时必须出现在括号内,否则会被认为是模式。所以真的有这个需求的话,要用个括号包起来。不过它们作为运算符出现在模式中好像也没有什么用。
作为模式使用时要么全是or
,要么全是and
,两个不能在一个模式中一起出现,否则编译器会无法判断。括号内的无此限制。
注意or else
是一种特殊的合并写法,并不属于or模式,所以or else
可以和and
一起出现。
和if
一样,then
块可以用缩进,这时就不用写then
了:
可以看出,即便是模拟传统的switch
,依然比switch
简洁。
循环其实并不符合函数式编程的理念,所以我有意使FutureScript没有循环。但我可以肯定地说,所有循环能做的事,FutureScript都能做,而且简洁程度不亚于循环。(本章不属于FutureScript语言规范,只是简要介绍一下fus-ext包的loop函数。本章的内容可能过时,以fus-ext的教程为准。为方便起见,本文中的“循环”一般指loop。)
可以使用FutureScript的扩展包fus-ext来实现循环。
把node_modules/fus-ext/examples/manifest.fus
文件拷贝到你的代码所在的目录中。
你的代码可以是这样:
console.log "This is \(i) time"
)
]]>
这里使用了“批量导入”的功能(详见前文的“模块”一节)。注意manifest.fus的导出和你的导入应该匹配,如果你想改用node而不是es,那么需要对两个文件的版本行都作出相应修改。
迭代器返回break
则表示跳出循环,类似于JS的break
,不过本质上不同,这里的break
是表达式,只能使余下的循环不做,并不能使函数内剩下的部分不执行。跳出循环时loop函数返回break
。这个例子是一个循环,从0到9,但到5时跳出:
if i < 5
console.log "This is \(i) time"
else
break
)
]]>
不设次数则表示永远,相当于JS的while (true)
:
这个相当于JS的for循环,从1到10:
console.log i
}
]]>
这个相当于JS的for循环,从10到1:
console.log i
}
]]>
递归的实现方式没有什么特别的,但我们加入了更简便的方法,你甚至无需命名递归函数,你想,自己调用自己,又何必给自己取名字?使用fun
关键字就行了。例如,如果你不想用外部库的话,可以用递归模拟循环:
if i < 10
console.log i
fun(i + 1)
]]>
用try
、catch
、finally
、throw
,具体实现和CoffeeScript相似。例如:
throw
后面甚至可以啥都没有,若在catch块内则编译成throw catchExceptionVar
,若不在catch块内则编译成throw undefined
。例如:
当你想在catch
块内重新抛出异常时就可以省略很多字符。
try...catch
的本质是函数式的。例如你可以写:
和CoffeeScript还有一点不同的是,如果只有try
和finally
,没有catch
,那么编译成的JS仍然会有一个空的catch
。例如:
上述代码中,外层的catch
并不会被触发,a
的最终值为4而不是12。这是因为编译成的JS中,内层会有一个空的catch
。
所以,在编译成的JS中,try
是永远带有catch
的。那么如果要实现JS的不带catch
的try...finally
,该怎么办?也简单,加上catch
,重新抛出:
这样a
就等于12了。它基本上等价于这段JS:
下面的表格可以帮助理解在哪些情况下捕获异常:
JS | CoffeeScript | FutureScript | |
---|---|---|---|
try...catch |
捕获 | 捕获 | 捕获 |
try |
非法 | 捕获 | 捕获 |
try...catch...finally |
捕获 | 捕获 | 捕获 |
try...finally |
不捕获 | 不捕获 | 捕获 |
可以看出,JS和Fus都比较纯粹,因为JS是仅当有catch
时捕获,而Fus则是全捕获。CoffeeScript则是存在try...finally
的特例。
有一点需要注意,catch
变量和普通变量没有区别,它的作用域并不局限于catch
块。这一点和CoffeeScript相同,但和JS不同。请看如下代码:
其输出的是false
而不是true
。所以,要尽量避免内外层的catch
变量名相同。
try
块和finally
块若是内联块,则必须占满整行。
'wait
使异步函数从内部看上去像同步一样(然而不改变其异步的本质)。考虑这样一个JS:
ES2017把它简化成:
{
console.log(await (await (await aaa()).bbb()).ccc());
})();
]]>
在Fus中,我们进一步简化:
一旦代码有'wait
,那么代码所在的函数(直接所在,非嵌套)就被编译成异步函数。我们不用async关键字声明异步函数,这个关键字根本多余。
还有一点不同,就是在JS中await
放在左边,而Fus中'wait
放在右边。我们的要好于JS,因为这就减少了很多括号,可读性也更强。
'wait
必须出现在函数中。
类的“字段”、“属性”是同义词。当字段是函数时,才能称为“方法”。类的字段(包括静态字段)、构造函数(包括静态构造函数)统称为类的成员。注意:类的成员和对象的成员不是一个概念。
例子:
me.name: name
speak: <>
console.log me.name + " makes a noise."
Lion: class from Cat
speak: <>
super()
console.log me.name + " roars."
]]>
constructor
我们简写为new
。
JS的this
我们用me
代替。我们还有一个关键字Me
,M大写,表示me
所在的类。当静态调用时Me
和me
相同,但建议使用Me
,因为大写比较像类。
FutureScript的类的一个亮点是“防冲突字段”,可以把它近似看成是私有字段。防冲突字段的好处不是外部无法访问(外部只是无法通过对象属性直接访问而已),最大的好处是继承类可以放心地使用同名字段,而不用担心会覆盖,这在类关系很复杂的项目中尤其重要。
我们先看一个反面例子,假设别人用JavaScript写了一个库,里面有个ES6类:
他还在API文档中写明了User
类的构造器和getUsername
的使用方法。于是你写了一个HumanUser
类,继承User
:
由于别人在API文档中没有告诉你用到了哪些“私有字段”,而你的“私有字段”正好也使用了_name
,这样一来,你的类就无法正常运行了。
在FutureScript中,只要字段名以_
开头,便自动成为防冲突字段。这两个类用FutureScript可以改写为:
me._name: username
getUsername: <>
me._name
]]>
和
super()
me._name: name
getName: <>
me._name
]]>
为什么它能防冲突?因为me._name
不是编译成this._name
,而是编译成this[_name]
,其中_name
不是字符串,而是Symbol的一个实例。编译器一碰到me.
,并且后面是以_
开头的话,即这样编译。你可以试试看,在JS中还能不能用instance._name
来访问这个属性。
防冲突字段还可以出现在字段的声明中,例如:
防冲突字段不能用引号包起来。所以如果你实在是想让你的普通字段以_
开头,就可以写成像me."_name"
的形式,这就不是防冲突字段了。
静态字段我们使用static
,例如:
防冲突字段也适用于静态字段,me.
改成Me.
即可。
还支持像C#一样的“静态构造函数”,只要static
后面不跟任何名称,例如:
result: longTimeTask()
Me.part1: result.0
Me.part2: result.1
static ok: <> Me.part1'ok
]]>
它就相当于:
Me.part1'ok
result: longTimeTask()
Website.part1: result.0
Website.part2: result.1
]]>
静态构造函数的好处是所有和类有关的代码可以都写在类里面,看上去更舒服,还能减少输入量,变量可以不暴露在外面。
静态构造函数内的me.
和Me.
都指向当前闭包所在的类,即这个类本身,而不是当前对象的类。
写过C#的程序员一定不会对getter和setter陌生,我们的类也支持:
me._name: @0
name'get: <>
me._name.toUpperCase()
name'set: <>
me._name: @0
]]>
fun
对应的函数不能是类成员。
创建实例时无需使用new,因为其实JS的new是多余的,一个类作为一般函数也没有什么实用价值。所以在FutureScript中,只要这被识别为一个类,那么当被作为函数调用时,编译出来的就会自动加上new。反之,如果不是类,编译出来的就不会加上new。具体的识别方法由版本行指定。当然,罕见的情况下,某个库的函数名可能会不符合约定,造成被识别为类,这时可以用nonew
关键字,语法上和new
一样。
JS支持new a
(不加括号)的形式,但我们不支持,因为容易误解。
JS支持new new a(b)(c)
的形式,但我们不支持,这太没有可读性了,完全可以写成new (new a(b))(c)
。
只要类有实例化需求,就应该取大写字母开头的名称,否则会影响编译器判断。如果类的所有需求都是静态,那么就任意了。
Number
、String
、Boolean
、Symbol
始终被识别为不是类,无论编译选项如何。
FutureScript的类属于ES6的类,而不是CoffeeScript的类或ES5中用函数模拟的类。这有什么区别呢?在CoffeeScript中,继承类的构造函数里面super
和this
的位置可以任意;但在FutureScript(或ES6)中,继承类的构造函数里面me
不能出现在super
之前。这的确更严格,但其实和C#以及主流编程语言相同,并且使这成为可能:继承Array
、Error
等内置类,这在ES5时代是不可能的。
但是FutureScript中super
的语法比较像CoffeeScript,不用写super.methodName()
,直接super()
即可。当然,我们也支持super.methodName()
,因为这可以用来调用不同名字的方法。我们不支持孤立的super
,不过你可以写成super'(@)
或super(@...)
。
我们知道一般构造函数是没有返回值的,但其实在JS(包括ES6)中,构造函数可以有返回值,可以返回任何对象,这样创造的就不是该类的实例。但这很怪异,没有什么实用价值(有这个用途的话,可以使用静态方法)。所以在FutureScript里,构造函数虽然也可以有返回值,但它几乎不起作用,创造的永远是该类的实例。
不要在类的方法或构造器内部为另一个对象的原型方法赋值。例如这样不合理(虽然不会出错):
Abc.prototype.def: <>
doSomething()
]]>
这是因为在该原型方法内使用me
的话表示不了你想要表示的东西。正确的方法是把它写在类的外面。至于嵌套类,是可以的,因为编译器能识别class
关键字,从而做出对于me
的正确解释。不过嵌套类好像用处也不大,建议类与类的关系还是扁平为好。
如果你使用过underscore函数库,应该比较熟悉这样的代码:
管道操作符是|>
,可以把顺序调整为人类习惯的顺序,于是可以这样写:
_.map(x -> x * 2) |> _.max
]]>
注意,由于运算符的优先级,管道内的函数调用不应使用空格,例如上例不应写成:
_.map x -> x * 2 |> _.max
]]>
还有一种运算符::
,称之为“胖点”,专门应用于含有管道运算符的表达式,如果你想让管道和点号混合,胖点能提供更强的可读性。例如:
_.map(x -> x * 2) :: map(x -> x + 1) |> _.max
]]>
相当于:
_.map(x -> x * 2)).map(x -> x + 1)) |> _.max
]]>
所以胖点基本就是点号,只不过我们规定当它出现在管道右边时作为结束标记,而且看起来比较“胖”,和管道运算符比较接近,显得比较舒服。
管道还可以和异步相结合,例如:
lib.process1'wait |> lib.process2'wait
y: data |> lib.remoteCombine(anotherData1)'wait |> lib.remoteCombine(anotherData2)'wait
console.log(x, y)
]]>
相当于:
等价的JS代码是:
{
var x = await lib.process2(await lib.process1(data));
var y = await lib.remoteCombine(await lib.remoteCombine(data, anotherData1), anotherData2);
console.log(x, y);
})();
]]>
管道也支持自动加上new
,例如:
Foo
bar: 2 |> Bar()
]]>
相当于JS的:
不过,如果标出new
或nonew
,则必须加上括号。例如如下的代码是非法的:
nonew Foo
]]>
用到管道的代码可以是兼容模式,也可以是激进模式。但如果管道使用的外部库也用FutureScript写的话,还是用兼容模式来写比较好,因为外部库中的函数必须是多参数的,因为管道本身就是分割参数的。
点点..
方法可以看成是虚拟的方法,它和管道的用途相似,但让使用underscore变得更为简单:
_
console.log [3, 4, 5]..map(x -> x * 2)..max()
]]>
相当于JS的:
你也可以让点点方法和点号方法混合,点号方法即是对象的原生方法,例如:
x * 2).map(x -> x + 1)..max()
]]>
第二个map
是Array
的原生方法。
对..
赋值可以用冒号,也可以用as
。
支持导出和导入..
。这样你就可以把它放进被批量导入的manifest模块,就不用在每个文件中都给..
赋值了。例如你在manifest.fus中可以写:
_
]]>
导入和导出的不同形式也适用于..
,例如:
或:
对..
所赋的值必须是一个函数,这看似多余但却使它更为健全,因为你可以给不同的类型使用不同的类库。这样,某个类库就可以真正成为某个类专属的“虚拟方法”,即使两个类库的函数重名也不会引起冲突。例如:
if x is String
require("lodash")
else
require("underscore")
]]>
..
作为运算符时,右边必须紧接字母、_
或$
。
do
是(...)()
的简便方法,例如:
相当于
比CoffeeScript的do
功能略少,但更纯粹。它可以用来:
它的功能比CoffeeScript少在哪里?看一段CoffeeScript代码:
button.onclick = ->
console.log button.textContent
]]>
这种通过do
来hold住一个变量名的方法,由于FutureScript实行严格的禁止层级变量重名,所以我们不支持,但是现在看来也无需支持,因为有了更好的方法:
button.onclick: --
console.log button.textContent
]]>
或:
button.onclick: --
console.log button.textContent
true
]]>
这就是函数式风格胜过循环的地方。
但forEach
和every
有啥区别?这已经超出了本文的范围,不过挺重要的,还是说一下。这两个数组方法都属于ES5规范,当使用forEach
时,是无法模拟break
的。当使用every
时,可以巧妙地模拟break
,只需让它返回false即可。例如:
console.log i
i < 5 ? true | false
]]>
也可以不用true
、false
,而用更短的1
、0
:
console.log i
i < 5 ? 1 | 0
]]>
当然,do
虽然不能像CoffeeScript一样hold住变量名,但你只要取个不一样的名字,就可以达到相同的目的:
但问题是我们没有原生的循环,所以上面这个例子也没有什么用。
do
的右面必须是函数字面量,这个比CoffeeScript严格。
这几个关键字或操作符和“存在性”有关:'ok
、ifvoid
、ifnull
。
a'ok
,意思就是a既不是void,也不是null。
ifnull
和ifvoid
有两种形式,一种是后面跟冒号,一种是后面不跟冒号。例如:
后面跟冒号时,即是赋值,目前暂不支持与[...]
、,
、'export
共存,例如这些是非法的:
前面介绍的很多都用到了单引号,这称为变体,下面总结一下。
形式有两种,一元'
和二元'
。一元'
即函数的单参数变体。其他的都是二元'
。
编译器如何判断某个'
是一元还是二元?看'
后面紧跟的字符。如果是数字或字母,则是二元,否则(例如是符号、空格或根本没有字符)要么是一元,要么无法解析。
所以这两者是一样的:
但这个不一样:
为了可读性,一元的'
前面的那个字符不能是空白。像这个就是非法的,必须无法解析:
行不能以'
开始。像这个是非法的(在未来版本中可能会合法):
冒号左边的'
目前不能超过2个。
in
的用法和CoffeeScript几乎完全相同,当然,不支持!in
,可以用... not in ...
或not ... in ...
。示例:
is
表示左边是右边的类型,类似于JavaScript的instanceof
,但又有些区别,它还能判断原始类型,可以说结合了instanceof
和typeof
。当右边是Number
、Boolean
、String
、Symbol
(必须严格表示为这4个之中的1个,不能是引用)时使用typeof
。
与in
相似,is
表示否定时也是可以用not is
,不过也可以使用更顺眼的isnt
,甚至可以用is not
。
这4个表达式的功能是一样的。你可能要问,a is not A
不会引起歧义吗?不会,因为a is (not A)
是无意义的,布尔值不是类。
delete
和JavaScript中的稍有不同。它不再是一个表达式。这是因为在JavaScript strict mode中并没有值为false的情况,而是抛出异常。既然我们都是用strict mode,所以没必要弄成表达式。
rem
即是JavaScript的%
。mod
是另一种求余,正负号和除数相同,只要除数是正数,结果就不为负,所以这种求余适用范围可能更广一些。a mod b
相当于JavaScript的(a % b + b) % b
。
**
表示幂运算。例如:
一元的+
、-
(正负号)的用法和CoffeeScript相似但有微小区别,我们规定它只能是紧接着算元,如果后面的字符是空白则被当作加号和减号。例如:
如果后面的字符不是空白,则如果前面的字符是字母、数字、_
、$
、)
、]
、}
、"
或@
,则被解析为加号和减号,否则被解析为正负号。
另外,作为运算符时,+
、-
、*
、/
之后不能紧接+
或-
。
使用pause
命令插入断点。它等价于JS的debugger
命令,因为debugger
违反了关键字不能大于7个字符的原则,所以改名为pause
。
使用js"
。例如:
它完全废除了\
的功能。如果是内联,那么双引号也无法在其中用任何变通的办法表示(否则会有代价),不过妙在JS也支持单引号。如果必须用双引号,就用格式。
为了能够正常运行,插入的整体必须是表达式,例如JS函数表达式、JS赋值表达式等。这一点,和CoffeeScript相同。
因为整体必须是表达式,所以最外层不能有分号,例如你可以这样:
但你不能这样:
变量名不可以是关键字。变量名可以是JS关键字(只要它不是FutureScript关键字即可),例如function
、instanceof
都可以作为变量名。
类声明中的属性名如不用引号,则不可以是new
、static
。如用引号,则没有限制。注意,static
不是关键字,new
用来进行类声明(即后面是冒号)时不是关键字。
对象字面量的属性名没有限制。
当然,不用引号时,以上名称都需要满足标识符约束,即:每个字符都是字母、数字、_
或$
,且起始字符不是数字。
运算符的运算结果是表达式,但算元不一定是表达式,也可以是块,甚至是非值的东西。不是所有运算符都有优先级。算元必须占满该行剩余部分(除非用<<
和>>
)的运算符(即通常分多行写的)就没有优先级,例如match
和class
。后置的if
也没有优先级。下面表格中,a、b、c表示表达式算元,m、n表示块算元,x、y、z表示不一定是表达式或块的算元。
注意,优先级从低到高。别的语言参考,都是优先级从高到低,但我觉得不妥,因为用函数式(递归)思维来看,应该从“大”(低)到“小”(高)。结合性,我们也和传统的写法相反,传统写的是执行顺序,我们写的是解析顺序(更精确地说是用递归解析的顺序)。即:执行时自左向右的,由于右侧较“大”,我们写为自右向左,简称“右”,而执行时自右向左的,由于左侧较“大”,我们写为自左向右,简称“左”。打个比方,1 + 2 + 3
其实是被解析为plus(plus(1, 2), 3)
。我们仍然是以大为先。总之这方面大家反着看就行了。
Precedence | Operator type | Examples |
---|---|---|
0 left | Arrow Function | x -> m |
Diamond Function | <> m |
|
Dash Function | -- m |
|
1 left | Conditional | if a then m else n |
2 left | Space Function Call | a b |
Space new | new a b |
|
Space nonew | nonew a b |
|
3 right | or | a or b |
4 right | and | a and b |
5 left | not | not a |
6 right | Equality | a = b |
Inequality | a /= b |
|
Less Than | a < b |
|
Less Than Or Equal | a <= b |
|
Greater Than | a > b |
|
Greater Than Or Equal | a >= b |
|
7 right | in | a in b |
Negative in | a not in b |
|
is | a is b |
|
Negative is | a isnt b |
|
8 right | Addition | a + b |
Subtraction | a - b |
|
9 right | Multiplication | a * b |
Division | a / b |
|
Remainder | a rem b |
|
Modulo | a mod b |
|
10 left | Exponentiation | a ** b |
11 left | Unary Plus | +a |
Unary Negation | -a |
|
12 right | ifvoid | a ifvoid b |
ifnull | a ifnull b |
|
13 right | as | a as x |
export as | a export as x |
|
14 right | Pipe | a |> b |
15 right | Member Access | a.x |
Dot Dot Member Access | a..x |
|
Fat Dot Member Access | a :: x |
|
Function Call | a(b) |
|
new | new a(b) |
|
nonew | nonew a(b) |
|
Variant | a'x |
|
Function Variant | a' |
|
import | import "x" |
问:CoffeeScript也有return
,FutureScript居然没有?
答:return
是一种跳跃式语句,函数式风格应该尽量避免这种语句,很多函数式语言,像微软的F#,是没有return
的,F#有循环,但是也没有能跳出循环的break
命令。就好比现在人们都认为C语言的goto
命令是很邪恶的,就是因为goto
是跳跃性的。当然return
只是跳出而已,不能自由跳跃,远没有goto
那么邪恶,但我想,至少在FutureScript的初创阶段,还是彻底一些好。其实你很容易就可以达到相同的目的。比如你想这样写:
if x isnt Number return
x * 2
]]>
现在你可以这样写:
if x isnt Number
void
else
x * 2
]]>
如果你嫌行多,可以这样写,也是2行:
x isnt Number ? void |
x * 2
]]>
这种方式的好处就是结构更清晰,只要不抛出异常,函数的出口永远只有一个,永远在底部。
问:为啥没有位操作符?
答:首先,FutureScript作为高级语言,没必要支持。其次,JavaScript的位操作是专门针对32位数字的,这本身就不合理。
问:为啥没有生成器?
答:生成器是适合特定用途的,似乎普适性还不够。至于缩短代码长度?加一个类库也可以达到这个目的,干嘛非得改动语言层面呢?至于在异步操作中的用途,FutureScript也已经有了更好的替代方法。
问:为啥格式字符串的缩进必须大于引号处的缩进?等于也不行吗?
答:如果允许等于或小于,就有可能造成编译器误判,当字符串中某行行首正好是引号的时候。
问:为啥函数除了箭头符号以外还要弄个菱形符号?
答:我的目标是消除CoffeeScript中参数两边的括号。但是如果没有括号的话会产生歧义。所以我就想出了这个办法,如果不加括号,那就必须为1个参数。那零个参数怎么办?就再造一个符号呗。至于多个参数,就必须加括号,不过多参数并不在我的理想化(即激进模式)的构想当中,所以这也可以接受。再有一个原因,就是CoffeeScript用箭头表示零参数的话看上去既别扭又误导,箭头嘛,感觉应该左边有东西才对。
问:既然如此,那为啥激进模式不废除箭头符号?反正有@
可以用。
答:激进模式中,大多数情况下确实没必要给参数取名,但当你有多层函数,内层调用外层参数的时候,取名就有必要了。
问:为啥没有保留字?
答:很多语言都有很多保留字,但我觉得你设计语言的时候预先定义好未来的功能,那未来有可能是你没料到的。我们在版本行使用版本号,也达到了“安全”的目的,因为可以保证只要你不修改版本号,就不会出现兼容性问题。
问:不支持var
是受CoffeeScript启发,但为啥不支持ES6的let
和const
?
答:我的理念是,一名程序员也许不熟悉正在开发的文件之外的其他文件,但是一般都会熟悉这个文件当中的其他区域。let
提供块级的保护,但是只要你熟悉的不是只是这个块,你完全可以使用一个不同的变量名。块级保护太小了,函数级保护已经足够坚固。否则的话语言本身的复杂度也会增加。let
和CoffeeScript风格之间也有矛盾。而且我们是编译成var
,曾经我想把所有变量编译成let
,但是考虑这样的代码:
0
a: 1
b: 2
c: 3
else
a: 4
b: 5
c: 6
console.log(a + b + c)
]]>
如果是编译成let
,那么这段代码是有错的,必须要在顶部给a
、b
、c
赋值才行。所以我觉得还是编译成var
好。
至于const
,我觉得理念有问题,const
只是针对常量而已,而我们让某个东西不可变,不是防自己改,是防别人改。通常是自己做了个库,别人使用时,在运行时可能会乱改内部逻辑。变量仅自己可见,别人改不了,又何必成为常量?真正要保护的其实是对象的属性或方法,这个const
却无法做到。可以通过Object.defineProperty
来做到。但要动就得动包括原生对象在内的所有的对象,让方法都变得坚固(即变成类似静态语言的样子),这工作量太大了,可行性也值得怀疑,所以目前暂不做。
问:为何不支持对象解构赋值和复杂数组的解构赋值?
答:对象解构赋值过于复杂。它符合了人类一部分直觉,但违反了人类另一部分直觉,导致一开始根本就看不懂。复杂数组的解构赋值是中等复杂,以后也许会支持。至于对象解构赋值,以后即使支持,语法也要改得更自然。
问:为什么arguments
用@
表示?
答:首先,@
形状也像个a。其次,@
是符号中看上去最不像符号的,我们经常会使用孤立的@
作为一个表达式,这时如果用其他符号,会显得不像个表达式,就不舒服了。
问:为什么以多参数调用函数一定要加括号?CoffeeScript不是可以不加吗?
答:多参数要用到逗号,不加括号的话,逗号到底分隔什么,不容易看清楚,大多数时候违反人类直觉。而且,现在默认是兼容模式,但因为我觉得激进模式更完美,如果多参数可以不加括号,就显得更加鼓励人们采用兼容模式了。
问:@
很好用,如果catch
的上下文也能用一个符号表示就好了。
答:本来我考虑再用个新的符号来指代子语句内的上下文,不仅限于catch
,但想想,不宜。用@
时你很容易看清某个@
所对应的是哪个函数,因为函数符号很好辨认。但子语句仅有的特征是缩进,而缩进我们是用得非常多的,很容易这个新符号的意思会变来变去,导致你搞混。如果这个新符号仅限于catch
,又适用面太小。
问:as
为何不能用逗号同时赋值?
答:若支持,像这个表达式[aaa as a, bbb]
就容易产生歧义。
问:保留旧版本的编译器,那岂不越来越庞大?好恐怖!
答:其实也没那么恐怖,我们在做FutureScript的时候会采取一些手段,使增量不那么大。
问:真的所有旧版本的编译器都包括吗?
答:我们尽量包括。但如果我们发现某个旧版本有安全漏洞,那么我们则会删掉。若不存在与代码相同版本的编译器,编译的时候会出现警告,同时会自动用新版本的编译器编译。
问:CoffeeScript多行链式(chaining)方法调用不用加括号,这里为啥要加括号?
答:CoffeeScript其实是硬生生加了条规则,这会破坏语法的统一,而且虽然在一部分需求中更简洁了,但当有另一部分需求的时候却更复杂了。所以我决定不用这套方法。
问:为啥一个模块既有“默认导出”又有“命名导出”不好?
答:你完全可以分两个模块,或者给“默认导出”取个非default的名字(事实上“默认导出”的确是“命名导出”的一部分,名字是default)。
问:为啥->
函数没有定义@
?
答:当你参数有名称时,再用@
就显得多余。此行为类似ES6的=>
函数。
问:JS中可以用typeof a === "undefined"
检查变量是否已声明又不引发异常,这里为啥做不到?
答:这好像并没有什么用。FutureScript的变量统统编译成在函数顶部声明,所以变量的声明与否不是动态的,所以检查这个不应该是程序运行时做的事情。
问:为啥幂运算符**
的优先级和CoffeeScript不一样?
答:虽然和CoffeeScript不一样,但是和大多数有这个运算符的语言一样,比如F#,比如ES2017。CoffeeScript应该是考虑到-3 ** 2
如果书写成-32应该是-9,但这看上去非常别扭,除非你加个空格,成为- 3 ** 2
,或者干脆都不用空格,成为-3**2
,但很少有人会这么写,前者还和多行连接有冲突。另一个原因是,CoffeeScript这样做弄得不大协调,就**
显得那么特殊,其实*
的优先级理应只比**
低一级,只不过(-a) * b
恒等于-(a * b)
,所以可能他们觉得无所谓就不调整了而已。
问:not
优先级被大幅降低,这合理吗?
答:这看上去更自然,更符合人类阅读方式。我承认,这个改变从来没有人做过,具有冒险性。为了看看有没有反例,支持not
应该维持目前通用的优先级,我看了GitHub的Atom编辑器的源代码(它是用CoffeeScript编写的),还看了我自己的代码。我发现,几乎没有一处not
在目前通用的优先级下更简单的,这给了我信心。因为我的想法是,算术运算符,像加法、乘法,对它们的算元使用not
是非常不好的,也许有人会利用这种方式来取巧,但可读性非常差,所以好的代码中是不会出现not a + not b
的,这种反例不应该使用。引申到=
、<
、>
也是一样。
问:为啥格式注释是用###
?如果像格式字符串一样,不是更简洁吗?
答:有时我们喜欢用注释把一段代码作废,就不可能弄得很整齐。
问:如果export {abc}
表示命名导出,export abc
表示默认导出不是很好嘛?
答:原先是有这种考虑,但这会产生歧义,因为{abc}
也可以被当作一个对象。
问:为啥不支持Unicode变量名?
答:很少有这种需求。不过以后版本中可能会支持。
问:above
关键字有啥用?
答:above
关键字本来是用来帮助你看清某些特殊形式的代码,但后来我决定在将来版本中再支持这种“特殊形式的代码”,所以above
现在没有任何作用。
问:CoffeeScript的?.
很好用,为啥Fus一开始支持,现在却取消与它相似的'ok.
?
答:如果支持,那么这样的代码:
你的本意是让它在满足既'ok
又整个链条的值不等于3
时才执行foo()
,但实际上当不'ok
时链条值为void
,也不等于3
,所以也会执行foo()
。所以我觉得这个功能会产生陷阱,并不完美。
还有一个原因,就是一开始支持的功能并不完整,是有缺陷和bug的。若要使它完整,要极大改变编译器现有架构,工程量太大。所以干脆先删掉这个功能,若以后需要,再做。