JS中闭包知多少

66 阅读6分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

闭包

JS中函数是一等公民

  • 在JavaScript中,函数是很重要的,并且是一等公民:用法非常灵活,可以作为另一个函数的参数,也可以作为另一个函数的返回值;

在JS的数组中,提供了一些高阶函数供我们使用:

  1. forEach:遍历数组中的元素
var counts = [1, 3, 9, 10, 15]

counts.forEach(function(value, index, arr){
  console.log(value, index, arr, this); 
  // 可以传入回调函数要绑定的this,默认会绑定为全局对象
}, counts)
// 输出结果:
// 1 0 [ 1, 3, 9, 10, 15 ] [ 1, 3, 9, 10, 15 ]
// 3 1 [ 1, 3, 9, 10, 15 ] [ 1, 3, 9, 10, 15 ]
// 9 2 [ 1, 3, 9, 10, 15 ] [ 1, 3, 9, 10, 15 ]
// 10 3 [ 1, 3, 9, 10, 15 ] [ 1, 3, 9, 10, 15 ]
// 15 4 [ 1, 3, 9, 10, 15 ] [ 1, 3, 9, 10, 15 ]
  1. map:根据原数组生成一个新数组
var counts = [1, 3, 9, 10, 15]

var newCounts = counts.map(function(value, index) {
  return value * 2 + index
})
console.log(newCounts); // [ 2, 7, 20, 23, 34 ]
  1. filter:对原数组进行过滤生成一个新数组
var counts = [1, 3, 9, 10, 15]

var evenCounts = counts.filter(function (value) {
  return value % 2 === 0
})
console.log(evenCounts); // [10]
  1. find/findIndex:查找数组符合条件的第一个元素/下标
var counts = [1, 3, 9, 10, 15]

var num = counts.find(function(value) {
  return value > 5
})

console.log(num); // 9
  1. reduce:累加器,返回一个最终的计算结果
var counts = [1, 3, 9, 10, 15]

var sum = counts.reduce(function(preValue, curValue, curIndex, arr) {
  console.log(preValue, curValue);
  return preValue + curValue
}, 10)

console.log(sum); // 48

所谓的高阶函数,它要满足以下其中一个条件:

  • 接收一个或多个函数作为参数;
  • 输出一个函数

高阶函数的执行过程

有下面这样一段全局代码:

function foo() {
  function bar() {
    console.log("bar")
  }

  return bar
}

var fn = foo()
fn()

那么它是如何被JS引擎解析执行的呢?

上图描述了代码在被执行前的解析阶段,此时堆内存中会有一个GO对象,该对象中有全局代码中声明的属性:foo,fn;其中foo是通过函数声明所声明的一个函数,所以它会在堆内存中创建出一个函数对象,但不会堆函数中的内容进行完全解析(只会进行预解析),GO中的foo会保存这个函数对象的内存地址;而fn则是一个变量,由于此时代码还未执行,因此此时它的值为undefined。完成解析后,将开始执行代码,ECS中压入GEC,GEC中的VO指向的是GO,开始执行代码。

开始执行全局的代码:var fn = foo(),执行foo函数,此时会在ECS中压入foo函数的FEC,在这个FEC中的VO指向的是foo函数的AO,此时会在foo函数进行真正的解析,即AO中会有一个bar属性并且指向bar函数的内存地址;执行foo函数的结果是返回bar函数的内存地址,并将这个执行结果赋给GO中的fn,此时foo函数的FEC会出栈;执行下一行代码:fn(),此时ECS中会压入fn函数执行的FEC,重复类似foo函数执行的过程。

JS中闭包的定义

  • 在计算机科学中:
    • 闭包(Closure),又称词法闭包(Lexical Closure)或函数闭包(Function Closures);
    • 是在支持头等函数的编程语言中,实现词法绑定的一种技术;
    • 闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表);
    • 闭包跟函数最大的区别在于,当捕获闭包时,它的自由变量会在捕获时被确定,这样即使脱离了捕获时的上下文,它也能照常运行;

词法闭包:在词法解析阶段就形成了闭包

函数闭包:通过函数的方式来实现闭包

  • MDN对闭包的解释:
    • 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure);
    • 也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域;
    • 在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来;
  • 理解和总结:
    • 一个普通的函数function,如果它可以访问外层作用域的自由变量,那么这个函数就是一个闭包;
    • 从广义的角度来说:JavaScript中的函数都是闭包;
    • 从狭义的角度来说:JavaScript中一个函数,如果访问了外层作用域的变量,那么它就是一个闭包;

闭包的访问过程

下面这段代码将会形成闭包:

return function(){ } // 每次返回时都会创建一个新的函数对象

function makeAdder(count) {
  return function (num) {
    return count + num
  }
}

var add10 = makeAdder(10)
console.log(add10(5))

闭包的执行过程

上面代码执行结束之后,正常来说,makeAdder函数执行完毕了,对应的AO对象会被释放;但是,因为在0xb00的函数中有作用域引用指向了这个AO对象中count,所以它不会被释放掉;

闭包的内存泄漏

在上面的案例中,如果后续我们不再使用add10函数了,那么该函数对象应该要销毁掉,并且其引用的父作用域AO也应该销毁掉;

但是目前因为在全局作用域下add10变量对0xb00的函数对象存在引用,而0xb00的作用域中对父级AO(0x200)存在引用,所以最终会造成这些内存无法被释放,这样导致了内存泄漏;

如何解决这个问题呢?

  • 通过删除GO中对这个函数对象的引用,那么对应的AO对象0x200就会变成从根节点不可达了;
  • 在GC的下一次检测中,它们就会被销毁掉;
add10 = null

将对象设置为null,并不会立即被GC回收(GC内部根据相关算法再进行回收)

闭包的内存泄漏测试

function testArray() {
  var arr = new Array(1024* 1024).fill(1)
  return function() {
    console.log(arr.length);
  }
}

var arrFns = []
for(var i = 0; i < 100; i++) {
  setTimeout(() => {
    arrFns.push(testArray())
  }, 100*i)
}

setTimeout(() => {
  for(var i =0; i < 50; i++) {
    setTimeout(()=> {
      arrFns.pop()
    }, 100*i)
  }
}, 10000)

AO不使用的属性

当存在闭包时,我们知道闭包所引用的AO对象不会被销毁,那么是否里面的所有属性都不会被释放呢?

有下面一段代码:

function foo() {
  var name = 'loftyamb'
  var age = 24
  return function() {
    debugger
    console.log(name);
  }
}

var bar = foo()
bar()

在浏览器中运行,但运行到断点时:

此时的控制台的执行上下文是在当前所执行的函数的上下文,我们在控制台中分别打印:name,age:

这说明对于闭包引用的AO对象中没有使用到的属性,是会被释放掉的。

在ECMA规范中,若某个AO存在被引用,则它的属性都不会被销毁;但对JS引擎的具体实现(比如:V8引擎),则会将一个被引用的AO中没有被使用到的属性进行删除。