vue传值前端知识梳理前端开发那些事

js闭包其实不难,你需要的只是了解何时使用它

2017-03-14  本文已影响3330人  佩吉秋

对于初学者来说,常常会觉得闭包是个很难理解的概念,我认为之所以觉得难以理解,是因为没有了解到闭包的用途以及它通常的使用场景,实际开发中,闭包的运用非常广泛。
如果知道了使用闭包可以解决哪些问题,使用闭包会带来哪些好处,完全掌握并熟练使用闭包就不再是一个难题了。还是那句话:学以致用,如果不知道如何使用,就是没有学会。do it, make your hand dirty!

“闭包”的概念 ——《百度百科》

闭包是指可以包含自由(未绑定到特定对象)变量的代码块;这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。“闭包” 一词来源于以下两者的结合:要执行的代码块(由于自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)和为自由变量提供绑定的计算环境(作用域)。在PHP、Scala、Scheme、Common Lisp、Smalltalk、Groovy、JavaScript、Ruby、 Python、Go、Lua、objective c、swift 以及Java(Java8及以上)等语言中都能找到对闭包不同程度的支持。--百度百科

从“闭包”的概念上可以知道,闭包其实是一个通用的概念,在很多领域中(比如“离散数学”,“计算机”等领域都有使用)。

“闭包”的概念 ——《javascript权威指南 第6版》

Like most modern programming languages, JavaScript uses lexical scoping. This means that functions are executed using the variable scope that was in effect when they were defined, not the variable scope that is in effect when they are invoked. In order to implement lexical scoping, the internal state of a JavaScript function object must include not only the code of the function but also a reference to the current scope chain.This combination of a function object and a scope (a set of variable bindings) in which the function’s variables are resolved is called a closure in the computer science literature. ——引用自“javascript权威指南 第6版”

在javascript语言中,闭包就是函数和该函数作用域的组合。从这个概念上来讲,在js中,所有函数都是闭包(函数都是对象并且函数都有和他们相关联的作用域链scope chain)。

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

大多数函数被调用时(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(嵌套函数)。

代码执行过程分析:


函数作用域.png

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的索引编号。

循环给多个button绑定事件.png
<!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;
    }
}())

闭包+设计模式

在诸多设计模式中,闭包都有广泛的应用。对象==数据+方法。而闭包是在过程中以环境的形式包含了数据。因此,通常面向对象能实现的功能,使用闭包也可以实现。

涉及到设计模式,闭包就是一种理所当然的存在,必须熟练使用,才可以理解每种设计模式的意图。这里先不引入设计模式了,内容太多,有空再总结吧 :)

上一篇下一篇

猜你喜欢

热点阅读