前端基本功-常見概念(一) 點這裏
前端基本功-常見概念(二) 點這裏
前端基本功-常見概念(三) 點這裏
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即可
本節參考文章:鏈接
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
請求報文
-
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}
響應報文
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
構造函數相比工廠模式:
- 沒有集中實例化
- 沒有返回對象實例
- 直接將屬性和方法賦值給this
- 解決了對象實例歸屬問題
構造函數編寫規範:
- 構造函數也是函數,但是函數名的第一個字母大寫
- 必須使用new運算符 + 函數名(首字母大寫)例如:var box = new Create();
構造函數和普通函數的區別:
- 普通函數,首字母無需大寫
- 構造函數,用普通函數調用方式無效
查看歸屬問題,要創建兩個構造函數:
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 樹無法描述的狀態下才能爲元素添加樣式,所以將其稱爲僞類。
僞元素
僞元素 用於創建一些不在文檔樹中
的元素,併爲其添加樣式。
我們可以通過 :before
來在一個元素前增加一些文本,併爲這些文本添加樣式。雖然用戶可以看到這些文本,但是這些文本實際上不在文檔樹中。
本節參考文章:前端面試題-僞類和僞元素、總結僞類與僞元素
8.DOMContentLoaded / load
DOM文檔加載的步驟爲:
- 解析HTML結構。
- DOM樹構建完成。//DOMContentLoaded
- 加載外部腳本和樣式表文件。
- 解析並執行腳本代碼。
- 加載圖片等外部文件。
- 頁面加載完畢。//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樹的生成需要整個文檔解析完畢。
我們再來看一下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-橫向滾動軸高度。
這裏寫圖片描述
offsetheight,它包含padding、border、橫向滾動軸高度。
offsetheight=padding+height+border+橫向滾動軸高度
scrollheight,可滾動高度,就是將滾動框拉直,不再滾動的高度,這個很好理解。 It includes the element’s padding, but not its border or margin.
本節參考文章:css clientheight、offsetheight、scrollheight詳解
11.use strict 有什麼意義和好處
- 使調試更加容易。那些被忽略或默默失敗了的代碼錯誤,會產生錯誤或拋出異常,因此儘早提醒你代碼中的問題,你才能更快地指引到它們的源代碼。
- 防止意外的全局變量。如果沒有嚴格模式,將值分配給一個未聲明的變量會自動創建該名稱的全局變量。這是JavaScript中最常見的錯誤之一。在嚴格模式下,這樣做的話會拋出錯誤。
- 消除 this 強制。如果沒有嚴格模式,引用null或未定義的值到 this 值會自動強制到全局變量。這可能會導致許多令人頭痛的問題和讓人恨不得拔自己頭髮的bug。在嚴格模式下,引用 null或未定義的 this 值會拋出錯誤。
-
不允許重複的屬性名稱或參數值。當檢測到對象中重複命名的屬性,例如:
var object = {foo: "bar", foo: "baz"};)
或檢測到函數中重複命名的參數時,例如:
function foo(val1, val2, val1){})
嚴格模式會拋出錯誤,因此捕捉幾乎可以肯定是代碼中的bug可以避免浪費大量的跟蹤時間。
- 使 eval() 更安全。在嚴格模式和非嚴格模式下, eval() 的行爲方式有所不同。最顯而易見的是,在嚴格模式下,變量和聲明在 eval() 語句內部的函數不會在包含範圍內創建(它們會在非嚴格模式下的包含範圍中被創建,這也是一個常見的問題源)。
- 在 delete 使用無效時拋出錯誤。 delete 操作符(用於從對象中刪除屬性)不能用在對象不可配置的屬性上。當試圖刪除一個不可配置的屬性時,非嚴格代碼將默默地失敗,而嚴格模式將在這樣的情況下拋出異常。
本節參考文章:經典面試題(4)
12.常見 JavaScript 內存泄漏
- 意外的全局變量
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 ,避免意外的全局變量。
-
被遺忘的計時器或回調函數
在 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 了。
-
脫離 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 元素引用的時候,要小心謹慎。
- 閉包
閉包是 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)前端路由實現原理