跳至主要內容

闭包

wzCoding大约 5 分钟JavaScript闭包

闭包

我们会在开发某些需求时用到 闭包,它可以通过函数来访问或保存一些局部变量,这种特性在某些情况下会非常有用

什么是闭包

闭包(closure) 就是在函数内部嵌套了函数,并且这个嵌套的函数引用了它外部的变量(词法环境),嵌套的函数与它引用的变量(词法环境)共同组合就形成了闭包

function foo(){
    let name = "foo";  //name是在foo函数内部定义的一个局部变量 
    function getName(){  //getName函数是在foo函数内部定义的一个函数
        console.log(name)  //getName函数内部引用了外部函数foo中定义的变量,形成了闭包
    }
    getName();
}
foo();  //foo

上面的例子就是一个典型的闭包

function baz(){
    let name = "baz";
    function getName(){
        console.log("baz");
    }
    getName();
}
baz(); //baz

在这个例子中,虽然在函数 baz 内部定义了函数,但是内部的 getName 函数并没有引用外部的变量,就无法形成闭包

闭包的作用

闭包的作用主要有以下两点:

  • 隔离或隐藏变量,防止变量被全局污染
  • 读取函数内部定义的变量

避免变量污染

let num = 1;  //定义了一个全局变量num
function add(){
    num++;
    console.log(num);
}
add();  //2
add();  //3

在上面的代码中,我们定义了一个全局变量 num 与一个能够使 num 增加的函数 add ,虽然每次调用 add 方法时都会使 num 的值增加,但是如果也有其他的函数中同样引用了全局变量 num ,那么这样使用全局变量就会变得十分不安全,所以我们可以使用闭包来修改上面的方法,避免变量污染

let num = 1;  //定义了一个全局变量num
function add(){
    let val = num;   //将num的值赋值给add函数内部定义的变量val
    function plus(){  //定义一个新的函数plus来使val增加(此时形成了闭包)
        val++;
        return val;
    }
    //将plus函数作为add函数的结果返回,这样就可以将val的值保持在内存中,也可以从外部访问到val的值了
    return plus;  
}
const newAdd = add();
console.log(newAdd());  //2 //实际上运行的是plus函数
console.log(newAdd());  //3
console.log(num); //1   //全局变量num并没有受到干扰

模拟私有方法

const car = function (){         //定义一辆汽车
    let speed = 100;    //定义汽车速度相关的属性和方法
    function speedChange(_speed){
        speed += _speed;
    }
    return {
        speedUp:function(up){  //汽车加速方法
             speedChange(up);
        },
        slowDown:function(slow){  //汽车减速方法
            speedChange(-slow);
        },
        speedNow:function(){  //汽车当前速度
            return speed;
        }
    }
}

const car1 = car();  // speed:100
const car2 = car();  // speed:100

car1.speedUp(60);    // 160
car2.speedUp(100);   // 200

car1.slowDown(80);  // 80
car2.slowDown(50);  // 150

console.log(car1.speedNow());  // 80
console.log(car2.speedNow());  // 150

在上面的例子中我们通过 car 函数创建了两个“汽车”对象,虽然都是通过同一个函数创建的对象,但是它们各自都有自己的“速度”,并且各自操作“速度”属性不会相互干扰,是因为我们在 car 函数内部创建了闭包 speedChange函数(通过 speedChange 函数来模拟 car 的私有方法),因为闭包都是引用它自身词法环境中的变量 speed,所以它们相互独立,互不干扰

闭包的缺陷

闭包并非完全 “封闭” 的,在某些情况下,我们还是可以通过原型链来访问到闭包中的变量,比如:

// 一个闭包,屏蔽了对象 obj
function closure() {
    const obj = {
      a: 1,
      b: 2
    }
    return function (key) {
      return obj[key]
    }
  }
  const closureFn = closure()
  console.log(closureFn('a'))  //1
  console.log(closureFn('b'))  //2
  Object.defineProperty(Object.prototype, 'private', {
    get() {
      return this
    }
  })
  
  const private = closureFn('private')
  private.c = 3
  console.log(closureFn('c'))
  console.log(private)

上面示例中的函数是一个典型的闭包,我们似乎只能通过 closureFn 函数来访问到 obj 的属性,但是我们还是可以通过原型链来访问到 obj 对象本身,所以,稍不注意,闭包也不是完全封闭的,访问 obj 对象本身的代码如下:

function closure() {
    const obj = {
      a: 1,
      b: 2
    }
    return function (key) {
      return obj[key]
    }
  }
  const closureFn = closure()
  console.log(closureFn('a'))  //1
  console.log(closureFn('b'))  //2

  Object.defineProperty(Object.prototype, 'inner', {
    get() {
      return this
    }
  })
  
  const inner = closureFn('inner') //{a: 1, b: 2,inner: {a: 1, b: 2}}
  inner.c = 3
  console.log(closureFn('c')) // 3
  console.log(inner)  //{a: 1, b: 2, c:3,inner: {a: 1, b: 2, c:3}}

想要避免上面代码的问题,就要防止原型链上的操作,在操作闭包中的变量时将原型链先屏蔽掉,具体代码如下:

function closure() {
    const obj = {
      a: 1,
      b: 2
    }
    // 将obj的原型对象设置为null,屏蔽原型链
    // Object.setPrototypeOf(obj, null)
    return function (key) {
      //判断key是否是obj自身的属性,如果是则返回obj的属性值
      if(obj.hasOwnProperty(key)){
         return obj[key]
      }
      return undefined
    }
  }

闭包虽然可以使我们能够很方便的将变量保存在局部作用域中,避免了全局污染,但是它也会消耗内存,严重时甚至可能造成内存泄露的问题

通过前面的 执行上下文 章节我们知道函数在运行时会在 中创建自己的函数执行上下文,在函数运行完成后出栈,但是如果在函数内部创建了闭包,外部函数的变量被闭包引用,那么就会导致外部函数的执行上下文保持在栈中,并且外部函数中的变量也无法被垃圾回收机制销毁。试想一下,栈的内存容量使有限的,如果过多的使用闭包,可能就会造成栈内存被过多的占用从而导致内存泄露的问题