javascript拥有简洁的表达,使你可以专心于算法攻略。就好像黑白机上的闯关游戏,你拾取了宝剑,只需要不停地点A就可以了。你唯一要思考的就是如何不停地跳躲Boss的大招。
javascript成为浏览器的唯一语言,并且成为世界标准许多年,是有非常重要的理由的。《JavaScript: The Good Parts》做出了非常清晰地解释。
JavaScript:The World's Most Misunderstood Programming Language
然而,想要掌握javascript的正确编写方式并不容易。尤其是当你从教科书开始的时候,大部分给你的信息都是面向对象的东西:new
, prototype
,class
,extend
,...
这些都不是正确编写javascript的方式,当然你可以这么做,但是你会恨上javascript,这样编写的感觉会让你觉得你在写java却不能拥有java一样的计算速度。
想要喜欢上javascript并且享受programming的快感,你需要放下一切对面向对象的理解,走进函数和求值的世界。你的所有代码只有function
,这是简单的并且灵活的。
什么是函数?
如同数学界,函数function
即代表了求值。然而,从另一个角度来讲,函数function
也是值:
g = f(x)
我们认为g是一个值,来自f函数的计算,输入是x。然而,我们可以把值g作为一个函数,输入y,再次进行求值:
v = g(y)
把函数function
赋予值的定位,可以使计算充满了灵活:
v = f(x)(y) = k(f, x, y)
什么是柯里化Currying?
f(x)(y)
就是柯里化:使用函数f,输入x,计算,获得一个新的函数,再次输入y,计算,获取结果。f(x)(y)(z)(a)(b)(c)
,你完全可以写这样的函数。每次进行一次计算时,都返回一个新的函数。当然,你也可以写成这样的方式g(x, y, z, a, b, c)
。
function f (x) {
return function (y) {
return function (z) {
return function (a) {
return function (b) {
return function (c) {
console.log(x + y + z + a + b + c);
};
};
};
};
};
}
计算6个数字相加为什么要费这么大的周折?原因在于可以获得拥有内部记忆的函数: g = f(1)
可以获得一个新的函数g
。可以使用g
编写多种不同的求值组合,而使第一个输入始终是1
:
g(2)(3)(4)(5)(6);
g(3)(3)(4)(5)(6);
g(9)(9)(1)(2)(3);
什么是闭包Closure?
上边的函数就是闭包,确切的说,利用柯里化机制的函数function
就是闭包函数。通过柯里化,获取一个拥有记忆功能的函数,简化后续的多种计算操作,这就是闭包。
function move(start) {
var pos = start;
return function () {
console.log('Move to ' + (pos += 2) + '.');
}
}
var move_next = move(6);
move_next(); // Move to 8.
move_next(); // Move to 10.
进阶:记忆
下面我们来看一下经典的缓存函数。开始有两个输入参数,一个是数组sets
,一个是求值函数f
:
function memorize(sets, f) {
var cache = {};
return function (x) {
console.log('cache: %j', cache);
return x in cache
? cache[x]
: cache[x] = f(sets[x]);
}
}
首先,我们在memorize
的内部空间放置了一个记忆单元:cache
,是一个Object
类型,这样我们就可以用来存储任何我们想要的数据。Object
类型可以看做是简化并且统一版的C语言中的struct
:不需要考虑链接,不需要考虑类型,解释器会为你完成。
接下来,我们运用柯里化返回一个函数。这个函数有一个输入参数,指定了sets
数组中的第几项进行计算。我们首先使用console.log
打印当时内部空间的记忆单元cache
的值,然后判断输入参数是不是在cache
的键中。如果已经存在,直接返回记忆的内容,如果没有存在,使用函数f
对输入参数sets[x]
求值,然后把结果记忆到内部空间的记忆单元:cache
中。
通过记忆,每次使用求值函数f
计算后,都将结果保存在cache
中,这样可以极大的降低重复计算:
var g = memorize([1000, 2000, 3000], function (x) { return x * x; });
g(0); // cache: {},计算1000*1000
g(0); // cache: {"0":1000000},来自记忆
g(0); // cache: {"0":1000000},来自记忆
g(0); // cache: {"0":1000000},来自记忆
g(1); // cache: {"0":1000000},计算2000*2000
g(1); // cache: {"0":1000000, "1":4000000},来自记忆
g(1); // cache: {"0":1000000, "1":4000000},来自记忆
进阶:让函数循环起来
我们已经看到了柯里化(闭包)的好处和妙处,同时这些也都是函数function
概念帮助我们完成了一系列繁琐的工作。下面我们将把函数function
运用到循环中,进一步了解函数的好处和妙处。
function map(sets, f) {
var i = 0, len = sets.length, result = [], val;
while (i < len) {
val = f(sets[i]);
result.push(val);
++i;
}
return result;
}
函数map
使用两个输入参数:一个数组sets
,一个求值函数f
。
首先,我们计算数组sets
的长度,设定一个位置符i
。然后对sets
进行循环的操作,把其中的sets[i]
进行求值,然后压入result
中,最后将result
返回。
通过这样的设定,我们使函数f
在循环中运转。我们还可以进一步,再放入一个条件函数,只有条件成功的时候才进行求值:
function map(sets, condf, f) {
var i = 0, len = sets.length, result = [], val, set;
while (i < len) {
set = sets[i];
if (condf(set)) {
val = f(set);
result.push(val);
}
++i;
}
return result;
}
上边我们刚刚讨论了柯里化,所以把这个函数改一改,变成柯里化:
function map(sets, f) {
return function (condf) {
var i = 0, len = sets.length, result = [], val, set;
while (i < len) {
set = sets[i];
if (condf(set)) {
val = f(set);
result.push(val);
}
++i;
}
return result;
}
}
现在,函数map
在循环中进一步增加了灵活性,我们可以这样方便的使用:
var mymap = map([1,2,3,4,5,6], function (set) { return set + 1; });
mymap(function (set) { return set % 2 === 0 }); // 偶数
mymap(function (set) { return set > 9 }); // 大于9
// 如果你熟悉Ecmascript 6,那么代码会非常有趣
var mymap = map([1,2,3,4,5,6], set => set + 1);
mymap(set => set % 2 === 0); // 偶数
mymap(set => set > 9 ); // 大于9
→ 如何玩转闭包和柯里化