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

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

1.HTML / XML / XHTML

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

2.AMD/CMD/CommonJs/ES6 Module

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

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

    首先咱們須要引入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()的第一參數。vue

    // 定義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 在推廣過程當中對模塊定義的規範化產出,延遲執行,推崇依賴就近java

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

    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推廣過程當中產生的。jquery

    /** 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加載模塊。git

    // 定義模塊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用同步的方式加載模塊。在服務端,模塊文件都存在本地磁盤,讀取很是快,因此這樣作不會有問題。可是在瀏覽器端,限於網絡緣由,更合理的方案是使用異步加載。github

  • 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)前端路由實現原理

    相關文章
    相關標籤/搜索