濃縮解讀《JavaScript設(shè)計(jì)模式與開發(fā)實(shí)踐》③

三景东、閉包和高階函數(shù)

IMG_20170112_004128.jpg

3.1 閉包

3.1.1 變量的作用域

  • 所謂變量的作用域闻察,就是變量的有效范圍邻梆。通過作用域的劃分守伸,JavaScript變量分為全局變量和局部變量。
  • 聲明在函數(shù)外的變量為全局變量浦妄;在函數(shù)內(nèi)并且以var關(guān)鍵字聲明的變量為局部變量尼摹。
  • 我們都知道,全局變量能在任何作用域訪問到剂娄,但這很容易造成命名沖突蠢涝;而局部變量只有在函數(shù)里面能訪問到,這是因?yàn)镴avaScript的查找變量的規(guī)則是從內(nèi)往外搜索的阅懦。

3.1.2 變量的生命周期

  • 全局變量的生命周期是永久的(除非我們主動(dòng)銷毀這個(gè)全局變量)和二,而局部變量則當(dāng)函數(shù)執(zhí)行完畢時(shí)被銷毀。
  • 那JavaScript中是否存在耳胎,即便函數(shù)執(zhí)行完畢惯吕,依然不會(huì)被銷毀的局部變量惕它?答案是肯定的。
<script type="text/javascript">
    //現(xiàn)在有一個(gè)名為func的函數(shù)
    var func = function(){
        //①函數(shù)執(zhí)行體中,將局部變量a賦值為1
        var a = 1;
        //②返回一個(gè)function執(zhí)行環(huán)境
        return function(){
            //③執(zhí)行環(huán)境中毕荐,將func.a局部變量加1貌亭,然后輸出到控制臺(tái)
            a++;
            console.info(a);
        }
    };
    
    //調(diào)用:將func函數(shù)執(zhí)行后的返回,賦值給f
    var f = func();
    f();    //f()調(diào)用一次揭北,輸出2
    f();    //f()再調(diào)用一次,輸出3
    f();    //f()接著調(diào)用吏颖,輸出4
</script>
  • 以上案例中的func.a局部變量搔体,在func()函數(shù)執(zhí)行過后并沒有被銷毀。每次執(zhí)行f()時(shí)半醉,仍能對(duì)它進(jìn)行累加疚俱,就是佐證。這是因?yàn)?code>func()返回了一個(gè)匿名函數(shù)的引用賦值給f缩多,正是由于被外部變量引用了呆奕,所以不被銷毀,此時(shí)這個(gè)匿名函數(shù)就稱為閉包衬吆。
  • 什么是閉包梁钾?

    在一個(gè)函數(shù)內(nèi)定義另外一個(gè)函數(shù)(內(nèi)部函數(shù)可以訪問外部函數(shù)的變量),如果將這個(gè)內(nèi)部函數(shù)提供給其他變量引用時(shí)逊抡,內(nèi)部函數(shù)作用域以及依賴的外部作用域的執(zhí)行環(huán)境就不會(huì)被銷毀姆泻。此時(shí)這個(gè)內(nèi)部函數(shù)就像一個(gè)可以訪問封閉數(shù)據(jù)包的執(zhí)行環(huán)境冒嫡,也就是閉包。

3.1.3 閉包的用途

  • 我們不但要學(xué)習(xí)什么是JavaScript閉包方咆,更要了解如何利用閉包特性來寫代碼。由于篇幅有限蟀架,書中只羅列了幾個(gè)使用閉包的例子辜窑,但要知道實(shí)際開發(fā)中運(yùn)用閉包非常廣泛钩述,遠(yuǎn)不止于此。
  1. 封裝變量:通過閉包將不需要暴露的變量封裝成“私有變量”
var person = (function(){
    var name = "William";
    return function(){
        console.info(name);          
    };
})();
person();   // 輸出成功
console.info(person.name);  //// 輸出失敗
  1. 延續(xù)變量的生命周期:我們經(jīng)常用<img>標(biāo)簽進(jìn)行數(shù)據(jù)上報(bào)牙勘,創(chuàng)建一個(gè)臨時(shí)的img標(biāo)簽方面,將需要上報(bào)的數(shù)據(jù)附加在img的url后綴,從而上送到服務(wù)器操禀。如例子所示:
var report = function(dataSrc){
    var img = new Image();  //創(chuàng)建image對(duì)象
    img.src = dataSrc;  //將要上送的數(shù)據(jù)url賦值給img的url
};
report('http://xxx.com/uploadUserData?name=william');
  • 可經(jīng)過排查發(fā)現(xiàn)颓屑,使用report()函數(shù)存在30%丟失數(shù)據(jù)的情況揪惦。這是因?yàn)椋?code>img是report()函數(shù)中的局部變量罗侯,函數(shù)執(zhí)行完畢后就被銷毀了钩杰,而這個(gè)時(shí)候往往HTTP請(qǐng)求還沒建立成功讲弄。而通過閉包來保存img變量可以解決請(qǐng)求丟失的問題:
//注意:我們將普通函數(shù)改成了自執(zhí)行函數(shù)
var report = (function(){
    var imgs = [];
    return function(dataSrc){
        var img = new Image();
        images.push(img);
        img.src = dataSrc;
    }
})();
  1. 用閉包實(shí)現(xiàn)面向?qū)ο?/strong>:我們經(jīng)常使用過程數(shù)據(jù)來描述面向?qū)ο缶幊坍?dāng)中的對(duì)象。對(duì)象的方法包含了過程避除,而閉包則是在過程中以執(zhí)行環(huán)境的方式包含了數(shù)據(jù)驹饺。
  • 既然閉包可以封裝私有變量缴渊,自然也能完成面向?qū)ο蟮脑O(shè)計(jì)衔沼。實(shí)際上指蚁,用面向?qū)ο笏枷肽軐?shí)現(xiàn)的功能凝化,用閉包也能實(shí)現(xiàn),反之亦然混巧,這就是JavaScript的靈活之處咧党。
  • 有這樣一段面向?qū)ο蟮腏S代碼:
//Person構(gòu)造器傍衡,里面有一個(gè)name屬性
var Person = function(){
  this.name = "William";
};
//給Person的原型添加一個(gè)sayName()方法
Person.prototype.sayName = function(){
    console.info("hello,my name is " + this.name);
};
//實(shí)例化Person
var person1 = new Person();
person1.sayName();
  • 用閉包可以實(shí)現(xiàn)同樣的效果:因?yàn)樵贘avaScript用new執(zhí)行構(gòu)造函數(shù)蛙埂,本質(zhì)也是返回一個(gè)對(duì)象
//person()函數(shù)返回一個(gè)有sayName()方法的對(duì)象
var person = function(){
    var name = "William";
    return {
        sayName : function(){
            console.info("hello,my name is " + name);
        }
    }
};
//執(zhí)行person()函數(shù)箱残,將返回的對(duì)象賦值給person1
var person1 = person();
//調(diào)用person1.sayName()方法
person1.sayName();
//控制臺(tái)輸出 "hello,my name is William"
  1. 用閉包實(shí)現(xiàn)命令模式
  • 命令模式是將請(qǐng)求封裝成對(duì)象被辑,從而可以把不同的請(qǐng)求對(duì)象進(jìn)行參數(shù)化盼理、對(duì)請(qǐng)求對(duì)象排隊(duì)或者記錄日志以及執(zhí)行可撤銷的操作宏怔。
  • 命令模式的能夠分離請(qǐng)求發(fā)起者和執(zhí)行者之間的耦合關(guān)系臊诊。往往在命令被執(zhí)行之前抓艳,就預(yù)先往命令對(duì)象中植入命令的執(zhí)行者玷或。
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
        <button id="execute">開啟</button>
        <button id="undo">關(guān)閉</button>
        <script type="text/javascript">
            var Tv = {
                open : function(){
                    console.info("打開電視機(jī)");
                },
                close : function(){
                    console.info("關(guān)閉電視機(jī)");
                }
            };
            var OpenTvCommand = function(receiver){
                this.receiver = receiver;
            };
            OpenTvCommand.prototype.execute = function(){
                this.receiver.open();
            };
            OpenTvCommand.prototype.undo = function(){
                this.receiver.close();
            };
            var setCommand = function(command){
                document.getElementById("execute").onclick = function(){
                    command.execute();
                }
                document.getElementById("undo").onclick = function(){
                    command.undo();
                }
            };
            //調(diào)用
            setCommand(new OpenTvCommand(Tv));
        </script>
    </body>
</html>
  • 用閉包實(shí)現(xiàn)命令模式:
<script type="text/javascript">
    var Tv = {
        open : function(){
            console.info("打開電視機(jī)");
        },
        close : function(){
            console.info("關(guān)閉電視機(jī)");
        }
    };
    var createCommand = function(receiver){
        var execute = function(){
            return receiver.open();
        }
        var undo = function(){
            return receiver.close();
        }
        return {
            execute : execute,
            undo : undo
        }
    };
    var setCommand = function(command){
        document.getElementById("execute").onclick = function(){
            command.execute();
        }
        document.getElementById("undo").onclick = function(){
            command.undo();
        }
    };
    //調(diào)用
    setCommand(createCommand(Tv));
</script>
3.1.4 閉包與內(nèi)存管理
  • 一直流傳著一種聳人聽聞的說法,聲稱閉包會(huì)造成內(nèi)存泄漏位他,所以應(yīng)當(dāng)盡量避免使用閉包棱诱。
  • 局部變量本來應(yīng)該在函數(shù)退出的時(shí)候被釋放迈勋,但在閉包形成的環(huán)境中靡菇,局部變量不被釋放厦凤。從這個(gè)意義上看较鼓,確實(shí)會(huì)造成一些數(shù)據(jù)無法被及時(shí)銷毀博烂。但我們使用閉包禽篱,是我們主動(dòng)選擇延長(zhǎng)局部變量的生命周期,不能說成是內(nèi)存泄漏玛界。當(dāng)使用完畢后慎框,大可手動(dòng)將這些變量設(shè)為null鲤脏。
  • 而只有閉包形成循環(huán)引用的情況下,才會(huì)導(dǎo)致內(nèi)存泄漏努溃。但這也不是閉包或者JavaScript的問題梧税,我們可以避免循環(huán)引用的情況第队,而不是因噎廢食凳谦,徹底摒棄閉包尸执。

3.2 高階函數(shù)

  • 高階函數(shù)是指滿足以下兩個(gè)條件之一的函數(shù):
  1. 函數(shù)可以作為參數(shù)被傳遞如失;
  2. 函數(shù)可以作為返回值輸出褪贵;
  • 顯然脆丁,JavaScript語(yǔ)言中的函數(shù)兩個(gè)條件都滿足偎快,下面將講解JavaScript高階函數(shù)特性的應(yīng)用示例晒夹。丐怯。
3.2.1 函數(shù)作為參數(shù)傳入
  • 把函數(shù)當(dāng)做參數(shù)傳遞读跷,使得我們可以抽離出一部分容易變化的業(yè)務(wù)邏輯效览。
  • 這樣的例子在JavaScript代碼中比比皆是丐枉,比如jQuery中事件的綁定瘦锹,或者jQuery中的ajax請(qǐng)求:
//按鈕監(jiān)聽事件
$("btn").click(function(){
  console.info("btn clicked");
});
//可以發(fā)現(xiàn),其本質(zhì)就是執(zhí)行了click()方法泪掀,然后傳入一個(gè)函數(shù)作為參數(shù)。
//注意到:在按鈕點(diǎn)擊后的處理是變化的祝辣,通過回調(diào)函數(shù)來封裝變化蝙斜。
  • 另外還有Array.sort()孕荠。這是用來排序數(shù)組的一個(gè)方法稚伍,傳入一個(gè)自定義的函數(shù)來指定是遞增還是遞減排序个曙。
var arr = [1,7,9,2];
//從小到大排序
arr.sort(function(){
  return a - b;
});
console.info(arr); //輸出 "[1, 2, 7, 9]"
//從大道小排序
arr.sort(function(){
  return b - a;
});
console.info(arr); //輸出 "[9, 7, 2, 1]"
3.2.2 函數(shù)作為返回值輸出
  • 讓函數(shù)返回一個(gè)可執(zhí)行的函數(shù)垦搬,在之前的代碼我們已經(jīng)接觸過了猴贰,這使得整個(gè)運(yùn)算過程是可延續(xù)米绕。
  • 我們通過優(yōu)化一段類型判斷的JavaScript代碼來感受函數(shù)作為返回值輸出的靈活:
//判斷是否為String
var isString = function(obj){
    //通過傳入的obj對(duì)象執(zhí)行toString()方法栅干,將結(jié)果值和預(yù)期字符串比較
    return Object.prototype.toString.call(obj) === '[object String]';
}
//判斷是否為數(shù)組
var isArray = function(obj){
    return Object.prototype.toString.call(obj) === '[object Array]';
}
//判斷是否為數(shù)字
var isNumber = function(obj){
    return Object.prototype.toString.call(obj) === '[object Number]';
}
  • 可以發(fā)現(xiàn)上面的代碼toString部分都是相同的,我們通過將函數(shù)作為返回值的方式優(yōu)化代碼劫笙。
//抽象出一個(gè)類型判斷的通用函數(shù)
var isType = funcion(type){
    //該函數(shù)返回一個(gè)可執(zhí)行的函數(shù)填大,用來執(zhí)行toString方法和預(yù)期字符串做比較
    return function(obj){
        return Object.prototype.toString.call(obj) === '[Object '+type+']';
    }
}
//預(yù)先注冊(cè)具體的類型判斷方法
var isString = isType("String");
var isArray = isType("Array");
var isNumber = isType("Number");
//調(diào)用
console.info(isArray([1,3,2]));  //輸出: true
  • 另外一個(gè)例子允华,是利用JavaScript函數(shù)作為返回值這個(gè)特性實(shí)現(xiàn)單例模式靴寂。
var getSingle = function(fn){
    var ret;  //臨時(shí)變量
    return function(){
        //如果ret已經(jīng)存在的話則返回百炬;否則新創(chuàng)建對(duì)象
        return ret || (ret = fn.apply(this,arguments));
    }
}
var getScript = getSingle(function(){
    return document.createElement('script');
});
var script1 = getScript();
var script2 = getScript();
console.info(script1 === script2);//輸出: true
3.2.3 高階函數(shù)實(shí)現(xiàn)AOP
  • AOP(面向切面編程)是指將日志統(tǒng)計(jì)、安全控制德澈、異常處理等與業(yè)務(wù)邏輯無關(guān)的模塊代碼獨(dú)立出來梆造,通過“動(dòng)態(tài)植入”的方式參入到業(yè)務(wù)邏輯模塊當(dāng)中镇辉。這樣可以保持業(yè)務(wù)邏輯模塊的純凈和高內(nèi)聚性摊聋。
  • Java語(yǔ)言可以通過反射和動(dòng)態(tài)代理機(jī)制來實(shí)現(xiàn)AOP技術(shù)麻裁,而JavaScript函數(shù)作為返回值的特性就可以簡(jiǎn)單的實(shí)現(xiàn)煎源,這是JavaScript與生俱來的能力。
Function.prototype.invokeBefore = function(beforFn){
    var _self = this;   //原函數(shù)的引用
    return function(){
        //先執(zhí)行傳入的before函數(shù)
        beforFn.apply(this,arguments);
        //然后再執(zhí)行自身
        return _self.apply(this,arguments);
    }
}
Function.prototype.invokeAfter = function(afterFn){
    var _self = this;   //
    return function(){
        //先執(zhí)行函數(shù)锋拖,并保存執(zhí)行結(jié)果
        var ret = _self.apply(this,arguments);
        //然后再執(zhí)行after函數(shù)
        afterFn.apply(this,arguments);
        //最后返回結(jié)果
        return ret;
    }
}
//定義一個(gè)方法兽埃,控制臺(tái)輸出2
var func = function(){
    console.info(2);
};
//指定func()函數(shù)執(zhí)行前和執(zhí)行后要做的事情
func = func.invokeBefore(function(){
    console.info(1);
}).invokeAfter(function(){
    console.info(3);
});
//調(diào)用func()函數(shù)柄错,控制臺(tái)輸出 1 2 3
func();
3.2.4 高階函數(shù)實(shí)現(xiàn)柯里化
  • 函數(shù)柯里化(function currying)的概念是由注明數(shù)理邏輯學(xué)家Haskell Curry豐富和發(fā)展起來的给猾,所以因此得名敢伸。
  • currying又稱為部分求值详拙。currying函數(shù)首先接受一些參數(shù),接受這些參數(shù)之后并不立即求值,而是返回另外一個(gè)函數(shù),并將傳入的參數(shù)函數(shù)保存起來.等真正需要求值的時(shí)候,將之前傳入的所有參數(shù)一次性的求值.
  • 我們通過JavaScript,通過一個(gè)記賬的代碼來模擬currying函數(shù)
var currying = function(fn){
    var args = [];  //緩存對(duì)象
    return function(){
        if(arguments.length == 0){
            //如果傳入的參數(shù)為空饶辙,則直接返回結(jié)果
            return fn.apply(this,args);
        }else{
            //如果參數(shù)不為空,則將傳入?yún)?shù)push到args數(shù)組中緩存起來
            [].push.apply(args,arguments);
            //并返回函數(shù)本身
            return arguments.callee;
        }
    }
}
var cost = (function(){
    var money = 0;
    return function(){
        for(var i=0;l = arguments.length;i<l;i++){
            money += arguments[i];
        }
        return money;
    }
});
//轉(zhuǎn)換成currying函數(shù)
var cost = currying(cost);
cost(100);  //記賬100矿微,未真正求值
cost(100);  //記賬100涌矢,未真正求值
cost(400);  //記賬400娜庇,未真正求值
console.info(cost());   //求值名秀,并輸出:600
3.2.5 高階函數(shù)實(shí)現(xiàn)反柯里化
  • 通過call()apply()方法可以借用別的對(duì)象的方法,比如借用Array.prototype.push()方法.那么有沒有辦法將借用的方法提取出來呢?uncurrying就是用來解決這個(gè)問題的.
//為Function對(duì)象的原型添加uncurrying方法
Function.prototype.uncurrying = function(){
    var self = this;
    return function(){
        var obj = Array.prototype.call(arguments);
        return self.apply(obj,arguments);
    }
}
//提取push方法并使用
var push = Array.prototype.uncurrying();
(function(){
    push(arguments,4);
    console.info(arguments);//輸出 [1,2,3,4]
})(1,2,3);
3.2.6 高階函數(shù)實(shí)現(xiàn)函數(shù)節(jié)流
  • JavaScript中的函數(shù)大多數(shù)都是由用戶主動(dòng)觸發(fā)的,尤其在瀏覽器端的某些情況下函數(shù)被非常頻繁的調(diào)用,從而導(dǎo)致性能問題。
  • 比如用來監(jiān)聽瀏覽器窗口大小的window.onresize事件,當(dāng)瀏覽器窗口被不斷拉伸時(shí),這個(gè)事件觸發(fā)的頻率會(huì)非常高;又比如元素的拖拽監(jiān)聽事件onmousemove,如果元素被不停的拖拽,也會(huì)頻繁的觸發(fā)汁掠;還有最典型的監(jiān)聽文件上傳進(jìn)度的事件晋南,由于需要不斷掃描文件用以在頁(yè)面中顯示掃描進(jìn)度。導(dǎo)致通知的頻率非常之高姜凄,大約一秒鐘10次态秧,遠(yuǎn)超過人眼所能覺察的極限。
  • throttle函數(shù)就是解決此類問題的方案捐友。throttle顧名思義節(jié)流器,借鑒的是工程學(xué)里的思想,比如用節(jié)流器來穩(wěn)定短距離的管道的水壓或者氣壓,而在JavaScript中則是通過忽略短時(shí)間內(nèi)函數(shù)的密集執(zhí)行,達(dá)到穩(wěn)定性能的作用匣砖。
var throttle = function(fn,interval){
    var _self = fn,
            timer,
            firstTime = true;
    return function(){
        var args = arguments,
                _me = this;
        if(firstTime){
            _self.apply(_me,args);
            return firstTime = false;
        }
        if(timer){
            return false;
        }
        timer = setTimeout(function(){
            clearTimeout(timer);
            timer = null;
            _self.apply(_me,args);
        },interval || 500);
    };
};
window.onresize = throttle(function(){
    console.info("resize come in");
},500);
3.2.7 高階函數(shù)實(shí)現(xiàn)分時(shí)函數(shù)
  • 函數(shù)節(jié)流是限制函數(shù)被頻繁調(diào)用的解決方案,但還有另外一種情況,某些不能忽略的頻繁操作,同時(shí)也影響著頁(yè)面的性能。比如WebQQ加載好友列表拂共,往往需要短時(shí)間內(nèi)一次性創(chuàng)建成百上千個(gè)節(jié)點(diǎn)宜狐,嚴(yán)重影響頁(yè)面性能肌厨。
//模擬添加1000個(gè)數(shù)據(jù)
var ary = [];
for (var i=1;i<=1000;i++) {
    ary.push(i);
};
var renderFriendList = function(data){
    for (var i=0;l=data.length;i<l;i++) {
        var div = document.createElement('div');
        div.innerHTML = i;
        document.body.appendChild(div);
    }
};
renderFriendList(ary);
  • 通過分時(shí)函數(shù)讓創(chuàng)建節(jié)點(diǎn)的工作分批進(jìn)行。
//創(chuàng)建timeChunk函數(shù)
var timeChunk = function(ary,fn,count){
    var obj,t,len = ary.length;
    var start = function(){
        for (var i=0;i<Math.min(count || 1,ary.length);i++) {
            var obj = ary.shift();
            fn(obj);
        }
    }
    return function(){
        t = setInterval(function(){
            if(ary.length === 0){
                return clearInterval(t);
            }
            start();
        },200);
    }
};
//測(cè)試
var ary = [];
for (var i=1;i<=1000;i++) {
    ary.push(i);
};
var renderFriendList = timeChunk(ary,function(n){
    var div = document.createElement('div');
    div.innerHTML = i;
    document.body.appendChild(div);
},8);
renderFriendList(ary);
  • 除此之外馅而,書中還有通過高階函數(shù)的特性實(shí)現(xiàn)惰性加載函數(shù)的案例瓮恭,考慮到文章篇幅的關(guān)系屯蹦,這里就不贅述了登澜。

3.1 閉包

3.1.1 變量的作用域

  • 所謂變量的作用域,就是變量的有效范圍谴仙。通過作用域的劃分晃跺,JavaScript變量分為全局變量和局部變量哼审。
  • 聲明在函數(shù)外的變量為全局變量;在函數(shù)內(nèi)并且以var關(guān)鍵字聲明的變量為局部變量春霍。
  • 我們都知道址儒,全局變量能在任何作用域訪問到莲趣,但這很容易造成命名沖突;而局部變量只有在函數(shù)里面能訪問到潘鲫,這是因?yàn)镴avaScript的查找變量的規(guī)則是從內(nèi)往外搜索的溉仑。

3.1.2 變量的生命周期

  • 全局變量的生命周期是永久的(除非我們主動(dòng)銷毀這個(gè)全局變量)浊竟,而局部變量則當(dāng)函數(shù)執(zhí)行完畢時(shí)被銷毀怨喘。
  • 那JavaScript中是否存在,即便函數(shù)執(zhí)行完畢振定,依然不會(huì)被銷毀的局部變量哲思?答案是肯定的。
<script type="text/javascript">
    //現(xiàn)在有一個(gè)名為func的函數(shù)
    var func = function(){
        //①函數(shù)執(zhí)行體中吩案,將局部變量a賦值為1
        var a = 1;
        //②返回一個(gè)function執(zhí)行環(huán)境
        return function(){
            //③執(zhí)行環(huán)境中,將func.a局部變量加1徘郭,然后輸出到控制臺(tái)
            a++;
            console.info(a);
        }
    };
    
    //調(diào)用:將func函數(shù)執(zhí)行后的返回,賦值給f
    var f = func();
    f();    //f()調(diào)用一次丧肴,輸出2
    f();    //f()再調(diào)用一次残揉,輸出3
    f();    //f()接著調(diào)用,輸出4
</script>
  • 以上案例中的func.a局部變量芋浮,在func()函數(shù)執(zhí)行過后并沒有被銷毀抱环。每次執(zhí)行f()時(shí),仍能對(duì)它進(jìn)行累加纸巷,就是佐證镇草。這是因?yàn)?code>func()返回了一個(gè)匿名函數(shù)的引用賦值給f,正是由于被外部變量引用了瘤旨,所以不被銷毀梯啤,此時(shí)這個(gè)匿名函數(shù)就稱為閉包
  • 什么是閉包存哲?

    在一個(gè)函數(shù)內(nèi)定義另外一個(gè)函數(shù)(內(nèi)部函數(shù)可以訪問外部函數(shù)的變量)因宇,如果將這個(gè)內(nèi)部函數(shù)提供給其他變量引用時(shí),內(nèi)部函數(shù)作用域以及依賴的外部作用域的執(zhí)行環(huán)境就不會(huì)被銷毀祟偷。此時(shí)這個(gè)內(nèi)部函數(shù)就像一個(gè)可以訪問封閉數(shù)據(jù)包的執(zhí)行環(huán)境察滑,也就是閉包。

3.1.3 閉包的用途

  • 我們不但要學(xué)習(xí)什么是JavaScript閉包修肠,更要了解如何利用閉包特性來寫代碼贺辰。由于篇幅有限,書中只羅列了幾個(gè)使用閉包的例子,但要知道實(shí)際開發(fā)中運(yùn)用閉包非常廣泛魂爪,遠(yuǎn)不止于此先舷。
  1. 封裝變量:通過閉包將不需要暴露的變量封裝成“私有變量”
var person = (function(){
    var name = "William";
    return function(){
        console.info(name);          
    };
})();
person();   // 輸出成功
console.info(person.name);  //// 輸出失敗
  1. 延續(xù)變量的生命周期:我們經(jīng)常用<img>標(biāo)簽進(jìn)行數(shù)據(jù)上報(bào),創(chuàng)建一個(gè)臨時(shí)的img標(biāo)簽滓侍,將需要上報(bào)的數(shù)據(jù)附加在img的url后綴蒋川,從而上送到服務(wù)器。如例子所示:
var report = function(dataSrc){
    var img = new Image();  //創(chuàng)建image對(duì)象
    img.src = dataSrc;  //將要上送的數(shù)據(jù)url賦值給img的url
};
report('http://xxx.com/uploadUserData?name=william');
  • 可經(jīng)過排查發(fā)現(xiàn)撩笆,使用report()函數(shù)存在30%丟失數(shù)據(jù)的情況捺球。這是因?yàn)椋?code>img是report()函數(shù)中的局部變量,函數(shù)執(zhí)行完畢后就被銷毀了夕冲,而這個(gè)時(shí)候往往HTTP請(qǐng)求還沒建立成功氮兵。而通過閉包來保存img變量可以解決請(qǐng)求丟失的問題:
//注意:我們將普通函數(shù)改成了自執(zhí)行函數(shù)
var report = (function(){
    var imgs = [];
    return function(dataSrc){
        var img = new Image();
        images.push(img);
        img.src = dataSrc;
    }
})();
  1. 用閉包實(shí)現(xiàn)面向?qū)ο?/strong>:我們經(jīng)常使用過程數(shù)據(jù)來描述面向?qū)ο缶幊坍?dāng)中的對(duì)象。對(duì)象的方法包含了過程歹鱼,而閉包則是在過程中以執(zhí)行環(huán)境的方式包含了數(shù)據(jù)泣栈。
  • 既然閉包可以封裝私有變量,自然也能完成面向?qū)ο蟮脑O(shè)計(jì)弥姻。實(shí)際上南片,用面向?qū)ο笏枷肽軐?shí)現(xiàn)的功能,用閉包也能實(shí)現(xiàn)庭敦,反之亦然疼进,這就是JavaScript的靈活之處。
  • 有這樣一段面向?qū)ο蟮腏S代碼:
//Person構(gòu)造器秧廉,里面有一個(gè)name屬性
var Person = function(){
  this.name = "William";
};
//給Person的原型添加一個(gè)sayName()方法
Person.prototype.sayName = function(){
    console.info("hello,my name is " + this.name);
};
//實(shí)例化Person
var person1 = new Person();
person1.sayName();
  • 用閉包可以實(shí)現(xiàn)同樣的效果:因?yàn)樵贘avaScript用new執(zhí)行構(gòu)造函數(shù)伞广,本質(zhì)也是返回一個(gè)對(duì)象
//person()函數(shù)返回一個(gè)有sayName()方法的對(duì)象
var person = function(){
    var name = "William";
    return {
        sayName : function(){
            console.info("hello,my name is " + name);
        }
    }
};
//執(zhí)行person()函數(shù),將返回的對(duì)象賦值給person1
var person1 = person();
//調(diào)用person1.sayName()方法
person1.sayName();
//控制臺(tái)輸出 "hello,my name is William"
  1. 用閉包實(shí)現(xiàn)命令模式
  • 命令模式是將請(qǐng)求封裝成對(duì)象疼电,從而可以把不同的請(qǐng)求對(duì)象進(jìn)行參數(shù)化嚼锄、對(duì)請(qǐng)求對(duì)象排隊(duì)或者記錄日志以及執(zhí)行可撤銷的操作。
  • 命令模式的能夠分離請(qǐng)求發(fā)起者和執(zhí)行者之間的耦合關(guān)系蔽豺。往往在命令被執(zhí)行之前灾票,就預(yù)先往命令對(duì)象中植入命令的執(zhí)行者。
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
        <button id="execute">開啟</button>
        <button id="undo">關(guān)閉</button>
        <script type="text/javascript">
            var Tv = {
                open : function(){
                    console.info("打開電視機(jī)");
                },
                close : function(){
                    console.info("關(guān)閉電視機(jī)");
                }
            };
            var OpenTvCommand = function(receiver){
                this.receiver = receiver;
            };
            OpenTvCommand.prototype.execute = function(){
                this.receiver.open();
            };
            OpenTvCommand.prototype.undo = function(){
                this.receiver.close();
            };
            var setCommand = function(command){
                document.getElementById("execute").onclick = function(){
                    command.execute();
                }
                document.getElementById("undo").onclick = function(){
                    command.undo();
                }
            };
            //調(diào)用
            setCommand(new OpenTvCommand(Tv));
        </script>
    </body>
</html>
  • 用閉包實(shí)現(xiàn)命令模式:
<script type="text/javascript">
    var Tv = {
        open : function(){
            console.info("打開電視機(jī)");
        },
        close : function(){
            console.info("關(guān)閉電視機(jī)");
        }
    };
    var createCommand = function(receiver){
        var execute = function(){
            return receiver.open();
        }
        var undo = function(){
            return receiver.close();
        }
        return {
            execute : execute,
            undo : undo
        }
    };
    var setCommand = function(command){
        document.getElementById("execute").onclick = function(){
            command.execute();
        }
        document.getElementById("undo").onclick = function(){
            command.undo();
        }
    };
    //調(diào)用
    setCommand(createCommand(Tv));
</script>
3.1.4 閉包與內(nèi)存管理
  • 一直流傳著一種聳人聽聞的說法茫虽,聲稱閉包會(huì)造成內(nèi)存泄漏刊苍,所以應(yīng)當(dāng)盡量避免使用閉包。
  • 局部變量本來應(yīng)該在函數(shù)退出的時(shí)候被釋放濒析,但在閉包形成的環(huán)境中正什,局部變量不被釋放。從這個(gè)意義上看号杏,確實(shí)會(huì)造成一些數(shù)據(jù)無法被及時(shí)銷毀婴氮。但我們使用閉包斯棒,是我們主動(dòng)選擇延長(zhǎng)局部變量的生命周期,不能說成是內(nèi)存泄漏主经。當(dāng)使用完畢后荣暮,大可手動(dòng)將這些變量設(shè)為null
  • 而只有閉包形成循環(huán)引用的情況下罩驻,才會(huì)導(dǎo)致內(nèi)存泄漏穗酥。但這也不是閉包或者JavaScript的問題,我們可以避免循環(huán)引用的情況惠遏,而不是因噎廢食砾跃,徹底摒棄閉包。

3.2 高階函數(shù)

  • 高階函數(shù)是指滿足以下兩個(gè)條件之一的函數(shù):
  1. 函數(shù)可以作為參數(shù)被傳遞节吮;
  2. 函數(shù)可以作為返回值輸出抽高;
  • 顯然,JavaScript語(yǔ)言中的函數(shù)兩個(gè)條件都滿足透绩,下面將講解JavaScript高階函數(shù)特性的應(yīng)用示例翘骂。。
3.2.1 函數(shù)作為參數(shù)傳入
  • 把函數(shù)當(dāng)做參數(shù)傳遞帚豪,使得我們可以抽離出一部分容易變化的業(yè)務(wù)邏輯碳竟。
  • 這樣的例子在JavaScript代碼中比比皆是,比如jQuery中事件的綁定志鞍,或者jQuery中的ajax請(qǐng)求:
//按鈕監(jiān)聽事件
$("btn").click(function(){
  console.info("btn clicked");
});
//可以發(fā)現(xiàn),其本質(zhì)就是執(zhí)行了click()方法方仿,然后傳入一個(gè)函數(shù)作為參數(shù)固棚。
//注意到:在按鈕點(diǎn)擊后的處理是變化的,通過回調(diào)函數(shù)來封裝變化仙蚜。
  • 另外還有Array.sort()此洲。這是用來排序數(shù)組的一個(gè)方法,傳入一個(gè)自定義的函數(shù)來指定是遞增還是遞減排序委粉。
var arr = [1,7,9,2];
//從小到大排序
arr.sort(function(){
  return a - b;
});
console.info(arr); //輸出 "[1, 2, 7, 9]"
//從大道小排序
arr.sort(function(){
  return b - a;
});
console.info(arr); //輸出 "[9, 7, 2, 1]"
3.2.2 函數(shù)作為返回值輸出
  • 讓函數(shù)返回一個(gè)可執(zhí)行的函數(shù)呜师,在之前的代碼我們已經(jīng)接觸過了,這使得整個(gè)運(yùn)算過程是可延續(xù)贾节。
  • 我們通過優(yōu)化一段類型判斷的JavaScript代碼來感受函數(shù)作為返回值輸出的靈活:
//判斷是否為String
var isString = function(obj){
    //通過傳入的obj對(duì)象執(zhí)行toString()方法汁汗,將結(jié)果值和預(yù)期字符串比較
    return Object.prototype.toString.call(obj) === '[object String]';
}
//判斷是否為數(shù)組
var isArray = function(obj){
    return Object.prototype.toString.call(obj) === '[object Array]';
}
//判斷是否為數(shù)字
var isNumber = function(obj){
    return Object.prototype.toString.call(obj) === '[object Number]';
}
  • 可以發(fā)現(xiàn)上面的代碼toString部分都是相同的,我們通過將函數(shù)作為返回值的方式優(yōu)化代碼栗涂。
//抽象出一個(gè)類型判斷的通用函數(shù)
var isType = funcion(type){
    //該函數(shù)返回一個(gè)可執(zhí)行的函數(shù)知牌,用來執(zhí)行toString方法和預(yù)期字符串做比較
    return function(obj){
        return Object.prototype.toString.call(obj) === '[Object '+type+']';
    }
}
//預(yù)先注冊(cè)具體的類型判斷方法
var isString = isType("String");
var isArray = isType("Array");
var isNumber = isType("Number");
//調(diào)用
console.info(isArray([1,3,2]));  //輸出: true
  • 另外一個(gè)例子,是利用JavaScript函數(shù)作為返回值這個(gè)特性實(shí)現(xiàn)單例模式斤程。
var getSingle = function(fn){
    var ret;  //臨時(shí)變量
    return function(){
        //如果ret已經(jīng)存在的話則返回角寸;否則新創(chuàng)建對(duì)象
        return ret || (ret = fn.apply(this,arguments));
    }
}
var getScript = getSingle(function(){
    return document.createElement('script');
});
var script1 = getScript();
var script2 = getScript();
console.info(script1 === script2);//輸出: true
3.2.3 高階函數(shù)實(shí)現(xiàn)AOP
  • AOP(面向切面編程)是指將日志統(tǒng)計(jì)、安全控制、異常處理等與業(yè)務(wù)邏輯無關(guān)的模塊代碼獨(dú)立出來扁藕,通過“動(dòng)態(tài)植入”的方式參入到業(yè)務(wù)邏輯模塊當(dāng)中沮峡。這樣可以保持業(yè)務(wù)邏輯模塊的純凈和高內(nèi)聚性。
  • Java語(yǔ)言可以通過反射和動(dòng)態(tài)代理機(jī)制來實(shí)現(xiàn)AOP技術(shù)亿柑,而JavaScript函數(shù)作為返回值的特性就可以簡(jiǎn)單的實(shí)現(xiàn)邢疙,這是JavaScript與生俱來的能力。
Function.prototype.invokeBefore = function(beforFn){
    var _self = this;   //原函數(shù)的引用
    return function(){
        //先執(zhí)行傳入的before函數(shù)
        beforFn.apply(this,arguments);
        //然后再執(zhí)行自身
        return _self.apply(this,arguments);
    }
}
Function.prototype.invokeAfter = function(afterFn){
    var _self = this;   //
    return function(){
        //先執(zhí)行函數(shù)橄杨,并保存執(zhí)行結(jié)果
        var ret = _self.apply(this,arguments);
        //然后再執(zhí)行after函數(shù)
        afterFn.apply(this,arguments);
        //最后返回結(jié)果
        return ret;
    }
}
//定義一個(gè)方法秘症,控制臺(tái)輸出2
var func = function(){
    console.info(2);
};
//指定func()函數(shù)執(zhí)行前和執(zhí)行后要做的事情
func = func.invokeBefore(function(){
    console.info(1);
}).invokeAfter(function(){
    console.info(3);
});
//調(diào)用func()函數(shù),控制臺(tái)輸出 1 2 3
func();
3.2.4 高階函數(shù)實(shí)現(xiàn)柯里化
  • 函數(shù)柯里化(function currying)的概念是由注明數(shù)理邏輯學(xué)家Haskell Curry豐富和發(fā)展起來的式矫,所以因此得名乡摹。
  • currying又稱為部分求值。currying函數(shù)首先接受一些參數(shù),接受這些參數(shù)之后并不立即求值,而是返回另外一個(gè)函數(shù),并將傳入的參數(shù)函數(shù)保存起來.等真正需要求值的時(shí)候,將之前傳入的所有參數(shù)一次性的求值.
  • 我們通過JavaScript,通過一個(gè)記賬的代碼來模擬currying函數(shù)
var currying = function(fn){
    var args = [];  //緩存對(duì)象
    return function(){
        if(arguments.length == 0){
            //如果傳入的參數(shù)為空采转,則直接返回結(jié)果
            return fn.apply(this,args);
        }else{
            //如果參數(shù)不為空聪廉,則將傳入?yún)?shù)push到args數(shù)組中緩存起來
            [].push.apply(args,arguments);
            //并返回函數(shù)本身
            return arguments.callee;
        }
    }
}
var cost = (function(){
    var money = 0;
    return function(){
        for(var i=0;l = arguments.length;i<l;i++){
            money += arguments[i];
        }
        return money;
    }
});
//轉(zhuǎn)換成currying函數(shù)
var cost = currying(cost);
cost(100);  //記賬100,未真正求值
cost(100);  //記賬100故慈,未真正求值
cost(400);  //記賬400板熊,未真正求值
console.info(cost());   //求值,并輸出:600
3.2.5 高階函數(shù)實(shí)現(xiàn)反柯里化
  • 通過call()apply()方法可以借用別的對(duì)象的方法,比如借用Array.prototype.push()方法.那么有沒有辦法將借用的方法提取出來呢?uncurrying就是用來解決這個(gè)問題的.
//為Function對(duì)象的原型添加uncurrying方法
Function.prototype.uncurrying = function(){
    var self = this;
    return function(){
        var obj = Array.prototype.call(arguments);
        return self.apply(obj,arguments);
    }
}
//提取push方法并使用
var push = Array.prototype.uncurrying();
(function(){
    push(arguments,4);
    console.info(arguments);//輸出 [1,2,3,4]
})(1,2,3);
3.2.6 高階函數(shù)實(shí)現(xiàn)函數(shù)節(jié)流
  • JavaScript中的函數(shù)大多數(shù)都是由用戶主動(dòng)觸發(fā)的,尤其在瀏覽器端的某些情況下函數(shù)被非常頻繁的調(diào)用,從而導(dǎo)致性能問題察绷。
  • 比如用來監(jiān)聽瀏覽器窗口大小的window.onresize事件,當(dāng)瀏覽器窗口被不斷拉伸時(shí),這個(gè)事件觸發(fā)的頻率會(huì)非常高;又比如元素的拖拽監(jiān)聽事件onmousemove,如果元素被不停的拖拽,也會(huì)頻繁的觸發(fā)干签;還有最典型的監(jiān)聽文件上傳進(jìn)度的事件,由于需要不斷掃描文件用以在頁(yè)面中顯示掃描進(jìn)度拆撼。導(dǎo)致通知的頻率非常之高容劳,大約一秒鐘10次,遠(yuǎn)超過人眼所能覺察的極限闸度。
  • throttle函數(shù)就是解決此類問題的方案竭贩。throttle顧名思義節(jié)流器,借鑒的是工程學(xué)里的思想,比如用節(jié)流器來穩(wěn)定短距離的管道的水壓或者氣壓,而在JavaScript中則是通過忽略短時(shí)間內(nèi)函數(shù)的密集執(zhí)行,達(dá)到穩(wěn)定性能的作用。
var throttle = function(fn,interval){
    var _self = fn,
            timer,
            firstTime = true;
    return function(){
        var args = arguments,
                _me = this;
        if(firstTime){
            _self.apply(_me,args);
            return firstTime = false;
        }
        if(timer){
            return false;
        }
        timer = setTimeout(function(){
            clearTimeout(timer);
            timer = null;
            _self.apply(_me,args);
        },interval || 500);
    };
};
window.onresize = throttle(function(){
    console.info("resize come in");
},500);
3.2.7 高階函數(shù)實(shí)現(xiàn)分時(shí)函數(shù)
  • 函數(shù)節(jié)流是限制函數(shù)被頻繁調(diào)用的解決方案,但還有另外一種情況,某些不能忽略的頻繁操作,同時(shí)也影響著頁(yè)面的性能莺禁。比如WebQQ加載好友列表留量,往往需要短時(shí)間內(nèi)一次性創(chuàng)建成百上千個(gè)節(jié)點(diǎn),嚴(yán)重影響頁(yè)面性能哟冬。
//模擬添加1000個(gè)數(shù)據(jù)
var ary = [];
for (var i=1;i<=1000;i++) {
    ary.push(i);
};
var renderFriendList = function(data){
    for (var i=0;l=data.length;i<l;i++) {
        var div = document.createElement('div');
        div.innerHTML = i;
        document.body.appendChild(div);
    }
};
renderFriendList(ary);
  • 通過分時(shí)函數(shù)讓創(chuàng)建節(jié)點(diǎn)的工作分批進(jìn)行楼熄。
//創(chuàng)建timeChunk函數(shù)
var timeChunk = function(ary,fn,count){
    var obj,t,len = ary.length;
    var start = function(){
        for (var i=0;i<Math.min(count || 1,ary.length);i++) {
            var obj = ary.shift();
            fn(obj);
        }
    }
    return function(){
        t = setInterval(function(){
            if(ary.length === 0){
                return clearInterval(t);
            }
            start();
        },200);
    }
};
//測(cè)試
var ary = [];
for (var i=1;i<=1000;i++) {
    ary.push(i);
};
var renderFriendList = timeChunk(ary,function(n){
    var div = document.createElement('div');
    div.innerHTML = i;
    document.body.appendChild(div);
},8);
renderFriendList(ary);
  • 除此之外,書中還有通過高階函數(shù)的特性實(shí)現(xiàn)惰性加載函數(shù)的案例浩峡,考慮到文章篇幅的關(guān)系孝赫,這里就不贅述了。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末红符,一起剝皮案震驚了整個(gè)濱河市青柄,隨后出現(xiàn)的幾起案子伐债,更是在濱河造成了極大的恐慌,老刑警劉巖致开,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件峰锁,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡双戳,警方通過查閱死者的電腦和手機(jī)虹蒋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來飒货,“玉大人魄衅,你說我怎么就攤上這事√粮ǎ” “怎么了晃虫?”我有些...
    開封第一講書人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)扣墩。 經(jīng)常有香客問我哲银,道長(zhǎng),這世上最難降的妖魔是什么呻惕? 我笑而不...
    開封第一講書人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任荆责,我火速辦了婚禮,結(jié)果婚禮上亚脆,老公的妹妹穿的比我還像新娘做院。我一直安慰自己,他們只是感情好濒持,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開白布键耕。 她就那樣靜靜地躺著,像睡著了一般弥喉。 火紅的嫁衣襯著肌膚如雪郁竟。 梳的紋絲不亂的頭發(fā)上玛迄,一...
    開封第一講書人閱讀 51,754評(píng)論 1 307
  • 那天由境,我揣著相機(jī)與錄音,去河邊找鬼蓖议。 笑死虏杰,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的勒虾。 我是一名探鬼主播纺阔,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼修然!你這毒婦竟也來了笛钝?” 一聲冷哼從身側(cè)響起质况,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎玻靡,沒想到半個(gè)月后结榄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡囤捻,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年臼朗,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蝎土。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡视哑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出誊涯,到底是詐尸還是另有隱情挡毅,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布醋拧,位于F島的核電站慷嗜,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏丹壕。R本人自食惡果不足惜庆械,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望菌赖。 院中可真熱鬧缭乘,春花似錦、人聲如沸琉用。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)邑时。三九已至奴紧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間晶丘,已是汗流浹背黍氮。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留浅浮,地道東北人沫浆。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像滚秩,于是被迫代替她去往敵國(guó)和親专执。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容