有时,优雅的实现是一个函数。不是方法。不是类。不是框架。只是函数。 - John Carmack,游戏《毁灭战士》首席程序员
Bei der funktionalen Programmierung geht es darum, ein Problem in eine Reihe von Funktionen zu zerlegen. Typischerweise werden Funktionen miteinander verkettet, ineinander verschachtelt, hin- und hergereicht und als Bürger erster Klasse behandelt. Wenn Sie ein Framework wie jQuery oder Node.js verwendet haben, haben Sie wahrscheinlich einige dieser Techniken verwendet, ohne es zu merken.
Wir beginnen mit einer kleinen Einführung in Javascript.
Angenommen, wir benötigen eine Liste von Werten, die gewöhnlichen Objekten zugewiesen werden. Diese Objekte können alles enthalten: Daten, HTML-Objekte usw.
var obj1 = {value: 1}, obj2 = {value: 2}, obj3 = {value: 3}; var values = []; function accumulate(obj) { values.push(obj.value); } accumulate(obj1); accumulate(obj2); console.log(values); // Output: [obj1.value, obj2.value]
Dieser Code funktioniert, ist aber instabil. Jeder Code kann das Werteobjekt ändern, ohne die Funktion Accumulate() zu übergeben. Und wenn wir vergessen, den Werten ein leeres Array [] zuzuweisen, funktioniert dieser Code überhaupt nicht.
Aber wenn die Variable innerhalb der Funktion deklariert wird, wird sie durch keinen böswilligen Code verändert.
function accumulate2(obj) { var values = []; values.push(obj.value); return values; } console.log(accumulate2(obj1)); // Returns: [obj1.value] console.log(accumulate2(obj2)); // Returns: [obj2.value] console.log(accumulate2(obj3)); // Returns: [obj3.value]
Auf keinen Fall! Es wird nur der Wert des zuletzt übergebenen Objekts zurückgegeben.
Möglicherweise können wir dieses Problem lösen, indem wir eine Funktion in die erste Funktion verschachteln.
var ValueAccumulator = function(obj) { var values = [] var accumulate = function() { values.push(obj.value); }; accumulate(); return values; };
Aber das Problem besteht immer noch und wir können jetzt nicht auf die Akkumulationsfunktion und die Wertevariable zugreifen.
Was wir brauchen, ist eine selbstaufrufende Funktion
Selbstaufrufende Funktionen und Abschlüsse
Was wäre, wenn wir einen Funktionsausdruck zurückgeben könnten, der wiederum ein Array von Werten zurückgibt? Auf Variablen, die innerhalb einer Funktion deklariert werden, kann der gesamte Code innerhalb der Funktion zugreifen, einschließlich selbstaufrufender Funktionen.
Durch die Verwendung von Selbstaufruffunktionen verschwindet die bisherige Peinlichkeit.
var ValueAccumulator = function() { var values = []; var accumulate = function(obj) { if (obj) { values.push(obj.value); return values; } else { return values; } }; return accumulate; }; //This allows us to do this: var accumulator = ValueAccumulator(); accumulator(obj1); accumulator(obj2); console.log(accumulator()); // Output: [obj1.value, obj2.value] ValueAccumulator = -> values = [] (obj) -> values.push obj.value if obj values
Hier dreht sich alles um den Umfang. Variablenwerte sind in der inneren Funktion akkumulieren() sichtbar, auch wenn externer Code diese Funktion aufruft. Dies nennt man Schließung.
Schließung bedeutet in Javascript, dass die Funktion auf den übergeordneten Bereich zugreifen kann, auch wenn die Ausführung der übergeordneten Funktion abgeschlossen ist.
Abschlüsse sind ein Merkmal aller funktionalen Sprachen. Traditionelle Imperativsprachen haben keine Abschlüsse.
Funktionen höherer Ordnung
Selbstaufrufende Funktionen sind eigentlich eine Form von Funktionen höherer Ordnung. Funktionen höherer Ordnung sind Funktionen, die andere Funktionen als Eingabe verwenden oder eine Funktion als Ausgabe zurückgeben.
Funktionen höherer Ordnung sind in der traditionellen Programmierung nicht üblich. Während imperative Programmierer Schleifen verwenden, um über Arrays zu iterieren, verwenden funktionale Programmierer einen völlig anderen Ansatz. Bei Funktionen höherer Ordnung kann jedes Element in einem Array auf eine Funktion angewendet werden und ein neues Array zurückgeben.
Dies ist die zentrale Idee der funktionalen Programmierung. Funktionen höherer Ordnung haben die Fähigkeit, Logik an Funktionen wie Objekte zu übergeben.
In Javascript werden Funktionen als erstklassige Bürger behandelt, genau wie klassische Funktionen wie Scheme und Haskell Sprachen sind. Das klingt vielleicht etwas seltsam, bedeutet aber tatsächlich, dass Funktionen wie Zahlen und Objekte als primitive Typen behandelt werden. Wenn Zahlen und Objekte hin- und hergereicht werden können, können das auch Funktionen.
Kommen Sie und überzeugen Sie sich selbst. Verwenden Sie nun die Funktion ValueAccumulator() aus dem vorherigen Abschnitt mit Funktionen höherer Ordnung:
// Verwenden Sie forEach(), um ein Array zu durchlaufen und die Rückruffunktion accumulator2
für jedes Element aufzurufen
var accumulator2 = ValueAccumulator();
var object = [obj1, obj2, obj3]; // Dieses Array kann sehr groß sein
Objekte.forEach(accumulator2);
console.log(accumulator2());
Reine Funktion
Die von reinen Funktionen zurückgegebenen Berechnungsergebnisse beziehen sich nur auf die übergebenen Parameter. Externe Variablen und der globale Status werden hier nicht verwendet und es treten keine Nebenwirkungen auf. Mit anderen Worten: Die als Eingabe übergebenen Variablen können nicht geändert werden. Daher können im Programm nur die von reinen Funktionen zurückgegebenen Werte verwendet werden.
Verwenden Sie mathematische Funktionen, um ein einfaches Beispiel zu geben. Math.sqrt(4) gibt immer 2 zurück, verwendet keine versteckten Informationen wie Einstellungen oder Status und verursacht keine Nebenwirkungen.
Eine reine Funktion ist eine wahre Interpretation einer mathematischen „Funktion“, die die Beziehung zwischen Eingabe und Ausgabe darstellt. Ihre Ideen sind einfach und leicht wiederzuverwenden. Da reine Funktionen vollständig in sich abgeschlossen sind, eignen sie sich besser für die wiederholte Verwendung.
Beispiel zum Vergleich unreiner Funktionen und reiner Funktionen.
// 把信息打印到屏幕中央的函数 var printCenter = function(str) { var elem = document.createElement("div"); elem.textContent = str; elem.style.position = 'absolute'; elem.style.top = window.innerHeight / 2 + "px"; elem.style.left = window.innerWidth / 2 + "px"; document.body.appendChild(elem); }; printCenter('hello world'); // 纯函数完成相同的事情 var printSomewhere = function(str, height, width) { var elem = document.createElement("div"); elem.textContent = str; elem.style.position = 'absolute'; elem.style.top = height; elem.style.left = width; return elem; }; document.body.appendChild( printSomewhere('hello world', window.innerHeight / 2) + 10 + "px", window.innerWidth / 2) + 10 + "px"));
Uneigentliche Funktionen basieren auf dem Zustand des Fensterobjekts, um Breite und Höhe zu berechnen, während eigenständige reine Funktionen die Übergabe dieser Werte als Parameter erfordern. Tatsächlich können Informationen überall gedruckt werden, was diese Funktion vielseitiger macht.
Die unreine Funktion scheint eine einfachere Wahl zu sein, da sie intern anhängende Elemente implementiert, anstatt Elemente zurückzugeben. Die reine Funktion printSomewhere(), die einen Wert zurückgibt, erzielt in Kombination mit anderen funktionalen Programmiertechniken eine bessere Leistung.
var messages = ['Hi', 'Hello', 'Sup', 'Hey', 'Hola']; messages.map(function(s, i) { return printSomewhere(s, 100 * i * 10, 100 * i * 10); }).forEach(function(element) { document.body.appendChild(element); });
当一个函数是纯的,也就是不依赖于状态和环境,我们就不用管它实际是什么时候被计算出来。 后面的惰性求值将讲到这个。
匿名函数
把函数作为头等对象的另一个好处是匿名函数。
就像名字暗示的那样,匿名函数就是没有名字的函数。实际不止这些。它允许了在现场定义临时逻辑的能力。 通常这带来的好处就是方便:如果一个函数只用一次,没有必要给它浪费一个变量名。
下面是一些匿名函数的例子:
// 写匿名函数的标准方式 function() { return "hello world" }; // 匿名函数可以赋值给变量 var anon = function(x, y) { return x + y }; // 匿名函数用于代替具名回调函数,这是匿名函数的一个更常见的用处 setInterval(function() { console.log(new Date().getTime()) }, 1000); // Output: 1413249010672, 1413249010673, 1413249010674, ... // 如果没有把它包含在一个匿名函数中,他将立刻被执行, // 并且返回一个undefined作为回调函数: setInterval(console.log(new Date().getTime()), 1000) // Output: 1413249010671
下面是匿名函数和高阶函数配合使用的例子
function powersOf(x) { return function(y) { // this is an anonymous function! return Math.pow(x, y); }; } powerOfTwo = powersOf(2); console.log(powerOfTwo(1)); // 2 console.log(powerOfTwo(2)); // 4 console.log(powerOfTwo(3)); // 8 powerOfThree = powersOf(3); console.log(powerOfThree(3)); // 9 console.log(powerOfThree(10)); // 59049
这里返回的那个函数不需要命名,它可以在powersOf()函数外的任何地方使用,这就是匿名函数。
还记得累加器的那个函数吗?它可以用匿名函数重写
var obj1 = { value: 1 }, obj2 = { value: 2 }, obj3 = { value: 3 }; var values = (function() { // 匿名函数 var values = []; return function(obj) { // 有一个匿名函数! if (obj) { values.push(obj.value); return values; } else { return values; } } })(); // 让它自执行 console.log(values(obj1)); // Returns: [obj.value] console.log(values(obj2)); // Returns: [obj.value, obj2.value] obj1 = { value: 1 } obj2 = { value: 2 } obj3 = { value: 3 } values = do -> valueList = [] (obj) -> valueList.push obj.value if obj valueList console.log(values(obj1)); # Returns: [obj.value] console.log(values(obj2)); # Returns: [obj.value, obj2.value]
真棒!一个高阶匿名纯函数。我们怎么这么幸运?实际上还不止这些,这里面还有个自执行的结构, (function(){...})();。函数后面跟的那个括号可以让函数立即执行。在上面的例子里, 给外面values赋的值是函数执行的结果。
匿名函数不仅仅是语法糖,他们是lambda演算的化身。请听我说下去…… lambda演算早在计算机和计算机语言被发明的很久以前就出现了。它只是个研究函数的数学概念。 非同寻常的是,尽管它只定义了三种表达式:变量引用,函数调用和匿名函数,但它被发现是图灵完整的。 如今,lambda演算处于所有函数式语言的核心,包括javascript。
由于这个原因,匿名函数往往被称作lambda表达式。
匿名函数也有一个缺点,那就是他们在调用栈中难以被识别,这会对调试造成一些困难。要小心使用匿名函数。
方法链
在Javascript中,把方法链在一起很常见。如果你使用过jQuery,你应该用过这种技巧。它有时也被叫做“建造者模式”。
这种技术用于简化多个函数依次应用于一个对象的代码。
// 每个函数占用一行来调用,不如…… arr = [1, 2, 3, 4]; arr1 = arr.reverse(); arr2 = arr1.concat([5, 6]); arr3 = arr2.map(Math.sqrt); // ……把它们串到一起放在一行里面 console.log([1, 2, 3, 4].reverse().concat([5, 6]).map(Math.sqrt)); // 括号也许可以说明是怎么回事 console.log(((([1, 2, 3, 4]).reverse()).concat([5, 6])).map(Math.sqrt));
这只有在函数是目标对象所拥有的方法时才有效。如果你要创建自己的函数,比如要把两个数组zip到一起, 你必须把它声明为Array.prototype对象的成员.看一下下面的代码片段:
Array.prototype.zip = function(arr2) {
// ...
}
这样我们就可以写成下面的样子
arr.zip([11,12,13,14).map(function(n){return n*2});
// Output: 2, 22, 4, 24, 6, 26, 8, 28
递归
递归应该是最著名的函数式编程技术。就是一个函数调用它自己。
当函数调用自己,有时奇怪的事情就发生了。它的表现即是一个循环,多次执行同样的代码,也是一个函数栈。
使用递归函数时必须十分小心地避免无限循环(这里应该说是无限递归)。就像循环一样,必须有个停止条件。 这叫做基准情形(base case)。
下面有个例子
var foo = function(n) { if (n < 0) { // 基准情形 return 'hello'; } else { // 递归情形 return foo(n - 1); } } console.log(foo(5));
译注:原文中的代码有误,递归情形的函数调用缺少return,导致函数执行得最后没有结果。这里已经纠正。
递归和循环可以相互转换。但是递归算法往往更合适,甚至是必要的,因为有些情形用循环很费劲。
一个明显的例子就是遍历树。
var getLeafs = function(node) { if (node.childNodes.length == 0) { // base case return node.innerText; } else { // recursive case: return node.childNodes.map(getLeafs); } }
分而治之
递归不只是代替for和while循环的有趣的方式。有个叫分而治之的算法,它递归地把问题拆分成更小的情形, 直到小到可以解决。
历史上有个欧几里得算法用于找出两个数的最大公分母
function gcd(a, b) { if (b == 0) { // 基准情形 (治) return a; } else { // 递归情形 (分) return gcd(b, a % b); } } console.log(gcd(12,8)); console.log(gcd(100,20)); gcb = (a, b) -> if b is 0 then a else gcb(b, a % b)
理论上来说,分而治之很牛逼,但是现实中有用吗?当然!用Javascript的函数对数组排序不是很好, 它不但替换了原数组,也就是说数据不是不变的,并且它还不够可靠、灵活。通过分而治之,我们可以做得更好。
全部的实现代码大概要40行,这里只展示伪代码:
var mergeSort = function(arr) { if (arr.length < 2) { // 基准情形: 只有0或1个元素的数组是不用排序的 return items; } else { // 递归情形: 把数组拆分、排序、合并 var middle = Math.floor(arr.length / 2); // 分 var left = mergeSort(arr.slice(0, middle)); var right = mergeSort(arr.slice(middle)); // 治 // merge是一个辅助函数,返回一个新数组,它将两个数组合并到一起 return merge(left, right); } }
译注:关于用分而治之的思路进行排序的一个更好的例子是快排,使用Javascript也只有13行代码。 具体请参考我以前的博文 《优雅的函数式编程语言》
惰性求值
惰性求值,也叫做非严格求值,它会按需调用并推迟执行,它是一种直到需要时才计算函数结果的求值策略, 这对函数式编程特别有用。比如有行代码是 x = func(),调用这个func()函数得到的返回值会赋值给x。 但是x等于什么一开始并不重要,直到需要用到x的时候。等到需要用x的时候才调用func()就是惰性求值。
这一策略可以让性能明显增强,特别是当使用方法链和数组这些函数式程序员最喜爱的程序流技术的时候。 惰性求值让人兴奋的一个优点是让无限序列成为可能。因为在它实在无法继续延迟之前,什么都不需要被真正计算出来。 它可以是这个样子:
// 理想化的JavaScript伪代码: var infinateNums = range(1 to infinity); var tenPrimes = infinateNums.getPrimeNumbers().first(10);
这为很多可能性敞开了大门,比如异步执行、并行计算、组合,这只列举了一点。
然而,还有个问题,Javascript本身并不支持惰性求值,也就是说存在让Javascript模拟惰性求值的函数库, 这是第三章的主题。