前端面試知識點大全——JS篇(二)
目錄
1.閉包 與 作用域、作用域鏈、執行環境
1.1 閉包
《JS高程》定義:有權訪問另一個函式作用域內變數的函式都是閉包。通俗說法,可以訪問其他函式內部變數的函式。
閉包的作用:
1、函式柯里化的核心
2、匿名自執行函式(可以訪問全域性物件,同時有不會汙染全域性)
3、快取結果(父函式的變數不會釋放)
4、程式碼封裝
5、實現類和繼承
閉包的缺點:
(1)由於閉包會使得函式中的變數都被儲存在記憶體中,記憶體消耗很大,所以不能濫用閉包,否則會造成網頁的效能問題,在IE中可能導致記憶體洩露。解決方法是,在退出函式之前,將不使用的區域性變數全部刪除。
(2)閉包會在父函式外部,改變父函式內部變數的值。所以,如果你把父函式當作物件(object)使用,把閉包當作它的公用方法(Public Method),把內部變數當作它的私有屬性(private value),這時一定要小心,不要隨便改變父函式內部變數的值。
1.2 執行環境(執行上下文)
《JS高程》中的解釋是:執行環境定義了變數或者函式有權訪問的其他資料,決定了他們各自的行為。每個執行環境都至少有一個與之關聯的變數物件,環境中定義的所有變數和函式都儲存在這個物件中。
通俗的說,所謂的執行環境,可以看做程式碼當前執行的環境。
全域性執行環境(上下文)是最外圍的一個執行環境。在web瀏覽器中,全域性執行環境指的是window物件,因此所有全域性變數和函式都是作為window物件的屬性和方法建立的。
某個執行環境的所有程式碼執行完畢後,該環境被銷燬,儲存在其中的所有變數和函式定義也隨之銷燬。全域性執行環境直到應用程式退出(例如關閉網頁或瀏覽器)時才會被銷燬。
執行環境會隨著函式的呼叫和返回,不斷的重建和銷燬。但變數物件在有變數引用(如閉包)的情況下,將留在記憶體中不被銷燬。整個變數物件不銷燬,而不是某個變數不銷燬。
三類執行環境(上下文):
1、全域性級別的程式碼 – 這個是預設的程式碼執行環境,一旦程式碼被載入,js引擎最先進入的就是這個環境
2、函式級別的程式碼 – 當執行一個函式時,執行函式體中的程式碼
3、Eval的程式碼 – 在Eval函式內執行的程式碼(這個不常使用,也不推薦使用,故不作了解)
PS:不管什麼情況下,只存在一個全域性的上下文,該上下文能被任何其它的上下文所訪問到。函式上下文的個數是沒有任何限制的,每到呼叫執行一個函式時,js 引擎就會自動新建出一個函式上下文。在外部的上下文中是無法直接訪問到其內部上下文裡的變數的,但是內部上下文可以訪問到外部上下文中的的變數。
存在一個執行環境棧,全域性執行環境肯定是在最底部。當執行流進入一個函式時,函式的環境就會被推入一個環境棧中。在函式執行之後,棧將其環境彈出,把控制權返回給之前的執行環境。
1.3 作用域
ES5只有函式作用域和全域性作用域,沒有塊級作用域,ES6引入let和const,有了塊級作用域。一段程式程式碼中所用到的名字並不總是有效/可用的,而限定這個名字的可用性的程式碼範圍就是這個名字的作用域。其本身是一套規則,用於確定在何處以及如何查詢識別符號,規定了函式和變數可使用的位置
1.4 作用域鏈
《JS高程》:作用域鏈本質上是一個指向變數物件的指標列表,它只引用但不實際包含變數物件。
保證對執行環境有權訪問的所有變數和函式的有序訪問。每個函式都有一個執行環境,定義了變數或函式有權訪問的其他資料,與之關聯的就是一個變數物件,這個物件中包含了當前函式可訪問的變數及函式。當代碼在執行環境中執行時,就形成了作用域鏈。而作用域鏈就是指向這些變數物件的一個指標列表,他的最前端指向的就是當前執行環境的變數物件,最後面指向window全域性執行環境的變數物件。
作用域鏈的延長:with會將物件繫結到作用域鏈的頂端,try...catch語句中的catch捕獲到的物件會被放到作用域頂端,離開的時候會自動銷燬。
作用域的作用:使用作用域鏈主要是進行識別符號的查詢,識別符號解析就是沿著作用域鏈一級一級地搜尋識別符號的過程,而作用域鏈就是要保證對變數和函式的有序訪問
1.5 LHS(left-hand side)和RHS(right-hand side)查詢
在引擎執行的第一步操作中,對變數a進行了查詢,這種查詢叫做LHS查詢。實際上,引擎查詢共分為兩種:LHS查詢和RHS查詢 。
從字面意思去理解,當變量出現在賦值操作的左側時進行LHS查詢,出現在右側時進行RHS查詢。
更準確地講,RHS查詢與簡單地查詢某個變數的值沒什麼區別,而LHS查詢則是試圖找到變數的容器本身,從而可以對其賦值。
1.6 小結
1、執行環境有全域性執行環境(也稱為全域性環境)和函式執行環境之分
2、每次進入一個新執行環境,都會建立一個用於搜尋變數和函式的作用域鏈
3、函式的區域性環境不僅有權訪問函式作用域中的變數,而且有權訪問其包含(父)環境,乃至全域性環境,沿作用域鏈向外訪問變數
4、全域性環境只能訪問在全域性環境中定義的變數和函式,而不能直接訪問區域性環境中的任何資料
5、變數的執行環境有助於確定應該合適釋放記憶體
【1】javascript使用的是詞法作用域。對於函式來說,詞法作用域是在函式定義時就已經確定了,與函式是否被呼叫無關。通過作用域,可以知道作用域範圍內的變數和函式有哪些,卻不知道變數的值是什麼。所以作用域是靜態的
【2】對於函式來說,執行環境是在函式呼叫時確定的,執行環境包含作用域內所有變數和函式的值。在同一作用域下,不同的呼叫(如傳遞不同的引數)會產生不同的執行環境,從而產生不同的變數的值。所以執行環境是動態的
2.匿名函式
在js中分為匿名函式和命名函式,匿名函式常配合閉包和IIFE使用,用於減少全域性變數的汙染。也可以用變數繫結匿名函式,這樣變數就會指向匿名函式。
有一種特殊情況 const func = function bar(){};
這種情況下外部使用bar()會是未定義,bar主要用於函式內部使用,比如遞迴。
3.你是如何組織自己的程式碼?是使用模組模式,還是使用經典繼承的方法?
內部一些模組或者元件使用模組的方式,外部呼叫和正常使用時使用繼承的方式。
4.請指出以下程式碼的區別:function Person(){}、var person = Person()、var person = new Person()?
第一個是宣告定義一個Person函式
第二個是直接呼叫Person()函式,並將返回值賦值給變數person
第三個是建立了一個Person例項物件
5.apply call bind
5.1 區別
1、apply 、 call 、bind 三者都是用來改變函式的this物件的指向的;
2、apply 、 call 、bind 三者第一個引數都是this要指向的物件,也就是想指定的上下文;
3、apply 、 call 、bind 三者都可以利用後續引數傳參,call和bind一個一個寫,apply傳入陣列;
4、bind 是返回對應函式,便於稍後呼叫;apply 、call 則是立即呼叫 。
5.2 實現原理
apply 和call 是返回執行後的結果,所以在要繫結物件的物件內繫結一個函式執行即可。引數使用arguments匯出一個數組將剩餘的引數傳入。
1. 將函式設為物件的屬性
2. 執行該函式
3. 刪除該函式
(注:arguments是一個類似陣列的東西,只有索引和長度。在不手動的情況下可以使用Array.prototype.slice.call(arguments);但是這裡是手動情況所以要一點點讀取匯出。)
bind 的話,對於執行上面使用eval直接運算函式。返回函式的時候使用一個閉包
5.3 手動實現
call和apply就是將函式加入到繫結的物件中,然後在這個物件中執行函式。
比如:bar.call(foo),將this繫結到foo物件上,執行bar方法,就相當於,
var foo = {
value: 1,
bar: function() {
console.log(this.value);
}
};
foo.bar();
call的模擬實現:
Function.prototype.myCall = function(context) {
//call方法,第一項可以是null或者沒有,預設指向window
var context = context || window;
//獲取呼叫call的函式,用this獲取
context.fn = this;
//存放後面的陣列
var args = [];
//可以ES5的語法,args = Array.prototype.slice.call(arguments, 1);
for(var i=1,len=arguments.length;i<len; i++) {
//只有變成字串,在eval時才能轉成程式碼執行
args.push('arguments['+ i +']');
}
//直接呼叫toString方法
var result = eval('context.fn('+ args +')');
delete context.fn;
return result;
};
apply的模擬實現:
Function.prototype.myApply = function(context, argsArr) {
//call方法,第一項可以是null或者沒有,預設指向window
var context = context || window;
//獲取呼叫call的函式,用this獲取呼叫方法的物件(這裡是函式呼叫的apply,所以this指向函式本身,函式也是物件)
context.fn = this;
var result;
if (!argsArr) {
result = context.fn();
} else {
var args = [];
for(var i = 0, len = argsArr.length; i < len; i++) {
args.push('argsArr[' + i + ']');
}
result = eval('context.fn(' + args + ')')
}
delete context.fn
return result;
};
bind的模擬實現:
bind() 方法會建立一個新函式。當這個新函式被呼叫時,bind() 的第一個引數將作為它執行時的 this之後的序列引數將會在傳遞的實參前傳入作為它的引數。
Function.prototype.myBind2 = function(context) {
var aArgs = Array.prototype.slice.call(arguments, 1);
var that = this;
return function() {
return that.apply(context, aArgs.concat(Array.prototype.slice.call(arguments)))
}
}
6.new操作符
一個new操作符的內部行為,比如let func = new Func(params);
第一步:新建一個空物件,let obj=new Object();
第二步:為這個新建物件設定原型,繼承建構函式的原型 obj.__proto__ = Func.prototype;//有可能是基本型別
第三步:使用指定的引數呼叫建構函式 Foo ,並將 this 繫結到新建立的物件。let result = Func.call(obj,params); 執行建構函式並繫結this
第四步:判斷建構函式的返回型別,基本型別或無返回值就返回obj,是物件就返回result,即return typeof result === 'object' ? result : obj;
上述步驟具體等同於:
new Animal('cat') = {
var obj = {};
obj.__proto__ = Animal.prototype;
var result = Animal.call(obj,"cat");
return typeof result === 'object'? result : obj;
}
7.document.write() 與 innerHTML
document.write()用於寫入文件內容,可以傳入多個字串,寫入的字串會被HTML解析;返回undefined
注意事項:
1、如果document.write()在DOMContentLoaded或load事件的回撥函式中,當文件載入完成,則會先清空文件(自動呼叫document.open()),再把引數寫入body內容的開頭。也就是說原文件內容會被清除
2、在非同步引入的js和DOMContentLoaded或load事件的回撥函式中執行document.write(),執行完後,最好手動關閉文件寫入(document.close())。非同步引用外部js檔案,JS檔案中必須先執行document.open()清空文件,然後才能執行document.write(),引數寫在body內容的開頭。如果不先執行document.open(),直接執行document.write(),則無效。
document.write() 只能重新渲染繪製整個頁面
innerHTML 可以迴流頁面的一部分
8.特性檢測 特性推斷 UA字串嗅探
8.1 能力檢測(也稱為特性檢測)
能力檢測是識別瀏覽器的能力,只需要確定瀏覽器支援特定的能力,就能給出解決方案
通用方法檢測默寫方法和屬性是否可用或者存在,其次利用typeof進行能力檢測
8.2 怪癖檢測
也就是bug檢測,如果有bug就先修復
8.3 使用者代理檢測
navigator.useAgent屬性訪問,可以獲取使用者代理字串。這種方法優先順序最靠後,因為可以欺騙。
例如:Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.87 Safari/537.36
9.Ajax工作原理
9.1 作用
區域性重新整理頁面。就是能在不更新整個頁面的前提下維護資料。這使得Web應用程式更為迅捷地迴應使用者動作,並避免了在網路上傳送那些沒有改變過的資訊。
9.2 使用
const xhr = new XMLHttpRequest() //或者IE情況下使用ActiveXObject('Microsoft.XMLHttp')
xhr.open('POST','ajax.php',true);//預設就是async非同步
xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
xhr.send("fname=suifeng&lname=nifeng");
//非同步
xhr.onreadystatechange = function () {
if(xhr.readyState === 4 && xhr.status === 200){
document.getElementById("id").innerHTML = xhr.responseText;
}
}
//同步
xhr.open('GET','ajax.txt',false);
xhr.send(null);
document.getElementById("id").innerHTML = xhr.responseText;
9.3 響應資料格式
responseText:獲得字串形式的響應資料(DOMString)
responseXML:獲得XML形式的響應資料(document)
9.4 state表示xhr的狀態
0:請求未初始化,尚未呼叫open()方法
1:伺服器連線已建立,已呼叫open(),尚未呼叫send()
2:請求已接收,已呼叫send(),尚未收到響應
3:請求處理中,已經接收部分響應資料
4:請求已完成,且響應已就緒。資料全部接收,可以使用
9.5 status表示http狀態碼
200:“ok”
204:無內容
301:(永久重定向)
302:(臨時重定向)
304:(未修改)伺服器資源未修改,http響應不會返回網頁內容
403:(禁止)伺服器拒絕請求
404:(未找到)伺服器找不到請求的資源
500:(伺服器內部錯誤)伺服器遇到錯誤,無法完成請求
502:(錯誤閘道器) 伺服器作為閘道器或代理,從上游伺服器收到無效響應。
503:(服務不可達)伺服器維護或者過載,無法發完成請求
504:(閘道器超時)伺服器作為閘道器或代理,但沒有及時從上游伺服器收到請求
9.6 優點
1、最大的一點是頁面無重新整理,使用者的體驗非常好。
2、使用非同步方式與伺服器通訊,具有更加迅速的響應能力。
3、可以把以前一些伺服器負擔的工作轉嫁到客戶端,利用客戶端閒置的能力來處理,減輕伺服器和頻寬的負擔,節約空間和寬頻租用成本。並且減輕伺服器的負擔,ajax的原則是“按需取資料”,可以最大程度的減少冗餘請求,和響應對伺服器造成的負擔。
4、基於標準化的並被廣泛支援的技術,不需要下載外掛或者小程式。
9.7 缺點
1、ajax不支援瀏覽器back按鈕。
2、安全問題 AJAX暴露了與伺服器互動的細節。
3、對搜尋引擎的支援比較弱。
4、破壞了程式的異常機制。
5、不容易除錯。
10.跨域
跨域請求:從一個域的網頁去請求另一個域的資源,實現不同的域之間進行資料傳輸或通訊。協議、域名、埠有任何一處不一樣都被視為跨域。
PS:1、如果是協議和埠造成的跨域問題,前端是無能為力的;2、在跨域問題上,域僅僅是通過“URL的首部(協議、域名、埠)”來識別,而不會根據域名對應的IP地址是否相同來判斷。(也就是IP地址沒用)
10.1 圖片ping
它是與伺服器進行簡單、單項跨域通訊的一種方式。請求的資料是通過查詢字串形式傳送的(比如http://www.example.com/test.html?name=Jhon),他的響應通常書畫素圖或者204(無內容)響應,通過監聽load和error事件,可以知道響應是什麼時候收到的。
var img = new Image();
img.onload = img.onerror = function () {
console.log("receive");
};
img.src = "http://www.example.com/test.html?name=Jhon";
用途:影象ping最常用於跟蹤使用者點選頁面或者動態廣告曝光次數。
缺點:1、只能傳送get請求;2、無法訪問伺服器的響應文字
10.2 JSONP
JSON with padding(填充式JSON或引數式JSON)。通過動態生成script標籤,然後新增src,產生get請求,且不會發生跨域問題。通過get請求新增callback引數的方式,後臺返回由callback函式包裹JSON資料的方式傳回給前端,這個程式碼會在前端作為js程式碼直接執行,只要前端這邊提前寫好了callback函式即可。(需要後臺配合)
優點:相容性更好,可以在舊瀏覽器中執行,在請求完畢後可以通過呼叫callback的方式回傳結果
缺點:1、只支援get請求;2、安全問題(請求返回的程式碼中可能存在安全隱患,需要字串過濾?);3、要確定jsonp請求是否失敗並不容易;4、需要商量基礎token作為身份驗證,不然是個網站都可以拿到網站的資料。
10.3 CORS(跨域資源共享 )
請求頭部設定Origin:URL,響應頭部設定Acess-Control-Access-Origin:URL(或者*,表示所有的都行),兩個URL相同就可以實現跨域訪問了。
10.4 document.domain
document.domain="example.com"的方式進行修改二級域名,實現後兩個域名相同的網頁通訊。一般適用於不同子域的框架之間的通訊,這樣可以獲取別的框架頁面的指令碼的物件和屬性
如果你想通過ajax的方法去與不同子域的頁面互動,除了使用jsonp的方法外,還可以用一個隱藏的iframe來做一個代理。原理就是讓這個iframe載入一個與你想要通過ajax獲取資料的目標頁面處在相同的域的頁面,所以這個iframe中的頁面是可以正常使用ajax去獲取你要的資料的,然後就是通過我們剛剛講得修改document.domain的方法,讓我們能通過js完全控制這個iframe(利用iframe框架的contentWindow屬性可以獲取該頁面window物件,裡面包含了頁面內的所有活動屬性和物件),這樣我們就可以讓iframe去傳送ajax請求,然後收到的資料我們也可以獲得了。
10.5 window.name
window.name:window物件有個name屬性,該屬性有個特徵:即在一個視窗(window)的生命週期內,視窗載入的所有的頁面都是共享一個window.name的,每個頁面對window.name都有讀寫的許可權,window.name是持久存在一個視窗載入過的所有頁面中的,並不會因新頁面的載入而進行重置。
10.6 postMessage
postMessage:跨文件訊息則是通過向Window例項傳送訊息來完成的。在使用時,軟體開發人員需要通過呼叫一個Window的postMessage()函式來向該Window例項傳送訊息。此時Window例項內部的onmessage事件將被觸發,進而使得該事件的訊息處理函式被呼叫。但是在接收到訊息的時候,訊息處理函式首先需要判斷訊息來源的合法性,以避免惡意使用者通過傳送訊息的方式來非法執行程式碼。otherWindow.postMessage(message, targetOrigin, [transfer]);