您的位置:首页 > Web前端 > JavaScript

Js中的闭包

2017-08-02 09:41 225 查看
闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。

下面就是我的学习笔记,对于Javascript初学者应该是很有用的。

一、变量的作用域

要理解闭包,首先必须理解Javascript特殊的变量作用域。

变量的作用域无非就是两种:全局变量和局部变量。

Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。

  var n=999;

  function f1(){

    alert(n);

  }

  f1(); // 999

另一方面,在函数外部自然无法读取函数内的局部变量。

  function f1(){

    var n=999;

  }

  alert(n); // error

这里有一个地方需要注意,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!

  function f1(){

    n=999;

  }

  f1();

  alert(n); // 999

二、如何从外部读取局部变量?

出于种种原因,我们有时候需要得到函数内的局部变量。但是,前面已经说过了,正常情况下,这是办不到的,只有通过变通方法才能实现。

那就是在函数的内部,再定义一个函数。

  function f1(){

    var n=999;

    function f2(){

      alert(n); // 999

    }

  }

在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是Javascript语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!

  function f1(){

    var n=999;

    function f2(){

      alert(n); 

    }

    return f2;

  }

  var result=f1();

  result(); // 999

三、闭包的概念

上一节代码中的f2函数,就是闭包。

各种专业文献上的"闭包"(closure)定义非常抽象,很难看懂。我的理解是,闭包就是能够读取其他函数内部变量的函数。

由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。

所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

四、闭包的用途

闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

怎么来理解这句话呢?请看下面的代码。

  function f1(){

    var n=999;

    nAdd=function(){n+=1}

    function f2(){

      alert(n);

    }

    return f2;

  }

  var result=f1();

  result(); // 999

  nAdd();

  result(); // 1000

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

五、使用闭包的注意点

1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

六、思考题

如果你能理解下面两段代码的运行结果,应该就算理解闭包的运行机制了。

代码片段一。

  var name = "The Window";

  var object = {

    name : "My Object",

    getNameFunc : function(){

      return function(){

        return this.name;

      };

    }

  };

  alert(object.getNameFunc()());

代码片段二。

  var name = "The Window";

  var object = {

    name : "My Object",

    getNameFunc : function(){

      var that = this;

      return function(){

        return that.name;

      };

    }

  };

  alert(object.getNameFunc()());

闭包,是 javascript 中重要的一个概念,对于初学者来讲,闭包是一个特别抽象的概念,特别是ECMA规范给的定义,如果没有实战经验,你很难从定义去理解它。下面是作者从作用域链慢慢讲到闭包以及在后面提到了一些闭包的高级用法。下面大家一起来学习Javascript中的闭包。


谈一谈JavaScript作用域链

  当执行一段JavaScript代码(全局代码或函数)时,JavaScript引擎会创建为其创建一个作用域又称为执行上下文(Execution Context),在页面加载后会首先创建一个全局的作用域,然后每执行一个函数,会建立一个对应的作用域,从而形成了一条作用域链。每个作用域都有一条对应的作用域链,链头是全局作用域,链尾是当前函数作用域。

  作用域链的作用是用于解析标识符,当函数被创建时(不是执行),会将this、arguments、命名参数和该函数中的所有局部变量添加到该当前作用域中,当JavaScript需要查找变量X的时候(这个过程称为变量解析),它首先会从作用域链中的链尾也就是当前作用域进行查找是否有X属性,如果没有找到就顺着作用域链继续查找,直到查找到链头,也就是全局作用域链,仍未找到该变量的话,就认为这段代码的作用域链上不存在x变量,并抛出一个引用错误(ReferenceError)的异常。

看下面的例子:

//定义全局变量color,对于全局都适用,即在任何地方都可以使用全局变量color
var color = "red";

function changeColor(){
//在changeColor()函数内部定义局部变量anotherColor,只在函数changeColor()里面有效
var anotherColor = "blue";

function swapColor(){
//在swapColor()函数内部定义局部变量tempColor,只在函数swapColor()里面有效
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;

//这里可以访问color、anotherColor和tempColor
console.log(color);                //blue
console.log(anotherColor);        //red
console.log(tempColor);            //blue
}

swapColor();
//这里只能访问color,不能访问anotherColor、tempColor
console.log(color);                //blue
console.log(anotherColor);        //anotherColor is not defined
console.log(tempColor);            //tempColor is not defined
}

changeColor();
//这里只能访问color
console.log(color);                //blue
console.log(anotherColor);        //anotherColor is not defined
console.log(tempColor);            //tempColor is not defined


 还有几个坑需要注意一下:


1、var和函数的提前声明

var color = "red";

function changeColor(){
var color = "yellow";
return color;
}

var result = changeColor();
console.log(result);


再如:

function fn(a) {
console.log(a);
var a = 2;
function a() {}
console.log(a);
}
fn(1);
//输出:function a() {} ,2


 


2、Javascript中没有块级作用域,但是有词法作用域,比如:

function f1(){var a=1;f2();}
function f2(){return a;}
var result = f1();
console.log(result);
//输出结果:a is not defined



3、在函数内部不用var关键字申明变量,则默认该变量为全局变量,比如:

function add(a,b){
var sum = a+b;//次世代sum为add函数内部的变量,仅限在函数内部使用,在函数外面不可以使用
return sum;
}
var result = add(1,2);
console.log(result);    //3
console.log(sum);        //sum is not defined
//不使用var关键字声明变量
function add(a,b){
sum = a+b;//此时的sum为全局变量,在函数之外也可以调用
return sum;
}
var result = add(1,2);
console.log(result);    //3
console.log(sum);        //3



补充:


在JavaScript中如果不创建变量,直接去使用,则报错:


JavaScript中如果创建值而不赋值,则该值为 undefined,如:

在函数内如果这么写:

上述代码,不报错而是输出 undefined,其原因是:JavaScript的函数在被执行之前,会将其中的变量全部声明,而不赋值。所以,相当于上述实例中,函数在“预编译”时,已经执行了var xo;所以上述代码中输出的是undefined。

注意:我们平时在声明变量时一定要注意!!!还有不要滥用全局变量(在forin循环的时候特别注意)!!!


4、词法作用域是不可逆的,我们可以从下面的例子中看到结果:

// name = undefined
var scope1 = function () {
// name = undefined
var scope2 = function () {
// name = undefined
var scope3 = function () {
var name = 'Todd'; // locally scoped
};
};
};


 

 

前面我们了解了作用域的一些基本知识,我们发现有作用域的存在能帮我们省去不少事,但是于此同时,也给我们带来了很多麻烦,比如说我们想在下面的函数A中,调用函数B,我们该怎么办呢?

function A(){

    function B(){

           //

    }

}

思路:我们给函数B设一个返回值,然后在函数A中调用,代码如下:

function A(){
function B(){
console.log("Hello foodoir!");
}
return B;
}
var c = A();
c();//Hello foodoir!


这样我们就可以得到我们想要的结果。这样,我们基本上到了一个最简单的闭包形式。我们再回过头分析代码:

把这5步操作总结成一句话:函数A的内部函数B被函数A外的一个变量 c 引用。当一个内部函数被其外部函数之外的变量引用时,就形成了一个闭包。

思考:我们还有没有其他的方法?

思路:使用匿名函数

function A(){
//匿名函数
var B = function(x,y) {
return x+y;
}
console.log(B(1,2));//3
return B(1,2);
}
var c = A();
console.log(c);//3


然而,在Javascript高级程序设计中是这样描述闭包的“闭包是指有权访问另一个函数作用域中的变量的函数”,但是我们看匿名函数的例子,很明显,这种方法不可取!

通过这个例子,能让我们更好的理解闭包。


下面我们再来看下面的几种闭包


demo1:

function fn(){
var b = "foodoir";
return function(){
console.log(b);//foodoir
return b;
}
}
//console.log(b);//b is not defined
var result = fn();
console.log(result());//foodoir



demo2:

var n;
function f(){
var b = "foodoir";
n = function(){
return b;
}
}
f();
console.log(n());//foodoir



demo3:

//相关定义与闭包
function f(arg){
var n = function(){
return arg;
};
arg++;
return n;
}
var m = f(123);
console.log(m());//124
//注意,当我们返回函数被调用时,arg++已经执行过一次递增操作了,所以m()返回的是更新后的值。



demo4:闭包中的读取与修改

//闭包中的设置与修改
var getValue,setValue;
(function(){
var n = 0;
getValue = function(){
return n;
};
setValue = function(x){
n = x;
}
})();
//console.log(n);
console.log(getValue());//0
console.log(setValue());//undefined

setValue(123);
console.log(getValue());//123



demo5:用闭包实现迭代效果

//用闭包实现迭代器效果
function test(x){
//得到一个数组内部指针的函数
var i=0;
return function(){
return x[i++];
};
}
var next = test(["a","b","c","d"]);
console.log(next());//a
console.log(next());//b
console.log(next());//c
console.log(next());//d



demo6:循环中的闭包

//循环中的闭包
function fn(){
var a = [];
for(var i=0;i<3;i++){
a[i] = function(){
return i;
}
}
return a;
}
var a = fn();
console.log(a[0]());//3
console.log(a[1]());//3
console.log(a[2]());//3

/*
* 我们这里创建的三个闭包,结果都指向一个共同的局部变量i。
* 但是闭包并不会记录它们的值,它们所拥有的只是一个i的连接,因此只能返回i的当前值。
* 由于循环结束时i的值为3,所以这三个函数都指向了3这一个共同值。
* */


思考:如何使结果输出分别为0、1、2呢?

思路一:我们可以尝试使用自调用函数

function fn(){
var a = [];
for(var i=0;i<3;i++){
a[i] = (function(x){
return function(){
return x;
}
})(i);
}
return a;
}
var a = fn();
console.log(a[0]());//0
console.log(a[1]());//1
console.log(a[2]());//2


思路二:我们将i值本地化

function fa(){
function fb(x){
return function(){
return x;
}
}
var a = [];
for(var i=0;i<3;i++){
a[i] = fb(i)
}
return a;
}
console.log(a[0]());//0
console.log(a[1]());//1
console.log(a[2]());//2


------------------------------------------------------分界线-------------------------------------------------------

在这里,我们来对闭包进行更深一步的操作


我们再将demo1的例子进行扩展

代码示例如下:

function funcTest(){
var tmpNum=100; //私有变量
//在函数funcTest内
//定义另外的函数作为funcTest的方法函数
function innerFuncTest(
{
alert(tmpNum);
//引用外层函数funcTest的临时变量tmpNum
}

return innerFuncTest; //返回内部函数
}

//调用函数
var myFuncTest=funcTest();
myFuncTest();//弹出100


到样,我们对闭包的概念和用法有更加熟悉


闭包和this相关

闭包应用举例,模拟类的私有属性,利用闭包的性质,局部变量只有在sayAge方法中才可以访问,而name在外部也访问,从而实现了类的私有属性。

function User(){
this.name = "foodoir";  //共有属性
var age = 21;    //私有属性
this.sayAge=function(){
console.log("my age is " + age);
}
}
var user = new User();
console.log(user.name); //"foodoir"
console.log(user.age);  //"undefined"
user.sayAge();   //"my age is 21"


关于闭包更深入的了解

前面在demo6中,我们了解了用自调用方法来实现闭包,下面我们用这种方法来进行更复杂的操作(写一个简单的组件)。

(function(document){
var viewport;
var obj = {
init:function(id){
viewport = document.querySelector("#"+id);
},
addChild:function(child){
viewport.appendChild(child);
},
removeChild:function(child){
viewport.removeChild(child);
}
}
window.jView = obj;
})(document);


这个组件的作用是:初始化一个容器,然后可以给这个容器添加子容器,也可以移除一个容器。功能很简单,但这里涉及到了另外一个概念:立即执行函数。 简单了解一下就行。主要是要理解这种写法是怎么实现闭包功能的。


闭包并不是万能的,它也有它的缺点

  1、闭包会使得函数中的变量都保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页性能问题。另外在IE下有可能引发内存泄漏 (内存泄漏指当你的页面跳转的时候 内存不会释放 一直占用你的CPU 只有当你关闭了浏览器才会被释放);

  2、闭包会在父函数外部改变父函数内部的变量的值,所以不要随便改动父函数内部的值。


更多参考资料:

  《Javascript高级程序设计(第三版)》第四章、第七章

  《Javascript面向对象编程指南》第三章


作者的话:

  这篇文章主要先是通过几个简单的例子介绍作用域链(顺便补充了几个和作用域链相关的易出错的小知识),然后通过提问慢慢过渡到闭包(在闭包这部分介绍了几种常见闭包的例子),后面又进一步讲到了关于闭包的更高级的用法。后面遇到关于闭包的较好的用法会继续更新。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: