一文读懂闭包函数

js闭包:
根据《javascript权威指南 第6版》所说
在javascript语言中,闭包就是函数和该函数作用域的组合。从这个概念上来讲,在js中,所有函数都是闭包

既然所有函数都是闭包,还有必要专门提这个概念吗?

大多数函数被调用时(invoked),使用的作用域和他们被定义时(defined)使用的作用域是同一个作用域,这种情况下,闭包神马的,无关紧要。但是,当他们被invoked的时候,使用的作用域不同于他们定义时使用的作用域的时候,闭包就会变的非常有趣,并且开始有了很多的使用场景,这就是你之所以要掌握闭包的原因。 理解“闭包” step 1:掌握嵌套函数的词法作用域规则(lexical scoping rules)

var scope = "global scope"; 
function checkScope() {
    var scope = "local scope";
    function f() {
        return scope;
    }
    return f();
}
checkScope();   //=> "local scope"

分析一下上面的代码,该代码定义了一个全局变量 scope,以及一个函数checkScope,在函数checkScope中,定义一个局部变量,同样命名为scope,以及一个函数f(嵌套函数)

注意:
在js中,函数可以用来创建函数作用域;
函数就像一层半透明玻璃,在函数内部可以看到函数外部的变量,但是在函数外部,看不到函数内部的变量。
变量的搜索是从内向外而不是从外向内搜索的。

代码执行过程分析:在这里插入图片描述
checkScope被invoke时,return f(),运行内部嵌套函数f,f沿着作用域链从内向外寻找变量scope,找到“local scope”,停止寻找,因此,函数返回 “local scope”;

接下来,代码稍作修改:

var scope = "global scope"; 
function checkScope() {
    var scope = "local scope";
    function f() {
        return scope;
    }
    return f;
}
checkScope()();   //=> "这次返回什么?"

代码执行过程分析:
checkScope被invoke时,将内部嵌套的函数f返回,因此checkScope()()这句执行时,其实运行的是f(),f函数返回scope变量,在这种情况下,f会从哪个作用域里去寻找变量scope呢?

remember 词法作用域的基础规则:函数被执行时(executed)使用的作用域链(scope chain)是被定义时的scope chain,而不是执行时的scope chain

嵌套函数f(), 被定义时,所在的作用域链中,变量scope是被绑定的值是“local scope”,而不是"global scope",因此,以上代码的结果是啥?没错,是"local scope"!

这就是闭包的神奇特性:闭包可以捕获到局部变量和参数的外部函数绑定,即便外部函数的调用已经结束。
只要记住一点:词法作用域的规则,即函数被执行时(executed)使用的作用域链(scope chain)是被定义时的scope chain,而不是执行时的scope chain,就可以很容易的理解闭包的行为了。

理解“闭包” step 2:掌握闭包的使用场景
在js版本的设计模式中,很多模式的实现都需要借助于闭包,因此,掌握闭包的使用场景,可以结合设计模式一起理解学习。这里引用了《JavaScript设计模式与开发实践》中的很多例子,书很好,推荐大家阅读学习。

闭包经典使用场景一:通过循环给页面上多个dom节点绑定事件
场景描述:假如页面上有5个button,要给button绑定onclick事件,点击的时候,弹出对应button的索引编号。
在这里插入图片描述

<!DOCTYPE html>
<html>
<head>
     <meta charset="UTF-8">
</head>
<body>
    <button>Button0</button>
    <button>Button1</button>
    <button>Button2</button>
    <button>Button3</button>
    <button>Button4</button>
</body>
</html>

先随手来一段for循环:

var btns = document.getElementsByTagName(‘button’);
for(var i = 0, len = btns.length; i < len; i++) {
btns[i].onclick = function() {
alert(i);
}
}

通过执行该段代码,发现不论点击哪个button ,均alert 5;

why?
因为,onclick事件是被异步触发的,当事件被触发时,for循环早已结束,此时变量 i 的值已经是 5 。所以,当onlick事件函数顺着作用域链从内向外查找变量 i 时,找到的值总是 5 。

那怎么能循环给button添加事件,并且还能alert出来不同的值呢?答案当然是:“闭包”!在闭包的作用下,定义事件函数的时候,每次循环的i值都被封闭起来,这样在函数执行时,会查找定义时的作用域链,这个作用域链里的i值是在每次循环中都被保留的,因此点击不同的button会alert出来不同的i。

上代码:
Tip: 在js中,没有块级作用域 ,只有函数作用域。可以采用“立即执行函数Immediately-Invoked Function Expression (IIFE)”的方式创建作用域。

for(var i = 0, len = btns.length; i < len; i++) {
    (function(i) {
        btns[i].onclick = function() {
            alert(i);
        }
    }(i))
}

运行以上代码,是符合我们需求的。

闭包使用场景二:封装变量
闭包可以将一些不希望暴露在全局的变量封装成“私有变量”。
假如有一个计算乘积的函数,mult函数接收一些number类型的参数,并返回乘积结果。为了提高函数性能,我们增加缓存机制,将之前计算过的结果缓存起来,下次遇到同样的参数,就可以直接返回结果,而不需要参与运算。这里,存放缓存结果的变量不需要暴露给外界,并且需要在函数运行结束后,仍然保存,所以可以采用闭包。
上代码:

var mult = (function(){
    var cache = {};
    var calculate = function() {
        var a = 1;
        for(var i = 0, len = arguments.length; i < len; i++) {
            a = a * arguments[i];
        }
        return a;
    }
    
    return function() {
        var args = Array.prototype.join.call(arguments, ',');
        if(args in cache) {
            return cache[args];
        }
        
        return cache[args] = calculate.apply(null, arguments);
    }
}())

闭包使用场景三:延续局部变量的寿命
img对象经常用于数据上报,如下:

var report = function(src) {
    var img = new Image();
    img.src = src;
}
report('http://xxx.com/getUserInfo');

这段代码在运行时,发现在一些低版本浏览器上存在bug,会丢失部分数据上报,原因是img是report函数中的局部变量,当report函数调用结束后,img对象随即被销毁,而此时可能还没来得及发出http请求,所以此次请求就会丢失。
因此,我们使用闭包把img对象封闭起来,就可以解决数据丢失的问题:
var report = (function() {
var imgs = [];
return function(src) {
var img = new Image();
imgs.push(img);
img.src = src;
}
}())

闭包+设计模式

在诸多设计模式中,闭包都有广泛的应用。对象==数据+方法。而闭包是在过程中以环境的形式包含了数据。因此,通常面向对象能实现的功能,使用闭包也可以实现。
涉及到设计模式,闭包就是一种理所当然的存在,必须熟练使用,才可以理解每种设计模式的意图

发布了70 篇原创文章 · 获赞 154 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/weixin_39815001/article/details/95084440