搜索
首页web前端js教程深入探讨javascript函数式编程_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()来遍历一个数组,并对其每个元素调用回调函数accumulator2
var 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)); // 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);&#65532;
 }
}

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模拟惰性求值的函数库, 这是第三章的主题。

声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
在JavaScript中替换字符串字符在JavaScript中替换字符串字符Mar 11, 2025 am 12:07 AM

JavaScript字符串替换方法详解及常见问题解答 本文将探讨两种在JavaScript中替换字符串字符的方法:在JavaScript代码内部替换和在网页HTML内部替换。 在JavaScript代码内部替换字符串 最直接的方法是使用replace()方法: str = str.replace("find","replace"); 该方法仅替换第一个匹配项。要替换所有匹配项,需使用正则表达式并添加全局标志g: str = str.replace(/fi

自定义Google搜索API设置教程自定义Google搜索API设置教程Mar 04, 2025 am 01:06 AM

本教程向您展示了如何将自定义的Google搜索API集成到您的博客或网站中,提供了比标准WordPress主题搜索功能更精致的搜索体验。 令人惊讶的是简单!您将能够将搜索限制为Y

示例颜色json文件示例颜色json文件Mar 03, 2025 am 12:35 AM

本文系列在2017年中期进行了最新信息和新示例。 在此JSON示例中,我们将研究如何使用JSON格式将简单值存储在文件中。 使用键值对符号,我们可以存储任何类型的

10个jQuery语法荧光笔10个jQuery语法荧光笔Mar 02, 2025 am 12:32 AM

增强您的代码演示:开发人员的10个语法荧光笔 在您的网站或博客上共享代码片段是开发人员的常见实践。 选择合适的语法荧光笔可以显着提高可读性和视觉吸引力。 t

构建您自己的Ajax Web应用程序构建您自己的Ajax Web应用程序Mar 09, 2025 am 12:11 AM

因此,在这里,您准备好了解所有称为Ajax的东西。但是,到底是什么? AJAX一词是指用于创建动态,交互式Web内容的一系列宽松的技术。 Ajax一词,最初由Jesse J创造

8令人惊叹的jQuery页面布局插件8令人惊叹的jQuery页面布局插件Mar 06, 2025 am 12:48 AM

利用轻松的网页布局:8个基本插件 jQuery大大简化了网页布局。 本文重点介绍了简化该过程的八个功能强大的JQuery插件,对于手动网站创建特别有用

什么是这个&#x27;在JavaScript?什么是这个&#x27;在JavaScript?Mar 04, 2025 am 01:15 AM

核心要点 JavaScript 中的 this 通常指代“拥有”该方法的对象,但具体取决于函数的调用方式。 没有当前对象时,this 指代全局对象。在 Web 浏览器中,它由 window 表示。 调用函数时,this 保持全局对象;但调用对象构造函数或其任何方法时,this 指代对象的实例。 可以使用 call()、apply() 和 bind() 等方法更改 this 的上下文。这些方法使用给定的 this 值和参数调用函数。 JavaScript 是一门优秀的编程语言。几年前,这句话可

10 JavaScript和JQuery MVC教程10 JavaScript和JQuery MVC教程Mar 02, 2025 am 01:16 AM

本文介绍了关于JavaScript和JQuery模型视图控制器(MVC)框架的10多个教程的精选选择,非常适合在新的一年中提高您的网络开发技能。 这些教程涵盖了来自Foundatio的一系列主题

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
2 周前By尊渡假赌尊渡假赌尊渡假赌
仓库:如何复兴队友
4 周前By尊渡假赌尊渡假赌尊渡假赌
Hello Kitty Island冒险:如何获得巨型种子
3 周前By尊渡假赌尊渡假赌尊渡假赌

热工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版

SecLists

SecLists

SecLists是最终安全测试人员的伙伴。它是一个包含各种类型列表的集合,这些列表在安全评估过程中经常使用,都在一个地方。SecLists通过方便地提供安全测试人员可能需要的所有列表,帮助提高安全测试的效率和生产力。列表类型包括用户名、密码、URL、模糊测试有效载荷、敏感数据模式、Web shell等等。测试人员只需将此存储库拉到新的测试机上,他就可以访问到所需的每种类型的列表。

WebStorm Mac版

WebStorm Mac版

好用的JavaScript开发工具

SublimeText3 英文版

SublimeText3 英文版

推荐:为Win版本,支持代码提示!