中高階前端大廠面試祕籍,為你保駕護航金三銀四,直通大廠(上)

郭東東發表於2019-02-14

引言

當下,正面臨著近幾年來的最嚴重的網際網路寒冬,聽得最多的一句話便是:相見於江湖~????。縮減HC、裁員不絕於耳,大家都是人心惶惶,年前如此,年後想必肯定又是一場更為慘烈的江湖廝殺。但博主始終相信,寒冬之中,人才更是尤為珍貴。只要有過硬的操作和裝備,在逆風局下,同樣也能來一波收割翻盤。

博主也是年前經歷了一番廝殺,最終拿到多家大廠的 offer。在閉關修煉的過程中,自己整理出了一套面試祕籍供自己反覆研究,後來給了多位有需要的兄臺,均表示相當靠譜,理應在這寒冬之中回報於社會。於是決定花點精力整理成文,讓大家能比較系統的反覆學習,快速提升自己。

面試固然有技巧,但絕不是偽造與吹流弊,通過一段短時間沉下心來閉關修煉,出山收割,步入大廠,薪資翻番,豈不爽哉?????

修煉原則

想必大家很厭煩筆試和考察知識點。因為其實在平時實戰中,講究的是開發效率,很少會去刻意記下一些細節和深挖知識點,腦海中都是一些分散的知識點,無法系統性地關聯成網,一直處於時曾相識的狀態。不知道多少人和博主一樣,至今每次寫阻止冒泡都需要谷歌一番如何拼寫。????。

以如此的狀態,定然是無法在面試的戰場上縱橫的。其實面試就猶如考試,大家回想下高考之前所做的事,無非就是 理解系統性關聯記憶。本祕籍的知識點較多,花點時間一個個理解並記憶後,自然也就融會貫通,無所畏懼。

由於本祕籍為了便於記憶,快速達到應試狀態,類似於複習知識大綱。知識點會盡量的精簡與提煉知識脈絡,並不去展開深入細節,面面俱到。有興趣或者有疑問的童鞋可以自行谷歌下對應知識點的詳細內容。????

CSS

1. 盒模型

頁面渲染時,dom 元素所採用的 佈局模型。可通過box-sizing進行設定。根據計算寬高的區域可分為:

  • content-box (W3C 標準盒模型)
  • border-box (IE 盒模型)
  • padding-box
  • margin-box

2. BFC

塊級格式化上下文,是一個獨立的渲染區域,讓處於 BFC 內部的元素與外部的元素相互隔離,使內外元素的定位不會相互影響。

IE下為 Layout,可通過 zoom:1 觸發

  • 觸發條件:

    • 根元素
    • positon: absolute/fixed
    • display: inline-block / table
    • float 元素
    • ovevflow !== visible
  • 規則:

    • 屬於同一個 BFC 的兩個相鄰 Box 垂直排列
    • 屬於同一個 BFC 的兩個相鄰 Box 的 margin 會發生重疊
    • BFC 中子元素不會超出他的包含塊
    • BFC 的區域不會與 float 的元素區域重疊
    • 計算 BFC 的高度時,浮動子元素也參與計算
    • 文字層不會被浮動層覆蓋,環繞於周圍
  • 應用:

    • 阻止margin重疊
    • 可以包含浮動元素 —— 清除內部浮動(清除浮動的原理是兩個div都位於同一個 BFC 區域之中)
    • 自適應兩欄佈局
    • 可以阻止元素被浮動元素覆蓋

3.層疊上下文

元素提升為一個比較特殊的圖層,在三維空間中 (z軸) 高出普通元素一等。

  • 觸發條件

    • 根層疊上下文(html)
    • position
    • css3屬性
      • flex
      • transform
      • opacity
      • filter
      • will-change
      • -webkit-overflow-scrolling
  • 層疊等級:層疊上下文在z軸上的排序

    • 在同一層疊上下文中,層疊等級才有意義
    • z-index的優先順序最高
中高階前端大廠面試祕籍,為你保駕護航金三銀四,直通大廠(上)

4. 居中佈局

  • 水平居中

    • 行內元素: text-align: center
    • 塊級元素: margin: 0 auto
    • absolute + transform
    • flex + justify-content: center
  • 垂直居中

    • line-height: height
    • absolute + transform
    • flex + align-items: center
    • table
  • 水平垂直居中

    • absolute + transform
    • flex + justify-content + align-items

5. 選擇器優先順序

  • !important > 行內樣式 > #id > .class > tag > * > 繼承 > 預設
  • 選擇器 從右往左 解析

6.去除浮動影響,防止父級高度塌陷

  • 通過增加尾元素清除浮動
    • :after / <br> : clear: both
  • 建立父級 BFC
  • 父級設定高度

7.link 與 @import 的區別

  • link功能較多,可以定義 RSS,定義 Rel 等作用,而@import只能用於載入 css
  • 當解析到link時,頁面會同步載入所引的 css,而@import所引用的 css 會等到頁面載入完才被載入
  • @import需要 IE5 以上才能使用
  • link可以使用 js 動態引入,@import不行

8. CSS前處理器(Sass/Less/Postcss)

CSS前處理器的原理: 是將類 CSS 語言通過 Webpack 編譯 轉成瀏覽器可讀的真正 CSS。在這層編譯之上,便可以賦予 CSS 更多更強大的功能,常用功能:

  • 巢狀
  • 變數
  • 迴圈語句
  • 條件語句
  • 自動字首
  • 單位轉換
  • mixin複用

面試中一般不會重點考察該點,一般介紹下自己在實戰專案中的經驗即可~

9.CSS動畫

  • transition: 過渡動畫

    • transition-property: 屬性
    • transition-duration: 間隔
    • transition-timing-function: 曲線
    • transition-delay: 延遲
    • 常用鉤子: transitionend
  • animation / keyframes

    • animation-name: 動畫名稱,對應@keyframes
    • animation-duration: 間隔
    • animation-timing-function: 曲線
    • animation-delay: 延遲
    • animation-iteration-count: 次數
      • infinite: 迴圈動畫
    • animation-direction: 方向
      • alternate: 反向播放
    • animation-fill-mode: 靜止模式
      • forwards: 停止時,保留最後一幀
      • backwards: 停止時,回到第一幀
      • both: 同時運用 forwards / backwards
    • 常用鉤子: animationend
  • 動畫屬性: 儘量使用動畫屬性進行動畫,能擁有較好的效能表現

    • translate
    • scale
    • rotate
    • skew
    • opacity
    • color

經驗

通常,CSS 並不是重點的考察領域,但這其實是由於現在國內業界對 CSS 的專注不夠導致的,真正精通並專注於 CSS 的團隊和人才並不多。因此如果能在 CSS 領域有自己的見解和經驗,反而會為相當的加分和脫穎而出。

JavaScript

1. 原型 / 建構函式 / 例項

  • 原型(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 === 原型

例項.constructorr === 建構函式
複製程式碼

放大來看,我畫了張圖供大家徹底理解:

中高階前端大廠面試祕籍,為你保駕護航金三銀四,直通大廠(上)

2.原型鏈:

原型鏈是由原型物件組成,每個物件都有 __proto__ 屬性,指向了建立該物件的建構函式的原型,__proto__ 將物件連線起來組成了原型鏈。是一個用來實現繼承和共享屬性的有限的物件鏈。

  • 屬性查詢機制: 當查詢物件的屬性時,如果例項物件自身不存在該屬性,則沿著原型鏈往上一級查詢,找到時則輸出,不存在時,則繼續沿著原型鏈往上一級查詢,直至最頂級的原型物件Object.prototype,如還是沒找到,則輸出undefined

  • 屬性修改機制: 只會修改例項物件本身的屬性,如果不存在,則進行新增該屬性,如果需要修改原型的屬性時,則可以用: b.prototype.x = 2;但是這樣會造成所有繼承於該物件的例項的屬性發生改變。

3. 執行上下文(EC)

執行上下文可以簡單理解為一個物件:

  • 它包含三個部分:

    • 變數物件(VO)
    • 作用域鏈(詞法作用域)
    • this指向
  • 它的型別:

    • 全域性執行上下文
    • 函式執行上下文
    • eval執行上下文
  • 程式碼執行過程:

    • 建立 全域性上下文 (global EC)
    • 全域性執行上下文 (caller) 逐行 自上而下 執行。遇到函式時,函式執行上下文 (callee) 被push到執行棧頂層
    • 函式執行上下文被啟用,成為 active EC, 開始執行函式中的程式碼,caller 被掛起
    • 函式執行完後,callee 被pop移除出執行棧,控制權交還全域性上下文 (caller),繼續執行

2.變數物件

變數物件,是執行上下文中的一部分,可以抽象為一種 資料作用域,其實也可以理解為就是一個簡單的物件,它儲存著該執行上下文中的所有 變數和函式宣告(不包含函式表示式)

活動物件 (AO): 當變數物件所處的上下文為 active EC 時,稱為活動物件。

3. 作用域

執行上下文中還包含作用域鏈。理解作用域之前,先介紹下作用域。作用域其實可理解為該上下文中宣告的 變數和宣告的作用範圍。可分為 塊級作用域函式作用域

特性:

  • 宣告提前: 一個宣告在函式體內都是可見的, 函式優先於變數
  • 非匿名自執行函式,函式變數為 只讀 狀態,無法修改
const foo = 1
(function foo() {
    foo = 10  // 由於foo在函式中只為可讀,因此賦值無效
    console.log(foo)
}()) 

// 結果列印:  ƒ foo() { foo = 10 ; console.log(foo) }
複製程式碼

4.作用域鏈

我們知道,我們可以在執行上下文中訪問到父級甚至全域性的變數,這便是作用域鏈的功勞。作用域鏈可以理解為一組物件列表,包含 父級和自身的變數物件,因此我們便能通過作用域鏈訪問到父級裡宣告的變數或者函式。

  • 由兩部分組成:
    • [[scope]]屬性: 指向父級變數物件和作用域鏈,也就是包含了父級的[[scope]]AO
    • AO: 自身活動物件

如此 [[scopr]]包含[[scope]],便自上而下形成一條 鏈式作用域

5. 閉包

閉包屬於一種特殊的作用域,稱為 靜態作用域。它的定義可以理解為: 父函式被銷燬 的情況下,返回出的子函式的[[scope]]中仍然保留著父級的單變數物件和作用域鏈,因此可以繼續訪問到父級的變數物件,這樣的函式稱為閉包。

  • 閉包會產生一個很經典的問題:

    • 多個子函式的[[scope]]都是同時指向父級,是完全共享的。因此當父級的變數物件被修改時,所有子函式都受到影響。
  • 解決:

    • 變數可以通過 函式引數的形式 傳入,避免使用預設的[[scope]]向上查詢
    • 使用setTimeout包裹,通過第三個引數傳入
    • 使用 塊級作用域,讓變數成為自己上下文的屬性,避免共享

6. script 引入方式:

  • html 靜態<script>引入
  • js 動態插入<script>
  • <script defer>: 非同步載入,元素解析完成後執行
  • <script async>: 非同步載入,但執行時會阻塞元素渲染

7. 物件的拷貝

  • 淺拷貝: 以賦值的形式拷貝引用物件,仍指向同一個地址,修改時原物件也會受到影響

    • Object.assign
    • 展開運算子(…)
  • 深拷貝: 完全拷貝一個新物件,修改時原物件不再受到任何影響

    • JSON.parse(JSON.stringify(obj)): 效能最快
      • 具有迴圈引用的物件時,報錯
      • 當值為函式或undefined時,無法拷貝
    • 遞迴進行逐一賦值

8. new運算子的執行過程

  • 新生成一個物件
  • 連結到原型: obj.__proto__ = Con.prototype
  • 繫結this: apply
  • 返回新物件

9. instanceof原理

能在例項的 原型物件鏈 中找到該建構函式的prototype屬性所指向的 原型物件,就返回true。即:

// __proto__: 代表原型物件鏈
instance.[__proto__...] === instance.constructor.prototype

// return true
複製程式碼

10. 程式碼的複用

當你發現任何程式碼開始寫第二遍時,就要開始考慮如何複用。一般有以下的方式:

  • 函式封裝
  • 繼承
  • 複製extend
  • 混入mixin
  • 借用apply/call

11. 繼承

在 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

12. 型別轉換

大家都知道 JS 中在使用運算子號或者對比符時,會自帶隱式轉換,規則如下:

  • -、*、/、% :一律轉換成數值後計算
  • +:
    • 數字 + 字串 = 字串, 運算順序是從左到右
    • 數字 + 物件, 優先呼叫物件的valueOf -> toString
    • 數字 + boolean/null = 數字
    • 數字 + undefined == NaN
  • [1].toString() === `1`
  • {}.toString() === `[object object]`
  • NaN !== NaN+undefined === NaN

13. 型別判斷

判斷 Target 的型別,單單用 typeof 並無法完全滿足,這其實並不是 bug,本質原因是 JS 的萬物皆物件的理論。因此要真正完美判斷時,我們需要區分對待:

  • 基本型別(null): 使用 String(null)
  • 基本型別(string / number / boolean / undefined) + function: 直接使用 typeof即可
  • 其餘引用型別(Array / Date / RegExp Error): 呼叫toString後根據[object XXX]進行判斷

很穩的判斷封裝:

let class2type = {}
`Array Date RegExp Object Error`.split(` `).forEach(e => class2type[ `[object ` + e + `]` ] = e.toLowerCase()) 

function type(obj) {
    if (obj == null) return String(obj)
    return typeof obj === `object` ? class2type[ Object.prototype.toString.call(obj) ] || `object` : typeof obj
}
複製程式碼

14. 模組化

模組化開發在現代開發中已是必不可少的一部分,它大大提高了專案的可維護、可擴充和可協作性。通常,我們 在瀏覽器中使用 ES6 的模組化支援,在 Node 中使用 commonjs 的模組化支援。

  • 分類:

    • es6: import / exports
    • commonjs: require / module.exports / exports
    • amd: require / defined
  • requireimport的區別

    • require支援 動態匯入import不支援,正在提案 (babel 下可支援)
    • require同步 匯入,import屬於 非同步 匯入
    • require值拷貝,匯出值變化不會影響匯入值;import指向 記憶體地址,匯入值會隨匯出值而變化

15. 防抖與節流

防抖與節流函式是一種最常用的 高頻觸發優化方式,能對效能有較大的幫助。

  • 防抖 (debounce): 將多次高頻操作優化為只在最後一次執行,通常使用的場景是:使用者輸入,只需再輸入完成後做一次輸入校驗即可。
function debounce(fn, wait, immediate) {
    let timer = null

    return function() {
        let args = arguments
        let context = this

        if (immediate && !timer) {
            fn.apply(context, args)
        }

        if (timer) clearTimeout(timer)
        timer = setTimeout(() => {
            fn.apply(context, args)
        }, wait)
    }
}
複製程式碼
  • 節流(throttle): 每隔一段時間後執行一次,也就是降低頻率,將高頻操作優化成低頻操作,通常使用場景: 滾動條事件 或者 resize 事件,通常每隔 100~500 ms執行一次即可。
function throttle(fn, wait, immediate) {
    let timer = null
    let callNow = true
    
    return function() {
        let context = this,
            args = arguments

        if (callNow) {
            fn.apply(context, args)
            callNow = false
        }

        if (!timer) {
            timer = setTimeout(() => {
                fn.apply(context, args)
                timer = null
            }, wait)
        }
    }
}
複製程式碼

16. 函式執行改變this

由於 JS 的設計原理: 在函式中,可以引用執行環境中的變數。因此就需要一個機制來讓我們可以在函式體內部獲取當前的執行環境,這便是this

因此要明白 this 指向,其實就是要搞清楚 函式的執行環境,說人話就是,誰呼叫了函式。例如:

  • obj.fn(),便是 obj 呼叫了函式,既函式中的 this === obj
  • fn(),這裡可以看成 window.fn(),因此 this === window

但這種機制並不完全能滿足我們的業務需求,因此提供了三種方式可以手動修改 this 的指向:

  • call: fn.call(target, 1, 2)
  • apply: fn.apply(target, [1, 2])
  • bind: fn.bind(target)(1,2)

17. ES6/ES7

由於 Babel 的強大和普及,現在 ES6/ES7 基本上已經是現代化開發的必備了。通過新的語法糖,能讓程式碼整體更為簡潔和易讀。

  • 宣告

    • let / const: 塊級作用域、不存在變數提升、暫時性死區、不允許重複宣告
    • const: 宣告常量,無法修改
  • 解構賦值

  • class / extend: 類宣告與繼承

  • Set / Map: 新的資料結構

  • 非同步解決方案:

    • Promise的使用與實現

    • generator:

      • yield: 暫停程式碼
      • next(): 繼續執行程式碼
    function* helloWorld() {
      yield `hello`;
      yield `world`;
      return `ending`;
    }
    
    const generator = helloWorld();
    
    generator.next()  // { value: `hello`, done: false }
    
    generator.next()  // { value: `world`, done: false }
    
    generator.next()  // { value: `ending`, done: true }
    
    generator.next()  // { value: undefined, done: true }
    
    複製程式碼
    • await / async: 是generator的語法糖, babel中是基於promise實現。
    async function getUserByAsync(){
       let user = await fetchUser();
       return user;
    }
    
    const user = await getUserByAsync()
    console.log(user)
    複製程式碼

18. AST

抽象語法樹 (Abstract Syntax Tree),是將程式碼逐字母解析成 樹狀物件 的形式。這是語言之間的轉換、程式碼語法檢查,程式碼風格檢查,程式碼格式化,程式碼高亮,程式碼錯誤提示,程式碼自動補全等等的基礎。例如:

function square(n){
	return n * n
}
複製程式碼

通過解析轉化成的AST如下圖:

中高階前端大廠面試祕籍,為你保駕護航金三銀四,直通大廠(上)

19. babel編譯原理

  • babylon 將 ES6/ES7 程式碼解析成 AST
  • babel-traverse 對 AST 進行遍歷轉譯,得到新的 AST
  • 新 AST 通過 babel-generator 轉換成 ES5

20. 函式柯里化

在一個函式中,首先填充幾個引數,然後再返回一個新的函式的技術,稱為函式的柯里化。通常可用於在不侵入函式的前提下,為函式 預置通用引數,供多次重複呼叫。

const add = function add(x) {
	return function (y) {
		return x + y
	}
}

const add1 = add(1)

add1(2) === 3
add1(20) === 21
複製程式碼

21. 陣列(array)

  • map: 遍歷陣列,返回回撥返回值組成的新陣列

  • forEach: 無法break,可以用try/catchthrow new Error來停止

  • filter: 過濾

  • some: 有一項返回true,則整體為true

  • every: 有一項返回false,則整體為false

  • join: 通過指定連線符生成字串

  • push / pop: 末尾推入和彈出,改變原陣列, 返回推入/彈出項

  • unshift / shift: 頭部推入和彈出,改變原陣列,返回操作項

  • sort(fn) / reverse: 排序與反轉,改變原陣列

  • concat: 連線陣列,不影響原陣列, 淺拷貝

  • slice(start, end): 返回截斷後的新陣列,不改變原陣列

  • splice(start, number, value...): 返回刪除元素組成的陣列,value 為插入項,改變原陣列

  • indexOf / lastIndexOf(value, fromIndex): 查詢陣列項,返回對應的下標

  • reduce / reduceRight(fn(prev, cur), defaultPrev): 兩兩執行,prev 為上次化簡函式的return值,cur 為當前值(從第二項開始)

  • 陣列亂序:

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
arr.sort(function () {
    return Math.random() - 0.5;
});
複製程式碼
  • 陣列拆解: flat: [1,[2,3]] –> [1, 2, 3]
arr.prototype.flat = function() {
    this.toString().split(`,`).map(item => +item )
}
複製程式碼

瀏覽器

1. 跨標籤頁通訊

不同標籤頁間的通訊,本質原理就是去運用一些可以 共享的中間介質,因此比較常用的有以下方法:

  • 通過父頁面window.open()和子頁面postMessage

    • 非同步下,通過 window.open(`about: blank`)tab.location.href = `*`
  • 設定同域下共享的localStorage與監聽window.onstorage

    • 重複寫入相同的值無法觸發
    • 會受到瀏覽器隱身模式等的限制
  • 設定共享cookie與不斷輪詢髒檢查(setInterval)

  • 藉助服務端或者中間層實現

2. 瀏覽器架構

  • 使用者介面
  • 主程式
  • 核心
    • 渲染引擎
    • JS 引擎
      • 執行棧
    • 事件觸發執行緒
      • 訊息佇列
        • 微任務
        • 巨集任務
    • 網路非同步執行緒
    • 定時器執行緒

3. 瀏覽器下事件迴圈(Event Loop)

事件迴圈是指: 執行一個巨集任務,然後執行清空微任務列表,迴圈再執行巨集任務,再清微任務列表

  • 微任務 microtask(jobs): promise / ajax / Object.observe
  • 巨集任務 macrotask(task): setTimout / script / IO / UI Rendering

4. 從輸入 url 到展示的過程

  • DNS 解析
  • TCP 三次握手
  • 傳送請求,分析 url,設定請求報文(頭,主體)
  • 伺服器返回請求的檔案 (html)
  • 瀏覽器渲染
    • HTML parser –> DOM Tree
      • 標記化演算法,進行元素狀態的標記
      • dom 樹構建
    • CSS parser –> Style Tree
      • 解析 css 程式碼,生成樣式樹
    • attachment –> Render Tree
      • 結合 dom樹 與 style樹,生成渲染樹
    • layout: 佈局
    • GPU painting: 畫素繪製頁面

5. 重繪與迴流

當元素的樣式發生變化時,瀏覽器需要觸發更新,重新繪製元素。這個過程中,有兩種型別的操作,即重繪與迴流。

  • 重繪(repaint): 當元素樣式的改變不影響佈局時,瀏覽器將使用重繪對元素進行更新,此時由於只需要UI層面的重新畫素繪製,因此 損耗較少

  • 迴流(reflow): 當元素的尺寸、結構或觸發某些屬性時,瀏覽器會重新渲染頁面,稱為迴流。此時,瀏覽器需要重新經過計算,計算後還需要重新頁面佈局,因此是較重的操作。會觸發迴流的操作:

    • 頁面初次渲染
    • 瀏覽器視窗大小改變
    • 元素尺寸、位置、內容發生改變
    • 元素字型大小變化
    • 新增或者刪除可見的 dom 元素
    • 啟用 CSS 偽類(例如::hover)
    • 查詢某些屬性或呼叫某些方法
      • clientWidth、clientHeight、clientTop、clientLeft
      • offsetWidth、offsetHeight、offsetTop、offsetLeft
      • scrollWidth、scrollHeight、scrollTop、scrollLeft
      • getComputedStyle()
      • getBoundingClientRect()
      • scrollTo()

迴流必定觸發重繪,重繪不一定觸發迴流。重繪的開銷較小,迴流的代價較高。

最佳實踐:

  • css

    • 避免使用table佈局
    • 將動畫效果應用到position屬性為absolutefixed的元素上
  • javascript

    • 避免頻繁操作樣式,可彙總後統一 一次修改
    • 儘量使用class進行樣式修改
    • 減少dom的增刪次數,可使用 字串 或者 documentFragment 一次性插入
    • 極限優化時,修改樣式可將其display: none後修改
    • 避免多次觸發上面提到的那些會觸發迴流的方法,可以的話儘量用 變數存住

6. 儲存

我們經常需要對業務中的一些資料進行儲存,通常可以分為 短暫性儲存 和 永續性儲存。

  • 短暫性的時候,我們只需要將資料存在記憶體中,只在執行時可用

  • 永續性儲存,可以分為 瀏覽器端 與 伺服器端

    • 瀏覽器:
      • cookie: 通常用於儲存使用者身份,登入狀態等
        • http 中自動攜帶, 體積上限為 4K, 可自行設定過期時間
      • localStorage / sessionStorage: 長久儲存/視窗關閉刪除, 體積限制為 4~5M
      • indexDB
    • 伺服器:
      • 分散式快取 redis
      • 資料庫

7. Web Worker

現代瀏覽器為JavaScript創造的 多執行緒環境。可以新建並將部分任務分配到worker執行緒並行執行,兩個執行緒可 獨立執行,互不干擾,可通過自帶的 訊息機制 相互通訊。

基本用法:

// 建立 worker
const worker = new Worker(`work.js`);

// 向主程式推送訊息
worker.postMessage(`Hello World`);

// 監聽主程式來的訊息
worker.onmessage = function (event) {
  console.log(`Received message ` + event.data);
}
複製程式碼

限制:

  • 同源限制
  • 無法使用 document / window / alert / confirm
  • 無法載入本地資源

8. V8垃圾回收機制

垃圾回收: 將記憶體中不再使用的資料進行清理,釋放出記憶體空間。V8 將記憶體分成 新生代空間老生代空間

  • 新生代空間: 用於存活較短的物件
    • 又分成兩個空間: from 空間 與 to 空間
    • Scavenge GC演算法: 當 from 空間被佔滿時,啟動 GC 演算法
      • 存活的物件從 from space 轉移到 to space
      • 清空 from space
      • from space 與 to space 互換
      • 完成一次新生代GC
  • 老生代空間: 用於存活時間較長的物件
    • 從 新生代空間 轉移到 老生代空間 的條件
      • 經歷過一次以上 Scavenge GC 的物件
      • 當 to space 體積超過25%
    • 標記清除演算法: 標記存活的物件,未被標記的則被釋放
      • 增量標記: 小模組標記,在程式碼執行間隙執,GC 會影響效能
      • 併發標記(最新技術): 不阻塞 js 執行
    • 壓縮演算法: 將記憶體中清除後導致的碎片化物件往記憶體堆的一端移動,解決 記憶體的碎片化

9. 記憶體洩露

  • 意外的全域性變數: 無法被回收
  • 定時器: 未被正確關閉,導致所引用的外部變數無法被釋放
  • 事件監聽: 沒有正確銷燬 (低版本瀏覽器可能出現)
  • 閉包: 會導致父級中的變數無法被釋放
  • dom 引用: dom 元素被刪除時,記憶體中的引用未被正確清空

可用 chrome 中的 timeline 進行記憶體標記,視覺化檢視記憶體的變化情況,找出異常點。

服務端與網路

1. http/https 協議

  • 1.0 協議缺陷:

    • 無法複用連結,完成即斷開,重新慢啟動和 TCP 3次握手
    • head of line blocking: 線頭阻塞,導致請求之間互相影響
  • 1.1 改進:

    • 長連線(預設 keep-alive),複用
    • host 欄位指定對應的虛擬站點
    • 新增功能:
      • 斷點續傳
      • 身份認證
      • 狀態管理
      • cache 快取
        • Cache-Control
        • Expires
        • Last-Modified
        • Etag
  • 2.0:

    • 多路複用
    • 二進位制分幀層: 應用層和傳輸層之間
    • 首部壓縮
    • 服務端推送
  • https: 較為安全的網路傳輸協議

    • 證照(公鑰)
    • SSL 加密
    • 埠 443
  • TCP:

    • 三次握手
    • 四次揮手
    • 滑動視窗: 流量控制
    • 擁塞處理
      • 慢開始
      • 擁塞避免
      • 快速重傳
      • 快速恢復
  • 快取策略: 可分為 強快取協商快取

    • Cache-Control/Expires: 瀏覽器判斷快取是否過期,未過期時,直接使用強快取,Cache-Control的 max-age 優先順序高於 Expires

    • 當快取已經過期時,使用協商快取

      • 唯一標識方案: Etag(response 攜帶) & If-None-Match(request攜帶,上一次返回的 Etag): 伺服器判斷資源是否被修改,
      • 最後一次修改時間: Last-Modified(response) & If-Modified-Since (request,上一次返回的Last-Modified)
        • 如果一致,則直接返回 304 通知瀏覽器使用快取
        • 如不一致,則服務端返回新的資源
    • Last-Modified 缺點:

      • 週期性修改,但內容未變時,會導致快取失效
      • 最小粒度只到 s, s 以內的改動無法檢測到
    • Etag 的優先順序高於 Last-Modified

2. 常見狀態碼

  • 1xx: 接受,繼續處理
  • 200: 成功,並返回資料
  • 201: 已建立
  • 202: 已接受
  • 203: 成為,但未授權
  • 204: 成功,無內容
  • 205: 成功,重置內容
  • 206: 成功,部分內容
  • 301: 永久移動,重定向
  • 302: 臨時移動,可使用原有URI
  • 304: 資源未修改,可使用快取
  • 305: 需代理訪問
  • 400: 請求語法錯誤
  • 401: 要求身份認證
  • 403: 拒絕請求
  • 404: 資源不存在
  • 500: 伺服器錯誤

3. get / post

  • get: 快取、請求長度受限、會被歷史儲存記錄
    • 無副作用(不修改資源),冪等(請求次數與資源無關)的場景
  • post: 安全、大資料、更多編碼型別

兩者詳細對比如下圖:

中高階前端大廠面試祕籍,為你保駕護航金三銀四,直通大廠(上)

4. Websocket

Websocket 是一個 持久化的協議, 基於 http , 服務端可以 主動 push

  • 相容:

    • FLASH Socket
    • 長輪詢: 定時傳送 ajax
    • long poll: 傳送 –> 有訊息時再 response
  • new WebSocket(url)

  • ws.onerror = fn

  • ws.onclose = fn

  • ws.onopen = fn

  • ws.onmessage = fn

  • ws.send()

5. TCP三次握手

建立連線前,客戶端和服務端需要通過握手來確認對方:

  • 客戶端傳送 syn(同步序列編號) 請求,進入 syn_send 狀態,等待確認
  • 服務端接收並確認 syn 包後傳送 syn+ack 包,進入 syn_recv 狀態
  • 客戶端接收 syn+ack 包後,傳送 ack 包,雙方進入 established 狀態

6. TCP四次揮手

  • 客戶端 — FIN –> 服務端, FIN—WAIT
  • 服務端 — ACK –> 客戶端, CLOSE-WAIT
  • 服務端 — ACK,FIN –> 客戶端, LAST-ACK
  • 客戶端 — ACK –> 服務端,CLOSED

7. Node 的 Event Loop: 6個階段

  • timer 階段: 執行到期的setTimeout / setInterval佇列回撥
  • I/O 階段: 執行上輪迴圈殘流的callback
  • idle, prepare
  • poll: 等待回撥
      1. 執行回撥
      1. 執行定時器
      • 如有到期的setTimeout / setInterval, 則返回 timer 階段
      • 如有setImmediate,則前往 check 階段
  • check
    • 執行setImmediate
  • close callbacks

跨域

  • JSONP: 利用<script>標籤不受跨域限制的特點,缺點是隻能支援 get 請求
function jsonp(url, jsonpCallback, success) {
  const script = document.createElement(`script`)
  script.src = url
  script.async = true
  script.type = `text/javascript`
  window[jsonpCallback] = function(data) {
    success && success(data)
  }
  document.body.appendChild(script)
}
複製程式碼
  • 設定 CORS: Access-Control-Allow-Origin:*
  • postMessage

安全

  • XSS攻擊: 注入惡意程式碼
    • cookie 設定 httpOnly
    • 轉義頁面上的輸入內容和輸出內容
  • CSRF: 跨站請求偽造,防護:
    • get 不修改資料
    • 不被第三方網站訪問到使用者的 cookie
    • 設定白名單,不被第三方網站請求
    • 請求校驗

框架:Vue

1. nextTick

在下次dom更新迴圈結束之後執行延遲迴調,可用於獲取更新後的dom狀態

  • 新版本中預設是mincrotasks, v-on中會使用macrotasks

  • macrotasks任務的實現:

    • setImmediate / MessageChannel / setTimeout

2. 生命週期

  • _init_

    • initLifecycle/Event,往vm上掛載各種屬性
    • callHook: beforeCreated: 例項剛建立
    • initInjection/initState: 初始化注入和 data 響應性
    • created: 建立完成,屬性已經繫結, 但還未生成真實dom
    • 進行元素的掛載: $el / vm.$mount()
    • 是否有template: 解析成render function
      • *.vue檔案: vue-loader會將<template>編譯成render function
    • beforeMount: 模板編譯/掛載之前
    • 執行render function,生成真實的dom,並替換到dom tree
    • mounted: 元件已掛載
  • update:

    • 執行diff演算法,比對改變是否需要觸發UI更新
    • flushScheduleQueue
      • watcher.before: 觸發beforeUpdate鉤子 – watcher.run(): 執行watcher中的 notify,通知所有依賴項更新UI
    • 觸發updated鉤子: 元件已更新
  • actived / deactivated(keep-alive): 不銷燬,快取,元件啟用與失活

  • destroy:

    • beforeDestroy: 銷燬開始
    • 銷燬自身且遞迴銷燬子元件以及事件監聽
      • remove(): 刪除節點
      • watcher.teardown(): 清空依賴
      • vm.$off(): 解綁監聽
    • destroyed: 完成後觸發鉤子

上面是vue的宣告週期的簡單梳理,接下來我們直接以程式碼的形式來完成vue的初始化


new Vue({})

// 初始化Vue例項
function _init() {
	 // 掛載屬性
    initLifeCycle(vm) 
    // 初始化事件系統,鉤子函式等
    initEvent(vm) 
    // 編譯slot、vnode
    initRender(vm) 
    // 觸發鉤子
    callHook(vm, `beforeCreate`)
    // 新增inject功能
    initInjection(vm)
    // 完成資料響應性 props/data/watch/computed/methods
    initState(vm)
    // 新增 provide 功能
    initProvide(vm)
    // 觸發鉤子
    callHook(vm, `created`)
		
	 // 掛載節點
    if (vm.$options.el) {
        vm.$mount(vm.$options.el)
    }
}

// 掛載節點實現
function mountComponent(vm) {
	 // 獲取 render function
    if (!this.options.render) {
        // template to render
        // Vue.compile = compileToFunctions
        let { render } = compileToFunctions() 
        this.options.render = render
    }
    // 觸發鉤子
    callHook(`beforeMounte`)
    // 初始化觀察者
    // render 渲染 vdom, 
    vdom = vm.render()
    // update: 根據 diff 出的 patchs 掛載成真實的 dom 
    vm._update(vdom)
    // 觸發鉤子  
    callHook(vm, `mounted`)
}

// 更新節點實現
funtion queueWatcher(watcher) {
	nextTick(flushScheduleQueue)
}

// 清空佇列
function flushScheduleQueue() {
	 // 遍歷佇列中所有修改
    for(){
	    // beforeUpdate
        watcher.before()
         
        // 依賴區域性更新節點
        watcher.update() 
        callHook(`updated`)
    }
}

// 銷燬例項實現
Vue.prototype.$destory = function() {
	 // 觸發鉤子
    callHook(vm, `beforeDestory`)
    // 自身及子節點
    remove() 
    // 刪除依賴
    watcher.teardown() 
    // 刪除監聽
    vm.$off() 
    // 觸發鉤子
    callHook(vm, `destoryed`)
}
複製程式碼

3. 資料響應(資料劫持)

看完生命週期後,裡面的watcher等內容其實是資料響應中的一部分。資料響應的實現由兩部分構成: 觀察者( watcher )依賴收集器( Dep ),其核心是 defineProperty這個方法,它可以 重寫屬性的 get 與 set 方法,從而完成監聽資料的改變。

  • Observe (觀察者)觀察 props 與 state
    • 遍歷 props 與 state,對每個屬性建立獨立的監聽器( watcher )
  • 使用 defineProperty 重寫每個屬性的 get/set(defineReactive
    • get: 收集依賴
      • Dep.depend()
        • watcher.addDep()
    • set: 派發更新
      • Dep.notify()
      • watcher.update()
      • queenWatcher()
      • nextTick
      • flushScheduleQueue
      • watcher.run()
      • updateComponent()

大家可以先看下面的資料相應的程式碼實現後,理解後就比較容易看懂上面的簡單脈絡了。

let data = {a: 1}
// 資料響應性
observe(data)

// 初始化觀察者
new Watcher(data, `name`, updateComponent)
data.a = 2

// 簡單表示用於資料更新後的操作
function updateComponent() {
    vm._update() // patchs
}

// 監視物件
function observe(obj) {
	 // 遍歷物件,使用 get/set 重新定義物件的每個屬性值
    Object.keys(obj).map(key => {
        defineReactive(obj, key, obj[key])
    })
}

function defineReactive(obj, k, v) {
    // 遞迴子屬性
    if (type(v) == `object`) observe(v)
    
    // 新建依賴收集器
    let dep = new Dep()
    // 定義get/set
    Object.defineProperty(obj, k, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
        	  // 當有獲取該屬性時,證明依賴於該物件,因此被新增進收集器中
            if (Dep.target) {
                dep.addSub(Dep.target)
            }
            return v
        },
        // 重新設定值時,觸發收集器的通知機制
        set: function reactiveSetter(nV) {
            v = nV
            dep.nofify()
        },
    })
}

// 依賴收集器
class Dep {
    constructor() {
        this.subs = []
    }
    addSub(sub) {
        this.subs.push(sub)
    }
    notify() {
        this.subs.map(sub => {
            sub.update()
        })
    }
}

Dep.target = null

// 觀察者
class Watcher {
    constructor(obj, key, cb) {
        Dep.target = this
        this.cb = cb
        this.obj = obj
        this.key = key
        this.value = obj[key]
        Dep.target = null
    }
    addDep(Dep) {
        Dep.addSub(this)
    }
    update() {
        this.value = this.obj[this.key]
        this.cb(this.value)
    }
    before() {
        callHook(`beforeUpdate`)
    }
}
複製程式碼

4. virtual dom 原理實現

  • 建立 dom 樹

  • 樹的diff,同層對比,輸出patchs(listDiff/diffChildren/diffProps)

    • 沒有新的節點,返回
    • 新的節點tagNamekey不變, 對比props,繼續遞迴遍歷子樹
      • 對比屬性(對比新舊屬性列表):
        • 舊屬性是否存在與新屬性列表中
        • 都存在的是否有變化
        • 是否出現舊列表中沒有的新屬性
    • tagNamekey值變化了,則直接替換成新節點
  • 渲染差異

    • 遍歷patchs, 把需要更改的節點取出來
    • 區域性更新dom
// diff演算法的實現
function diff(oldTree, newTree) {
	 // 差異收集
    let pathchs = {}
    dfs(oldTree, newTree, 0, pathchs)
    return pathchs
}

function dfs(oldNode, newNode, index, pathchs) {
    let curPathchs = []
    if (newNode) {
        // 當新舊節點的 tagName 和 key 值完全一致時
        if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
        	  // 繼續比對屬性差異
            let props = diffProps(oldNode.props, newNode.props)
            curPathchs.push({ type: `changeProps`, props })
            // 遞迴進入下一層級的比較
            diffChildrens(oldNode.children, newNode.children, index, pathchs)
        } else {
        	  // 當 tagName 或者 key 修改了後,表示已經是全新節點,無需再比
            curPathchs.push({ type: `replaceNode`, node: newNode })
        }
    }

	 // 構建出整顆差異樹
    if (curPathchs.length) {
    		if(pathchs[index]){
    			pathchs[index] = pathchs[index].concat(curPathchs)
    		} else {
    			pathchs[index] = curPathchs
    		}
    }
}

// 屬性對比實現
function diffProps(oldProps, newProps) {
    let propsPathchs = []
    // 遍歷新舊屬性列表
    // 查詢刪除項
    // 查詢修改項
    // 查詢新增項
    forin(olaProps, (k, v) => {
        if (!newProps.hasOwnProperty(k)) {
            propsPathchs.push({ type: `remove`, prop: k })
        } else {
            if (v !== newProps[k]) {
                propsPathchs.push({ type: `change`, prop: k , value: newProps[k] })
            }
        }
    })
    forin(newProps, (k, v) => {
        if (!oldProps.hasOwnProperty(k)) {
            propsPathchs.push({ type: `add`, prop: k, value: v })
        }
    })
    return propsPathchs
}

// 對比子級差異
function diffChildrens(oldChild, newChild, index, pathchs) {
		// 標記子級的刪除/新增/移動
    let { change, list } = diffList(oldChild, newChild, index, pathchs)
    if (change.length) {
        if (pathchs[index]) {
            pathchs[index] = pathchs[index].concat(change)
        } else {
            pathchs[index] = change
        }
    }

	 // 根據 key 獲取原本匹配的節點,進一步遞迴從頭開始對比
    oldChild.map((item, i) => {
        let keyIndex = list.indexOf(item.key)
        if (keyIndex) {
            let node = newChild[keyIndex]
            // 進一步遞迴對比
            dfs(item, node, index, pathchs)
        }
    })
}

// 列表對比,主要也是根據 key 值查詢匹配項
// 對比出新舊列表的新增/刪除/移動
function diffList(oldList, newList, index, pathchs) {
    let change = []
    let list = []
    const newKeys = getKey(newList)
    oldList.map(v => {
        if (newKeys.indexOf(v.key) > -1) {
            list.push(v.key)
        } else {
            list.push(null)
        }
    })

    // 標記刪除
    for (let i = list.length - 1; i>= 0; i--) {
        if (!list[i]) {
            list.splice(i, 1)
            change.push({ type: `remove`, index: i })
        }
    }

    // 標記新增和移動
    newList.map((item, i) => {
        const key = item.key
        const index = list.indexOf(key)
        if (index === -1 || key == null) {
            // 新增
            change.push({ type: `add`, node: item, index: i })
            list.splice(i, 0, key)
        } else {
            // 移動
            if (index !== i) {
                change.push({
                    type: `move`,
                    form: index,
                    to: i,
                })
                move(list, index, i)
            }
        }
    })

    return { change, list }
}
複製程式碼

5. Proxy 相比於 defineProperty 的優勢

  • 陣列變化也能監聽到
  • 不需要深度遍歷監聽
let data = { a: 1 }
let reactiveData = new Proxy(data, {
	get: function(target, name){
		// ...
	},
	// ...
})
複製程式碼

6. vue-router

  • mode
    • hash
    • history
  • 跳轉
    • this.$router.push()
    • <router-link to=""></router-link>
  • 佔位
    • <router-view></router-view>

7. vuex

  • state: 狀態中心
  • mutations: 更改狀態
  • actions: 非同步更改狀態
  • getters: 獲取狀態
  • modules: 將state分成多個modules,便於管理

演算法

其實演算法方面在前端的實際專案中涉及得並不多,但還是需要精通一些基礎性的演算法,一些公司還是會有這方面的需求和考核,建議大家還是需要稍微準備下,這屬於加分題。

1. 五大演算法

  • 貪心演算法: 區域性最優解法
  • 分治演算法: 分成多個小模組,與原問題性質相同
  • 動態規劃: 每個狀態都是過去歷史的一個總結
  • 回溯法: 發現原先選擇不優時,退回重新選擇
  • 分支限界法

2. 基礎排序演算法

  • 氣泡排序: 兩兩比較
	function bubleSort(arr) {
	    var len = arr.length;
	    for (let outer = len ; outer >= 2; outer--) {
	        for(let inner = 0; inner <=outer - 1; inner++) {
	            if(arr[inner] > arr[inner + 1]) {
	                [arr[inner],arr[inner+1]] = [arr[inner+1],arr[inner]]
	            }
	        }
	    }
	    return arr;
	}
複製程式碼
  • 選擇排序: 遍歷自身以後的元素,最小的元素跟自己調換位置
function selectSort(arr) {
    var len = arr.length;
    for(let i = 0 ;i < len - 1; i++) {
        for(let j = i ; j<len; j++) {
            if(arr[j] < arr[i]) {
                [arr[i],arr[j]] = [arr[j],arr[i]];
            }
        }
    }
    return arr
}
複製程式碼
  • 插入排序: 即將元素插入到已排序好的陣列中
function insertSort(arr) {
    for(let i = 1; i < arr.length; i++) {  //外迴圈從1開始,預設arr[0]是有序段
        for(let j = i; j > 0; j--) {  //j = i,將arr[j]依次插入有序段中
            if(arr[j] < arr[j-1]) {
                [arr[j],arr[j-1]] = [arr[j-1],arr[j]];
            } else {
                break;
            }
        }
    }
    return arr;
}
複製程式碼

3. 高階排序演算法

  • 快速排序
    • 選擇基準值(base),原陣列長度減一(基準值),使用 splice
    • 迴圈原陣列,小的放左邊(left陣列),大的放右邊(right陣列);
    • concat(left, base, right)
    • 遞迴繼續排序 left 與 right
function quickSort(arr) {
    if(arr.length <= 1) {
        return arr;  //遞迴出口
    }
    var left = [],
        right = [],
        current = arr.splice(0,1); 
    for(let i = 0; i < arr.length; i++) {
        if(arr[i] < current) {
            left.push(arr[i])  //放在左邊
        } else {
            right.push(arr[i]) //放在右邊
        }
    }
    return quickSort(left).concat(current,quickSort(right));
}
複製程式碼
  • 希爾排序:不定步數的插入排序,插入排序

  • 口訣: 插冒歸基穩定,快選堆希不穩定

中高階前端大廠面試祕籍,為你保駕護航金三銀四,直通大廠(上)

穩定性: 同大小情況下是否可能會被交換位置, 虛擬dom的diff,不穩定性會導致重新渲染;

4. 遞迴運用(斐波那契數列): 爬樓梯問題

初始在第一級,到第一級有1種方法(s(1) = 1),到第二級也只有一種方法(s(2) = 1), 第三級(s(3) = s(1) + s(2))

function cStairs(n) {
    if(n === 1 || n === 2) {
        return 1;
    } else {
        return cStairs(n-1) + cStairs(n-2)
    }
}
複製程式碼

5. 資料樹

  • 二叉樹: 最多隻有兩個子節點
    • 完全二叉樹
    • 滿二叉樹
      • 深度為 h, 有 n 個節點,且滿足 n = 2^h – 1
  • 二叉查詢樹: 是一種特殊的二叉樹,能有效地提高查詢效率
    • 小值在左,大值在右
    • 節點 n 的所有左子樹值小於 n,所有右子樹值大於 n
中高階前端大廠面試祕籍,為你保駕護航金三銀四,直通大廠(上)
  • 遍歷節點
    • 前序遍歷
        1. 根節點
        1. 訪問左子節點,回到 1
        1. 訪問右子節點,回到 1
    • 中序遍歷
        1. 先訪問到最左的子節點
        1. 訪問該節點的父節點
        1. 訪問該父節點的右子節點, 回到 1
    • 後序遍歷
        1. 先訪問到最左的子節點
        1. 訪問相鄰的右節點
        1. 訪問父節點, 回到 1
  • 插入與刪除節點

6. 天平找次品

有n個硬幣,其中1個為假幣,假幣重量較輕,你有一把天平,請問,至少需要稱多少次能保證一定找到假幣?

  • 三等分演算法:
      1. 將硬幣分成3組,隨便取其中兩組天平稱量
      • 平衡,假幣在未上稱的一組,取其回到 1 繼續迴圈
      • 不平衡,假幣在天平上較輕的一組, 取其回到 1 繼續迴圈

結語

由於精力時間及篇幅有限,這篇就先寫到這。大家慢慢來不急。。????。下篇打算準備以下內容,我也得補補課先:

  • Webpack相關
    • 原理
    • Loader
    • Plugin
  • 專案效能優化
    • 首屏渲染優化
    • 使用者體驗優化
    • webpack 效能優化
  • Hybrid 與 Webview
    • webview 載入過程
    • bridge 原理
    • hybrid app 經驗
  • 框架: React

在面試中,很多領域並沒有真正的答案,能回答到什麼樣的深度,還是得靠自己真正的去使用和研究。知識面的廣度與深度應該並行,儘量的拓張自己的領域,至少都有些基礎性的瞭解,在被問到的時候可以同面試官嘮嗑兩句,然後在自己喜歡的領域,又有著足夠深入的研究,讓面試官覺得你是這方面的專家。

知識大綱還在不斷的完善和修正,由於也是精力時間有限,我會慢慢補充後面列出來的部分。當然,我也是在整理中不斷的學習,也希望大家能一起參與進來,要補充或修正的地方麻煩趕緊提出。另外,剛新建了個公眾號,想作為大家交流和分享的地方,有興趣想法的童鞋聯絡我哈~~????

Tips:
位元組跳動招中高階前端,有興趣內推的同學可簡歷郵件至 guoxiaodong.tayde@bytedance.com (標題: 姓名-崗位-地點) 或關注下面公眾號詳聊哈。

博主寫得很辛苦,感恩騙點star github。????

中高階前端大廠面試祕籍,為你保駕護航金三銀四,直通大廠(上)

相關文章