作用域和闭包

47 阅读4分钟

作用域和闭包

作用域

变量可分为全局变量和局部变量。全局变量的作用域就是全局性的,在 js 的任何地方都可以使用全局变量。 ES6 新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。

看一下下面的代码在一个块级作用域中let定义的变量在作用域外部是不能访问的,var则有变量提升

{
  let a = 100
  var b = 200
  
}
console.log(b); // 200 变量提升
console.log(a); // error

而在函数中,都是访问不到的,因为函数是一个作用域,不是它作用域下是访问不了的。而函数里面的变量为局部变量,在函数调用完成后,函数内的变量是会自动销毁的。

function fun() {
  let c = 100
  var d = 200
}

console.log(c); // error
console.log(d); // error

有了这个概念呢,我们再来看看一个例子:

let func = function () {
  let a = 'airhua';
  let func1 = function () {
    a += ' a';
    console.log(a);
  }
  return func1;
}

let func2 = func()
func2()
func2()
func2()

奇怪的是我们前面说的变量在函数调用完后会自动销毁,但是好像打印的结果和我们预想的有点不一样,在第一次调用完 func2 之后,func 中的变量 a 变成 ' airhua a',而没有被销毁。

其实就是此时 func1 形成了一个闭包,导致了 a 的生命周期延续了。

闭包

前面我们已经看到了产生闭包这个现象,那么什么是闭包呢?

  • 闭包是一个函数
  • 闭包可以使用在它外面定义的变量
  • 闭包存在定义该变量的作用域中

怎么理解呢,我们还是可以通过上面的例子来看一下:

  1. 闭包是一个函数,比如上面的 func1 函数

  2. 闭包使用其他函数定义的变量,使其不被销毁。比如上面 func1 调用了变量 a

  3. 闭包存在定义该变量的作用域中,变量 a 存在 func 的作用域中,那么 func1 也必然存在这个作用域中。

好了,可能看完还是有点不明白,那么我们可以详细看看几种闭包产生:

函数作为返回值

function create() {
  const a = 100
  return function () {
    console.log(a);
  }
}

const fn = create()
const a = 200
fn() // 100

函数作为参数被传递

function print(fn) {
  const b = 200
  fn()
}

const b = 100
function fn() {
  console.log(b);
}
print(fn) // 100

仔细观察上面的例子,可以发现所有的自由变量查找,是在函数定义的地方,往上级作用域查找,而不是在执行的地方。

经典例子

for (var i = 0; i < 4; i++) {
  setTimeout(function () {
    console.log(i)
  }, 0)
}

刷过题都知道,控制台打印结果为4 4 4 4,主要是因为setTimeout函数是异步的,等到函数去执行时for循环已经结束了,此时i的值为4,所以打印结果都是4,那么为了解决这个问题我们可以直接把var改为let,因为let会产生块级作用域。但是如果我们不使用let这个特性,而考虑使用闭包来改写把0 1 2 3保存起来应该怎么写呢?

当 i=0 时,把 0 作为参数传进匿名函数中,此时 function(i){} 此匿名函数中的 i 的值为 0,等到 setTimeout 执行时顺着外层去找 i,这时就能拿到 0。如此循环,就能拿到想要的 0 1 2 3。

for (var i = 0; i < 4; i++) {
  (function (i) {
    setTimeout(function () {
      console.log(i)
    }, 0)
  })(i)
}

闭包优缺点

前面我们总结了闭包的概念和例子后,这里总结一下优缺点:

优点:

  • 希望一个变量长期存储在内存中。
  • 避免全局变量的污染。
  • 私有成员的存在。

缺点:

  • 常驻内存,增加内存使用量。
  • 使用不当会很容易造成内存泄露。

私有成员

先来看看闭包实现私有成员,不能直接修改值,但可以自己写方法实现对外暴露来修改和赋值。

function createCache() {
  const data = {} // 私有成员
  return {
    set: function(key, value) {
      data[key] = value
    },
    get: function(key) {
      return data[key]
    }
  }
}

const boy = createCache()
boy.set('airhua', 20)
console.log(boy.get('airhua'));

内存管理

前面也提到闭包在使用不当会很容易造成内存泄露

在闭包中调用局部变量,会导致这个局部变量无法及时被销毁,相当于全局变量一样会一直占用着内存。如果需要回收这些变量占用的内存,可以手动将变量设置为null。

然而在使用闭包的过程中,比较容易形成JavaScript 对象和 DOM 对象的循环引用,就有可能造成内存泄露。这是因为浏览器的垃圾回收机制中,如果两个对象之间形成了循环引用,那么它们都无法被回收。

function func() {
  var test = document.getElementById('test');
  test.onclick = function () {
    console.log('hello airhua');
  }
}

在上面例子中,func 函数中用匿名函数创建了一个闭包。变量 test 是 JavaScript 对象,引用了 id 为 test的DOM 对象,DOM 对象的 onclick 属性又引用了闭包,而闭包又可以调用 test ,因而形成了循环引用,导致两个对象都无法被回收。要解决这个问题,只需要把循环引用中的变量设为 null 即可。

function func() {
  var test = document.getElementById('test');
  test.onclick = function () {
    console.log('hello airhua');
  }
  test = null
}