【JavaScript ES6 函数式编程入门经典】读书笔记(第四章)

第四章 闭包与高阶函数

4.1 理解闭包

简而言之,闭包就是一个内部函数,是在另一个函数内部的函数,比如

function outer() {
  function inner() {
  }
}

这就是闭包,函数inner称为闭包函数,闭包如此强大的原因在于它对作用域链(或作用域层级)的访问。
闭包有3个可访问的作用域:

  • 1.在它自身声明之内声明的变量
function outer() {
  function inner() {
    let a = 5;
    console.log(a)
  }
  inner() // 调用inner函数
}

// tips: inner函数在outer函数的外部是不可见的

当inner函数被调用时,控制台返回5,因为闭包函数可以访问所有在其声明内部声明的变量。

  • 2.对全局变量的访问
    现将上面的代码片段修改为:
let global = "global"
function outer() {
  function inner() {
    let a = 5
    console.log(global)
  }
  inner() // 调用inner函数
}

现在当inner函数执行后,打印出变量global,如此,闭包就能访问全局变量了

  • 3.对外部函数变量的访问
    再修改一下函数
let global = "global"
function outer() {
  let outer = "outer"
  function inner() {
    let a = 5
    console.log(outer)
  }
  inner() // 调用inner函数
}

当inner函数执行后,打印出变量outer,看起来是合理的,但却是一个非常重要的闭包属性。
闭包能够访问外部函数的变量,此处外部函数的含义是包裹闭包函数的函数。

闭包可以访问外部函数的参数,比如将outer函数添加一个参数,在inner函数中尝试访问它,是可以拿到该参数的。

闭包还有一个重要概念: 闭包可以记住它的上下文

var fn = (arg) => {
  let outer = "Visible"
  let innerFn = () => {
    console.log(outer)
    console.log(arg)
  }
  return innerFn
}

var closureFn = fn(5)
closureFn()
=>Visible
=>5

解析
1.var closureFn = fn(5),这里fn被参数5调用,它返回了innerFn,
2.当innerFn被返回时,javascript执行引擎视innerFn为一个闭包,并且相应地设置了它的作用域
闭包有3个作用于层级,这3个层级(arg、outer值将被设置到inner的作用域层级中)在innerFn返回时都被设置了,如此closureFn通过作用域链被调用时就记住了arg、outer值。
3.最后调用closureFn时,因为它已经记住了它的上下文(作用域,也就是outer和arg),因此对console.log的调用才能正确打印出结果。

回顾上章的sortBy函数

const sortBy = (property) => {
  return (a, b) => {
    var result = (a[property] < b[property]) ? -1 : (a[property] > b[property]) ? 1 : 0
    return result
  }
}

当我们以如下方式调用sortBy时,sortBy("firstname")
发生了以下事情:
sortBy函数返回了一个接受两个参数的新函数
(a, b) => { /* 实现 */ }
闭包的返回函数能够范围sortBy函数的参数property,该函数只有在sortBy被调用时才会返回。而这时property参数会被替换为一个值。因此返回函数将在其生命周期中持有该上下文:
// 通过闭包持有的作用域
property = "passedValue"
(a, b) => { /* 实现 */ }
由于返回函数在它的上下文中持有property的值,所以它将在合适并且需要的时候使用返回值。

4.2 真实的高阶函数

  • tap函数
const tap = (value) =>
  (fn) => (
    typeof(fn) === 'function' && fn(value),
    console.log(value)
  )
// tap函数接受一个value,并返回一个包含value的闭包函数,该函数将被执行。

javascript中,(exp1, exp2)的含义是它将执行两个参数并返回第二个表达式的结果,即exp2。

运行tap函数
tap("fun")((it) => console.log("value is ", it))
=> value is fun
=> fun
那么这个函数有什么用处,假设遍历一个来自服务器的数组,并发现数据错了,想调试一下,看看数组内究竟包含了什么,会如何做?抛弃命令式的方法,用函数是的方法来看,这正是使用tap函数,对于此场景可以这样做

forEach([1, 2, 3], (a) =>
  tap(a)(()=>{
    console.log(a)
  })
)
  • unary函数
    array原型有一个默认的方法称为map,map是一个与之前定义的forEach函数非常相似的函数(遍历),唯一的区别是map返回了回调函数的结果。
    假设要使一个数组加倍并得到结果,可以使用map函数以如下方式实现:
    [1, 2, 3].map((a) => { return a * a })
    =>[1, 4, 9]
    这里map用3个参数调用了函数,分别是element、index和arr,假设要把字符串数组解析为整数数组,有一个内置的函数叫做parseInt,它接受两个参数parse和radix,如果可能,该函数会把传入的parse转换成数字。如果把parseInt传给map函数,map会把index的值传给parseInt的radix参数,这将产生意想不到的行为。
['1', '2', '3'].map(parseInt)
=> [1, NaN, NaN]

这个结果并不是我们期望的,我们需要把parseInt函数转换成为另一个只接受一个参数的函数。
用unary函数可以做到,它的任务是接受一个给定的多参数函数,并把它转换成一个只接受一个参数的函数,如下:
const unary = (fn) =>
  fn.length === 1
    ? fn
    : (arg) => fn(arg)

检查传入的fn是否有一个长度为1的参数列表(可通过length属性查看),有就什么也不做;没有就返回一个新函数,只接收一个参数arg,并且该参数调用fn,重新运行上面的问题。
['1', '2', '3'].map(unary(parseInt))
=> [1, 2, 3]
此处unary函数返回了一个新函数(parseInt的克隆体),它只接收一个参数,如此map函数传入的index、arr参数就不会对程序产生影响。

也有像binary一样的函数,它们转换函数,使其接受相应的参数。

  • once函数
    只运行一次给定的函数,比如只想设置一次第三方库,或者初始化一次支付设置。
const once = (fn) => {
  let done = false
  return function () {
    return done ? undefined : (done =true), fn.apply(this, arguments)
  }
}

接受一个参数fn,并且通过调用它的apply方法返回结果。声明了一个done变量,初始值为false, 返回的函数会形成一个覆盖它的闭包作用域,因此,返回的函数会访问并检查done是否为true,是则返回undefined,否则将done设置为true(阻止了下一次执行),并用必要的参数调用fn

apply函数允许设置函数的上下文,并为给定的函数传递参数。

var doPayment = once(() => {
  console.log("Payment is done")
})
doPayment()
=> Payment is done
doPayment()
=> undefined
  • memoized函数
    假设有一个纯函数名为factorial,计算给定数字的阶乘
var factorial = (n) => {
  if (n === 0) {
    return 1
  }
  // 递归
  return n*facotrial(n-1)
}

该函数只依赖它的参数执行,其它不需要。有一个局限性,无法重用之前的计算结果,memoized函数能够记住其计算结果

const memoized = (fn) => {
  const lookupTable = {}
  return (arg) => lookupTable[arg] || (lookupTable[arg] = fn(arg))
}

声明lookupTable的局部变量,它在返回函数的闭包上下文中,返回函数将接受一个参数并检查它是否在lookupTable中,如果在则返回对应的值(lookupTable[arg]);否则,使用新的输入作为key,fn的结果作为value,更新lookupTable对象(lookupTable[arg] = fn(arg))
现在将上面的factorial用memoized函数改写

let fastFactorial = memoized((n) => {
  if (n === 0) return 1
  // 递归
  return n *fastFactorial(n-1)
})

调用fastFactorial
fastFactorial(5)
=> 120

它以同样的方式运行,但是比之前快得多,运行fastFactorial时,会检查lookupTable对象。

附上
第三章地址:高阶函数
第二章地址:JavaScript函数基础
第一章地址:函数编程简介

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容