为什么选择Coffeecript
Javacript是WEB世界的通用语言,但其各种缺点也让人头疼,包括设计缺陷,糟糕的语法,各种浏览器的不兼容解释......用作主力编程语言让人不太放心。所以,更多地是作为前端语言(特别是通过jQuery)和HTML一起工作,发挥其各种库的优势。
Coffeescript是对Javascript的改良,优点是:
(1)优雅简洁的语法(python-like);特别是使用缩进代替{}让我十分喜欢
(2)提供了类继承的功能;
(3)无缝地使用JS及其各种资源;
(4)避免了许多语言陷阱。
所以,我选择Coffeecript(CS)作为我的后端主力语言,配合Node.JS承担后端的主要工作:完成业务逻辑、数据库交互、系统IO等。
相信CS的优点会吸引越来越多的人使用它。而且,“使用各种特定优势语言,编译为javacript后运行”也成为一种潮流。
The golden rule of CoffeeScript is: "It's just JavaScript".
如何让Coffeescript和NodeJS协同工作
在使用CS之前首先要解决它和NodeJS的协作问题。毕竟,NodeJS只是Javacript的解释器。
方法有二种:
1、使用require.extension在后台自动翻译
NodeJS有require.extension功能,可以识别出CS文件,并在运行前自动编译为JS文件执行。
比如,
我写了helloworld.coffee,代码如下
console.log 'hello world jifeng'
如果我用nodejs代码,想要调用上面的helloworld.coffee的代码,只需要写index.js
require('coffee-script'); var hello = require('./helloworld.coffee');
它所依赖的就是nodejs的require.extensions的功能,具体代码可以看 coffee-script.js,主要代码就是这块
loadFile = function(module, filename) { .... return module._compile(compile(stripped, { filename: filename, literate: helpers.isLiterate(filename) }), filename); }; if (require.extensions) { _ref = ['.coffee', '.litcoffee', '.coffee.md']; for (_i = 0, _len = _ref.length; _i < _len; _i++) { ext = _ref[_i]; require.extensions[ext] = loadFile; } }
CoffeeScript就是的做法,就是通过nodejs自带的require.extensions扩展功能, 在require '.coffee' 文件时,先将改文件编译成JavaScript代码之后再被加载。
Coffeescript安装与编译
安装 npm install -g coffee-script 全局安装在usr/local/bin下面。版本1.6.3。以后只要在命令行执行 coffee filename就可以运行.coffee文件了。
在命令行执行 coffee -i 进入coffeesript的交互命令界面,可直接运行代码片段。
常用CS命令
-c, --compile 编译为javascript代码-i, --interactive 启用交互式命令终端 (Ctrl-D to exit, Ctrl-Vfor multi-line) -o, --output [DIR]将编译后的JS文件写入某个文件夹。常和-c, -w联合使用-w, --watch监控文件,常与-c联合使用,对更新的CS文件自动重新编译。 |
书写规范
//这是注释行一
#这是coffeecript注释行二
###
这是coffeescript风格的注释段
###
使用注释将程序分成很多模块。
console.log (”Hello, world“);
因为在C/C++中,双引号才表示字符串,所以这种情况下,还是使用双引号的好。
另外对于W3C标准来说, HTML中的属性值应该是使用双引号来包含的, JSON对象中也要用双引号标识字符串。所以,统一使用双引号。
另外,英文中的所有格要使用单引号,容易混淆。“I'm a student"。
理由:视觉上的清晰
if (c && c != ch)
awardMedals = (first, second, others...) -> gold = first silver = second rest = others
基本数据类型
coffeecripte使用javascript的基本数据类型,包括4种:数字:所有数字都是64位的浮点数。NaN表示不能产生正常结果的运算结果。Infinity表示无限大。可以用指数法,如1e2表示100。
没有整数类型,但可以用Math.floor(number)方法把浮点数转换成整数。
数字可以参与计算和比较。
字符串:由16位的unicode字符组成的一串字符,用单引号或双引号括起来,可能包含0个、1个或多个字符。有一些转义字符,如”\"等。
字符串有length属性和很多方法。
布尔值:true(yes,on)和false(no, off)
空值:null(空)和undefined(未定义)
数字
转换成整数 Math.floor(3.14) 值是3转换成字符串:用.toString(radix)方法。radix代表进制,默认是10。如果是2,则转化为二进制数字形式的字符串,8则转化成八进制形式的字符串,以此类推。
3.14.toString() 值是"3.14" .toString()可简写为String(number)
2.toString(2) 值是"10"
13.toString(8) 值是"15"
123.toString(16) 值是”7b"
上述字符串可以用parseInt()再转换回数字。如:
parseInt(2.toString(2)) 值是10
parseInt(13.toString(8)) 值是15
但不能用于其16进制。
10000.toExponential(2) 值是"1.00e+4"
转换成指定精度的字符串:用.toFixed(digits)方法。digits代表小数位数。
3.toFixed(5) 值是是‘3.00000’
字符串
转义字符把特殊符号插入到字符串中,用\作为前缀:\" 双引号
\' 单引号
\\ 反斜线
\/ 斜线
\b 回退(backspace)
\f (formfeed)
\n 换行
\r 回车
\t tab
\u 字符编码
.length 返回字符串的长度
当然是包含空格的
.toUpperCase 转换为大写
"one two three".toUpperCase()
# => 'ONE TWO THREE'
.toLowerCase 转换为小写
"ONE TWO THREE".toLowerCase()
# => 'one two three'
.concat(strint) 连接(将多个字符串连接在一起,用+更方便)
"coffee"+"script" 结果是"coffeescript"。
.split 分割,将字符串用指定的分界符分成数组中的各个元素。第一个参数是分界符,第二个参数是取几个元素。
“this is a book".split(" ",2) 以空格作为分界符,取头两个元素。结果是['this','is']
如果用.split(”“)会把整个字符串分解成字母数组。
.indexOf(starchString, position)查找子字符串
首个参数是要查找的字符串,可选参数position可指定从某个位置开始查找。如果被找到,则返回第一个匹配字符的位置,否则返回-1。
text = "Mississippi"
text.indexOf("ss") 值是 2
text.indexOf("ss", 3) 值是 5
text.indexOf("ss",6) 值是 -1
.lastIndexOf方法将从字符串的末尾开始查找。
.search(regexp) 查找(正则表达式)
如果找到,则返回第一个匹配的首字符位置,否则返回-1。此方法会忽略g标识。
text = "1234abc"
text.search(/[a-z]/),值是4。注意:正则表达式要包在/ /之间。
.charAt(pos) 返回指定位置的字符
如果pos小于0或大于字符串的长度,将返回空字符串。
name = "Curly"
name.charAt(0) 值是“C”。
.charCodeAt(pos) 返回指定位置的字符码
返回以整数形式表示的在pos处的字符的字符码。如果pos小于0或大于字符串的长度,则返回NaN。
name = "Curly"
name.charCodeAt(0) 值是67。
Array[n+1].join "string" 构造一个重复n遍string的字符串
Array(11).join 'foo'
# => "foofoofoofoofoofoofoofoofoofoo"
.localeCompare(that) 比较字符串
将当前字符串与that字符串相比较。如果相等则返回0,否则返回其他数字。
注意:不是.compare。
.slice(start,end) 复制字符串
复制字符串从start到end位置(之前)的部分来构造一个新的字符串。end参数是可选的,默认等于string.length。
如果要得到从位置p开始的n个字符,就用string.slice(p,p+n)。
text = "Curly's book store"
text.slice(0) 值是"Curly's book store"
text.slice(1,5),值是“urly”。end位置是\',不包括在结果中。
.match(regexp) 匹配
让字符串和一个正则表达式匹配。根据g标识,如果有g标识,则生成一个包含所有匹配的数组。如果没有g标识,则返回第一个匹配的内容及其索引。
text = "0123abcABC4567abcABC"
text.match(/\d/g) 返回['0','1','2','3','4','5','6','7']
text.match(/\d/) 返回['0', index:'0', input:'0123abcABC4567abcABC']
.replace(searchValue,replaceValue)替换
对字符串进行查找和替换,并返回一个新的字符串。参数searchValue如果是一个字符串,则只会替换第一个符合的对象。所以通常我们用带g标识的正则表达式。它会替换所有的符合项。
text = "0123abcABC4567abcABC"
text.replace(/ABC*/g, "XYZ") 返回“0123abcXYZ4567abcXYZ"。
如果replaceValue不是一个单一的字符串,而是根据不同情况替换为不同的字符串,可以用函数。每遇到一次匹配,函数就会被调用一次,而该函数返回的字符串会被用作替换文本。
高级数据结构
数组
表示法 list = [1, 2, 3, 4, 5] 这也是创建数组的字面量法。 如果是创建一个有规律的数组,可以用for循环语句。
数组是引用型对象(reference type),即使内容完全相同,也不相等。[1,2,3] != [1,2,3]
求长度 [1,2,3].length 值是3
数组成员可以竖着写,省略逗号
contenders = [
"Michael Phelps"
"Liu Xiang"
"Yao Ming"
"Allyson Felix"
"Shawn Johnson"
"Roman Sebrle"
"Guo Jingjing"
"Tyson Gay"
"Asafa Powell"
"Usain Bolt"
]
数组可用..来指定从第XX到第XX的范围(包括两头),或...(不包括末尾)
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
start = numbers[0..2] //1,2,3
middle = numbers[3...6] //4,5,6
end = numbers[6..] //7,8,9
copy = numbers[..] //全部
注意:如果索引超出了数组的边界,如numbers[10]或number[-1]结果是undefined。但如果numbers[0..-1],出来的还是全部值[0..8],而numbers[-1..0]出来的是[]。因此,必须保证数组的索引不能是负值。超出数组边界的范围,如number[10..5]都会导致空数组。
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
numbers[3..6] = [-3, -4, -5, -6]
第3到6个值被替换,结果是[0, 1, 2, -3, -4, -5, -6, 7, 8, 9]
list = [1,2,3,4]
a = list.splice(1,2) a的值是[2,3],list的值现在是[1,4]。
*splice(挖)还可用于把数据从数组中挖出来和替换某些值。如上例list = [1,2,3,4],r = list.splice(1,1,'ache','bug'),则r == [2],而list变为[1,'ache','bug', 4]。
.splice返回的结果是由挖出来的数组成的数组(原数组则被挖掉这一块)。
list1 = [1, 2, 3]
list2 = [4, 5, 6]
list1.concat(list2) 结果是[1,2,3,4,5,6]
list1.concat(list2,7) 结果是[1,2,3,4,5,6,7]
注意:如果用list1+list2,会得到[1,2,34,5,6]这样不正确的结果。
这种方法把数组中的每个元素都用separator分隔符(默认是逗号)连接在一起,组成一个字符串。要得到无间隔的连接,使用空字符串做分隔符。
list1 = [1, 2, 3]
list1.join(), 值是“1,2,3”
list1.join(''),值是“123”。
list1.join('-'),值是“1-2-3”。
pop方法移除数组的最后一个元素并返回该元素;push方法把一个或多个参数附加到数组尾部。如果push的是一个数组,会作为单个元素(子数组)被添加到数组中。push返回的是数组的长度。
list = [1,2,3,4]
list.pop() 值是4(最上方的元素)。 此时list的值变成[1,2,3]。
list.push(5) 值是5(数组长度)。此时list的值变成[1,2,3,4,5]
list2 = [6,7,8]
list.push(list2) 值是5(数组长度), 此时list的值变成[1,2,3,4,[6,7,8]]
array = ["a", "b", "c"]
array.reverse(), 值是["c", "b", "a"]
用默认的sort方法可以给字符串数组排序(按首字母)。
list = ["x", "a", "c", "abb", "y"]
list.sort() 返回["a", "abb", "c", "x", "y"]
对于数字数组的排序,请使用自己写的sortNumber()函数
return a - b
arr = [6,2.85,4,3,1000000]
result2 = arr.sort(sortNumber)
console.log(result2) #将显示[ 2.85, 3, 4, 6, 1000000 ]
n = [4,51,26,18,37,42]
Math.max.apply(null,n) 则返回最大值51.
Math.min.apply(null,n) 则返回最小值4.
对象
现实中的事物的信息很少是单一的,而往往是复合的。表示这种“复合数据结构”, 对象是最简单、最灵活的方法。对象是可变的“名-值”集合,用于汇集和管理数据。javascript中的对象是无类型的,是可以直接继承的。对象引用
在CS中,不能访问对象的物理表示,只能访问对象的引用。每次创建对象,存储在变量中的都是该对象的引用,而不是对象本身。所以,给对象赋一个新的名字会指向同一个实体。如下例子可以说明:
x =foo:'bar'bar:'foo'y = xy.foo = 'test'
对象的创建:
1.使用对象字面量,逐一写出对象里的名-值。2.解析json文件而生成。
3.继承另一个对象而来。
对象表示法:在CS中用逐级缩进代表层次关系。
math =
root: Math.sqrt
square: square
cube: (x) -> x * square x
kids =
brother:
name: "Max"
age: 11
sister:
name: "Ida"
age: 9
表示一个对象是否存在,可以用存在符?
alert "I knew it!" if elvis? //如果elvis存在(不是null或undefined),就警告"I knew it!"。
检索:要检索对象里包含的值(访问其属性和方法),可采用如下方式。
对象名称.属性.二级属性
如:student.score.Math
status = flight.status
更新:对象里的值可通过赋值语句来更新
如:student.score.Math = 99.5
删除:可通过delete运算符来删除。如果对象包含该属性,则该属性被移除。
delete storage.nickname
删除对象的属性可能会让来自原型链的属性暴露出来
对象的继承
原型继承:直接把一个对象的某些属性改改,或增加一些属性就变成了一个新对象。这是一种差异化继承。使用Object.create方法:
比如已经有了myMammal对象,现在增加一个myCat对象
name : "mammal"
get_name : ( )->
return this.name
says : "huuuuu"
myCat = Object.create(myMammal)
myCat.name = "cat"
myCat.says = "Mewoo"
myCat.food = "mouse
result = myCat.name
result1 = myCat.says
result2 = myCat.get_name()
result3 = myCat.food
console.log(result) #输出"cat"
console.log(result1) #输出"Mewo"
console.log(result2) #输出"cat"
console.log(result3) #输出"mouse"
JSON
JS/CSt程序可以读取整个json文件到内存中,构成一个对象,对其进行各种对象操作。首先要做的是解析一个JSON文件为一个对象。使用JSON.parse方法: var data = JSON.parse('mydata.json'); 就可以把整个mydata.json解析为一个对象--data。方便地访问它的各种属性和方法。
在javascript中对对象进行了各种操作后(都是在内存中),要持久化,即存到磁盘上的json文件中。
使用JSON.stringify()函数将对象转换成JSON串,可以写到磁盘文件中。
json数据库的使用
json可以当作数据库来使用,推荐使用TaffyDB,它封装了各种数据库操作的接口,而且可以和node,jQuery等联合使用。读写磁盘文件通过node的fs方法。JSON文件相对于普通数据库的表-记录结构,具有表内字段可以不统一,各种表可以放在一起,可多级无限嵌套等特点,而且不用事先定义表结构和字段属性,十分灵活。
CS之面向对象篇
面向对象编程的概念
面向对象是一种对现实世界理解和抽象的方法,是计算机编程技术[1]发展到一定阶段后的产物。早期的计算机编程是基于面向过程的方法,例如实现算术运算1+1+2 = 4,通过设计一个算法就可以解决当时的问题。随着计算机技术的不断提高,计算机被用于解决越来越复杂的问题。通过面向对象的方式,将现实世界的物抽象成对象,现实世界中的关系抽象成类、继承,帮助人们实现对现实世界的抽象与数字建模。通过面向对象的方法,更利于用人理解的方式对复杂系统进行分析、设计与编程。同时,面向对象能有效提高编程的效率,通过封装技术,消息机制可以像搭积木的一样快速开发出一个全新的系统。首先根据客户需求抽象出业务对象;然后对需求进行合理分层,构建相对独立的业务模块;之后设计业务逻辑,利用多态、继承、封装、抽象的编程思想,实现业务需求;最后通过整合各模块,达到高内聚、低耦合的效果,从而满足客户要求。
面向对象编程的三大理念:封装、继承、多态
CS中对象的继承
原型继承:使用Object.create(父对象)方法创造一个子对象,直接把把对父象的某些属性改改,或增加一些属性就变成了一个新对象。这是一种差异化继承。下例中中先有”动物“对象,”哺乳动物“继承自”动物“,”猫“又继承自”哺乳动物“,每个子对象都具备父对象的全部属性和方法。
Animal =
name : "Animal"
move : true
eat : (food) ->
console.log("eat the #{food}")
console.log(Animal.name) #Animal
console.log(Animal.move) #true
Animal.eat("grass") #eat the grass
Mammal = Object.create(Animal)
Mammal.name = "Mammal"
Mammal.milk = "yes"
Mammal.says = "huuuu"
Mammal.get_name = ()->
return this.name
console.log(Mammal.name) #Mammal
console.log(Mammal.milk) #yes
console.log(Mammal.move) #true
Mammal.eat("meat") #eat the meat
console.log(Mammal.says) #huuuu
console.log(Mammal.get_name()) #Mammal
Cat = Object.create(Mammal)
Cat.name = "Cat"
Cat.says = "mewoo"
console.log(Cat.name) #Cat
console.log(Cat.says) #mewoo
console.log(Cat.move) #true
Cat.eat("fish") #eat the fish
console.log(Cat.milk) #yes
console.log(Cat.get_name()) #Cat
当我们对某个父对象增加某个属性或方法后,立即对所有派生对象生效。例如,在上例中增加
Animal.breath = "air"
则
console.log(Mammal.breath) #air
console.log(Cat.breath) #air
对”动物“增加了”呼吸“属性(值是”空气“),则”哺乳动物“和”猫“都具有了呼吸属性。
对象可以表示一个个复杂的拥有很多属性和行为的实体。对象也可以是其他对象的抽象,如“动物”之于“猫”,甚至“猫”这个对象也可以从”动物“这个对象中继承出来。采用
var Cat = object.create(Animal)这样的形式。这叫做原型链。
但是,原型继承虽然灵活,但有几个弊端:
(1)没有私有属性和方法。定义的一切属性和方法都是公有的,谁都可见;
比如上例中,加入如下部分:
Tiger =
food : Cat.name
change_catSays : ()->
Cat.says = "wangwang"
return Cat.says
console.log(Tiger.food) #Cat
console.log(Tiger.change_catSays()) #wangwang
console.log(Cat.says) #wangwang
可见,”老虎“对象可以引用”猫“对象的属性,也可以改变猫对象的属性(猫的叫声被老虎改成了”汪汪)
(2)引用的使用方式,如果派生对象中改了某个属性,原型对象的该属性也被改了。
如果写成了:
Animal =
name : "Animal"
Tiger = Animal #这里没有写Object.create,而是误写成了赋值(引用),这是很容易犯的错误。
Tiger.name = "Tiger"
console.log (Tiger.name) #Tiger
console.log (Animal.name) #Tiger (”动物“的名字也被改成了”老虎“)。
(3)基于原型链的继承过于自由,继承者可以对各种属性/方法随意修改,可能破坏同类对象的一致性。
这是最大的问题,比如我们定义的”猫“这种动物,很容易把它的move属性设为”false“,则猫就和其他动物不一样了。
为了避免这些问题,JS有很多模仿”类“的构造对象-继承对象方法,如工厂函数等。但这写方法都不完美。
正是由于原型继承存在的上述弊端,所以推荐使用基于类的继承。这也是大多数面向对象语言使用的模式。”类“是更高一级的抽象,它提取了众多对象的共性。
它的类型化检查更严格。类的模型中,类只是特征的集合,在实际使用时要实例化为对象,这称为类的”实例“。
用基于类的办法创建对象包括两个步骤
1.用一个类的声明定义对象的结构。
2.实例化该类为创建一个新对象。用这种方式创建的对象都有一套该类的所有实例属性的副本,每一个实例方法都只存在一份,但每一个对象都有一个指向它的链接。
3.使用类的接口
C++的经典类声明(《C++ Premier Plus》第343页)
class Stock //声明一个叫做”股票“的类
{
private: //定义各个私有属性和方法
std::string company;
long shares;
double share_val;
double total_val;
void set_tot()
{
total_val = shares * share_val;
}
public: //定义了一些公有方法
void acquire(const std::string & co, long n, double pr);
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show();
};
在CS中要模拟出”类“来,实现以下目标:
(1)封装:私有属性和方法实现隐藏,只暴露公有属性和方法;
(2)继承:方便地继承,便于代码复用。实现单一继承和多重继承,同时提供构造函数的功能。
(3)多态:重写和重载,静态绑定和动态绑定
(4)接口:接口的定义和使用。
在coffee中提供了类(class)的概念,并且完整地实现了面向对象编程的各种理念(OOP)。
类的声明与封装性
Class关键字,属性和方法,公有和私有,this关键字和传统的C++和Java中类的声明方法类似。如”动物“这个类的声明:
class Animal
name : "Animal"
move : true
get_name : () ->
return this.name
在CS中可以用@代替this.所以上例可以简写为
class Animal
name : "Animal"
move : true
get_name : ()-> @name
类中的属性(除了name外)都是默认私有的,外界不可访问。如
console.log(Animal.move)
console.log(Animal.get_name())
都会返回undefined,这就实现了数据的封装。这对于继承的子类同样如此。
保护的太好了,类中的属性都无法修改了
Animal.move = false 没有效果。
封装是指将现实世界中存在的某个客体的属性与行为绑定在一起,并放置在一个逻辑单元内。该逻辑单元负责将所描述的属性隐藏起来,外界对客体内部属性的所有访问只能通过提供的用户接口实现。这样做既可以实现对客体属性的保护作用,又可以提高软件系统的可维护性。只要用户接口不改变,任何封装体内部的改变都不会对软件系统的其他部分造成影响。
那么,如何修改(或增加)已经定义好的类的属性和方法呢?这需要使用::符号(这像是从C++借用的类的指示符)
在上述Animal类之外这样写:
Animal::move = "yes"
Animal::milk = true
Animal::give_birth = ()->
console.log("give birth to a new #{@name}")
这会在Animal类定义完之后,修改move属性的值,增加milk属性和give_birth方法。
类的实例化
使用new关键字:Tom = new Animal Tom就是一个具体的动物这时,Tom就有了Animal的各种属性和方法。还可以为它添加属性:Tom.says = "Mewoo"
我们可以直接访问Tom.name,Tom.move,Tom.get_name()。也就是说,只有类的实例才可以被访问。
类一旦发生了变化,这种变化立即作用于它的所有实例和子类。这叫做类的动态性。
类的继承
继承性是子类自动共享父类数据结构和方法的机制,这是类之间的一种关系。在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把这个已经存在的类所定义的内容作为自己的内容,并加入若干新的内容。继承性是面向对象程序设计语言不同于其它语言的最重要的特点,是其他语言所没有的。
在软件开发中,类的继承性使所建立的软件具有开放性、可扩充性,这是信息组织与分类的行之有效的方法,它简化了对象、类的创建工作量,增加了代码的可重用性。
采用继承性,提供了类的规范的等级结构。通过类的继承关系,使公共的特性能够共享,提高了软件的重用性。
使用extend关键字。class Cat extends Animal
瞬间,Cat就具有了Animal的各种属性和方法。
还可以加入新的特征:
class Cat extends Animal
says : "Mewoo"
eat: () ->
return "mouse"
我们说,Cat是Animal的子类,Animal是Cat的父类。
在类层次中,子类只继承一个父类的数据结构和方法,则称为单重继承。
在类层次中,子类继承了多个父类的数据结构和方法,则称为多重继承。
在目前的主流语言中,C++支持多重继承,JAVA、VB、NET、Objective-C均仅支持单继承,CoffeeScript只支持单继承。但可以通过Mix-in技术实现从多个类继承。
构造函数和supper
在上述类的模型中,一个类的实例会在创建时立即拥有类的全部属性和方法,但其中的方法只有在调用时才会运行。而有时候我们希望某些方法在实例化时立即运行(作为初始状态),这就需要用到构造函数(构造器)了。构造函数的功能主要用于在类的对象创建时定义初始化的状态。构造函数不能被直接调用,必须通过new运算符在创建对象时才会自动调用;而一般的方法是在程序执行到它的时候被调用的。这在带参数的类中特别有用,因为我们希望用参数来初始化类。例如,我们想创建一个动物时带一个参数表,里面写好这个动物叫什么名字,有几条腿,肉食还是素食。动物一创建,这些特征就有了。需要这样写:
class Animal
constructor:(@name, @leg, @food) ->
Tom = new Animal("Tom", 4, "meat")
console.log Tom.name #Tom
console.log Tom.leg #4
console.log Tom.food #meat
上例中,constructor是构造器的关键字, ->表示这是方法,@name表示新对象的名字就是参数name。
再看这个例子:
假设我定义一个小物件(gadget),包括品牌、名称和价格。品牌是固定的。
class Gadget
@brand = "Apple"
constructor: (@name, @price) ->
buy : =>
"Buy #{@name} of #{Gadget.brand} with #{@price}."
iphone = new Gadget("iphone", "4999")
console.log iphone.name
console.log iphone.buy()
因为brand要被内部的方法所调用,所以写成了@brand = "Apple"的形式。buy方法最好使用=>绑定
在子类中,可以使用super关键字来调用父类的方法。
class Animal
constructor: (@name) -> #构造器
move: (meters) ->
console.log @name + " moved #{meters}m."
class Snake extends Animal
move: ->
console.log "Slithering..."
super 5 #调用父类的move方法,参数是5
class Horse extends Animal
move: ->
console.log "Galloping..."
super 45 #调用父类的move方法,参数是45
sam = new Snake "Sammy the Python"
tom = new Horse "Tommy the Palomino"
sam.move()
tom.move()
上述代码将返回
"Slithering..."
"Sammy the Python moved 5 m."
"Galloping..."
"Tommy the Palomino moved 45m."
上下文绑定--胖箭头
在JavaScript中上下文变化很频繁,CoffeeScript可以通过胖箭头函数(=>)来让this值锁定到某个特定的上下文中。这样无论这个函数在什么上下文中被调用,都保证该函数总是在其创建时的上下文中执行。CoffeeScript把胖箭头语法扩展到类中,因此在实例方法上使用胖箭头你就能确保方法能在正确的上下文中执行——this总是等于当前的实例对象。这主要用在jQuery之类绑定到DOM的库中。但推荐所有类中的方法都使用胖箭头(除了constructor)。class Animal
price: 5
sell: =>
alert "Give me #{@price} shillings!"
animal = new Animal
$("#sell").click(animal.sell)
如上例所示,这在事件回调是尤其有用。正常情况下sell()函数会以#sell元素为上下文调用。然而,通过使用胖箭头来定义sell(),我们能保证能保持正确的上下文,所以this.price等于5。
多态
多态性是指相同的操作或函数、过程可作用于多种类型的对象上并获得不同的结果。不同的对象,收到同一消息可以产生不同的结果,这种现象称为多态性。多态性允许每个对象以适合自身的方式去响应共同的消息。多态性增强了软件的灵活性和重用性。
作为一种弱类型语言,javascript 提供了丰富的多态性,javascript 的多态性是其它强类型面向对象语言所不能比的。
多态性体现在重写和重载两个方面
重写(overide)的意思是,子类中可以定义与父类中同名,并且参数类型和个数也相同的方法。这些方法定义后,在子类的实例化对象中,父类中继承的这些同名方法将被隐藏而体现的是子类的方法实现。
重载(overload)的意思是,同一个名字的函数或方法可以有多个实现,他们依靠参数的类型和(或)参数的个数来区分识别。
重写的例子:
class Animal
eat:() ->
console.log "Aminal eat food."
class Cat extends Animal
eat: ()->
console.log "Cat eat mouse."
class Tiger extends Animal
eat: ()->
console.log "Tiger eat deer."
Tom = new Cat
Sam = new Tiger
Tom.eat() 输出"Cat eat mouse."
Sam.eat() 输出"Tiger eat deer."
同样的eat方法,由于被不同子类改写,产生了不同的行为。
javascript 中函数的参数是没有类型的,并且参数个数也是任意的,因此,要定义重载方法,就不能像强类型语言中那样做了。
下例中eat()方法的参数不同,但生效的只有最后一个
class Animal
constructor: (@name) ->
eat:() ->
console.log @name + " eat."
eat:(food) ->
console.log @name + " eat #{food}"
eat: (food, number) ->
console.log @name + " eat #{number} pieces #{food} "
class Cat extends Animal
Tom = new Cat "Tom"
Tom.eat() #Tom eat undefined pieces undefined
Tom.eat("fish") #Tom eat undefined pieces fish
Tom.eat("fish", "2") #Tom eat 2 pieces fish
但是你仍然可以实现重载。就是通过函数的 arguments 属性。javascript的函数默认都有一个arguments数组,表示参数数组。它有一个arguments.length属性,代表参数的个数。这样我们可以在类中定义一个方法,根据参数的个数有不同的行为。
class Animal
constructor: (@name) ->
eat: (food, number) ->
if arguments.length == 0
console.log @name + " eat."
else if arguments.length == 1
console.log @name + " eat #{food}"
else if arguments.length == 2
console.log @name + " eat #{number} pieces #{food} "
class Cat extends Animal
Tom = new Cat "Tom"
Tom.eat() #Tom eat
Tom.eat("fish") #Tom eat fish
Tom.eat("fish", "2") #Tom eat 2 pieces fish
接口
接口是一种抽象类,或者说是类的模型,它里面只有共性的属性和方法的原型,但没有方法的具体实现。此时可以对类进行各种操作。接口的引入降低了对象间的耦合性,我们的函数不需要知道对象是何种实例,只需要对象实现了某一套方法集合,就可以调用,是不是有点面向对象的感觉了?
在许多内置接口的语言中(如Java),通常有专用的接口关键字interface和专用的接口实现方法implements。 而javascript中,则没有专用的方式,只能自己定义抽象类作为接口,然后再定义子类来具体实现(重写)接口的方法。如下例:
class Student #define a abstract class "student" as interface
constructor: (@name) ->
ClassID : 0
learning: () =>
eat: () =>
sleep: () =>
class maleStudent extends Student #realize the interface
sex : "male"
learning : () =>
console.log "learning Mathmatics"
eat: () =>
console.log "eat meat"
sleep: () =>
console.log "huuuuuu~"
class femaleStudent extends Student
sex : "male"
learning : () =>
console.log "learning Art"
eat: () =>
console.log "eat rice"
sleep: () =>
console.log "herrrrr~"
Jack = new maleStudent "Jack"
Angela = new femaleStudent "Angela"
register = (student, ID) -> #define a function using interface class"student"
student.ClassID = ID
console.log "#{student.name} is in class #{ID}:"
student.learning()
student.eat()
student.sleep()
register(Jack, 1001)
register(Angela, 1002)
算法篇
基本符号
数学计算符号:+ - * / %(求余)关系运算符号: is(==) 相等 isnt 不相等(!=) > < >= <=
逻辑符号:and(&&)与 or(||)或 not(!)非
分割符号:()(运算分隔、函数参数) []数组 {}(对象) ''""(引号) ,(元素分隔) .(提取属性)
特殊符号:
..(数组中的省略号),包含头尾两端。如[3..6]意味着[3,4,5,6]
...(数组中的省略号),包含头端,不包含尾端。如[3...6]意味着[3,4,5]
@property this.property
? 存在(不等于undefined/null)
如footprints = animal ? "bear" 如果animal存在(被赋值过),则footprint等于animal的值,否则等于bear。
还可以在连续的属性取值链条中确保某个值存在才导致最终期望的结果,如zip = lottery.drawWinner?().address?.zipcode
基本表达式(基本的过程):
由基本数据和基本符号组成;任何表达式都有值;数字组成的表达式,其值就是计算结果。字符串组成的表达式,其值还是字符串(或布尔值)。
如:10 % 3 值是 1,
10 / 3 值是 3.333333333333333335 内部有理数的表示
3 + 2 * 4 值是 11, 满足运算的优先级
(3 + 2) * 4 值是 20 括号导致的优先计算
2 isnt 5 值是 true 逻辑运算
2 >= 5 值是 false 关系运算
“coffee" 值是‘coffee' 还是字符串的字面量
”coffee" is "coffee" 值是 true, 字符串的关系运算
“coffee" + "script" 值是'coffeescript", 字符串的连接
”coffee" > "coff" 值是true, 字符串的关系运算:长度的比较
可以用is(==)判断两个值或表达式是否相等。
两个表达式的相等有4重含义:
(1)类型相同;
(2)值相同;
(3)类型和值都相同;
(4)引用(reference)相同;
两个内容完全一样的数组是不相同(identical)的,因为引用不同。[1,2,3] isnt [1,2,3]
变量的赋值
赋值是一种特殊的表达式,它可以把一个名字和一个值联系在一起。如pi = 3.14, 以后引用pi,就等于引用3.14。
不用写var了,可以直接赋值,也不用担心污染全局变量。
可以连续赋值 gold = silver = rest = "unknown"。
对变量的赋值可能破坏“引用透明性”(这是函数式编程的一个显著优点),所以尽量不要使用赋值语句。使用也仅限于常量的赋值。
在输出语句中指代变量,用#{},如print inspect "My name is #{name}"
多行字符串的赋值是允许的:
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..."
用三个”括起来的段落,可以换行
html = """
<strong>
cup of coffeescript
</strong>
"""
输出也是换行的,虽然没有\n。
块状注释使用###括起来:
###
SkinnyMochaHalfCaffScript Compiler v1.0
Released under the MIT License
###
块状正则表达式(用///)更加清晰:
OPERATOR = /// ^ (
?: [-=]> # function
| [-+*/%<>&|^!?=]= # compound assign / compare
| >>>=? # zero-fill right shift
| ([-+:])\1 # doubles
| ([&|<>])\2=? # logic / shift
| \?\. # soak access
| \.{2,3} # range or splat
) ///
链式比较
cholesterol = 127
healthy = 200 > cholesterol > 60 //true
try/catch的使用
alert(try
nonexistent / undefined
catch error
"And the error is ... #{error}"
)
try
allHellBreaksLoose()
catsAndDogsLivingTogether()
catch error
print error
finally
cleanUp()
控制流
顺序结构
一条语句接一条语句执行;分支结构
if语句根据表达式的值改变流程的顺序。表达式的值为真时执行其后的代码块,否则执行可选的分支。下列值被当作“假”:false, null, undefined, “”(空字符串), 0, NaN
其他所有值都被但做真,包括字符串“false”,以及所有的对象。
if-else语句一律使用缩进而不是{}来表示范围。
if opposite
number == -42
也可以把条件放在后面
number = -42 if opposite
经典的表达法
if a == b
c()
else
d()
更加口语化的单行写法 if 1 > 0 then "Ok" else "Y2K!"
等同于更简单的三元表示法
(1 > 0) ? "Ok" : "Y2K!"
多重条件使用 and
if happy and knowsIt
clapsHands()
chaChaCha()
else
showIt()
多种可能使用else if
grade = (student) ->
if student.excellentWork
"A+"
else if student.okayStuff
if student.triedHard then "B" else "B-"
else
"C"
三元判断(date = friday ? sue : jill;)写成if-then
date = if friday then sue else jill
switch语句
CS中使用switch-when-else语句,避免了javascript中总要break的麻烦
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
else
go dancing
when "Sun" then go church
else
go work
还可以写成简约的形式:
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'
循环结构
计算机最强大的地方就在于可以飞快地循环做一件事。而循环(loops)也是程序设计中最常用的结构之一。下面的求圆周率的循环(CS写法)在3秒中内进行了一亿次运算!求出了圆周率小数点后7位正确值(利用了SICP第38页的近似算法)。
pi = (n) ->
a = 1
sum = 0
for i in [0..n]
sum += 1 / (a * (a+2))
a += 4
sum * 8
result = pi(100000000)
console.log(result)
JS经典的循环是两种:
(1)While循环
while (expression) {
do something();
};
当表达式为真时执行循环,当表达式为假时退出循环。
CS仍然支持这种循环,如
i = 0
while i<10
console.log(i)
i += 1
(2)for循环
for (initialization, condition, increment){
do something();
};
由三个可选从句控制:首先初始化从句(initialization),它的作用通常是初始化循环变量。接着,计算条件从句(condition)的值,通常它检测循环变量。如果值为真,执行代码块。并且按增量从句(increment)改变循环变量。接着会重复计算条件从句。
如:
for (i=0, i<10, i+=1)
console.log(i)
但是,CS摒弃了这种写法,改用更简练的for-in语句。上述例子应写为:
for i in [0..10]
console.log(i)
也可以把for-in条件放在后面,更符合自然语言顺序。
eat food for food in ['toast', 'cheese', 'wine']
foods = ['broccoli', 'spinach', 'chocolate']
eat food for food in foods when food isnt 'chocolate'
可以用when给条件过滤
prisoners = ["Roger", "Roderick", "Brian"]
release prisoner for prisoner in prisoners when prisoner[0] is "R"
循环可以给变量循环赋值,其结果将形成一个数组。
shortNames = (name for name in list when name.length < 5)
countdown = (num for num in [10..1])
还可以加上固定步长
evens = (x for x in [0..10] by 2) //步长为2,evens的值是[0,2,4,6,8,10]
用for-of语句可以遍历对象的名-值对
person =
"name": "Seaborn"
"age": 17
"sex": "male"
for key, value of person
console.log("#{key} is #{value}")
用do 来实现每次循环做一件事
for filename in list
do (filename) ->
fs.readFile filename, (err, contents) ->
compile filename, contents.toString()
不太常用的while-until语句
if this.studyingEconomics
buy() while supply > demand
sell() until supply < demand
命名与环境:
可以给任何对象起一个名字;可以用=给对象赋值,如Pi = 3.14159265
环境:保持名称-值关联关系的存储空间。命名只在某个环境中有效。离开这个环境将被释放掉。
大多数类C语言都有块级作用域,在一个代码块中(括在一对花括号里的一组语句)定义的所有变量在代码块外是不可见的。定义在代码块中的变量在代码块执行结束后会被释放掉。
JS没有块级作用域,只有函数作用域。定义在函数中的变量和参数在函数外是不可见的。而在一个函数内部任何位置定义的变量,在该函数内部任何地方都可见。
JS的一个弊病就是容易污染全局变量。所谓全局变量(global variable)就是在某个代码文件中任何地方都可以引用的变量,其作用域是整个源程序。JS只要没有在声明变量时写var,就会自动把这个变量作为全局变量。全局变量在任何地方都可以随时引用,用起来非常方便。但正因为它可以被程序的任何部分在任何时候修改,使得程序的行为变得极度复杂,不利于模块化开发。同时这容易导致名字冲突,削弱了程序的灵活性。另外,全局变量的生命周期很长,占用内存迟迟不释放。
在现代的面向对象语言如Java,C++,C#,Ruby中,由于变量都是封装在类里面的,对别的类不可见,所以已经几乎完全抛弃了全局变量的概念。然而,可以通过把一个类定义为public static,把类成员变量也定义为public static,使该变量在内存中占用固定、唯一的一块空间,来实现全局变量的功能。
CS为避免这个问题,干脆摒弃了全局变量。你可以写一个变量的声明
myVariable = "test"
CS会自动在它前面加上var前缀,并且用一个匿名函数包起来。CS又更进一步,使你很难覆盖上一级变量。
但有时候你的确需要类似全局变量的东西,你可以把它作为window的属性声明,或使用如下export方法:
exports = this
exports.MyVariable = "foo-bar"
一个好的习惯是:只在函数中使用变量。
函数
函数的写法
在JS中你必须这样写函数:var add = function (a, b) {
return a + b;
};
在CS中,函数的写法十分简洁。你不用写function语句,只要用一个->符号。也不用{},函数体用缩进的多条语句组成。最后一条语句的值就是函数的返回值。你不用显式地写return语句,除非你要提前返回某个值。当然,用return也可以,显得返回值清楚一些。
square = (x) -> x * x //square是x的函数,内容是x*x。
一个函数里面可以调用另一个(已定义过的)函数。
square = (x) -> x * x
cube = (x) -> square(x) * x
调用函数
square(x)
或者写成apply(square(x)),call(square(x)
数组和函数都属于引用类型,即使内容相同,也不相等。
函数的应用:把函数应用到值上,产生值。When apply a function, it evaluates to a value.
函数符号-> 就是从输入指向输出的箭头。
undefined表示“无值”。undefined = undefined。
在程序中,经常要判断一个值是否存在,就是看某值 isnt undefined是否为真。isnt undefined 可以简写为?
没有函数体(body)的函数都会产生undefined。
函数体的值是最后一个表达式的值The value of the body is the value of the final expression.
-> 一个函数 //function
(->)()运行一个参数为空的函数 //undefined
->5 一个函数,函数体是表达式5 //function
(->5)() 运行一个函数体是5,参数为空的函数 //5
->-> 函数的函数,还是函数 //function
(->->)()函数的函数,求值一次,还是函数 //function
(->->)()()函数的函数(参数为空),求值二次,就是undefined //undefined
函数的参数
函数的参数可以预先定义times = (a = 1, b = 2) -> a * b
函数的参数可以有默认值
fill = (container, liquid = "coffee") ->
"Filling the #{container} with #{liquid}..." //liquid参数如果没有给出,默认就是coffee。
函数的参数可以有不定多个,用...代替
awardMedals = (first, second, others...) ->
gold = first
silver = second
rest = others
函数的参数可以为空,这代表执行一个自给自足的过程。
myFunction = () ->
a = 3 + 5
b = a + 1
将返回9
当一个值-任何值-被当作参数传递给函数的时候,这个绑定到函数环境中的值必须和原值完全相同。
When a value–any value–is passed as an argument to a function, the value bound in the function’s environment must be identical to the original.
在调用函数时,参数会绑定到对应的变量(名字)上。对于数字/字符串/布尔值,函数绑定的是它们的副本(copy),因为副本可以和原本完全相同。但对于引用类型,如数组、函数,副本和原本无法相同,所以函数绑定的是它们的引用(reference)。在需要时再调用原本。这叫call by sharing.
函数体一般不能为空,为空则函数没有定义,将返回undefined。
结果的返回
函数结果的返回不用显式地写return了grade = (student) ->
if student.excellentWork
"A+" //return "A+"
else if student.okayStuff
if student.triedHard then "B" else "B-" //return"B", else return "B-"
else
"C" //return "C"
*如果要想尽早从函数中返回某个值,还是需要显式地写return。
一个函数只能有一个返回值。如果要返回多个值,可以1.在不需要后续引用的情况下,用字符串嵌套#{返回值}的形式;2.如果需要后续引用,则返回一个数组(带有多个值)。
用三引号“”“返回带换行的文本。如
return """
This is first line
This is second line
This is third line
"""
将返回
This is first line
This is second line
This is third line
=>
通过胖箭头函数(=>)来让this值锁定到某个特定的上下文中。这样无论这个函数在什么上下文中被调用,都保证该函数总是在其创建时的上下文中执行。
这主要用在callback和eventLisener中。
this.clickHandler = -> alert "clicked"
element.addEventListener "click", (e) => this.clickHandler(e)
你之所以要这样做的原因是,来自addEventListener的回调函数会以element为上下文被调用,也就是说,this就相当于这个元素。如果你想让this等于当前上下文,除了使用self=this,胖箭头也是一种方式。
关于函数式编程的实现
1.只用"表达式",不用"语句"
"表达式"(expression)是一个单纯的运算过程,总是有返回值;"语句"(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。值都是表达式(value is expression)。以下都是表达式(当然也是值):
42 值是42
a = 1 值是1
"coffee"+"script" 值是"coffeescript"
2 is 2 值是true
(2 + 2 is 4) is (2 isnt 5) 值是true
[1, 2, 3] 值是[1, 2, 3]
square = (x) -> x * x 值是x*x
可以用is判断两个值或表达式是否相等。
两个表达式的相等有4重含义:
(1)类型相同;
(2)值相同;
(3)类型和值都相同;
(4)引用(reference)相同;
两个内容完全一样的数组是不相同(identical)的,因为引用不同。[1,2,3] isnt [1,2,3]
什么叫语句?就是执行系统IO的指令。对于我的环境,就是node的命令,如console.log("Hello")。所以系统交互都交给node语句。
2.函数即数据。所有函数均有返回值
函数体的值是最后一个表达式的值The value of the body is the value of the final expression.下面的函数将返回3
myFunction = (x) ->
a = 1
a = 2
a = 3
return语句可以使函数提前返回。当return执行时,函数立即返回而不再执行余下的部分。下面的函数的将返回2(不论x是什么:
myFunction = (x) ->
a = 1
b = a + 2
return a = 2
x + b
这可以用来在测试函数时“打断点”。
3.高阶函数:
3.1 函数作为函数的表达式(函数嵌套)。
被嵌套的函数可以是已经定义过的过程(这时可以直接调用),也可以在父函数中当场定义。看下面的例子:
square = (x) ->
x * x
cube = (x) ->
x * square(x)
result = cube(2)
console.log(result)
square是x的平方函数,已定义过。我们在写立方函数cube时就不用重新实现平方的算法,只要把square拿来在函数体内调用它就可以了。在赋值时square可以不用赋值,因为它的参数就是cube的参数。注意:在写调用时,在参数表中要把所有的参数-高阶函数和低阶函数的都写进去。
3.2 函数作为函数的参数
一个函数的值是另一个函数的参数。这可以将一个过程的结果作为参数参与另一个过程。plus = (x) ->
x + 1
test1 = (x, y,plus) ->
y * plus(x)
result = test1(3,5,plus)
console.log(result)
注意:作为参数的函数和主函数的参数都要写在调用主函数的参数表中。
下面给出一个计算圆面积的函数,其中半径r、圆周率pi是它的两个参数,而pi是另一个函数pi计算出来的。
pi = (n) ->
a = 1
sum = 0
for i in [0..n]
sum += 1 / (a * (a+2))
a += 4
sum * 8
square = (x) ->
x * x
area = (r,pi,n) ->
pi(n) * square(r)
result = area(2,10000,pi)
console.log(result)
我们看到,为了让pi的精度参数n能被引用到,在主函数area中也要列入n。
其实上面的area函数可以不把pi作为参数,而直接在过程中调用:
area = (r,n) ->
pi(n) * square(r)
其结果是一样的。那么函数作为函数的参数(除了便于理解外)还有什么意义呢?
其实这种方式主要用在回调函数中。
在计算机程序设计中,回调函数,或简称回调(Callback),是指通过函数参数传递到其它代码的,某一块可执行代码的引用。这一设计允许了底层代码调用在高层定义的子程序。回调的用途十分广泛。一个主要用途是在信号处理中作为异步调用的工具。回调的形式因程序设计语言的不同而不同。
C, C++ and Pascal允许将函数指针作为参数传递给其它函数。其它语言,例如JavaScript,Python,Perl[1][2]和PHP,允许简单的将函数名作为参数传递。
Javascript中的回调函数,相信大家都不陌生,最明显的例子是做Ajax请求时,提供的回调函数, 还有node的回调机制。
实际上DOM节点的事件处理方法(onclick,ondblclick等)也是回调函数。
以下是JS写的node中读取文件的代码:
//readfile.js
var fs = require('fs');
fs.readFile('file.txt', 'utf-8', function(err, data) {
if (err) {
console.error(err);
} else {
console.log(data);
}
});
console.log('end.');
其中function(err, data)就是用于处理错误信息的回调函数,当读取出错时,会打印标准错误提示;如果正常打开,则显示文件内容。
结果是先显示了end.再显示文件的内容。这是因为I/O慢,所以先显示了后面的"end.",当文件打开时再显示了文件的内容。
那么在CS中这段怎么写呢?
回调函数使得对不连续事件的处理变得更容易。在WEB世界,假定有这么一个序列,由用户交互行为触发,向服务器发送请求,最终显示服务器的响应。最自然的写法可能会是这样:
request = prepare_the_request();
response = send_request_synchronously(request);
display(response);
这种方式的问题在于,网络上的同步请求会导致客户端进入假死状态。如果网络传输或服务器很慢,响应会慢到令人不可接受。
更好的方式是发起异步请求,提供一个当服务器的响应到达时随即触发的回调函数。异步函数立即返回,这样客户端不会被阻塞。
request = prepare_the_request;
send_response_asynchronously(request,function(response) {
display(response);
)};
传递一个函数作为参数给send_request_asynchronously函数,一旦接受到响应,它就执行response。
回调函数和异步编程方式与传统的线性编程方式在思维上不太一致,要适应一阵。
3.3函数作为函数返回的结果
在一个函数里返回另一个(或几个)函数。本来函数作为表达式在另一个函数中最后出现(或被显式return)并没有特殊的意义,返回的无非是这个函数的引用。f1 = (x) ->
x +=1
f2 = (x) ->
x *2
result = f1(1)
console.log(result) #返回的是[function]而不是结果4!
r = result(1)
console.log(r) #返回结果2,因为result是个函数。
这可以用于,先用一个函数构造出另一个函数,再调用结果函数。
同时,这种做法在闭包和为对象添加方法等方面得到了广泛使用。闭包中就是返回了一个引用了父级自由变量的函数。而对象把方法函数返回,以后就可以调用这些方法且保护了对象的私有变量。如这个序列号发生器:
var serial_maker = function () {
var prefix = '';
var seq = 0;
return {
set_prefix: function (p) {
prefix = String(p);
},
set_seq: function(s) {
seq = s;
},
getsym: function () {
var result = prefix + seq;
seq += 1;
return result;
}
};
};
var seqer = serial_maker();
seqer.set_prefix('A');
seqer.set_seq(1000);
var unique = seqer.getsym();
console.log(unique);
unique = seqer.getsym();
console.log(unique);
unique = seqer.getsym();
console.log(unique);
对于给定的前缀A和初始值1000,将顺序产生A1000,A1001,A1002.
这在JS中是有用的,但在CS中由于有了类(CLASS),所以这点意义不大。
4. 引用透明性
纯函数:唯一的输入是它的参数,唯一的输出是它的返回值。不依赖外部变量或类成员/数据库。。纯函数具有引用透明性,对于相同的输入,总是给出相同的输出。纯函数输出不依赖系统状态,易于调试和模块化。
对于纯粹的过程,要尽量写成纯函数(虽然参数表可能长一些)。对于与外部(或其他部分)交互的部分,不适合应用纯函数。
传统编程使用变量存储程序的状态(内存中变量的值),而函数式编程使用lamda演算,改变字符串变换,通过参数来保持状态。变量仅仅是名称,而不是内存中的值。
4.1.不修改变量,用参数保持状态
“折半查找”的例子对一个已知有序数组给出一个数字,判断是否在这个数组中。如果在,则返回其索引,如果不在则返回提示。
首先要选取中点(最大/最小数的平均值)和提问数对比,如果提问数大,则在中点的右侧递归查找;如果提问数小,则在左侧递归查找。直到相等,则给出索引。或者直到某侧的数只剩一个且不相等,则给出提示。
CS版本:
在折半查找的代码中,start,end参数保持更新的起点、终点。
binarySearch = (list, find) ->
search =(start, end, list, find)->
if start <= end
index = Math.floor((start+end)/2);
if list[index] == find
return index;
else if find > list[index]
return search(index+1,end,list,find);
else if find <= list[index]
return search(start,index-1,list,find);
else
return -1;
else
return -1;
return search(0,list.length-1,list,find);
5. 闭包
闭包是指在函数中定义另一个函数时,如果内部的函数引用了外部的函数的变量,则产生闭包。运行时,一旦外部的 函数被执行,一个闭包就形成了,闭包中包含了内部函数的代码,以及所需外部函数中的变量的引用。其中所引用的变量称作内部函数的自由变量(free viariable),因为它没有在内部函数中被绑定。不带自由变量的函数叫纯函数;带有自由变量的函数叫闭包。纯函数总是意味着同样的事,所以具有引用透明性,闭包则不然,因为其自由变量是不确定的。
信息隐藏:闭包是由函数和与其相关的引用环境组合而成的实体。很适合作为一个模块整体使用。变量的作用域仅限于包含它们的函数,因此无法从其它程序代码部分进行访问。但可以返回这个内部函数,实现变量内容外部可见。
记忆性:在常见的语言中,变量的生命周期只限于创建它的环境。一旦离开这个环境,它的内容就会被“垃圾回收”机制清理。但有时,我们需要“记忆”某种状态。在有闭包的语言中,只要有一个闭包引用了这个变量,当外部函数调用完毕后,这些变量在内存不会被释放,因为闭包需要它们,它就会一直存在。这个变量被“记忆”了。
因为闭包只有在被调用时才执行操作,所以它可以被用来定义控制结构。
如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包(closure)。
闭包用起来就像是个函数样板,其中保留了一些可以在稍後再填入的空格.闭包的最典型的应用是实现回调函数(callback)。
闭包的应用场景
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
以上两点是闭包最基本的应用场景,很多经典案例都源于此。
闭包最经典的例子是累加器的实现:调用一个函数,初始值每次都增加一个固定值。比如每次在初始值5上加3,5,8,11,14,17...
如果我们简单地写成:
increment = (n,i) ->
n += i
result = increment(5,3)
console.log(result)
则每次程序运行都会显示结果是8。很显然,每次函数increment运行完,n和i的值都被“垃圾回收”了。怎样把每次的结果记忆下来,在下次调用函数时累加进去呢?
JS版本:
function counter(n,i){
return function increment(){
n += i;
console.log(n);
};
};
var result = counter(5,3);
result();
result();
result();
result(); //输出8,11,14,17
CS版本如下:
counter = (n,i) ->
increment = () ->
n += i
console.log(n)
result = counter(5,3)
result()
result()
result()
result() #输出8,11,14,17
要点:在一个函数内部嵌套一个累加函数,实现累加和输出的功能,返回这个累加函数。在函数体外给外部函数赋值并调用。
6.柯里化
闭包和柯里化都是JavaScript经常用到而且比较高级的技巧,所有的函数式编程语言都支持这两个概念,因此,我们想要充分发挥出JavaScript中的函数式编程特征,就需要深入的了解这两个概念,闭包事实上更是柯里化所不可缺少的基础。柯里化( currying ); 又称部分求值,是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数并且返回结果的新函数的技术。柯里化就是预先将某些参数传入,得到一个简单的函数。但是预先传入的参数被保存在闭包中,因此会有一些奇特的特性。这也充分证明“函数返回函数”是有用的。通俗点讲,currying有点类似买房子时分期付款的方式,先给一部分首付( 一部分参数 ), 返回一个存折( 返回一个函数 ),合适的时候再给余下的参数并且求值计算。这对于一开始所有参数无法确定的过程是有意义的。比如,一个函数一开始只有一部分参数可以确定,就先写只用这部分参数的函数,然后返回一个函数,需要使用另外的参数,当其他参数确定后,再调用这个函数计算结果。
在下面这个小例子中,adder是一个加法器函数,它有一个加数参数,返回一个函数等待另一个加数参数。你可以把参数一确定后命名为一个新函数(inc),然后再用新函数去计算不同的参数二。
var adder = function(num) {
return function(y) {
return num + y;
}
}
var inc = adder(1);
var result = inc(99);
7.递归
递归算法是一种直接或间接调用自身算法的过程。递归算法对解决一大类问题(可以分解为类似小问题的问题)是十分有效的,它它使算法的描述简洁且利于理解。递归是函数式编程的一个重要的概念,循环可以没有,但是递归对于函数式编程却是不可或缺的。递归充分地发挥了函数的威力,也解决了函数式编程无状态的问题。
递归其实就是将大问题无限地分解,直到问题足够小。而递归与循环在编程模型和思维模型上最大的区别则在于:循环是在描述我们该如何地去解决问题。递归是在描述这个问题的定义。
使用递归,必须有一个明确的递归结束条件。但递归运行效率较低,由于系统为每一层返回点、局部量等开辟栈来存储,递归次数过多容易造成栈溢出,所以要小心使用。
7.1递归四大例子:阶乘,Fibonacci数列、快速排序和汉诺塔问题
阶乘定义:n!=n*(n-1)*(n-2)*...*1
递归的写法很简单,利用了n!=n*(n-1)!的变通定义:
f = (n) ->
if n is 1
1
else
n * f(n-1)
result = f(10)
console.log(result) #结果是3628800
如果用循环来写,费了大劲了,需要有变量n作为递减因子,变量k作为累计结果的保存,还需要判断n是否减少到1。就是
f = (n) ->
if n is 1
1
else
k = n
while n isnt 1
k *= (n-1)
n -= 1
return k
result = f(10)
console.log(result) #结果是3628800
Fibonacci数列
一个Fibonacci数字是之前两个Fibonacci数字之和。最前面的两个数字是0和1。如:0,1,1,2,3,5,8,13,...
定义:i[n] = i[n-1]+i[n-2]
递归的写法:
fibonacci = (n) ->
if n == 0
return 0
else if n == 1
return 1
else if n > 1
return fibonacci(n-1) + fibonacci(n-2)
#数组式输出
list = [0]
n = 10
for i in [0..n]
list1 = [fibonacci(i)]
list = list.concat(list1)
console.log(list)
排序
首先,我们看用双层循环写的JS的冒泡排序算法:
function bubbleSort(arr){
var temp;//先定义缓存
for(var i=0;i<arr.length-1;i++){//一共比较n-1趟
for(var j=0;j<arr.length-i-1;j++){//对当前无序区arr[i..n]自左向右扫描
if(arr[j]>arr[j+1]){//两两交换
temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
}
return arr;
}
array1 = [1,7,24,5,19,72,4]
r = bubbleSort(array1)
console.log(r);
快速排序(Quicksort)是对冒泡排序的一种改进。由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用中间的数)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。
JS版本:
var quickSort = function(arr) {
if (arr.length <= 1) { return arr; }
var pivotIndex = Math.floor(arr.length / 2);
var pivot = arr.splice(pivotIndex, 1)[0];
var left = [];
var right = [];
for (var i = 0; i < arr.length; i++){
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return quickSort(left).concat([pivot], quickSort(right));
};
array1 = [84,63,71,9,18,16,25,77,43]
result = quickSort(array1)
console.log(result)
首先,定义一个quickSort函数,它的参数是一个数组。
var quickSort = function(arr) {
};
然后,检查数组的元素个数,如果小于等于1,就返回。
var quickSort = function(arr) {
if (arr.length <= 1) { return arr; }
};
接着,选择"基准"(pivot),并将其与原数组分离,再定义两个空数组,用来存放一左一右的两个子集。
var pivot = arr.splice(pivotIndex, 1)[0];
var left = [];
var right = [];
然后,开始遍历数组,小于"基准"的元素放入左边的子集,大于基准的元素放入右边的子集。使用PUSH方法。
for (var i = 0; i < arr.length; i++){
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
最后,使用递归不断重复这个过程,就可以得到排序后的数组(把左、中、右连接起来)。
return quickSort(left).concat([pivot], quickSort(right));
};
使用的时候,直接调用quickSort()就行了。
用CS写的快速排序函数
quickSort = (arr) ->
if arr.length <= 1
return arr
pivotIndex = Math.floor(arr.length / 2)
pivot = arr.splice(pivotIndex, 1)[0]
left = []
right = []
for i in [0..arr.length-1]
if arr[i] < pivot
left.push(arr[i])
else
right.push(arr[i])
return quickSort(left).concat([pivot], quickSort(right))
array1 = [84,63,71,9,18,16,25,77,43]
result = quickSort(array1)
console.log(result)
但这个算法有个问题,就是不能连续使用。在node中每次重复调用quickSort,则pivot值都会从array1的序列中被挖掉,则后面的quickSort是在缺值的数组上排序,结果就不对了。所以,要重复使用,必须在上一次的排序结果上重新调用quickSort。
汉诺塔问题
这是递归的经典问题。塔上有序根柱子和一套直径不一的空心圆盘。开始时,所有源柱子上的圆盘都按照从小到大的顺序堆叠。目标是通过每次移动一个圆盘到另一个柱子,最终把源柱子上的圆盘都挪到目标柱子上。过程中不允许大盘放在小盘上。
基本解法是:用一个辅助柱子,对于一对圆盘(disc=2),先移动最小的圆盘到辅助柱子上,从而露出下面最大的圆盘,然后移动下面的大圆盘到目标柱子上,再把小圆盘从辅助柱子移动到目标柱子上。对于大于2个的圆盘,递归调用这一过程。每次都是先移动两个到某个作为辅助的柱子上,把第三个暴露出来,把它挪到目标柱子上,再解决把先前的个挪到目标柱子的问题。(源柱子、辅助柱子、目标柱子的角色会不断变换)
CS版本:
hanoi = (disc, src, aux, dst) ->
if disc > 0
hanoi(disc-1, src, dst, aux) #挪动上面的圆盘到辅助柱上
console.log("Move disc", disc, "from", src, "to", dst) #挪动下面的圆盘到目标柱上
hanoi(disc-1, aux, src, dst) #挪动辅助柱上的圆盘到目标柱上
hanoi(3, "Src", "Aux", "Dst")
这个算法非常巧妙地变换了参数的位置,实现了不同的“挪法”。
7.2 递归与树形结构
树的定义树(tree)是包含n(n>0)个结点的有穷集合,其中:
(1)每个元素称为结点(node);
(2)有一个特定的结点被称为根结点或树根(root)。
(3)除根结点之外的其余数据元素被分为m(m≥0)个互不相交的结合T1,T2,……Tm-1,其中每一个集合Ti(1<=i<=m)本身也是一棵树,被称作原树的子树(subtree)。
树也可以这样定义:树是有根结点和若干颗子树构成的。树是由一个集合以及在该集合上定义的一种关系构成的。集合中的元素称为树的结点,所定义的关系称为父子关系。父子关系在树的结点之间建立了一个层次结构。在这种层次结构中有一个结点具有特殊的地位,这个结点称为该树的根结点,或称为树根。
我们可以形式地给出树的递归定义如下: 单个结点是一棵树,树根就是该结点本身。 设T1,T2,..,Tk是树,它们的根结点分别为n1,n2,..,nk。用一个新结点n作为n1,n2,..,nk的父亲,则得到一棵新树,结点n就是新树的根。我们称n1,n2,..,nk为一组兄弟结点,它们都是结点n的子结点。我们还称n1,n2,..,nk为结点n的子树。
空集合也是树,称为空树。空树中没有结点
树本身就是递归定义的,这就导致了很多树的算法自然而然用到递归,并且用非递归的话实现起来很不方便.比如SICP第25页的Fibonacci数列例子。
递归算法可以非常高效地处理树形结果,比如浏览器端的文档对象模型(DOM)。每此递归调用时处理指定的树的一小段。
7.3.对于递归的优化--尾递归、惰性计算、记忆技术
递归算法虽然自然和强大,但由于机器对于每一层次的递归都要建立堆栈,非常耗费内存,也存在大量重复计算。见SICP第21页阶乘计算的图示。所以在解决复杂递归时,需要对递归算法进行优化。对递归的优化包括以下技术:尾递归、惰性计算、记忆。
尾递归
如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活跃记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。因此,只要有可能我们就需要将递归函数写成尾递归的形式。
例子:阶乘函数的尾递归写法
fib = (n) ->
if n <= 0
return 0
else if n == 1
return 1
else
TailRescuvie = (n,a) ->
if n == 1
return a
else
TailRescuvie(n-1, n*a)
return TailRescuvie(n,1)
result = fib(5)
console.log(result)
最后执行了一个递归函数,其中引入一个变量a保存每次计算的结果。
对于普通递归, 他的递归过程如下:
Rescuvie(5)
{5 * Rescuvie(4)}
{5 * {4 * Rescuvie(3)}}
{5 * {4 * {3 * Rescuvie(2)}}}
{5 * {4 * {3 * {2 * Rescuvie(1)}}}}
{5 * {4 * {3 * {2 * 1}}}}
{5 * {4 * {3 * 2}}}
{5 * {4 * 6}}
{5 * 24}
120
对于尾递归, 他的递归过程如下:
TailRescuvie(5)
TailRescuvie(5, 1)
TailRescuvie(4, 5)
TailRescuvie(3, 20)
TailRescuvie(2, 60)
TailRescuvie(1, 120)
120
很容易看出, 普通的线性递归比尾递归更加消耗资源, 在实现上说, 每次重复的过程
调用都使得调用链条不断加长. 系统不得不使用栈进行数据保存和恢复.而尾递归就
不存在这样的问题, 因为他的状态完全由n和a保存。一些语言(如scheme)提供了尾递归优化,如果函数返回自身递归调用的结果,调用的过程会被自动替换为一个循环,它可以显著提高速度。但JS没有提供尾递归优化,所以深度递归的函数可能会因为堆栈溢出而运行失败。所以要手动写成尾递归的形式。
惰性求值(Lazy Evaluation),又称懒惰求值、懒汉求值,是一个计算机编程中的一个概念,它的目的是要最小化计算机要做的工作。它有两个相关而又有区别的含意,可以表示为“延迟求值”和“最小化求值”,本条目专注前者,后者请参见最小化计算条目。除可以得到性能的提升外,惰性计算的最重要的好处是它可以构造一个无限的数据类型。
惰性求值的相反是及早求值,这是一个大多数编程语言所拥有的普通计算方式。
延迟求值特别用于函数式编程语言中。在使用延迟求值的时候,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用的时候求值,也就是说,语句如 x:=expression; (把一个表达式的结果赋值给一个变量)明显的调用这个表达式被计算并把结果放置到 x 中,但是先不管实际在 x 中的是什么,直到通过后面的表达式中到 x 的引用而有了对它的值的需求的时候,而后面表达式自身的求值也可以被延迟,最终为了生成让外界看到的某个符号而计算这个快速增长的依赖树。
scheme中有delay语句用于惰性求值。
记忆
函数可以将先前操作的结果记录在某个对象里,从而避免无谓的重复计算。这种优化称为记忆(memoization)。使用对象和数组来实现记忆是很方便的。
我们可以编写一个通用的带记忆功能的函数----memoizer函数取得一个初始的memo数组和formula函数。它返回一个管理memo存储和在需要时调用formula函数的recur递归函数。我们把这个recur函数和它的参数传递给formula函数。
下面的例子展示了如何使用memoizer计算fibonacci数列和阶乘:
momoizer = (memo, formula) ->
recur = (n) ->
result = memo[n]
if typeof result != 'number'
result = formula(recur, n)
memo[n] = result
return result
return recur
fib = (recur, n) ->
return recur(n-1) + recur(n-2)
f = (recur, n) ->
return n * recur(n-1)
fibonacci = momoizer([0,1], fib)
factorial = momoizer([1,1], f)
console.log fibonacci(10) #55
console.log factorial(5) #120