写在前面
(这篇本来是上周的内容,昨天忘记写啦,赶紧偷偷补上)
最近事情有点多,虽然sort过了,还是略显忙乱。但无论怎样,计划摆在那里,最终都会一件一件完成
废话适可而止,愿老妈身体赶紧好起来~
一.箭头函数简介
箭头函数(arrow function),就是C#中的lambda表达式,据说Java8也把它加入了豪华午餐。但不管怎样,JS正在从其它语言吸纳优秀的特性(比如yield, class, 默认参数等等),且不论这些特性好坏,这件事本身就是极好的(至少我们正在使用的是一个充满活力的工具)
只是Java用->
箭头,C#用的箭头与JS一样:=>
,这个箭头叫“lambda运算符”,行话读作”goes to”
lambda表达式(箭头函数)据说是定义函数最简洁的方法,语法上几乎没有冗余成分了。因为JS弱类型的特点,JS中的lambda表达式要比C#和Java中的更简洁(少了参数类型声明)
一句话,箭头函数就是lambda表达式,提供了更简洁的function定义方式
二.语法
arg => returnVal
语法是创建函数最简洁的方式,定义了一个形参为arg
,返回值为returnVal
的function
其它语法如下表:
语法 | 等价代码 | 含义 |
---|---|---|
x => f(x) |
function(x) { return f(x); } |
y=f(x) |
(x, y)=>x + y; |
function(x, y) { return x + y; } |
y=f(x,y)=x+y |
(x, y)=>{g(x); g(y); return h(x, y);}; |
function(x, y) { g(x); g(y); return h(x, y); } |
g(x), g(y) y=f(x,y)==============h(x,y) |
()=>({}); |
function() { return {}; } |
y={} |
P.S.第三列的“含义”指的是数学函数含义,lambda表达式本来就是数学家弄出来的
简单示例如下:
// 简单例子,简化匿名函数的定义
var arr = [1, 3, 21, 12];
console.log(arr.map(x => 2 * x)); // [2, 6, 42, 24]
console.log(arr.sort((a, b) => a - b)); // [1, 3, 12, 21]
arr.forEach((item, index, arr) => {
if (index %2 == 0) {
console.log(item);
}
if (index == arr.length - 1) {
console.log(`last item is ${item}`);
}
});
复杂一点的示例:
// 复杂例子
var app = {
cache: {},
ajax: function(url, callback) {
var self = this;
function req(url) {
var res = `data from ${url}`;
console.log(`ajax request ${url}`);
// cache res
self.cache[url] = res;
return res;
}
var data = req(url);
callback(data);
}
}
app.ajax('http://www.xxx.xx', function(data) {
console.log(`receive: ${data}`);
});
console.log(app.cache);
用箭头函数改写上例:
// 用箭头函数改写
var es6App = {
cache: {},
ajax(url, callback) {
var req = url => {
var res = `data from ${url}`;
console.log(`ajax request ${url}`);
// cache res
this.cache[url] = res;
return res;
}
var data = req(url);
callback(data);
}
}
es6App.ajax('http://www.q.xx', function(data) {
console.log(`receive: ${data}`);
});
console.log(es6App.cache);
消除了that = this
这种必要的废话,其实只要遵守一项原则就可以消除所有的that = this
,见下文注意事项中的3.关于this
三.特点及注意事项
1.参数列表与返回值的语法
1个参数时,左边直接写参数名,0个或者多个参数时,参数列表要用()
包裹起来
函数体只有1条语句时,右边值自动成为函数返回值,函数体不止1条语句时,函数体需要用{}
包裹起来,并且需要手动return
P.S.当然,可能很容易想到不分青红皂白,把() => {}
作为箭头函数的起手式,但不建议这样做,因为下一条说了{
是有歧义的,可能会带来麻烦
2.有歧义的字符
{
是唯一1个有歧义的字符,所以返回对象字面量时需要用()
包裹,否则会被当作块语句解析
例如:
var f1 = () => {};
f1(); // 返回undefined
// 等价于
// var f1 = function() {};
var f2 = () => ({});
f2(); // 返回空对象{}
// 等价于
// var f2 = function() {return {};};
3.关于this
箭头函数会从外围作用域继承this,为了避免that = this
,需要遵守:除了对象上的直接函数属性值用function语法外,其它函数都用箭头函数
这个规则很容易理解,示例如下:
// 场景1
function MyType() {}
MyType.prototype.fn = function() {/*定义箭头函数*/}; // 箭头函数中this指向MyType类型实例
// 场景2
var obj = {};
obj.fn = function() {/*定义箭头函数*/}; // 箭头函数中this指向obj
区别在于function
关键字定义的函数属性中,该函数的this
指向这个函数属性所属的对象(匿名函数的this
指向global对象或者undefined
)。说白了,function
能定义一个新this
,而箭头函数不能,它只能从外层借一个this
。所以,需要新this
出现的时候用function
定义函数,想沿用外层的this
时就用箭头函数
4.关于arguments对象
箭头函数没有arguments对象,因为标准鼓励使用默认参数、可变参数、参数解构
例如:
// 一般函数
(function(a) {console.log(`a = ${a}, arguments[0] = ${arguments[0]}`)})(1);
// log print: a = 1, arguments[0] = 1
// 与上面等价的箭头函数
(a => console.log(`a = ${a}, arguments[0] = ${arguments[0]}`))(1);
// log print: Uncaught ReferenceError: arguments is not defined
这与函数匿名不匿名无关,规则就是箭头函数中无法访问arguments对象(undefined),如下:
// 非匿名函数
var f = a => console.log(`a = ${a}, arguments[0] = ${arguments[0]}`);
f(2);
// log print: Uncaught ReferenceError: arguments is not defined
四.题外话
就ES6箭头函数而言,上面的内容足以随心所欲地驾驭它了,下面我们扯点别的(有意思的)
1.JS中支持的所有箭头
箭头 | 含义 |
---|---|
<!– | 单行注释 |
–> | 在行首表示单行注释,在其它位置表示“趋向于”(n –> 0等价于n– > 0) |
<= | 比较运算符,小于等于 |
=> | 箭头函数 |
看到两个单行注释语法不要大惊小怪,历史原因,但目前所有浏览器都支持。没什么用,冷知识吧
2.lambda演算与邱奇数
lambda演算中唯一基础数据类型是函数,邱奇数(church numerals)就是用高阶函数表示常见的基础数据类型(整数、布尔值、键值对、列表等等)
自然数都是数字,邱奇数都是函数,邱奇数的n是n阶函数,f^n(inc, base) === f(inc, f(inc, ...f(inc, base)))
,所有邱奇数都是有2个参数的函数
如何用函数表示自然数?内容比较多,这里给一个自然数集小例子:
// 定义自然数集合
var number = (function*(inc, base) {
var n = zero;
while(true) {
yield n(inc, base);
n = succ(n);
}
})(inc, 0);
for (var n of number) {
console.log(n); // 0, 1, 2, 3, 4
if (n > 3) {
break;
}
}
用邱奇数表示的自然数集如下:
// 0, 1, 2
var zero = (inc, base) => base;
var one = (inc, base) => inc(base);
var two = (inc, base) => inc(inc(base));
// 定义后继函数 f^n -> f^(n+1)
// succ = ln.lf.lx.f (n f x)
var succ = n => (inc, base) => inc(n(inc, base));
// 定义邱奇数集合
var church = (function*() {
var fn = zero;
while(true) {
yield fn;
fn = succ(fn);
}
})();
// test
var [, , , three, four, five, six, seven] = church;
console.log(three(inc, 0)); // 3
console.log(four(inc, 0)); // 4
console.log(five(inc, 0)); // 5
console.log(six(inc, 0)); // 6
console.log(seven(inc, 0)); // 7
仔细想想的话会发现世界真奇妙,这样也行??感兴趣的话请查看笔者的Demo(实现了减法、乘法和减法)
3.Y组合子与函数式编程
Y组合子能实现匿名递归函数
Y组合子就是一个函数,如下:
var Y = F => G(G), var G = slef => F(self(self))
其中有个不动点的概念。不动点:若F(f) = f,则f是不动点,在Y组合子中,G(G)是不动点
假设现有一个用来求阶乘的递归函数:
var fact = n => n === 0 ? 1 : n * fact(n - 1);
这显然不是一个匿名递归,fact
是函数名,递归调用它实现计算阶乘。那么如何实现一个匿名的递归函数?这有可能吗?
用Y组合子来一发就好了,如下:
// 定义Y组合子
// var Y = F => G(G);
// var G = self => F(self(self));
var Y = F =>
((g => g(g))
(g =>
(F((...x) =>
g(g)(...x)))));
// 实现匿名递归求阶乘
var yFact = Y(f =>
n => n === 0 ? 1 : n * f(n - 1));
console.log(yFact(5)); // 120
奇妙吧?函数式编程中有各种类似的奇妙变换,且不说FP的理解成本,执行效率,这些变换本身就是一些有意思的值得研究的东西,给思维多一点空间,让cpu跑起来
五.总结
lambda表达式的极致简洁很诱人,定义函数就像写数学公式一样,支持函数式编程的语言本该如此
参考资料
APIO讲稿——函数式编程:byvoid前辈的讲稿,汗颜
《ES6 in Depth》:InfoQ中文站提供的免费电子书