(以下所有答案僅供參考)
簡答題
1、什麼是防抖和節流?有什麼差別?如何實作?
參考答案
防抖
觸發高頻事件後n秒内函數隻會執行一次,如果n秒内高頻事件再次被觸發,則重新計算時間
- 思路:
每次觸發事件時都取消之前的延時調用方法
function debounce(fn) {
let timeout = null; // 建立一個标記用來存放定時器的傳回值
return function () {
clearTimeout(timeout); // 每當使用者輸入的時候把前一個 setTimeout clear 掉
timeout = setTimeout(() => { // 然後又建立一個新的 setTimeout, 這樣就能保證輸入字元後的 interval 間隔内如果還有字元輸入的話,就不會執行 fn 函數
fn.apply(this, arguments);
}, 500);
};
}
function sayHi() {
console.log('防抖成功');
}
var inp = document.getElementById('inp');
inp.addEventListener('input', debounce(sayHi)); // 防抖
節流
高頻事件觸發,但在n秒内隻會執行一次,是以節流會稀釋函數的執行頻率
- 思路:
每次觸發事件時都判斷目前是否有等待執行的延時函數
function throttle(fn) {
let canRun = true; // 通過閉包儲存一個标記
return function () {
if (!canRun) return; // 在函數開頭判斷标記是否為true,不為true則return
canRun = false; // 立即設定為false
setTimeout(() => { // 将外部傳入的函數的執行放在setTimeout中
fn.apply(this, arguments);
// 最後在setTimeout執行完畢後再把标記設定為true(關鍵)表示可以執行下一次循環了。當定時器沒有執行的時候标記永遠是false,在開頭被return掉
canRun = true;
}, 500);
};
}
function sayHi(e) {
console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi));
2、 get請求傳參長度的誤區、get和post請求在緩存方面的差別
誤區:我們經常說get請求參數的大小存在限制,而post請求的參數大小是無限制的。
參考答案
實際上HTTP 協定從未規定 GET/POST 的請求長度限制是多少。對get請求參數的限制是來源與浏覽器或web伺服器,浏覽器或web伺服器限制了url的長度。為了明确這個概念,我們必須再次強調下面幾點:
- HTTP 協定 未規定 GET 和POST的長度限制
- GET的最大長度顯示是因為 浏覽器和 web伺服器限制了 URI的長度
- 不同的浏覽器和WEB伺服器,限制的最大長度不一樣
- 要支援IE,則最大長度為2083byte,若隻支援Chrome,則最大長度 8182byte
補充補充一個get和post在緩存方面的差別:
- get請求類似于查找的過程,使用者擷取資料,可以不用每次都與資料庫連接配接,是以可以使用緩存。
- post不同,post做的一般是修改和删除的工作,是以必須與資料庫互動,是以不能使用緩存。是以get請求适合于請求緩存。
3、子產品化發展曆程
可從IIFE、AMD、CMD、CommonJS、UMD、webpack(require.ensure)、ES Module、
<script type="module">
這幾個角度考慮。
參考答案
子產品化主要是用來抽離公共代碼,隔離作用域,避免變量沖突等。
IIFE:使用自執行函數來編寫子產品化,特點:在一個單獨的函數作用域中執行代碼,避免變量沖突。
(function(){
return {
data:[]
}
})()
AMD:使用requireJS 來編寫子產品化,特點:依賴必須提前聲明好。
define('./index.js',function(code){
// code 就是index.js 傳回的内容
})
CMD:使用seaJS 來編寫子產品化,特點:支援動态引入依賴檔案。
define(function(require, exports, module) {
var indexCode = require('./index.js');
})
CommonJS:nodejs 中自帶的子產品化。
var fs = require('fs');
UMD:相容AMD,CommonJS 子產品化文法。
webpack(require.ensure):webpack 2.x 版本中的代碼分割。
ES Modules:ES6 引入的子產品化,支援import 來引入另一個 js 。
import a from 'a';
4、npm 子產品安裝機制,為什麼輸入 npm install 就可以自動安裝對應的子產品?
參考答案
1. npm 子產品安裝機制:
- 發出
指令npm install
- 查詢node_modules目錄之中是否已經存在指定子產品
-
- npm 向 registry 查詢子產品壓縮包的網址
- 下載下傳壓縮包,存放在根目錄下的
目錄裡.npm
- 解壓壓縮包到目前項目的
目錄node_modules
- 若存在,不再重新安裝
- 若不存在
2. npm 實作原理
輸入 npm install 指令并敲下回車後,會經曆如下幾個階段(以 npm 5.5.1 為例):
-
執行工程自身 preinstall
目前 npm 工程如果定義了 preinstall 鈎子此時會被執行。
-
确定首層依賴子產品
首先需要做的是确定工程中的首層依賴,也就是 dependencies 和 devDependencies 屬性中直接指定的子產品(假設此時沒有添加 npm install 參數)。
工程本身是整棵依賴樹的根節點,每個首層依賴子產品都是根節點下面的一棵子樹,npm 會開啟多程序從每個首層依賴子產品開始逐漸尋找更深層級的節點。
-
擷取子產品
擷取子產品是一個遞歸的過程,分為以下幾步:
- 擷取子產品資訊。在下載下傳一個子產品之前,首先要确定其版本,這是因為 package.json 中往往是 semantic version(semver,語義化版本)。此時如果版本描述檔案(npm-shrinkwrap.json 或 package-lock.json)中有該子產品資訊直接拿即可,如果沒有則從倉庫擷取。如 packaeg.json 中某個包的版本是 ^1.1.0,npm 就會去倉庫中擷取符合 1.x.x 形式的最新版本。
- 擷取子產品實體。上一步會擷取到子產品的壓縮包位址(resolved 字段),npm 會用此位址檢查本地緩存,緩存中有就直接拿,如果沒有則從倉庫下載下傳。
- 查找該子產品依賴,如果有依賴則回到第1步,如果沒有則停止。
-
子產品扁平化(dedupe)
上一步擷取到的是一棵完整的依賴樹,其中可能包含大量重複子產品。比如 A 子產品依賴于 loadsh,B 子產品同樣依賴于 lodash。在 npm3 以前會嚴格按照依賴樹的結構進行安裝,是以會造成子產品備援。
從 npm3 開始預設加入了一個 dedupe 的過程。它會周遊所有節點,逐個将子產品放在根節點下面,也就是 node-modules 的第一層。當發現有重複子產品時,則将其丢棄。
這裡需要對重複子產品進行一個定義,它指的是子產品名相同且 semver 相容。每個 semver 都對應一段版本允許範圍,如果兩個子產品的版本允許範圍存在交集,那麼就可以得到一個相容版本,而不必版本号完全一緻,這可以使更多備援子產品在 dedupe 過程中被去掉。
比如 node-modules 下 foo 子產品依賴 lodash@^1.0.0,bar 子產品依賴 lodash@^1.1.0,則 ^1.1.0 為相容版本。
而當 foo 依賴 lodash@^2.0.0,bar 依賴 lodash@^1.1.0,則依據 semver 的規則,二者不存在相容版本。會将一個版本放在 node_modules 中,另一個仍保留在依賴樹裡。
舉個例子,假設一個依賴樹原本是這樣:
node_modules
-- foo
---- [email protected]
-- bar
---- [email protected]
假設 version1 和 version2 是相容版本,則經過 dedupe 會成為下面的形式:
node_modules
-- foo
-- bar
-- lodash(保留的版本為相容版本)
假設 version1 和 version2 為非相容版本,則後面的版本保留在依賴樹中:
node_modules
-- foo
-- [email protected]
-- bar
---- [email protected]
-
安裝子產品
這一步将會更新工程中的 node_modules,并執行子產品中的生命周期函數(按照 preinstall、install、postinstall 的順序)。
-
執行工程自身生命周期
目前 npm 工程如果定義了鈎子此時會被執行(按照 install、postinstall、prepublish、prepare 的順序)。
最後一步是生成或更新版本描述檔案,npm install 過程完成。
-
5、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關鍵字,否則報錯。
6、setTimeout、Promise、Async/Await 的差別
參考答案:
https://gongchenghuigch.github.io/2019/09/14/awat/
7、定時器的執行順序或機制?
因為js是單線程的,浏覽器遇到setTimeout或者setInterval會先執行完目前的代碼塊,在此之前會把定時器推入浏覽器的待執行事件隊列裡面,等到浏覽器執行完目前代碼之後會看一下事件隊列裡面有沒有任務,有的話才執行定時器的代碼。是以即使把定時器的時間設定為0還是會先執行目前的一些代碼。參考答案
輸出結果:function test(){ var aa = 0; var testSet = setInterval(function(){ aa++; console.log(123); if(aa<10){ clearInterval(testSet); } },20); var testSet1 = setTimeout(function(){ console.log(321) },1000); for(var i=0;i<10;i++){ console.log('test'); } } test()
test //10次 undefined 123 321
8、['1','2','3'].map(parseInt) 輸出什麼,為什麼?
輸出:[1, NaN, NaN]參考答案
- 首先讓我們回顧一下,map函數的第一個參數callback:
這個callback一共可以接收三個參數,其中第一個參數代表目前被處理的元素,而第二個參數代表該元素的索引。var new_array = arr.map(function callback(currentValue[, index[, array]]) { // Return element for new_array }[, thisArg])
- 而parseInt則是用來解析字元串的,使字元串成為指定基數的整數。
接收兩個參數,第一個表示被處理的值(字元串),第二個表示為解析時的基數。parseInt(string, radix)
- 了解這兩個函數後,我們可以模拟一下運作情況
- parseInt('1', 0) //radix為0時,且string參數不以“0x”和“0”開頭時,按照10為基數處理。這個時候傳回1
- parseInt('2', 1) //基數為1(1進制)表示的數中,最大值小于2,是以無法解析,傳回NaN
- parseInt('3', 2) //基數為2(2進制)表示的數中,最大值小于3,是以無法解析,傳回NaN
- map函數傳回的是一個數組,是以最後結果為[1, NaN, NaN]
9、Doctype作用? 嚴格模式與混雜模式如何區分?它們有何意義?
Doctype聲明于文檔最前面,告訴浏覽器以何種方式來渲染頁面,這裡有兩種模式,嚴格模式和混雜模式。參考答案
- 嚴格模式的排版和 JS 運作模式是 以該浏覽器支援的最高标準運作。
- 混雜模式,向後相容,模拟老式浏覽器,防止浏覽器無法相容頁面。
10、fetch發送2次請求的原因
參考答案
fetch發送post請求的時候,總是發送2次,第一次狀态碼是204,第二次才成功?
原因很簡單,因為你用fetch的post請求的時候,導緻fetch 第一次發送了一個Options請求,詢問伺服器是否支援修改的請求頭,如果伺服器支援,則在第二次中發送真正的請求。
http、浏覽器對象
1、HTTPS 握手過程中,用戶端如何驗證證書的合法性
參考答案
-
首先什麼是HTTP協定?
http協定是超文本傳輸協定,位于tcp/ip四層模型中的應用層;通過請求/響應的方式在用戶端和伺服器之間進行通信;但是缺少安全性,http協定資訊傳輸是通過明文的方式傳輸,不做任何加密,相當于在網絡上裸奔;容易被中間人惡意篡改,這種行為叫做中間人攻擊;
-
加密通信:
為了安全性,雙方可以使用對稱加密的方式key進行資訊交流,但是這種方式對稱加密秘鑰也會被攔截,也不夠安全,進而還是存在被中間人攻擊風險;
于是人們又想出來另外一種方式,使用非對稱加密的方式;使用公鑰/私鑰加解密;通信方A發起通信并攜帶自己的公鑰,接收方B通過公鑰來加密對稱秘鑰;然後發送給發起方A;A通過私鑰解密;雙發接下來通過對稱秘鑰來進行加密通信;但是這種方式還是會存在一種安全性;中間人雖然不知道發起方A的私鑰,但是可以做到偷天換日,将攔截發起方的公鑰key;并将自己生成的一對公/私鑰的公鑰發送給B;接收方B并不知道公鑰已經被偷偷換過;按照之前的流程,B通過公鑰加密自己生成的對稱加密秘鑰key2;發送給A;
這次通信再次被中間人攔截,盡管後面的通信,兩者還是用key2通信,但是中間人已經掌握了Key2;可以進行輕松的加解密;還是存在被中間人攻擊風險;
- 解決困境:權威的證書頒發機構CA來解決;
-
- 制作證書:作為服務端的A,首先把自己的公鑰key1發給證書頒發機構,向證書頒發機構進行申請證書;證書頒發機構有一套自己的公私鑰,CA通過自己的私鑰來加密key1,并且通過服務端網址等資訊生成一個證書簽名,證書簽名同樣使用機構的私鑰進行加密;制作完成後,機構将證書發給A;
- 校驗證書真僞:當B向服務端A發起請求通信的時候,A不再直接傳回自己的公鑰,而是傳回一個證書;
說明:各大浏覽器和作業系統已經維護了所有的權威證書機構的名稱和公鑰。B隻需要知道是哪個權威機構發的證書,使用對應的機構公鑰,就可以解密出證書簽名;接下來,B使用同樣的規則,生成自己的證書簽名,如果兩個簽名是一緻的,說明證書是有效的;
簽名驗證成功後,B就可以再次利用機構的公鑰,解密出A的公鑰key1;接下來的操作,就是和之前一樣的流程了;
- 中間人是否會攔截發送假證書到B呢?
- https主要的思想是在http基礎上增加了ssl安全層,即以上認證過程;
2、TCP三向交握和四次揮手
參考答案
三次握手之是以是三次是保證client和server均讓對方知道自己的接收和發送能力沒問題而保證的最小次數。
第一次client => server 隻能server判斷出client具備發送能力
第二次 server => client client就可以判斷出server具備發送和接受能力。此時client還需讓server知道自己接收能力沒問題于是就有了第三次
第三次 client => server 雙方均保證了自己的接收和發送能力沒有問題
其中,為了保證後續的握手是為了應答上一個握手,每次握手都會帶一個辨別 seq,後續的ACK都會對這個seq進行加一來進行确認。
3、img iframe script 來發送跨域請求有什麼優缺點?
參考答案
- iframe
優點:跨域完畢之後DOM操作和互相之間的JavaScript調用都是沒有問題的
缺點:1.若結果要以URL參數傳遞,這就意味着在結果資料量很大的時候需要分割傳遞,巨煩。2.還有一個是iframe本身帶來的,母頁面和iframe本身的互動本身就有安全性限制。
- script
優點:可以直接傳回json格式的資料,友善處理
缺點:隻接受GET請求方式
- 圖檔ping
優點:可以通路任何url,一般用來進行點選追蹤,做頁面分析常用的方法
缺點:不能通路響應文本,隻能監聽是否響應
4、http和https的差別?
http傳輸的資料都是未加密的,也就是明文的,網景公司設定了SSL協定來對http協定傳輸的資料進行加密處理,簡單來說https協定是由http和ssl協定建構的可進行加密傳輸和身份認證的網絡協定,比http協定的安全性更高。主要的差別如下:參考答案
- Https協定需要ca證書,費用較高。
- http是超文本傳輸協定,資訊是明文傳輸,https則是具有安全性的ssl加密傳輸協定。
- 使用不同的連結方式,端口也不同,一般而言,http協定的端口為80,https的端口為443
- http的連接配接很簡單,是無狀态的;HTTPS協定是由SSL+HTTP協定建構的可進行加密傳輸、身份認證的網絡協定,比http協定安全。
5、什麼是Bom?有哪些常用的Bom屬性?
參考答案
Bom是浏覽器對象
location對象
- location.href-- 傳回或設定目前文檔的URL
- location.search -- 傳回URL中的查詢字元串部分。例如 http://www.dreamdu.com/dreamd... 傳回包括(?)後面的内容?id=5&name=dreamdu
- location.hash -- 傳回URL#後面的内容,如果沒有#,傳回空 location.host -- 傳回URL中的域名部分,例如www.dreamdu.com
- location.hostname -- 傳回URL中的主域名部分,例如dreamdu.com
- location.pathname -- 傳回URL的域名後的部分。例如 http://www.dreamdu.com/xhtml/ 傳回/xhtml/
- location.port -- 傳回URL中的端口部分。例如 http://www.dreamdu.com:8080/xhtml/ 傳回8080
- location.protocol -- 傳回URL中的協定部分。例如 http://www.dreamdu.com:8080/xhtml/ 傳回(//)前面的内容http:
- location.assign -- 設定目前文檔的URL
- location.replace() -- 設定目前文檔的URL,并且在history對象的位址清單中移除這個URL location.replace(url);
- location.reload() -- 重載目前頁面
- history.go() -- 前進或後退指定的頁面數
- history.go(num); history.back() -- 後退一頁
- history.forward() -- 前進一頁
- navigator.userAgent -- 傳回使用者代理頭的字元串表示(就是包括浏覽器版本資訊等的字元串)
- navigator.cookieEnabled -- 傳回浏覽器是否支援(啟用)cookie
6、Cookie、sessionStorage、localStorage的差別
共同點:都是儲存在浏覽器端,并且是同源的參考答案
- Cookie:cookie資料始終在同源的http請求中攜帶(即使不需要),即cookie在浏覽器和伺服器間來回傳遞。而sessionStorage和localStorage不會自動把資料發給伺服器,僅在本地儲存。cookie資料還有路徑(path)的概念,可以限制cookie隻屬于某個路徑下,存儲的大小很小隻有4K左右。(key:可以在浏覽器和伺服器端來回傳遞,存儲容量小,隻有大約4K左右)
- sessionStorage:僅在目前浏覽器視窗關閉前有效,自然也就不可能持久保持,localStorage:始終有效,視窗或浏覽器關閉也一直儲存,是以用作持久資料;cookie隻在設定的cookie過期時間之前一直有效,即使視窗或浏覽器關閉。(key:本身就是一個回話過程,關閉浏覽器後消失,session為一個回話,當頁面不同即使是同一頁面打開兩次,也被視為同一次回話)
- localStorage:localStorage 在所有同源視窗中都是共享的;cookie也是在所有同源視窗中都是共享的。(key:同源視窗都會共享,并且不會失效,不管視窗或者浏覽器關閉與否都會始終生效)
- 儲存使用者登入狀态。例如将使用者id存儲于一個cookie内,這樣當使用者下次通路該頁面時就不需要重新登入了,現在很多論壇和社群都提供這樣的功能。cookie還可以設定過期時間,當超過時間期限後,cookie就會自動消失。是以,系統往往可以提示使用者保持登入狀态的時間:常見選項有一個月、三個 月、一年等。
- 跟蹤使用者行為。例如一個天氣預報網站,能夠根據使用者選擇的地區顯示當地的天氣情況。如果每次都需要選擇所在地是煩瑣的,當利用了 cookie後就會顯得很人性化了,系統能夠記住上一次通路的地區,當下次再打開該頁面時,它就會自動顯示上次使用者所在地區的天氣情況。因為一切都是在後 台完成,是以這樣的頁面就像為某個使用者所定制的一樣,使用起來非常友善
- 定制頁面。如果網站提供了換膚或更換布局的功能,那麼可以使用cookie來記錄使用者的選項,例如:背景色、分辨率等。當使用者下次通路時,仍然可以儲存上一次通路的界面風格。
7、Cookie如何防範XSS攻擊
XSS(跨站腳本攻擊)是指攻擊者在傳回的HTML中嵌入javascript腳本,為了減輕這些攻擊,需要在HTTP頭部配上,set-cookie:參考答案
- httponly-這個屬性可以防止XSS,它會禁止javascript腳本來通路cookie。
- secure - 這個屬性告訴浏覽器僅在請求為https的時候發送cookie。
8、浏覽器和 Node 事件循環的差別?
其中一個主要的差別在于浏覽器的event loop 和nodejs的event loop 在處理異步事件的順序是不同的,nodejs中有micro event;其中Promise屬于micro event 該異步事件的處理順序就和浏覽器不同.nodejs V11.0以上 這兩者之間的順序就相同了.參考答案
function test () { console.log('start') setTimeout(() => { console.log('children2') Promise.resolve().then(() => {console.log('children2-1')}) }, 0) setTimeout(() => { console.log('children3') Promise.resolve().then(() => {console.log('children3-1')}) }, 0) Promise.resolve().then(() => {console.log('children1')}) console.log('end') } test() // 以上代碼在node11以下版本的執行結果(先執行所有的宏任務,再執行微任務) // start // end // children1 // children2 // children3 // children2-1 // children3-1 // 以上代碼在node11及浏覽器的執行結果(順序執行宏任務和微任務) // start // end // children1 // children2 // children2-1 // children3 // children3-1
9、簡述HTTPS中間人攻擊
參考答案
https協定由 http + ssl 協定構成,具體的連結過程可參考SSL或TLS握手的概述
中間人攻擊過程如下:
- 伺服器向用戶端發送公鑰。
- 攻擊者截獲公鑰,保留在自己手上。
- 然後攻擊者自己生成一個【僞造的】公鑰,發給用戶端。
- 用戶端收到僞造的公鑰後,生成加密hash值發給伺服器。
- 攻擊者獲得加密hash值,用自己的私鑰解密獲得真秘鑰。
- 同時生成假的加密hash值,發給伺服器。
- 伺服器用私鑰解密獲得假秘鑰。
- 伺服器用加秘鑰加密傳輸資訊
- 服務端在發送浏覽器的公鑰中加入CA憑證,浏覽器可以驗證CA憑證的有效性
10、說幾條web前端優化政策
參考答案
(1). 減少HTTP請求數
這條政策基本上所有前端人都知道,而且也是最重要最有效的。都說要減少HTTP請求,那請求多了到底會怎麼樣呢?首先,每個請求都是有成本的,既包 含時間成本也包含資源成本。一個完整的請求都需要經過DNS尋址、與伺服器建立連接配接、發送資料、等待伺服器響應、接收資料這樣一個“漫長”而複雜的過程。時間成本就是使用者需要看到或者“感受”到這個資源是必須要等待這個過程結束的,資源上由于每個請求都需要攜帶資料,是以每個請求都需要占用帶寬。
另外,由于浏覽器進行并發請求的請求數是有上限的,是以請求數多了以後,浏覽器需要分批進行請求,是以會增加使用者的等待時間,會給 使用者造成站點速度慢這樣一個印象,即使可能使用者能看到的第一屏的資源都已經請求完了,但是浏覽器的進度條會一直存在。減少HTTP請求數的主要途徑包括:
(2). 從設計實作層面簡化頁面
如果你的頁面像百度首頁一樣簡單,那麼接下來的規則基本上都用不着了。保持頁面簡潔、減少資源的使用時最直接的。如果不是這樣,你的頁面需要華麗的皮膚,則繼續閱讀下面的内容。
(3). 合理設定HTTP緩存
緩存的力量是強大的,恰當的緩存設定可以大大的減少HTTP請求。以有啊首頁為例,當浏覽器沒有緩存的時候通路一共會發出78個請求,共600多K 資料(如圖1.1),而當第二次通路即浏覽器已緩存之後通路則僅有10個請求,共20多K資料(如圖1.2)。(這裡需要說明的是,如果直接F5重新整理頁面 的話效果是不一樣的,這種情況下請求數還是一樣,不過被緩存資源的請求伺服器是304響應,隻有Header沒有Body,可以節省帶寬)
怎樣才算合理設定?原則很簡單,能緩存越多越好,能緩存越久越好。例如,很少變化的圖檔資源可以直接通過HTTP Header中的Expires設定一個很長的過期頭;變化不頻繁而又可能會變的資源可以使用Last-Modifed來做請求驗證。盡可能的讓資源能夠 在緩存中待得更久。
(4). 資源合并與壓縮
如果可以的話,盡可能的将外部的腳本、樣式進行合并,多個合為一個。另外,CSS、Javascript、Image都可以用相應的工具進行壓縮,壓縮後往往能省下不少空間。
(5). CSS Sprites
合并CSS圖檔,減少請求數的又一個好辦法。
(6). Inline Images
使用data: URL scheme的方式将圖檔嵌入到頁面或CSS中,如果不考慮資源管理上的問題的話,不失為一個好辦法。如果是嵌入頁面的話換來的是增大了頁面的體積,而且無法利用浏覽器緩存。使用在CSS中的圖檔則更為理想一些。
(7). Lazy Load Images
這條政策實際上并不一定能減少HTTP請求數,但是卻能在某些條件下或者頁面剛加載時減少HTTP請求數。對于圖檔而言,在頁面剛加載的時候可以隻 加載第一屏,當使用者繼續往後滾屏的時候才加載後續的圖檔。這樣一來,假如使用者隻對第一屏的内容感興趣時,那剩餘的圖檔請求就都節省了。有啊首頁曾經的做法 是在加載的時候把第一屏之後的圖檔位址緩存在Textarea标簽中,待使用者往下滾屏的時候才“惰性”加載。
11、你了解的浏覽器的重繪和回流導緻的性能問題
參考答案
重繪(Repaint)和回流(Reflow)
重繪和回流是渲染步驟中的一小節,但是這兩個步驟對于性能影響很大。
- 重繪是當節點需要更改外觀而不會影響布局的,比如改變
就叫稱為重繪color
- 回流是布局或者幾何屬性需要改變就稱為回流。
回流必定會發生重繪,重繪不一定會引發回流。回流所需的成本比重繪高的多,改變深層次的節點很可能導緻父節點的一系列回流。
是以以下幾個動作可能會導緻性能問題:
- 改變 window 大小
- 改變字型
- 添加或删除樣式
- 文字改變
- 定位或者浮動
- 盒模型
- 當 Event loop 執行完 Microtasks 後,會判斷 document 是否需要更新。因為浏覽器是 60Hz 的重新整理率,每 16ms 才會更新一次。
- 然後判斷是否有
或者resize
,有的話會去觸發事件,是以scroll
和resize
事件也是至少 16ms 才會觸發一次,并且自帶節流功能。scroll
- 判斷是否觸發了 media query
- 更新動畫并且發送事件
- 判斷是否有全屏操作事件
- 執行
回調requestAnimationFrame
- 執行
回調,該方法用于判斷元素是否可見,可以用于懶加載上,但是相容性不好InterpObserver
- 更新界面
- 以上就是一幀中可能會做的事情。如果在一幀中有空閑時間,就會去執行
回調。requestIdleCallback
- 使用
替代translate
top
<div class="test"></div> <style> .test { position: absolute; top: 10px; width: 100px; height: 100px; background: red; } </style> <script> setTimeout(() => { // 引起回流 document.querySelector('.test').style.top = '100px' }, 1000) </script>
- 使用
替換visibility
display: none
,因為前者隻會引起重繪,後者會引發回流(改變了布局)
把 DOM 離線後修改,比如:先把 DOM 給
display:none
(有一次 Reflow),然後你修改100次,然後再把它顯示出來
不要把 DOM 結點的屬性值放在一個循環裡當成循環裡的變量
for(let i = 0; i < 1000; i++) { // 擷取 offsetTop 會導緻回流,因為需要去擷取正确的值 console.log(document.querySelector('.test').style.offsetTop) }
- 不要使用 table 布局,可能很小的一個小改動會造成整個 table 的重新布局
- 動畫實作的速度的選擇,動畫速度越快,回流次數越多,也可以選擇使用
requestAnimationFrame
- CSS 選擇符從右往左比對查找,避免 DOM 深度過深
- 将頻繁運作的動畫變為圖層,圖層能夠阻止該節點回流影響别的元素。比如對于
标簽,浏覽器會自動将該節點變為圖層。video
react、Vue
1、寫 React / Vue 項目時為什麼要在清單元件中寫 key,其作用是什麼?
參考答案
vue和react都是采用diff算法來對比新舊虛拟節點,進而更新節點。在vue的diff函數中(建議先了解一下diff算法過程)。
在交叉對比中,當新節點跟舊節點
頭尾交叉對比
沒有結果時,會根據新節點的key去對比舊節點數組中的key,進而找到相應舊節點(這裡對應的是一個key => index 的map映射)。如果沒找到就認為是一個新增節點。而如果沒有key,那麼就會采用周遊查找的方式去找到對應的舊節點。一種一個map映射,另一種是周遊查找。相比而言。map映射的速度更快。
vue部分源碼如下:
建立map函數// vue項目 src/core/vdom/patch.js -488行 // 以下是為了閱讀性進行格式化後的代碼 // oldCh 是一個舊虛拟節點數組 if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) } if(isDef(newStartVnode.key)) { // map 方式擷取 idxInOld = oldKeyToIdx[newStartVnode.key] } else { // 周遊方式擷取 idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) }
周遊尋找function createKeyToOldIdx (children, beginIdx, endIdx) { let i, key const map = {} for (i = beginIdx; i <= endIdx; ++i) { key = children[i].key if (isDef(key)) map[key] = i } return map }
// sameVnode 是對比新舊節點是否相同的函數 function findIdxInOld (node, oldCh, start, end) { for (let i = start; i < end; i++) { const c = oldCh[i] if (isDef(c) && sameVnode(node, c)) return i } }
2、React 中 setState 什麼時候是同步的,什麼時候是異步的?
參考答案
在React中,如果是由React引發的事件處理(比如通過onClick引發的事件處理),調用setState不會同步更新this.state,除此之外的setState調用會同步執行this.state。所謂“除此之外”,指的是繞過React通過addEventListener直接添加的事件處理函數,還有通過setTimeout/setInterval産生的異步調用。
**原因:**在React的setState函數實作中,會根據一個變量isBatchingUpdates判斷是直接更新this.state還是放到隊列中回頭再說,而isBatchingUpdates預設是false,也就表示setState會同步更新this.state,但是,有一個函數batchedUpdates,這個函數會把isBatchingUpdates修改為true,而當React在調用事件處理函數之前就會調用這個batchedUpdates,造成的後果,就是由React控制的事件處理過程setState不會同步更新this.state。
3、下面輸出什麼
class Example extends React.Component { constructor() { super(); this.state = { val: 0 }; } componentDidMount() { this.setState({val: this.state.val + 1}); console.log(this.state.val); // 第 1 次 log this.setState({val: this.state.val + 1}); console.log(this.state.val); // 第 2 次 log setTimeout(() => { this.setState({val: this.state.val + 1}); console.log(this.state.val); // 第 3 次 log this.setState({val: this.state.val + 1}); console.log(this.state.val); // 第 4 次 log }, 0); } render() { return null; } };
1、第一次和第二次都是在 react 自身生命周期内,觸發時 isBatchingUpdates 為 true,是以并不會直接執行更新 state,而是加入了 dirtyComponents,是以列印時擷取的都是更新前的狀态 0。 2、兩次 setState 時,擷取到 this.state.val 都是 0,是以執行時都是将 0 設定成 1,在 react 内部會被合并掉,隻執行一次。設定完成後 state.val 值為 1。 3、setTimeout 中的代碼,觸發時 isBatchingUpdates 為 false,是以能夠直接進行更新,是以連着輸出 2,3。 輸出: 0 0 2 3
4、為什麼虛拟dom會提高性能?
參考答案
虛拟dom相當于在js和真實dom中間加了一個緩存,利用dom diff算法避免了沒有必要的dom操作,進而提高性能。
具體實作步驟如下:
用 JavaScript 對象結構表示 DOM 樹的結構;然後用這個樹建構一個真正的 DOM 樹,插到文檔當中
當狀态變更的時候,重新構造一棵新的對象樹。然後用新的樹和舊的樹進行比較,記錄兩棵樹差異
把2所記錄的差異應用到步驟1所建構的真正的DOM樹上,視圖就更新了。
css
1、分析比較 opacity: 0、visibility: hidden、display: none 優劣和适用場景
參考答案
結構:
display:none: 會讓元素完全從渲染樹中消失,渲染的時候不占據任何空間, 不能點選,
visibility: hidden:不會讓元素從渲染樹消失,渲染元素繼續占據空間,隻是内容不可見,不能點選
opacity: 0: 不會讓元素從渲染樹消失,渲染元素繼續占據空間,隻是内容不可見,可以點選
繼承:
display: none和opacity: 0:是非繼承屬性,子孫節點消失由于元素從渲染樹消失造成,通過修改子孫節點屬性無法顯示。
visibility: hidden:是繼承屬性,子孫節點消失由于繼承了hidden,通過設定visibility: visible;可以讓子孫節點顯式。
性能:
displaynone : 修改元素會造成文檔回流,讀屏器不會讀取display: none元素内容,性能消耗較大
visibility:hidden: 修改元素隻會造成本元素的重繪,性能消耗較少讀屏器讀取visibility: hidden元素内容
opacity: 0 :修改元素會造成重繪,性能消耗較少
聯系:它們都能讓元素不可見
2、清除浮動的方式有哪些?比較好的是哪一種?
常用的一般為三種參考答案
,.clearfix
,clear:both
overflow:hidden
;
比較好是
,僞元素萬金油版本,後兩者有局限性..clearfix
.clearfix:after { visibility: hidden; display: block; font-size: 0; content: " "; clear: both; height: 0; } <!-- 為毛沒有 zoom ,_height 這些,IE6,7這類需要 csshack 不再我們考慮之内了 .clearfix 還有另外一種寫法, --> .clearfix:before, .clearfix:after { content:""; display:table; } .clearfix:after{ clear:both; overflow:hidden; } .clearfix{ zoom:1; } <!-- 用display:table 是為了避免外邊距margin重疊導緻的margin塌陷, 内部元素預設會成為 table-cell 單元格的形式 -->
:若是用在同一個容器内相鄰元素上,那是賊好的,有時候在容器外就有些問題了, 比如相鄰容器的包裹層元素塌陷clear:both
:這種若是用在同個容器内,可以形成overflow:hidden
避免浮動造成的元素塌陷BFC
4、css sprite 是什麼,有什麼優缺點
參考答案
概念:将多個小圖檔拼接到一個圖檔中。通過 background-position 和元素尺寸調節需要顯示的背景圖案。
優點:
- 減少 HTTP 請求數,極大地提高頁面加載速度
- 增加圖檔資訊重複度,提高壓縮比,減少圖檔大小
- 更換風格友善,隻需在一張或幾張圖檔上修改顔色或樣式即可實作
- 圖檔合并麻煩
- 維護麻煩,修改一個圖檔可能需要重新布局整個圖檔,樣式
5、
與link
的差別@import
參考答案
-
是 HTML 方式,link
是 CSS 方式@import
-
最大限度支援并行下載下傳,link
過多嵌套導緻串行下載下傳,出現FOUC@import
-
可以通過link
指定候選樣式rel="alternate stylesheet"
- 浏覽器對
支援早于link
,可以使用@import
對老浏覽器隐藏樣式@import
-
必須在樣式規則之前,可以在 css 檔案中引用其他檔案@import
- 總體來說:link 優于@import
6、
和display: block;
的差別display: inline;
參考答案
block
元素特點:
1.處于正常流中時,如果
沒有設定,會自動填充滿父容器 2.可以應用width
3.在沒有設定高度的情況下會擴充高度以包含正常流中的子元素 4.處于正常流中時布局時在前後元素位置之間(獨占一個水準空間) 5.忽略margin/padding
vertical-align
inline
元素特點
1.水準方向上根據
direction
依次布局
2.不會在元素前後進行換行
3.受
white-space
控制
4.
margin/padding
在豎直方向上無效,水準方向上有效
5.
width/height
屬性對非替換行内元素無效,寬度由元素内容決定
6.非替換行内元素的行框高由
确定,替換行内元素的行框高由line-height
,height
,margin
,padding
border
決定
7.浮動或絕對定位時會轉換為
8.block
屬性生效vertical-align
7、容器包含若幹浮動元素時如何清理浮動
參考答案
-
容器元素閉合标簽前添加額外元素并設定`clear: both`
-
父元素觸發塊級格式化上下文\(見塊級可視化上下文部分\)
-
設定容器元素僞元素進行清理
/** * 在标準浏覽器下使用 * 1 content内容為空格用于修複opera下文檔中出現 * contenteditable屬性時在清理浮動元素上下的空白 * 2 使用display使用table而不是block:可以防止容器和 * 子元素top-margin折疊,這樣能使清理效果與BFC,IE6/7 * zoom: 1;一緻 **/ .clearfix:before, .clearfix:after { content: " "; /* 1 */ display: table; /* 2 */ } .clearfix:after { clear: both; } /** * IE 6/7下使用 * 通過觸發hasLayout實作包含浮動 **/ .clearfix { *zoom: 1; }
8、PNG,GIF,JPG 的差別及如何選
GIF:參考答案
- 8 位像素,256 色
- 無損壓縮
- 支援簡單動畫
- 支援 boolean 透明
- 适合簡單動畫
- 顔色限于 256
- 有損壓縮
- 可控制壓縮品質
- 不支援透明
- 适合照片
- 有 PNG8 和 truecolor PNG
- PNG8 類似 GIF 顔色上限為 256,檔案小,支援 alpha 透明度,無動畫
- 适合圖示、背景、按鈕
9、display,float,position 的關系
參考答案
- 如果
為 none,那麼 position 和 float 都不起作用,這種情況下元素不産生框display
- 否則,如果 position 值為 absolute 或者 fixed,框就是絕對定位的,float 的計算值為 none,display 根據下面的表格進行調整。
- 否則,如果 float 不是 none,框是浮動的,display 根據下表進行調整
- 否則,如果元素是根元素,display 根據下表進行調整
- 其他情況下 display 的值為指定值 總結起來:絕對定位、浮動、根元素都需要調整display
10、如何水準居中一個元素
參考答案
- 如果需要居中的元素為正常流中 inline 元素,為父元素設定
即可實作text-align: center;
- 如果需要居中的元素為正常流中 block 元素,1)為元素設定寬度,2)設定左右 margin 為 auto。3)IE6 下需在父元素上設定
,再給子元素恢複需要的值text-align: center;
-
<body> <div class="content"> aaaaaa aaaaaa a a a a a a a a </div> </body> <style> body { background: #DDD; text-align: center; /* 3 */ } .content { width: 500px; /* 1 */ text-align: left; /* 3 */ margin: 0 auto; /* 2 */ background: purple; } </style>
- 如果需要居中的元素為浮動元素,1)為元素設定寬度,2)
,3)浮動方向偏移量(left 或者 right)設定為 50%,4)浮動方向上的 margin 設定為元素寬度一半乘以-1position: relative;
<body> <div class="content"> aaaaaa aaaaaa a a a a a a a a </div> </body> <style> body { background: #DDD; } .content { width: 500px; /* 1 */ float: left; position: relative; /* 2 */ left: 50%; /* 3 */ margin-left: -250px; /* 4 */ background-color: purple; } </style>
- 如果需要居中的元素為絕對定位元素,1)為元素設定寬度,2)偏移量設定為 50%,3)偏移方向外邊距設定為元素寬度一半乘以-1
<body> <div class="content"> aaaaaa aaaaaa a a a a a a a a </div> </body> <style> body { background: #DDD; position: relative; } .content { width: 800px; position: absolute; left: 50%; margin-left: -400px; background-color: purple; } </style>
- 如果需要居中的元素為絕對定位元素,1)為元素設定寬度,2)設定左右偏移量都為 0,3)設定左右外邊距都為 auto
<body> <div class="content"> aaaaaa aaaaaa a a a a a a a a </div> </body> <style> body { background: #DDD; position: relative; } .content { width: 800px; position: absolute; margin: 0 auto; left: 0; right: 0; background-color: purple; } </style>
JavaScript
1、JS有幾種資料類型,其中基本資料類型有哪些?
七種資料類型參考答案
- Boolean
- Null
- Undefined
- Number
- String
- Symbol (ECMAScript 6 新定義)
- Object
,string
,number
,boolean
,null
undefined
,
ES6出來的
也是原始資料類型 ,表示獨一無二的值Symbol
為引用類型(範圍挺大),也包括數組、函數,Object
2、Promise 構造函數是同步執行還是異步執行,那麼 then 方法呢?
參考答案
輸出結果是:const promise = new Promise((resolve, reject) => { console.log(1) resolve() console.log(2) }) promise.then(() => { console.log(3) }) console.log(4)
1 2 4 3 promise構造函數是同步執行的,then方法是異步執行的 Promise new的時候會立即執行裡面的代碼 then是微任務 會在本次任務執行完的時候執行 setTimeout是宏任務 會在下次任務執行的時候執行
3、JS的四種設計模式
參考答案
工廠模式
簡單的工廠模式可以了解為解決多個相似的問題;
function CreatePerson(name,age,sex) { var obj = new Object(); obj.name = name; obj.age = age; obj.sex = sex; obj.sayName = function(){ return this.name; } return obj; } var p1 = new CreatePerson("longen",'28','男'); var p2 = new CreatePerson("tugenhua",'27','女'); console.log(p1.name); // longen console.log(p1.age); // 28 console.log(p1.sex); // 男 console.log(p1.sayName()); // longen console.log(p2.name); // tugenhua console.log(p2.age); // 27 console.log(p2.sex); // 女 console.log(p2.sayName()); // tugenhua
單例模式
隻能被執行個體化(構造函數給執行個體添加屬性與方法)一次
// 單體模式 var Singleton = function(name){ this.name = name; }; Singleton.prototype.getName = function(){ return this.name; } // 擷取執行個體對象 var getInstance = (function() { var instance = null; return function(name) { if(!instance) {//相當于一個一次性閥門,隻能執行個體化一次 instance = new Singleton(name); } return instance; } })(); // 測試單體模式的執行個體,是以a===b var a = getInstance("aa"); var b = getInstance("bb");
沙箱模式
将一些函數放到自執行函數裡面,但要用閉包暴露接口,用變量接收暴露的接口,再調用裡面的值,否則無法使用裡面的值
let sandboxModel=(function(){ function sayName(){}; function sayAge(){}; return{ sayName:sayName, sayAge:sayAge } })()
釋出者訂閱模式
就例如如我們關注了某一個公衆号,然後他對應的有新的消息就會給你推送,
代碼實作邏輯是用數組存貯訂閱者, 釋出者回調函數裡面通知的方式是周遊訂閱者數組,并将釋出者内容傳入訂閱者數組//釋出者與訂閱模式 var shoeObj = {}; // 定義釋出者 shoeObj.list = []; // 緩存清單 存放訂閱者回調函數 // 增加訂閱者 shoeObj.listen = function(fn) { shoeObj.list.push(fn); // 訂閱消息添加到緩存清單 } // 釋出消息 shoeObj.trigger = function() { for (var i = 0, fn; fn = this.list[i++];) { fn.apply(this, arguments);//第一個參數隻是改變fn的this, } } // 小紅訂閱如下消息 shoeObj.listen(function(color, size) { console.log("顔色是:" + color); console.log("尺碼是:" + size); }); // 小花訂閱如下消息 shoeObj.listen(function(color, size) { console.log("再次列印顔色是:" + color); console.log("再次列印尺碼是:" + size); }); shoeObj.trigger("紅色", 40); shoeObj.trigger("黑色", 42);
4、列舉出集中建立執行個體的方法
1.字面量參考答案
2.Object構造函數建立let obj={'name':'張三'}
3.使用工廠模式建立對象let Obj=new Object() Obj.name='張三'
4.使用構造函數建立對象function createPerson(name){ var o = new Object(); o.name = name; }; return o; } var person1 = createPerson('張三');
function Person(name){ this.name = name; } var person1 = new Person('張三');
5、簡述一下前端事件流
參考答案
HTML中與javascript互動是通過事件驅動來實作的,例如滑鼠點選事件onclick、頁面的滾動事件onscroll等等,可以向文檔或者文檔中的元素添加事件偵聽器來預訂事件。想要知道這些事件是在什麼時候進行調用的,就需要了解一下“事件流”的概念。
什麼是事件流:事件流描述的是從頁面中接收事件的順序,DOM2級事件流包括下面幾個階段。
- 事件捕獲階段
- 處于目标階段
- 事件冒泡階段
addEventListener:addEventListener是DOM2 級事件新增的指定事件處理程式的操作,這個方法接收3個參數:要處理的事件名、作為事件處理程式的函數和一個布爾值。最後這個布爾值參數如果是true,表示在捕獲階段調用事件處理程式;如果是false,表示在冒泡階段調用事件處理程式。
IE隻支援事件冒泡。
6、
Function._proto_(getPrototypeOf)是什麼?
參考答案
擷取一個對象的原型,在chrome中可以通過__proto__的形式,或者在ES6中可以通過Object.getPrototypeOf的形式。
那麼Function.proto是什麼麼?也就是說Function由什麼對象繼承而來,我們來做如下判别。
Function.__proto__==Object.prototype //false Function.__proto__==Function.prototype//true
我們發現Function的原型也是Function。
我們用圖可以來明确這個關系:
image-201909142352108877、簡述一下原型 / 構造函數 / 執行個體
參考答案
- 原型
: 一個簡單的對象,用于實作對象的 屬性繼承。可以簡單的了解成對象的爹。在 Firefox 和 Chrome 中,每個(prototype)
對象中都包含一個JavaScript
(非标準)的屬性指向它爹(該對象的原型),可__proto__
進行通路。obj.__proto__
- 構造函數: 可以通過
來 建立一個對象的函數。new
- 執行個體: 通過構造函數和
建立出來的對象,便是執行個體。執行個體通過__proto__指向原型,通過constructor指向構造函數。new
為例,我們常用的Object
便是一個構造函數,是以我們可以通過它建構執行個體。Object
則此時, 執行個體為instance, 構造函數為Object,我們知道,構造函數擁有一個// 執行個體 const instance = new Object()
的屬性指向原型,是以原型為:prototype
這裡我們可以來看出三者的關系:// 原型 const prototype = Object.prototype
執行個體.__proto__ === 原型 原型.constructor === 構造函數 構造函數.prototype === 原型 // 這條線其實是是基于原型進行擷取的,可以了解成一條基于原型的映射線 // 例如: // const o = new Object() // o.constructor === Object --> true // o.__proto__ = null; // o.constructor === Object --> false 執行個體.constructor === 構造函數
8、簡述一下JS繼承,并舉例
在 JS 中,繼承通常指的便是 原型鍊繼承,也就是通過指定原型,并可以通過原型鍊繼承原型上的屬性或者方法。參考答案
- 最優化: 聖杯模式
var inherit = (function(c,p){ var F = function(){}; return function(c,p){ F.prototype = p.prototype; c.prototype = new F(); c.uber = p.prototype; c.prototype.constructor = c; } })();
- 使用 ES6 的文法糖
class / extends
9、函數柯裡化
參考答案
在函數式程式設計中,函數是一等公民。那麼函數柯裡化是怎樣的呢?
函數柯裡化指的是将能夠接收多個參數的函數轉化為接收單一參數的函數,并且傳回接收餘下參數且傳回結果的新函數的技術。
函數柯裡化的主要作用和特點就是參數複用、提前傳回和延遲執行。
在一個函數中,首先填充幾個參數,然後再傳回一個新的函數的技術,稱為函數的柯裡化。通常可用于在不侵入函數的前提下,為函數 預置通用參數,供多次重複調用。
const add = function add(x) { return function (y) { return x + y } } const add1 = add(1) add1(2) === 3 add1(20) === 21
10、說說bind、call、apply 差別?
參考答案
和call
都是為了解決改變apply
this
的指向。作用都是相同的,隻是傳參的方式不同。
除了第一個參數外,
可以接收一個參數清單,call
隻接受一個參數數組。apply
let a = { value: 1 } function getValue(name, age) { console.log(name) console.log(age) console.log(this.value) } getValue.call(a, 'yck', '24') getValue.apply(a, ['yck', '24'])
和其他兩個方法作用也是一緻的,隻是該方法會傳回一個函數。并且我們可以通過bind
實作柯裡化。bind
(下面是對這三個方法的擴充介紹)
如何實作一個 bind 函數
對于實作以下幾個函數,可以從幾個方面思考
- 不傳入第一個參數,那麼預設為
window
- 改變了 this 指向,讓新的對象可以執行該函數。那麼思路是否可以變成給新的對象添加一個函數,然後在執行完以後删除?
如何實作一個call函數Function.prototype.myBind = function (context) { if (typeof this !== 'function') { throw new TypeError('Error') } var _this = this var args = [...arguments].slice(1) // 傳回一個函數 return function F() { // 因為傳回了一個函數,我們可以 new F(),是以需要判斷 if (this instanceof F) { return new _this(...args, ...arguments) } return _this.apply(context, args.concat(...arguments)) } }
如何實作一個apply函數Function.prototype.myCall = function (context) { var context = context || window // 給 context 添加一個屬性 // getValue.call(a, 'yck', '24') => a.fn = getValue context.fn = this // 将 context 後面的參數取出來 var args = [...arguments].slice(1) // getValue.call(a, 'yck', '24') => a.fn('yck', '24') var result = context.fn(...args) // 删除 fn delete context.fn return result }
Function.prototype.myApply = function (context) { var context = context || window context.fn = this var result // 需要判斷是否存儲第二個參數 // 如果存在,就将第二個參數展開 if (arguments[1]) { result = context.fn(...arguments[1]) } else { result = context.fn() } delete context.fn return result }
11、箭頭函數的特點
參考答案
箭頭函數其實是沒有function a() { return () => { return () => { console.log(this) } } } console.log(a()()())
的,這個函數中的this
隻取決于他外面的第一個不是箭頭函數的函數的this
。在這個例子中,因為調用this
符合前面代碼中的第一個情況,是以a
是this
。并且window
一旦綁定了上下文,就不會被任何代碼改變。this
程式閱讀題
1、下面程式輸出的結果是什麼?
function sayHi() { console.log(name); console.log(age); var name = "Lydia"; let age = 21; } sayHi();
- A:
和Lydia
undefined
- B:
和Lydia
ReferenceError
- C:
和ReferenceError
21
- D:
和undefined
ReferenceError
在函數中,我們首先使用參考答案
關鍵字聲明了var
變量。這意味着變量在建立階段會被提升(name
會在建立變量建立階段為其配置設定記憶體空間),預設值為JavaScript
,直到我們實際執行到使用該變量的行。我們還沒有為undefined
變量指派,是以它仍然保持name
undefined
的值。
使用
關鍵字(和let
)聲明的變量也會存在變量提升,但與const
不同,初始化沒有被提升。在我們聲明(初始化)它們之前,它們是不可通路的。這被稱為“暫時死區”。當我們在聲明變量之前嘗試通路變量時,var
會抛出一個JavaScript
ReferenceError
。
關于
的是否存在變量提升,我們何以用下面的例子來驗證:let
let name = 'ConardLi' { console.log(name) // Uncaught ReferenceError: name is not defined let name = 'code秘密花園' }
變量如果不存在變量提升,let
就會輸出console.log(name)
,結果卻抛出了ConardLi
,那麼這很好的說明了,ReferenceError
let
也存在變量提升,但是它存在一個“暫時死區”,在變量未初始化或指派前不允許通路。
變量的指派可以分為三個階段:
- 建立變量,在記憶體中開辟空間
- 初始化變量,将變量初始化為
undefined
- 真正指派
、let
和var
:function
-
的「建立」過程被提升了,但是初始化沒有提升。let
-
的「建立」和「初始化」都被提升了。var
-
的「建立」「初始化」和「指派」都被提升了。function
2、下面代碼輸出什麼
依次輸出:undefined -> 10 -> 20var a = 10; (function () { console.log(a) a = 5 console.log(window.a) var a = 20; console.log(a) })()
在立即執行函數中,var a = 20; 語句定義了一個局部變量 a,由于js的變量聲明提升機制,局部變量a的聲明會被提升至立即執行函數的函數體最上方,且由于這樣的提升并不包括指派,是以第一條列印語句會列印undefined,最後一條語句會列印20。 由于變量聲明提升,a = 5; 這條語句執行時,局部的變量a已經聲明,是以它産生的效果是對局部的變量a指派,此時window.a 依舊是最開始指派的10,
3、下面的輸出結果是什麼?
class Chameleon { static colorChange(newColor) { this.newColor = newColor; } constructor({ newColor = "green" } = {}) { this.newColor = newColor; } } const freddie = new Chameleon({ newColor: "purple" }); freddie.colorChange("orange");
- A:
orange
- B:
purple
- C:
green
- D:
TypeError
方法是靜态的。靜态方法僅在建立它們的構造函數中存在,并且不能傳遞給任何子級。由于colorChange
是一個子級對象,函數不會傳遞,是以在freddie
執行個體上不存在freddie
方法:抛出freddie
。TypeError
4、下面代碼中什麼時候會輸出1?
var a = ?; if(a == 1 && a == 2 && a == 3){ conso.log(1); }
參考答案
因為==會進行隐式類型轉換 是以我們重寫toString方法就可以了
5、下面的輸出結果是什麼?var a = { i: 1, toString() { return a.i++; } } if( a == 1 && a == 2 && a == 3 ) { console.log(1); }
var obj = { '2': 3, '3': 4, 'length': 2, 'splice': Array.prototype.splice, 'push': Array.prototype.push } obj.push(1) obj.push(2) console.log(obj)
1.使用第一次push,obj對象的push方法設定參考答案
2.使用第二次push,obj對象的push方法設定obj[2]=1;obj.length+=1
obj[3]=2;obj.length+=1
3.使用console.log輸出的時候,因為obj具有 length 屬性和 splice 方法,故将其作為數組進行列印
4.列印時因為數組未設定下标為 0 1 處的值,故列印為empty,主動 obj[0] 擷取為 undefined
6、下面代碼輸出的結果是什麼?
var a = {n: 1}; var b = a; a.x = a = {n: 2}; console.log(a.x) console.log(b.x)
參考答案
undefined
{n:2}
首先,a和b同時引用了{n:2}對象,接着執行到a.x = a = {n:2}語句,盡管指派是從右到左的沒錯,但是.的優先級比=要高,是以這裡首先執行a.x,相當于為a(或者b)所指向的{n:1}對象新增了一個屬性x,即此時對象将變為{n:1;x:undefined}。之後按正常情況,從右到左進行指派,此時執行a ={n:2}的時候,a的引用改變,指向了新對象{n:2},而b依然指向的是舊對象。之後執行a.x = {n:2}的時候,并不會重新解析一遍a,而是沿用最初解析a.x時候的a,也即舊對象,故此時舊對象的x的值為{n:2},舊對象為 {n:1;x:{n:2}},它被b引用着。
後面輸出a.x的時候,又要解析a了,此時的a是指向新對象的a,而這個新對象是沒有x屬性的,故通路時輸出undefined;而通路b.x的時候,将輸出舊對象的x的值,即{n:2}。
7、下面代碼的輸出是什麼?
function checkAge(data) { if (data === { age: 18 }) { console.log("You are an adult!"); } else if (data == { age: 18 }) { console.log("You are still an adult."); } else { console.log(`Hmm.. You don't have an age I guess`); } } checkAge({ age: 18 });
參考答案
Hmm.. You don't have an age I guess
在比較相等性,原始類型通過它們的值進行比較,而對象通過它們的引用進行比較。JavaScript
檢查對象是否具有對記憶體中相同位置的引用。
我們作為參數傳遞的對象和我們用于檢查相等性的對象在記憶體中位于不同位置,是以它們的引用是不同的。
這就是為什麼
和{ age: 18 } === { age: 18 }
傳回{ age: 18 } == { age: 18 }
的原因。false
8、下面代碼的輸出是什麼?
const obj = { 1: "a", 2: "b", 3: "c" }; const set = new Set([1, 2, 3, 4, 5]); obj.hasOwnProperty("1"); obj.hasOwnProperty(1); set.has("1"); set.has(1);
參考答案
true
true
false
true
)都會被存儲為字元串,即使你沒有給定字元串類型的鍵。這就是為什麼Symbols
也傳回obj.hasOwnProperty('1')
true
。
上面的說法不适用于
。在我們的Set
中沒有Set
:“1”
傳回set.has('1')
。它有數字類型false
,1
傳回set.has(1)
。true
9、下面代碼的輸出是什麼?
// example 1 var a={}, b='123', c=123; a[b]='b'; a[c]='c'; console.log(a[b]); --------------------- // example 2 var a={}, b=Symbol('123'), c=Symbol('123'); a[b]='b'; a[c]='c'; console.log(a[b]); --------------------- // example 3 var a={}, b={key:'123'}, c={key:'456'}; a[b]='b'; a[c]='c'; console.log(a[b]);
這題考察的是對象的鍵名的轉換。參考答案
- 對象的鍵名隻能是字元串和 Symbol 類型。
- 其他類型的鍵名會被轉換成字元串類型。
- 對象轉字元串預設會調用 toString 方法。
// example 1 var a={}, b='123', c=123; a[b]='b'; // c 的鍵名會被轉換成字元串'123',這裡會把 b 覆寫掉。 a[c]='c'; // 輸出 c console.log(a[b]); // example 2 var a={}, b=Symbol('123'), c=Symbol('123'); // b 是 Symbol 類型,不需要轉換。 a[b]='b'; // c 是 Symbol 類型,不需要轉換。任何一個 Symbol 類型的值都是不相等的,是以不會覆寫掉 b。 a[c]='c'; // 輸出 b console.log(a[b]); // example 3 var a={}, b={key:'123'}, c={key:'456'}; // b 不是字元串也不是 Symbol 類型,需要轉換成字元串。 // 對象類型會調用 toString 方法轉換成字元串 [object Object]。 a[b]='b'; // c 不是字元串也不是 Symbol 類型,需要轉換成字元串。 // 對象類型會調用 toString 方法轉換成字元串 [object Object]。這裡會把 b 覆寫掉。 a[c]='c'; // 輸出 c console.log(a[b]);
10、下面代碼的輸出是什麼?
(() => { let x, y; try { throw new Error(); } catch (x) { (x = 1), (y = 2); console.log(x); } console.log(x); console.log(y); })();
參考答案
1
undefined
2
塊接收參數catch
。當我們傳遞參數時,這與變量的x
不同。這個變量x
是屬于x
catch
作用域的。
之後,我們将這個塊級作用域的變量設定為
,并設定變量1
的值。現在,我們列印塊級作用域的變量y
,它等于x
1
。
在
塊之外,catch
仍然是x
,而undefined
是y
。當我們想在2
塊之外的catch
時,它傳回console.log(x)
,而undefined
傳回y
。2
11、下面代碼的輸出結果是什麼?
function Foo() { Foo.a = function() { console.log(1) } this.a = function() { console.log(2) } } Foo.prototype.a = function() { console.log(3) } Foo.a = function() { console.log(4) } Foo.a(); let obj = new Foo(); obj.a(); Foo.a();
參考答案
輸出順序是 4 2 1
function Foo() { Foo.a = function() { console.log(1) } this.a = function() { console.log(2) } } // 以上隻是 Foo 的建構方法,沒有産生執行個體,此刻也沒有執行 Foo.prototype.a = function() { console.log(3) } // 現在在 Foo 上挂載了原型方法 a ,方法輸出值為 3 Foo.a = function() { console.log(4) } // 現在在 Foo 上挂載了直接方法 a ,輸出值為 4 Foo.a(); // 立刻執行了 Foo 上的 a 方法,也就是剛剛定義的,是以 // # 輸出 4
來源:煙雨平生V
https://blog.csdn.net/sinat_37903468/article/details/100887223
最後
歡迎關注【前端瓶子君】✿✿ヽ(°▽°)ノ✿
回複「算法」,加入前端程式設計源碼算法群,每日一道面試題(工作日),第二天瓶子君都會很認真的解答喲!
回複「交流」,吹吹水、聊聊技術、吐吐槽!
回複「閱讀」,每日刷刷高品質好文!
如果這篇文章對你有幫助,「在看」是最大的支援
》》面試官也在看的算法資料《《
“在看和轉發”就是最大的支援