你肯定听说过JavaScript是一个函数式语言,或者至少知道他支持函数式编程。但是究竟什么是函数式编程?对于这个问题,如果你打算比较通用的编程范式,函数式的实现跟我们平时写的JavaScript又有什么不同?
好消息是在编程范式方面JavaScript没有那么讲究,你可以混入命令式(Imperative Programming)、面向对象、原型和函数式代码等等,只要你觉得合适,都能得到一样的结果。但是坏消息也因它而起,JavaScript在同样的代码库下同时提供了众多编程风格,所以你需要根据代码的可维护性、可读性和性能选择合适的编程方式。
函数式JavaScript并不是一定要用于整个项目中来体现它的价值。学习一点函数式的方法可以在我们构建项目时提供一些参考和帮助,而不论我们喜欢哪种方式。学习一些函数式模式和技术可以帮助我们写出更整洁、优雅的JavaScript代码,不论我们是不是真的用它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
var result; function getText() { var someText = prompt("Give me something to capitalize"); capWords(someText); alert(result.join(" ")); }; function capWords(input) { var counter; var inputArray = input.split(" "); var transformed = ""; result = []; for (counter = 0; counter < inputArray.length; counter++) { transformed = [ inputArray[counter].charAt(0).toUpperCase(), inputArray[counter].substring(1) ].join(""); result.push(transformed); } }; document.getElementById("main_button").onclick = getText; |
这一小段代码中做了很多事:定义了一个全局变量;字符串值被传来传去,并被函数修改;还夹杂着原生JavaScript的DOM方法。函数名不是很有描述力,一定程度上因为这一切都依赖于不确定存在或不存在的上下文。但是如果你碰巧在浏览器执行了这段代码,而且网页中定义了一个<button id="main_button">
,你会收到可输入文本的对话框提示,会看到那串被首字母大写了的文本。
像这种命令式代码会从顶部开始执行(如果不考虑variable hoisting的情况)。但是我们仍然可以利用JavaScript的面向对象能力来提高代码的可读性和整洁度。
面向对象的JavaScript
几年后,开发者开始注意到像浏览器这样的共享环境下的命令式编程的问题。一段JavaScript代码中定义的全局变量可能会破坏另一个全局变量。代码执行的顺序和效果无法预测,尤其在网络延迟和渲染时间的影响下。
后来出现了一些好办法来封装JS代码,让它能更好地跟DOM工作。比如下面这段代码是前面代码的一种改写,使用了面向对象的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
(function() { "use strict"; var SomeText = function(text) { this.text = text; }; SomeText.prototype.capify = function(str) { var firstLetter = str.charAt(0); var remainder = str.substring(1); return [firstLetter.toUpperCase(), remainder].join(""); }; SomeText.prototype.capifyWords = function() { var result = []; var textArray = this.text.split(" "); for (var counter = 0; counter < textArray.length; counter++) { result.push(this.capify(textArray[counter])); } return result.join(" "); }; document.getElementById("main_button").addEventListener("click", function(e) { var something = prompt("Give me something to capitalize"); var newText = new SomeText(something); alert(newText.capifyWords()); }); }()); |
在面向对象的版本中,构造函数模拟了一个类供我们设计我们想要的对象,方法定义在对象的原型中来降低内存使用。所有的代码都封闭在一个匿名的及时调用函数表达式中,所以它不会污染全局作用域。”use strict”强制使用最新的JavaScript引擎,onclick方法被新引入的addEventListner代替,毕竟没人再使用比IE8还古董的浏览器了。这段代码可以被插入在<body>段的最后,使得所有的DOM加载完成后才执行,这时候<button>才可用。
函数式JavaScript
相比我们开始说的命令式代码,面向对象确实能使代码更整洁、模块化更好,但我们看能否通过解决它的一些缺点来使它更完美。如果我们能通过JavaScript的一些内建特性,把函数当“第一类对象”看待,我们的代码会更加干净、稳定、易拓展。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
(function() { "use strict"; var capify = function(str) { return [str.charAt(0).toUpperCase(), str.substring(1)].join(""); }; var processWords = function(fn, str) { return str.split(" ").map(fn).join(" "); }; document.getElementById("main_button").addEventListener("click", function(e) { var something = prompt("Give me something to capitalize"); alert(processWords(capify, something)); }); }()); |
发现这个版本短了多少没?我们制定一了capify和processWords两个函数,每个函数都是pure的,也就是说他们不依赖于调用他们的代码的状态。函数不会改变他们之外的变量。针对给定的任意参数,返回唯一的结果。由于这些改进,新的函数易于测试,也很容易被移植。
刚才那段代码中可能有个函数你没见过,我们在Array上使用map方法,可以对Array上每个元素应用fn函数(fn是传入的参数)。运行在现代浏览器和服务端的实现了ES5的JavaScript为我们提供了这个方便的函数。这里不需要使用for循环,只使用一个map,可以避免使用循环变量,提高代码的整洁度和可读性。
对函数式的思考
你用为了函数范式而抛弃使用的一切,而是在你做下一个项目的时候,可以用函数的思维来考虑一下下面的问题:
- 我的函数依赖调用上下文吗?他们是独立的吗?
- 我可以把函数写成对某个输入总有相同返回值的吗?
- 我的函数不改变函数外的变量吗?
- 如果我想要在其他项目中使用这个函数,我需要修改他们吗?
这篇介绍仅仅是揭露了函数式JavaScript的表面,但希望它能引起你的探究欲望而学到更多。