JavaScript 問題彙總(二)

關於前端174道 JavaScript知識
3月31日

最近在整理 JavaScript 的時候發現遇到了很多面試中常見的面試題,本部分主要是作者在 Github 等各大論壇收錄的 JavaScript 相關知識和一些相關面試題時所做的筆記,分享這份總結給大家,對大家對 JavaScript 的可以來一次全方位的檢漏和排查,感謝原作者 CavsZhouyou 的付出,原文鏈接放在文章最下方,如果出現錯誤,希望大家共同指出!

前面JavaScript知識點從01-50點已經講過了,在本篇文章我就不再重複了。具體如下:

《關於前端174道 JavaScript知識點彙總(一)》

接下來開始進入正題:

  1. Javascript 中,有一個函數,執行時對象查找時,永遠不會去查找原型,這個函數是?

hasOwnProperty

所有繼承了 Object 的對象都會繼承到 hasOwnProperty 方法。這個方法可以用來檢測一個對象是否含有特定的自身屬性,和
in 運算符不同,該方法會忽略掉那些從原型鏈上繼承到的屬性。
詳細資料可以參考:《
Object.prototype.hasOwnProperty()》

  1. 對於 JSON 的瞭解?

相關知識點:

JSON 是一種數據交換格式,基於文本,優於輕量,用於交換數據。

JSON 可以表示數字、布爾值、字符串、null、數組(值的有序序列),以及由這些值(或數組、對象)所組成的對象(字符串與
值的映射)。

JSON 使用 JavaScript 語法,但是 JSON 格式僅僅是一個文本。文本可以被任何編程語言讀取及作爲數據格式傳遞。
回答:

JSON 是一種基於文本的輕量級的數據交換格式。它可以被任何的編程語言讀取和作爲數據格式來傳遞。

在項目開發中,我們使用 JSON 作爲前後端數據交換的方式。在前端我們通過將一個符合 JSON 格式的數據結構序列化爲 JSON 字符串,然後將它傳遞到後端,後端通過 JSON 格式的字符串解析後生成對應的數據結構,以此來實現前後端數據的一個傳遞。

因爲 JSON 的語法是基於 js 的,因此很容易將 JSON 和 js 中的對象弄混,但是我們應該注意的是 JSON 和 js 中的對象不是一回事,JSON 中對象格式更加嚴格,比如說在 JSON 中屬性值不能爲函數,不能出現 NaN 這樣的屬性值等,因此大多數的 js 對象是不符合 JSON 對象的格式的。

在 js 中提供了兩個函數來實現 js 數據結構和 JSON 格式的轉換處理,一個是 JSON.stringify 函數,通過傳入一個符合 JSON 格式的數據結構,將其轉換爲一個 JSON 字符串。如果傳入的數據結構不符合 JSON 格式,那麼在序列化的時候會對這些值進行對應的特殊處理,使其符合規範。在前端向後端發送數據時,我們可以調用這個函數將數據對象轉化爲 JSON 格式的字符串。

另一個函數 JSON.parse() 函數,這個函數用來將 JSON 格式的字符串轉換爲一個 js 數據結構,如果傳入的字符串不是標準的 JSON 格式的字符串的話,將會拋出錯誤。當我們從後端接收到 JSON 格式的字符串時,我們可以通過這個方法來將其解析爲一個 js 數據結構,以此來進行數據的訪問。
詳細資料可以參考:《深入瞭解 JavaScript 中的 JSON 》

  1. [].forEach.call($$(""),function(a){a.style.outline=“1px solid #”+(~~(Math.random()(1<<24))).toString(16)}) 能解釋一下這段代碼的意思嗎?

(1)選取頁面所有 DOM 元素。在瀏覽器的控制檯中可以使用$$()方法來獲取頁面中相應的元素,這是現代瀏覽器提供的一個命令行 API 相當於 document.querySelectorAll 方法。

(2)循環遍歷 DOM 元素

(3)給元素添加 outline 。由於渲染的 outline 是不在 CSS 盒模型中的,所以爲元素添加 outline 並不會影響元素的大小和頁面的佈局。

(4)生成隨機顏色函數。Math.random()*(1<<24) 可以得到 0~2^24 - 1 之間的隨機數,因爲得到的是一個浮點數,但我們只需要整數部分,使用取反操作符 ~ 連續兩次取反獲得整數部分,然後再用 toString(16) 的方式,轉換爲一個十六進制的字符串。
詳細資料可以參考:《通過一行代碼學 JavaScript》

  1. js 延遲加載的方式有哪些?

相關知識點:

js 延遲加載,也就是等頁面加載完成之後再加載 JavaScript 文件。 js 延遲加載有助於提高頁面加載速度。
一般有以下幾種方式:

defer 屬性
async 屬性
動態創建 DOM 方式
使用 setTimeout 延遲方法
讓 JS 最後加載
回答:

js 的加載、解析和執行會阻塞頁面的渲染過程,因此我們希望 js 腳本能夠儘可能的延遲加載,提高頁面的渲染速度。

我瞭解到的幾種方式是:

第一種方式是我們一般採用的是將 js 腳本放在文檔的底部,來使 js 腳本儘可能的在最後來加載執行。

第二種方式是給 js 腳本添加 defer 屬性,這個屬性會讓腳本的加載與文檔的解析同步解析,然後在文檔解析完成後再執行這個腳本文件,這樣的話就能使頁面的渲染不被阻塞。多個設置了 defer 屬性的腳本按規範來說最後是順序執行的,但是在一些瀏覽器中可能不是這樣。

第三種方式是給 js 腳本添加 async 屬性,這個屬性會使腳本異步加載,不會阻塞頁面的解析過程,但是當腳本加載完成後立即執行 js 腳本,這個時候如果文檔沒有解析完成的話同樣會阻塞。多個 async 屬性的腳本的執行順序是不可預測的,一般不會按照代碼的順序依次執行。

第四種方式是動態創建 DOM 標籤的方式,我們可以對文檔的加載事件進行監聽,當文檔加載完成後再動態的創建 script 標籤來引入 js 腳本。
詳細資料可以參考:《JS 延遲加載的幾種方式》《HTML 5

  1. Ajax 是什麼? 如何創建一個 Ajax?

相關知識點:

2005 年 2 月,AJAX 這個詞第一次正式提出,它是 Asynchronous JavaScript and XML 的縮寫,指的是通過 JavaScript 的異步通信,從服務器獲取 XML 文檔從中提取數據,再更新當前網頁的對應部分,而不用刷新整個網頁。

具體來說,AJAX 包括以下幾個步驟。

1.創建 XMLHttpRequest 對象,也就是創建一個異步調用對象
2.創建一個新的 HTTP 請求,並指定該 HTTP 請求的方法、URL 及驗證信息
3.設置響應 HTTP 請求狀態變化的函數
4.發送 HTTP 請求
5.獲取異步調用返回的數據
6.使用 JavaScript 和 DOM 實現局部刷新
一般實現:

const SERVER_URL = “/server”;

let xhr = new XMLHttpRequest();

// 創建 Http 請求
xhr.open(“GET”, SERVER_URL, true);

// 設置狀態監聽函數
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;

// 當請求成功時
if (this.status === 200) {
handle(this.response);
} else {
console.error(this.statusText);
}
};

// 設置請求失敗時的監聽函數
xhr.onerror = function() {
console.error(this.statusText);
};

// 設置請求頭信息
xhr.responseType = “json”;
xhr.setRequestHeader(“Accept”, “application/json”);

// 發送 Http 請求
xhr.send(null);

// promise 封裝實現:

function getJSON(url) {
// 創建一個 promise 對象
let promise = new Promise(function(resolve, reject) {
let xhr = new XMLHttpRequest();

// 新建一個 http 請求
xhr.open("GET", url, true);

// 設置狀態的監聽函數
xhr.onreadystatechange = function() {
  if (this.readyState !== 4) return;

  // 當請求成功或失敗時,改變 promise 的狀態
  if (this.status === 200) {
    resolve(this.response);
  } else {
    reject(new Error(this.statusText));
  }
};

// 設置錯誤監聽函數
xhr.onerror = function() {
  reject(new Error(this.statusText));
};

// 設置響應的數據類型
xhr.responseType = "json";

// 設置請求頭信息
xhr.setRequestHeader("Accept", "application/json");

// 發送 http 請求
xhr.send(null);

});

return promise;
}
回答:

我對 ajax 的理解是,它是一種異步通信的方法,通過直接由 js 腳本向服務器發起 http 通信,然後根據服務器返回的數據,更新網頁的相應部分,而不用刷新整個頁面的一種方法。

創建一個 ajax 有這樣幾個步驟

首先是創建一個 XMLHttpRequest 對象。

然後在這個對象上使用 open 方法創建一個 http 請求,open 方法所需要的參數是請求的方法、請求的地址、是否異步和用戶的認證信息。

在發起請求前,我們可以爲這個對象添加一些信息和監聽函數。比如說我們可以通過 setRequestHeader 方法來爲請求添加頭信息。我們還可以爲這個對象添加一個狀態監聽函數。一個 XMLHttpRequest 對象一共有 5 個狀態,當它的狀態變化時會觸發onreadystatechange 事件,我們可以通過設置監聽函數,來處理請求成功後的結果。當對象的 readyState 變爲 4 的時候,代表服務器返回的數據接收完成,這個時候我們可以通過判斷請求的狀態,如果狀態是 2xx 或者 304 的話則代表返回正常。這個時候我們就可以通過 response 中的數據來對頁面進行更新了。

當對象的屬性和監聽函數設置完成後,最後我們調用 sent 方法來向服務器發起請求,可以傳入參數作爲發送的數據體。
詳細資料可以參考:《XMLHttpRequest 對象》《從 ajax 到 fetch、axios》《Fetch 入門》《傳統 Ajax 已死,Fetch 永生》

  1. 談一談瀏覽器的緩存機制?

瀏覽器的緩存機制指的是通過在一段時間內保留已接收到的 web 資源的一個副本,如果在資源的有效時間內,發起了對這個資源的再一次請求,那麼瀏覽器會直接使用緩存的副本,而不是向服務器發起請求。使用 web 緩存可以有效地提高頁面的打開速度,減少不必要的網絡帶寬的消耗。

web 資源的緩存策略一般由服務器來指定,可以分爲兩種,分別是強緩存策略和協商緩存策略。

使用強緩存策略時,如果緩存資源有效,則直接使用緩存資源,不必再向服務器發起請求。強緩存策略可以通過兩種方式來設置,分別是 http 頭信息中的 Expires 屬性和 Cache-Control 屬性。

服務器通過在響應頭中添加 Expires 屬性,來指定資源的過期時間。在過期時間以內,該資源可以被緩存使用,不必再向服務器發送請求。這個時間是一個絕對時間,它是服務器的時間,因此可能存在這樣的問題,就是客戶端的時間和服務器端的時間不一致,或者用戶可以對客戶端時間進行修改的情況,這樣就可能會影響緩存命中的結果。

Expires 是 http1.0 中的方式,因爲它的一些缺點,在 http 1.1 中提出了一個新的頭部屬性就是 Cache-Control 屬性,
它提供了對資源的緩存的更精確的控制。它有很多不同的值,常用的比如我們可以通過設置 max-age 來指定資源能夠被緩存的時間
的大小,這是一個相對的時間,它會根據這個時間的大小和資源第一次請求時的時間來計算出資源過期的時間,因此相對於 Expires
來說,這種方式更加有效一些。常用的還有比如 private ,用來規定資源只能被客戶端緩存,不能夠代理服務器所緩存。還有如 n
o-store ,用來指定資源不能夠被緩存,no-cache 代表該資源能夠被緩存,但是立即失效,每次都需要向服務器發起請求。

一般來說只需要設置其中一種方式就可以實現強緩存策略,當兩種方式一起使用時,Cache-Control 的優先級要高於 Expires 。

使用協商緩存策略時,會先向服務器發送一個請求,如果資源沒有發生修改,則返回一個 304 狀態,讓瀏覽器使用本地的緩存副本。
如果資源發生了修改,則返回修改後的資源。協商緩存也可以通過兩種方式來設置,分別是 http 頭信息中的 Etag 和 Last-Modified 屬性。

服務器通過在響應頭中添加 Last-Modified 屬性來指出資源最後一次修改的時間,當瀏覽器下一次發起請求時,會在請求頭中添加一個 If-Modified-Since 的屬性,屬性值爲上一次資源返回時的 Last-Modified 的值。當請求發送到服務器後服務器會通過這個屬性來和資源的最後一次的修改時間來進行比較,以此來判斷資源是否做了修改。如果資源沒有修改,那麼返回 304 狀態,讓客戶端使用本地的緩存。如果資源已經被修改了,則返回修改後的資源。使用這種方法有一個缺點,就是 Last-Modified 標註的最後修改時間只能精確到秒級,如果某些文件在1秒鐘以內,被修改多次的話,那麼文件已將改變了但是 Last-Modified 卻沒有改變,
這樣會造成緩存命中的不準確。

因爲 Last-Modified 的這種可能發生的不準確性,http 中提供了另外一種方式,那就是 Etag 屬性。服務器在返回資源的時候,在頭信息中添加了 Etag 屬性,這個屬性是資源生成的唯一標識符,當資源發生改變的時候,這個值也會發生改變。在下一次資源請求時,瀏覽器會在請求頭中添加一個 If-None-Match 屬性,這個屬性的值就是上次返回的資源的 Etag 的值。服務接收到請求後會根據這個值來和資源當前的 Etag 的值來進行比較,以此來判斷資源是否發生改變,是否需要返回資源。通過這種方式,比 Last-Modified 的方式更加精確。

當 Last-Modified 和 Etag 屬性同時出現的時候,Etag 的優先級更高。使用協商緩存的時候,服務器需要考慮負載平衡的問題,因此多個服務器上資源的 Last-Modified 應該保持一致,因爲每個服務器上 Etag 的值都不一樣,因此在考慮負載平衡時,最好不要設置 Etag 屬性。

強緩存策略和協商緩存策略在緩存命中時都會直接使用本地的緩存副本,區別只在於協商緩存會向服務器發送一次請求。它們緩存不命中時,都會向服務器發送請求來獲取資源。在實際的緩存機制中,強緩存策略和協商緩存策略是一起合作使用的。瀏覽器首先會根據請求的信息判斷,強緩存是否命中,如果命中則直接使用資源。如果不命中則根據頭信息向服務器發起請求,使用協商緩存,如果協商緩存命中的話,則服務器不返回資源,瀏覽器直接使用本地資源的副本,如果協商緩存不命中,則瀏覽器返回最新的資源給瀏覽器。
詳細資料可以參考:《淺談瀏覽器緩存》《前端優化:瀏覽器緩存技術介紹》《請求頭中的 Cache-Control》《Cache-Control 字段值詳解》

  1. Ajax 解決瀏覽器緩存問題?

1.在 ajax 發送請求前加上 anyAjaxObj.setRequestHeader(“If-Modified-Since”,“0”)。
2.在 ajax 發送請求前加上 anyAjaxObj.setRequestHeader(“Cache-Control”,“no-cache”)。
3.在 URL 後面加上一個隨機數: “fresh=” + Math.random();。
4.在 URL 後面加上時間戳:“nowtime=” + new Date().getTime();。
5.如果是使用 jQuery,直接這樣就可以了$.ajaxSetup({cache:false})。這樣頁面的所有 ajax 都會執行這條語句就是不需要保存緩存記錄。
詳細資料可以參考:《Ajax 中瀏覽器的緩存問題解決方法》《淺談瀏覽器緩存》

  1. 同步和異步的區別?

相關知識點:

同步,可以理解爲在執行完一個函數或方法之後,一直等待系統返回值或消息,這時程序是處於阻塞的,只有接收到返回的值或消息後才往下執行其他的命令。

異步,執行完函數或方法後,不必阻塞性地等待返回值或消息,只需要向系統委託一個異步過程,那麼當系統接收到返回值或消息時,系統會自動觸發委託的異步過程,從而完成一個完整的流程。
回答:

同步指的是當一個進程在執行某個請求的時候,如果這個請求需要等待一段時間才能返回,那麼這個進程會一直等待下去,直到消息返
回爲止再繼續向下執行。

異步指的是當一個進程在執行某個請求的時候,如果這個請求需要等待一段時間才能返回,這個時候進程會繼續往下執行,不會阻塞等
待消息的返回,當消息返回時系統再通知進程進行處理。
詳細資料可以參考:《同步和異步的區別》

  1. 什麼是瀏覽器的同源政策?

我對瀏覽器的同源政策的理解是,一個域下的 js 腳本在未經允許的情況下,不能夠訪問另一個域的內容。這裏的同源的指的是兩個
域的協議、域名、端口號必須相同,否則則不屬於同一個域。

同源政策主要限制了三個方面

第一個是當前域下的 js 腳本不能夠訪問其他域下的 cookie、localStorage 和 indexDB。

第二個是當前域下的 js 腳本不能夠操作訪問操作其他域下的 DOM。

第三個是當前域下 ajax 無法發送跨域請求。

同源政策的目的主要是爲了保證用戶的信息安全,它只是對 js 腳本的一種限制,並不是對瀏覽器的限制,對於一般的 img、或者
script 腳本請求都不會有跨域的限制,這是因爲這些操作都不會通過響應結果來進行可能出現安全問題的操作。
60. 如何解決跨域問題?

相關知識點:

1.通過 jsonp 跨域
2.document.domain + iframe 跨域
3.location.hash + iframe
4.window.name + iframe 跨域
5.postMessage 跨域
6.跨域資源共享(CORS)
7.nginx 代理跨域
8.nodejs 中間件代理跨域
9.WebSocket 協議跨域
回答:

解決跨域的方法我們可以根據我們想要實現的目的來劃分。

首先我們如果只是想要實現主域名下的不同子域名的跨域操作,我們可以使用設置 document.domain 來解決。

(1)將 document.domain 設置爲主域名,來實現相同子域名的跨域操作,這個時候主域名下的 cookie 就能夠被子域名所訪問。同時如果文檔中含有主域名相同,子域名不同的 iframe 的話,我們也可以對這個 iframe 進行操作。

如果是想要解決不同跨域窗口間的通信問題,比如說一個頁面想要和頁面的中的不同源的 iframe 進行通信的問題,我們可以使用 location.hash 或者 window.name 或者 postMessage 來解決。

(2)使用 location.hash 的方法,我們可以在主頁面動態的修改 iframe 窗口的 hash 值,然後在 iframe 窗口裏實現監聽函數來實現這樣一個單向的通信。因爲在 iframe 是沒有辦法訪問到不同源的父級窗口的,所以我們不能直接修改父級窗口的 hash 值來實現通信,我們可以在 iframe 中再加入一個 iframe ,這個 iframe 的內容是和父級頁面同源的,所以我們可以 window.parent.parent 來修改最頂級頁面的 src,以此來實現雙向通信。

(3)使用 window.name 的方法,主要是基於同一個窗口中設置了 window.name 後不同源的頁面也可以訪問,所以不同源的子頁面可以首先在 window.name 中寫入數據,然後跳轉到一個和父級同源的頁面。這個時候級頁面就可以訪問同源的子頁面中 window.name 中的數據了,這種方式的好處是可以傳輸的數據量大。

(4)使用 postMessage 來解決的方法,這是一個 h5 中新增的一個 api。通過它我們可以實現多窗口間的信息傳遞,通過獲取到指定窗口的引用,然後調用 postMessage 來發送信息,在窗口中我們通過對 message 信息的監聽來接收信息,以此來實現不同源間的信息交換。

如果是像解決 ajax 無法提交跨域請求的問題,我們可以使用 jsonp、cors、websocket 協議、服務器代理來解決問題。

(5)使用 jsonp 來實現跨域請求,它的主要原理是通過動態構建 script 標籤來實現跨域請求,因爲瀏覽器對 script 標籤的引入沒有跨域的訪問限制 。通過在請求的 url 後指定一個回調函數,然後服務器在返回數據的時候,構建一個 json 數據的包裝,這個包裝就是回調函數,然後返回給前端,前端接收到數據後,因爲請求的是腳本文件,所以會直接執行,這樣我們先前定義好的回調函數就可以被調用,從而實現了跨域請求的處理。這種方式只能用於 get 請求。

(6)使用 CORS 的方式,CORS 是一個 W3C 標準,全稱是"跨域資源共享"。CORS 需要瀏覽器和服務器同時支持。目前,所有瀏覽器都支持該功能,因此我們只需要在服務器端配置就行。瀏覽器將 CORS 請求分成兩類:簡單請求和非簡單請求。對於簡單請求,瀏覽器直接發出 CORS 請求。具體來說,就是會在頭信息之中,增加一個 Origin 字段。Origin 字段用來說明本次請求來自哪個源。服務器根據這個值,決定是否同意這次請求。對於如果 Origin 指定的源,不在許可範圍內,服務器會返回一個正常的 HTTP 迴應。瀏覽器發現,這個迴應的頭信息沒有包含 Access-Control-Allow-Origin 字段,就知道出錯了,從而拋出一個錯誤,ajax 不會收到響應信息。如果成功的話會包含一些以 Access-Control- 開頭的字段。

非簡單請求,瀏覽器會先發出一次預檢請求,來判斷該域名是否在服務器的白名單中,如果收到肯定回覆後纔會發起請求。

(7)使用 websocket 協議,這個協議沒有同源限制。

(8)使用服務器來代理跨域的訪問請求,就是有跨域的請求操作時發送請求給後端,讓後端代爲請求,然後最後將獲取的結果發返回。
詳細資料可以參考:《前端常見跨域解決方案(全)》《瀏覽器同源政策及其規避方法》《跨域,你需要知道的全在這裏》《爲什麼 form 表單提交沒有跨域問題,但 ajax 提交有跨域問題?》

  1. 服務器代理轉發時,該如何處理 cookie?

詳細資料可以參考:《深入淺出 Nginx》

  1. 簡單談一下 cookie ?

我的理解是 cookie 是服務器提供的一種用於維護會話狀態信息的數據,通過服務器發送到瀏覽器,瀏覽器保存在本地,當下一次有同源的請求時,將保存的 cookie 值添加到請求頭部,發送給服務端。這可以用來實現記錄用戶登錄狀態等功能。cookie 一般可以存儲 4k 大小的數據,並且只能夠被同源的網頁所共享訪問。

服務器端可以使用 Set-Cookie 的響應頭部來配置 cookie 信息。一條cookie 包括了5個屬性值 expires、domain、path、secure、HttpOnly。其中 expires 指定了 cookie 失效的時間,domain 是域名、path是路徑,domain 和 path 一起限制了 cookie 能夠被哪些 url 訪問。secure 規定了 cookie 只能在確保安全的情況下傳輸,HttpOnly 規定了這個 cookie 只能被服務器訪問,不能使用 js 腳本訪問。

在發生 xhr 的跨域請求的時候,即使是同源下的 cookie,也不會被自動添加到請求頭部,除非顯示地規定。
詳細資料可以參考:《HTTP cookies》《聊一聊 cookie》

  1. 模塊化開發怎麼做?

我對模塊的理解是,一個模塊是實現一個特定功能的一組方法。在最開始的時候,js 只實現一些簡單的功能,所以並沒有模塊的概念
,但隨着程序越來越複雜,代碼的模塊化開發變得越來越重要。

由於函數具有獨立作用域的特點,最原始的寫法是使用函數來作爲模塊,幾個函數作爲一個模塊,但是這種方式容易造成全局變量的污
染,並且模塊間沒有聯繫。

後面提出了對象寫法,通過將函數作爲一個對象的方法來實現,這樣解決了直接使用函數作爲模塊的一些缺點,但是這種辦法會暴露所
有的所有的模塊成員,外部代碼可以修改內部屬性的值。

現在最常用的是立即執行函數的寫法,通過利用閉包來實現模塊私有作用域的建立,同時不會對全局作用域造成污染。
詳細資料可以參考:《淺談模塊化開發》《Javascript 模塊化編程(一):模塊的寫法》《前端模塊化:CommonJS,AMD,CMD,ES6》《Module 的語法》

  1. js 的幾種模塊規範?

js 中現在比較成熟的有四種模塊加載方案。

第一種是 CommonJS 方案,它通過 require 來引入模塊,通過 module.exports 定義模塊的輸出接口。這種模塊加載方案是
服務器端的解決方案,它是以同步的方式來引入模塊的,因爲在服務端文件都存儲在本地磁盤,所以讀取非常快,所以以同步的方式
加載沒有問題。但如果是在瀏覽器端,由於模塊的加載是使用網絡請求,因此使用異步加載的方式更加合適。

第二種是 AMD 方案,這種方案採用異步加載的方式來加載模塊,模塊的加載不影響後面語句的執行,所有依賴這個模塊的語句都定
義在一個回調函數裏,等到加載完成後再執行回調函數。require.js 實現了 AMD 規範。

第三種是 CMD 方案,這種方案和 AMD 方案都是爲了解決異步模塊加載的問題,sea.js 實現了 CMD 規範。它和 require.js
的區別在於模塊定義時對依賴的處理不同和對依賴模塊的執行時機的處理不同。參考60

第四種方案是 ES6 提出的方案,使用 import 和 export 的形式來導入導出模塊。這種方案和上面三種方案都不同。參考 61。
65. AMD 和 CMD 規範的區別?

它們之間的主要區別有兩個方面。

(1)第一個方面是在模塊定義時對依賴的處理不同。AMD 推崇依賴前置,在定義模塊的時候就要聲明其依賴的模塊。而 CMD 推崇就近依賴,只有在用到某個模塊的時候再去 require。

(2)第二個方面是對依賴模塊的執行時機處理不同。首先 AMD 和 CMD 對於模塊的加載方式都是異步加載,不過它們的區別在於模塊的執行時機,AMD 在依賴模塊加載完成後就直接執行依賴模塊,依賴模塊的執行順序和我們書寫的順序不一定一致。而 CMD在依賴模塊加載完成後並不執行,只是下載而已,等到所有的依賴模塊都加載好後,進入回調函數邏輯,遇到 require 語句的時候才執行對應的模塊,這樣模塊的執行順序就和我們書寫的順序保持一致了。

// CMD
define(function(require, exports, module) {
var a = require("./a");
a.doSomething();
// 此處略去 100 行
var b = require("./b"); // 依賴可以就近書寫
b.doSomething();
// …
});

// AMD 默認推薦
define(["./a", “./b”], function(a, b) {
// 依賴必須一開始就寫好
a.doSomething();
// 此處略去 100 行
b.doSomething();
// …
});
詳細資料可以參考:《前端模塊化,AMD 與 CMD 的區別》

  1. ES6 模塊與 CommonJS 模塊、AMD、CMD 的差異。

1.CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。ES6 模塊的運行機制與 CommonJS 不一樣。JS 引擎對腳本靜態分析的時候,遇到模塊加載命令 import,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊裏面去取值。
2.CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。CommonJS 模塊就是對象,即在輸入時是先加載整個模塊,生成一個對象,然後再從這個對象上面讀取方法,這種加載稱爲“運行時加載”。而 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。
67. requireJS 的核心原理是什麼?(如何動態加載的?如何避免多次加載的?如何 緩存的?)

require.js 的核心原理是通過動態創建 script 腳本來異步引入模塊,然後對每個腳本的 load 事件進行監聽,如果每個腳本都加載完成了,再調用回調函數。
詳細資料可以參考:《requireJS 的用法和原理分析》《requireJS 的核心原理是什麼?》《從 RequireJs 源碼剖析腳本加載原理》《requireJS 原理分析》

  1. JS 模塊加載器的輪子怎麼造,也就是如何實現一個模塊加載器?

詳細資料可以參考:《JS 模塊加載器加載原理是怎麼樣的?》

  1. ECMAScript6 怎麼寫 class,爲什麼會出現 class 這種東西?

在我看來 ES6 新添加的 class 只是爲了補充 js 中缺少的一些面嚮對象語言的特性,但本質上來說它只是一種語法糖,不是一個新的東西,其背後還是原型繼承的思想。通過加入 class 可以有利於我們更好的組織代碼。

在 class 中添加的方法,其實是添加在類的原型上的。
詳細資料可以參考:《ECMAScript 6 實現了 class,對 JavaScript 前端開發有什麼意義?》《Class 的基本語法》

  1. documen.write 和 innerHTML 的區別?

document.write 的內容會代替整個文檔內容,會重寫整個頁面。

innerHTML 的內容只是替代指定元素的內容,只會重寫頁面中的部分內容。
詳細資料可以參考:《簡述 document.write 和 innerHTML 的區別。》

  1. DOM 操作——怎樣添加、移除、移動、複製、創建和查找節點?

(1)創建新節點

createDocumentFragment(node);
createElement(node);
createTextNode(text);
(2)添加、移除、替換、插入

appendChild(node)
removeChild(node)
replaceChild(new,old)
insertBefore(new,old)
(3)查找

getElementById();
getElementsByName();
getElementsByTagName();
getElementsByClassName();
querySelector();
querySelectorAll();
(4)屬性操作

getAttribute(key);
setAttribute(key, value);
hasAttribute(key);
removeAttribute(key);
詳細資料可以參考:《DOM 概述》《原生 JavaScript 的 DOM 操作彙總》《原生 JS 中 DOM 節點相關 API 合集》

  1. innerHTML 與 outerHTML 的區別?

對於這樣一個 HTML 元素:

content

innerHTML:內部 HTML,content

outerHTML:外部 HTML,

content

innerText:內部文本,content ;
outerText:內部文本,content ;
73. .call() 和 .apply() 的區別?

它們的作用一模一樣,區別僅在於傳入參數的形式的不同。

apply 接受兩個參數,第一個參數指定了函數體內 this 對象的指向,第二個參數爲一個帶下標的集合,這個集合可以爲數組,也可以爲類數組,apply 方法把這個集合中的元素作爲參數傳遞給被調用的函數。

call 傳入的參數數量不固定,跟 apply 相同的是,第一個參數也是代表函數體內的 this 指向,從第二個參數開始往後,每個參數被依次傳入函數。
詳細資料可以參考:《apply、call 的區別和用途》

  1. JavaScript 類數組對象的定義?

一個擁有 length 屬性和若干索引屬性的對象就可以被稱爲類數組對象,類數組對象和數組類似,但是不能調用數組的方法。

常見的類數組對象有 arguments 和 DOM 方法的返回結果,還有一個函數也可以被看作是類數組對象,因爲它含有 length
屬性值,代表可接收的參數個數。
常見的類數組轉換爲數組的方法有這樣幾種:

(1)通過 call 調用數組的 slice 方法來實現轉換

Array.prototype.slice.call(arrayLike);
(2)通過 call 調用數組的 splice 方法來實現轉換

Array.prototype.splice.call(arrayLike, 0);
(3)通過 apply 調用數組的 concat 方法來實現轉換

Array.prototype.concat.apply([], arrayLike);
(4)通過 Array.from 方法來實現轉換

Array.from(arrayLike);
詳細的資料可以參考:《JavaScript 深入之類數組對象與 arguments》《javascript 類數組》《深入理解 JavaScript 類數組》

  1. 數組和對象有哪些原生方法,列舉一下?

數組和字符串的轉換方法:toString()、toLocalString()、join() 其中 join() 方法可以指定轉換爲字符串時的分隔符。

數組尾部操作的方法 pop() 和 push(),push 方法可以傳入多個參數。

數組首部操作的方法 shift() 和 unshift() 重排序的方法 reverse() 和 sort(),sort() 方法可以傳入一個函數來進行比較,傳入前後兩個值,如果返回值爲正數,則交換兩個參數的位置。

數組連接的方法 concat() ,返回的是拼接好的數組,不影響原數組。

數組截取辦法 slice(),用於截取數組中的一部分返回,不影響原數組。

數組插入方法 splice(),影響原數組查找特定項的索引的方法,indexOf() 和 lastIndexOf() 迭代方法 every()、some()、filter()、map() 和 forEach() 方法

數組歸併方法 reduce() 和 reduceRight() 方法
詳細資料可以參考:《JavaScript 深入理解之 Array 類型詳解》

  1. 數組的 fill 方法?

fill() 方法用一個固定值填充一個數組中從起始索引到終止索引內的全部元素。不包括終止索引。
fill 方法接受三個參數 value,start 以及 end,start 和 end 參數是可選的,其默認值分別爲 0 和 this 對象的 length 屬性值。
詳細資料可以參考:《Array.prototype.fill()》

  1. [,] 的長度?

尾後逗號 (有時叫做“終止逗號”)在向 JavaScript 代碼添加元素、參數、屬性時十分有用。如果你想要添加新的屬性,並且上一行已經使用了尾後逗號,你可以僅僅添加新的一行,而不需要修改上一行。這使得版本控制更加清晰,以及代碼維護麻煩更少。

JavaScript 一開始就支持數組字面值中的尾後逗號,隨後向對象字面值(ECMAScript 5)中添加了尾後逗號。最近(ECMAS
cript 2017),又將其添加到函數參數中。但是 JSON 不支持尾後逗號。

如果使用了多於一個尾後逗號,會產生間隙。 帶有間隙的數組叫做稀疏數組(密緻數組沒有間隙)。稀疏數組的長度爲逗號的數
量。
詳細資料可以參考:《尾後逗號》

  1. JavaScript 中的作用域與變量聲明提升?

變量提升的表現是,無論我們在函數中何處位置聲明的變量,好像都被提升到了函數的首部,我們可以在變量聲明前訪問到而不會報錯。

造成變量聲明提升的本質原因是 js 引擎在代碼執行前有一個解析的過程,創建了執行上下文,初始化了一些代碼執行時需要用到的對象。當我們訪問一個變量時,我們會到當前執行上下文中的作用域鏈中去查找,而作用域鏈的首端指向的是當前執行上下文的變量對象,這個變量對象是執行上下文的一個屬性,它包含了函數的形參、所有的函數和變量聲明,這個對象的是在代碼解析的時候創建的。這就是會出現變量聲明提升的根本原因。
詳細資料可以參考:《JavaScript 深入理解之變量對象》

  1. 如何編寫高性能的 Javascript ?

1.使用位運算代替一些簡單的四則運算。
2.避免使用過深的嵌套循環。
3.不要使用未定義的變量。
4.當需要多次訪問數組長度時,可以用變量保存起來,避免每次都會去進行屬性查找。
詳細資料可以參考:《如何編寫高性能的 Javascript?》

  1. 簡單介紹一下 V8 引擎的垃圾回收機制

v8 的垃圾回收機制基於分代回收機制,這個機制又基於世代假說,這個假說有兩個特點,一是新生的對象容易早死,另一個是不死的對象會活得更久。基於這個假說,v8 引擎將內存分爲了新生代和老生代。

新創建的對象或者只經歷過一次的垃圾回收的對象被稱爲新生代。經歷過多次垃圾回收的對象被稱爲老生代。

新生代被分爲 From 和 To 兩個空間,To 一般是閒置的。當 From 空間滿了的時候會執行 Scavenge 算法進行垃圾回收。當我們執行垃圾回收算法的時候應用邏輯將會停止,等垃圾回收結束後再繼續執行。這個算法分爲三步:

(1)首先檢查 From 空間的存活對象,如果對象存活則判斷對象是否滿足晉升到老生代的條件,如果滿足條件則晉升到老生代。如果不滿足條件則移動 To 空間。

(2)如果對象不存活,則釋放對象的空間。

(3)最後將 From 空間和 To 空間角色進行交換。

新生代對象晉升到老生代有兩個條件:

(1)第一個是判斷是對象否已經經過一次 Scavenge 回收。若經歷過,則將對象從 From 空間複製到老生代中;若沒有經歷,則複製到 To 空間。

(2)第二個是 To 空間的內存使用佔比是否超過限制。當對象從 From 空間複製到 To 空間時,若 To 空間使用超過 25%,則對象直接晉升到老生代中。設置 25% 的原因主要是因爲算法結束後,兩個空間結束後會交換位置,如果 To 空間的內存太小,會影響後續的內存分配。

老生代採用了標記清除法和標記壓縮法。標記清除法首先會對內存中存活的對象進行標記,標記結束後清除掉那些沒有標記的對象。由於標記清除後會造成很多的內存碎片,不便於後面的內存分配。所以瞭解決內存碎片的問題引入了標記壓縮法。

由於在進行垃圾回收的時候會暫停應用的邏輯,對於新生代方法由於內存小,每次停頓的時間不會太長,但對於老生代來說每次垃圾回收的時間長,停頓會造成很大的影響。 爲了解決這個問題 V8 引入了增量標記的方法,將一次停頓進行的過程分爲了多步,每次執行完一小步就讓運行邏輯執行一會,就這樣交替運行。
詳細資料可以參考:《深入理解 V8 的垃圾回收原理》《JavaScript 中的垃圾回收》

  1. 哪些操作會造成內存泄漏?

相關知識點:

1.意外的全局變量
2.被遺忘的計時器或回調函數
3.脫離 DOM 的引用
4.閉包
回答:

第一種情況是我們由於使用未聲明的變量,而意外的創建了一個全局變量,而使這個變量一直留在內存中無法被回收。

第二種情況是我們設置了 setInterval 定時器,而忘記取消它,如果循環函數有對外部變量的引用的話,那麼這個變量會被一直留
在內存中,而無法被回收。

第三種情況是我們獲取一個 DOM 元素的引用,而後面這個元素被刪除,由於我們一直保留了對這個元素的引用,所以它也無法被回
收。

第四種情況是不合理的使用閉包,從而導致某些變量一直被留在內存當中。
詳細資料可以參考:《JavaScript 內存泄漏教程》《4 類 JavaScript 內存泄漏及如何避免》《杜絕 js 中四種內存泄漏類型的發生》《javascript 典型內存泄漏及 chrome 的排查方法》

  1. 需求:實現一個頁面操作不會整頁刷新的網站,並且能在瀏覽器前進、後退時正確響應。給出你的技術實現方案?

通過使用 pushState + ajax 實現瀏覽器無刷新前進後退,當一次 ajax 調用成功後我們將一條 state 記錄加入到 history
對象中。一條 state 記錄包含了 url、title 和 content 屬性,在 popstate 事件中可以獲取到這個 state 對象,我們可
以使用 content 來傳遞數據。最後我們通過對 window.onpopstate 事件監聽來響應瀏覽器的前進後退操作。

使用 pushState 來實現有兩個問題,一個是打開首頁時沒有記錄,我們可以使用 replaceState 來將首頁的記錄替換,另一個問
題是當一個頁面刷新的時候,仍然會向服務器端請求數據,因此如果請求的 url 需要後端的配合將其重定向到一個頁面。
詳細資料可以參考:《pushState + ajax 實現瀏覽器無刷新前進後退》《Manipulating the browser history》

  1. 如何判斷當前腳本運行在瀏覽器還是 node 環境中?(阿里)

this === window ? ‘browser’ : ‘node’;

通過判斷 Global 對象是否爲 window,如果不爲 window,當前腳本沒有運行在瀏覽器中。
84. 把 script 標籤放在頁面的最底部的 body 封閉之前和封閉之後有什麼區別?瀏覽器會如何解析它們?

詳細資料可以參考:《爲什麼把 script 標籤放在 body 結束標籤之後 html 結束標籤之前?》《從 Chrome 源碼看瀏覽器如何加載資源》

  1. 移動端的點擊事件的有延遲,時間是多久,爲什麼會有? 怎麼解決這個延時?

移動端點擊有 300ms 的延遲是因爲移動端會有雙擊縮放的這個操作,因此瀏覽器在 click 之後要等待 300ms,看用戶有沒有下一次點擊,來判斷這次操作是不是雙擊。
有三種辦法來解決這個問題:

1.通過 meta 標籤禁用網頁的縮放。
2.通過 meta 標籤將網頁的 viewport 設置爲 ideal viewport。
3.調用一些 js 庫,比如 FastClick
click 延時問題還可能引起點擊穿透的問題,就是如果我們在一個元素上註冊了 touchStart 的監聽事件,這個事件會將這個元素隱藏掉,我們發現當這個元素隱藏後,觸發了這個元素下的一個元素的點擊事件,這就是點擊穿透。
詳細資料可以參考:《移動端 300ms 點擊延遲和點擊穿透》

  1. 什麼是“前端路由”?什麼時候適合使用“前端路由”?“前端路由”有哪些優點和缺點?

(1)什麼是前端路由?

前端路由就是把不同路由對應不同的內容或頁面的任務交給前端來做,之前是通過服務端根據 url 的不同返回不同的頁面實現的。

(2)什麼時候使用前端路由?

在單頁面應用,大部分頁面結構不變,只改變部分內容的使用

(3)前端路由有什麼優點和缺點?

優點:用戶體驗好,不需要每次都從服務器全部獲取,快速展現給用戶

缺點:單頁面無法記住之前滾動的位置,無法在前進,後退的時候記住滾動的位置

前端路由一共有兩種實現方式,一種是通過 hash 的方式,一種是通過使用 pushState 的方式。
詳細資料可以參考:《什麼是“前端路由”》《淺談前端路由》《前端路由是什麼東西?》

  1. 如何測試前端代碼麼? 知道 BDD, TDD, Unit Test 麼? 知道怎麼測試你的前端工程麼(mocha, sinon, jasmin, qUnit…)?

詳細資料可以參考:《淺談前端單元測試》

  1. 檢測瀏覽器版本版本有哪些方式?

檢測瀏覽器版本一共有兩種方式:

一種是檢測 window.navigator.userAgent 的值,但這種方式很不可靠,因爲 userAgent 可以被改寫,並且早期的瀏覽器如 ie,會通過僞裝自己的 userAgent 的值爲 Mozilla 來躲過服務器的檢測。

第二種方式是功能檢測,根據每個瀏覽器獨有的特性來進行判斷,如 ie 下獨有的 ActiveXObject。
詳細資料可以參考:《JavaScript 判斷瀏覽器類型》

  1. 什麼是 Polyfill ?

Polyfill 指的是用於實現瀏覽器並不支持的原生 API 的代碼。

比如說 querySelectorAll 是很多現代瀏覽器都支持的原生 Web API,但是有些古老的瀏覽器並不支持,那麼假設有人寫了一段代碼來實現這個功能使這些瀏覽器也支持了這個功能,那麼這就可以成爲一個 Polyfill。

一個 shim 是一個庫,有自己的 API,而不是單純實現原生不支持的 API。
詳細資料可以參考:《Web 開發中的“黑話”》《Polyfill 爲何物》

  1. 使用 JS 實現獲取文件擴展名?

// String.lastIndexOf() 方法返回指定值(本例中的’.’)在調用該方法的字符串中最後出現的位置,如果沒找到則返回 -1。

// 對於 ‘filename’ 和 ‘.hiddenfile’ ,lastIndexOf 的返回值分別爲 0 和 -1 無符號右移操作符(>>>) 將 -1 轉換爲 4294967295 ,將 -2 轉換爲 4294967294 ,這個方法可以保證邊緣情況時文件名不變。

// String.prototype.slice() 從上面計算的索引處提取文件的擴展名。如果索引比文件名的長度大,結果爲""。
function getFileExtension(filename) {
return filename.slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2);
}
詳細資料可以參考:《如何更有效的獲取文件擴展名》

  1. 介紹一下 js 的節流與防抖?

相關知識點:

// 函數防抖: 在事件被觸發 n 秒後再執行回調,如果在這 n 秒內事件又被觸發,則重新計時。

// 函數節流: 規定一個單位時間,在這個單位時間內,只能有一次觸發事件的回調函數執行,如果在同一個單位時間內某事件被觸發多次,只有一次能生效。

// 函數防抖的實現
function debounce(fn, wait) {
var timer = null;

return function() {
var context = this,
args = arguments;

// 如果此時存在定時器的話,則取消之前的定時器重新記時
if (timer) {
  clearTimeout(timer);
  timer = null;
}

// 設置定時器,使事件間隔指定事件後執行
timer = setTimeout(() => {
  fn.apply(context, args);
}, wait);

};
}

// 函數節流的實現;
function throttle(fn, delay) {
var preTime = Date.now();

return function() {
var context = this,
args = arguments,
nowTime = Date.now();

// 如果兩次時間間隔超過了指定時間,則執行函數。
if (nowTime - preTime >= delay) {
  preTime = Date.now();
  return fn.apply(context, args);
}

};
}
回答:

函數防抖是指在事件被觸發 n 秒後再執行回調,如果在這 n 秒內事件又被觸發,則重新計時。這可以使用在一些點擊請求的事件上,避免因爲用戶的多次點擊向後端發送多次請求。

函數節流是指規定一個單位時間,在這個單位時間內,只能有一次觸發事件的回調函數執行,如果在同一個單位時間內某事件被觸發多次,只有一次能生效。節流可以使用在 scroll 函數的事件監聽上,通過事件節流來降低事件調用的頻率。
詳細資料可以參考:《輕鬆理解 JS 函數節流和函數防抖》《JavaScript 事件節流和事件防抖》《JS 的防抖與節流》

  1. Object.is() 與原來的比較操作符 “=”、“” 的區別?

相關知識點:

兩等號判等,會在比較時進行類型轉換。
三等號判等(判斷嚴格),比較時不進行隱式類型轉換,(類型不同則會返回false)。

Object.is 在三等號判等的基礎上特別處理了 NaN 、-0 和 +0 ,保證 -0 和 +0 不再相同,但 Object.is(NaN, NaN) 會返回 true.

Object.is 應被認爲有其特殊的用途,而不能用它認爲它比其它的相等對比更寬鬆或嚴格。
回答:

使用雙等號進行相等判斷時,如果兩邊的類型不一致,則會進行強制類型轉化後再進行比較。

使用三等號進行相等判斷時,如果兩邊的類型不一致時,不會做強制類型準換,直接返回 false。

使用 Object.is 來進行相等判斷時,一般情況下和三等號的判斷相同,它處理了一些特殊的情況,比如 -0 和 +0 不再相等,兩個 NaN 認定爲是相等的。
93. escape,encodeURI,encodeURIComponent 有什麼區別?

相關知識點:

escape 和 encodeURI 都屬於 Percent-encoding,基本功能都是把 URI 非法字符轉化成合法字符,轉化後形式類似「%*」。
它們的根本區別在於,escape 在處理 0xff 之外字符的時候,是直接使用字符的 unicode 在前面加上一個「%u」,而 encode URI 則是先進行 UTF-8,再在 UTF-8 的每個字節碼前加上一個「%」;在處理 0xff 以內字符時,編碼方式是一樣的(都是「%XX」,XX 爲字符的 16 進制 unicode,同時也是字符的 UTF-8),只是範圍(即哪些字符編碼哪些字符不編碼)不一樣。
回答:

encodeURI 是對整個 URI 進行轉義,將 URI 中的非法字符轉換爲合法字符,所以對於一些在 URI 中有特殊意義的字符不會進行轉義。

encodeURIComponent 是對 URI 的組成部分進行轉義,所以一些特殊字符也會得到轉義。

escape 和 encodeURI 的作用相同,不過它們對於 unicode 編碼爲 0xff 之外字符的時候會有區別,escape 是直接在字符的 unicode 編碼前加上 %u,而 encodeURI 首先會將字符轉換爲 UTF-8 的格式,再在每個字節前加上 %。
詳細資料可以參考:《escape,encodeURI,encodeURIComponent 有什麼區別?》

  1. Unicode 和 UTF-8 之間的關係?

Unicode 是一種字符集合,現在可容納 100 多萬個字符。每個字符對應一個不同的 Unicode 編碼,它只規定了符號的二進制代碼,卻沒有規定這個二進制代碼在計算機中如何編碼傳輸。

UTF-8 是一種對 Unicode 的編碼方式,它是一種變長的編碼方式,可以用 1~4 個字節來表示一個字符。
詳細資料可以參考:《字符編碼詳解》《字符編碼筆記:ASCII,Unicode 和 UTF-8》

  1. js 的事件循環是什麼?

相關知識點:

事件隊列是一個存儲着待執行任務的隊列,其中的任務嚴格按照時間先後順序執行,排在隊頭的任務將會率先執行,而排在隊尾的任務會最後執行。事件隊列每次僅執行一個任務,在該任務執行完畢之後,再執行下一個任務。執行棧則是一個類似於函數調用棧的運行容器,當執行棧爲空時,JS 引擎便檢查事件隊列,如果不爲空的話,事件隊列便將第一個任務壓入執行棧中運行。
回答:

因爲 js 是單線程運行的,在代碼執行的時候,通過將不同函數的執行上下文壓入執行棧中來保證代碼的有序執行。在執行同步代碼的時候,如果遇到了異步事件,js 引擎並不會一直等待其返回結果,而是會將這個事件掛起,繼續執行執行棧中的其他任務。當異步事件執行完畢後,再將異步事件對應的回調加入到與當前執行棧中不同的另一個任務隊列中等待執行。任務隊列可以分爲宏任務對列和微任務對列,噹噹前執行棧中的事件執行完畢後,js 引擎首先會判斷微任務對列中是否有任務可以執行,如果有就將微任務隊首的事件壓入棧中執行。當微任務對列中的任務都執行完成後再去判斷宏任務對列中的任務。

微任務包括了 promise 的回調、node 中的 process.nextTick 、對 Dom 變化監聽的 MutationObserver。

宏任務包括了 script 腳本的執行、setTimeout ,setInterval ,setImmediate 一類的定時事件,還有如 I/O 操作、UI 渲
染等。
詳細資料可以參考:《瀏覽器事件循環機制(event loop)》《詳解 JavaScript 中的 Event Loop(事件循環)機制》《什麼是 Event Loop?》《這一次,徹底弄懂 JavaScript 執行機制》

  1. js 中的深淺拷貝實現?

相關資料:

// 淺拷貝的實現;

function shallowCopy(object) {
// 只拷貝對象
if (!object || typeof object !== “object”) return;

// 根據 object 的類型判斷是新建一個數組還是對象
let newObject = Array.isArray(object) ? [] : {};

// 遍歷 object,並且判斷是 object 的屬性才拷貝
for (let key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] = object[key];
}
}

return newObject;
}

// 深拷貝的實現;

function deepCopy(object) {
if (!object || typeof object !== “object”) return;

let newObject = Array.isArray(object) ? [] : {};

for (let key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] =
typeof object[key] === “object” ? deepCopy(object[key]) : object[key];
}
}

return newObject;
}
回答:

淺拷貝指的是將一個對象的屬性值複製到另一個對象,如果有的屬性的值爲引用類型的話,那麼會將這個引用的地址複製給對象,因此兩個對象會有同一個引用類型的引用。淺拷貝可以使用 Object.assign 和展開運算符來實現。

深拷貝相對淺拷貝而言,如果遇到屬性值爲引用類型的時候,它新建一個引用類型並將對應的值複製給它,因此對象獲得的一個新的引用類型而不是一個原有類型的引用。深拷貝對於一些對象可以使用 JSON 的兩個函數來實現,但是由於 JSON 的對象格式比 js 的對象格式更加嚴格,所以如果屬性值裏邊出現函數或者 Symbol 類型的值時,會轉換失敗。
詳細資料可以參考:《JavaScript 專題之深淺拷貝》《前端面試之道》

  1. 手寫 call、apply 及 bind 函數

相關資料:

// call函數實現
Function.prototype.myCall = function(context) {
// 判斷調用對象
if (typeof this !== “function”) {
console.error(“type error”);
}

// 獲取參數
let args = […arguments].slice(1),
result = null;

// 判斷 context 是否傳入,如果未傳入則設置爲 window
context = context || window;

// 將調用函數設爲對象的方法
context.fn = this;

// 調用函數
result = context.fn(…args);

// 將屬性刪除
delete context.fn;

return result;
};

// apply 函數實現

Function.prototype.myApply = function(context) {
// 判斷調用對象是否爲函數
if (typeof this !== “function”) {
throw new TypeError(“Error”);
}

let result = null;

// 判斷 context 是否存在,如果未傳入則爲 window
context = context || window;

// 將函數設爲對象的方法
context.fn = this;

// 調用方法
if (arguments[1]) {
result = context.fn(…arguments[1]);
} else {
result = context.fn();
}

// 將屬性刪除
delete context.fn;

return result;
};

// bind 函數實現
Function.prototype.myBind = function(context) {
// 判斷調用對象是否爲函數
if (typeof this !== “function”) {
throw new TypeError(“Error”);
}

// 獲取參數
var args = […arguments].slice(1),
fn = this;

return function Fn() {
// 根據調用方式,傳入不同綁定值
return fn.apply(
this instanceof Fn ? this : context,
args.concat(…arguments)
);
};
};
回答:

call 函數的實現步驟:

1.判斷調用對象是否爲函數,即使我們是定義在函數的原型上的,但是可能出現使用 call 等方式調用的情況。
2.判斷傳入上下文對象是否存在,如果不存在,則設置爲 window 。
3.處理傳入的參數,截取第一個參數後的所有參數。
4.將函數作爲上下文對象的一個屬性。
5.使用上下文對象來調用這個方法,並保存返回結果。
6.刪除剛纔新增的屬性。
7.返回結果。
apply 函數的實現步驟:

1.判斷調用對象是否爲函數,即使我們是定義在函數的原型上的,但是可能出現使用 call 等方式調用的情況。
2.判斷傳入上下文對象是否存在,如果不存在,則設置爲 window 。
3.將函數作爲上下文對象的一個屬性。
4.判斷參數值是否傳入
4.使用上下文對象來調用這個方法,並保存返回結果。
5.刪除剛纔新增的屬性
6.返回結果
bind 函數的實現步驟:

1.判斷調用對象是否爲函數,即使我們是定義在函數的原型上的,但是可能出現使用 call 等方式調用的情況。
2.保存當前函數的引用,獲取其餘傳入參數值。
3.創建一個函數返回
4.函數內部使用 apply 來綁定函數調用,需要判斷函數作爲構造函數的情況,這個時候需要傳入當前函數的 this 給 apply 調用,其餘情況都傳入指定的上下文對象。
詳細資料可以參考:《手寫 call、apply 及 bind 函數》《JavaScript 深入之 call 和 apply 的模擬實現》

  1. 函數柯里化的實現

// 函數柯里化指的是一種將使用多個參數的一個函數轉換成一系列使用一個參數的函數的技術。

function curry(fn, args) {
// 獲取函數需要的參數長度
let length = fn.length;

args = args || [];

return function() {
let subArgs = args.slice(0);

// 拼接得到現有的所有參數
for (let i = 0; i < arguments.length; i++) {
  subArgs.push(arguments[i]);
}

// 判斷參數的長度是否已經滿足函數所需參數的長度
if (subArgs.length >= length) {
  // 如果滿足,執行函數
  return fn.apply(this, subArgs);
} else {
  // 如果不滿足,遞歸返回科裏化的函數,等待參數的傳入
  return curry.call(this, fn, subArgs);
}

};
}

// es6 實現
function curry(fn, …args) {
return fn.length <= args.length ? fn(…args) : curry.bind(null, fn, …args);
}
詳細資料可以參考:《JavaScript 專題之函數柯里化》

  1. 爲什麼 0.1 + 0.2 != 0.3?如何解決這個問題?

當計算機計算 0.1+0.2 的時候,實際上計算的是這兩個數字在計算機裏所存儲的二進制,0.1 和 0.2 在轉換爲二進制表示的時候會出現位數無限循環的情況。js 中是以 64 位雙精度格式來存儲數字的,只有 53 位的有效數字,超過這個長度的位數會被截取掉這樣就造成了精度丟失的問題。這是第一個會造成精度丟失的地方。在對兩個以 64 位雙精度格式的數據進行計算的時候,首先會進行對階的處理,對階指的是將階碼對齊,也就是將小數點的位置對齊後,再進行計算,一般是小階向大階對齊,因此小階的數在對齊的過程中,有效數字會向右移動,移動後超過有效位數的位會被截取掉,這是第二個可能會出現精度丟失的地方。當兩個數據階碼對齊後,進行相加運算後,得到的結果可能會超過 53 位有效數字,因此超過的位數也會被截取掉,這是可能發生精度丟失的第三個地方。

對於這樣的情況,我們可以將其轉換爲整數後再進行運算,運算後再轉換爲對應的小數,以這種方式來解決這個問題。

我們還可以將兩個數相加的結果和右邊相減,如果相減的結果小於一個極小數,那麼我們就可以認定結果是相等的,這個極小數可以
使用 es6 的 Number.EPSILON
詳細資料可以參考:《十進制的 0.1 爲什麼不能用二進制很好的表示?》《十進制浮點數轉成二進制》《浮點數的二進制表示》《js 浮點數存儲精度丟失原理》《浮點數精度之謎》《JavaScript 浮點數陷阱及解法》《0.1+0.2 !== 0.3?》《JavaScript 中奇特的~運算符》

  1. 原碼、反碼和補碼的介紹

原碼是計算機中對數字的二進制的定點表示方法,最高位表示符號位,其餘位表示數值位。優點是易於分辨,缺點是不能夠直接參與運算。

正數的反碼和其原碼一樣;負數的反碼,符號位爲1,數值部分按原碼取反。
如 [+7]原 = 00000111,[+7]反 = 00000111; [-7]原 = 10000111,[-7]反 = 11111000。

正數的補碼和其原碼一樣;負數的補碼爲其反碼加1。

例如 [+7]原 = 00000111,[+7]反 = 00000111,[+7]補 = 00000111;
[-7]原 = 10000111,[-7]反 = 11111000,[-7]補 = 11111001

之所以在計算機中使用補碼來表示負數的原因是,這樣可以將加法運算擴展到所有的數值計算上,因此在數字電路中我們只需要考慮加法器的設計就行了,而不用再爲減法設置新的數字電路。
詳細資料可以參考:《關於 2 的補碼》

推薦

筆者再次牆裂推薦收藏這個倉庫,收錄於CavsZhouyou - 前端面試複習筆記,這個倉庫是原作者校招時的前端複習筆記,主要總結一些比較重要的知識點和前端面試問題,希望對大家有所幫助。

最後如果文章和筆記能帶您一絲幫助或者啓發,請不要吝嗇你的贊和收藏,你的肯定是我前進的最大動力

未完結,請持續關注

上半部分JavaScript 知識點01-50 寫法:《關於前端174道 JavaScript知識點彙總

轉載鏈接:
https://github.com/CavsZhouyou/Front-End-Interview-Not
搜索
js100個經典實例
前端js面試題及答案
javascript各種實例
js的重點和難點
60個css常

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