dudu嘟嘟


js闭包从哪来到哪去

JS有几个老生常谈的基础问题,只要稍微有段时间不接触就会忘掉,其中一个需要注意的就是闭包问题。很多资料都会在js基础篇提下闭包,但是大多都是一带而过,来龙去脉络都没有讲清,而剩下的则是浓浓的学院派说法,绕几圈作者自己都会晕掉。

直到我在读到了这篇文章closures,它把闭包的种种都讲的很清晰,示例也尤其好。我以这篇文章为主体,加入了一些自己的理解,形成本文,还望指正。 ##闭包是如何形成的 网上有一种说法是说函数内嵌套的函数就是闭包,确实如此,但是对于初学者来说,这种认识会刻意增大理解闭包的难度。我觉得是不是闭包应该按照函数的作用来区分,而不是形式。简单来说通常闭包的作用是保存作用域中的局部变量(内部函数保存外部函数的变量和参数),以防止变量在函数执行完成后释放。

function init() {
  var name = "Mozilla"; // name is a local variable created by init
  function displayName() { // displayName() is the inner function, a closure
    alert(name); // use variable declared in the parent function
  }
  displayName();
};
init();

在init函数中name是局部变量,display是其的一个内部方法。根据js的作用域规则,displayName可以访问到name,同时如果在displayName中定义一个变量,init是无法访问的。

现在我们改写一下这个函数:

function makeFunc() {
  var name = "Mozilla";
  function displayName() {
    alert(name);
  }
  return displayName;
};

var myFunc = makeFunc();
myFunc();

myFunc()执行的就是displayName()函数,此时我们发现makeFunc函数已经执行过了,它的变量应该已经被释放才对,但是执行结果却返回了Mozilla

这是由于displayName()构成了一个闭包,因为它引用了makeFunc的变量,在释放makeFunc函数时这个变量被放到另外一个位置缓存下来,供displayName使用。

我们再看一个稍微复杂一点的例子:

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
};

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

大多数人在写函数的都时候,应该都不会这么写,因为很不直观。不过作为一个闭包的例子来说却恰好在一个侧面体现了闭包的强大。可以看到makeAddr接收一个参数,并返回一个新的方法,新方法又接收另外一个参数,返回两个参数的和。

也就是说makeAddr是一个工厂方法,由它生成的add5add10是两个闭包,这两个函数把传入的不同的x值保存了下来.

##闭包可以用来做什么 闭包为我们提供了通过函数操作数据的方法,这与面向对象的思路是很相似的。因此我们可以把闭包应用在很多地方.

###1)工厂方法

工厂方法是一种比较基础的设计模式,在面向对象中工厂方法用于创建对象的接口,让子类决定实例化哪一个类,它把类的实例化过程延迟到子类,子类可以重写接口方法以便在创建的时候指定自己的对象类型。

上面的的例子已经在一定程度上体现了闭包的强大,下面来看一个更为典型的例子。

假设我们想要在页面中添加能够控制页面文本大小的按钮,可以指定<doby>元素的大小,然后使用em单位设置其他元素在页面中的相对大小。

body {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 12px;
}

h1 {
  font-size: 1.5em;
}

h2 {
  font-size: 1.2em;
}

通过改变body元素上font-size的大小,我们就能够自适应其他元素,典型的rem布局也是利用了这种方式,下面是控制body font-szie的js代码:

function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + 'px';
  };
};

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

通过给按钮click绑定不同的事件,就可以控制body的字体大小。 ###2)使用闭包实现私有方法 通常面向对象语言都提供了私有方法,这种方法可以保护私有变量不受污染。javascript本身并没有提供私有方法,但是使用闭包可以模拟一种实现。私有方法不仅可以控制代码访问,还是管理全局命名空间的一种方式,它使你的代码逻辑更清晰,不混乱。

下面我们来看一个通过公有函数访问私有方法和变量的闭包实例

通常也被称为模块模式:具有模块化,可重用,封装了变量和function,和全局的namespace不接触,松耦合,只暴露可用的public方法,其他私有方法全部隐藏。具体可以参考http://www.cnblogs.com/TomXu/archive/2011/12/30/2288372.html

var counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  };
})();

console.log(counter.value()); // logs 0
counter.increment();
counter.increment();
console.log(counter.value()); // logs 2
counter.decrement();
console.log(counter.value()); // logs 1

1)中例子中每一个的闭包都拥有自己的变量环境,而本例中三个方法:counter.increment,counter.decrement,couter.value共享一个变量环境。

共享的变量环境在匿名函数定义后就被创建,它定义了一个私有变量privateCounter和一个私有方法changeBy,都是不能够从匿名函数外部直接访问的,只能通过匿名函数返回的三个公有函数访问。这三个公有函数共享变量环境,因为js静态作用域的关系,这三个函数都能够访问到privateCounterchangeBy方法。我们也可以给makeCounter指定不同的变量,从而创建多个counter。

var counter1 = makeCounter();
var counter2 = makeCounter();
alert(counter1.value()); /* Alerts 0 */
counter1.increment();
counter1.increment();
alert(counter1.value()); /* Alerts 2 */
counter1.decrement();
alert(counter1.value()); /* Alerts 1 */
alert(counter2.value()); /* Alerts 0 */

可以看到counter1和counter2是独立的,也就是说privateCounter存在不同的实例。闭包的这种用法实现了面向对象的私有变量和函数封装。

##闭包的副作用以及如何避免这种副作用 据我所知,很多人踏过闭包坑的人都是从下面这段代码开始的。

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

先说一下这段代码想要完成的功能:通过调用setupHelp()方法给DOM上的元素循环注册事件。然而实际结果是仅仅能注给循环的最后一个节点添加onfocus事件。

这是因为匿名函数部分是一个闭包,循环的确创建了三个事件,但是这三个事件是共享一个变量环境的,而在onfucs执行时循环变量已经是最后一个了,因此三个事件都被注册到了最后一个节点上,通过前面的几个例子这一点是比较容易理解的。

通过改写这个匿名函数,让函数的注册不在一个共享的变量环境中,我们就可以如愿实现我们想要的功能:

  • 方法1:使用我们在2)中提到的工厂方法:
function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function makeHelpCallback(help) {
  return function() {
    showHelp(help);
  };
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  }
}

setupHelp();

makeHelpCallback方法为匿名函数创建一个新的变量环境。

  • 方法2:利用立即执行函数在声明闭包时就执行注册方法:
function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
      (function(){
      	var item = helpText[i];
        document.getElementById(item.id).onfocus = function(){
       	   showHelp(item.help);
       }
    })(i)
	}
}
setupHelp();

如果不是特别需要,建议还是避免使用闭包。因为闭包会给脚本的处理性能和内容开销带来影响。这就是为什么在我们在创建对象时,通常会把方法定义在对象的原型上,而不是对象的构造函数上。

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

在使用Myobject每实例一个对象都会重新声明方法,创建闭包。通过prototype改写,可以提高效率并节省内存:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};

通过这种方法声明的对象成员方法可以被所有对象共享,不需要在每次创建对象时再次创建函数。