专栏首页Czy‘s BlogJavaScript闭包
原创

JavaScript闭包

JavaScript闭包

函数和对其词法环境lexical environment的引用捆绑在一起构成闭包,也就是说,闭包可以让你从内部函数访问外部函数作用域。在JavaScript,函数在每次创建时生成闭包。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

定义闭包

为了定义一个闭包,首先需要一个函数来套一个匿名函数。闭包是需要使用局部变量的,定义使用全局变量就失去了使用闭包的意义,最外层定义的函数可实现局部作用域从而定义局部变量,函数外部无法直接访问内部定义的变量。

function student(){
    var name = "Ming";
    var sayMyName = function(){ // sayMyName作为内部函数,有权访问父级函数作用域student中的变量
        console.log(name);
    }
    console.dir(sayMyName); // ... [[Scopes]]: Scopes[2] 0: Closure (student) {name: "Ming"} 1: Global ...
    return sayMyName; // return是为了让外部能访问闭包,挂载到window对象也可以 
}
var stu = student(); 
stu(); // Ming

可以看到定义在函数内部的name变量并没有被销毁,我们仍然可以在外部使用函数访问这个局部变量,使用闭包,可以把局部变量驻留在内存中,从而避免使用全局变量。全局变量污染会导致应用程序不可预测性,每个模块都可调用必将引来灾难。

词法环境

闭包共享相同的函数定义,但是保存了不同的词法环境lexical environment

function student(name){
    var sayMyName = function(){
        console.log(name);
    }
    return sayMyName;
}
var stu1 = student("Ming"); 
var stu2 = student("Yang"); 
stu1(); // Ming
stu2(); // Yang

模拟私有方法

在面向对象的语言中,例如JavaPHP等,都是支持定义私有成员的,即只有类内部能够访问,而无法被外部类访问。JavaScript并未原生支持定义私有成员,但是可以使用闭包来模拟实现,私有方法不仅仅有利于限制对代码的访问,还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。

function student(){
    var HP = 100;
    var addHP = function(){
        return ++HP;
    }
    var decHP = function(){
        return --HP;
    }
    return {
        addHP,
        decHP
    };
}
var stu = student();
console.log(stu.HP); // undefined 不允许直接访问
console.log(stu.addHP()); // 101
console.log(stu.decHP()); // 100

回调机制

Js的闭包为回调机制提供了支持,无论函数是否立马被调用,这个闭包都不会被释放。而且在Js里,无论把callback函数作为参数传递给其他函数,或者作为返回值返回,以便于之后调用,都是合法的。

function localContext(){
    var localVal = 1;
    var callback = function(){
        console.log(localVal); // ... [[Scopes]]: Scopes[2] 0: Closure (localContext) {localVal: 1} 1: Global ...
    }
    console.dir(callback);
    setTimeout(callback, 1000); // 1
}
localContext();

在本例中,callback函数与其词法环境构成了闭包,其词法环境中存在的变量localVal = 1在函数callback作为回调函数传递时并没有被立即释放,而可以在回调执行时继续使用,这就是闭包为回调机制提供了支持。

循环创建闭包

ECMAScript 2015引入let关键字之前,只有函数作用域和全局作用域,函数作用域中又可以继续嵌套函数作用域,在for并未具备局部作用域,于是有一个常见的闭包创建问题。

function counter(){
    var arr = [];
    for(var i = 0 ; i < 3 ; ++i){
        arr[i] = function(){
            return i;
        }
    }
    return arr;
}

var coun = counter();
for(var i = 0 ; i < 3 ; ++i){
    console.log(coun[i]()); // 3 3 3
}

可以看到运行输出是3 3 3,而并不是期望的0 1 2,原因是这三个闭包在循环中被创建的时候,共享了同一个词法作用域,这个作用域由于存在一个ivar声明,由于变量提升,具有函数作用域,当执行闭包函数的时候,由于循环早已执行完毕,i已经被赋值为3,所以打印为3 3 3

匿名函数新建函数作用域来解决

function counter(){
    var arr = [];
    for(var i = 0 ; i < 3 ; ++i){
        (function(i){
            arr[i] = function(){
                return i;
            }
        })(i);
    }
    return arr;
}

var coun = counter();
for(var i = 0 ; i < 3 ; ++i){
    console.log(coun[i]()); // 0 1 2
}

使用let关键字

function counter(){
    var arr = [];
    for(let i = 0 ; i < 3 ; ++i){
        arr[i] = function(){
            return i;
        }
    }
    return arr;
}

var coun = counter();
for(var i = 0 ; i < 3 ; ++i){
    console.log(coun[i]()); // 0 1 2
}

内存泄漏

内存泄露是指你用不到(访问不到)的变量,依然占据着内存空间,不能被再次利用起来。

闭包引用的变量应该是需要使用的,不应该属于内存泄漏,但是在IE8浏览器中JScript.dll引擎使用会出现一些问题,造成内存泄漏。

对于各种引擎闭包内存回收具体的表现参阅 这篇文章

性能考量

如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。

在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次。

参考

https://zhuanlan.zhihu.com/p/22486908
https://www.cnblogs.com/Renyi-Fan/p/11590231.html
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • JavaScript变量提升

    在JavaScript中变量声明与函数声明都会被提升到作用域顶部,优先级依次为:变量声明 函数声明 变量赋值

    WindrunnerMax
  • Js中Currying的应用

    柯里化Currying是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术,是函数式编程应用。

    WindrunnerMax
  • async/await剖析

    JavaScript是单线程的,为了避免同步阻塞可能会带来的一些负面影响,引入了异步非阻塞机制,而对于异步执行的解决方案从最早的回调函数,到ES6的Promis...

    WindrunnerMax
  • JavaScript闭包原理与用法实例

    (1)变量的作用域 不带有关键字var的变量会成为全局变量; 在函数中使用关键字var声明的变量是局部变量。 局部变量只有在函数内部才能访问到,在函数外面是访问...

    Javanx
  • JavaScript入门总结第四弹——函数+十分钟了解闭包

    Hello~~偶又来咯,昨天有小可爱说数组有点随意,其实数组的应用的比较广泛,但是并不是很难,主要是不容易都记住,所以兔妞就是给大家将数组进行了...

    萌兔IT
  • 实用 | 读源码,学JavaScript

    Javascript于1995年由网景公司的Brendan Eich发明。 最初发明的目的是作为一个简单的网站脚本语言,来作为复杂网站应用java的补充。但由于...

    疯狂的技术宅
  • 献给前端求职路上的你们(下)

    真正面试中,面试官往往采用的是由难到易的套路,那js和jQuery就是重中之重了,以及针对项目和所用技术方面的一些问题也就是你的必备储粮啦! JavaScrip...

    用户1667431
  • JavaScript作用域闭包(你不知道的JavaScript)

    JavaScript闭包,是JS开发工程师必须深入了解的知识。3月份自己曾撰写博客《JavaScript闭包》,博客中只是简单阐述了闭包的工作过程和列举了几个示...

    奋飛
  • 初探JavaScript(四)——作用域链和声明提前

    前言:最近恰逢毕业季,千千万万的学生党开始步入社会,告别象牙塔似的学校生活。往往在人生的各个拐点的时候,情感丰富,感触颇深,各种对过去的美好的总结,对未来的展望...

    JackieZheng
  • JS执行上下文/作用域/闭包

    2)一般来说内部能访问外部,外部不能访问内部。 那么怎么让外部也能访问内部? —— return

    杨肆月

扫码关注云+社区

领取腾讯云代金券