挑戰一輪大廠後的面試總結 (含六個方向) - javascript 篇(萬字長文)

在去年底開始換工作,直到現在算是告了一個段落,斷斷續續的也面試了不少公司,現在回想起來,那段時間經歷了被面試官手撕,被筆試題狂懟,悲傷的時候差點留下沒技術的淚水。

這篇文章我打算把我找工作遇到的各種面試題(每次面試完我都會總結)和我自己複習遇到比較有意思的題目,做一份彙總,年後是跳槽高峯期,也許能幫到一些小夥伴。

先說下這些題目難度,大部分都是基礎題,因爲這段經歷給我的感覺就是,不管你面試的是高級還是初級,基礎的知識一定會問到,甚至會有一定的深度,所以基礎還是非常重要的。

我將根據類型分爲幾篇文章來寫:

面試總結:javascript 面試點彙總(萬字長文)(已完成) 強烈大家看看這篇,面試中 js 是大頭

面試總結:nodejs 面試點彙總(已完成)

面試總結:瀏覽器相關 面試點彙總(已完成)

面試總結:css 面試點彙總(已完成)

面試總結:框架 vue 和工程相關的面試點彙總(已完成)

面試總結:面試技巧篇(已完成)

六篇文章都已經更新完啦~

這篇文章是對 javascript 相關的題目做總結,內容有點長,大致算了下,有接近 2W 字,推薦用電腦閱讀,歡迎朋友們先收藏在看。

先看看目錄(這長圖在手機上比較模糊,可點擊圖片看大圖)

Q:介紹下原型鏈

原型鏈這東西,基本上是面試必問,而且不是知識點還都是基於原型鏈擴展的,所以我們先把原先鏈整明白。
我們看一張網上非常流行的圖
js-object-layout
嗯,箭頭有點多且有點繞,沒關係,我們可逐步分析,我們從結果倒推結論,這樣更直觀些,看代碼

function person() {
  this.name = 10
}
person.prototype.age = 10
const p = new person()

分析構造函數

我們通過斷點看下 person 這個函數的內容

prototype

它是一個自定義的函數類型,看關鍵的兩個屬性 prototype__proto__ ,我們一一分析

  1. prototype 分析

prototype 展開看,是個自定義的對象,這個對象有三個屬性 age constructor __proto__
age 的值是 10 ,那麼可以得出通過person.prototype 賦值的參數都是在 prototype 這個對象中的。

點開 constructor,發現這個屬性的值就是指向構造器 preson 函數,其實就是循環引用,這時候就有點套娃的意思了

constructor

那麼,根據字面意思, prototype 可以翻譯成,原先對象,用於擴展屬性和方法。

  1. __proto__ 分析
    __proto__ 展開看看

prop

person 中的 __proto__ 是一個原始的 function 對象,在 function 對象中,又看到了 __proto__ 這個屬性,這時候它的值是原始的 Object 對象,在 Object 對象中又再次發現了 __proto__ 屬性,這時候 __proto__ 等於 null

js 中數據類型分爲兩種,基本類型和對象類型,所以我們可以這麼猜測,person 是一個自定義的函數類型,它應該是屬於函數這一家族下的,對於函數,我們知道它是屬於對象的,那麼它們幾個是怎麼關聯起來的呢?

沒錯,就是通過 __proto__ 這個屬性,而由這個屬性組成的鏈,就叫做原型鏈。

根據上面的例子我們,可得出,原型鏈的最頂端是 null ,往下是 Object 對象,而且只要是對象或函數類型都會有 __proto__ 這個屬性,畢竟大家都是 js-family 的一員嘛。

分析生成的對象

上面我們已經知道了原型和原型鏈,那麼對於 new 出來的對象,它們的關係又是怎麼樣的呢?繼續斷點分析

p

p 對象中有個 __proto__ 屬性,我們已經知道這是個原型鏈,通過它可以找到我們的祖先,展開 __proto__ ,大家看到這裏有沒有發現很眼熟,在看一張圖,

p1

沒錯!p.__proto__ 就是 person 函數的 prototype ,這一步也就是 new 的核心點(下個題目我們會說到)。

那麼 p 這實例的原型鏈是怎麼樣的?
p.__proto__ => {constructor:func}.__proto__ => Object => null

proto-link

對於實例對象來說,原先鏈主要用來做什麼呢?

  • 實現繼承:如果沒有原型鏈,每個對象就都是孤立的,對象間就沒有關聯,所以原型鏈就像一顆樹幹,從而可以實現面對對象中的繼承
  • 屬性查找:首先在當前實例對象上查找,要是沒找到,那麼沿着 __proto__ 往上查找
  • 實例類型判斷:判斷這個實例是否屬於某類對象

還有就是,光看文字的解釋還是有點費解的,要想深入理解,還是需要多動手斷點調試,才能很快理順。

若還是不太理解實例對象的原型鏈關係,可以看下一題:解釋構造函數

Q:介紹下構造函數是什麼?

構造函數與普通函數在編碼上沒有區別,只要可以通過 new 來調用的就是構造函數。

那麼什麼函數不可以作爲構造函數呢?

箭頭函數不可以作爲構造函數。

new 是一個語法糖,對執行的原理一步步拆分並自己寫一個模擬 new 的函數:
0. 自定義一個 objectFactory 模擬 new 語法糖,函數可以接受多個參數,但要求第一個參數必須爲構造函數

  1. 創建一個空對象 obj ,分配內存空間

  2. 從參數列表中獲取構造函數,並將 obj__proto__ 屬性指向構造函數的 prototype

  3. 通過 apply 執行構造,並將當前 this 的指向改爲 obj

  4. 返回構造函數的執行結果,或者當前的 obj 對象

function objectFactory() {
  var obj = {},
  Constructor = [].shift.call(arguments);
  obj.__proto__ = Constructor.prototype;
  var ret = Constructor.apply(obj, arguments);
  return typeof ret === 'object' ? ret : obj;
};
function fnf() {
    this.x = 123
}
let a2 = objectFactory(fnf) // 模擬 new fnf()
console.log(a2.x) // 123

可看出並不複雜,關鍵點在第二步,設置對象的原型鏈,這也是創建實例對象的核心點。

Q:typeof 和 instanceof 有什麼區別

js 中數據類型分爲兩類,一類是基本數據類型,一類是對象類型。

基本數據類型有:Number String Boolean Null Undefined BigInt Symbol

對象類型: Object 也叫引用類型

  1. typeof(a) 用於返回值的類型,有 “number”、“string”、“boolean”、“null”、“function” 和 “undefined”、“symble”、“object”
let a = 1
let a1 = '1'
let a2 = true
let a3 = null
let a4 = undefined
let a5 = Symbol
let a6 = {}
console.log(typeof(a),typeof(a1),typeof(a2),typeof(a3),typeof(a4),typeof(a5),typeof(a6))
// number string boolean object undefined function object
  1. instanceof 用於判斷該對象是否是目標實例,根據原型鏈 __proto__ 逐層向上查找,通過 instanceof 也可以判斷一個實例是否是其父類型或者祖先類型的實例。

有這麼個面試題

function person() {
    this.name = 10
}
console.log(person instanceof person)

結果是 false ,看下 person 函數的原型鏈 person.proto_ => Function.proto=> Object.proto=> null ,所以在原型鏈上是找不到 person

Q:數據類型有哪幾種?

  • 7 種原始數據類型: Null Undefined String Number Boolean BigInt Symbol
  • Object 對象類型,也稱爲引用類型

Q:JS中基本數據類型和引用類型在內存上有什麼區別?

基本類型:存儲在棧內存中,因爲基本類型的大小是固定,在棧內可以快速查找。

引用類型:存儲在堆內存中,因爲引用類型的大小是不固定的,所以存儲在堆內存中,然後棧內存中僅存儲堆中的內存地址。

我們在查找對象是從棧中查找,那麼可得知,對於基本對象我們是對它的值進行操作,而對於引用類型,我們是對其引用地址操作。

var name = 'xiaoming'
var name1 = name; // 值拷貝
var obj = {age:10}
var obj1 = obj // 引用地址的拷貝,所以這兩個對象指向同一個內存地址,那麼他們其實是同一個對象

stack-heap

關於函數的傳參是傳值還是傳引用呢?

很多人說基本類型傳值,對象類型傳引用,但嚴格來說,函數參數傳遞的是值,上圖可以看出,就算是引用類型,它在棧中存儲的還是一串內存地址,所以也是一個值。不過我覺得沒必要過於糾結這句話,理解就行。

Q:描述 NaN 指的是什麼

NaN 屬性是代表非數字值的特殊值,該屬性用於表示某個值不是數字。

NaN 是 Number 對象中的靜態屬性

typeof(NaN) // "number"
NaN == NaN // false

那怎麼判斷一個值是否是 NAN 呢? 若支持 es6 ,可直接使用 Number.isNaN()

若不支,可根據 NAN !== NAN 的特性

function isReallyNaN(val) {
    let x = Number(val);
    return x !== x;
}

Q:描述 null

null 是基本類型之一,不是 Object 對象,至於爲什麼?答曰:歷史原因,咱也不敢多問

typeof(null) // "object"
null instanceof Object // false

那怎麼判斷一個值是 null 呢?可根據上面描述的特性,得

function isNull(a) {
    if (!a && typeof (a) === 'object') {
        return true
    }
    return false
}
console.log(isNull(0))    // false
console.log(isNull(false))// false
console.log(isNull(''))   // false
console.log(isNull(null)) // true

Q:什麼是包裝對象

包裝對象,只要是爲了便於基本類型調用對象的方法。

包裝對象有三種:String Number Boolean

這三種原始類型可以與實例對象進行自動轉換,可把原始類型的值變成(包裝成)對象,比如在字符串調用函數時,引擎會將原始類型的值轉換成只讀的包裝對象,執行完函數後就銷燬。

Q:class 和 function 的區別

class 也是一個語法糖,本質還是基於原型鏈,class 語義化和編碼上更加符合面向對象的思維。

對於 function 可以用 call apply bind 的方式來改變他的執行上下文,但是 class 卻不可以,class 雖然本質上也是一個函數,但在轉成 es5 (babel)做了一層代理,來禁止了這種行爲。

  • class 中定義的方法不可用 Object.keys() 遍歷
  • class 不可以定義私有的屬性和方法, function 可以,只要不掛載在 this 作用域下就行
  • class 只能通過類名調用
  • class 的靜態方法,this 指向類而非實例

Q:實現繼承的幾種方法

因爲涉及的代碼較多,所以獨立寫一篇文章來總結,傳送門: js-實現繼承的幾種方式

Q:談談作用域鏈機制

先說下作用域的這個概念,作用域就是變量和函數的可訪問範圍,控制這個變量或者函數可訪問行和生命週期(這個很重要)。

在 js 中是詞法作用域,意思就是你的變量函數的作用域是由你的編碼中的位置決定的,當然可以通過 apply bind 等函數進行修改。

在 ES6 之前,js 中的作用域分爲兩種:函數作用域和全局作用域。

全局作用域顧名思義,瀏覽器下就是 window ,作用域鏈的頂級就是它,那麼只要不是被函數包裹的變量或者函數,它的作用域就是全局。

而函數作用域,就是在函數的體內聲明的變量、函數及函數的參數,它們的作用域都是在這個函數內部。
那麼函數中的未在該函數內定義的變量呢?這個變量怎麼獲取呢?這就是作用域鏈的概念了。

我們知道函數在執行時是有個執行棧,在函數執行的時候會創建執行環境,也就是執行上下文,在上下文中有個大對象,保存執行環境定義的變量和函數,在使用變量的時候,就會訪問這個大對象,這個對象會隨着函數的調用而創建,函數執行結束出棧而銷燬,那麼這些大對象組成一個鏈,就是作用域鏈。

那麼函數內部未定義的變量,就會順着作用域鏈向上查找,一直找到同名的屬性。

看下面這個栗子

var a = 10;
function fn() {
  var b = 20;
  function bar() {
    console.log(a + b) // a 一直往上找,直到最高層級找到了, b 往上找,在函數 fn 這一層級的上下文中找到了 b=20 ,就沒有繼續往上找
  }
  return bar
}
b = 200;
var x = fn();
x()

在看看閉包的作用域,只要存在函數內部調用,執行棧中就會保留父級函數和函數對於的作用域,所以父函數的作用域在子函數的作用域鏈中,直到子函數被銷燬,父級作用域纔會釋放,來個很常見的面試題

function test() {
    for (var index = 0; index < 3; index++) {
        setTimeout(() => {
            console.log('index:' + index)
        })
    }
}

test() 
//  index:3
//  index:3
//  index:3

執行結果是 3個3,因爲js的事件循環機制,就不細說,那麼我們想讓它按順序輸出,咋辦呢?

思路就是,因爲定時器的回調肯定是在循環結束後才執行,那時候 index 已經是3了,那麼可以利用上面說的閉包中的作用域鏈,在子函數中去引用父級的變量,這樣子函數沒有被銷燬前,這個變量是會一直存在的,所以我們可以這麼改。

function test() {
    for (var index = 0; index < 3; index++) {
        ((index) => {
            setTimeout(() => {
                console.log('index:' + index)
            })
        })(index)
    }
}

我們在看一道面試題

function f(fn, x) {
    console.log('into')
    if (x < 1) {
        f(g, 1);
    } else {
        fn();
    }
    function g() {
        console.log('x' + x);
    }
}

function h() {
}

f(h, 0) // x 0

邏輯很簡單,但面試題就是這麼鬼精,越是簡單越有坑。

g 函數中的 x 變量是引用父級的,而 f 函數執行了兩次,x 變量依次爲 0 1,在 f(h,0) 這個函數執行的時候,這個函數的作用域中的 x=0,這個時候 g 函數中引用的 x 就是當前執行上下文中的 x=0 這個變量,但這個函數還沒被執行,接着到了 f(g, 1) 執行,這一層執行上下文中的 x=1 ,但注意兩次f執行的作用域不是同一個對象,是作用域鏈上兩個獨立的對象,最後到了 fn() ,這個fn是一個參數,也就是在 f(h,0) 執行的時候 g 函數,那麼 g 函數在這裏被執行,g 打印出來的 x 就是 0 。

塊級作用域: let const 的出現就是爲了解決 js 中沒有塊級作用域的弊端。

其他小點:

  • for循環還有一個特別之處,就是設置循環變量的那部分是一個父作用域,而循環體內部是一個單獨的子作用域
  • 函數中的變量可以分爲自由變量(當前作用域沒有定義的變量)和本作用域變量,自由變量的取值要到創建這個函數的那個域(非常重要),也叫做靜態作用域。
  • 作用域和執行上下文的區別,看下引擎執行腳本的兩個階段

解釋階段: 詞法分析 -> 語法分析 -> 作用域規則確定
執行階段: 創建執行上下文 -> 執行函數代碼 -> 垃圾回收

參考連接:
https://segmentfault.com/a/1190000018513150
https://www.cnblogs.com/dolphinX/p/3280876.html

Q:let var const 的區別

var: 解析器在對js解析時,會將腳本掃描一遍,將變量的聲明提前到代碼塊的頂部,賦值還是在原先的位置,若在賦值前調用,就會出現暫時性死區,值爲 undefined

let const:不存在在變量提升,且作用域是存在於塊級作用域下,所以這兩個的出現解決了變量提升的問題,同時引用塊級作用域。
注:變量提升的原因是爲了解決函數互相調用的問題。

Q:數據屬性和訪問器屬性的區別

其實就是問對 Object.defineProperty 的掌握程度。

  1. 數據屬性(數據描述符)

相關的屬性如下:

[[Configurable]]:表示能否通過 delete 刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改爲訪問器屬性。

[[Enumerable]]:表示能否通過 for-in 循環返回屬性。

[[Writable]]:表示能否修改屬性的值。

[[Value]]:包含這個屬性的值。讀取屬性值的時候,從這個位置讀;寫入屬性值的時候,把新值保存在這個位置。這個特性的默認值爲 undefined。

數據屬性可以直接定義,如 var p = {name:'xxx'} 這個 name 就是數據屬性,直接定義下,相關屬性值都是 true ,如果要修改默認的定義值,那麼使用 Object.defineProperty() 方法,如下面這個栗子

var p = {
    name:'dage'
}
Object.defineProperty(p,'name',{
    value:'xxx'
})
p.name = '4rrr'
console.log(p.name) // 4rrr
Object.defineProperty(p,'name',{
    writable:false,
    value:'again'
})
p.name = '4rrr'
console.log(p.name) // again
  • 調用Object.defineProperty()方法時,如果不顯示指定configurable,enumerable,writable的值,就默認爲false
  • 如果 writable 爲 false,但是 configurable 爲 true,還是可以對屬性重新賦值的。
  1. 訪問器屬性(存取描述符)

訪問器屬性不包含數據值,沒有 value 屬性,有 get set 屬性,通過這兩個屬性來對值進行自定義的讀和寫,可以理解爲取值和賦值前的攔截器,相關屬性如下:

[[Configurable]]:表示能否通過 delete 刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改爲數據屬性,默認 false

[[Enumerable]]:表示能否通過 for-in 循環返回屬性,默認 false

[[Get]]:在讀取屬性時調用的函數。默認值爲 undefined

[[Set]]:在寫入屬性時調用的函數。默認值爲 undefined

  • 訪器屬性不能直接定義,必須使用 Object.defineProperty() 來定義。
  • 根據 get set 的特性,可以實現對象的代理, vue 就是通過這個實現數據的劫持。

兩者的相同點:都有 Configurable 和 Enumerable 屬性。

一個簡單的小demo

var p = {
    name:''
}
Object.defineProperty(p,'name',{
    get:function(){
        return 'right yeah !'
    },
    set:function(val){
        return 'handsome '+val
    }
})
p.name = `xiaoli`
console.log(p.name) // right yeah !

參考連接:

https://cloud.tencent.com/developer/article/1345012

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

Q:toString 和 valueOf 有什麼區別

在 Object 中存在這個兩個方法,繼承Object的對象可以重寫方法。這兩個方法主要用於隱式轉換,比如

js 不同於其他語言,兩個不同的數據類型可以進行四則運算和判斷,這就歸功於隱式轉換了,隱式轉換我就不詳細介紹了,因爲我沒有被問到~

1 + '1' // 11 :整型 1 被轉換成字符串 '1',變成了 '1' + '1' = '11'
2 * '3' // 6  :字符串 '3' 被轉換成整型 3 ,變成了 2 * 3 = 6

那麼我們也可以對自定義的對象重寫這兩個函數,以便進行隱式轉換

let o = function () {
    this.toString = () => {
        return 'my is o,'
    }
    this.valueOf = () => {
        return 99
    }
}
let n = new o()
console.log(n + 'abc') // 99abc
console.log(n * 10) // 990
// 有沒有很酷炫

當這兩個函數同時存在時候,會先調用 valueOf ,若返回的不是原始類型,那麼會調用 toString 方法,如果這時候 toString 方法返回的也不是原始數據類型,那麼就會報錯 TypeError: Cannot convert object to primitive value 如下

let o = function () {
    this.toString = () => {
        console.log('into toString')
        return { 'string': 'ssss' }
    }
    this.valueOf = () => {
        console.log('into valueOf')
        return { 'val': 99 }
    }
}
let n = new o()
console.log(n + 'xx')
//into valueOf
//into toString
// VM1904:12 Uncaught TypeError: Cannot convert object to primitive value

Q:箭頭函數有沒有 arguments 對象?

(非常感謝評論區夥伴的提醒)

arguments 是一個類數組對象,可以獲取到參數個數和參數列表數組,對於不定參數的函數,可以用 arguments 獲取參數。

那麼對於箭頭函數有沒有 arguments 呢? 需要看具體執行的場景了

// 箭頭函數
let aa1 = (...args) => {
    let bb = [].slice.call(arguments, 0)
    let a = arguments[0]
    let b = arguments[1]
    let c = arguments[2]
    console.log(a + b + c)
}

// 正常的函數
let aa = function (...args) {
    let bb = [].slice.call(arguments, 0)
    let a = arguments[0]
    let b = arguments[1]
    let c = arguments[2]
    console.log(a + b + c)
}
aa(1, 2, 3)
aa1(1, 2, 3)

分別觀察以下兩個場景的執行結果

瀏覽器中執行

直接看結果

func1

很明顯,在瀏覽器中 arguments 是不存在的

nodejs 中執行

結果(爲了辨認,輸出前加了段字符串)

func2

執行過程沒有報錯,說明 arguments 是存在的,那爲啥結果不是預期的 6 呢?

我們對箭頭函數打斷點看看

func3

arguments 對象看着沒啥問題,傳入的參數也看到了

我們看看通過數組方式獲取到的值

func4

竟然是這些東西,這些是當前腳本執行的模塊信息,並不是我們預期的參數列表

結論

  1. 在瀏覽器中箭頭函數沒有 arguments
  2. 在 nodejs 中,有 arguments ,可通過其獲取參數長度,但不能通過改對象獲取參數列表

(我也不太懂這個對象的原理,還請知道的夥伴在評論區告知,謝謝)

Q:js 精度丟失問題

浮點數的精度丟失不僅僅是js的問題, java 也會出現精度丟失的問題(沒有黑java),主要是因爲數值在內存是由二進制存儲的,而某些值在轉換成二進制的時候會出現無限循環,由於位數限制,無限循環的值就會採用“四捨五入法”截取,成爲一個計算機內部很接近數字,即使很接近,但是誤差已經出現了。

舉個栗子

0.1 + 0.2  = 0.30000000000000004
// 0.1 轉成二進制會無限循環
// "0.000110011001100110011001100110011001100110011001100..."

那麼如何避免這問題呢?解決辦法:可在操作前,放大一定的倍數,然後再除以相同的倍數

(0.1 *100 + 0.2*100) / 100 = 0.3

js 的 number 採用 64位雙精度存儲
JS 中能精準表示的最大整數是 Math.pow(2, 53)

推薦一個開源工具 (number-precision)[https://github.com/nefe/number-precision]

Q: toFixed 可以做到四捨五入嗎

toFixed 對於四捨六入沒問題,但對於尾數是 5 的處理就非常詭異

(1.235).toFixed(2) // "1.24" 正確
(1.355).toFixed(2)   // "1.35" 錯誤

我也沒明白爲啥這麼設計,嚴格的四捨五入可以採用以下函數

// 使用 Math.round 可以四捨五入的特性,把數組放大一定的倍數處理
function round(number, precision) {
    return Math.round(+number + 'e' + precision) / Math.pow(10, precision);
}

原理是,Math.round 是可以做到四捨五入的,但是僅限於正整數,那麼我們可以放大至保留一位小數,計算完成後再縮小倍數。

Q: js中不同進制怎麼轉換

10 進制轉其他進制:Number(val).toString([2,8,10,16])

其他進制轉成10進制:Number.parseInt("1101110",[2,8,10,16])

其他進制互轉:先將其他進制轉成 10 進制,在把 10 進制轉成其他進制

Q:對js處理二進制有了解嗎

ArrayBuffer: 用來表示通用的、固定長度的原始二進制數據緩衝區,作爲內存區域,可以存放多種類型的數據,它不能直接讀寫,只能通過視圖來讀寫。

同一段內存,不同數據有不同的解讀方式,這就叫做“視圖”(view),視圖的作用是以指定格式解讀二進制數據。目前有兩種視圖,一種是 TypedArray 視圖,另一種是 DataView 視圖,兩者的區別主要是字節序,前者的數組成員都是同一個數據類型,後者的數組成員可以是不同的數據類型。

Blob: 也是存放二進制的容器,通過 FileReader 進行轉換。

之前有做過簡單的總結,大家可以看看:nodejs 二進制與Buffer

畢竟對這塊應用的比較少,推薦一篇文章給大家 二進制數組

Q:異步有哪些解決方案

這個問題出場率很高呀!常見的有如下幾個:

  • 回調函數:通過嵌套調用實現
  • Generator: 異步任務的容器,生成器本質上是一種特殊的迭代器, Generator 執行後返回的是個指針對象,調用對象裏的 next 函數,會移動內部指針,分階段執行 Generator 函數 ,指向 yield 語句,返回一個對象 {value:當前的執行結果,done:是否結束}
  • promise: 而是一種新的語法糖, Promise 的最大問題是代碼冗餘,通過 then 傳遞執行權,因爲需求手動調用 then 方法,當異步函數多的時候,原來的語義變得很不清楚
  • co: 把 Generator 和 Promise 封裝,達到自動執行
  • async\await: 目前是es7草案,可通過 bable webpack 等工具提前使用,目前原生瀏覽器支持還不太好。其本質上是語法糖,跟 co 庫一樣,都是對 generator+promise 的封裝,不過相比 co ,語義化更好,可以像普通函數一樣調用,且大概率是未來的趨勢。

Q:簡單介紹Generator

Generator 函數就是一個封裝的異步任務,或者說是異步任務的容器。

Generator 的核心是可以暫停函數執行,然後在從上一次暫停的位置繼續執行,關鍵字 yield 標識暫停的位置。

Generator 函數返回一個迭代器對象,並不會立即執行函數裏面的方法,對象中有 next() 函數,函數返回 value 和 done 屬性,value 屬性表示當前的內部狀態的值,done 屬性標識是否結束的標誌位。

Generator 的每一步執行是通過調用 next() 函數,next 方法可以帶一個參數,該參數就會被當作上一個yield表達式的返回值。
執行的步驟如下:

(1)遇到 yield 表達式,就暫停執行後面的操作,並將緊跟在 yield 後面的那個表達式的值,作爲返回的對象的 value 屬性的值。

(2)下一次調用 next 方法時,再繼續往下執行,直到遇到下一個 yield 表達式。

(3)如果沒有再遇到新的 yield 表達式,就一直運行到函數結束,直到 return 語句爲止,並將 return 語句後面的表達式的值,作爲返回的對象的value屬性值。

(4)如果該函數沒有 return 語句,則返回的對象的 value 屬性值爲 undefined。
注意: yield 表達式,本身是沒有值的,需要通過 next() 函數的參數將值傳進去。

let go = function* (x) {
    console.log('one', x)
    let a = yield x * 2
    console.log('two', a)
    let b = yield x + 1
    sum = a + b
    return sum
}
let g = go(10)
let val = g.next()
while (!val.done) {
    val = g.next(val.value)
}
console.log(val)

可見 Generator 的弊端很明顯,執行流程管理不方便,異步返回的值需要手動傳遞,編碼上較容易出錯。

Q: 講一講 Promise

Promise 已經是 ES6 的規範了,相比 Generator ,設計的更加合理和便捷。

看看Promise的規範:

  1. 一個 Promise 的當前狀態必須爲以下三種狀態中的一種:等待態(Pending)、執行態(Fulfilled)和拒絕態(Rejected),狀態的改變只能是單向的,且變化後不可在改變。
  2. 一個 Promise 必須提供一個 then 方法以訪問其當前值、終值和據因。 promise.then(onFulfilled, onRejected) 回調函數只能執行一次,且返回 promise 對象

promise 的每個操作返回的都是 promise 對象,可支持鏈式調用。通過 then 方法執行回調函數,Promise 的回調函數是放在事件循環中的微隊列。

Q: co庫的執行原理

co 用 promise 的特性,將 Generator 包裹在 Promise 中,然後循環執行 next 函數,把 next 函數返回的的 value 用 promise 包裝,通過 then.resolve 調用下一個 next 函數,並將值傳遞給 next 函數,直到 done 爲 true,最後執行包裹 Generator 函數的 resolve。

我們看下源碼,源碼做了截取

function co(gen) {
    return new Promise(function(resolve, reject) { // 最外層是一個 Promise 對象
      if (typeof gen === 'function') gen = gen.apply(ctx, args);
      if (!gen || typeof gen.next !== 'function') return resolve(gen);
  
      onFulfilled();
  
      function onFulfilled(res) {
        var ret;
        try {
          ret = gen.next(res); // 將上一步的返回值傳遞給 next
        } catch (e) {
          return reject(e);
        }
        next(ret); // 將上一步執行結果轉換成 promise
        return null;
      }
  
      /**
       * Get the next value in the generator,
       * return a promise.
       *
       * @param {Object} ret
       * @return {Promise}
       * @api private
       */
  
      function next(ret) {
        if (ret.done) return resolve(ret.value); // done爲true,就表示執行結束,resolve結果出去
        var value = toPromise.call(ctx, ret.value); // toPromise 是個工具函數,將對象轉換成 promise,可以理解返回的 value 就是 promise
        if (value && isPromise(value)) return value.then(onFulfilled, onRejected); // then 函數執行回調 onFulfilled
        return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, ' // 異常處理
          + 'but the following object was passed: "' + String(ret.value) + '"'));
      }
    });
  }

Q:介紹下瀏覽器的事件循環

這是必考題呀,盆友們,這個可閱讀我以前寫的一篇文章,傳送門: js 事件循環

Q:介紹下模塊化方案

這個東西有點多,可以看我之前的一篇總結,傳送門:面試官讓我解釋前端模塊化

Q:垃圾回收機制

爲什麼需要垃圾回收:因爲對象需要佔用內存,而內存資源是有限的。

js 會週期性的對不在使用的對象銷燬,釋放內存,關鍵點就在於怎麼識別哪些對象是垃圾。

垃圾對象:對象沒有被引用,或者幾個對象形成循環引用,但是根訪問不到他們,這些都是可回收的垃圾。

垃圾回收的兩種機制:標記清除和引用計數

標記清除法

垃圾收集器在運行的時候會給存儲在內存中的所有變量都加上標記,然後,它會去掉環境中的變量以及被環境中的變量引用的標記,而在此之後再被加上標記的變量將被視爲準備刪除的變量,原因是環境中的變量已經無法訪問到這些變量了。

最後。垃圾收集器完成內存清除工作,銷燬那些帶標記的值,並回收他們所佔用的內存空間。

比如說函數中聲明瞭一個變量,就做一個標記,當函數執行完成,退出執行棧,這個變量的標記就變成已使用完。

目前主流瀏覽器採用的是這個策略

引用計數

跟蹤每個值被引用的次數,聲明一個變量後,這個變量每被其他變量引用一次,就加 1 ,如果變量引用釋放了,就減 1,當引用次數爲 0 的時候,對象就被清理。但這個有個循環引用的弊端,所以應用的比較少。

垃圾收集的性能優化

  1. 分代回收,對象分成兩組,新生帶、老生帶,
  2. 增量回收
  3. 空閒時間回收

編碼可以做的優化

  1. 避免重複創建對象。
  2. 在適當的時候解除引用,是爲頁面獲的更好性能的一個重要方式。
  3. 全局變量什麼時候需要自動釋放內存空間則很難判斷,因此在開發中,需要儘量避免使用全局變量。

Q:什麼是嚴格模式

通過在腳本的最頂端放上一個特定語句 "use strict"; 整個腳本就可開啓嚴格模式語法。

嚴格模式下有以下好處:

  1. 消除Javascript語法的一些不合理、不嚴謹之處,減少一些怪異行爲;
  2. 消除代碼運行的一些不安全之處,保證代碼運行的安全;
  3. 提高編譯器效率,增加運行速度;
  4. 爲未來新版本的Javascript做好鋪墊。

如以下具體的場景:

  1. 嚴格模式會使引起靜默失敗(silently fail,注:不報錯也沒有任何效果)的賦值操作拋出異常
  2. 嚴格模式下的 eval 不再爲上層範圍(surrounding scope,注:包圍eval代碼塊的範圍)引入新變量
  3. 嚴格模式禁止刪除聲明變量
  4. 在嚴格模式中一部分字符變成了保留的關鍵字。這些字符包括implements, interface, let, package, private, protected, public, static和yield。在嚴格模式下,你不能再用這些名字作爲變量名或者形參名。
  5. 嚴格模式下 arguments 和參數值是完全獨立的,非嚴格下修改是會相互影響的

Q:map 和 weekMap 的區別

mapkey 可以是任意類型,在 map 內部有兩個數組,分別存放 keyvalue ,用下標保證兩者的一一對應,在對 map 操作時,內部會遍歷數組,時間複雜度O(n),其次,因爲數組會一直引用每個鍵和值,回收算法沒法回收處理,可能會導致內存泄露。

相比之下, WeakMap 的鍵值必須是對象,持有的是每個鍵對象的 弱引用 ,這意味着在沒有其他引用存在時垃圾回收能正確進行。

const wm1 = new WeakMap();
const o1 = {};
wm1.set(o1, 37);  // 當 o1 對象被回收,那麼 WeakMap 中的值也被釋放

Q:String 和 Array 有哪些常用函數

我也不知道爲什麼會有這種筆試題…

  1. String:

split(): 方法使用指定的分隔符字符串將一個String對象分割成子字符串數組

slice(): 方法提取某個字符串的一部分,並返回一個新的字符串,且不會改動原字符串

substring(): 方法返回一個字符串在開始索引到結束索引之間的一個子集, 或從開始索引直到字符串的末尾的一個子集

  1. Array:

slice(): 方法返回一個新的數組對象,這一對象是一個由 begin 和 end 決定的原數組的淺拷貝(包括 begin,不包括end)。原始數組不會被改變。

splice(): 方法通過刪除或替換現有元素或者原地添加新的元素來修改數組,並以數組形式返回被修改的內容。此方法會改變原數組。

push(): 方法將一個或多個元素添加到數組的末尾,並返回該數組的新長度。

pop(): 方法從數組中刪除最後一個元素,並返回該元素的值。此方法更改數組的長度。

shift():方法從數組中刪除第一個元素,並返回該元素的值。此方法更改數組的長度。

unshift(): 方法將一個或多個元素添加到數組的開頭,並返回該數組的新長度(該方法修改原有數組)。

Q:判斷數組的幾種方法

這題主要還是考察對原型鏈的理解

  1. Array.isArray() ES6 api
  2. obj instanceof Array 原型鏈查找
  3. obj.constructor === Array 構造函數類型判斷
  4. Object.prototype.toString.call(obj) === '[object Array]' toString
    返回表示該對象的字符串,若這個方法沒有被覆蓋,那麼默認返回 "[object type]" ,其中 type 是對象的類型。需要準確判斷類型的話,建議使用這種方法

Q:循環有幾種方式,是否支持中斷和默認情況下是否支持async/await

  • for 支持中斷、支持異步事件
  • for of 支持中斷、支持異步事件
  • for in 支持中斷、支持異步事件
  • forEach 不支持中斷、不支持異步事件
  • map 不支持中斷、不支持異步事件,支持異步處理方法:map 返回promise數組,在使用 Promise.all 一起處理異步事件數組
  • reduce 不支持中斷、不支持異步事件,支持異步處理方法:返回值返回 promise 對象

map 的比較簡單就不寫了,我寫個 reduce 處理 async/await 的 demo

const sleep = time => new Promise(res => setTimeout(res, time))
async function ff(){
    let aa = [1,2,3]
    let pp = await aa.reduce(async (re,val)=>{
        let r = await re;
        await sleep(3000)
        r += val;
        return Promise.resolve(r)
    },Promise.resolve(0))
    console.log(pp) // 6
}
ff()

Q:閉包的使用場景列舉

閉包:定義在一個函數內部的函數,內部函數持有外部函數內變量的引用,這個內部的函數有自己的執行作用域,可以避免外部污染。

關於閉包的理解,可以說是一千個讀者就有一千個哈姆雷特,找到適合自己理解和講述的就行。

場景有:

  1. 函數式編程,compose curry
  2. 函數工廠、單利
  3. 私有變量和方法,面向對象編程

Q:擴展運算符

這題面試官估計是想知道你是不是真的用過 es6 吧

擴展運算符(…)也會調用默認的 Iterator 接口。

擴展運算符主要用在不定參數上,可以將參數轉成數組形式

function fn(...arg){
    console.log(arg) // [ 1, 2, 3 ]
}
fn(1,2,3)

Q:線程和進程分別是什麼

首先來一句話概括:進程和線程都是一個時間段的描述,都是對CPU工作時間段的描述。

當一個任務得到 CPU 資源後,需要加載執行這個任務所需要的執行環境,也叫上下文,進程就是包含上下文切換的程序執行時間總和 = CPU加載上下文 + CPU執行 + CPU保存上下文。可見進程的顆粒度太大,每次都需要上下文的調入,保存,調出。

如果我們把進程比喻爲一個運行在電腦上的軟件,那麼一個軟件的執行不可能是一條邏輯執行的,必定有多個分支和多個程序段,就好比要實現程序A,實際分成 a,b,c等多個塊組合而成。

那麼這裏具體的執行就是:程序A得到CPU => CPU加載上下文 => 開始執行程序A的a小段 => 然後執行A的b小段 => 然後再執行A的c小段 => 最後CPU保存A的上下文。這裏a,b,c 的執行共享了A的上下文,CPU在執行的時候沒有進行上下文切換的。

a,b,c 我們就是稱爲線程,就是說線程是共享了進程的上下文環境,是更爲細小的 CPU 執行時間段。

cpu

Q:瞭解函數式編程嗎

函數式編程的兩個核心:合成和柯里化,之前對函數式編程做過總結,傳送門:【面試官問】你懂函數式編程嗎?

Q:什麼是尾遞歸?

先給面試官簡單說下什麼是遞歸函數:函數內部循環調用自身的就是遞歸函數,若函數沒有執行完畢,執行棧中會一直保持函數相關的變量,一直佔用內存,當遞歸次數過大的時候,就可能會出現內存溢出,也叫爆棧,頁面可能會卡死。
所以爲了避免出現這種情況,可以採用尾遞歸。

尾遞歸:在函數的最後一步是調用函數,進入下一個函數不在需要上一個函數的環境了,內存空間 O(n) 到 O(1) 的優化 ,這就是尾遞歸。
尾遞歸的好處:可以釋放外層函數的調用棧,較少棧層級,節省內存開銷,避免內存溢出。

網上很多用斐波那契數列作爲栗子,但我偏不,我用個數組累加的栗子

function add1(arr) {
    if (arr.length === 0) {
        return 0
    }
    return add1(arr.slice(1)) + arr[0] // 還有父級函數中 arr[0] 的引用
}

function add(arr, re) {
    if (arr.length === 0) {
        return re + 0
    } else {
        return add(arr.slice(1), arr[0] + re) // 僅僅是函數調用
    }
}

console.log(add([1, 2, 3, 4], 0))  // 10
console.log(add1([1, 2, 3, 4])) // 10

Q:觀察者模式 發佈-訂閱模式 的區別

兩者都是訂閱-通知的模式,區別在於:

觀察者模式:觀察者和訂閱者是互相知道彼此的,是一個緊耦合的設計

發佈-訂閱:觀察者和訂閱者是不知道彼此的,因爲他們中間是通過一個訂閱中心來交互的,訂閱中心存儲了多個訂閱者,當有新的發佈的時候,就會告知訂閱者

設計模式的名詞實在有點多且繞,我畫個簡單的圖:

dingyue

Q:WebSocket

這個就問到了一次,所以簡單進行了瞭解。

簡單來說,WebSocket 是應用層協議,基於 tcp,與HTTP協議一樣位於應用層,都是TCP/IP協議的子集。

socket-tcp

HTTP 協議是單向通信協議,只有客戶端發起HTTP請求,服務端纔會返回數據。而 WebSocket 協議是雙向通信協議,在建立連接之後,客戶端和服務器都可以主動向對方發送或接受數據。

socket-http

參考資料:
http://www.ruanyifeng.com/blog/2017/05/websocket.html

最後

以上就是 javascript 相關的題目彙總,後續遇到有代表性的題目還會繼續補充。

文章中如有不對的地方,歡迎小夥伴們多多指正。

如果你喜歡探討技術,歡迎添加我微信一起學習探討,大家都是同行,非常期待能與大夥聊技術、聊愛好。

下面是我的微信二維碼,可掃碼添加
wechat

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章