首页 > 网站 > WEB开发 > 正文

JS函数式编程【译】2.2 与函数共舞

2024-04-27 14:06:18
字体:
来源:转载
供稿:网友

JS函数式编程【译】2.2 与函数共舞

?? Functional PRogramming in javascript 主目录第二章 函数式编程基础上一节 函数式编程语言

与函数共舞

有时,优雅的实现是一个函数。不是方法。不是类。不是框架。只是函数。 - John Carmack,游戏《毁灭战士》首席程序员

函数式编程全都是关于如何把一个问题分解为一系列函数的。通常,函数会链在一起,互相嵌套, 来回传递,被视作头等公民。如果你使用过诸如jQuery或Node.js这样的框架,你应该用过一些这样的技术, 只不过你没有意识到。

我们从Javascript的一个小尴尬开始。

假设我们需要一个值的列表,这些值会赋值给普通的对象。这些对象可能包含任何东西:数据、HTML对象等等。

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]

这个代码能用但是不稳定。任何代码都可以不通过accumulate()函数改变values对象。 而且如果我们忘记了给values赋上空数组[],这个代码压根儿就不会工作。

但是如果变量声明在函数内部,他就不会被任何捣蛋的代码给更改。

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]

不行呀!只有最后传入的那个对象的值才被返回。

我们也许可以通过在第一个函数内部嵌套一个函数来解决这个问题。

var ValueAccumulator = function(obj) {  var values = []  var accumulate = function() {    values.push(obj.value);  };  accumulate();  return values;};

可是问题依然存在,而且我们现在无法访问accumulate函数和values变量了。

我们需要的是一个自调用函数

自调用函数和闭包

如果我们能够返回一个可以依次返回values数组的函数表达式怎么样?在函数内声明的变量可以被函数内的所有代码访问到, 包括自调用函数。

通过使用自调用函数,前面的尴尬消失了。

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

这些都是关于作用域的。变量values在内部函数accumulate()中可见,即便是在外部的代码在调用这个函数时。 这叫做闭包。

Javascript中的闭包就是函数可以访问父作用域,哪怕父函数已经执行完毕。

闭包是所有函数式语言都具有的特征。传统的命令式语言没有闭包。

高阶函数

自调用函数实际上是高阶函数的一种形式。高阶函数就是以其它函数为输入,或者返回一个函数为输出的函数。

高阶函数在传统的编程中并不常见。当命令式程序员使用循环来迭代数组的时候,函数式程序员会采用完全不同的一种实现方式。 通过高阶函数,数组中的每一个元素可以被应用到一个函数上,并返回新的数组。

这是函数式编程的中心思想。高阶函数具有把逻辑像对象一样传递给函数的能力。

在Javascript中,函数被作为头等公民对待,这和Scheme、Haskell等经典函数是语言一样的。 这话听起来可能有点古怪,其实实际意思就是函数被当做基本类型,就像数字和对象一样。 如果数字和对象可以被来回传递,那么函数也可以。

来实际看看。现在把上一节的ValueAccumulator()函数配合高阶函数使用:

// 使用forEach()来遍历一个数组,并对其每个元素调用回调函数accumulator2var accumulator2 = ValueAccumulator();var objects = [obj1, obj2, obj3]; // 这个数组可以很大objects.forEach(accumulator2);console.log(accumulator2());

纯函数

纯函数返回的计算结果仅与传入的参数相关。这里不会使用外部的变量和全局状态,并且没有副作用。 换句话说就是不能改变作为输入传入的变量。所以,程序里只能使用纯函数返回的值。

用数学函数来举一个简单的例子。Math.sqrt(4)将总是返回2,不使用任何隐藏的信息,如设置或状态, 而且不会带来任何副作用。

纯函数是对数学上的“函数”的真实演绎,就是输入和输出的关系。它们思路简单也便于重用。 由于纯函数是完全独立的,它们更适合被一次又一次地使用。

举例说明来对比一下非纯函数和纯函数。

// 把信息打印到屏幕中央的函数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")?);

非纯函数依赖window对象的状态来计算宽度和高度,自给自足的纯函数则要求这些值作为参数传入。 实际上它就允许了信息打印到任何地方,这也让这个函数有了更多用途。

非纯函数看起来是一个更容易的选择,因为它在自己内部实现了追加元素,而不是返回元素。 返回了值的纯函数printSomewhere()则会在跟其他函数式编程技术的配合下有更好的表现。

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)); // 2console.log(powerOfTwo(2)); // 4console.log(powerOfTwo(3)); // 8powerOfThree = powersOf(3);console.log(powerOfThree(3)); // 9console.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    valueListconsole.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对象的成员.看一下下面的代码片段:

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表