InterviewMap —— Javascript (五)

1、垃圾回收機制和內存泄漏

不想C語言那樣,擁有原始底層的內存操做方法如 malloc free。js使用的是自動垃圾回收機制,也就是說js引擎會自動去判別變量的使用狀況來自動回收那些不使用的內存塊。javascript

即便是使用高級語言,開發者對內存管理也應該有所瞭解(至少要有基礎的瞭解)。有時,開發者必須理解自動內存管理會遇到問題(例如:垃圾回收中的錯誤或者性能問題等),以便可以正確處理它們。(或者是找到適當的解決方法,用最小的代價去解決。)css

若是一個值再也不須要了,可是垃圾回收機制確沒法回收,這時候就是內存泄漏了。html

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

上面代碼中,數組[1, 2, 3, 4]是一個值,會佔用內存。變量arr是僅有的對這個值的引用,所以引用次數爲1。儘管後面的代碼沒有用到arr,它仍是會持續佔用內存。前端

若是增長一行代碼,解除arr[1, 2, 3, 4]引用,這塊內存就能夠被垃圾回收機制釋放了。vue

const arr = [1, 2, 3, 4];
console.log('hello world');
arr = null; 
複製代碼

以上例子是在全局下的,arr爲全局變量,它屬於全局變量對象,全局變量對象只有在瀏覽器窗口關閉的時候纔會被銷燬,所以咱們纔會不推薦使用過多的全局變量。java

所以,並非說有了垃圾回收機制,程序員就輕鬆了。你仍是須要關注內存佔用:那些很佔空間的值,一旦再也不用到,你必須檢查是否還存在對它們的引用。若是是的話,就必須手動解除引用。node

一、內存的生命週期

內存每每經歷: 操做系統分配內存 == 使用內存 == 內存釋放 三個階段。

二、垃圾回收機制

(1)標記清除react

該算法由如下步驟組成:jquery

  • 垃圾回收器構建「roots」列表。Roots 一般是代碼中保留引用的全局變量。在 JavaScript 中,「window」 對象能夠做爲 root 全局變量示例。
  • 全部的 roots 被檢查並標記爲 active(即不是垃圾)。全部的 children 也被遞歸檢查。從 root 可以到達的一切都不被認爲是垃圾。
  • 全部未被標記爲 active 的內存能夠被認爲是垃圾了。收集器限制能夠釋放這些內存並將其返回到操做系統

若是是該算法,循環引用就不會出現。在函數調用後,兩個對象再也不被從全局對象可訪問的東西所引用。所以,垃圾回收器將發現它們是不可達的。nginx

(2)引用計數

若是一個值的引用次數是0,就表示這個值再也不用到了,所以能夠將這塊內存釋放。

上圖中,左下角的兩個值,沒有任何引用,因此能夠釋放。

function f() {
  var o1 = {};
  var o2 = {};
  o1.p = o2; // o1 references o2
  o2.p = o1; // o2 references o1. This creates a cycle.
}
 
f();
複製代碼

在函數調用以後,它們離開了做用域,所以它們實際上已經無用了,能夠被釋放了。然而,引用計數算法認爲,因爲兩個對象中的每個至少被引用了一次,因此也不能被垃圾回收。

三、什麼是內存泄漏

實質上,內存泄漏能夠被定義爲應用程序再也不須要的內存,但因爲某種緣由,內存不會返回到操做系統或可用內存池中。

四、內存泄漏的例子

(1)意外的全局變量

function foo(arg) { 
    bar = "this is a hidden global variable";
    //等同於window.bar="this is a hidden global variable"
    this.bar2= "potential accidental global";
    //這裏的this 指向了全局對象(window),等同於window.bar2="potential accidental global"
}
複製代碼

若是是在函數中未使用var聲明的變量,那麼會將其放到全局window上,會產生一個意外的全局變量。全局變量會一直駐留內存,一次咱們要堅定避免這種意外發生。

解決辦法就是使用'use strict'開啓嚴格模式。

(2)循環引用

let obj1 = { a: 1 }; // 一個對象(稱之爲 A)被建立,賦值給 obj1,A 的引用個數爲 1 
let obj2 = obj1; // A 的引用個數變爲 2 
  
obj1 = null; // A 的引用個數變爲 1 
obj2 = null; // A 的引用個數變爲 0,此時對象 A 就能夠被垃圾回收了
複製代碼

可是引用計數有個最大的問題: 循環引用。

function func() {  
    let obj1 = {};  
    let obj2 = {};  
  
    obj1.a = obj2; // obj1 引用 obj2 
    obj2.a = obj1; // obj2 引用 obj1 
}
複製代碼

函數執行完畢以後,按道理是能夠被銷燬的。內部的變量也會被銷燬。但根據引用計數方法,obj1 和 obj2 的引用次數都不爲 0,因此他們不會被回收。要解決循環引用的問題,最好是在不使用它們的時候手工將它們設爲空。上面的例子能夠這麼作:

obj1 = null;  
obj2 = null;
複製代碼

(3)被遺忘的計時器和回調函數

let someResource = getData();  
setInterval(() => {  
    const node = document.getElementById('Node');  
    if(node) {  
        node.innerHTML = JSON.stringify(someResource));  
    }  
}, 1000);
複製代碼

每隔一秒執行一次匿名回調函數,該函數因爲會被長期調用,所以其內部的變量都不會被回收,引用外部的someResource也不會被回收。那什麼才叫結束呢?就是調用了 clearInterval。

好比開發SPA頁面,當咱們的某一個頁面中存在這類定時器,跳轉到另外一個頁面的時候,其實這裏的定時器已經暫時沒用了,可是咱們在另外一個頁面的時候,內存中仍是回你保留上一個頁面的定時器資源,所以這就會致使內存泄漏。解決辦法就是即便的使用clearInterval來清除定時器。

(4)閉包

JavaScript 開發的一個關鍵方面就是閉包:一個能夠訪問外部(封閉)函數變量的內部函數。

值得注意的是閉包自己不會形成內存泄漏,但閉包過多很容易致使內存泄漏。閉包會形成對象引用的生命週期脫離當前函數的上下文,若是閉包若是使用不當,能夠致使環形引用(circular reference),相似於死鎖,只能避免,沒法發生以後解決,即便有垃圾回收也仍是會內存泄露。

(5)console

console.log:向web開發控制檯打印一條消息,經常使用來在開發時調試分析。有時在開發時,須要打印一些對象信息,但發佈時卻忘記去掉console.log語句,這可能形成內存泄露。

在傳遞給console.log的對象是不能被垃圾回收 ♻️,由於在代碼運行以後須要在開發工具能查看對象信息。因此最好不要在生產環境中console.log任何對象。

(6)DOM泄漏

在Js中對DOM操做是很是耗時的。由於JavaScript/ECMAScript引擎獨立於渲染引擎,而DOM是位於渲染引擎,相互訪問須要消耗必定的資源。

假如將JavaScript/ECMAScript、DOM分別想象成兩座孤島,兩島之間經過一座收費橋鏈接,過橋須要交納必定「過橋費」。JavaScript/ECMAScript每次訪問DOM時,都須要交納「過橋費」。所以訪問DOM次數越多,費用越高,頁面性能就會受到很大影響。

爲了減小DOM訪問次數,通常狀況下,當須要屢次訪問同一個DOM方法或屬性時,會將DOM引用緩存到一個局部變量中。但若是在執行某些刪除、更新操做後,可能會忘記釋放掉代碼中對應的DOM引用,這樣會形成DOM內存泄露。

var refA = document.getElementById('refA');
document.body.removeChild(refA);
 // #refA不能回收,由於存在變量refA對它的引用。將其對#refA引用釋放,但仍是沒法回收#refA。
 
 // 使用refA = null; 來釋放內存
複製代碼
var MyObject = {}; 
document.getElementById('myDiv').myProp = MyObject; 

解決方法: 
在window.onunload事件中寫上: document.getElementById('myDiv').myProp = null; 
複製代碼

給DOM對象用attachEvent綁定事件:

function doClick() {} 
element.attachEvent("onclick", doClick); 

解決方法: 
在onunload事件中寫上: element.detachEvent('onclick', doClick); 
複製代碼

從外到內執行appendChild。這時即便調用removeChild也沒法釋放。範例:

var parentDiv = document.createElement("div"); 
var childDiv = document.createElement("div"); 
document.body.appendChild(parentDiv); 
parentDiv.appendChild(childDiv); 
解決方法: 
從內到外執行appendChild: 
var parentDiv = document.createElement("div"); 
var childDiv = document.createElement("div"); 
parentDiv.appendChild(childDiv); 
document.body.appendChild(parentDiv); 
複製代碼

反覆重寫同一個屬性會形成內存大量佔用(但關閉IE後內存會被釋放)。範例:

for(i = 0; i < 5000; i++) { 
    hostElement.text = "asdfasdfasdf"; 
} 
這種方式至關於定義了5000個屬性! 
解決方法: 
其實沒什麼解決方法:P~~~就是編程的時候儘可能避免出現這種狀況咯~~ 
複製代碼

五、WeakMap 你瞭解嗎?

前面說過,及時清除引用很是重要。可是,你不可能記得那麼多,有時候一疏忽就忘了,因此纔有那麼多內存泄漏。

最好能有一種方法,在新建引用的時候就聲明,哪些引用必須手動清除,哪些引用能夠忽略不計,當其餘引用消失之後,垃圾回收機制就能夠釋放內存。這樣就能大大減輕程序員的負擔,你只要清除主要引用就能夠了。

ES6 考慮到了這一點,推出了兩種新的數據結構:WeakSet 和 WeakMap。它們對於值的引用都是不計入垃圾回收機制的,是一種弱引用,因此名字裏面纔會有一個"Weak",表示這是弱引用。

const wm = new WeakMap();

const element = document.getElementById('example'); // 引用計數1

wm.set(element, 'some information'); // 此處是弱引用,不計數
wm.get(element) // "some information" 
複製代碼

WeakMap裏面對element的引用就是弱引用,不會被計入垃圾回收機制。

也就是說,DOM節點對象的引用計數是1,而不是2。這時,一旦消除對該節點的引用,它佔用的內存就會被垃圾回收機制釋放。Weakmap保存的這個鍵值對,也會自動消失。

總結

雖然當下的瀏覽器已經對垃圾回收機制作出了必定的改進和提高,可是內存泄漏的問題咱們仍是須要關注的。

2、AJAX相關面試問題

一、什麼是Ajax

Ajax是全稱是asynchronous JavaScript andXML,簡答來記就是異步的js和XML。它是一種異步加載數據的機制,可使得咱們在不刷新整個頁面的狀況下去請求數據內容,實現局部刷新。

優勢就是:實現異步通訊,速度快,頁面局部刷新,用戶體驗好。AJAX出現以前一直是服務端渲染的天下,服務器去處理頁面的數據填充,而後響應頁面給咱們。

當須要修改頁面的時候,須要表單進行提交,而後服務器接收到請求後去查詢和處理數據,從新填充到頁面上,返回新的html頁面給咱們,這種交互的的缺陷是顯而易見的,任何和服務器的交互都須要刷新頁面,用戶體驗很是差,Ajax的出現解決了這個問題。

ajax能夠實現,咱們發送請求,獲取相應的數據,而後經過js去動態渲染頁面,而不須要服務器拼接HTML,頁面的刷新也只是局部的刷新,再也不是整個頁面的刷新了。

二、原生ajax怎麼寫?

// [1] 
var xhr = new XMLHttpRequest();

// [2]
xhr.onreadystatechange = function() {
	if(xhr.readyState == 4 && (xhr.status > 200 && xhr.status < 300 || xhr.status == 304)) {
		console.log(xhr.responseText);
	}
}

// [3]
xhr.open('POST', 'http://', true);

// [4]
xhr.setRequestHeader("Content-type",
        "application/x-www-form-urlencoded");

// [5]
xhr.send("name=zjj&pwd=123456");

// [6]
xhr.onerror = function() {
	console.log('err');
}
複製代碼

IE中經過new ActiveXObject()獲得,其餘主流瀏覽器中經過newXMLHttpRequest()獲得.使用jquery封裝好的ajax,會避免這些問題.

三、XMLHttpRequest對象

XMLHttpRequest是ajax的核心。咱們的ajax請求就是經過該對象來完成的,他有一些屬性和方法。

var xhr = new XMLHttpRequest();
複製代碼

比較重要的兩個方法open、send。

xhr.open(method, url, async)
// open 方法用於初始化一個請求,提供請求方式 、請求url、以及是否執行異步。

xhr.send(data)
// send方法用於發起請求,咱們能夠將須要傳遞的數據做爲參數傳入。
// 當請求方式爲 post 時,能夠將請求體的參數傳入
// 當請求方式爲 get 時,能夠不傳或傳入 null
// 無論是 get 仍是 post,參數都須要經過 encodeURIComponent 編碼後拼接
複製代碼
  • responseXML 接收響應的字符串類型數據
  • responseText 接收"text/xml""application/xml"格式的響應
  • status 響應的HTTP狀態碼
  • timeout 超時時間
  • readyState 請求和響應的當前階段

如何肯定咱們的請求到了哪一階段了呢,咱們須要藉助 readyState來識別。

xhr.readyStatus==0 還沒有調用 open 方法
xhr.readyStatus==1 已調用 open 但還未發送請求(未調用 send)
xhr.readyStatus==2 已發送請求(已調用 send)
xhr.readyStatus==3 已接收到請求返回的數據
xhr.readyStatus==4 請求已完成
複製代碼

readyStatus的狀態發生改變時,會觸發 xhr 的事件onreadystatechange,因而咱們就能夠在這個方法中,對接收到的數據進行處理.

xhr.onreadystatechange = () => {
    if (xhr.readyStatus === 4) {
        // HTTP 狀態在 200-300 之間表示請求成功
        // HTTP 狀態爲 304 表示請求內容未發生改變,可直接從緩存中讀取
        if (xhr.status >= 200 && 
            xhr.status < 300 || 
            xhr.status == 304) {
            console.log('請求成功', xhr.responseText)
        }
    }
}
複製代碼

當網絡不佳時,咱們須要給請求設置一個超時時間

// 超時時間單位爲毫秒
xhr.timeout = 1000

// 當請求超時時,會觸發 ontimeout 方法
xhr.ontimeout = () => console.log('請求超時')
複製代碼
  • onprogress
xhr.onprogress = function(event){
  console.log(event.loaded / event.total);
}
複製代碼

回調函數能夠獲取資源總大小total,已經加載的資源大小loaded,用這兩個值能夠計算加載進度

  • 該 ajax 方法經過 Promise 方式實現回調
function ajax (options) {
    let url = options.url
    const method = options.method.toLocaleLowerCase() || 'get'
    const async = options.async != false // default is true
    const data = options.data
    const xhr = new XMLHttpRequest()

    if (options.timeout && options.timeout > 0) {
        xhr.timeout = options.timeout
    }

    return new Promise ( (resolve, reject) => {
        xhr.ontimeout = () => reject && reject('請求超時')
        xhr.onreadystatechange = () => {
            if (xhr.readyState == 4) {
                if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
                    resolve && resolve(xhr.responseText)
                } else {
                    reject && reject()
                }
            }
        }
        xhr.onerror = err => reject && reject(err)

        let paramArr = []
        let encodeData
        if (data instanceof Object) {
            for (let key in data) {
                // 參數拼接須要經過 encodeURIComponent 進行編碼
                paramArr.push( encodeURIComponent(key) + '=' + encodeURIComponent(data[key]) )
            }
            encodeData = paramArr.join('&')
        }

        if (method === 'get') {
              // 檢測 url 中是否已存在 ? 及其位置
            const index = url.indexOf('?')
            if (index === -1) url += '?'
            else if (index !== url.length -1) url += '&'
              // 拼接 url
            url += encodeData
        }

        xhr.open(method, url, async)
        if (method === 'get') xhr.send(null)
        else {
            // post 方式須要設置請求頭
            xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded;charset=UTF-8')
            xhr.send(encodeData)
        }
    } )
}
複製代碼
ajax({
    url: 'your request url',
    method: 'get',
    async: true,
    timeout: 1000,
    data: {
        test: 1,
        aaa: 2
    }
}).then(
    res => console.log('請求成功: ' + res),
    err => console.log('請求失敗: ' + err)
)
複製代碼

四、jquery中的ajax

$.ajax({
     url:發送請求的地址,
     data:數據的拼接,//發送到服務器的數據
     type:'get',//請求方式,默認get請求
     contentType: 'application/json', // 設置參數類型
	 headers: {'Content-Type','application/json'},// 設置請求頭
     dataType:'json',//服務器返回的數據類型
     async:true,//是否異步,默認true
     cache:false,//設置爲 false 將不會從瀏覽器緩存中加載請求信息
     success:function(){},//請求成功後的回調函數
     error: function(){}//請求失敗時調用此函數
})
複製代碼

五、說如下異步與同步的區別

同步會阻塞,異步不會阻塞

同步:程序運行從上而下,瀏覽器必須把這個任務執行完畢,才能繼續執行下一個任務

異步:程序運行從上而下,瀏覽器任務沒有執行完,可是能夠繼續執行下一行代碼
複製代碼

六、AJAX的底層實現?

面試官曾考過我一次這樣的問題,當時一臉懵,沒有猜到面試管想問的底層原理是什麼?結果沒答出來~~回來後網上找了好多文章,說的都是XMLHttpRequest。哦個人天~~

後來本身斟酌以爲其實面試管想考的並非太底層的東西,不會問你C++源碼的實現原理。只是想考你幾個關鍵詞:異步線程回調

AJAX告訴瀏覽器,我要準備發送一個HTTP請求了,你幫我重開一個線程(網絡線程),這時候咱們的請求就前往了網絡線程去執行,主線程繼續執行咱們的代碼(這就是異步和線程)。同時回設置一個事件監聽,去監聽咱們請求的狀態,若是請求完畢,就回去執行咱們回調隊列中的回調函數,將其調入主線程去執行 (回調)。

其實完整的一次AJAX過程就是一次HTTP請求過程。

3、瀏覽器事件流

針對事件,面試官可能問:

一、瞭解事件流的順序,對平常的工做有什麼幫助麼?
二、在 vue 的文檔中,有一個修飾符 native ,把它用 . 的形式 連結在事件以後,就能夠監聽原生事件了。它的背後有什麼原理?
三、事件的 event 對象中,有好多的屬性和方法,該如何使用?
複製代碼

一、事件流的概念

事件流分爲三個階段:捕獲階段、目標階段、冒泡階段。 先調用捕獲階段的處理函數,其次調用目標階段的處理函數,最後調用冒泡階段的處理函數。

最初網景公司提出了捕獲事件,微軟公司提出了冒泡事件。

低版本IE(IE8及如下版本)不支持捕獲階段

捕獲事件流:Netscape提出的事件流,即事件由頁面元素接收,逐級向下,傳播到最具體的元素。(頂層元素先收到事件,而後往下傳遞,直到目標元素)

冒泡事件流:IE提出的事件流,即事件由最具體的元素接收,逐級向上,傳播到頁面。(目標元素先收到事件,而後往上,直到最頂層)

w3c 爲了制定統一的標準,採起了折中的方式:先捕獲在冒泡

W3C

同一個 DOM 元素能夠註冊多個同類型的事件,經過 addEventListenerremoveEventListener進行管理。addEventListener 的第三個參數,就是爲了捕獲和冒泡準備的。

  • 註冊事件
target.addEventListener(type, listener[, useCapture]);

// 第三個事件來區分,true爲事件捕獲,false爲事件冒泡
複製代碼
  • 移除事件
target.removeEventListener(type, listener[, useCapture]);
複製代碼
const btn = document.getElementById("test");
//將回調存儲在變量中
const fn = function(e){
    alert("ok");
};
//綁定
btn.addEventListener("click", fn, false);
//解除
btn.removeEventListener("click", fn, false);
複製代碼

兼容IE

  • 註冊事件
target.attacEvent(type,listener);
複製代碼
btn.attachEvent('onclick',function(){
  //do something...
})
複製代碼
  • 移除事件
detachEvent(event,function);
複製代碼
目前支持以addEventListener綁定事件的瀏覽器:
FF、Chrome、Safari、Opera、IE9-11
目前支持以attachEvent綁定事件的瀏覽器:IE6-10
複製代碼

經過stopPropagation()cancelBubble來阻止事件進一步傳播。 cancelBubbleIE標準下阻止事件傳遞的屬性,設置cancelBubble=true表示阻止冒泡

通常來講,咱們只但願事件只觸發在目標上,這時候可使用 stopPropagation 來阻止事件的進一步傳播。一般咱們認爲 stopPropagation 是用來阻止事件冒泡的,其實該函數也能夠阻止捕獲事件。stopImmediatePropagation 一樣也能實現阻止事件冒泡,可是還能阻止該事件目標執行別的註冊事件。

node.addEventListener(
  'click',
  event => {
    event.stopPropagation();
    console.log('只在目標階段觸發,不冒泡');
  },
  false
)
複製代碼
node.addEventListener(
  'click',
  event => {
    event.stopImmediatePropagation()
    console.log('冒泡')
  },
  false
)
// 點擊 node 只會執行上面的函數,該函數不會執行
node.addEventListener(
  'click',
  event => {
    console.log('捕獲 ')
  },
  true
)
複製代碼

原本當一個DOM綁定了兩個事件,一個冒泡、一個捕獲,那麼會按照哦順序執行,可是使用了event.stopImmediatePropagation()以後,就只執行一個。

二、事件代理

咱們常常會遇到,要監聽列表中多項 li 的狀況,假設咱們有一個列表以下:

<ul id="list">
    <li id="item1">item1</li>
    <li id="item2">item2</li>
    <li id="item3">item3</li>
    <li id="item4">item4</li>
</ul>
複製代碼

若是咱們要實現如下功能:當鼠標點擊某一 li 時,輸出該 li 的內容,咱們一般的寫法是這樣的:

window.onload=function(){
    const ulNode = document.getElementById("list");
    const liNodes = ulNode.children;
    for(var i=0; i<liNodes.length; i++){
        liNodes[i].addEventListener('click',function(e){
            console.log(e.target.innerHTML);
        }, false);
    }
}

複製代碼

在傳統的事件處理中,咱們可能會按照須要,爲每個元素添加或者刪除事件處理器。然而,事件處理器將有可能致使內存泄露,或者性能降低,用得越多這種風險就越大。JavaScript 的事件代理,則是一種簡單的技巧。

事件代理: 經過監聽子元素從哪裏冒泡上來,實現事件的代理。

window.onload=function(){
    const ulNode=document.getElementById("list");
    ulNode.addEventListener('click', function(e) {
        /*判斷目標事件是否爲li*/
        if(e.target && e.target.nodeName.toUpperCase()=="LI"){
            console.log(e.target.innerHTML);
        }
    }, false);
};
複製代碼

三、vue 中的 native 修飾符

四、 react 中的合成事件

五、事件對象 event

event.target:指的是觸發事件的那個節點,也就是事件最初發生的節點。
event.target.matches:能夠對關鍵節點進行匹配,來執行相應操做。
event.currentTarget:指的是正在執行的監聽函數的那個節點。
event.isTrusted:表示事件是不是真實用戶觸發。
event.preventDefault():取消事件的默認行爲。
event.stopPropagation():阻止事件的派發(包括了捕獲和冒泡)。
event.stopImmediatePropagation():阻止同一個事件的其餘監聽函數被調用。
複製代碼

六、測試題

題目二

<div class="test1">
    <div class="test2"></div>
</div>
<script>
    document.querySelector('.test1').addEventListener('click',function () {
        console.log(1)
    })
    document.querySelector('.test2').addEventListener('click',function () {
        console.log(2)
    })
</script>
複製代碼

點擊test1,只打印1。若是點擊test2,打印2,1;

題目三

<div class="test1">
    <div class="test2"></div>
</div>
<script>
    document.querySelector('.test1').addEventListener('click', function () {
        console.log(1)
    }, true)
    document.querySelector('.test2').addEventListener('click', function () {
        console.log(2)
    }, true)
</script>
複製代碼

點擊test1,只打印1。若是點擊test2,打印1,2;

題目四

<div class="test1">
    <div class="test2"></div>
</div>
<script>
    document.querySelector('.test1').addEventListener('click', function () {
        console.log(1)
    }, false)
    document.querySelector('.test2').addEventListener('click', function () {
        console.log(2)
    }, true)
</script>
複製代碼

點擊test1,只打印1。若是點擊test2,打印2,1;

題目五

<div class="test1">
    <div class="test2"></div>
</div>
<script>
    document.querySelector('.test1').addEventListener('click', function () {
        console.log(1)
    }, true)
    document.querySelector('.test2').addEventListener('click', function () {
        console.log(2)
    }, false)
</script>
複製代碼

點擊test1,只打印1。若是點擊test2,打印1,2;

從八道面試題看JavaScript DOM事件機制

4、事件循環EventLoop

一、js單線程

JavaScript語言的一大特色就是單線程,也就是說,同一個時間只能作一件事。那麼,爲何JavaScript不能有多個線程呢?這樣能提升效率啊。

JavaScript的單線程,與它的用途有關。做爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操做DOM。這決定了它只能是單線程,不然會帶來很複雜的同步問題。好比,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準?

因此,爲了不復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特徵,未來也不會改變。

爲了利用多核CPU的計算能力,HTML5提出Web Worker標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此,這個新標準並無改變JavaScript單線程的本質。

二、任務隊列

單線程就意味這咱們必須等待一個任務結束以後再去執行下一個任務,若是耗時很長,那麼需等待好久,若是是計算任務也就算了,可是每每不少都是一個IO操做、網絡請求、事件監聽觸發等,在等待期間CPU空閒,是的CPU資源浪費。

js設計者也認識到了,所以遇到的異步任務,將會放入到主線程以外的一個任務隊列中。主線程的同步任務繼續執行。具體來講,事件循環的機制是這樣的:

(1)全部的同步任務都在主線程上執行,造成了一個執行棧。
(2)遇到異步任務,就將去放入到任務隊列中,只要異步任務有告終果,那麼就放置一個事件(同時綁定相應的回調函數)。
(3)一旦執行棧中清空了,那麼系統會自動讀取任務隊列,看看有哪些事件,那麼就讓其結束等待狀態,將其調入主線程的執行棧,開始執行。
(4)以後會一直循環第三步。
複製代碼

三、事件和回調函數

"任務隊列"中的事件,除了IO設備的事件之外,還包括一些用戶產生的事件(好比鼠標點擊、頁面滾動等等)。只要指定過回調函數,這些事件發生時就會進入"任務隊列",等待主線程讀取。

回調函數就是那些被主線程掛起的代碼。當拿到任務隊列中的事件以後,放入主線程執行其實就是去調用相對應的回調函數去了~

四、事件循環

主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,因此整個的這種運行機制又稱爲Event Loop(事件循環)

五、不一樣的任務隊列

不一樣的任務源會被分配到不一樣的 Task 隊列中,任務源能夠分爲 微任務(microtask)宏任務(macrotask)。在 ES6 規範中,microtask 稱爲 jobs,macrotask 稱爲 task。

微任務包括 process.nextTickpromiseObject.observeMutationObserver

宏任務包括 scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

先來個例子:

console.log('script start');

setTimeout(function() {
  console.log('setTimeout')
}, 0)

new Promise(resolve => {
  console.log('Promise')
  resolve()
})
  .then(function() {
    console.log('promise1')
  })
  .then(function() {
    console.log('promise2')
  })

console.log('script end')
// script start => Promise => script end => promise1 => promise2 => setTimeout
複製代碼

首先執行同步代碼,遇到promise的話,會首先執行內部的同步代碼,而後再繼續執行同步代碼。途中遇到的settimeout和promise放入不一樣的任務隊列中,這時候因爲執行棧已經爲空,因此須要開始執行異步任務,首先查看微任務隊列,發現又promise已經能夠了,那麼就執行promise的then,把全部能夠執行的微任務都執行完成以後纔會去宏任務隊列找,發現又setTimeout能夠執行了,就執行內部的代碼。

因此正確的一次 Event loop 順序是這樣的

先執行同步代碼,(script)至關因而宏任務。
執行棧爲空後,去查詢微任務隊列,若是有則調用執行全部的微任務(promise...)
必要的時候渲染UI
而後執行棧清空,開始下一輪循環,去執行宏任務隊列中的任務(定時器...)
複製代碼

六、測試題

題目一

console.log(1);
  setInterval(()=>{
    console.log('setInterval');
  },0);
  setTimeout(()=>{
    console.log(2);
  },0);
  setTimeout(()=>{
    console.log(3);
  },0);
  
  
  new Promise((resolve)=>{
    console.log(4);
    for(let i=0;i<10000;i++){
      i===9999&&resolve();
    }
    console.log(5);
  }).then(()=>{
    console.log(6);
  });
  
  new Promise((resolve)=>{
    resolve();
    console.log(10);
  }).then(()=>{
    console.log(11);
  });
  
  console.log(7);
  console.log(8);
  
// 一、四、五、十、七、八、六、十一、'setInterval'、二、3
複製代碼

題目二

const interval = setInterval(() => {
  console.log('setInterval')
}, 0)

setTimeout(() => {  
  console.log('setTimeout 1')
  Promise.resolve().then(() => {
    console.log('promise 3')
  }).then(() => {
    console.log('promise 4')
  }).then(() => {
    setTimeout(() => {
      console.log('setTimeout 2')
      Promise.resolve().then(() => {
        console.log('promise 5')
      }).then(() => {
        console.log('promise 6')
      }).then(() => {
          clearInterval(interval)
      })
    }, 0)
  })
}, 0)

Promise.resolve().then(() => {
  console.log('promise 1')
}).then(() => {
  console.log('promise 2')
})
複製代碼
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval // 大部分狀況下2次, 少數狀況下一次
setTimeout 2
promise 5
promise 6
複製代碼

promise 4後面大部分狀況下出現2次setInterval、少數狀況出現一次的緣由就是瀏覽器在執行setInterval回調函數後、執行setTimeout回調函數前, 時間間隔大部分狀況超過了這個最短期.

上訴題目尚未涉及async/await。因此推薦幾篇文章,大佬寫很是好~~

看完上訴幾篇文章,相信咱們就能應付大部分的異步執行面試考題了,加油!!

5、跨域解決方案

跨域問題是常常出現的一種狀況,咱們須要對其有必定的認識~~

一、同源和同源策略

域名、端口、協議都相同的狀況下才是同源。有一處不一樣都稱爲是非同源。

瀏覽器的同源策略是一種安全協議。保證了非同源下不能訪問對方的資源。若是不使用同源策略,那麼瀏覽器很容易受到XSS、CSFR的攻擊。

二、跨域

非同源下的訪問和交互就屬於跨域行爲。

script、img、link、iframe這幾個標籤容許跨域訪問資源。

cookies不能在不一樣域名下使用、ajax跨域不容許都是同源策略的限制。

  • 注意:協議和端口形成的跨域,前端沒法處理。

若是發生跨域了,那麼請求到底發送過去了嗎?

跨域並非沒有發請求也不是沒有發過去,服務端可以接受到發來的請求,只是瀏覽器以爲它不安全,因此攔截掉了。你可能會疑問爲何表單可以發送跨域請求,爲何ajax不會?由於歸根揭底跨域就是瀏覽器爲了阻止用戶讀取非同源下的目標。ajax能夠響應,可是瀏覽器說它不安全,因此必須攔截掉,可是表單並不會獲取新的內容,只是提交就好了,所以能夠跨域請求。同時也說明了跨域不能徹底解決CSRF,由於畢竟請求瀏覽器仍是收到了。

三、跨域解決方案

JSONP跨域

jsonp跨域就是利用script標籤能夠跨域的特色,jsonp有一個缺點就是隻支持get方法。

第一步

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
  <body>
    <script src="./jsonp.js" charset="utf-8"></script>
    <script type="text/javascript"> JSONP({ url: 'http://localhost:3000/say', params: { wd: 'Iloveyou' }, callback: 'show' }).then(data => { console.log(data) }) </script>
  </body>
</html>

複製代碼
// jsonp.js
var JSONP = (function(window) {
  var jsonp = function({ url, params, callback }) {
    return new Promise((resolve, reject) => {
      // 【1】動態建立script
      let script = document.createElement('script');
      // 【2】全局設置一個回調函數,服務器返回後會調用執行
      window[callback] = function(data) {
        resolve(data)
        document.body.removeChild(script)
      }
      // 【3】將回調函數做爲參數傳遞
      params = {
        ...params,
        callback
      }
      // 數據拼接處理
      let arrs = []
      for (let key in params) {
        arrs.push(`${key}=${params[key]}`)
      }
      // wd=b&callback=show
      script.src = `${url}?${arrs.join('&')}`
      // 【4】開始請求數據
      document.body.appendChild(script)
    })
  }
  return jsonp;
})(window);

複製代碼
let express = require('express')
let app = express()
app.get('/say', function(req, res) {
  let { wd, callback } = req.query
  console.log(wd) // Iloveyou
  console.log(callback) // show
  res.end(`${callback}('我不愛你')`)
})
app.listen(3000)

複製代碼

jsonp跨域原理:

一、客戶端使用script標籤發送get請求,須要傳遞的參數拼接到後面,外加一個callback函數。
二、客戶端回調函數須要在全局添加一個,參數爲服務端返回的數據字符串。
三、服務器收到get請求以後,會解析參數,查詢數據,將數據以字符串的形式與callback函數名拼接。返回出去。
四、客戶端接收到以後就會調用全局的callback函數,而後經過參數能夠接收到服務器返回的數據。
複製代碼

CORS跨域資源共享

CORS被稱爲是跨域資源共享,須要客戶端和服務端都支持。瀏覽器是默認開啓的,關鍵就是服務器。一般項目中使用該方式來實現跨域訪問。

原理是在請求頭中設置Access-control-Allow-Origin來開啓CORS。該屬性能夠指定哪些域名能夠訪問資源,若是是*則表示全部均可以。

若是Origin指定的源,不在許可範圍內,服務器會返回一個正常的HTTP迴應。瀏覽器發現,這個迴應的頭信息沒有包含Access-Control-Allow-Origin字段(詳見下文),就知道出錯了,從而拋出一個錯誤,被XMLHttpRequestonerror回調函數捕獲。注意,這種錯誤沒法經過狀態碼識別,由於HTTP迴應的狀態碼有多是200。【這樣其實就是攔截掉了

const express = require('express');
const app = express();
let whitList = ['http://localhost:3000', 'http://127.0.0.1:62997'] //設置白名單


app.use((req, res, next) => {
  let origin = req.headers.origin // 獲取來源
  if(whitList.includes(origin)) {
    // 設置那個源頭訪問的我
    res.setHeader('Access-Control-Allow-Origin', origin)
    // 容許攜帶哪一個頭訪問我
    res.setHeader('Access-Control-Allow-Headers', 'name')
    // 容許哪一個方法訪問我
    res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, PUT, HEAD')
    // 容許攜帶cookie
    res.setHeader('Access-Control-Allow-Credentials', true)
    // 預檢的存活時間
    res.setHeader('Access-Control-Max-Age', 6)
    // 容許返回的頭
    res.setHeader('Access-Control-Expose-Headers', 'name')

    if (req.method === 'OPTIONS') {
      res.end() // OPTIONS請求不作任何處理
    }
  }
  next()
})

app.put('/getData', function(req, res) {
  console.log(req.headers)
  res.setHeader('name', 'jw') //返回一個響應頭,後臺需設置
  res.end('我愛小寶貝')
})

app.get('/getData1', function(req, res) {
  res.end('get支持')
})

app.post('/getData2', function(req, res) {
  res.end('post支持')
})

app.listen(4000)
複製代碼

上面是一個簡單的例子,咱們能夠設置白名單,當請求的Origin匹配的時候,就經過設置 Access-Control-Allow-Origin來告訴瀏覽器我服務器容許,而後瀏覽器就不會攔截了,這樣就能看到服務器的響應了~~

代理服務器代理

同源策略是瀏覽器須要遵循的標準,而若是是服務器向服務器請求就無需遵循同源策略。代理服務器,須要作如下幾個步驟:

// index.html(http://127.0.0.1:5500)
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script> $.ajax({ url: 'http://localhost:3000', type: 'post', data: { name: 'xiamen', password: '123456' }, contentType: 'application/json;charset=utf-8', success: function(result) { console.log(result) // {"title":"fontend","password":"123456"} }, error: function(msg) { console.log(msg) } }) </script>
複製代碼
// server1.js 代理服務器(http://localhost:3000)
const http = require('http')
// 第一步:接受客戶端請求
const server = http.createServer((request, response) => {
  // 代理服務器,直接和瀏覽器直接交互,須要設置CORS 的首部字段
  response.writeHead(200, {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': '*',
    'Access-Control-Allow-Headers': 'Content-Type'
  })
  // 第二步:將請求轉發給服務器
  const proxyRequest = http
    .request(
      {
        host: '127.0.0.1',
        port: 4000,
        url: '/',
        method: request.method,
        headers: request.headers
      },
      serverResponse => {
        // 第三步:收到服務器的響應
        var body = ''
        serverResponse.on('data', chunk => {
          body += chunk
        })
        serverResponse.on('end', () => {
          console.log('The data is ' + body)
          // 第四步:將響應結果轉發給瀏覽器
          response.end(body)
        })
      }
    )
    .end()
})
server.listen(3000, () => {
  console.log('The proxyServer is running at http://localhost:3000')
})
複製代碼
// server2.js(http://localhost:4000)
const http = require('http')
const data = { title: 'fontend', password: '123456' }
const server = http.createServer((request, response) => {
  if (request.url === '/') {
    response.end(JSON.stringify(data))
  }
})
server.listen(4000, () => {
  console.log('The server is running at http://localhost:4000')
})
複製代碼

postMessage

postMessage是H5的新API。能夠實現多窗口之間的信息傳遞,能夠是頁面和iframe之間、頁面與新打開的窗口之間、多個窗口之間的消息傳遞。

<body>
  <iframe src="http://127.0.0.1:53402/b.html" frameborder="0" id="frame" onload="load()"></iframe>
  <script>
    function load() {
      let frame = document.getElementById('frame')
      frame.contentWindow.postMessage('a發過去的', 'http://127.0.0.1:53402') //發送數據

      // 監聽消息傳來的事件
      window.onmessage = function (e) { //接受返回數據
        console.log(e.data) //我不愛你
      }
    }
  </script>

</body>


複製代碼
<body>
  <script>
    // 監聽消息傳來的事件
    window.onmessage = function (e) {
      console.log(e.data) // a發來的
      // 監聽到a發來的時候,再去發送出去
      e.source.postMessage('b發來的', e.origin)
    }
  </script>
</body>


複製代碼

nginx跨域

location / {  
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';

    if ($request_method = 'OPTIONS') {
        return 204;
    }
}
複製代碼

總結

CORS支持全部類型的HTTP請求,是跨域HTTP請求的根本解決方案

JSONP只支持GET請求,JSONP的優點在於支持老式瀏覽器,以及能夠向不支持CORS的網站請求數據。

無論是Node中間件代理仍是nginx反向代理,主要是經過同源策略對服務器不加限制。

平常工做中,用得比較多的跨域方案是cors和nginx反向代理.

6、函數柯里化

一、什麼是函數柯里化

柯里化,是函數式編程的一個重要概念。它既能減小代碼冗餘,也能增長可讀性.

以下案例:

// 寫一個 sum 方法,當使用下面的語法調用時,能正常工做
console.log(sum(2, 3)); // Outputs 5
console.log(sum(2)(3)); // Outputs 5
複製代碼

實現能夠是:

function sum(x) {
    if(arguments.length == 2) {
        return arguments[0] + arguments[1];
    }
    
    return function(y) {
        return x + y;
    }
}
複製代碼

二、函數柯里化的實現

若是是這樣:

function sum (a, b, c) {
    console.log(a + b + c);
}
sum(1, 2, 3); // 6
複製代碼

調用的寫法能夠是這樣: sum(1, 2)(3); 或這樣 sum(1, 2)(10); 。就是,先把前2個參數的運算結果拿到後,再與第3個參數相加。

好比:sum(1, 2)(3),sum(1,2)執行以後應該仍是一個函數。

實現一個通用的函數柯里化封裝:

function curry (fn, currArgs) {
    return function() {
        let args = [].slice.call(arguments);

        // 首次調用時,若未提供最後一個參數currArgs,則不用進行args的拼接
        if (currArgs !== undefined) {
            args = args.concat(currArgs);
        }

        // 遞歸調用
        if (args.length < fn.length) {
            return curry(fn, args);// curry執行後仍是會返回一個新的函數f
        }

        // 遞歸出口
        return fn.apply(null, args);
    }
}


function sum(a, b, c) {
    console.log(a + b + c);
}

const fn = curry(sum);

// fn(1, 2, 3); // 6
fn(1, 2)(3); // 6
// fn(1)(2, 3); // 6
// fn(1)(2)(3); // 6
複製代碼
相關文章
相關標籤/搜索