首页 web前端 js教程 JavaScript的函数式编程基础指南_基础知识

JavaScript的函数式编程基础指南_基础知识

May 16, 2016 pm 03:10 PM
javascript js 函数 函数式

引言

JavaScript是一种强大的,却被误解的编程语言。一些人喜欢说它是一个面向对象的编程语言,或者它是一个函数式编程语言。另外一些人喜欢说,它不是一个面向对象的编程语言,或者它不是一个函数式编程语言。还有人认为它兼具面向对象语言和函数式语言的特点,或者,认为它既不是面向对象的也不是函数式的,好吧,让我们先搁置那些争论。

让我们假设我们共有这样的一个使命:在JavaScript语言所允许的范围内,尽可能多的使用函数式编程的原则来编写程序。

首先,我们需要清理下脑子里那些关于函数式编程的错误观念。

在JS界被(重度)误解的函数式编程

显然有相当一批开发者一天到晚的以函数式范式的方式使用JavaScript。我还是要说有更大量的JavaScript开发者,并不真正理解那佯做的真正意义。

我确信,导致这种局面是因为很多用于服务端的web开发语言都源自C语言,而C语言,很显然不是一种函数式编程语言。

似乎有两个层级的混乱,第一个层级的混乱我们用下面这个在jQuery中经常会用到的例子来说明:

$(".signup").click(function(event){
  $("#signupModal").show();
  event.preventDefault();
});
登录后复制

嘿,仔细看。我传递一个匿名函数作为参数,这在JavaScript世界里被称作众所周知“CallBack”(回调)函数。

真有人会认为这就是函数式编程吗?根本不是!

这个例子展示了一个函数式语言的关键特性:函数作为参数。另一方面,这个3行代码的例子也违背了几乎所有其他的函数式编程范式。

第二个层级的混乱有点微妙。读到这里,一些追求潮流的JS开发者在暗自思考。

好吧,废话!但是我已经知道了所有关于函数式编程的知识与技能。我在我所有的项目上使用Underscore.js。

Underscore.js 是一个广受欢迎的JavaScript库,到处都在使用。举个例子,我有一组单词,我需要获得一个集合,集合里的每个元素是各个单词的头两个字母。用Underscore.js实现这个相当简单:

var firstTwoLetters = function(words){
  return _.map(words,function(word){
    return _.first(word,2);
  });
};
登录后复制

看!看JavaScript巫术。我正在使用这些高级的函数式应用函数,像_.map 和 _.first。你还有什么要说的,利兰(译注:作者Leland)?

尽管underscore 和 像_.map这样的函数是非常有价值的函数式范式,但是像这个例子中所采用的组织代码的方法看起来…冗长而且对于我来说太难于理解。我们真的需要这样做吗?

如果开始思考的时候多一点“函数式”的思维,可能我们能够把上面的例子改成这样:

// ...一点魔法
var firstTwoLetters = map(first(2));
登录后复制

仔细想想,在1行代码中包含了和上面5行代码同样的信息。words 和word 仅仅是参数/占位符。这个方法的核心是用一种更明显的方式组合map函数,first函数,和常量2。

JavaScript是函数式编程语言吗?

没有神奇的公式能够判定一种语言是不是“函数式”语言。有些语言很明显就是函数式的,就像另外一些语言很明显不是函数式的,但是有大量语言的是模棱两可的中间派。

于是这里给出一些常用的、重要的函数式语言的“配料”(JavaScript能实现用粗体标志)

  • 函数是“第一等公民”
  • 函数能够返回函数
  • 词法上支持闭包
  • 函数要“纯粹”
  • 可靠递归
  • 没有变异状态

这决不是一个排它的列表,但是我们至少要逐个讨论Javascript中最重要的三个特性,它们支撑我们可以用函数式的方式来编写程序。

让我们逐个详细的了解下:

函数是“第一等公民”

这条可能是在所有的配料中最明显的,并且可能是在很多现代编程语言中最常见到的。

在JavaScript局部变量是通过var关键字来定义的。

var foo = "bar";
登录后复制

JavaScript中把函数以局部变量的方式定义是非常容易做到的。

var add = function (a, b) { return a + b; };
var even = function (a) { return a % 2 === 0; };
登录后复制

这些都是事实,变量:变量add和变量even通过被赋值的方式,与函数定义建立引用关系,这种引用关系是在任何时候如果需要是可以被改变的。

// capture the old version of the function
var old_even = even; 
 
// assign variable `even` to a new, different function
even = function (a) { return a & 1 === 0; };
登录后复制

当然,这没有什么特别的。但是成为“第一等公民”这个重要的特性使得我们能够把函数以参数的方式传递给另一个函数。举个例子:

var binaryCall = function (f, a, b) { return f(a, b); };
登录后复制

这是一个函数,他接受了一个二元函数f,和两个参数a,b,然后调用这个二元函数f,该二元函数f以a、b为输入参数。

add(1,2) === binaryCall(add, 1, 2); 
// true
登录后复制

这样做看起来有点笨拙,但是当把接下来的函数式编程“配料”合并考虑的时候,牛叉之处就显而易见了…

函数能返回函数(换个说法“高阶函数”)

事情开始变的酷起来。尽管开始比较简单。函数最终以新的函数作为返回值。举个例子:

var applyFirst = function (f, a) { 
 return function (b) { return f(a, b); };
};
登录后复制

这个函数(applyFirst)接受一个二元函数作为其中一个参数,可以把第一个参数(即二元函数)看作是这个applyFirst函数的“部分操作”,然后返回一个一元(一个参数)函数,该一元函数被调用的时候返回外部函数的第一个参数(f)的二元函数f(a, b)。返回两个参数的二元函数。

让我们再谈谈一些函数,例如mult(乘法)函数:

var mult = function(a, b) { return a * b; };
登录后复制

依循mult(乘法)函数的逻辑,我们可以写一个新的函数double(乘方):

var double = applyFirst(mult, 2);
 
double(32); 
// 64
double(7.5); 
// 15
登录后复制

这就是偏函数,在FP中经常会用到。(译注:FP全名为 Functional Programming 函数式程序设计 )

我们当然可以像applyFirst那样定义函数:

var curry2 = function (f) {
 return function (a) {
  return function (b) {
   return f(a, b);
  };
 };
};
登录后复制

现在,我想要一个double(乘方)函数,我们换种方式做:

var double = curry2(mult)(2);
登录后复制

这种方式被称作“函数柯里化”。有点类似partial application(偏函数应用),但是更强大一点。

准确的说,函数式编程之所以强大,大部分因于此。简单和易理解的函数成为我们构筑软件的基础构件。当拥有高水平的组织能力、很少重用的逻辑的时候,函数能够被组合和混合在一起用来表达出更复杂的行为。

高阶函数可以得到的乐趣更多。让我们看两个例子:

1.翻转二元函数参数顺序

// flip the argument order of a function
var flip = function (f) {
 return function (a, b) { return f(b, a); };
};
 
divide(10, 5) === flip(divide)(5, 10); 
// true
登录后复制

2.创建一个组合了其他函数的函数

// return a function that's the composition of two functions...
// compose (f, g)(x) -> f(g(x))
var compose = function (f1, f2) {
 return function (x) {
  return f1(f2(x));
 };
};
 
// abs(x) = Sqrt(x^2)
var abs = compose(sqrt, square);
 
abs(-2); 
// 2
登录后复制

这个例子创建了一个实用的函数,我们可以使用它来记录下每次函数调用。

var logWrapper = function (f) {
 return function (a) {
  console.log('calling "' + f.name + '" with argument "' + a);
  return f(a);
 };
};

var app_init = function(config) { 
/* ... */
 };
 
if(DEBUG) {
 
// log the init function if in debug mode
 app_init = logWrapper(app_init);
}
 
// logs to the console if in debug mode
app_init({
 
/* ... */
});

登录后复制

词法闭包+作用域

我深信理解如何有效利用闭包和作用域是成为一个伟大JavaScript开发者的关键。
那么…什么是闭包?

简单的说,闭包就是内部函数一直拥有父函数作用域的访问权限,即使父函数已经返回。<译注4>
可能需要个例子。

var createCounter = function () {
 var count = 0;
 return function () {
  return ++count;
 };
};
 
var counter1 = createCounter();
 
counter1(); 
// 1
counter1(); 
// 2
 
var counter2 = createCounter();
 
counter2(); 
// 1
counter1(); 
// 3
登录后复制

一旦createCounter函数被调用,变量count就被分配一个新的内存区域。然后,返回一个函数,这个函数持有对变量count的引用,并且每次调用的时候执行count加1操作。

注意从createCounter函数的作用域之外,我们是没有办法直接操作count的值。Counter1和Counter2函数可以操作各自的count变量的副本,但是只有在这种非

常具体的方式操作count(自增1)才是被支持的。

在JavaScript,作用域的边界检查只在函数被声明的时候。逐个函数,并且仅仅逐个函数,拥有它们各自的作用域表。(注:在ECMAScript 6中不再是这样,因为let的引入)

一些进一步的例子来证明这论点:

// global scope
var scope = "global";
 
var foo = function(){
 
// inner scope 1
 var scope = "inner";
 var myscope = function(){
  
// inner scope 2
  return scope;
 };
 return myscope;
};
 
console.log(foo()()); 
// "inner"
 
console.log(scope); 
// "global"
登录后复制

关于作用域还有一些重要的事情需要考虑。例如,我们需要创建一个函数,接受一个数字(0-9),返回该数字相应的英文名称。

简单点,有人会这样写:

// global scope...
var names = ['zero','one','two','three','four','five','six','seven','eight','nine'];
var digit_name1 = function(n){
 return names[n];
};
登录后复制

但是缺点是,names定义在了全局作用域,可能会意外的被修改,这样可能致使digit_name1函数所返回的结果不正确。
那么,这样写:

var digit_name2 = function(n){
 var names = ['zero','one','two','three','four','five','six','seven','eight','nine'];
 return names[n];
};
登录后复制

这次把names数组定义成函数digit_name2局部变量.这个函数远离了意外风险,但是带来了性能损失,由于每次digit_name2被调用的时候,都将重新为names数组定义和分配空间。换个例子如果names是个非常大的数组,或者可能digit_name2函数在一个循环中被调用多次,这时候性能影响将非常明显。

// "An inner function enjoys that context even after the parent functions have returned."
var digit_name3 = (function(){
 var names = ['zero','one','two','three','four','five','six','seven','eight','nine'];
 return function(n){
  return names[n];
 };
})();
登录后复制

这时候我们面临第三个选择。这里我们实现立即调用的函数表达式,仅仅实例化names变量一次,然后返回digit_name3函数,在 IIFE (Immediately-Invoked-Function-Expression 立即执行表达式)的闭包函数持有names变量的引用。
这个方案兼具前两个的优点,回避了缺点。搞定!这是一个常用的模式用来创建一个不可被外部环境修改“private”(私有)状态。


本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

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

AI Clothes Remover

AI Clothes Remover

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

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

golang函数动态创建新函数的技巧 golang函数动态创建新函数的技巧 Apr 25, 2024 pm 02:39 PM

Go语言提供了两种动态函数创建技术:closures和反射。closures允许访问闭包作用域内的变量,而反射可使用FuncOf函数创建新函数。这些技术在自定义HTTP路由器、实现高度可定制的系统和构建可插拔的组件方面非常有用。

C++ 函数命名中参数顺序的考虑 C++ 函数命名中参数顺序的考虑 Apr 24, 2024 pm 04:21 PM

在C++函数命名中,考虑参数顺序至关重要,可提高可读性、减少错误并促进重构。常见的参数顺序约定包括:动作-对象、对象-动作、语义意义和遵循标准库。最佳顺序取决于函数目的、参数类型、潜在混淆和语言惯例。

如何在Java中写出高效和可维护的函数? 如何在Java中写出高效和可维护的函数? Apr 24, 2024 am 11:33 AM

编写高效和可维护的Java函数的关键在于:保持简洁。使用有意义的命名。处理特殊情况。使用适当的可见性。

excel函数公式大全 excel函数公式大全 May 07, 2024 pm 12:04 PM

1、 SUM函数,用于对一列或一组单元格中的数字进行求和,例如:=SUM(A1:J10)。2、AVERAGE函数,用于计算一列或一组单元格中的数字的平均值,例如:=AVERAGE(A1:A10)。3、COUNT函数,用于计算一列或一组单元格中的数字或文本的数量,例如:=COUNT(A1:A10)4、IF函数,用于根据指定的条件进行逻辑判断,并返回相应的结果。

C++ 函数默认参数与可变参数的优缺点比较 C++ 函数默认参数与可变参数的优缺点比较 Apr 21, 2024 am 10:21 AM

C++函数中默认参数的优点包括简化调用、增强可读性、避免错误。缺点是限制灵活性、命名限制。可变参数的优点包括无限灵活性、动态绑定。缺点包括复杂性更高、隐式类型转换、调试困难。

C++ 函数返回引用类型有什么好处? C++ 函数返回引用类型有什么好处? Apr 20, 2024 pm 09:12 PM

C++中的函数返回引用类型的好处包括:性能提升:引用传递避免了对象复制,从而节省了内存和时间。直接修改:调用方可以直接修改返回的引用对象,而无需重新赋值。代码简洁:引用传递简化了代码,无需额外的赋值操作。

自定义 PHP 函数和预定义函数之间有什么区别? 自定义 PHP 函数和预定义函数之间有什么区别? Apr 22, 2024 pm 02:21 PM

自定义PHP函数与预定义函数的区别在于:作用域:自定义函数仅限于其定义范围,而预定义函数可在整个脚本中访问。定义方式:自定义函数使用function关键字定义,而预定义函数由PHP内核定义。参数传递:自定义函数接收参数,而预定义函数可能不需要参数。扩展性:自定义函数可以根据需要创建,而预定义函数是内置的且无法修改。

C++ 函数异常进阶:定制错误处理 C++ 函数异常进阶:定制错误处理 May 01, 2024 pm 06:39 PM

C++中的异常处理可通过定制异常类增强,提供特定错误消息、上下文信息以及根据错误类型执行自定义操作。定义继承自std::exception的异常类,提供特定的错误信息。使用throw关键字抛出定制异常。在try-catch块中使用dynamic_cast将捕获到的异常转换为定制异常类型。实战案例中,open_file函数抛出FileNotFoundException异常,捕捉并处理该异常可提供更具体的错误消息。

See all articles