# 前端基本功-常見概念(三)

前端基本功-常見概念(一) 點這裏
前端基本功-常見概念(二) 點這裏
前端基本功-常見概念(三) 點這裏

1.HTML / XML / XHTML

  • html:超文本標記語言,顯示信息,不區分大小寫
  • xhtml:升級版的html,區分大小寫
  • xml:可擴展標記語言被用來傳輸和存儲數據

2.AMD/CMD/CommonJs/ES6 Module

  • AMD:AMD規範採用異步方式加載模塊,模塊的加載不影響它後面語句的運行。所有依賴這個模塊的語句,都定義在一個回調函數中,等到加載完成之後,這個回調函數纔會運行。

    AMD是requirejs 在推廣過程中對模塊定義的規範化產出,提前執行,推崇依賴前置。用define()定義模塊,用require()加載模塊,require.config()指定引用路徑等

    首先我們需要引入require.js文件和一個入口文件main.js。main.js中配置require.config()並規定項目中用到的基礎模塊。

        /** 網頁中引入require.js及main.js **/
        
        <script src="js/require.js" data-main="js/main"></script>
        
        /** main.js 入口文件/主模塊 **/
        // 首先用config()指定各模塊路徑和引用名
        require.config({
          baseUrl: "js/lib",
          paths: {
            "jquery": "jquery.min",  //實際路徑爲js/lib/jquery.min.js
            "underscore": "underscore.min",
          }
        });
        // 執行基本操作
        require(["jquery","underscore"],function($,_){
          // some code here
        });

    引用模塊的時候,我們將模塊名放在[]中作爲reqiure()的第一參數;如果我們定義的模塊本身也依賴其他模塊,那就需要將它們放在[]中作爲define()的第一參數。

        // 定義math.js模塊
        define(function () {
            var basicNum = 0;
            var add = function (x, y) {
                return x + y;
            };
            return {
                add: add,
                basicNum :basicNum
            };
        });
        // 定義一個依賴underscore.js的模塊
        define(['underscore'],function(_){
          var classify = function(list){
            _.countBy(list,function(num){
              return num > 30 ? 'old' : 'young';
            })
          };
          return {
            classify :classify
          };
        })
            
        // 引用模塊,將模塊放在[]內
        require(['jquery', 'math'],function($, math){
          var sum = math.add(10,20);
          $("#sum").html(sum);
        });
  • CMD:seajs 在推廣過程中對模塊定義的規範化產出,延遲執行,推崇依賴就近

    require.js在申明依賴的模塊時會在第一之間加載並執行模塊內的代碼:

        define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
            // 等於在最前面聲明並初始化了要用到的所有模塊
            if (false) {
              // 即便沒用到某個模塊 b,但 b 還是提前執行了
              b.foo()
            } 
        });

    CMD是另一種js模塊化方案,它與AMD很類似,不同點在於:AMD 推崇依賴前置、提前執行,CMD推崇依賴就近、延遲執行。此規範其實是在sea.js推廣過程中產生的。

        /** CMD寫法 **/
        define(function(require, exports, module) {
            var a = require('./a'); //在需要時申明
            a.doSomething();
            if (false) {
                var b = require('./b');
                b.doSomething();
            }
        });
        
    
        /** sea.js **/
        // 定義模塊 math.js
        define(function(require, exports, module) {
            var $ = require('jquery.js');
            var add = function(a,b){
                return a+b;
            }
            exports.add = add;
        });
        // 加載模塊
        seajs.use(['math.js'], function(math){
            var sum = math.add(1+2);
        });
  • CommonJs:Node.js是commonJS規範的主要實踐者,它有四個重要的環境變量爲模塊化的實現提供支持:module、exports、require、global。實際使用時,用module.exports定義當前模塊對外輸出的接口(不推薦直接用exports),用require加載模塊。

        // 定義模塊math.js
        var basicNum = 0;
        function add(a, b) {
          return a + b;
        }
        module.exports = { //在這裏寫上需要向外暴露的函數、變量
          add: add,
          basicNum: basicNum
        }
        
        // 引用自定義的模塊時,參數包含路徑,可省略.js
        var math = require('./math');
        math.add(2, 5);
        
        // 引用核心模塊時,不需要帶路徑
        var http = require('http');
        http.createService(...).listen(3000);

    commonJS用同步的方式加載模塊。在服務端,模塊文件都存在本地磁盤,讀取非常快,所以這樣做不會有問題。但是在瀏覽器端,限於網絡原因,更合理的方案是使用異步加載。

  • ES6 Module:ES6 在語言標準的層面上,實現了模塊功能,而且實現得相當簡單,旨在成爲瀏覽器和服務器通用的模塊解決方案。其模塊功能主要由兩個命令構成:export和import。export命令用於規定模塊的對外接口,import命令用於輸入其他模塊提供的功能。

    /** 定義模塊 math.js **/
    var basicNum = 0;
    var add = function (a, b) {
        return a + b;
    };
    export { basicNum, add };
    
    /** 引用模塊 **/
    import { basicNum, add } from './math';
    function test(ele) {
        ele.textContent = add(99 + basicNum);
    }

    如上例所示,使用import命令的時候,用戶需要知道所要加載的變量名或函數名。其實ES6還提供了export default命令,爲模塊指定默認輸出,對應的import語句不需要使用大括號。這也更趨近於ADM的引用寫法。

    /** export default **/
    //定義輸出
    export default { basicNum, add };
    //引入
    import math from './math';
    function test(ele) {
        ele.textContent = math.add(99 + math.basicNum);
    }

    ES6的模塊不是對象,import命令會被 JavaScript 引擎靜態分析,在編譯時就引入模塊代碼,而不是在代碼運行時加載,所以無法實現條件加載。也正因爲這個,使得靜態分析成爲可能。

ES6 模塊與 CommonJS 模塊的差異

  • CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。

    • CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。
    • ES6 模塊的運行機制與 CommonJS 不一樣。JS 引擎對腳本靜態分析的時候,遇到模塊加載命令import,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊裏面去取值。換句話說,ES6 的import有點像 Unix 系統的“符號連接”,原始值變了,import加載的值也會跟着變。因此,ES6 模塊是動態引用,並且不會緩存值,模塊裏面的變量綁定其所在的模塊。
  • CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。

    • 運行時加載: CommonJS 模塊就是對象;即在輸入時是先加載整個模塊,生成一個對象,然後再從這個對象上面讀取方法,這種加載稱爲“運行時加載”。

- 編譯時加載: ES6 模塊不是對象,而是通過 export 命令顯式指定輸出的代碼,import時採用靜態命令的形式。即在import時可以指定加載某個輸出值,而不是加載整個模塊,這種加載稱爲“編譯時加載”。


CommonJS 加載的是一個對象(即module.exports屬性),該對象只有在腳本運行完纔會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。


本節參考文章:前端模塊化:CommonJS,AMD,CMD,ES6

3.ES5的繼承/ES6的繼承

ES5的繼承時通過prototype或構造函數機制來實現。ES5的繼承實質上是先創建子類的實例對象,然後再將父類的方法添加到this上(Parent.apply(this))。

ES6的繼承機制完全不同,實質上是先創建父類的實例對象this(所以必須先調用父類的super()方法),然後再用子類的構造函數修改this

具體的:ES6通過class關鍵字定義類,裏面有構造方法,類之間通過extends關鍵字實現繼承。子類必須在constructor方法中調用super方法,否則新建實例報錯。因爲子類沒有自己的this對象,而是繼承了父類的this對象,然後對其進行加工。如果不調用super方法,子類得不到this對象。

ps:super關鍵字指代父類的實例,即父類的this對象。在子類構造函數中,調用super後,纔可使用this關鍵字,否則報錯。


區別:(以SubClass,SuperClass,instance爲例)

    • ES5中繼承的實質是:(那種經典寄生組合式繼承法)通過prototype或構造函數機制來實現,先創建子類的實例對象,然後再將父類的方法添加到this上(Parent.apply(this))。

      • 先由子類(SubClass)構造出實例對象this
      • 然後在子類的構造函數中,將父類(SuperClass)的屬性添加到this上,SuperClass.apply(this, arguments)
      • 子類原型(SubClass.prototype)指向父類原型(SuperClass.prototype)
      • 所以instance是子類(SubClass)構造出的(所以沒有父類的[[Class]]關鍵標誌)
      • 所以,instance有SubClass和SuperClass的所有實例屬性,以及可以通過原型鏈回溯,獲取SubClass和SuperClass原型上的方法
    • ES6中繼承的實質是:先創建父類的實例對象this(所以必須先調用父類的super()方法),然後再用子類的構造函數修改this

      • 先由父類(SuperClass)構造出實例對象this,這也是爲什麼必須先調用父類的super()方法(子類沒有自己的this對象,需先由父類構造)
      • 然後在子類的構造函數中,修改this(進行加工),譬如讓它指向子類原型(SubClass.prototype),這一步很關鍵,否則無法找到子類原型(注,子類構造中加工這一步的實際做法是推測出的,從最終效果來推測)
      • 然後同樣,子類原型(SubClass.prototype)指向父類原型(SuperClass.prototype)
      • 所以instance是父類(SuperClass)構造出的(所以有着父類的[[Class]]關鍵標誌)
      • 所以,instance有SubClass和SuperClass的所有實例屬性,以及可以通過原型鏈回溯,獲取SubClass和SuperClass原型上的方法

    靜態方法繼承實質上只需要更改下SubClass.__proto__到SuperClass即可

    clipboard.png

    本節參考文章:鏈接

    4.HTTP request報文/HTTP response報文

    請求報文 響應報文
    請求行 請求頭 空行 請求體 狀態行 響應頭 空行 響應體
    • HTTP request報文結構是怎樣的

      首行是Request-Line包括:請求方法,請求URI,協議版本,CRLF
      首行之後是若干行請求頭,包括general-header,request-header或者entity-header,每個一行以CRLF結束
      請求頭和消息實體之間有一個CRLF分隔
      根據實際請求需要可能包含一個消息實體 一個請求報文例子如下:

      GET /Protocols/rfc2616/rfc2616-sec5.html HTTP/1.1
      Host: www.w3.org
      Connection: keep-alive
      Cache-Control: max-age=0
      Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
      User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36
      Referer: https://www.google.com.hk/
      Accept-Encoding: gzip,deflate,sdch
      Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
      Cookie: authorstyle=yes
      If-None-Match: "2cc8-3e3073913b100"
      If-Modified-Since: Wed, 01 Sep 2004 13:24:52 GMT
      
      name=qiu&age=25

    請求報文

    clipboard.png

    • HTTP response報文結構是怎樣的

      首行是狀態行包括:HTTP版本,狀態碼,狀態描述,後面跟一個CRLF
      首行之後是若干行響應頭,包括:通用頭部,響應頭部,實體頭部
      響應頭部和響應實體之間用一個CRLF空行分隔
      最後是一個可能的消息實體 響應報文例子如下:

      HTTP/1.1 200 OK
      Date: Tue, 08 Jul 2014 05:28:43 GMT
      Server: Apache/2
      Last-Modified: Wed, 01 Sep 2004 13:24:52 GMT
      ETag: "40d7-3e3073913b100"
      Accept-Ranges: bytes
      Content-Length: 16599
      Cache-Control: max-age=21600
      Expires: Tue, 08 Jul 2014 11:28:43 GMT
      P3P: policyref="http://www.w3.org/2001/05/P3P/p3p.xml"
      Content-Type: text/html; charset=iso-8859-1
      
      {"name": "qiu", "age": 25}

    響應報文

    clipboard.png

    5.面向對象的工廠模式/構造函數

    工廠模式集中實例化了對象,避免實例化對象大量重複問題

    //工廠模式
    function createObject(a,b){
        var obj = new Object();    //集中實例化
        obj.a = a;
        obj.b = b;
        obj.c = function () {
            return this.a + this.b;
        };
        return obj;        //返回實例化對象
    }
    var box = createObject('abc',10);
    var box1 = createObject('abcdef',20);
    alert(box.c());        //返回abc10
    alert(box1.c());       //返回abcdef20
    //構造函數
    function Create(a,b) {
        this.a =a;
        this.b =b;
        this.c = function () {
            return this.a + this.b;
        };
    }
    var box = new Create('abc',10);
    alert(box.run());    //返回abc10

    構造函數相比工廠模式:

    1. 沒有集中實例化
    2. 沒有返回對象實例
    3. 直接將屬性和方法賦值給this
    4. 解決了對象實例歸屬問題

    構造函數編寫規範:

    1. 構造函數也是函數,但是函數名的第一個字母大寫
    2. 必須使用new運算符 + 函數名(首字母大寫)例如:var box = new Create();

    構造函數和普通函數的區別:

    1. 普通函數,首字母無需大寫
    2. 構造函數,用普通函數調用方式無效

    查看歸屬問題,要創建兩個構造函數:

    function Create(a,b) {
        this.a =a;
        this.b =b;
        this.c = function () {
            return this.a + this.b;
        };
    }
    
    function DeskTop(a,b) {
        this.a =a;
        this.b =b;
        this.c = function () {
            return this.a + this.b;
        };
    }
    
    var box = new Create('abc',10);
    var box1 = new DeskTop('def',20);
    alert(box instanceof Object);
    //這裏要注意:所有的構造函數的對象都是Object.
    alert(box instanceof Create);    //true
    alert(box1 instanceof Create);   //false
    alert(box1 instanceof DeskTop);    //true

    6. new Promise / Promise.resolve()

    Promise.resolve()可以生成一個成功的Promise

    Promise.resolve()語法糖

    例1:
    Promise.resolve('成功')等同於new Promise(function(resolve){resolve('成功')})

    例2:

    var resolved = Promise.resolve('foo');
    
    resolved.then((str) => 
        console.log(str);//foo
    )

    相當於

    var resolved = new Promise((resolve, reject) => {
       resolve('foo')
    });
    
    resolved.then((str) => 
        console.log(str);//foo
    )

    Promise.resolve方法有下面三種形式:

    • Promise.resolve(value);
    • Promise.resolve(promise);
    • Promise.resolve(theanable);

    這三種形式都會產生一個新的Promise。其中:

    • 第一種形式提供了自定義Promise的值的能力,它與Promise.reject(reason)對應。兩者的不同,在於得到的Promise的狀態不同。
    • 第二種形式,提供了創建一個Promise的副本的能力。
    • 第三種形式,是將一個類似Promise的對象轉換成一個真正的Promise對象。它的一個重要作用是將一個其他實現的Promise對象封裝成一個當前實現的Promise對象。例如你正在用bluebird,但是現在有一個Q的Promise,那麼你可以通過此方法把Q的Promise變成一個bluebird的Promise。

    實際上第二種形式可以歸在第三種形式中。

    本節參考文章:ES6中的Promise.resolve()

    推薦閱讀:性感的Promise...

    7.僞類 / 僞元素

    僞類

    僞類 用於當已有元素處於的某個狀態時,爲其添加對應的樣式,這個狀態是根據用戶行爲而動態變化的。

    當用戶懸停在指定的元素時,我們可以通過 :hover 來描述這個元素的狀態。雖然它和普通的 CSS 類相似,可以爲已有的元素添加樣式,但是它只有處於 DOM 樹無法描述的狀態下才能爲元素添加樣式,所以將其稱爲僞類。

    clipboard.png

    僞元素

    僞元素 用於創建一些不在文檔樹中的元素,併爲其添加樣式。

    我們可以通過 :before 來在一個元素前增加一些文本,併爲這些文本添加樣式。雖然用戶可以看到這些文本,但是這些文本實際上不在文檔樹中。

    clipboard.png

    本節參考文章:前端面試題-僞類和僞元素總結僞類與僞元素

    8.DOMContentLoaded / load

    clipboard.png

    DOM文檔加載的步驟爲:

    1. 解析HTML結構。
    2. DOM樹構建完成。//DOMContentLoaded
    3. 加載外部腳本和樣式表文件。
    4. 解析並執行腳本代碼。
    5. 加載圖片等外部文件。
    6. 頁面加載完畢。//load

    觸發的時機不一樣,先觸發DOMContentLoaded事件,後觸發load事件。

    原生js

    // 不兼容老的瀏覽器,兼容寫法見[jQuery中ready與load事件](http://www.imooc.com/code/3253),或用jQuery
    document.addEventListener("DOMContentLoaded", function() {
       // ...代碼...
    }, false);
    
    window.addEventListener("load", function() {
        // ...代碼...
    }, false);

    jQuery

    // DOMContentLoaded
    $(document).ready(function() {
        // ...代碼...
    });
    
    //load
    $(document).load(function() {
        // ...代碼...
    });
    • head 中資源的加載

      • head 中 js 資源加載都會停止後面 DOM 的構建,但是不影響後面資源的下載。
      • css資源不會阻礙後面 DOM 的構建,但是會阻礙頁面的首次渲染。
    • body 中資源的加載

      • body 中 js 資源加載都會停止後面 DOM 的構建,但是不影響後面資源的下載。
      • css 資源不會阻礙後面 DOM 的構建,但是會阻礙頁面的首次渲染。
    • DomContentLoaded 事件的觸發
      上面只是講了 html 文檔的加載與渲染,並沒有講 DOMContentLoaded 事件的觸發時機。直截了當地結論是,DOMContentLoaded 事件在 html文檔加載完畢,並且 html 所引用的內聯 js、以及外鏈 js 的同步代碼都執行完畢後觸發
      大家可以自己寫一下測試代碼,分別引用內聯 js 和外鏈 js 進行測試。
    • load 事件的觸發
      當頁面 DOM 結構中的 js、css、圖片,以及 js 異步加載的 js、css 、圖片都加載完成之後,纔會觸發 load 事件。

      注意:
      頁面中引用的js 代碼如果有異步加載的 js、css、圖片,是會影響 load 事件觸發的。video、audio、flash 不會影響 load 事件觸發。

    推薦閱讀:再談 load 與 DOMContentLoaded
    本節參考文章:DOMContentLoaded與load的區別事件DOMContentLoaded和load的區別

    9. 爲什麼將css放在頭部,將js文件放在尾部

    因爲瀏覽器生成Dom樹的時候是一行一行讀HTML代碼的,script標籤放在最後面就不會影響前面的頁面的渲染。那麼問題來了,既然Dom樹完全生成好後頁面才能渲染出來,瀏覽器又必須讀完全部HTML才能生成完整的Dom樹,script標籤不放在body底部是不是也一樣,因爲dom樹的生成需要整個文檔解析完畢。

    clipboard.png

    我們再來看一下chrome在頁面渲染過程中的,綠色標誌線是First Paint的時間。納尼,爲什麼會出現firstpaint,頁面的paint不是在渲染樹生成之後嗎?其實現代瀏覽器爲了更好的用戶體驗,渲染引擎將嘗試儘快在屏幕上顯示的內容。它不會等到所有HTML解析之前開始構建和佈局渲染樹。部分的內容將被解析並顯示。也就是說瀏覽器能夠渲染不完整的dom樹和cssom,儘快的減少白屏的時間。假如我們將js放在header,js將阻塞解析dom,dom的內容會影響到First Paint,導致First Paint延後。所以說我們會 將js放在後面,以減少First Paint的時間,但是不會減少DOMContentLoaded被觸發的時間

    本節參考文章:DOMContentLoaded與load的區別

    10.clientheight / offsetheight

    clientheight:內容的可視區域,不包含border。clientheight=padding+height-橫向滾動軸高度。

    clipboard.png

    這裏寫圖片描述

    offsetheight,它包含padding、border、橫向滾動軸高度。
    offsetheight=padding+height+border+橫向滾動軸高度

    clipboard.png

    scrollheight,可滾動高度,就是將滾動框拉直,不再滾動的高度,這個很好理解。 It includes the element’s padding, but not its border or margin.

    clipboard.png

    本節參考文章:css clientheight、offsetheight、scrollheight詳解

    11.use strict 有什麼意義和好處

    1. 使調試更加容易。那些被忽略或默默失敗了的代碼錯誤,會產生錯誤或拋出異常,因此儘早提醒你代碼中的問題,你才能更快地指引到它們的源代碼。
    2. 防止意外的全局變量。如果沒有嚴格模式,將值分配給一個未聲明的變量會自動創建該名稱的全局變量。這是JavaScript中最常見的錯誤之一。在嚴格模式下,這樣做的話會拋出錯誤。
    3. 消除 this 強制。如果沒有嚴格模式,引用null或未定義的值到 this 值會自動強制到全局變量。這可能會導致許多令人頭痛的問題和讓人恨不得拔自己頭髮的bug。在嚴格模式下,引用 null或未定義的 this 值會拋出錯誤。
    4. 不允許重複的屬性名稱或參數值。當檢測到對象中重複命名的屬性,例如:

      var object = {foo: "bar", foo: "baz"};)

      或檢測到函數中重複命名的參數時,例如:

      function foo(val1, val2, val1){})

      嚴格模式會拋出錯誤,因此捕捉幾乎可以肯定是代碼中的bug可以避免浪費大量的跟蹤時間。

    5. 使 eval() 更安全。在嚴格模式和非嚴格模式下, eval() 的行爲方式有所不同。最顯而易見的是,在嚴格模式下,變量和聲明在 eval() 語句內部的函數不會在包含範圍內創建(它們會在非嚴格模式下的包含範圍中被創建,這也是一個常見的問題源)。
    6. 在 delete 使用無效時拋出錯誤。 delete 操作符(用於從對象中刪除屬性)不能用在對象不可配置的屬性上。當試圖刪除一個不可配置的屬性時,非嚴格代碼將默默地失敗,而嚴格模式將在這樣的情況下拋出異常。

    本節參考文章:經典面試題(4)

    12.常見 JavaScript 內存泄漏

    1. 意外的全局變量

    JavaScript 處理未定義變量的方式比較寬鬆:未定義的變量會在全局對象創建一個新變量。在瀏覽器中,全局對象是 window 。

    function foo(arg) {
        bar = "this is a hidden global variable";
    }
    真相是:
    
    ```
    function foo(arg) {
        window.bar = "this is an explicit global variable";
    }
    ```
    函數 foo 內部忘記使用 var ,意外創建了一個全局變量。此例泄漏了一個簡單的字符串,無傷大雅,但是有更糟的情況。
    
    另一種意外的全局變量可能由 this 創建:
    
    ```
    function foo() {
        this.variable = "potential accidental global";
    }
    // Foo 調用自己,this 指向了全局對象(window)
    // 而不是 undefined
    foo();
    ```
    在 JavaScript 文件頭部加上 'use strict',可以避免此類錯誤發生。啓用嚴格模式解析 JavaScript ,避免意外的全局變量。
    
    1. 被遺忘的計時器或回調函數
      在 JavaScript 中使用 setInterval 非常平常。一段常見的代碼:

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

      此例說明了什麼:與節點或數據關聯的計時器不再需要,node 對象可以刪除,整個回調函數也不需要了。可是,計時器回調函數仍然沒被回收(計時器停止纔會被回收)。同時,someResource 如果存儲了大量的數據,也是無法被回收的。

      對於觀察者的例子,一旦它們不再需要(或者關聯的對象變成不可達),明確地移除它們非常重要。老的 IE 6 是無法處理循環引用的。如今,即使沒有明確移除它們,一旦觀察者對象變成不可達,大部分瀏覽器是可以回收觀察者處理函數的。

      觀察者代碼示例:

      var element = document.getElementById('button');
      function onClick(event) {
          element.innerHTML = 'text';
      }
      element.addEventListener('click', onClick);

      對象觀察者和循環引用注意事項

      老版本的 IE 是無法檢測 DOM 節點與 JavaScript 代碼之間的循環引用,會導致內存泄漏。如今,現代的瀏覽器(包括 IE 和 Microsoft Edge)使用了更先進的垃圾回收算法,已經可以正確檢測和處理循環引用了。換言之,回收節點內存時,不必非要調用 removeEventListener 了。

    2. 脫離 DOM 的引用
      有時,保存 DOM 節點內部數據結構很有用。假如你想快速更新表格的幾行內容,把每一行 DOM 存成字典(JSON 鍵值對)或者數組很有意義。此時,同樣的 DOM 元素存在兩個引用:一個在 DOM 樹中,另一個在字典中。將來你決定刪除這些行時,需要把兩個引用都清除。

      var elements = {
          button: document.getElementById('button'),
          image: document.getElementById('image'),
          text: document.getElementById('text')
      };
      function doStuff() {
          image.src = 'http://some.url/image';
          button.click();
          console.log(text.innerHTML);
          // 更多邏輯
      }
      function removeButton() {
          // 按鈕是 body 的後代元素
          document.body.removeChild(document.getElementById('button'));
          // 此時,仍舊存在一個全局的 #button 的引用
          // elements 字典。button 元素仍舊在內存中,不能被 GC 回收。
      }

      此外還要考慮 DOM 樹內部或子節點的引用問題。假如你的 JavaScript 代碼中保存了表格某一個 <td> 的引用。將來決定刪除整個表格的時候,直覺認爲 GC 會回收除了已保存的 <td> 以外的其它節點。實際情況並非如此:此 <td> 是表格的子節點,子元素與父元素是引用關係。由於代碼保留了 <td> 的引用,導致整個表格仍待在內存中。保存 DOM 元素引用的時候,要小心謹慎。

    3. 閉包

    閉包是 JavaScript 開發的一個關鍵方面:匿名函數可以訪問父級作用域的變量。

    避免濫用

    本節參考文章:4類 JavaScript 內存泄漏及如何避免

    13.引用計數 / 標記清除

    js垃圾回收有兩種常見的算法:引用計數和標記清除。

    • 引用計數就是跟蹤對象被引用的次數,當一個對象的引用計數爲0即沒有其他對象引用它時,說明該對象已經無需訪問了,因此就會回收其所佔的內存,這樣,當垃圾回收器下次運行就會釋放引用數爲0的對象所佔用的內存。
    • 標記清除法是現代瀏覽器常用的一種垃圾收集方式,當變量進入環境(即在一個函數中聲明一個變量)時,就將此變量標記爲“進入環境”,進入環境的變量是不能被釋放,因爲只有執行流進入相應的環境,就可能會引用它們。而當變量離開環境時,就標記爲“離開環境”。

      垃圾收集器在運行時會給儲存在內存中的所有變量加上標記,然後會去掉環境中的變量以及被環境中的變量引用的變量的標記,當執行完畢那些沒有存在引用 無法訪問的變量就被加上標記,最後垃圾收集器完成清除工作,釋放掉那些打上標記的變量所佔的內存。

     function problem() {
        var A = {};
        var B = {};
        A.a = B;
        B.a = A;
    }
    引用計數存在一個弊端就是循環引用問題(上邊)
    標記清除不存在循環引用的問題,是因爲當函數執行完畢之後,對象A和B就已經離開了所在的作用域,此時兩個變量被標記爲“離開環境”,等待被垃圾收集器回收,最後釋放其內存。

    分析以下代碼:

        function createPerson(name){
            var localPerson = new Object();
            localPerson.name = name;
            return localPerson;
        }
        var globalPerson = createPerson("Junga");
        globalPerson = null;//手動解除全局變量的引用

    在這個🌰中,變量globalPerson取得了createPerson()函數的返回的值。在createPerson()的內部創建了一個局部變量localPerson並添加了一個name屬性。由於localPerson在函數執行完畢之後就離開執行環境,因此會自動解除引用,而對於全局變量來說則需要我們手動設置null,解除引用。

    不過,解除一個值的引用並不意味着自動回收該值所佔用的內存,解除引用真正的作用是讓值脫離執行環境,以便垃圾收集器下次運行時將其收回。

    本節參考文章:JavaScript的內存問題

    14.前後端路由差別

    • 1.後端每次路由請求都是重新訪問服務器
    • 2.前端路由實際上只是JS根據URL來操作DOM元素,根據每個頁面需要的去服務端請求數據,返回數據後和模板進行組合。

    本節參考文章:2018前端面試總結...

    15.window.history / location.hash

    通常 SPA 中前端路由有2種實現方式:

    • window.history
    • location.hash

    下面就來介紹下這兩種方式具體怎麼實現的

    一.history

    1.history基本介紹

    window.history 對象包含瀏覽器的歷史,window.history 對象在編寫時可不使用 window 這個前綴。history是實現SPA前端路由是一種主流方法,它有幾個原始方法:

    • history.back() - 與在瀏覽器點擊後退按鈕相同
    • history.forward() - 與在瀏覽器中點擊按鈕向前相同
    • history.go(n) - 接受一個整數作爲參數,移動到該整數指定的頁面,比如go(1)相當於forward(),go(-1)相當於back(),go(0)相當於刷新當前頁面
    • 如果移動的位置超出了訪問歷史的邊界,以上三個方法並不報錯,而是靜默失敗

    在HTML5,history對象提出了 pushState() 方法和 replaceState() 方法,這兩個方法可以用來向歷史棧中添加數據,就好像 url 變化了一樣(過去只有 url 變化歷史棧纔會變化),這樣就可以很好的模擬瀏覽歷史和前進後退了,現在的前端路由也是基於這個原理實現的。

    2.history.pushState

    pushState(stateObj, title, url) 方法向歷史棧中寫入數據,其第一個參數是要寫入的數據對象(不大於640kB),第二個參數是頁面的 title, 第三個參數是 url (相對路徑)。

    • stateObj :一個與指定網址相關的狀態對象,popstate事件觸發時,該對象會傳入回調函數。如果不需要這個對象,此處可以填null。
    • title:新頁面的標題,但是所有瀏覽器目前都忽略這個值,因此這裏可以填null。
    • url:新的網址,必須與當前頁面處在同一個域。瀏覽器的地址欄將顯示這個網址。

    關於pushState,有幾個值得注意的地方:

    • pushState方法不會觸發頁面刷新,只是導致history對象發生變化,地址欄會有反應,只有當觸發前進後退等事件(back()和forward()等)時瀏覽器纔會刷新
    • 這裏的 url 是受到同源策略限制的,防止惡意腳本模仿其他網站 url 用來欺騙用戶,所以當違背同源策略時將會報錯

    3.history.replaceState

    replaceState(stateObj, title, url) 和pushState的區別就在於它不是寫入而是替換修改瀏覽歷史中當前紀錄,其餘和 pushState一模一樣

    4.popstate事件

    • 定義:每當同一個文檔的瀏覽歷史(即history對象)出現變化時,就會觸發popstate事件。
    • 注意:僅僅調用pushState方法或replaceState方法 ,並不會觸發該事件,只有用戶點擊瀏覽器倒退按鈕和前進按鈕,或者使用JavaScript調用back、forward、go方法時纔會觸發。另外,該事件只針對同一個文檔,如果瀏覽歷史的切換,導致加載不同的文檔,該事件也不會觸發。
    • 用法:使用的時候,可以爲popstate事件指定回調函數。這個回調函數的參數是一個event事件對象,它的state屬性指向pushState和replaceState方法爲當前URL所提供的狀態對象(即這兩個方法的第一個參數)。

    5.history實現spa前端路由代碼

    <a class="api a">a.html</a>
    <a class="api b">b.html</a>
     // 註冊路由
        document.querySelectorAll('.api').forEach(item => {
          item.addEventListener('click', e => {
            e.preventDefault();
            let link = item.textContent;
            if (!!(window.history && history.pushState)) {
              // 支持History API
              window.history.pushState({name: 'api'}, link, link);
            } else {
              // 不支持,可使用一些Polyfill庫來實現
            }
          }, false)
        });
    
        // 監聽路由
        window.addEventListener('popstate', e => {
          console.log({
            location: location.href,
            state: e.state
          })
        }, false)

    popstate監聽函數裏打印的e.state便是history.pushState()裏傳入的第一個參數,在這裏即爲{name: 'api'}

    二.Hash

    1.Hash基本介紹

    url 中可以帶有一個 hash http://localhost:9000/#/a.html

    window 對象中有一個事件是 onhashchange,以下幾種情況都會觸發這個事件:

    • 直接更改瀏覽器地址,在最後面增加或改變#hash;
    • 通過改變location.href或location.hash的值;
    • 通過觸發點擊帶錨點的鏈接;
    • 瀏覽器前進後退可能導致hash的變化,前提是兩個網頁地址中的hash值不同。

    2.Hash實現spa前端路由代碼

        // 註冊路由
        document.querySelectorAll('.api').forEach(item => {
          item.addEventListener('click', e => {
            e.preventDefault();
            let link = item.textContent;
            location.hash = link;
          }, false)
        });
    
        // 監聽路由
        window.addEventListener('hashchange', e => {
          console.log({
            location: location.href,
            hash: location.hash
          })
        }, false)

    本節參考文章:vue 單頁應用(spa)前端路由實現原理

    發表評論
    所有評論
    還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
    相關文章