## 作用域鏈和閉包優化
> * 作用域。
>
>
> * 作用域(`scope`)是`JAVASCRIPT`編程中一個重要的`運行機制`,在`JAVASCRIPT`同步和異步編程以及`JAVASCRIPT`內存管理中起著至關重要的作用。
> * 在`JAVASCRIPT`中,能形成作用域的有如下幾點。
>
>
> * 函數的調用
> * with語句
> * `with`會創建自已的作用域,因此會增加其中執行代碼的作用域的長度。
> * 全局作用域。
>
> 以下代碼為例:
>
>
>
> ~~~
> var foo = function() {
> var local = {};
> };
> foo();
> console.log(local); //=> undefined
>
> var bar = function() {
> local = {};
> };
> bar();
> console.log(local); //=> {}
>
> /**這里我們定義了foo()函數和bar()函數,他們的意圖都是為了定義一個名為local的變量。在foo()函數中,我們使用var語句來聲明定義了一個local變量,而因為函數體內部會形成一個作用域,所以這個變量便被定義到該作用域中。而且foo()函數體內并沒有做任何作用域延伸的處理,所以在該函數執行完畢后,這個local變量也隨之被銷毀。而在外層作用域中則無法訪問到該變量。而在bar()函數內,local變量并沒有使用var語句進行聲明,取而代之的是直接把local作為全局變量來定義。故外層作用域可以訪問到這個變量。**/
>
> local = {};
> // 這里的定義等效于
> global.local = {};
> ~~~
>
>
> * 作用域鏈
>
>
> * 在`JAVASCRIPT`編程中,會遇到多層函數嵌套的場景,這就是典型的作用域鏈的表示。
>
>
>
> ~~~
> function foo() {
> var val = 'hello';
> function bar() {
> function baz() {
> global.val = 'world;'
> };
> baz();
> console.log(val); //=> hello
> };
> bar();
> };
> foo();
>
> /**在`JAVASCRIPT`中,變量標識符的查找是從當前作用域開始向外查找,直到全局作用域為止。所以`JAVASCRIPT`代碼中對變量的訪問只能向外進行,而不能逆而行之。baz()函數的執行在全局作用域中定義了一個全局變量val。而在bar()函數中,對val這一標識符進行訪問時,按照從內到外的查找原則:在bar函數的作用域中沒有找到,便到上一層,即foo()函數的作用域中查找。然而,使大家產生疑惑的關鍵就在這里:本次標識符訪問在foo()函數的作用域中找到了符合的變量,便不會繼續向外查找,故在baz()函數中定義的全局變量val并沒有在本次變量訪問中產生影響。**/
> ~~~
>
>
> * 減少作用域鏈上的查找次數
>
>
> * `JAVASCRIPT`代碼在執行的時候,如果需要訪問一個變量或者一個函數的時候,它需要遍歷當前執行環境的作用域鏈,而遍歷是從這個作用域鏈的前端一級一級的向后遍歷,直到全局執行環境。
>
>
>
> ~~~
> /**效率低**/
> for(var i = 0; i < 10000; i++){
> var but1 = document.getElementById("but1");
> }
> /**效率高**/
> /**避免全局查找**/
> var doc = document;
> for(var i = 0; i < 10000; i++){
> var but1 = doc.getElementById("but1");
> }
> /**上面代碼中,第二種情況是先把全局對象的變量放到函數里面先保存下來,然后直接訪問這個變量,而第一種情況是每次都遍歷作用域鏈,直到全局環境,我們看到第二種情況實際上只遍歷了一次,而第一種情況卻是每次都遍歷了,而且這種差別在多級作用域鏈和多個全局變量的情況下還會表現的非常明顯。在作用域鏈查找的次數是`O(n)`。通過創建一個指向`document`的局部變量,就可以通過限制一次全局查找來改進這個函數的性能。**/
> ~~~
>
>
> * 閉包
>
>
> * `JAVASCRIPT`中的標識符查找遵循從內到外的原則。
>
>
>
> ~~~
> function foo() {
> var local = 'Hello';
> return function() {
> return local;
> };
> }
> var bar = foo();
> console.log(bar()); //=> Hello
>
> /**這里所展示的讓外層作用域訪問內層作用域的技術便是閉包(Closure)。得益于高階函數的應用,使foo()函數的作用域得到`延伸`。foo()函數返回了一個匿名函數,該函數存在于foo()函數的作用域內,所以可以訪問到foo()函數作用域內的local變量,并保存其引用。而因這個函數直接返回了local變量,所以在外層作用域中便可直接執行bar()函數以獲得local變量。**/
> ~~~
>
>
> * 閉包是`JAVASCRIPT`的高級特性,因為把帶有??內部變量引用的函數帶出了函數外部,所以該作用域內的變量在函數執行完畢后的并不一定會被銷毀,直到內部變量的引用被全部解除。所以閉包的應用很容易造成內存無法釋放的情況。
> * 良好的閉包管理。
>
>
> * 循環事件綁定、私有屬性、含參回調等一定要使用閉包時,并謹慎對待其中的細節。
>
>
> * 循環綁定事件,我們假設一個場景:有六個按鈕,分別對應六種事件,當用戶點擊按鈕時,在指定的地方輸出相應的事件。
>
>
>
> ~~~
> var btns = document.querySelectorAll('.btn'); // 6 elements
> var output = document.querySelector('#output');
> var events = [1, 2, 3, 4, 5, 6];
> // Case 1
> for (var i = 0; i < btns.length; i++) {
> btns[i].onclick = function(evt) {
> output.innerText += 'Clicked ' + events[i];
> };
> }
> /**這里第一個解決方案顯然是典型的循環綁定事件錯誤,這里不細說,詳細可以參照我給一個網友的回答;而第二和第三個方案的區別就在于閉包傳入的參數。**/
> // Case 2
> for (var i = 0; i < btns.length; i++) {
> btns[i].onclick = (function(index) {
> return function(evt) {
> output.innerText += 'Clicked ' + events[index];
> };
> })(i);
> }
> /**第二個方案傳入的參數是當前循環下標,而后者是直接傳入相應的事件對象。事實上,后者更適合在大量數據應用的時候,因為在JavaScript的函數式編程中,函數調用時傳入的參數是基本類型對象,那么在函數體內得到的形參會是一個復制值,這樣這個值就被當作一個局部變量定義在函數體的作用域內,在完成事件綁定之后就可以對events變量進行手工解除引用,以減輕外層作用域中的內存占用了。而且當某個元素被刪除時,相應的事件監聽函數、事件對象、閉包函數也隨之被銷毀回收。**/
> // Case 3
> for (var i = 0; i < btns.length; i++) {
> btns[i].onclick = (function(event) {
> return function(evt) {
> output.innerText += 'Clicked ' + event;
> };
> })(events[i]);
> }
> ~~~
>
>
> * 避開閉包陷阱
> * 閉包是個強大的工具,但同時也是性能問題的主要誘因之一。不合理的使用閉包會導致內存泄漏。
> * 閉包的性能不如使用內部方法,更不如重用外部方法。
> * 由于`IE 9`瀏覽器的`DOM`節點作為`COM`對象來實現,`COM`的`內存管理`是通過引用計數的方式,引用計數有個難題就是循環引用,一旦`DOM`引用了閉包(例如`event handler`),閉包的上層元素又引用了這個`DOM`,就會造成循環引用從而導致內存泄漏。
> * 善用函數
>
>
> * 使用一個匿名函數在代碼的最外層進行包裹。
>
>
>
> ;(function() { // 主業務代碼 })();
>
>
>
> 有的甚至更高級一點:
>
>
>
> ~~~
> ;(function(win, doc, $, undefined) {
> // 主業務代碼
> })(window, document, jQuery);
> ~~~
>
>
>
> 甚至連如RequireJS, SeaJS, OzJS 等前端模塊化加載解決方案,都是采用類似的形式:
>
>
>
> ~~~
> /**RequireJS**/
> define(['jquery'], function($) {
> // 主業務代碼
> });
> /**SeaJS**/
> define('m??odule', ['dep', 'underscore'], function($, _) {
> // 主業務代碼
> });
> ~~~
>
>
>
> 被定義在全局作用域的對象,可能是會一直存活到進程退出的,如果是一個很大的對象,那就麻煩了。比如有的人喜歡在JavaScript中做模版渲染:
>
>
>
> ~~~
> <?php
> $db = mysqli_connect(server, user, password, 'myapp');
> $topics = mysqli_query($db, "SELECT * FROM topics;");
> ?>
> <!doctype html>
> <html lang="en">
> <head>
> <meta charset="UTF-8">
> <title>你是猴子請來的逗比么?</title>
> </head>
> <body>
> <ul id="topics"></ul>
> <script type="text/tmpl" id="topic-tmpl">
> <li class="topic">
> <h1><%=title%></h1>
> <p><%=content%></p>
> </li>
> </script>
> <script type="text/javascript">
> var data = <?php echo json_encode($topics); ?>;
> var topicTmpl = document.querySelector('#topic-tmpl').innerHTML;
> var render = function(tmlp, view) {
> var complied = tmlp
> .replace(/\n/g, '\\n')
> .replace(/<%=([\s\S]+?)%>/g, function(match, code) {
> return '" + escape(' + code + ') + "';
> });
>
> complied = [
> 'var res = "";',
> 'with (view || {}) {',
> 'res = "' + complied + '";',
> '}',
> 'return res;'
> ].join('\n');
>
> var fn = new Function('view', complied);
> return fn(view);
> };
>
> var topics = document.querySelector('#topics');
> function init()
> data.forEach(function(topic) {
> topics.innerHTML += render(topicTmpl, topic);
> });
> }
> init();
> </script>
> </body>
> </html>
> ~~~
>
>
>
> 在從數據庫中獲取到的數據的量是非常大的話,前端完成模板渲染以后,data變量便被閑置在一邊。可因為這個變量是被定義在全局作用域中的,所以`JAVASCRIPT`引擎不會將其回收銷毀。如此該變量就會一直存在于老生代堆內存中,直到頁面被關閉。可是如果我們作出一些很簡單的修改,在邏輯代碼外包裝一層函數,這樣效果就大不同了。當UI渲染完成之后,代碼對data的引用也就隨之解除,而在最外層函數執行完畢時,`JAVASCRIPT`引擎就開始對其中的對象進行檢查,data也就可以隨之被回收。