資深前端工程師大場面試總結

本文收錄於 GitHub 日問: DailyQuestion,內含大廠內推機會、面經大全及若干面試題,天天學習五分鐘,一年進入大廠中。javascript

01 什麼是防抖和節流,他們的應用場景有哪些

在 Issue 中交流與討論: 01 什麼是防抖和節流,他們的應用場景有哪些

防抖 (debounce)

防抖,顧名思義,防止抖動,以避免把一次事件誤認爲屢次,敲鍵盤就是一個天天都會接觸到的防抖操做。css

想要了解一個概念,必先了解概念所應用的場景。在 JS 這個世界中,有哪些防抖的場景呢html

  1. 登陸、發短信等按鈕避免用戶點擊太快,以至於發送了屢次請求,須要防抖
  2. 調整瀏覽器窗口大小時,resize 次數過於頻繁,形成計算過多,此時須要一次到位,就用到了防抖
  3. 文本編輯器實時保存,當無任何更改操做一秒後進行保存

代碼以下,能夠看出來防抖重在清零 clearTimeout(timer)前端

function debounce (f, wait) {
  let timer
  return (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => {
      f(...args)
    }, wait)
  }
}

節流 (throttle)

節流,顧名思義,控制水的流量。控制事件發生的頻率,如控制爲1s發生一次,甚至1分鐘發生一次。與服務端(server)及網關(gateway)控制的限流 (Rate Limit) 相似。java

  1. scroll 事件,每隔一秒計算一次位置信息等
  2. 瀏覽器播放事件,每一個一秒計算一次進度信息等
  3. input 框實時搜索併發送請求展現下拉列表,每隔一秒發送一次請求 (也可作防抖)

代碼以下,能夠看出來節流重在加鎖 timer=timeoutnode

function throttle (f, wait) {
  let timer
  return (...args) => {
    if (timer) { return }
    timer = setTimeout(() => {
      f(...args)
      timer = null
    }, wait)
  }
}

總結 (簡要答案)

  • 防抖:防止抖動,單位時間內事件觸發會被重置,避免事件被誤傷觸發屢次。代碼實現重在清零 clearTimeout。防抖能夠比做等電梯,只要有一我的進來,就須要再等一下子。業務場景有避免登陸按鈕屢次點擊的重複提交。
  • 節流:控制流量,單位時間內事件只能觸發一次,與服務器端的限流 (Rate Limit) 相似。代碼實現重在開鎖關鎖 timer=timeout; timer=null。節流能夠比做過紅綠燈,每等一個紅燈時間就能夠過一批。

02 在前端開發中,如何獲取瀏覽器的惟一標識

<blockquote> 更多描述: 如何獲取瀏覽器的惟一標識,原理是什麼 </blockquote>linux

在 Issue 中交流與討論: 02 在前端開發中,如何獲取瀏覽器的惟一標識

因爲不一樣的系統顯卡繪製 canvas 時渲染參數、抗鋸齒等算法不一樣,所以繪製成圖片數據的 CRC 校驗也不同。css3

function getCanvasFp () {
  const canvas = document.getElementById('canvas')
  const ctx = canvas.getContext('2d')
  ctx.font = '14px Arial'
  ctx.fillStyle = '#ccc'
  ctx.fillText('hello, shanyue', 2, 2)
  return canvas.toDataURL('image/jpeg')
}

所以根據 canvas 能夠獲取瀏覽器指紋信息。nginx

  1. 繪製 canvas,獲取 base64 的 dataurl
  2. 對 dataurl 這個字符串進行 md5 摘要計算,獲得指紋信息

可是對於常見的需求就有成熟的解決方案,若在生產環境使用,可使用如下庫git

它依據如下信息,獲取到瀏覽器指紋信息,而這些信息,則成爲 component

  1. canvas
  2. webgl
  3. UserAgent
  4. AudioContext
  5. 對新式 API 的支持程度等
requestIdleCallback(function () {
  Fingerprint2.get((components) => {
    const values = components.map((component) => component.value)
    const fp = Fingerprint2.x64hash128(values.join(''), 31)
  })
})

fingerprintjs2 中,對於 component 也有分類

  • browser independent component:有些 component 同一設備跨瀏覽器也能夠獲得相同的值,有些獨立瀏覽器,獲得不一樣的值
  • stable component: 有些 component 刷新後值就會發生變化,稱爲不穩定組件

在實際業務中,可根據業務選擇合適的組件

const options = {
  excludes: {userAgent: true, language: true}
}

簡答

根據 canvas 能夠獲取瀏覽器指紋信息

  1. 繪製 canvas,獲取 base64 的 dataurl
  2. 對 dataurl 這個字符串進行 md5 摘要計算,獲得指紋信息

若在生產環境使用,可使用 fingerprintjs2,根據業務需求,如單設備是否可跨瀏覽器,以此選擇合適的 component

03 在服務端應用中如何得到客戶端 IP

在 Issue 中交流與討論: 03 在服務端應用中如何得到客戶端 IP

若是有 x-forwarded-for 的請求頭,則取其中的第一個 IP,不然取創建鏈接 socket 的 remoteAddr。

x-forwarded-for 基本已成爲了基於 proxy 的標準HTTP頭,格式以下,可見第一個 IP 表明其真實的 IP,能夠參考 MDN X-Forwarded-For

X-Forwarded-For: 203.0.113.195, 70.41.3.18, 150.172.238.178
X-Forwarded-For: <client>, <proxy1>, <proxy2>

如下是 koa 獲取 IP 的方法

get ips() {
    const proxy = this.app.proxy;
    const val = this.get(this.app.proxyIpHeader);
    let ips = proxy && val
      ? val.split(/\s*,\s*/)
      : [];
    if (this.app.maxIpsCount > 0) {
      ips = ips.slice(-this.app.maxIpsCount);
    }
    return ips;
  },

  get ip() {
    if (!this[IP]) {
      this[IP] = this.ips[0] || this.socket.remoteAddress || '';
    }
    return this[IP];
  },

參見源碼: https://github.com/koajs/koa/...

04 js 如何所有替代一個子串爲另外一個子串

<blockquote> 更多描述: 假設有一個字符串 hello. hello. hello. 須要替換爲 AAA,即把 hello. 替換爲 A </blockquote>

在 Issue 中交流與討論: 04 js 如何所有替代一個子串爲另外一個子串

若是須要全量替換字符串,可使用 String.prototype.replace(re, replacer),其中正則表達式須要開啓 global flag

const s = 'foo foo foo'
s.replce(/foo/g, 'bar')

那如題中,是否可使用正則表達式來替代子串

答:不能夠,由於使用子串構建正則時,有可能有特殊字符,就有可能出現問題,以下

// 期待結果: 'AhelloX hello3 '
> 'hello. helloX hello3 '.replace(new RegExp('hello. ', 'g'), 'A')
< "AAA"

而在 javascript 中替換子串只能使用一種巧妙的辦法:str.split('foo').join('bar')

> 'hello. hello. hello. '.split('hello. ').join('A')
< "AAA"

真是一個巧(笨)妙(拙)的辦法啊!!!!!大概 TC39 也意識到了一個問題,因而出了一個新的 API,在 ESNext

String.prototype.replaceAll()

'aabbcc'.replaceAll('b', '.'); 
// 'aa..cc'

詳細文檔在 String.prototype.replaceAll

總結(及直接答案)

兩種辦法

  • str.split('foo').join('bar')
  • str.replaceAll('foo', 'bar'),在 ESNext 中,目前支持性很差

05 如何獲取一個進程的內存並監控

<blockquote> 更多描述: 在編寫腳本時,有時會出現內存過大發生 OOM 的事情,那咱們如何得知某個進程的內存?另外又如何監控它 </blockquote>

在 Issue 中交流與討論: 05 如何獲取一個進程的內存並監控

經過 ps 能夠獲知一個進程所佔用的內存

$ ps -O rss -p 3506
  PID   RSS S TTY          TIME COMMAND
 3506  6984 S pts/1    00:00:00 vim

若是要監控內存,確定使用對進程萬能的命令 pidstat (PS: 這名字一聽就知道是幹嗎的)

## -r 顯示內存信息
## -p 指定 pid
## 1: 每一個一秒打印一次
$ pidstat -r -p 3506 1
Linux 3.10.0-957.21.3.el7.x86_64 (shanyue)      11/04/19        _x86_64_        (2 CPU)

20:47:35      UID       PID  minflt/s  majflt/s     VSZ    RSS   %MEM  Command
20:47:36        0      3506      0.00      0.00  139940   6984   0.18  vim
20:47:37        0      3506      0.00      0.00  139940   6984   0.18  vim
20:47:38        0      3506      0.00      0.00  139940   6984   0.18  vim
20:47:39        0      3506      0.00      0.00  139940   6984   0.18  vim
20:47:40        0      3506      0.00      0.00  139940   6984   0.18  vim
20:47:41        0      3506      0.00      0.00  139940   6984   0.18  vim

pidstat 是屬於 sysstat 下的 linux 性能工具,但在 mac 中,如何定位內存的變化?此時可使用萬能的 top/htop

$ htop -p 31796

image

總結

簡而言之,有如下三個命令

  1. pidstat -r
  2. htop/top -p
  3. ps -O rss -p

關於更多指標的監控能夠參考個人文章: linux 各項監控指標小記

06 CORS 若是須要指定多個域名怎麼辦

在 Issue 中交流與討論: 06 CORS 若是須要指定多個域名怎麼辦

CORS 經過控制 Access-Control-Allow-Origin 控制哪些域名能夠共享資源,取值以下

Access-Control-Allow-Origin: <origin> | *

其中 * 表明全部域名,origin 表明指定特定域名,那如何設置多個域名了?

此時須要經過代碼實現,根據請求頭中的 Origin 來設置響應頭 Access-Control-Allow-Origin,那 Origin 又是什麼東西?

請求頭: Origin

並非全部請求都會自動帶上 Origin,在瀏覽器中帶 Origin 的邏輯以下

  1. 若是存在跨域,則帶上 Origin,值爲當前域名
  2. 若是不存在跨域,則不帶 Origin

邏輯理清楚後,關於服務器中對於 Access-Control-Allow-Origin 設置多域名的邏輯也很清晰了

  1. 若是請求頭不帶有 Origin,證實未跨域,則不做任何處理
  2. 若是請求頭帶有 Origin,證實跨域,根據 Origin 設置相應的 Access-Control-Allow-Origin: <Origin>

使用僞代碼實現以下:

// 獲取 Origin 請求頭
const requestOrigin = ctx.get('Origin');

// 若是沒有,則跳過
if (!requestOrigin) {
  return await next();
}

// 設置響應頭
ctx.set('Access-Control-Allow-Origin', requestOrigin)

Vary: Origin

此時能夠給多個域名控制 CORS,但此時假設有兩個域名訪問 static.shanyue.tech 的跨域資源

  1. foo.shanyue.tech,響應頭中返回 Access-Control-Allow-Origin: foo.shanyue.tech
  2. bar.shanyue.tech,響應頭中返回 Access-Control-Allow-Origin: bar.shanyue.tech

看起來一切正常,但若是中間有緩存怎麼辦?

  1. foo.shanyue.tech,響應頭中返回 Access-Control-Allow-Origin: foo.shanyue.tech,被 CDN 緩存
  2. bar.shanyue.tech,因由緩存,響應頭中返回 Access-Control-Allow-Origin: foo.shanyue.tech,跨域出現問題

此時,Vary: Origin 就上場了,表明爲不一樣的 Origin 緩存不一樣的資源

總結 (簡要答案)

CORS 如何指定多個域名?

根據請求頭中的 Origin 來設置響應頭 Access-Control-Allow-Origin,思路以下

  1. 老是設置 Vary: Origin,避免 CDN 緩存破壞 CORS 配置
  2. 若是請求頭不帶有 Origin,證實未跨域,則不做任何處理
  3. 若是請求頭帶有 Origin,證實瀏覽器訪問跨域,根據 Origin 設置相應的 Access-Control-Allow-Origin: <Origin>

使用僞代碼實現以下

// 獲取 Origin 請求頭
const requestOrigin = ctx.get('Origin');

ctx.set('Vary', 'Origin')

// 若是沒有,則跳過
if (!requestOrigin) {
  return await next();
}

// 設置響應頭
ctx.set('Access-Control-Allow-Origin', requestOrigin)
相關問題: 如何避免 CDN 爲 PC 端緩存移動端頁面

07 既然 cors 配置能夠作跨域控制,那能夠防止 CSRF 攻擊嗎

在 Issue 中交流與討論: 07 既然 cors 配置能夠作跨域控制,那能夠防止 CSRF 攻擊嗎

對 CORS 一點用也沒有

  1. form 提交不經過 CORS 檢測,你能夠在本地進行測試
  2. 即便經過 xhrfetch 進行提交被 CORS 攔住,可是對於簡單請求而言,請求仍被髮送,已形成了攻擊

08 如何避免 CDN 爲 PC 端緩存移動端頁面

在 Issue 中交流與討論: 08 如何避免 CDN 爲 PC 端緩存移動端頁面

若是 PC 端和移動端是一套代碼則不會出現這個問題。這個問題出如今 PC 端和移動端是兩套代碼,卻共用一個域名。

使用 nginx 配置以下,根據 UA 判斷是否移動端,而走不一樣的邏輯 (判斷UA是否移動端容易出問題)

location / {
    // 默認 PC 端
    root /usr/local/website/web;
    
    # 判斷 UA,訪問移動端
    if ( $http_user_agent ~* "(Android|webOS|iPhone|iPad|BlackBerry)" ){ 
        root /usr/local/website/mobile;
    }
 
    index index.html index.htm;
}

解決方案一般使用 Vary 響應頭,來控制 CDN 對不一樣請求頭的緩存。

此處可使用 Vary: User-Agent ,表明若是 User-Agent 不同,則從新發起請求,而非從緩存中讀取頁面

Vary: User-Agent

固然,User-Agent 實在過多,此時緩存失效就會過多。

簡答

使用 Vary: User-Agent,根據 UA 進行緩存。

Vary: User-Agent

但最好不要出現這種狀況,PC 端和移動端若是是兩套代碼,建議用兩個域名,理由以下

  1. nginx 判斷是否移動端容易出錯
  2. 對緩存不友好

09 如何實現表格單雙行條紋樣式

在 Issue 中交流與討論: 09 如何實現表格單雙行條紋樣式

經過 css3 中僞類 :nth-child 來實現。其中 :nth-child(an+b) 匹配下標 { an + b; n = 0, 1, 2, ...} 且結果爲整數的子元素

  • nth-child(2n)/nth-child(even): 雙行樣式
  • nth-child(2n+1)/nth-child(odd): 單行樣式

其中 tr 在表格中表明行,實現表格中單雙行樣式就很簡單了:

tr:nth-child(2n) {
  background-color: red;
}


tr:nth-child(2n+1) {
  background-color: blue;
}

同理:

  1. 如何匹配最前三個子元素: :nth-child(-n+3)
  2. 如何匹配最後三個子元素: :nth-last-child(-n+3)

10 簡述下 css specificity

在 Issue 中交流與討論: 10 簡述下 css specificity

css specificity 即 css 中關於選擇器的權重,如下三種類型的選擇器依次降低

  1. id 選擇器,如 #app
  2. classattributepseudo-classes 選擇器,如 .header[type="radio"]:hover
  3. type 標籤選擇器和僞元素選擇器,如 h1p::before

其中通配符選擇器 *,組合選擇器 + ~ >,否認僞類選擇器 :not() 對優先級無影響

另有內聯樣式 <div class="foo" style="color: red;"></div>!important(最高) 具備更高的權重

:not 的優先級影響 - codepen 能夠看出 :not 對選擇器的優先級無任何影響

11 node 中 module.exports 與 exports 有什麼區別

在 Issue 中交流與討論: 11 node 中 module.exports 與 exports 有什麼區別

一句話:exportsmodule.exports 的引用,若是 exports 沒有重賦值,則兩者沒有任何區別

相似以下所示

const exports = module.exports

那以下結果會如何導出?

module.exports = 100
exports = 3

很顯然會導出 100,畢竟 exports 進行了重賦值。

那在 node 源碼中如何實現的呢? 從源碼裏能夠看出 exports 的實質

module wrapper

詳見源碼: https://github.com/nodejs/nod...,能夠看出符合猜測

衆所周知,node 中全部的模塊代碼都被包裹在這個函數中

(function(exports, require, module, __filename, __dirname) {
  exports.a = 3
});

而如下源碼指出,exports 是如何得來

const dirname = path.dirname(filename);
const require = makeRequireFunction(this, redirects);
let result;
// 從這裏能夠看出來 exports 的實質
const exports = this.exports;
const thisValue = exports;
const module = this;
if (requireDepth === 0) statCache = new Map();
if (inspectorWrapper) {
  result = inspectorWrapper(compiledWrapper, thisValue, exports,
                            require, module, filename, dirname);
} else {

  // 這裏是模塊包裝函數
  result = compiledWrapper.call(thisValue, exports, require, module,
                                filename, dirname);
}

12 如何獲取當前系統中的在線用戶數 (併發用戶數)

<blockquote> 更多描述: 一些 SaaS 系統基於 Pricing 的考慮,會限制團隊人數及同時在線數,如何實現 </blockquote>

在 Issue 中交流與討論: 12 如何獲取當前系統中的在線用戶數 (併發用戶數)

一些 SaaS 系統基於訂價策略的考慮,會限制團隊人數及同時在線數,如何實現?

經過 rediszset 可實現併發用戶數。

當一個用戶請求任何接口時,實現一個 middleware,處理如下邏輯

// 當一個用戶訪問任何接口時,對該用戶Id,寫入 zset
await redis.zadd(`Organization:${organizationId}:concurrent`, Date.now(), `User:${userId}`)

// 查詢當前機構的併發數
// 經過查詢一分鐘內的活躍用戶來確認併發數,若是超過則拋出特定異常
const activeUsers = await redis.zrangebyscore(`Organization:${organizationId}:concurrent`, Date.now() - 1000 * 60, Date.now())

// 查出併發數
const count = activeUsers.length

// 刪掉過時的用戶
await redis.zrembyscore(`Organization:${organizationId}:concurrent`, Date.now() - 1000 * 60, Date.now())

總結

  1. 每當用戶訪問服務時,把該用戶的 ID 寫入優先級隊列,權重爲當前時間
  2. 根據權重(即時間)計算一分鐘內該機構的用戶數
  3. 刪掉一分鐘以上過時的用戶

13 如何把 json 數據轉化爲 demo.json 並下載文件

在 Issue 中交流與討論: 13 如何把 json 數據轉化爲 demo.json 並下載文件

json 視爲字符串,能夠利用 DataURL 進行下載

Text -> DataURL

除了使用 DataURL,還能夠轉化爲 Object URL 進行下載

Text -> Blob -> Object URL

能夠把如下代碼直接粘貼到控制檯下載文件

function download (url, name) {
  const a = document.createElement('a')
  a.download = name
  a.rel = 'noopener'
  a.href = url
  // 觸發模擬點擊
  a.dispatchEvent(new MouseEvent('click'))
  // 或者 a.click()
}

const json = {
  a: 3,
  b: 4,
  c: 5
}
const str = JSON.stringify(json, null, 2)

// 方案一:Text -> DataURL
const dataUrl = `data:,${str}`
download(dataUrl, 'demo.json')

// 方案二:Text -> Blob -> ObjectURL
const url = URL.createObjectURL(new Blob(str.split('')))
download(url, 'demo1.json')

總結

  1. 模擬下載,能夠經過新建一個 <a href="url" download><a> 標籤並設置 urldownload 屬性來下載
  2. 能夠經過把 json 轉化爲 dataurl 來構造 URL
  3. 能夠經過把 json 轉換爲 Blob 再轉化爲 ObjectURL 來構造 URL

14 在瀏覽器中如何監聽剪切板中內容

在 Issue 中交流與討論: 14 在瀏覽器中如何監聽剪切板中內容

經過 Clipboard API 能夠獲取剪切板中內容,但須要獲取到 clipboard-read 的權限,如下是關於讀取剪貼板內容的代碼:

// 是否可以有讀取剪貼板的權限
// result.state == "granted" || result.state == "prompt"
const result = await navigator.permissions.query({ name: "clipboard-read" })

// 獲取剪貼板內容
const text = await navigator.clipboard.readText()
注: 該方法在 devtools 中不生效

相關問題: 【Q019】如何實現選中複製的功能

關注我

我是山月,正致力於天天用五分鐘可以看完的簡短答案回答一個大廠高頻面試題

歡迎關注公衆號【互聯網大廠招聘】,定時推送大廠內推信息及面試題簡答,天天學習五分鐘,半年進入大廠中

天天五分鐘,半年大廠中

相關文章
相關標籤/搜索