JavaScript闭包的原理与缺陷

闭包的原理

闭包是指有权访问另一个函数作用域中的变量的函数。根据下面的代码示例来理解什么是闭包,在add函数内部的匿名函数中,访问到外部函数的变量outerArg,在执行add(10)之后外部函数返回了,并且将内部的匿名函数赋值给了变量addTen,此时通过addTen调用函数,依然可以访问到outerArg,也就是10。这个闭包中的变量,只能通过调用addTen函数访问,无法通过其他渠道访问到,下面代码最后一行通过输出属性的方式尝试访问结果是输出undefined。outerArg是属于add函数的作用域中的变量,addTen有权访问add函数作用域中的变量,因此addTen是一个闭包。闭包产生的本质是:在一个函数(外部函数)内部定义的函数(内部函数)会将外部函数作用域中的活动对象添加到自己的作用域链中,下面代码中inner函数将add函数的outerArg添加到自己的作用域链上。在add函数执行完之后,其执行环境会被销毁,但由于inner函数还在引用outerArg,所以outerArg不会被销毁,依然保留在inner函数的作用域链中。直到inner函数(addTen函数)被销毁之后,outerArg才会跟着其作用域链一起被销毁。由于闭包变量是位于作用域链上,因此必须调用闭包函数进入其作用域之后才能访问到闭包变量。

function add(outerArg) {
  function inner(innerArg) {
    return innerArg + outerArg;
  }
  return inner;
}

var addTen = add(10);
console.log(addTen(1)); // 输出11
console.log(addTen(2)); // 输出12
console.log(addTen(3)); // 输出13
console.log(addTen.outerArg); // undefined

闭包的缺陷

首先,闭包会将外部函数的活动对象都添加到自己的作用域链中,因此相对于普通的函数会更加耗费内存。

其次,闭包只能获取到外部函数中任何变量的最后一个值。如下面代码所示,在for循环中的匿名函数可以访问到闭包变量i,但是由于闭包所保存的是整个变量对象,因此所有闭包函数中访问到的变量i其实就是同一个变量i(Outer函数的变量i),而Outer函数在执行完毕后,其变量i的值为5,所以5个闭包函数访问到的值都是5。

function Outer() {
  var arr = new Array();
  for (var i = 0; i < 5; ++i) {
    arr[i] = function() {
      return i;
    };
  }
  return arr;
}

var arr = Outer();
console.log(arr[0]()); // 输出5
console.log(arr[1]()); // 输出5
console.log(arr[2]()); // 输出5
console.log(arr[3]()); // 输出5
console.log(arr[4]()); // 输出5

最后,this对象的指向可能与预期的不一致。以下面代码为例,getName的执行结果是输出Window而不是someObj,原因就是getNameFunc函数返回之后,它的执行环境会被销毁,返回的函数赋值给getName,当我执行getName的时候执行环境实在全局环境下,this指向的对象是window,this.name引用到的是全局作用域下的name,也就是’Window’。

var name = 'Window';
var someObj = {
  name: 'someObj',
  getNameFunc: function() {
    return function() {
      return this.name;
    }
  }
};
var getName = someObj.getNameFunc();
console.log(getName()); // 输出'Window'

闭包缺陷的解决方案

对于闭包只能获取到外部函数中任何变量的最后一个值问题,可通过定义一个立即执行函数(IIFE)来解决。这种方法的原理其实是在执行立即调用函数时,传入变量i作为参数,而i是按值传递的,相当于复制了一次i的值,所以5次循环调用了5次函数复制了5个不同的i的值创建了5个值不同的变量num,而内部函数(这里指代码中注释inner处的函数)的闭包变量不在是引用i,而是引用了变量num。每个内部函数都有其对应的闭包变量num,这个时候闭包函数的行为就符合我们的预期效果了。

function Outer() {
  var arr = new Array();
  for (var i = 0; i < 5; ++i) {
    arr[i] = function(num) {
      // inner
      return function() {
        return num;
      }
    }(i);
  }
  return arr;
}

var arr = Outer();
console.log(arr[0]()); // 输出0
console.log(arr[1]()); // 输出1
console.log(arr[2]()); // 输出2
console.log(arr[3]()); // 输出3
console.log(arr[4]()); // 输出4

对于this对象问题,可以通过避免直接使用this对象的方式来解决。这种方式其实是给this对象起了个别名,用that指向外部函数作用域的this对象,然后在内部函数中引用that,将其加入到闭包中,这样就可以正确地访问到外部函数作用域的this对象了。

var name = 'Window';
var someObj = {
  name: 'someObj',
  getNameFunc: function() {
    var that = this; // 别名
    return function() {
      return that.name;
    }
  }
};
var getName = someObj.getNameFunc();
console.log(getName()); // 输出'someObj'

最后,分享一个实用的闭包内存优化技巧。闭包会引用外部函数的整个活动对象,这种机制可能会导致保存多余的变量而造成内存浪费,以下面代码为例,内部函数仅仅引用了obj对象的id属性,而闭包会把整个obj对象都保存下来,如果该对象上有很多耗费内存的属性,那么这种简单的引用方式会导致产生一个很大的闭包。优化方式如AnotherOuter函数的写法所示,关键在于把需要引用的变量保存下来,然后通过把obj对象设置为null让它丢失引用以便被自动内存回收机制回收处理,这样,闭包函数引用的就只有id这个变量了。

function Outer() {
  var obj = {};
  obj.id = '12345678';
  obj.name = 'aha';
  // ... 假设经过很多处理过程,最后obj上带有很多属性
  return function() {
    return obj.id;
  };
}

function AnotherOuter() {
  var obj = {};
  obj.id = '12345678';
  obj.name = 'aha';
  // ... 假设经过很多处理过程,最后obj上带有很多属性
  var id = obj.id;
  obj = null;
  return function() {
    return id;
  };
}

你可能感兴趣的:(JavaScript)