JavaScript 忍者秘籍笔记——闭包

第五章 闭包

闭包是如何工作的

闭包是一个函数的创建时允许该自身函数访问并操作该自身函数之外的变量时所创建的作用域。闭包可以让函数访问所有的变量和函数,只要这些变量和函数存在于该函数声明时的作用域内就行。声明的函数在后续的时候都可以被调用,即便是声明时的作用域消失之后。

一个简单的闭包:

var outerValue = 'ninja';
var later;
funtion outerFunction(){
    var innerValue = 'samurai';

    function innerFunction(){
        /**/
    }
    later = innerFunction; // 将内部函数引用到later变量上
}

// 调用外部函数,将会声明内部函数,并将内部函数赋值给later变量
outerFunction();
later(); // 通过later调用内部函数,不能直接调用内部函数

在外部函数中声明innerFunction()的时候,不仅声明了函数,还创建了一个闭包,该闭包不仅包含函数声明,还包含了函数声明的那一时刻点上该作用域中的所有变量。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>闭包可以访问到什么内容</title>
    <style>
        /*定义结果样式*/
        li.pass{color: green;}
        li.fail{color: red;text-decoration: line-through;}
    </style>
</head>
<body>
    <!--显示测试结果-->
    <ul id="results"></ul>

    <script>
       var outerValue = 'ninja';
       var later;

       function outerFunction(){
           var innerValue = 'samurai';

           function innerFunction(paramValue){
               assert(outerValue, "Inner can see the ninja.");
               assert(innerValue, "Inner can see the samurai.");
               assert(paramValue, "Inner can see the wakizashi.");
               assert(tooLate, "Inner can see the ronin.");
           }
           later = innerFunction;
       }
       assert(!tooLate, "Outer can't see the ronin.");
       var tooLate = 'ronin';

       outerFunction();
       later('wakizashi');
        
       // 定义assert方法
        function assert(value,desc){
            var li = document.createElement('li');
            li.className = value ? "pass" : "fail";
            li.appendChild(document.createTextNode(desc));
            document.getElementById("results").appendChild(li);
        }
    </script>
</body>
</html>

测试结果说明了三个关于闭包的概念:

  • 内部函数的参数是包含在闭包中的。
  • 作用域之外的所有变量,即便是函数声明之后的那些声明,也都包含在闭包中。
  • 相同的作用域内,尚未声明的变量不能进行提前引用。

重要的是,这些结构不是可以轻易看到的,这种方式在存储和引用信息方面会直接影响性能。每个通过闭包进行信息访问的函数都有一个“锁链”,可以在它上面附加任何信息。使用闭包时,闭包里的信息会一直保存在内存里,直到这些信息确保不再被使用,或页面卸载时,JavaScript引擎才能清理这些信息。

使用闭包

闭包的一种常见用法是封装一些信息作为“私有变量”,限制这些变量的作用域。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>闭包可以访问到什么内容</title>
    <style>
        li.pass{color: green;}
        li.fail{color: red;text-decoration: line-through;}
    </style>
</head>
<body>
    <ul id="results"></ul>

    <script>
        // 定义一个Ninja构造器
       function Ninja(){
           // 私有变量,不能直接被外部访问
            var feints = 0;

            // 创建一个访问feints计数的方法
            this.getFeints = function(){
                return feints;
            };

            this.feint = function(){
                feints++;
            };
       }

       var ninja = new Ninja();
       ninja.feint();

       assert(ninja.getFeits() == 1, "We're able to access the internal feint count.");
       assert(ninja.feints === undefined, "And the private data is inaccessible to us")

       // 定义assert方法
        function assert(value,desc){
            var li = document.createElement('li');
            li.className = value ? "pass" : "fail";
            li.appendChild(document.createTextNode(desc));
            document.getElementById("results").appendChild(li);
        }
    </script>
</body>
</html>

在计时器间隔回调闭包,创建动画:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>在计时器间隔回调闭包</title>
    <style>
        #box{
            position: absolute;
        }
    </style>
</head>
<body>
    <div id="box">动画</div>

    <script>
        function animateIt(elementId){
            var elem = document.getElementById(elementId);
            var tick = 0;
            var timer = setInterval(function(){
                if(tick < 100){
                    elem.style.left = elem.style.top = tick + 'px';
                    tick++;
                }else{
                    clearInterval(timer);
                }
            },100);
        }
        animateIt('box');
    </script>
</body>
</html>

没有闭包,同时做多件事的时候,无论是事件处理,还是动画,甚至是Ajax请求,都将是极其困难的。闭包不是在创建那一时刻点的状态快照,而是一个真实的状态封装,只要闭包存在,就可以对其进行修改。

绑定函数上下文

创建 bind()方法,使用apply()调用原始函数,强制将上下文设置成任何对象。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>给事件处理绑定特定的上下文</title>
</head>
<body>
    <button id="test">click me</button>

    <script>
        function bind(context,name){
            return function(){
                return context[name].apply(context,arguments);
            };
        }

        var button = {
            clicked: false,
            click: function(){
                this.clicked = true;
                if(button.clicked) alert("The button has been clicked");
                console.log(this);
            }
        };
        var elem = document.getElementById('test');
        // 如果不使用bind,this指向的是<button>标签
        // elem.addEventListener("click",button.click,false);
        elem.addEventListener("click",bind(button,'click'),false);
    </script>
</body>
</html>

自 JavaScript 1.8.5起,已有原生 bind()方法。

偏应用函数

偏应用函数返回一个含有预处理参数的新函数,以便后期可以调用。

这种在一个函数中首先填充几个参数,然后再返回一个新函数的技术称为柯里化

Function.prototype.partial = function(){
    var fn = this, args = Array.prototype.slice.call(arguments);
    return function(){
        var arg = 0;
        for(var i=0; i<args.length && arg<arguments.leng; i++){
            if(args[i] === undefined){
                args[i] = arguments[arg++];
            }
        }
        return fn.apply(this,args);
    };
};

使用闭包实现缓存记忆

Function.prototype.memoized = function(key){
    this._values = this._values || {};
    return this._values[key] !== undefined ? 
        this._values[key] :
        this._values[key] = this.apply(this,arguments);
};

Function.prototype.memoized = function(){
    var fn = this;
    return function(){
        return fn.memoized.apply(fn,arguments);
    };
};

var isPrime = (function(num){
    var prime = num!=1;
    for(var i=2; i<num; i++){
        if(num % i ==0){
            prime = false;
            break;
        }
    }
    return prime;
}).memoized();

函数包装

函数包装是一种封装函数逻辑的技巧,用于在单个步骤内重载创建新函数或继承函数。最有价值的场景是,在重载一些已经存在的函数时,同时保持原始函数在被包装后仍然能够有效使用。

// 定义一个包装函数,接收3个参数:方法所属对象、方法名称、执行方法
function wrap(object, method, wrapper) {
    // 保存原函数,后面可以通过闭包进行引用
    var fn = object[method];
    // 创建新函数调用作为包装器传入的函数
    return object[method] = function () {
        // 使用apply强制将函数上下文设置为object对象,并将其作为参数与原始参数一起传递给原有方法
        return wrapper.apply(this, [fn.bind(this)].concat(Array.prototype.slice.call(arrguments)));
    };
}

if (Prototype.Browser.Opera) {
    wrap(Element.Methods, "readAttribute", function (original, elem, attr) {
        return attr == "title" ? elem.title : original(elem, attr);
    });
}

即时函数

利用即时函数创建一个临时的作用域,用于存储数据状态。

通过参数限制作用域内的名称

![](./ww.jpg)

    <script>
        // 定义$表示其它内容
        $ = function(){alert('no jquery!');};

        (function($){
            $('img').on('click',function(event){
                $(event.target).addClass('clickedOn');
            })
        })(jQuery);
    </script>
</body>

使用简洁名称让代码保持可读性

(function(v){
    Object.extend(v,{
        href: v._getAttr,
        src:  v._getAttr,
        type: v._getAttr,
        ...
    });
})(Element.attributeTranslations.read.values);

利用即时函数处理迭代问题

<body>
    <div>DIV 1</div>
    <div>DIV 2</div>
    <div>DIV 3</div>

    <script>
        var div = document.getElementsByTagName("div");

        for(var i=0; i<div.length; i++){
            (function(n){
                div[n].addEventListener("click",function(){
                    alert("div #" + n + " was clicked.");
                },false);
            })(i);
        }
    </script>
</body>

类库包装

使用闭包和即时函数,让类库尽可能的保持私有,并且可以有选择性的让一些变量暴露到全局命名空间内。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,932评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,554评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,894评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,442评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,347评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,899评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,325评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,980评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,196评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,163评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,085评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,826评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,389评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,501评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,753评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,171评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,616评论 2 339

推荐阅读更多精彩内容