【字節/阿里/騰訊】大前端職位面試必考知識點

語言: CN / TW / HK

B端架構組大前端職位

Hi, 大家好, 如標題所示本教程的目的是教你如何通過【字節/阿里/騰訊】B端架構組大前端的面試,這個職位對nodejs有一定的要求,所以我們的通關手冊更偏向服務端基礎中 Node.js 程序員需要了解的部分。

全網對於這部分知識比較有價值,成系統的文章之前只有【如何通過餓了麼 Node.js 面試】,也就是餓了麼大前端團隊出的面試指南,這個指南所列出來的一些題的方向,是實打實真實面試的問題,所以其參考價值是非常高的。

本文還在努力迭代中,目的是在其基礎上大大加強了其內容的深度和廣度(每一個話題,都是參考了多篇網上大神的回答,並結合筆者自身使用情況進行了增刪補),毫無誇張的説,你想驗證自己是否具備大前端的基本能力和應聘大前端職位,只此一家算得上比較系統的文章了。所以,千萬別錯過呦!

因為涉及的知識點很多,加之作者能力有限,難免有疏漏,並且歡迎在github上歡迎大家討論交流,一起維護這個項目。

此篇為第一章,javascript基礎,全文請移步github:

【字節/阿里/騰訊】大前端職位面試必考知識點

感謝star哦!

JavaScript 基礎問題

知識點:

  • 類型判斷
  • 原型和原型鏈
  • 作用域
  • 執行上下文棧
  • 引用傳遞
  • 內存釋放
  • 閉包
  • 如何排查內存泄露
  • ES6 新特性

簡述

與前端 Js 不同, 後端方面除了SSR/爬蟲之外很少會接觸 DOM, 所以關於 DOM 方面的各種知識基本不會討論. 瀏覽器端除了圖形業務外很少碰到內存問題, 但是後端幾乎是直面服務器內存的, 更加偏向內存方面, 對於一些更基礎的問題也會更加關注.

類型判斷

1、typeof能夠識別哪些類型?

typeof可以測試出number、string、boolean、Symbol、bigint、undefined及function,而對於null及數組、對象,typeof均檢測出為object,不能進一步判斷它們的類型。

>>> 延伸問題:typeof NaN 結果是什麼?以及為什麼
  • 答案是number,為什麼呢?因為IEEE-754標準,64位浮點數,當指數位全為1, 表示非數字(NaN Not a Number),諸如0除以0的結果。
>>> 延伸問題: 為什麼string這種基礎類型居然還能調用方法,例如'a'.indexOf('a'), 方法調用不是對象才有的能力嗎?
  • js中為了便於基本類型操作,提供了3個特殊的引用類型:Boolean、Number、String它們具有基本類型特殊行為。

  • 實際上,每當讀取一個基本類型的時候,js內部會自動創建一個基本包裝類型對象,可以讓我們調用一些方法來操作。

  • 'a'.indexOf('a')在調用過程中會先let str = new String('a'),然後調用indexOf,調用完畢str = null, 銷燬該對象

如何精確判斷引用類型?

Object.prototype.toString方法返回對象的類型字符串,因此可用來判斷一個值的類型。

```javascript Object.prototype.toString.call(undefined); // [object Undefined]

Object.prototype.toString.call(null); // [object Null]

Object.prototype.toString.call("這是字符串"); // [object String]

Object.prototype.toString.call(1); // [object Number]

Object.prototype.toString.call(true); // [object Boolean]

Object.prototype.toString.call({}); // [object Object]

Object.prototype.toString.call([]); // [object Array]

Object.prototype.toString.call(new Function()); // [object Function]

Object.prototype.toString.call(new Date()); // [object Date]

Object.prototype.toString.call(new RegExp()); // [object RegExp]

Object.prototype.toString.call(new Error()); // [object Error] ```

>>> 延伸問題:為什麼要用 Object.prototype.toString,不用自己原型對象的toString方法,比如函數const a = new Function(); 調用a.toString()不行嗎?

因為實例對象有可能會自定義toString方法,會覆蓋Object.prototype.toString,所以在使用時,最好加上call。

原型和原型鏈

請回答這個題,如果回答正確你的原型和原型鏈就完全沒問題了。 JAVASCRIPT class Foo{} const f1 = new Foo(); 請問: ```javascript // 難度小 console.log(Foo.prototype.constructor === Foo) // ? console.log(f1.proto === Foo.prototype) // ?

// 難度中 console.log(Object.prototype.proto === null) // ? console.log(Function.prototype.proto === Object.prototype) // ?

// 難度較大 console.log(Function.prototype === Object.proto) // ? ```

答案:全是true,至於為啥,可能要寫一篇很長的文章了,留給大家去思索吧。(簡單記憶,所有函數最終都收斂到Function.prototype,所有對象都收斂到Object.prototype,或者説null)

作用域

作用域是指程序源代碼中定義變量的區域。在這個區域內,變量有自己的訪問權限。在javascript中,採取的是靜態作用域

請看下面的案例,便知什麼是靜態作用域 ```JAVASCRIPT let value = 1;

function foo() { console.log(value); }

function bar() { let value = 2; foo(); }

bar(); ``` 答案是1,因為foo中的value的作用域在書寫完畢代碼的時候就已經決定了,這就是靜態作用域。

面試題: ```javascript let scope = "global scope"; function checkscope(){ let scope = "local scope"; function f(){ return scope; } return f(); }

checkscope();

let scope = "global scope"; function checkscope(){ let scope = "local scope"; function f(){ return scope; } return f; } checkscope()(); ``` 請問輸出什麼?

跟作用域相關的一個話題是變量提升和函數提升,想了解面試者函數提升掌握能力可以出下題:

```javascript function foo() {

console.log('foo1');

}

foo(); // foo2

function foo() {

console.log('foo2');

}

foo(); // foo2 ``` 請問兩次foo執行輸出什麼,為什麼?

執行上下文棧

有些人可能要説《高級程序設計》的VO,AO,對不起,這是ES3的説法。答這道題就走遠了。 詳細內容請看 https://juejin.cn/post/6844903682283143181

一般面試題會問,能談下執行上下文是什麼嗎?let和var在執行上下文棧保存的區域有什麼區別?

  • 每當 JavaScript 解釋器要執行我們編寫的函數或腳本時,它都會創建一個新的上下文。每個腳本/代碼都以一個稱為全局執行上下文的執行上下文開始。每次我們調用一個函數時,都會創建一個新的執行上下文並將其放在執行堆棧的頂部。當您調用調用另一個嵌套函數的嵌套函數時,也會放在執行堆棧的頂部。

簡而言之,

在全局環境中,環境記錄器是對象環境記錄器。 在函數環境中,環境記錄器是聲明式環境記錄器。

抽象地講,詞法環境在偽代碼中看起來像這樣: ```javascript // 全局的執行上下文 // LexicalEnvironment是指詞法環境主要保存let const聲明的變量 GlobalExectionContext = { ThisBinding = , // this綁定 LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // 在這裏綁定標識符 } outer: } }

// 函數的執行上下文 // outer指向上一個執行上下文,這裏是指向全局的執行上下文 FunctionExectionContext = { ThisBinding = , // this綁定 LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 在這裏綁定標識符 } outer: } } ``` 請注意,執行上下文中包含this的綁定,也就是説,this指向是在執行上下文中的,但是需要注意的是,函數的this綁定是動態的,在執行的時候才真正綁定。

我們再來看一下變量環境

javascript GlobalExectionContext = { ThisBinding: <Global Object>, LexicalEnvironment: { // 詞法環境 EnvironmentRecord: { Type: "Object", // 在這裏綁定標識符 a: < uninitialized >, // let、const聲明的變量 b: < uninitialized >, // let、const聲明的變量 multiply: < func > // 函數聲明 } outer: <null> }, VariableEnvironment: { // 變量環境 EnvironmentRecord: { Type: "Object", // 在這裏綁定標識符 c: undefined, // var聲明的變量 } outer: <null> } } 如上圖説是,變量環境一般聲明上下文棧的VariableEnvironment屬性中。

以上的上下文棧你可以考察的題目參考如下:

  • 請問this指向問題,然後引申一下this具體在js環境中的哪裏
  • 然後引出上下文棧的概念,問變量環境和詞法環境有什麼區別,然後繼續問閉包,因為閉包跟上下文棧息息相關,為啥呢,看下文的閉包這一小節。

閉包

什麼是閉包呢? 概念太生硬,我們舉個例子來看 ```typescript const scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; }

const foo = checkscope(); foo(); ``` 如上,checkscope調用完畢,從函數調用棧或者上下文棧彈出,也就是銷燬了,按道理説,這個函數裏的變量也都全部銷燬了。

但是foo調用的時候,依然能訪問到scope這個變量,這就是閉包。

簡單來説就是函數已經從上下文棧彈出了,銷燬了,但是依然能訪問到之前銷燬的變量。這樣的函數就是閉包。

同時也解釋了,為啥我們説閉包跟作用域鏈息息相關了。

面試問題參考問題: - 之前講了上下文棧裏有一個outer屬性指向上一個上下文,這樣就可以形成一個作用域鏈,但實際上,作用域鏈保存在[[Scopes]]中,首先你知道[[Scopes]]是什麼嗎?其次為什麼會保存到這裏,而不是用上下文的outer去尋找。

答:如果單純的通過 outer 鏈路來實現作用域鏈,那麼存在一個閉包時,整個鏈條上的所有詞法環境,變量環境,this指向都無法回收。其實,這麼做也不是不行,那有沒有更好的方案呢, V8 優化了這一點。而是通過 [[Scopes]] + Closure 解決。也就是説在編譯函數時,把用到的詞法環境的數據放到[[Scopes]] 的 Closure對象裏,這樣就不用保存整個詞法環境了。

詳細原理參考這篇文章:https://juejin.cn/post/7079995358624874509

來個終極閉包難度面試題,摘自上文連接,想知道原理可以進去看 ```javascript let theThing = null; let replaceThing = function () { let leak = theThing; function unused () { if (leak){} };

theThing = {  
    longStr: new Array(1000000),
    someMethod: function () {

    }
};

};

let index = 0; while(index < 100){ replaceThing() index++; } ```

引用傳遞

js 中什麼類型是引用傳遞, 什麼類型是值傳遞? 如何將值類型的變量以引用的方式傳遞?

簡單點説, 對象是引用拷貝(引用拷貝跟引用傳遞有區別參考下面的案例), 基礎類型是值傳遞, 通過將基礎類型包裝 (boxing) 可以以引用的方式傳遞.

```javascript function changeStuff(a, b, c) { a = a * 10; b.item = "changed"; c = {item: "changed"}; }

var num = 10; var obj1 = {item: "unchanged"}; var obj2 = {item: "unchanged"};

changeStuff(num, obj1, obj2);

console.log(num); console.log(obj1.item); console.log(obj2.item);

// 10 // changed // unchanged ```

引用傳遞和值傳遞是一個非常簡單的問題, 也是理解 JavaScript 中的內存方面問題的一個基礎. 如果不瞭解引用可能很難去看很多問題.

內存釋放

JavaScript 中不同類型以及不同環境下變量的內存都是何時釋放?

引用類型是在沒有引用之後, 通過 v8 的 GC 自動回收, 值類型如果是處於閉包的情況下, 要等閉包沒有引用才會被 GC 回收, 非閉包的情況下,分新生代和老生代的內存區,新生代 (new space)是 切換的時候回收,老生代是標記清除。

與前端 Js 不同, 2年以上經驗的 Node.js 一定要開始注意內存了, 不説對 v8 的 GC 有多瞭解, 基礎的內存釋放一定有概念了, 並且要開始注意內存泄漏的問題了.

你需要了解哪些操作一定會導致內存泄漏, 或者可以崩掉內存. 比如如下代碼能否爆掉 V8 的內存?

javaScript let arr = []; while(true) arr.push(1); 可以,因為數組佔用的是V8的內存

然後上述代碼與下方的情況有什麼區別?

javaScript let arr = []; while(true) arr.push();

如果 push 的是 Buffer 情況又會有什麼區別?

javaScript let arr = []; while(true) arr.push(new Buffer(1000));

思考完之後可以嘗試找找別的情況如何爆掉 V8 的內存. 以及來聊聊內存泄漏?

javaScript function out() { const bigData = new Buffer(100); inner = function () { void bigData; } }

閉包會引用到父級函數中的變量,如果閉包未釋放,就會導致內存泄漏。上面例子是 inner 直接掛在了 root 上,從而導致內存泄漏(bigData 不會釋放)。

對於一些高水平的同學, 要求能清楚的瞭解 v8 內存 GC 的機制, 懂得內存快照等 (之後會在調試/優化的小結中討論) 了. 比如 V8 中不同類型的數據存儲的位置, 在內存釋放的時候不同區域的不同策略等等.

我可能會接着問,如何排查內存泄露呢?

如何排查內存泄露

詳見《如何分析 Node.js 中的內存泄漏》

這篇文章分析的相當到位,主要是有很清晰的案例,讓你很快就能初步掌握排查內存泄露的方法。

快照工具推薦使用 heapdump 用來保存內存快照,使用 devtool 來查看內存快照。

簡單來説,就是使用heapdump.writeSnapshot,來打印內存泄露前的情況,然後對比打印前和打印後哪些變量存儲的數據直線上漲,然後devtool有代碼連接,就可以仔細分析具體代碼了。

ES6 新特性

推薦閲讀阮一峯的 《ECMAScript 6 入門》

比較簡單的會問 letvar 的區別, 以及 箭頭函數function 的區別等等.

深入的話, es6 有太多細節可以深入了. 比如結合 引用 的知識點來詢問 const 方面的知識. 結合 {} 的使用與缺點來談 Set, Map 等. 比如私有化的問題與 symbol 等等.

其他像是 閉包是什麼? 這種問爛了問題已經感覺沒必要問了, 取而代之的是詢問閉包應用的場景更加合理. 比如説, 如果回答者通常使用閉包實現數據的私有, 那麼可以接着問 es6 的一些新特性 (例如 class, symbol) 能否實現私有, 如果能的話那為什麼要用閉包? 亦或者是什麼閉包中的數據/私有化的數據的內存什麼時候釋放? 等等.

... 的使用上, 如何實現一個數組的去重 (使用 Set 可以加分).

const 定義的 Array 中間元素能否被修改? 如果可以, 那 const 修飾對象有什麼意義?

其中的值可以被修改. 意義上, 主要保護引用不被修改 (如用 Map 等接口對引用的變化很敏感, 使用 const 保護引用始終如一是有意義的), 也適合用在 immutable 的場景.

暫時寫上這些, 之後會慢慢整理, 如果內容比較多可能單獨歸一類來討論.

補充問題

  • js 中什麼類型是引用傳遞, 什麼類型是值傳遞? 如何將值類型的變量以引用的方式傳遞?

答:簡單點説,對象是引用傳遞,基礎類型是值傳遞,通過將基礎類型boxing(包裝)可以以引用方式傳遞。

  • js 中, 0.1 + 0.2 === 0.3 是否為 true ? 在不知道浮點數位數時應該怎樣判斷兩個浮點數之和與第三數是否相等?

這是爛大街的題目了,沒啥難度,主要是理解10進制轉2進制時會有誤差,解決方法一般要用庫,比如big.js,具體源碼沒看,待大家留言溝通。

  • JavaScript 中不同類型以及不同環境下變量的內存都是何時釋放?

值類型一般在複雜情況是閉包引用,然後只有閉包的引用全部斷開才會回收。

引用類型的話,內存上分新生代和老生代。具體算法不一樣,新生代是交換空間,老生代是標記清除。

具體請參考我的文章NodeJS有難度的面試題,你能答對幾個?,其中有一小節講的是垃圾回收機制