前端經典面試題 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 的長度。爲了明確這個概念,我們必須再次強調下面幾點:

補充補充一個 get 和 post 在緩存方面的區別:

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 模塊安裝機制:
  1. npm 實現原理

輸入 npm install 命令並敲下回車後,會經歷如下幾個階段(以 npm 5.5.1 爲例):

  1. 執行工程自身 preinstall

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

  2. 確定首層依賴模塊

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

    工程本身是整棵依賴樹的根節點,每個首層依賴模塊都是根節點下面的一棵子樹,npm 會開啓多進程從每個首層依賴模塊開始逐步尋找更深層級的節點。

  3. 獲取模塊

    獲取模塊是一個遞歸的過程,分爲以下幾步:

  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
    ---- lodash@version1

    -- bar
    ---- lodash@version2

    假設 version1 和 version2 是兼容版本,則經過 dedupe 會成爲下面的形式:

    node_modules
    -- foo

    -- bar

    -- lodash(保留的版本爲兼容版本)

    假設 version1 和 version2 爲非兼容版本,則後面的版本保留在依賴樹中:

    node_modules
    -- foo
    -- lodash@version1

    -- bar
    ---- lodash@version2

  2. 安裝模塊

    這一步將會更新工程中的 node_modules,並執行模塊中的生命週期函數(按照 preinstall、install、postinstall 的順序)。

  3. 執行工程自身生命週期

    當前 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]

var new_array = arr.map(function callback(currentValue[, index[, array]]) { // Return element for new_array }[, thisArg])
這個 callback 一共可以接收三個參數,其中第一個參數代表當前被處理的元素,而第二個參數代表該元素的索引。

  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

9、Doctype 作用? 嚴格模式與混雜模式如何區分?它們有何意義?

參考答案

Doctype 聲明於文檔最前面,告訴瀏覽器以何種方式來渲染頁面,這裏有兩種模式,嚴格模式和混雜模式。

10、fetch 發送 2 次請求的原因

參考答案

fetch 發送 post 請求的時候,總是發送 2 次,第一次狀態碼是 204,第二次才成功?

原因很簡單,因爲你用 fetch 的 post 請求的時候,導致 fetch 第一次發送了一個 Options 請求,詢問服務器是否支持修改的請求頭,如果服務器支持,則在第二次中發送真正的請求。

http、瀏覽器對象

1、HTTPS 握手過程中,客戶端如何驗證證書的合法性

參考答案

說明:各大瀏覽器和操作系統已經維護了所有的權威證書機構的名稱和公鑰。B 只需要知道是哪個權威機構發的證書,使用對應的機構公鑰,就可以解密出證書籤名;接下來,B 使用同樣的規則,生成自己的證書籤名,如果兩個簽名是一致的,說明證書是有效的;
簽名驗證成功後,B 就可以再次利用機構的公鑰,解密出 A 的公鑰 key1; 接下來的操作,就是和之前一樣的流程了;

因爲證書的簽名是由服務器端網址等信息生成的,並且通過第三方機構的私鑰加密中間人無法篡改;所以最關鍵的問題是證書籤名的真僞;

2、TCP 三次握手和四次揮手

參考答案

三次握手之所以是三次是保證 client 和 server 均讓對方知道自己的接收和發送能力沒問題而保證的最小次數。

第一次 client => server 只能 server 判斷出 client 具備發送能力
第二次 server => client client 就可以判斷出 server 具備發送和接受能力。此時 client 還需讓 server 知道自己接收能力沒問題於是就有了第三次
第三次 client => server 雙方均保證了自己的接收和發送能力沒有問題

其中,爲了保證後續的握手是爲了應答上一個握手,每次握手都會帶一個標識 seq,後續的 ACK 都會對這個 seq 進行加一來進行確認。

3、img iframe script 來發送跨域請求有什麼優缺點?

參考答案

優點:跨域完畢之後 DOM 操作和互相之間的 JavaScript 調用都是沒有問題的

缺點:1. 若結果要以 URL 參數傳遞,這就意味着在結果數據量很大的時候需要分割傳遞,巨煩。2. 還有一個是 iframe 本身帶來的,母頁面和 iframe 本身的交互本身就有安全性限制。

優點:可以直接返回 json 格式的數據,方便處理

缺點:只接受 GET 請求方式

優點:可以訪問任何 url,一般用來進行點擊追蹤,做頁面分析常用的方法

缺點:不能訪問響應文本,只能監聽是否響應

4、http 和 https 的區別?

參考答案

http 傳輸的數據都是未加密的,也就是明文的,網景公司設置了 SSL 協議來對 http 協議傳輸的數據進行加密處理,簡單來說 https 協議是由 http 和 ssl 協議構建的可進行加密傳輸和身份認證的網絡協議,比 http 協議的安全性更高。主要的區別如下:

5、什麼是 Bom?有哪些常用的 Bom 屬性?

參考答案

Bom 是瀏覽器對象

location 對象

history 對象

Navigator 對象

6、Cookie、sessionStorage、localStorage 的區別

參考答案

共同點:都是保存在瀏覽器端,並且是同源的

補充說明一下 cookie 的作用:

7、Cookie 如何防範 XSS 攻擊

參考答案

XSS(跨站腳本攻擊)是指攻擊者在返回的 HTML 中嵌入 javascript 腳本,爲了減輕這些攻擊,需要在 HTTP 頭部配上,set-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)

重繪和迴流是渲染步驟中的一小節,但是這兩個步驟對於性能影響很大。

迴流必定會發生重繪,重繪不一定會引發迴流。迴流所需的成本比重繪高的多,改變深層次的節點很可能導致父節點的一系列迴流。

所以以下幾個動作可能會導致性能問題:

很多人不知道的是,重繪和迴流其實和 Event loop 有關。

  1. 當 Event loop 執行完 Microtasks 後,會判斷 document 是否需要更新。因爲瀏覽器是 60Hz 的刷新率,每 16ms 纔會更新一次。

  2. 然後判斷是否有 resize或者 scroll,有的話會去觸發事件,所以 resize和 scroll事件也是至少 16ms 纔會觸發一次,並且自帶節流功能。

  3. 判斷是否觸發了 media query

  4. 更新動畫並且發送事件

  5. 判斷是否有全屏操作事件

  6. 執行 requestAnimationFrame回調

  7. 執行 IntersectionObserver回調,該方法用於判斷元素是否可見,可以用於懶加載上,但是兼容性不好

  8. 更新界面

  9. 以上就是一幀中可能會做的事情。如果在一幀中有空閒時間,就會去執行 requestIdleCallback回調。

減少重繪和迴流

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、清除浮動的方式有哪些? 比較好的是哪一種?

參考答案

常用的一般爲三種.clearfixclear: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屬性對非替換行內元素無效,寬度由元素內容決定

  1. 非替換行內元素的行框高由line-height確定,替換行內元素的行框高由height,margin,padding,border決定
  2. 浮動或絕對定位時會轉換爲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、如何水平居中一個元素

參考答案

JavaScript

1、JS 有幾種數據類型, 其中基本數據類型有哪些?

參考答案

七種數據類型

(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='張三'
  1. 使用工廠模式創建對象
function createPerson(name){
 var o = new Object();
 o.name = name;
 };
 return o; 
}
var person1 = createPerson('張三');
  1. 使用構造函數創建對象
function Person(name){
 this.name = name;
}
var person1 = new Person('張三');

5、簡述一下前端事件流

參考答案

HTML 中與 javascript 交互是通過事件驅動來實現的,例如鼠標點擊事件 onclick、頁面的滾動事件 onscroll 等等,可以向文檔或者文檔中的元素添加事件偵聽器來預訂事件。想要知道這些事件是在什麼時候進行調用的,就需要了解一下 “事件流” 的概念。

什麼是事件流:事件流描述的是從頁面中接收事件的順序, DOM2 級事件流包括下面幾個階段。

addEventListeneraddEventListener 是 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-20190914235210887

7、簡述一下原型 / 構造函數 / 實例

參考答案

這裏來舉個栗子,以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 中,繼承通常指的便是 原型鏈繼承,也就是通過指定原型,並可以通過原型鏈繼承原型上的屬性或者方法。

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 函數

對於實現以下幾個函數,可以從幾個方面思考

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();

參考答案

在函數中,我們首先使用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也存在變量提升,但是它存在一個 “暫時死區”,在變量未初始化或賦值前不允許訪問。

變量的賦值可以分爲三個階段:

關於letvarfunction

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");

答案: 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

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。它有數字類型1set.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]);

參考答案

這題考察的是對象的鍵名的轉換。

// 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,而y2。當我們想在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

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/A2RtdHVhRq3ZTRchktPrrg