JavaScript ES6函数式编程(一):闭包与高阶函数

函数式编程的历史

函数的第一原则是要小,第二原则则是要更小 —— ROBERT C. MARTINhtml

解释一下上面那句话,就是咱们常说的一个函数只作一件事,好比:将字符串首字母和尾字母都改为大写,咱们此时应该编写两个函数。为何呢?为了更好的复用,这样作保证了函数更加的颗粒化。前端

早在 1950 年代,随着 Lisp 语言的建立,函数式编程( Functional Programming,简称 FP)就已经开始出如今你们视野。而直到近些年,函数式以其优雅,简单的特色开始从新风靡整个编程界,主流语言在设计的时候无一例外都会更多的参考函数式特性( Lambda 表达式,原生支持 map ,reduce ……),Java8 开始支持函数式编程。ajax

而在前端领域,咱们一样能看到不少函数式编程的影子:Lodash.js、Ramda.js库的普遍使用,ES6 中加入了箭头函数,Redux 引入 Elm 思路下降 Flux 的复杂性,React16.6 开始推出 React.memo(),使得 pure functional components 成为可能,16.8 开始主推 Hooks,建议使用 pure functions 进行组件编写……编程

这些无一例外的说明,函数式编程这种古老的编程范式并无随着岁月而褪去其光彩,反而越发生机勃勃。api

什么是函数式编程

上面咱们了解了函数式编程的历史,肯定它是个很棒的东西。接下来,咱们要去了解一下什么是函数式编程?数组

其实函数咱们从小就学,什么一元函数(f(x) = 3x),二元函数……根据学术上函数的定义,函数便是一种描述集合和集合之间的转换关系,输入经过函数都会返回有且只有一个输出值。浏览器

因此,函数其实是一个关系,或者说是一种映射,而这种映射关系是能够组合的,一旦咱们知道一个函数的输出类型能够匹配另外一个函数的输入,那他们就能够进行组合。缓存

在编程的世界里,咱们须要处理其实也只有“数据”和“关系”,而“关系”就是函数,“数据”就是要传入的实参。咱们所谓的编程工做也不过就是在找一种映射关系,好比:将字符串首字母转为大写。一旦关系找到了,问题就解决了,剩下的事情,就是让数据流过这种关系,而后转换成另外一个数据返回给咱们。闭包

想象一个流水线车间的工做过程,把输入当作原料,把输出当作产品,数据能够不断的从一个函数的输出能够流入另外一个函数输入,最后再输出结果,这不就是一套流水线嘛?app

因此,如今你明确了函数式编程是什么了吧?它其实就是强调在编程过程当中把更多的关注点放在如何去构建关系。经过构建一条高效的建流水线,一次解决全部问题。而不是把精力分散在不一样的加工厂中来回奔波传递数据。

参考连接:阮一峰 - 函数式编程入门教程

函数式编程的特色

  • 函数是一等公民

根据维基百科,编程语言中一等公民的概念是由英国计算机学家Christopher Strachey提出来的,时间则早在上个世纪60年代,那个时候尚未我的电脑,没有互联网,没有浏览器,也没有JavaScript。而且当时也没给出清晰的定义。

关于一等公民,我找到一个权威的定义,来自于一本书《Programming Language Pragmatics》,这本书是不少大学的程序语言设计的教材。

In general, a value in a programming language is said to have first-class status if it can be passed as a parameter, returned from a subroutine, or assigned into a variable.

也就是说,在编程语言中,一等公民能够做为函数参数,能够做为函数返回值,也能够赋值给变量。

例如,字符串在几乎全部编程语言中都是一等公民,字符串能够作为函数参数,字符串能够做为函数返回值,字符串也能够赋值给变量。

对于各类编程语言来讲,函数就不必定是一等公民了,好比Java 8以前的版本。

对于JavaScript来讲,函数能够赋值给变量,也能够做为函数参数,还能够做为函数返回值,所以JavaScript中函数是一等公民。

  • 声明式编程 (Declarative Programming)

经过上面的例子能够看出来,函数式编程大多时候都是在声明我须要作什么,而非怎么去作。这种编程风格称为**声明式编程 **。

// 好比:咱们要打印数组中的每一个元素
// 1. 命令式编程
let arr = [1, 2, 3];
for (let i = 0, len = arr.length; i < len; i++) {
  console.log(arr[i])
}

// 2. 声明式编程
let arr = [1, 2, 3];
arr.forEach(item => {
  console.log(item)
})

/*
* 相对于命令式编程的 for 循环拿到每一个元素,声明式编程不须要本身去找每一个元素
* 由于 forEach 已经帮咱们拿到了,就是 item,直接打印出来就行
*/

这样有个好处是代码的可读性特别高,由于声明式代码大多都是接近天然语言的,同时,它解放了大量的人力,由于它不关心具体的实现,所以它能够把优化能力交给具体的实现,这也方便咱们进行分工协做。

  • 惰性执行(Lazy Evaluation)

所谓惰性执行指的是函数只在须要的时候执行,即不产生无心义的中间变量。

  • 无状态和数据不可变 (Statelessness and Immutable data)

这是函数式编程的核心概念:

数据不可变:它要求你全部的数据都是不可变的,这意味着若是你想修改一个对象,那你应该建立一个新的对象用来修改,而不是修改已有的对象。
**无状态: **主要是强调对于一个函数,无论你什么时候运行,它都应该像第一次运行同样,给定相同的输入,给出相同的输出,彻底不依赖外部状态的变化。

  • 没有反作用(side effect)

反作用,通常指完成份内的事情以后还带来了很差的影响。在函数中,最多见的反作用就是随意修改外部变量。因为js对象传递的是引用地址,这很容易带来bug。

例如: map 函数的原本功能是将输入的数组根据一个函数转换,生成一个新的数组。而在 JS 中,咱们常常能够看到下面这种对 map 的 “错误” 用法,把 map 看成一个循环语句,而后去直接修改数组中的值。

const list = [...];
// 修改 list 中的 type 和 age
list.map(item => {
  item.type = 1;
  item.age++;
})

传递引用一时爽,代码重构火葬场

这样函数最主要的输出功能没有了,变成了直接修改了外部变量,这就是它的反作用。而没有反作用的写法应该是:

const list = [...];
// 修改 list 中的 type 和 age
const newList = list.map(item => ({...item, type: 1, age:item.age + 1}));

保证函数没有反作用,一来能保证数据的不可变性,二来能避免不少由于共享状态带来的问题。当你一我的维护代码时候可能还不明显,但随着项目的迭代,项目参与人数增长,你们对同一变量的依赖和引用愈来愈多,这种问题会愈来愈严重。最终可能连维护者本身都不清楚变量究竟是在哪里被改变而产生 Bug。

  • 纯函数 (pure functions)

函数式编程最关注的对象就是纯函数,纯函数的概念有两点:

不依赖外部状态(无状态): 函数的的运行结果不依赖全局变量,this 指针,IO 操做等。
没有反作用(数据不变): 不修改全局变量,不修改入参。

因此纯函数才是真正意义上的 “函数”, 它也遵循引用透明性——相同的输入,永远会获得相同的输出

咱们这么强调使用纯函数,纯函数的意义是什么?

便于测试和优化:这个意义在实际项目开发中意义很是大,因为纯函数对于相同的输入永远会返回相同的结果,所以咱们能够轻松断言函数的执行结果,同时也能够保证函数的优化不会影响其余代码的执行。这十分符合测试驱动开发 TDD(Test-Driven Development ) 的思想,这样产生的代码每每健壮性更强。

可缓存性:由于相同的输入老是能够返回相同的输出,所以,咱们能够提早缓存函数的执行结果,有不少库有所谓的 memoize 函数,下面以一个简化版的 memoize 为例,这个函数就能缓存函数的结果,对于像 fibonacci 这种计算,就能够起到很好的缓存效果。

function memoize(fn) {
    const cache = {};
    return function() {
      const key = JSON.stringify(arguments);
      var value = cache[key];
      if(!value) {
        value = [fn.apply(null, arguments)];  // 放在一个数组中,方便应对 undefined,null 等异常状况
        cache[key] = value; 
      }
      return value[0];
    }
  }

  const fibonacci = memoize(n => n < 2 ? n: fibonacci(n - 1) + fibonacci(n - 2));
  console.log(fibonacci(4))  // 执行后缓存了 fibonacci(2), fibonacci(3),  fibonacci(4)
  console.log(fibonacci(10)) // fibonacci(2), fibonacci(3),  fibonacci(4) 的结果直接从缓存中取出,同时缓存其余的

闭包

定义:一个可以读取其余函数内部变量的函数,实质是变量的解析过程(由内而外)

闭包是ES中一个离不开的话题,并且也是是一个难懂又必须搞明白的概念!提及闭包,就不得不提与它密切相关的变量做用域和变量的生命周期。下面来看下:

变量做用域

变量做用域分为两类:全局做用域和局部做用域。

  • 编写在script标签中的变量或者没用var关键字声明的变量,就表明全局变量,在页面的任意位置均可以访问到
  • 在函数中声明变量带有var关键字的便是局部变量,局部变量只能在函数内才能访问到
function fn() {
    var a = 1;     // a为局部变量
    console.log(a);  // 1
}
fn();
console.log(a);     // a is not defined  外部访问不到内部的变量

上面代码展现了在函数中声明的局部变量a在函数外部拿不到。但是咱们就想要在函数外拿到它,怎么办?下面就要看发挥闭包的威力了。

函数能够创造函数做用域,在函数做用域中若是要查找一个变量的时候,若是在该函数内没有声明这个变量,就会向该函数的外层继续查找,一直查到全局变量为止。

因此变量的查找是由内而外的,这也造成了所谓的做用域链。

var a = 7;
function outer() {
    var b = 8;
    function inner() {
        var c = 9;
        alert(b);
        alert(a);
    }
    inner();
    alert(c);   // c is not defined
}
outer();    // 调用函数

仍是最开始的函数,利用做用域链,咱们试着去拿到a,改造一下fn函数:

function fn() {
    var a = 1;     // a为局部变量
    return function() {
        console.log(a);
    }
}
var fn2 = fn();
fn2();      // 1

理解了变量做用域,顺着这条做用域链,再来回顾一下闭包的定义:**闭包就是可以读取其余函数内部变量的函数,实质是变量的解析过程(由内而外) **

变量生命周期

理解了变量做用域,再来看看变量的生命周期,直白一点就是它能在程序中存活多久。

  • 对于全局变量而言,它的生命周期机就是永久的,除非咱们手动销毁它(这一点也是颇有必要的,防止内存溢出)
  • 对于在函数中经过var声明的变量而言,就没那么幸运了。当函数执行完毕后,它也就没什么利用价值了,随之被浏览器的垃圾处理机制当垃圾处理掉了
    好比下面这段代码:
var forever = 'i am forever exist'  // 全局变量,永生
function fn() {
    var a = 123;    // fn执行完毕后,变量a就将被销毁了
    console.log(a);
}
fn();

函数执行完毕,内部的变量a就被无情的销毁了。那么咱们有没有办法拯救这个变量呢?答案是确定的,救星来了——闭包

闭包的建立

function outFn() {
    var i = 1;
    function inFn () {
        return ++i
    }
    return inFn;
}
var fn = outFn(); // 此处建立了一个闭包
fn();   // 2
fn();   // 3
fn();   // 4

上面的代码建立了一个闭包,有两个特色:

  1. 函数inFn嵌套在函数outFn内部
  2. 函数outFn返回内部函数inFn

在执行完var fn = outFn();后,变量 fn 其实是指向了函数 inFn,再执行 fn( ) 后就会返回 i 的值(第一次为1)。这段代码其实就建立了一个闭包,这是由于函数 outFn 外的变量 fn 引用了函数 outFn 内的函数inFn。也就是说,当函数 outFn 的内部函数 inFn 被函数 outFn 外的一个变量 fn 引用的时候,就建立了一个闭包(函数内部的变量 i 被保存到内存中,不会被当即销毁)。

参考连接:
闭包的建立
闭包和内存

高阶函数

定义:高阶函数就是接受函数做为参数或者返回函数做为输出的函数。

下面分两种状况讲解,搞清这两种应用场景,这将有助于理解并运用高阶函数。

函数做为参数传入

函数做为参数传入最多见的就是回调函数。例如:在 ajax 异步请求的过程当中,回调函数使用的很是频繁。由于异步执行不能肯定请求返回的时间,将callback回调函数当成参数传入,待请求完成后执行 callback 函数。

$.ajax({
  url: 'http://musicapi.leanapp.cn/search',  // 以网易云音乐为例
  data: {
      keywords
  },
  success: function (res) {
      callback && callback(res.result.songs);
  }
})

函数做为返回值输出

函数做为返回值输出的应用场景那就太多了,这也体现了函数式编程的思想。其实从闭包的例子中咱们就已经看到了关于高阶函数的相关内容了。

还记得在咱们去判断数据类型的时候,咱们都是经过Object.prototype.toString来计算的,每一个数据类型之间只是'[object XXX]'不同而已。

下面咱们封装一个高阶函数,实现对不一样类型变量的判断:

function isType (type) {
    return function (obj) {
        return Object.prototype.toString.call(obj) === `[object ${type}]
    }
}

const isArray = isType('Array'); // 判断数组类型的函数
const isString = isType('String'); // 判断字符串类型的函数
console.log(isArray([1, 2]); // true
console.log(isString({});  // false

参考连接:
高阶函数,你怎么那么漂亮呢!
简明 JavaScript 函数式编程——入门篇

总结

最后总结一下此次的重点:纯函数、变量做用域、闭包、高阶函数。

  1. 纯函数的定义:给定的输入返回相同的输出的函数。
  2. 变量做用域是闭包的实质。根据变量做用域向上查找的特性,闭包能够缓存变量到内存中,函数执行完毕不会当即销毁。
  3. 高阶函数的核心是闭包,利用闭包缓存一些将来会用到的变量,能够实现柯里化、偏应用...

下一节介绍柯里化、偏应用、组合、管道...

相关文章
相关标签/搜索