整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          JavaScript深入 閉包

          、理論上的閉包

          從技術理論的角度講,所有的JavaScript函數都是閉包。

          閉包定義:閉包是指那些能夠訪問自由變量的函數。

          自由變量:是指在函數中使用的,但既不是函數參數也不是函數的局部變量的變量。

          由此,可以看出閉包共有兩部分組成:閉包 = 函數 + 函數能夠訪問的自由變量

          舉個例子:

          var a = 1;
          
          function foo() {
              console.log(a);
          }
          foo();

          foo 函數可以訪問變量 a,但是 a 既不是 foo 函數的局部變量,也不是 foo 函數的參數,所以 a 就是自由變量。 那么,函數 foo + foo 函數訪問的自由變量 a 就構成了一個閉包……

          從技術理論的角度講,所有的JavaScript函數都是閉包。

          顯然上面講述的并不是我們實踐中用的閉包,我們再接著往下看。


          二、實踐上的閉包

          上面是理論上的閉包,其實還有一個實踐角度上的閉包。

          先舉個栗子:

          function checkscope(){
              var scope = "local scope";
              function f(){
                  return scope;
              }
              return f;
          }
          
          var foo = checkscope();
          console.log(foo());  					//'local scope'

          我們先分析一下這段代碼中執行上下文棧和執行上下文的變化情況。 注:如果看不懂以下的執行過程,建議先閱讀《JavaScript深入 執行上下文(五):整個過程》。

          這里直接給出簡要的執行過程:

          • (1)進入全局代碼,創建全局執行上下文,全局執行上下文壓入執行上下文棧
          • (2)全局執行上下文初始化
          • (3)執行 checkscope 函數,創建 checkscope 函數執行上下文,checkscope 執行上下文被壓入執行上下文棧
          • (4)checkscope 執行上下文初始化,創建活動對象、作用域鏈、this等
          • (5)checkscope 函數執行完畢,checkscope 執行上下文從執行上下文棧中彈出
          • (6)執行 f 函數,創建 f 函數執行上下文,f 執行上下文被壓入執行上下文棧
          • (7)f 執行上下文初始化,創建變量對象、作用域鏈、this等
          • (8)f 函數執行完畢,f 函數上下文從執行上下文棧中彈出

          了解到這個過程,我們應該思考一個問題,那就是: 當 f 函數執行的時候,checkscope 函數上下文已經被銷毀了啊(即從執行上下文棧中被彈出),怎么還會讀取到 checkscope 作用域下的 scope 值呢?

          1. [[scope]]屬性

          每個函數都有一個內部屬性[[scope]](即作用域鏈) 現在我們根據上面談的程序具體執行過程,來看下f函數的內部屬性[[scope]],即f 執行上下文維護的作用域鏈:

          fContext = {
              Scope: [AO, checkscopeContext.AO, globalContext.VO],
          }

          沒錯,, 就是因為上面這個作用域鏈,f 函數依然可以讀取到 checkscopeContext.AO 的值(即 變量scope)。 說明當 f 函數引用了 checkscopeContext.AO 中的值的時候,即使 checkscopeContext 被銷毀了,但是 JavaScript 依然會讓 checkscopeContext.AO 活在內存中,f 函數依然可以通過 f 函數的作用域鏈找到它。從而實現了閉包這個概念。

          那問題又來了:checkscopeContext都被銷毀了,為什么checkscopeContext.AO 還能活在內存中呢?

          如果你知道 JavaScript的垃圾回收機理,你就懂了。


          2. JavaScript垃圾回收

          JavaScript垃圾回收的機理:垃圾收集器 會跟蹤找出不再使用的變量,然后 每隔固定時間間隔 釋放掉其內存。

          再看上面的例子:

          • ① checkscope函數沒被誰引用或使用著,說明它執行完后會被垃圾收集器銷毀;checkscopeContext也沒被誰引用或使用著,所以在checkscope函數執行完畢后,它也會被一同銷毀。
          • ② checkscope返回的f函數被foo所引用著,說明我們還會使用f函數,所以f函數不被銷毀。且f函數執行上下文的[[scope]]屬性(即 作用域鏈)還引用著 checkscopeContext.AO,說明我們還會使用 checkscopeContext.AO,所以不被銷毀。

          綜述: 每個函數都有一個內部屬性[[scope]](即作用域鏈),而正因為f函數沒被銷毀,所以該屬性也被保留著;又因為作用域鏈的本質是一個指向 變量對象/活動對象 的指針列表(它只是引用 不包含實際對象),所以作用域鏈上的這些對象不會被垃圾收集器銷毀,所以我們可以通過f函數的作用域鏈找到 它的父級乃至父父級的變量。

          讓我們再看一遍實踐角度上閉包的定義:

          • (1)即使創建它的上下文已經銷毀,它仍然存在(比如,內部函數從父函數中返回)
          • (2)在代碼中引用了自由變量

          再總結一遍~

          ECMAScript中,閉包指的是:

          • 從理論角度:閉包指所有的函數。因為它們都在創建的時候就將上層上下文的數據保存起來了。哪怕是簡單的全局變量也是如此,因為函數中訪問全局變量就相當于是在訪問自由變量,這個時候使用最外層的作用域。
          • 從實踐角度:以下函數才算是閉包:即使創建它的上下文已經銷毀,它仍然存在(比如,內部函數從父函數中返回)在代碼中引用了自由變量


          三、必刷題

          例1:

          var data = [];
          
          for (var i = 0; i < 3; i++) {
            data[i] = function () {
              console.log(i);
            };
          }
          
          data[0]();
          data[1]();
          data[2]();

          輸出: 3 3 3

          讓我們分析一下原因: 當執行到 data[0] 函數之前,此時全局上下文的 VO 為:

          globalContext = {
             VO: { data: [...],   i: 3 }
          }

          當執行 data[0] 函數的時候,data[0] 函數的作用域鏈為:

          data[0]Context = {
             Scope: [AO, globalContext.VO]
          }

          data[0]Context 的 AO 并沒有 i 值,所以會從 globalContext.VO 中查找,i 為 3,所以打印的結果就是 3。

          data[1] 和 data[2] 是一樣的道理。

          所以讓我們改成閉包看看:

          var data = [];
          
          for (var i = 0; i < 3; i++) {
            data[i] = (function (i) {
               return function(){
                  console.log(i);
               }
            })(i);
          }
          
          data[0]();
          data[1]();
          data[2]();

          當執行到 data[0] 函數之前,此時全局上下文的 VO 仍為:

          globalContext = {
             VO: { data: [...],   i: 3 }
          }

          跟沒改之前一模一樣。

          但當執行 data[0] 函數(即 return的函數)的時候,其作用域鏈為:

          data[0]Context = {
             Scope: [AO, 匿名函數Context.AO,globalContext.VO]
          }

          匿名函數執行上下文的 AO 為:

          匿名函數Context = {
            AO: {
              arguments: {
                0: 0,
                length: 1
              },
              i: 0
            }
          }

          data[0]Context 的 AO 并沒有 i 值,所以會沿著作用域鏈從匿名函數Context.AO 中查找,這時候就會找 i 為 0,找到了就不會往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值為3),所以打印的結果就是 0。 data[1] 和 data[2] 是一樣的道理。


          其實我們要想輸出0 1 2,可以直接將上面代碼改為:

          var data = [];
          
          for (let i = 0; i < 3; i++) {
            data[i] = function () {
              console.log(i);
            };
          }
          
          data[0]();
          data[1]();
          data[2]();

          ES6中加入了塊級作用域。 我們每創建一個函數會創建一個作用域。同理上面代碼,我們用let聲明i后,相當于每進行一次for循環就創建了一個(塊級)作用域。每個作用域的AO都保存了一個不同的i值。

          執行 data[0] 函數時,由于我們要打印i值,所以會沿著作用域鏈回溯尋找:首先會在當前匿名函數的作用域尋找i值,發現沒有;再到塊級作用域中找,發現有且為0,所以打印0;之后同理,輸出:0 1 2


          例2:

          var globals = 0;
          function test(parameter){
             var outerVal = 0;
             var outerVal2 = 0;
             console.log('outerVal2:',++outerVal2);
             return function(){
                var innerVal = 0;
                console.log('globals:',++globals);
                console.log('outerVal:',++outerVal);
                console.log('innerVal:',++innerVal);
                console.log('parameter:',++parameter);
             }
          }
          
          var a = test(0);
          a();
          a();

          輸出:

          (1) 在執行到 a() 函數(即return的匿名函數)之前,此時全局上下文的 VO 為:

          globalContext = {
             VO: { 
               globals: 0, 
               test:? test(parameter),
               a:?() 
             }
          }

          而test函數執行上下文的 AO 為:

          testContext = {
              AO: {
                  arguments: {
                      0: 0,
                      length: 1
                  },
                  parameter: 0,
                  outerVal: 0,
                  outerVal2: 0,
              }
          }

          (2) 當執行 a() 函數的時候,a() 函數的作用域鏈為:

          aContext = {
              Scope: [AO, testContext.AO, globalContext.VO]
          }

          用上面的理論,解釋這個例子,輸出同理。

          這里需要注意的是:第一次執行a()時,創建了a函數的執行上下文(aContext),執行完后銷毀;第二次執行a()時,再次創建aContext,然后再次銷毀。雖然aContext兩次被銷毀,但a函數的[[scope]]屬性一直都留在內存里。


          下面我們看一個誤用閉包的例子~

          例3:

          function test(){
              var outerVal = 0;
              return function(){
                  console.log(++outerVal);
              }
          }
          test()();
          test()();

          輸出:1 1

          為什么不是輸出 1 2 呢? 首先你要知道,test()即返回的匿名函數,test()()即執行匿名函數。

          再根據 JS垃圾回收的機理: 因為test函數返回的匿名函數沒有被其他變量引用或使用著,說明我們不再繼續使用該匿名函數,所以垃圾收集器會將其銷毀。所以每次執行test()()后,變量outerVal都會被銷毀。


          為了方便閉包的分析,我們可以認為每個函數的[[scope]]屬性(即作用域鏈),一直活在內存里,無論這個函數是否執行完畢。

          些人會說語言學到最后不都差不多嗎?其實可以這樣講,也可以不這樣講。雖然每種語言的表達能力大部分是重合的,只是語法表現形式不一樣,但是由于歷史發展的原因,每種語言形成了自己的支撐環境,所以都有其主要的適用范圍。

          C、C++、Python和Java四種是通用編程語言,JavaScript和PHP算是Web環境的專用編程語言。

          C(令人崇拜的語言)

          由于其底層操作特性和歷史的積累,在嵌入式領域是當之無愧的王者。

          C++(神秘莫測的語言)

          是一種支持最廣泛編程范式的復雜語言,在高級語言當中,處理運行速度是最快的,大部分的游戲軟件,系統都是由C++來編寫的。

          Python(高端大氣上檔次的語言)

          作為一種靈活的輕便的通用型腳本語言,使用范圍比較廣,從應用軟件到Web開發都有它的身影,由于其解釋語言的特點,比較適合輕量級或原型開發;

          Java(有噱頭的語言)

          Java由于其跨平臺可移植性,在Web開發領域大放異彩,特別是在企業級Web開發,同時由于Android系統采用Java來開發應用程序,所以也隨著Android的發展而應用越發廣泛;

          JavaScript(有潛力的語言)

          JavaScript語言由于其是瀏覽器內置的腳本語言,是Web前端開發的主流,近年來由于google的V8引擎開源,出現了Node.js之類JavaScript后臺開發框架,把JavaScript的應用領域擴展到了Web后臺。

          PHP(低調奢華的語言)

          獨特的語法混合了C、Java、Perl以及PHP自創的語法。它可以比CGI或者Perl更快速地執行動態網頁;還可以執行編譯后代碼,編譯可以達到加密和優化代碼運行,使代碼運行更快。

          理清不同語言間主要語法特性的差異,才能更好的在合適的領域或場景下去應用合適的編程語言,以滿足我們所面對的需求。這六種語言都是從C語言發展而來,所以它們的語法都比較像C語言,下面我就主要語法特性對各個語言做一個對比。

          1、常量定義

          C:#define TEST 0

          C++:#define TEST 0

          或者

          const test = 0;

          Python:test = 0

          C#:不支持

          PHP:define('test', 1);

          Java:final int test = 0;

          分析:JavaScript不支持常量,C、C++都用特有的預定義宏,PHP用特殊的define語法,其它的都用定義不變變量的方式。

          2、變量定義

          C:int test = 0;

          C++:int test = 0;

          Python:test = 0

          JavaScript:val test = 0;

          PHP:$test = 0;

          Java:int test = 0;

          分析:這個最基本的都支持了。

          3、函數定義

          C:int test(int param){}

          C++:int test(int param){}

          Python:def test(param):

          JavaScript:function test(param){}

          PHP:function test($param){}

          Java:public class test{

          public int test(int param){} }

          分析:這個也是最基本的了,只是Java比較特殊,不支持定義類之外的函數。

          4、類定義(含繼承)

          C:不支持

          C++:class test2: public test1{}

          Python:class test2(test1):

          JavaScript:function test2(){}

          test2.prototype =inherit(test1.prototype){}

          PHP:class test2 extend test1{}

          Java:class test2 extends test1{}

          分析:C由于是傳統面向過程的語言不支持類,其他的都支持了,只是JavaScript的類模型比較特殊,把函數作為類來使用。

          5、對象定義

          C:不支持

          C++:test2 obj = new test2();

          Python:obj = test2()

          JavaScript:var obj = new test2();

          PHP:$obj = new test2();

          Java:test2 obj = new test2();

          分析:除了C外其它語言都是通過new一個對象。

          6、數組定義

          C:int a[] = {1, 2, 3};

          C++:int a[] = {1, 2, 3};

          Python:a = [1, 2, 3]

          JavaScript:var a = [1, 2, 3];

          PHP:$a = array("1", "2", "3");

          Java:int a[] = {1, 2, 3};

          分析:數組是語言的基本特性,都支持了,只是PHP通過類似函數調用的語法來完成。

          7、條件語句

          C:if (test > 0){}

          else if (test < 0){}

          else{}

          C++:if (test > 0){}

          else if (test < 0){}

          else{}

          Python:if test > 0:

          elif test < 0:

          else:

          JavaScript:if (test > 0){}

          else if (test < 0){}

          else{}

          PHP:if ($test > 0){}

          elseif ($test < 0){}

          else{}

          Java:if (test > 0){}

          else if (test < 0){}

          else{}

          分析:這是最基本的語句,都支持了。

          8、循環語句

          C:for (idx=0; idx<num; idx++){}

          C++:for (idx=0; idx<num; idx++){}

          Python:for idx in range(1,10):

          JavaScript:for (var idx=0; idx<num; idx++){}

          PHP:for ($idx=0; $idx<$num; $idx++){}

          Java:for (idx=0; idx<num; idx++){}

          分析:這個也是基本的語句,都支持了。

          9、foreach語句

          C:不支持

          C++:不支持

          Python:for i in a:

          或者

          for key in d:

          d[key]

          JavaScript:for(i in a){}

          PHP:foreach($a as $i){}

          Java:for(int i : a){}

          分析:foreach算是循環語句的一個變種,在操作順序容器的時候非常有用,可以看到C和C++不支持,其它的都語言內置支持了。

          10、打印語句

          C:printf("test: %d", val);

          C++:cout<<"test: "<<val<<endl;

          Python:print "test: "+val

          JavaScript:不支持

          PHP:echo "test: $val";

          Java:System.out.println("test :"+val);

          分析:打印算是語言所運行環境的支持庫功能,除了JavaScript外都支持了,因為JavaScript主要使用來操控DOM樹的,沒有自己的輸出窗口所以也沒必要支持。

          11、字符串定義

          C:char test[] = {"helloworld"};

          C++:String test = "helloworld";

          Python:test = "helloworld"

          JavaScript:var test = "helloworld";

          PHP:$test = "helloworld";

          Java:String test = "helloworld";

          分析:這個都支持了,其中C++、Java都是用標準庫來現實的。

          12、字符串串接

          C:test = strcat(test1, test2);

          C++:test = test1 + test2;(STL庫)

          Python:test = test1 + test2

          JavaScript:var test = test1 + test2;

          PHP:$test = $test1 .= $test2;

          Java:test = test1 + test2;

          分析:很有用的功能,除了C是用標準庫函數來實現,其它都是語言內置支持了。

          13、字符串分割

          C:不支持

          C++:test.substr(3, 8);

          Python:test[3:8]

          JavaScript:test.slice(3, 5);

          PHP:substr($test, 3, 5);

          Java:test.substring(3, 8);

          分析:常用的功能,C不支持,Python是語言內置支持,其他的都依靠庫來完成。

          14、字符串正則表達式

          C:不支持

          C++:不支持

          Python:test.replace("test1", "test2")

          JavaScript:test.replace(/test1/gi, "test2");

          PHP:str_replace($test, "test1", "test2");

          Java:test.replaceAll("test1", "test2");

          分析:常用的功能,可惜C、C++不支持,其他都有標準庫來支持。

          15、內置容器類型

          C:數組

          C++:數組

          順序容器 Vector

          關聯容器 Pair MapSet

          Python:列表/元組

          字典

          JavaScript:數組

          對象

          PHP:數組(含關聯數組)

          Java:數組

          序列 Collection

          映射表 Map

          分析:C最簡單只支持數組,其他都支持容器,不過主要還是順序容器和關聯容器兩大類。

          16、注釋方式

          C:/* */

          C++://

          Python:#

          JavaScript:/* */

          //

          PHP:/* */

          //

          #

          Java:/* */

          //

          分析:大概就/**/、//、#三種方式,各自支持情況不一。

          17、多線程支持

          C:支持

          C++:支持

          Python:支持

          JavaScript:不支持

          PHP:不支持

          Java:支持

          分析:四種通用編程語言都支持了,兩種專用編程語言都不支持。

          18、socket支持

          C:支持

          C++:支持

          Python:支持

          JavaScript:不支持

          PHP:支持

          Java:支持

          分析:除了JavaScript以外都支持,這也是JavaScript的應用領域限制所決定的。

          19、垃圾回收機制

          C:不支持

          C++:不支持

          Python:支持

          JavaScript:支持

          PHP:支持

          Java:支持

          分析:這是現代語言的重要機制,C和C++不支持,其他的都支持了。

          20、引入其他文件中的函數

          C:export int test();

          C++:export int test();

          Python:from test import *

          JavaScript:<script language='javascript' src="test.js"charset="utf-8"></script>

          PHP:require_once('test.php');

          或者

          include_once('test.php');

          Java:import java.util.test.*;

          分析:都支持,C和C++用export,Python和Java用import,JavaScript依靠HTML腳本,PHP用自己的函數調用。

          21、將字符串作為指令執行

          C:不支持

          C++:不支持

          Python:eval("port=5060")

          JavaScript:eval("port=5060;");

          PHP:eval("port=5060;");

          Java:Porcess proc = new ProcessBuilder(“test”).start();

          分析:很有用的一個動態語言特性,C和C++都不支持,Java要類庫來支持,其它的語言內置eval關鍵字.

          C/C++資料分享:

          需要的小伙伴們可以【點擊下方】鏈接哦~

          遞歸(英語:Recursion)

          程序調用自身的編程技巧稱為遞歸( recursion)。遞歸作為一種算法程序設計語言中廣泛應用。 一個過程或函數在其定義或說明中有直接或間接調用自身的一種方法,它通常把一個大型復雜的問題層層轉化為一個與原問題相似的規模較小的問題來求解,遞歸策略只需少量的程序就可描述出解題過程所需要的多次重復計算,大大地減少了程序的代碼量。遞歸的能力在于用有限的語句來定義對象的無限集合。一般來說,遞歸需要有邊界條件、遞歸前進段和遞歸返回段。當邊界條件不滿足時,遞歸前進;當邊界條件滿足時,遞歸返回。遞歸返回階段。當邊界條件不滿足時,遞歸前進;當邊界條件滿足時,遞歸返回

          下面實現一個函數 pow(x, n),它可以計算 xn 次方

          使用迭代的方式,如下:

          function pow(x, n) {
            let result = 1;
          
            // 再循環中,用 x 乘以 result n 次
            for (let i = 0; i < n; i++) {
              result *= x;
            }
            return result;
          }

          使用遞歸的方式,如下:

          function pow(x, n) {
            if (n == 1) {
              return x;
            } else {
              return x * pow(x, n - 1);
            }
          }

          pow(x, n) 被調用時,執行分為兩個分支:

                       if n==1  = x
                       /
          pow(x, n) =
                       \
                        else     = x * pow(x, n - 1)

          也就是說pow 遞歸地調用自身 直到 n == 1

          為了計算 pow(2, 4),遞歸變體經過了下面幾個步驟:

          1. pow(2, 4) = 2 * pow(2, 3)
          2. pow(2, 3) = 2 * pow(2, 2)
          3. pow(2, 2) = 2 * pow(2, 1)
          4. pow(2, 1) = 2

          因此,遞歸將函數調用簡化為一個更簡單的函數調用,然后再將其簡化為一個更簡單的函數,以此類推,直到結果

          尾遞歸

          尾遞歸,即在函數尾位置調用自身(或是一個尾調用本身的其他函數等等)。尾遞歸也是遞歸的一種特殊情形。尾遞歸是一種特殊的尾調用,即在尾部直接調用自身的遞歸函數

          尾遞歸在普通尾調用的基礎上,多出了2個特征:

          • 在尾部調用的是函數自身
          • 可通過優化,使得計算僅占用常量棧空間

          在遞歸調用的過程當中系統為每一層的返回點、局部量等開辟了棧來存儲,遞歸次數過多容易造成棧溢出

          這時候,我們就可以使用尾遞歸,即一個函數中所有遞歸形式的調用都出現在函數的末尾,對于尾遞歸來說,由于只存在一個調用記錄,所以永遠不會發生"棧溢出"錯誤

          實現一下階乘,如果用普通的遞歸,如下:

          function factorial(n) {
            if (n === 1) return 1;
            return n * factorial(n - 1);
          }
          
          factorial(5) // 120
          

          如果n等于5,這個方法要執行5次,才返回最終的計算表達式,這樣每次都要保存這個方法,就容易造成棧溢出,復雜度為O(n)

          如果我們使用尾遞歸,則如下:

          function factorial(n, total) {
            if (n === 1) return total;
            return factorial(n - 1, n * total);
          }
          
          factorial(5, 1) // 120

          可以看到,每一次返回的就是一個新的函數,不帶上一個函數的參數,也就不需要儲存上一個函數了。尾遞歸只需要保存一個調用棧,復雜度 O(1)

          應用場景

          數組求和

          function sumArray(arr, total) {
              if(arr.length === 1) {
                  return total
              }
              return sum(arr, total + arr.pop())
          }

          使用尾遞歸優化求斐波那契數列

          function factorial2 (n, start = 1, total = 1) {
              if(n <= 2){
                  return total
              }
              return factorial2 (n -1, total, total + start)
          }

          數組扁平化

          let a = [1,2,3, [1,2,3, [1,2,3]]]
          // 變成
          let a = [1,2,3,1,2,3,1,2,3]
          // 具體實現
          function flat(arr = [], result = []) {
              arr.forEach(v => {
                  if(Array.isArray(v)) {
                      result = result.concat(flat(v, []))
                  }else {
                      result.push(v)
                  }
              })
              return result
          }

          數組對象格式化

          let obj = {
              a: '1',
              b: {
                  c: '2',
                  D: {
                      E: '3'
                  }
              }
          }
          // 轉化為如下:
          let obj = {
              a: '1',
              b: {
                  c: '2',
                  d: {
                      e: '3'
                  }
              }
          }
          
          // 代碼實現
          function keysLower(obj) {
              let reg = new RegExp("([A-Z]+)", "g");
              for (let key in obj) {
                  if (obj.hasOwnProperty(key)) {
                      let temp = obj[key];
                      if (reg.test(key.toString())) {
                          // 將修改后的屬性名重新賦值給temp,并在對象obj內添加一個轉換后的屬性
                          temp = obj[key.replace(reg, function (result) {
                              return result.toLowerCase()
                          })] = obj[key];
                          // 將之前大寫的鍵屬性刪除
                          delete obj[key];
                      }
                      // 如果屬性是對象或者數組,重新執行函數
                      if (typeof temp === 'object' || Object.prototype.toString.call(temp) === '[object Array]') {
                          keysLower(temp);
                      }
                  }
              }
              return obj;
          };
          
          //案例1:求和,1-100
              function sun(n){
                  if(n==1) return 1
                  
              }
          //案例2:遞歸方法1,1,2,3,5,8,13,21,34,55,89…求第 n 項
             function fib(n) {
                  if (n === 1 || n === 2) return 1
                  return fib(n - 2) + fib(n - 1)
              }
              console.log(fib(3)) 
          //案例3:深拷貝
              function clone(o) {
                  var temp = {}
                  for (var key in o) {
                      if (typeof o[key] == 'object') {
                          temp[key] = clone(o[key])
                      } else {
                          temp[key] = o[key]
                      }
                  }
                  return temp
              }
          //案例4:遞歸組件
          //組件在它的模板內可以遞歸的調用自己,只要給組件設置 name 組件就可以了。
          //不過需要注意的是,必須給一個條件來限制數量,否則會拋出錯誤: max stack size exceeded
          //組件遞歸用來開發一些具體有未知層級關系的獨立組件。比如:聯級選擇器和樹形控件
              function clone(o) {
                  var temp = {}
                  for (var key in o) {
                      if (typeof o[key] == 'object') {
                          temp[key] = clone(o[key])
                      } else {
                          temp[key] = o[key]
                      }
                  }
                  return temp
              }
          //hash模式和history模式
          //這里的hash指的就是url后的 # 號以及后面的支付,比如說:www.baidu.com/#hashhash,其中#hashhash 就是我們期望的 hash值,
          //由于hash值的變化不會導致瀏覽器向服務器發送請求,而且在hash的改變會觸發hashchange事件,瀏覽器的前進后退也能對其進行控制,所以在H5的history模式出現之前,基本都是使用hash模式來實現前端路由,代碼如下
              window.addEventListener('hashchange',function(event){
                  let newUrl=event.newURL;//hash改變后的新的URL
                  let loadUrl=event.oldURL;//hash改變前的URL
              })
          
          //history模式,以下是history的相關API:
              history.go(-1);       // 后退一頁
              history.go(2);        // 前進兩頁
              history.forward();     // 前進一頁
              history.back();      // 后退一頁
              //規范新增
              history.pushState();         // 添加新的狀態到歷史狀態棧
              history.replaceState();      // 用新的狀態代替當前狀態
              history.state                // 返回當前狀態對象
          
          

          內存泄漏是什么

          內存泄漏(Memory leak)是在計算機科學中,由于疏忽或錯誤造成程序未能釋放已經不再使用的內存

          并非指內存在物理上的消失,而是應用程序分配某段內存后,由于設計錯誤,導致在釋放該段內存之前就失去了對該段內存的控制,從而造成了內存的浪費

          程序的運行需要內存。只要程序提出要求,操作系統或者運行時就必須供給內存

          對于持續運行的服務進程,必須及時釋放不再用到的內存。否則,內存占用越來越高,輕則影響系統性能,重則導致進程崩潰

          C語言中,因為是手動管理內存,內存泄露是經常出現的事情。

          char * buffer;
          buffer = (char*) malloc(42);
          
          // Do something with buffer
          
          free(buffer);

          上面是 C 語言代碼,malloc方法用來申請內存,使用完畢之后,必須自己用free方法釋放內存。

          這很麻煩,所以大多數語言提供自動內存管理,減輕程序員的負擔,這被稱為"垃圾回收機制"

          內存生命周期:


          內存也是有生命周期的,一般可以按順序分為三個周期:

          • 分配期(分配所需要的內存)
          • 使用期(使用分配到的內存(讀、寫))
          • 釋放期(不需要時將其釋放和歸還)

          內存分配 -> 內存使用 -> 內存釋放。

          內存管理機制:

          JavaScript是在創建變量(對象,字符串等)時自動進行了分配內存,并且在不使用它們時“自動”釋放。 釋放的過程稱為垃圾回收。

          JavaScript 內存管理機制和內存的生命周期是一一對應的。首先需要分配內存,然后使用內存,最后釋放內存。

          其中 JavaScript 語言不需要程序員手動分配內存,絕大部分情況下也不需要手動釋放內存,對 JavaScript 程序員來說通常就是使用內存(即使用變量、函數、對象等)。


          垃圾回收機制

          Javascript 具有自動垃圾回收機制(GC:Garbage Collecation),也就是說,執行環境會負責管理代碼執行過程中使用的內存

          原理:垃圾收集器會定期(周期性)找出那些不在繼續使用的變量,然后釋放其內存

          通常情況下有兩種實現方式:

          • 標記清除
          • 引用計數

          標記清除

          JavaScript最常用的垃圾收回機制

          當變量進入執行環境是,就標記這個變量為“進入環境“。進入環境的變量所占用的內存就不能釋放,當變量離開環境時,則將其標記為“離開環境“

          垃圾回收程序運行的時候,會標記內存中存儲的所有變量。然后,它會將所有在上下文中的變量,以及被在上下文中的變量引用的變量的標記去掉

          在此之后再被加上標記的變量就是待刪除的了,原因是任何在上下文中的變量都訪問不到它們了

          隨后垃圾回收程序做一次內存清理,銷毀帶標記的所有值并收回它們的內存

          舉個例子:

          var m = 0,n = 19 // 把 m,n,add() 標記為進入環境。
          add(m, n) // 把 a, b, c標記為進入環境。
          console.log(n) // a,b,c標記為離開環境,等待垃圾回收。
          function add(a, b) {
            a++
            var c = a + b
            return c
          }

          引用計數

          語言引擎有一張"引用表",保存了內存里面所有的資源(通常是各種值)的引用次數。如果一個值的引用次數是0,就表示這個值不再用到了,因此可以將這塊內存釋放

          如果一個值不再需要了,引用數卻不為0,垃圾回收機制無法釋放這塊內存,從而導致內存泄漏

          const arr = [1, 2, 3, 4];
          console.log('hello world');

          上面代碼中,數組[1, 2, 3, 4]是一個值,會占用內存。變量arr是僅有的對這個值的引用,因此引用次數為1。盡管后面的代碼沒有用到arr,它還是會持續占用內存

          如果需要這塊內存被垃圾回收機制釋放,只需要設置如下:

          arr = null

          通過設置arrnull,就解除了對數組[1,2,3,4]的引用,引用次數變為 0,就被垃圾回收了

          小結

          有了垃圾回收機制,不代表不用關注內存泄露。那些很占空間的值,一旦不再用到,需要檢查是否還存在對它們的引用。如果是的話,就必須手動解除引用

          常見內存泄露情況

          意外的全局變量

          一個未聲明變量的引用會在全局對象中創建一個新的變量。在瀏覽器的環境下,全局對象就是window,也就是說:

          function foo(arg) {
              bar = "this is a hidden global variable";
          }

          另一種意外的全局變量可能由 this 創建:

          function foo() {
              this.variable = "potential accidental global";
          }
          // foo 調用自己,this 指向了全局對象(window)
          foo();

          上述使用嚴格模式,可以避免意外的全局變量

          閉包引起的內存泄漏

          閉包可以使變量常駐內存,但如果使用不當就會在成內存泄漏

          var theThing = null;
          var replaceThing = function () {
            var originalThing = theThing;
            var unused = function () {
              if (originalThing)
                console.log("hi");
            };
            theThing = {
              longStr: new Array(1000000).join('*'),
              someMethod: function () {
                console.log(someMessage);
              }
            };
          };
          setInterval(replaceThing, 1000);

          上面代碼中,每次調用 replaceThing 時,theThing 都會得到新的包含一個大數組和新的閉包(someMethod)的對象。

          同時,沒有用到的那個變量持有一個引用了 originalThingreplaceThing 調用之前的 theThing)閉包。

          關鍵的問題是每當在同一個父作用域下創建閉包作用域的時候,這個作用域是被共享的。在這種情況下,someMethod 的閉包作用域和 unused 的作用域是共享的。

          unused 持有一個 originalThing 的引用。盡管 unused 從來沒有被使用過,someMethod 可以在 theThing 之外被訪問。

          而且 someMethodunused 共享了閉包作用域,即便 unused 從來都沒有被使用過,它對 originalThing 的引用還是強制它保持活躍狀態(阻止它被回收)。

          當這段代碼重復運行時,將可以觀察到內存消耗穩定地上漲,并且不會因為 GC 的存在而下降。

          本質上來講,創建了一個閉包鏈表(根節點是 theThing 形式的變量),而且每個閉包作用域都持有一個對大數組的間接引用,這導致了一個巨大的內存泄露。

          定時器也常會造成內存泄露

          var someResource = getData();
          setInterval(function() {
              var node = document.getElementById('Node');
              if(node) {
                  // 處理 node 和 someResource
                  node.innerHTML = JSON.stringify(someResource));
              }
          }, 1000);

          如果id為Node的元素從DOM中移除,該定時器仍會存在,同時,因為回調函數中包含對someResource的引用,定時器外面的someResource也不會被釋放

          包括我們之前所說的閉包,維持函數內局部變量,使其得不到釋放

          function bindEvent() {
            var obj = document.createElement('XXX');
            var unused = function () {
              console.log(obj, '閉包內引用obj obj不會被釋放');
            };
            obj = null; // 解決方法
          }

          沒有清理對DOM元素的引用同樣造成內存泄露

          const refA = document.getElementById('refA');
          document.body.removeChild(refA); // dom刪除了
          console.log(refA, 'refA'); // 但是還存在引用能console出整個div 沒有被回收
          refA = null;
          console.log(refA, 'refA'); // 解除引用

          包括使用事件監聽addEventListener監聽的時候,在不監聽的情況下使用removeEventListener取消對事件監聽

          怎樣避免內存泄漏

          1)減少不必要的全局變量,或者生命周期較長的對象,及時對無用的數據進行垃圾回收;

          2)注意程序邏輯,避免“死循環”之類的 ;

          3)避免創建過多的對象 原則:不用了的東西要及時歸還。


          給大家分享我收集整理的各種學習資料,前端小白交學習流程,入門教程等回答-下面是學習資料參考。

          前端學習交流、自學、學習資料等推薦 - 知乎


          主站蜘蛛池模板: 视频在线一区二区| 麻豆AV无码精品一区二区| 国产韩国精品一区二区三区| 亚洲无线码在线一区观看| 国精产品一区一区三区MBA下载| 日本一区二区三区高清| 国产一区视频在线免费观看| 中文字幕AV无码一区二区三区| 亚洲一区二区三区偷拍女厕| 国产成人高清精品一区二区三区| 九九无码人妻一区二区三区| 亚洲一区二区影视| 精品亚洲一区二区三区在线观看| 人妻精品无码一区二区三区| 亚洲电影唐人社一区二区| 久久精品国产一区| 久久国产三级无码一区二区| 一区二区三区在线播放| 无码欧精品亚洲日韩一区夜夜嗨| 人妻夜夜爽天天爽一区| 国产午夜精品一区二区三区极品| 3D动漫精品一区二区三区| 国产乱码一区二区三区四 | 天码av无码一区二区三区四区 | 亚洲av成人一区二区三区在线播放| 久久99国产精品一区二区| 无码人妻精品一区二区在线视频| 亚洲乱码一区二区三区在线观看| 最美女人体内射精一区二区| 国产无套精品一区二区| 亚洲国产老鸭窝一区二区三区 | 一区 二区 三区 中文字幕| 精品人妻少妇一区二区| 91午夜精品亚洲一区二区三区| 黑人大战亚洲人精品一区| 99久久精品午夜一区二区| 国产一区二区三区不卡在线观看| 国产SUV精品一区二区88L| 日韩在线一区二区| 乱中年女人伦av一区二区| 国产精久久一区二区三区|