很多人在刚刚接触JavaScript的时候,对闭包的理解总是模糊的,下面就来说说闭包的那些事儿,说到闭包自然少不了作用域这个东西。

变量的作用域

根据作用域的不同,可以将变量分为:局部变量和全局变量。

Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量,而在函数外部无法读取函数内的局部变量,如果想在外部访问内部的局部变量,可以在函数内部再定义一个函数,该函数可以访问父函数内部的局部变量,因此可以将该函数作为父函数的返回值——链式作用域。

闭包

定义:

闭包就是能够读取其他函数内部变量的函数。换句话说,闭包是链接外部函数与内部函数的纽带。

特性:

  • 可以读取函数内部的变量
  • 让这些变量的值始终保存在内存中

闭包的作用:

  • setTimeout/setInterval
  • 回调函数(callback)
  • 事件句柄(event handle)
  • 模块化(Module)

使用闭包应该注意什么?

  • 因为闭包让变量始终保存在内存中,因此会造成内存消耗大的问题,因此不能滥用,否则会造成性能问题,so应该在函数退出前,删除所有不用的局部变量
  • 闭包可以在父函数外部改变父函数内部变量的值。所以,如果把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值,因为其他指向对象的引用的值也将被修改——模块化。

引用阮老师的例子:

1
2
3
4
5
6
7
8
9
10
11
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
  return function(){
    return this.name;
  };
}
};
console.log(object.getNameFunc()());//输出The Window

这个例子中,全局作用域里面包含name、object两个变量,对象object包含一个内部变量name和一个内部函数getNameFunc,getNameFn是一个匿名函数,且它返回一个匿名函数,此处产生一个闭包,意思就是在这个闭包里面我们可以访问全局作用域。

更多:nodejs-global-object

当我们执行object.getNameFunc()()的时候,实际上是在调用内部的匿名函数,这个匿名函数返回this.name,首先我们要确定this是什么,this的指向是由它所在函数调用的上下文决定的,而不是由它所在函数定义的上下文决定的,因此,此处的this指向全局对象window,因此this.name为外部全局变量name,so结果是”The Window”
那么我们怎么才能访问到object自己的属性name呢?
两种方式:
1、在调用的时候,更改this的指向,这里用到call

1
2
3
4
5
6
7
8
9
10
11
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
  return function(){
    return this.name;
  };
}
};
console.log(object.getNameFunc().call(object));//输出My Object

this被指向了object对象。

2、在getNameFunc里面将this保存成局部变量,然后在闭包里访问

1
2
3
4
5
6
7
8
9
10
11
12
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var self = this;
  return function(){
    return self.name;
  };
}
};
console.log(object.getNameFunc()());//输出My Object

因为闭包里面访问了外部函数的局部变量,根据作用域链,将会由内向外查找self变量,在外部函数的内部找到self指向objct,因此输出object.name,so结果是”My Object”

我当时在执行这个例子的时候,并没有使用浏览器来执行,而是用node来执行的,因此结果并没有像预想的那样,为什么呢?
在浏览器 JavaScript 中,通常 window 是全局对象, 而 Node.js 中的全局对象是 global,所有全局变量(除了 global 本身以外)都是 global 对象的属性。在 Node.js 我们可以直接访问到 global 的属性,而不需要在应用中包含它。
当你定义一个全局变量时,这个变量同时也会成为全局对象的属性,反之亦然。需要注意的是,在 Node.js 中你不可能在最外层定义变量,因为所有用户代码都是属于当前模块的, 而模块本身不是最外层上下文。
因此,闭包中的this是指向global的,而global对象中不存在name和object变量,因为name和object是使用var声明的,var声明的变量是其顶级作用域,即:声明的模块中,因此他俩是局部变量,并非是全局变量。

那么我们在node中执行才能拿到想要的结果呢?像这样儿:

1
2
3
4
5
6
7
8
9
10
11
12
var name = "The Window";
global.name = name;//在全局对象里面添加name
var object = {
name : "My Object",
getNameFunc : function(){
  return function(){
    return this.name;
  };
}
};
console.log(object.getNameFunc()());

或者这样儿:

1
2
3
4
5
6
7
8
9
10
11
name = "The Window";//直接将name升级为全局变量
var object = {
name : "My Object",
getNameFunc : function(){
  return function(){
    return this.name;
  };
}
};
console.log(object.getNameFunc()());

注意:在node中永远使用 var 定义变量以避免引入全局变量,因为全局变量会污染命名空间,提高代码的耦合风险。

来个糟糕的例子

1
2
3
4
5
6
7
8
9
10
var see_you_later = function (arr) {
var i;
for (i = 0; i < arr.length; i++) {
setTimeout(function () {
console.log(i);//输出3 3 3
},100);
}
}
see_you_later([1,2,3]);

想象中,输出结果应该是0、1、2,但是真实的结果是3、3、3,写这个函数的本意是传给每一个定时器中的执行函数一个唯一的i值,但并未如愿,因为执行函数绑定了变量i本身,而不是函数在构造时的变量i,因此在执行函数被调用的时候i值已经递增到峰值了,那么我们改良一下~

1
2
3
4
5
6
7
8
9
10
11
12
13
var see_you_later = function (arr) {
var fun = function (i) {
return function () {
console.log(i);//输出0 1 2
}
}
var i;
for (i = 0; i < arr.length; i++) {
setTimeout(fun(i),100);
}
}
see_you_later([1,2,3]);

我们在循环之外创建一个辅助函数,在辅助函数中返回一个绑定了当前i值的函数,作为一个闭包,该函数可以访问fun函数中的参数i。在实际应用中我们应该避免在循环中创建函数,它可能只会带来无谓的计算,还会引起混淆。

参考:

learning_javascript_closures
图解Javascript上下文与作用域
变量对象(Variable Object)
Closures