资讯专栏INFORMATION COLUMN

JavaScript高级程序设计——闭包

Lucky_Boy / 1341人阅读

摘要:条件闭包是允许函数访问局部作用域之外的数据。这就是需要理解闭包的核心内容。我们可以通过创建另一个匿名函数强制让闭包的行为符合预期。而这个匿名函数内部又创建并返回了一个访问的闭包。

前言

有很多人搞不清匿名函数闭包这两个概念,经常混用。闭包是指有权访问另一个函数作用域中的变量的函数。匿名函数就是没有实际名字的函数。

闭包 概念

闭包,其实是一种语言特性,它是指的是程序设计语言中,允许将函数看作对象,然后能像在对象中的操作搬在函数中定义实例(局部)变量,而这些变量能在函数中保存到函数的实例对象销毁为止,其它代码块能通过某种方式获取这些实例(局部)变量的值并进行应用扩展。

条件

闭包是允许函数访问局部作用域之外的数据。即使外部函数已经退出,外部函数的变量仍可以被内部函数访问到。

因此闭包的实现需要三个条件:

内部函数实用了外部函数的变量
外部函数已经退出
内部函数可以访问

function a() {
            var x = 0;
            return function(y) {
                x = x + y;
                // return x;
                console.log(x);

            }

        }
        var b = a();
        b(1); //1
        b(1); //2

上述代码在执行的时候,b得到的是闭包对象的引用,虽然a执行完毕后,但是a的活动对象由于闭包的存在并没有被销毁,在执行b(1)的时候,仍然访问到了x变量,并将其加1,若再执行b(1),则x是2,因为闭包的引用b并没有消除。(后面会解释,闭包返回了函数,函数可以创建独立的作用域)

闭包,其实就是指程序语言中能让代码调用已运行的函数中所定义的局部变量。

但是你只需要知道应用的两种情况即可——函数作为返回值,函数作为参数传递。

function fn() {
            var max = 10;
            return function bar(x) {
                if (x > max) {
                    console.log(x);
                }
            };
        }
        var f1 = fn();
        f1(15);

如上代码,bar函数作为返回值,赋值给f1变量。执行f1(15)时,用到了fn作用域下的max变量的值。至于如何跨作用域取值,可以参考上一篇文章。

var max = 10,
            fn = function(x) {
                if (x > max) {
                    console.log(x);  //15
                }
            };
        (function(f) {
            var max = 100;
            f(15);
        })(fn); 

如上代码中,fn函数作为一个参数被传递进入另一个函数,赋值给f参数。执行f(15)时,max变量的取值是10,而不是100。

上一篇讲到自由变量跨作用域取值时,曾经强调过:要去创建这个函数的作用域取值,而不是“父作用域”。理解了这一点,以上两端代码中,自由变量如何取值应该比较简单.

另外,讲到闭包,除了结合着作用域之外,还需要结合着执行上下文栈来说一下。

在前面讲执行上下文栈时,我们提到当一个函数被调用完成之后,其执行上下文环境将被销毁,其中的变量也会被同时销毁。

有些情况下,函数调用完成之后,其执行上下文环境不会接着被销毁。这就是需要理解闭包的核心内容。

可以拿本文的之前代码(只做注释修改)来分析一下。

1//全局作用域
2        function fn() {
3            var max = 10;
4            // fn作用域
5            return function bar(x) {
6                if (x > max) {
7                    console.log(x);
8                }
9            }; //bar作用域
10        }
11       var f1 = fn();
12        f1(15);

全局作用域为:代码1-12行;fn作用域为:代码2-10行;bar作用域为:代码5-8行。

举例

第一步,代码执行前生成全局上下文环境,并在执行时对其中的变量进行赋值。此时全局上下文环境是活动状态。

第二步,执行第17行代码时,调用fn(),产生fn()执行上下文环境,压栈,并设置为活动状态。

第三步,执行完第17行,fn()调用完成。按理说应该销毁掉fn()的执行上下文环境,但是这里不能这么做。注意,重点来了:

因为执行fn()时,返回的是一个函数。函数的特别之处在于可以创建一个独立的作用域。而正巧合的是,返回的这个函数体中,还有一个自由变量max要引用fn作用域下的fn()上下文环境中的max。因此,这个max不能被销毁,销毁了之后bar函数中的max就找不到值了。

因此,这里的fn()上下文环境不能被销毁,还依然存在与执行上下文栈中。

——即,执行到第18行时,全局上下文环境将变为活动状态,但是fn()上下文环境依然会在执行上下文栈中。另外,执行完第18行,全局上下文环境中的max被赋值为100。如下图:

第四步,执行到第20行,执行f1(15),即执行bar(15),创建bar(15)上下文环境,并将其设置为活动状态。

执行bar(15)时,max是自由变量,需要向创建bar函数的作用域中查找,找到了max的值为10。这个过程在作用域链一节已经讲过。

这里的重点就在于,创建bar函数是在执行fn()时创建的。fn()早就执行结束了,但是fn()执行上下文环境还存在与栈中,因此bar(15)时,max可以查找到。如果fn()上下文环境销毁了,那么max就找不到了。

总结:使用闭包会增加内容开销

第五步,执行完20行就是上下文环境的销毁过程,这里就不再赘述了。

闭包与变量 概念

闭包只能取得包含函数中任何变量的最后一个值,闭包所保存的是整个变量对象,而不是某个特殊变量。

例子
function createFunctions() {
            var result = new Array();

            for (var i = 0; i < 10; i++) {
                result[i] = function() {
                    return i;
                };
            }

            return result;
        }

        var funcs = createFunctions();

        //每个函数都输出10
        for (var i = 0; i < funcs.length; i++) {
            document.write(funcs[i]() + "
"); }

总结:每个函数的作用域链中都保存着createFunctions()函数的活动对象,所以它们引用的都是同一个变量i。当createFunctions()函数返回后,变量i的值为10。

我们可以通过创建另一个匿名函数强制让闭包的行为符合预期。

function createFunctions() {
            var result = new Array();

            for (var i = 0; i < 10; i++) {
                result[i] = function(x) {
                    return function() {
                        return x;
                    };
                }(i);
            }

            return result;
        }

        var funcs = createFunctions();

        //循环输出0-10
        for (var i = 0; i < funcs.length; i++) {
            document.write(funcs[i]() + "
"); }

总结:没有直接把闭包赋值给数组,而是定义了一个匿名函数,并通过立即执行该匿名函数的结果赋值给数组,并带了for循环的参数i进去,让x能找到传入的参数值为0-10,这就解释了函数参数是按值传递的,所以会将变量i的当前值复制给参数x。而这个匿名函数内部又创建并返回了一个访问x的闭包。这样以来result数组中的每个函数都有自己x变量的一个副本,所以会符合我们的预期输出不同的值。

函数按值传递

函数传参就两个类型,基本类型和引用类型,大家纠结的都是引用类型的传递。

引用类型作为参数传入函数,传的是个地址值,或者指针值,不是那个引用类型本身,它还好好的呆在堆内存呢。赋值给argument的同样是地址值或者指针。所以说是value值传递一点没错,传的是个地址值。通过两个例子看懂就行了。

例子1:

function setName(obj) {
obj.name = "aaa";
var obj = new Object(); // 如果是按引用传递的,此处传参进来obj应该被重新引用新的内存单元
obj.name = "ccc";
return obj;
}

var person = new Object();
person.name = "bbb";
var newPerson = setName(person);
console.log(person.name + " | " + newPerson.name); // aaa | ccc

从结果看,并没有显示两个"ccc"。这里是函数内部重写了obj,重写的obj是一个局部对象。当函数执行完后,立即被销毁。

引用值:对象变量它里面的值是这个对象在堆内存中的内存地址。因此如果按引用传递,它传递的值也就是这个内存地址。那么var obj = new Object();会重新给obj分配一个地址,比如是0x321了,那么它就不在指向有name = "aaa";属性的内存单元了。相当于把实参obj和形参obj的地址都改了,那么最终就是输出两个ccc了。

例子2

var a = {
num:"1"
};

var b = {
num:"2"
};

function change(obj){
obj.num = "3";
obj = b;
return obj.num;
}

var result = change(a);
console.log(result + " | " + a.num); // 2 | 3

首先把a的值传到change函数内,obj.num = "3";后a.name被修改为3;
a的地址被换成b的地址;
返回此时的a中a.num。

闭包中使用this对象 概念

this对象是在运行时基于函数的执行环境绑定的:全局函数中,this等于window;当函数被作用某个对象的方法调用时,this等于那个对象。

但在匿名函数中,由于匿名函数的执行环境具有全局性,因此this对象通常指向window(在通过call或apply函数改变函数执行环境的情况下,会指向其他对象)。

var name = "The Window";
        
        var object = {
            name : "My Object",
        
            getNameFunc : function(){
                return function(){
                    return this.name;
                };
            }
        };
        
        alert(object.getNameFunc()());  //"The Window"

通过修改把作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。如下代码:

var name = "The Window";
            
            var object = {
                name : "My Object",
            
                getNameFunc : function(){
                    var that = this;
                    return function(){
                        return that.name;
                    };
                }
            };
            
            alert(object.getNameFunc()());  //"MyObject"
变量声明提前
var scope="global";  

function scopeTest() { 
    console.log(scope);  
    var scope="local";  
}  
scopeTest(); //undefined

此处的输出是undefined,并没有报错,这是因为在前面我们提到的函数内的声明在函数体内始终可见,上面的函数等效于:

var scope="global";  
function scopeTest() {  
    var scope;  
    console.log(scope);  
    scope="local";  
}  
scopeTest(); //undefined

注意,如果忘记var,那么变量就被声明为全局变量了。结果就是global

没有块级作用域

和其他我们常用的语言不同,在Javascript中没有块级作用域:

 function scopeTest() {
            var scope = {};
            if (scope instanceof Object) {
                var j = 1;
                for (var i = 0; i < 10; i++) {
                    console.log(i); //输出0-9
                }
                console.log(i); //输出10 
            }
            console.log(j); //输出1 
        }
        scopeTest();

在javascript中变量的作用范围是函数级的,即在函数中所有的变量在整个函数中都有定义,这也带来了一些我们稍不注意就会碰到的“潜规则”:

var scope = "hello"; 
function scopeTest() { 
    console.log(scope);//① 
    var scope = "no"; 
    console.log(scope);//② 
}

在①处输出的值竟然是undefined,简直丧心病狂啊,我们已经定义了全局变量的值啊,这地方不应该为hello吗?其实,上面的代码等效于:

var scope = "hello"; 
function scopeTest() { 
    var scope; 
    console.log(scope);//① 
    scope = "no"; 
    console.log(scope);//② 
}

声明提前、全局变量优先级低于局部变量,根据这两条规则就不难理解为什么输出undefined了。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/83141.html

相关文章

  • 到底什么是闭包?深入理解javascript闭包

    摘要:如何使用闭包所以这时候我们就要用闭包去解决这个问题了,先看代码。计数器为这时候的就形成了一个闭包。一个闭包由两部分组成,函数和创建该函数的环境。就是创建了一个匿名函数调用函数解除对匿名函数的引用,以便释放内存 古老定义 闭包(closure),是指函数变量可以保存在函数作用域内,因此看起来是函数将变量包裹了起来。 //根据定义,包含变量的函数就是闭包 function foo() { ...

    figofuture 评论0 收藏0
  • 闭包详解一

    摘要:再看一段代码这样就清晰地展示了闭包的词法作用域能访问的作用域将当做一个值返回执行后,将的引用赋值给执行,输出了变量我们知道通过引用的关系,就是函数本身。 在正式学习闭包之前,请各位同学一定要确保自己对词法作用域已经非常的熟悉了,如果对词法作用域还不够熟悉的话,可以先看: 深入理解闭包之前置知识---作用域与词法作用域 前言 现在去面试前端开发的岗位,如果你的面试官也是个前端,并且不是太...

    cnio 评论0 收藏0
  • 《你不知道的javascript》笔记_作用域与闭包

    摘要:建筑的顶层代表全局作用域。实际的块级作用域远不止如此块级作用域函数作用域早期盛行的立即执行函数就是为了形成块级作用域,不污染全局。这便是闭包的特点吧经典面试题下面的代码输出内容答案个如何处理能够输出闭包方式方式下一篇你不知道的笔记 下一篇:《你不知道的javascript》笔记_this 写在前面 这一系列的笔记是在《javascript高级程序设计》读书笔记系列的升华版本,旨在将零碎...

    galaxy_robot 评论0 收藏0
  • JavaScript闭包与立即执行函数

    摘要:闭包与立即执行函数前言最近在细读高级程序设计,对于我而言,中文版,书中很多地方翻译的差强人意,所以用自己所理解的,尝试解读下。作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含任何变量的最后一个值。 闭包与立即执行函数 前言:最近在细读Javascript高级程序设计,对于我而言,中文版,书中很多地方翻译的差强人意,所以用自己所理解的,尝试解读下。如有纰漏或错误,会...

    Scorpion 评论0 收藏0
  • JavaScript高级程序设计》(第3版)读书笔记 第7章 函数表达式

    摘要:定义函数表达式的方式有两种函数声明。不过,这并不是匿名函数唯一的用途。可以使用命名函数表达式来达成相同的结果闭包匿名函数和闭包是两个概念,容易混淆。匿名函数的执行环境具有全局性,因此其对象通常指向通过改变函数的执行环境的情况除外。 定义函数表达式的方式有两种: 函数声明。它的重要特征就是 函数声明提升(function declaration hoisting) 即在执行代码之前会...

    邹立鹏 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<