天天看點

前端經典面試題 ( 60道前端面試題包含 JS、CSS、React、網絡、浏覽器、程式題等)...

(以下所有答案僅供參考)

簡答題

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 為例):

  1. 執行工程自身 preinstall

    目前 npm 工程如果定義了 preinstall 鈎子此時會被執行。

  2. 确定首層依賴子產品

    首先需要做的是确定工程中的首層依賴,也就是 dependencies 和 devDependencies 屬性中直接指定的子產品(假設此時沒有添加 npm install 參數)。

    工程本身是整棵依賴樹的根節點,每個首層依賴子產品都是根節點下面的一棵子樹,npm 會開啟多程序從每個首層依賴子產品開始逐漸尋找更深層級的節點。

  3. 擷取子產品

    擷取子產品是一個遞歸的過程,分為以下幾步:

  • 擷取子產品資訊。在下載下傳一個子產品之前,首先要确定其版本,這是因為 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:

    var new_array = arr.map(function callback(currentValue[, index[, array]]) { // Return element for new_array }[, thisArg])

    這個callback一共可以接收三個參數,其中第一個參數代表目前被處理的元素,而第二個參數代表該元素的索引。
    • 而parseInt則是用來解析字元串的,使字元串成為指定基數的整數。

      parseInt(string, radix)

      接收兩個參數,第一個表示被處理的值(字元串),第二個表示為解析時的基數。
    • 了解這兩個函數後,我們可以模拟一下運作情況
    1. parseInt('1', 0) //radix為0時,且string參數不以“0x”和“0”開頭時,按照10為基數處理。這個時候傳回1
    2. parseInt('2', 1) //基數為1(1進制)表示的數中,最大值小于2,是以無法解析,傳回NaN
    3. 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對象
    • history.go() -- 前進或後退指定的頁面數
    • history.go(num); history.back() -- 後退一頁
    • history.forward() -- 前進一頁
    Navigator對象
    • 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:同源視窗都會共享,并且不會失效,不管視窗或者浏覽器關閉與否都會始終生效)
    補充說明一下cookie的作用:
    • 儲存使用者登入狀态。例如将使用者id存儲于一個cookie内,這樣當使用者下次通路該頁面時就不需要重新登入了,現在很多論壇和社群都提供這樣的功能。cookie還可以設定過期時間,當超過時間期限後,cookie就會自動消失。是以,系統往往可以提示使用者保持登入狀态的時間:常見選項有一個月、三個 月、一年等。
    • 跟蹤使用者行為。例如一個天氣預報網站,能夠根據使用者選擇的地區顯示當地的天氣情況。如果每次都需要選擇所在地是煩瑣的,當利用了 cookie後就會顯得很人性化了,系統能夠記住上一次通路的地區,當下次再打開該頁面時,它就會自動顯示上次使用者所在地區的天氣情況。因為一切都是在後 台完成,是以這樣的頁面就像為某個使用者所定制的一樣,使用起來非常友善
    • 定制頁面。如果網站提供了換膚或更換布局的功能,那麼可以使用cookie來記錄使用者的選項,例如:背景色、分辨率等。當使用者下次通路時,仍然可以儲存上一次通路的界面風格。

    7、Cookie如何防範XSS攻擊

    參考答案

    XSS(跨站腳本攻擊)是指攻擊者在傳回的HTML中嵌入javascript腳本,為了減輕這些攻擊,需要在HTTP頭部配上,set-cookie:
    • httponly-這個屬性可以防止XSS,它會禁止javascript腳本來通路cookie。
    • secure - 這個屬性告訴浏覽器僅在請求為https的時候發送cookie。
    結果應該是這樣的:Set-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握手的概述

    中間人攻擊過程如下:

    1. 伺服器向用戶端發送公鑰。
    2. 攻擊者截獲公鑰,保留在自己手上。
    3. 然後攻擊者自己生成一個【僞造的】公鑰,發給用戶端。
    4. 用戶端收到僞造的公鑰後,生成加密hash值發給伺服器。
    5. 攻擊者獲得加密hash值,用自己的私鑰解密獲得真秘鑰。
    6. 同時生成假的加密hash值,發給伺服器。
    7. 伺服器用私鑰解密獲得假秘鑰。
    8. 伺服器用加秘鑰加密傳輸資訊
    防範方法:
    1. 服務端在發送浏覽器的公鑰中加入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 有關。
    1. 當 Event loop 執行完 Microtasks 後,會判斷 document 是否需要更新。因為浏覽器是 60Hz 的重新整理率,每 16ms 才會更新一次。
    2. 然後判斷是否有

      resize

      或者

      scroll

      ,有的話會去觸發事件,是以

      resize

      scroll

      事件也是至少 16ms 才會觸發一次,并且自帶節流功能。
    3. 判斷是否觸發了 media query
    4. 更新動畫并且發送事件
    5. 判斷是否有全屏操作事件
    6. 執行

      requestAnimationFrame

      回調
    7. 執行

      InterpObserver

      回調,該方法用于判斷元素是否可見,可以用于懶加載上,但是相容性不好
    8. 更新界面
    9. 以上就是一幀中可能會做的事情。如果在一幀中有空閑時間,就會去執行

      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部分源碼如下:

    // 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)
    }
               
    建立map函數
    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 和元素尺寸調節需要顯示的背景圖案。

    優點:

    1. 減少 HTTP 請求數,極大地提高頁面加載速度
    2. 增加圖檔資訊重複度,提高壓縮比,減少圖檔大小
    3. 更換風格友善,隻需在一張或幾張圖檔上修改顔色或樣式即可實作
    缺點:
    1. 圖檔合并麻煩
    2. 維護麻煩,修改一個圖檔可能需要重新布局整個圖檔,樣式

    5、

    link

    @import

    的差別

    參考答案

    1. link

      是 HTML 方式,

      @import

      是 CSS 方式
    2. link

      最大限度支援并行下載下傳,

      @import

      過多嵌套導緻串行下載下傳,出現FOUC
    3. link

      可以通過

      rel="alternate stylesheet"

      指定候選樣式
    4. 浏覽器對

      link

      支援早于

      @import

      ,可以使用

      @import

      對老浏覽器隐藏樣式
    5. @import

      必須在樣式規則之前,可以在 css 檔案中引用其他檔案
    6. 總體來說:link 優于@import

    6、

    display: block;

    display: inline;

    的差別

    參考答案

    block

    元素特點:

    1.處于正常流中時,如果

    width

    沒有設定,會自動填充滿父容器 2.可以應用

    margin/padding

    3.在沒有設定高度的情況下會擴充高度以包含正常流中的子元素 4.處于正常流中時布局時在前後元素位置之間(獨占一個水準空間) 5.忽略

    vertical-align

    inline

    元素特點

    1.水準方向上根據

    direction

    依次布局

    2.不會在元素前後進行換行

    3.受

    white-space

    控制

    4.

    margin/padding

    在豎直方向上無效,水準方向上有效

    5.

    width/height

    屬性對非替換行内元素無效,寬度由元素内容決定

    6.非替換行内元素的行框高由

    line-height

    确定,替換行内元素的行框高由

    height

    ,

    margin

    ,

    padding

    ,

    border

    決定

    7.浮動或絕對定位時會轉換為

    block

    8.

    vertical-align

    屬性生效

    7、容器包含若幹浮動元素時如何清理浮動

    參考答案

    1. 容器元素閉合标簽前添加額外元素并設定`clear: both`
                 
    2. 父元素觸發塊級格式化上下文\(見塊級可視化上下文部分\)
                 
    3. 設定容器元素僞元素進行清理
                 
    /**
    * 在标準浏覽器下使用
    * 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:
    1. 8 位像素,256 色
    2. 無損壓縮
    3. 支援簡單動畫
    4. 支援 boolean 透明
    5. 适合簡單動畫
    JPEG:
    1. 顔色限于 256
    2. 有損壓縮
    3. 可控制壓縮品質
    4. 不支援透明
    5. 适合照片
    PNG:
    1. 有 PNG8 和 truecolor PNG
    2. PNG8 類似 GIF 顔色上限為 256,檔案小,支援 alpha 透明度,無動畫
    3. 适合圖示、背景、按鈕

    9、display,float,position 的關系

    參考答案

    1. 如果

      display

      為 none,那麼 position 和 float 都不起作用,這種情況下元素不産生框
    2. 否則,如果 position 值為 absolute 或者 fixed,框就是絕對定位的,float 的計算值為 none,display 根據下面的表格進行調整。
    3. 否則,如果 float 不是 none,框是浮動的,display 根據下表進行調整
    4. 否則,如果元素是根元素,display 根據下表進行調整
    5. 其他情況下 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)

      position: relative;

      ,3)浮動方向偏移量(left 或者 right)設定為 50%,4)浮動方向上的 margin 設定為元素寬度一半乘以-1
      <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
    (ES6之前)其中5種為基本類型:

    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.字面量
    let obj={'name':'張三'}
               
    2.Object構造函數建立
    let Obj=new Object()
    Obj.name='張三'
               
    3.使用工廠模式建立對象
    function createPerson(name){
     var o = new Object();
     o.name = name;
     };
     return o; 
    }
    var person1 = createPerson('張三');
               
    4.使用構造函數建立對象
    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。

    我們用圖可以來明确這個關系:

    前端經典面試題 ( 60道前端面試題包含 JS、CSS、React、網絡、浏覽器、程式題等)...
    image-20190914235210887

    7、簡述一下原型 / 構造函數 / 執行個體

    參考答案

    • 原型

      (prototype)

      : 一個簡單的對象,用于實作對象的 屬性繼承。可以簡單的了解成對象的爹。在 Firefox 和 Chrome 中,每個

      JavaScript

      對象中都包含一個

      __proto__

      (非标準)的屬性指向它爹(該對象的原型),可

      obj.__proto__

      進行通路。
    • 構造函數: 可以通過

      new

      來 建立一個對象的函數。
    • 執行個體: 通過構造函數和

      new

      建立出來的對象,便是執行個體。執行個體通過__proto__指向原型,通過constructor指向構造函數。
    這裡來舉個栗子,以

    Object

    為例,我們常用的

    Object

    便是一個構造函數,是以我們可以通過它建構執行個體。
    // 執行個體
    const instance = new Object()
               
    則此時, 執行個體為instance, 構造函數為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 指向,讓新的對象可以執行該函數。那麼思路是否可以變成給新的對象添加一個函數,然後在執行完以後删除?
    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))
      }
    }
               
    如何實作一個call函數
    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
    }
               
    如何實作一個apply函數
    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、下面代碼輸出什麼

    var a = 10;
    (function () {
        console.log(a)
        a = 5
        console.log(window.a)
        var a = 20;
        console.log(a)
    })()
               
    依次輸出:undefined -> 10 -> 20
    在立即執行函數中,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

    答案: D

    colorChange

    方法是靜态的。靜态方法僅在建立它們的構造函數中存在,并且不能傳遞給任何子級。由于

    freddie

    是一個子級對象,函數不會傳遞,是以在

    freddie

    執行個體上不存在

    freddie

    方法:抛出

    TypeError

    4、下面代碼中什麼時候會輸出1?

    var a = ?;
    if(a == 1 && a == 2 && a == 3){
         conso.log(1);
    }
               

    參考答案

    因為==會進行隐式類型轉換 是以我們重寫toString方法就可以了
    var a = {
      i: 1,
      toString() {
        return a.i++;
      }
    }
    
    if( a == 1 && a == 2 && a == 3 ) {
      console.log(1);
    }
               
    5、下面的輸出結果是什麼?
    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方法設定

    obj[2]=1;obj.length+=1

    2.使用第二次push,obj對象的push方法設定

    obj[3]=2;obj.length+=1

    3.使用console.log輸出的時候,因為obj具有 length 屬性和 splice 方法,故将其作為數組進行列印

    4.列印時因為數組未設定下标為 0 1 處的值,故列印為empty,主動 obj[0] 擷取為 undefined

    前端經典面試題 ( 60道前端面試題包含 JS、CSS、React、網絡、浏覽器、程式題等)...

    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

    最後

    歡迎關注【前端瓶子君】✿✿ヽ(°▽°)ノ✿

    回複「算法」,加入前端程式設計源碼算法群,每日一道面試題(工作日),第二天瓶子君都會很認真的解答喲!

    回複「交流」,吹吹水、聊聊技術、吐吐槽!

    回複「閱讀」,每日刷刷高品質好文!

    如果這篇文章對你有幫助,「在看」是最大的支援

     》》面試官也在看的算法資料《《

    “在看和轉發”就是最大的支援

繼續閱讀