简单理解JavaScript中的闭包

0

零. 前言

从我开始接触前端时就听说过闭包,但是一直不理解闭包究竟是什么。上网看了各种博客,大家对闭包的说法不一。闭包在我理解是一种比较抽象的东西。所以我写了一篇博文来方便自己理解闭包。博主是第一次写博文,如果在文章中有什么看不懂或者概念错误的地方,请大家多多见谅和指出错误。

一. 闭包的定义

再说闭包之前,首先让我们先来理解一下自由变量和约束变量。

在程序设计语言中,变量可以分为自由变量约束变量两种。简单来说,一个函数里局部变量和参数都被认为是约束变量;而不是约束变量的则是自由变量。下面我们通过一个demo来解说。

var x = 10; // 相对于fn来说,x是一个自由变量
function fn(){
    var b = 20;
    console.log( x + b ); // 30 
}
fn();

在上述例子中,相对于函数实例fn而言,x是一个自由变量,因为x并不是fn的局部变量和参数。而b是fn的局部参数,所以b是fn的约束变量。

那么现在我们可以解释一下闭包的第一个定义:

在计算机科学中,闭包是引用了自由变量的函数。

其实闭包不一定要是函数实例,也可以是代码块,只要满足可以保存变量在内存,同时有一些方法对于这些变量进行访问就行了。

所以,我们可以引申出闭包的第二个定义:

闭包是由函数和与其相关的引用环境组合而成的实例,环境由闭包创建时在作用域中的任何局部变量和参数组成。

闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

// 例子1
function Person(){
    var name,
        age;

    function init(name, age){
        name = name;
        age = age;
    }
    function show(){
        console.log('name: %s, age: %d', name, age);
    }

    return {
        init: init,
        show: show
    }
}

var eyesiM = Person.init('EyesiM', 22); // 闭包的实例1
var dcc = Person.init('Dcc', 20); // 闭包的实例2

eyesiM.show(); // name: EyesiM, age: 22
dcc.show(); // name: Dcc, age: 20

上面的变量eyesiM和变量dcc就是闭包的实例,其中变量eyesiM的环境中局部变量name和age的值为'EyesiM'和22;变量dcc的环境中的局部变量name和age的值为'Dcc'和20。

二. 闭包的应用

闭包可以用来在一个函数与一组“私有”变量之间建立关联关系。在给定函数被多次调用的过程中,这些私有变量能够保存在内存中。变量的作用域仅限于包含它们的函数,因此无法从除包含它们的函数之外进行访问。

1. 模块模式

在Java等等一些语言里面会有private关键字来将方法和变量声明为私有的,即它们只能被同一个类中的其它方法所调用。

JavaScript不提供原生的支持,但是可以使用闭包模拟私有变量和私有方法。私有变量可以限制对代码的访问;避免非核心的方法弄乱了代码的公共接口部分。

// 例子2
var demo = (function(){
    // 模拟私有变量
    var count = 0;

    function show(){
        console.log('count: %d', count++);
    }

    return {
        show: show
    }
})();

demo.show(); // 0
demo.count; // 我们不能直接引用,所以这里会返回undefined

在我理解,模块模式可以有两种用途:

  1. 立即调用函数表达式(IIFE):将我们自身的变量和方法封装起来,可以避免全局污染。例子2就是一个IIFE的例子。

  2. 引入依赖:我们可以引入对某一个全局对象的依赖,对这一个全局进行扩充。下面我们可以通过一个例子来表示。

// MODULE 就是一个全局对象,如果不存在就初始化为`{}`,我们使用局部参数my指向这个对象,接着我们给这个全局对象添加属性`method`,然后返回指向这个全局对象的引用。
var MODULE = (function (my) {
    my.method = {
        // add code
    }
    return my;
}(MODULE || {}));

2. 循环中创建闭包

在我们使用ES6的let关键字之前,闭包的一个常见问题就出现在循环中创建闭包。

// 例子3
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="apple">apple</div>
    <div id="banana">banana</div>
    <div id="orange">orange</div>
</body>
</html>
function showColor(item) {
    console.log('id: %s, color: %s', item.id, item.color);
}

function addHTML() {
    var colors = [{
        id: 'apple',
        color: 'red'
    }, {
        id: 'banana',
        color: 'yellow'
    }, {
        id: 'orange',
        color: 'orange'
    }];

    for (var i = 0, length = colors.length; i < length; i++) {
        var item = colors[i];

        // error
        // 当我们把鼠标依次移过id为'apple', 'banana', 'orange'的div时,控制台打印出的是
        // id: orange, color: orange
        // id: orange, color: orange
        // id: orange, color: orange
        // document.getElementById(item.id).onmouseover = function(){
        //    showColor(item);
        // }

        // success
        // 当我们把鼠标依次移过id为'apple', 'banana', 'orange'的div时,控制台打印出的是
        // id: apple, color: red
        // id: banana, color: yellow
        // id: orange, color: orange
        document.getElementById(item.id).onmouseover = function(item) {
            return function() {
                showColor(item);
            };
        }(item);
    }
}
addHTML();

三. 闭包的注意点

闭包避免了环境中的变量被当成垃圾回收,因此使用闭包会使得闭包中的变量都被保存在内存中。

在一般的多页面中,我们关闭或重定向了页面之后,浏览器会自动回收原页面所占用的资源,但是如果我们所做的项目是SPA的话,就需要考虑到内存的使用,所以一定要慎用闭包。

四. 参考资料

  1. yangfch3的笔记-闭包

  2. 深入理解JavaScript 模块模式

  3. 详解javascript立即执行函数表达式(IIFE)

你可能感兴趣的

载入中...