JS Advance --- 高阶函数 和 闭包

249 阅读6分钟

高阶函数

函数是一等公民

函数可以作为另外一个函数的参数,也可以作为另外一个函数的返回值来使用

当一个函数中存在接收另一个函数作为参数或者将某一个函数作为函数的返回值的时候,这个函数就被称之为高阶函数

// 接收函数作为参数
function calc(num1, num2, fn) {
  // 直接使用fn.toString()或String(fn)返回的结果是使用字符串形式表示的函数体
  if (Object.prototype.toString.call(fn) === '[object Function]') {
    return fn(num1, num2)
  }

  console.error('fn is not a function')
}

console.log(calc(10, 20, (num1, num2) => num1 + num2)) // => 30
console.log(calc(10, 20, (num1, num2) => num1 - num2)) // => -10
console.log(calc(10, 20, (num1, num2) => num1 * num2)) // => 200
// 函数作为函数的返回值
function add(count) {
  return num => num + count
}

console.log(add(5)(10)) // => 15
console.log(add(10)(10)) // => 20

数组的高阶函数

filter --- 过滤函数

  1. 参数是一个回调函数,数组有有几个元素,这个回调函数就会执行几次,也就是会为每一个数组元素执行传入的回调
  2. 这个callback需要返回一个boolean值,如果为true的时候,当前数组元素会被加入到新的数组中
  3. 这个callback接收三个参数(item, index, arr), 分别为(当前元素项,当前索引值,当前数组)
// 取出数组中所有的偶数,组成一个新的数组
console.log([23, 44, 67, 22, 35, 64, 33].filter(v => v % 2 === 0))

map --- 映射函数

  1. 参数是一个回调函数,数组有有几个元素,这个回调函数就会执行几次,也就是会为每一个数组元素执行传入的回调
  2. 这个callback接收三个参数(item, index, arr), 分别为(当前元素项,当前索引值,当前数组)
  3. callback的返回值会被加入到新的数组中
// 数组的每一项都翻倍
console.log([23, 44, 67, 22, 35, 64, 33].map(item => item * 2))

forEach --- 遍历函数

  1. 参数是一个回调函数,数组有有几个元素,这个回调函数就会执行几次,也就是会为每一个数组元素执行传入的回调
  2. 这个callback接收三个参数(item, index, arr), 分别为(当前元素项,当前索引值,当前数组)
// 遍历打印数组的每一项
[23, 44, 67, 22, 35, 64, 33].forEach(item => console.log(item))

find / findIndex --- 查找函数

  1. 参数是一个回调函数,该回调函数需要返回一个boolean值,当返回值为true的时候,结束find/findIndex函数的执行

    • find函数会将当前callback的item作为返回值返回
    • findIndex函数会将当前callback的index作为返回值返回
  2. 这个callback接收三个参数(item, index, arr), 分别为(当前元素项,当前索引值,当前数组)

  3. find/findIndex只能查找到第一个满足条件的值,无法查找到所有满足条件的值

const arr = [23, 44, 67, 22, 35, 64, 33]

console.log(arr.find(item => item % 2 ===  0)) // => 44
console.log(arr.findIndex(item => item % 2 ===  0)) // => 1

reduce --- 累加函数

reduce函数接收两个参数

  • 参数1 为回调函数
    • 回调函数接收两个参数: 之前的值(prevValue)和当前值(currentItem)
    • 当前callback的返回值会作为下一个callback的prevValue的值
    • 如果是最后一个callback,那么该callback的返回值会直接作为最终的返回值返回
  • 参数2 为 初始值(inital value)
    • 如果没有设置初始值的时候,默认的初始值是0
    • 初始值会作为第一个callback的prevValue的值
// 求和
// arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
console.log([23, 44, 67, 22, 35, 64, 33].reduce((prev, item) => prev + item, 0))

闭包

一个普通的函数,如果它可以访问外层作用域的自由变量,那么这个函数就是一个闭包

如果在某个作用域中使用了变量“a”,而变量“a”并未在该作用域中声明(在其它作用域中声明了),则该变量“a”即为自由变量

闭包(closure)这个特性并不仅仅存在于JS中,只要是函数作为一等公民的语言中都存在闭包这个特性

从广义的角度来说: JavaScript中的函数都是闭包

// 在广义上讲,一个函数只要是可以访问到自由变量就可以构成闭包
// 所以foo这个函数也是一个闭包函数
function foo() {}

从狭义的角度来说: JavaScript中一个函数,如果访问了外层作用域的变量,那么它才是一个闭包

let name = 'coderwxf'

function printName() {
  // printName 是一个函数
  // printName访问了外层作用域的自由变量 name
  // 所以printName就会一个闭包
  console.log(name)
}

内存分析

function foo() {
  var name = "foo"
  var age = 18

  function bar() {
    console.log(name)
    console.log(age)
  }
  return bar
}

var fn = foo()
fn()

当此时代码执行完第12行var fn = foo()的时候,内存的示意图如下:

ItoYXC.png

此时,在全局GO中,有一个属性fn指向bar函数所对应的函数对象,而foo的AO对象存在于bar函数对象的[[ scope ]]属性中

所以foo函数的AO对象此时也有被引用,因此,虽然此时bar函数和foo函数的函数执行上下文被弹出栈了,

但是bar函数的函数对象和foo函数的AO对象因为依旧存在引用,所以并不会被GC移除,所以可以在第13行来调用fn函数

清除闭包

function foo() {
  var name = "foo"
  var age = 18

  function bar() {
    console.log(name)
    console.log(age)
  }
  return bar
}

var fn = foo()
fn()

fn = null

ItoYXC.png

如果此时在第13行fn()执行完毕以后,不需要在执行fn函数了,原则上fn函数应该被GC所移除

但是因为此时fn属性存在于GO上,被GO所引用,而GO会存在于整个的代码执行周期,所以闭包会一直存在,无法自动被GC销毁

但是内存容量是有限的,过多的不再使用的闭包会造成实际可使用内存空间的减少,这种情况就被称之为内存泄露

所以对于闭包,我们需要手动进行清除,即执行第15行代码所对应的操作,将闭包所对应的引用设置为null

null是JS中定义的一块特殊的内存地址(假设为0x0),当GO中对应的属性被设置为了null后,就意味着GO的fn属性指向了null所对应的哪块内存地址

不再指向bar函数所对应的函数对象了,此时GC就会自动在合适的时候,去回收bar函数所对应的函数对象和foo函数所对应的AO对象,从而移除我们不需要再使用的闭包