在去年底開始換工作,直到現在算是告了一個段落,斷斷續續的也面試了不少公司,現在回想起來,那段時間經歷了被面試官手撕,被筆試題狂懟,悲傷的時候差點留下沒技術的淚水。
這篇文章我打算把我找工作遇到的各種面試題(每次面試完我都會總結)和我自己複習遇到比較有意思的題目,做一份彙總,年後是跳槽高峯期,也許能幫到一些小夥伴。
先說下這些題目難度,大部分都是基礎題,因爲這段經歷給我的感覺就是,不管你面試的是高級還是初級,基礎的知識一定會問到,甚至會有一定的深度,所以基礎還是非常重要的。
我將根據類型分爲幾篇文章來寫:
面試總結:javascript 面試點彙總(萬字長文)(已完成) 強烈大家看看這篇,面試中 js 是大頭
面試總結:nodejs 面試點彙總(已完成)
面試總結:瀏覽器相關 面試點彙總(已完成)
面試總結:css 面試點彙總(已完成)
面試總結:面試技巧篇(已完成)
六篇文章都已經更新完啦~
這篇文章是對 javascript
相關的題目做總結,內容有點長,大致算了下,有接近 2W 字,推薦用電腦閱讀,歡迎朋友們先收藏在看。
先看看目錄(這長圖在手機上比較模糊,可點擊圖片看大圖)
Q:介紹下原型鏈
原型鏈這東西,基本上是面試必問,而且不是知識點還都是基於原型鏈擴展的,所以我們先把原先鏈整明白。
我們看一張網上非常流行的圖
嗯,箭頭有點多且有點繞,沒關係,我們可逐步分析,我們從結果倒推結論,這樣更直觀些,看代碼
function person() {
this.name = 10
}
person.prototype.age = 10
const p = new person()
分析構造函數
我們通過斷點看下 person 這個函數的內容
它是一個自定義的函數類型,看關鍵的兩個屬性 prototype
和 __proto__
,我們一一分析
- prototype 分析
對 prototype
展開看,是個自定義的對象,這個對象有三個屬性 age constructor __proto__
,
age
的值是 10 ,那麼可以得出通過person.prototype
賦值的參數都是在 prototype
這個對象中的。
點開 constructor
,發現這個屬性的值就是指向構造器 preson
函數,其實就是循環引用,這時候就有點套娃的意思了
那麼,根據字面意思, prototype
可以翻譯成,原先對象,用於擴展屬性和方法。
__proto__
分析
對__proto__
展開看看
person 中的 __proto__
是一個原始的 function 對象,在 function 對象中,又看到了 __proto__
這個屬性,這時候它的值是原始的 Object 對象,在 Object 對象中又再次發現了 __proto__
屬性,這時候 __proto__
等於 null
js 中數據類型分爲兩種,基本類型和對象類型,所以我們可以這麼猜測,person 是一個自定義的函數類型,它應該是屬於函數這一家族下的,對於函數,我們知道它是屬於對象的,那麼它們幾個是怎麼關聯起來的呢?
沒錯,就是通過 __proto__
這個屬性,而由這個屬性組成的鏈,就叫做原型鏈。
根據上面的例子我們,可得出,原型鏈的最頂端是 null
,往下是 Object
對象,而且只要是對象或函數類型都會有 __proto__
這個屬性,畢竟大家都是 js-family 的一員嘛。
分析生成的對象
上面我們已經知道了原型和原型鏈,那麼對於 new 出來的對象,它們的關係又是怎麼樣的呢?繼續斷點分析
p
對象中有個 __proto__
屬性,我們已經知道這是個原型鏈,通過它可以找到我們的祖先,展開 __proto__
,大家看到這裏有沒有發現很眼熟,在看一張圖,
沒錯!p.__proto__
就是 person 函數的 prototype
,這一步也就是 new 的核心點(下個題目我們會說到)。
那麼 p 這實例的原型鏈是怎麼樣的?
p.__proto__ => {constructor:func}.__proto__ => Object => null
對於實例對象來說,原先鏈主要用來做什麼呢?
- 實現繼承:如果沒有原型鏈,每個對象就都是孤立的,對象間就沒有關聯,所以原型鏈就像一顆樹幹,從而可以實現面對對象中的繼承
- 屬性查找:首先在當前實例對象上查找,要是沒找到,那麼沿着
__proto__
往上查找 - 實例類型判斷:判斷這個實例是否屬於某類對象
還有就是,光看文字的解釋還是有點費解的,要想深入理解,還是需要多動手斷點調試,才能很快理順。
若還是不太理解實例對象的原型鏈關係,可以看下一題:解釋構造函數
Q:介紹下構造函數是什麼?
構造函數與普通函數在編碼上沒有區別,只要可以通過 new 來調用的就是構造函數。
那麼什麼函數不可以作爲構造函數呢?
箭頭函數不可以作爲構造函數。
new
是一個語法糖,對執行的原理一步步拆分並自己寫一個模擬 new 的函數:
0. 自定義一個 objectFactory 模擬 new 語法糖,函數可以接受多個參數,但要求第一個參數必須爲構造函數
-
創建一個空對象 obj ,分配內存空間
-
從參數列表中獲取構造函數,並將
obj
的__proto__
屬性指向構造函數的prototype
-
通過
apply
執行構造,並將當前this
的指向改爲obj
-
返回構造函數的執行結果,或者當前的
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
也叫引用類型
- 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
- 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 // 引用地址的拷貝,所以這兩個對象指向同一個內存地址,那麼他們其實是同一個對象
關於函數的傳參是傳值還是傳引用呢?
很多人說基本類型傳值,對象類型傳引用,但嚴格來說,函數參數傳遞的是值,上圖可以看出,就算是引用類型,它在棧中存儲的還是一串內存地址,所以也是一個值。不過我覺得沒必要過於糾結這句話,理解就行。
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
的掌握程度。
- 數據屬性(數據描述符)
相關的屬性如下:
[[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,還是可以對屬性重新賦值的。
- 訪問器屬性(存取描述符)
訪問器屬性不包含數據值,沒有 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)
分別觀察以下兩個場景的執行結果
瀏覽器中執行
直接看結果
很明顯,在瀏覽器中 arguments
是不存在的
nodejs 中執行
結果(爲了辨認,輸出前加了段字符串)
執行過程沒有報錯,說明 arguments
是存在的,那爲啥結果不是預期的 6 呢?
我們對箭頭函數打斷點看看
arguments 對象看着沒啥問題,傳入的參數也看到了
我們看看通過數組方式獲取到的值
竟然是這些東西,這些是當前腳本執行的模塊信息,並不是我們預期的參數列表
結論
- 在瀏覽器中箭頭函數沒有
arguments
- 在 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的規範:
- 一個 Promise 的當前狀態必須爲以下三種狀態中的一種:等待態(Pending)、執行態(Fulfilled)和拒絕態(Rejected),狀態的改變只能是單向的,且變化後不可在改變。
- 一個 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 的時候,對象就被清理。但這個有個循環引用的弊端,所以應用的比較少。
垃圾收集的性能優化
- 分代回收,對象分成兩組,新生帶、老生帶,
- 增量回收
- 空閒時間回收
編碼可以做的優化
- 避免重複創建對象。
- 在適當的時候解除引用,是爲頁面獲的更好性能的一個重要方式。
- 全局變量什麼時候需要自動釋放內存空間則很難判斷,因此在開發中,需要儘量避免使用全局變量。
Q:什麼是嚴格模式
通過在腳本的最頂端放上一個特定語句 "use strict";
整個腳本就可開啓嚴格模式語法。
嚴格模式下有以下好處:
- 消除Javascript語法的一些不合理、不嚴謹之處,減少一些怪異行爲;
- 消除代碼運行的一些不安全之處,保證代碼運行的安全;
- 提高編譯器效率,增加運行速度;
- 爲未來新版本的Javascript做好鋪墊。
如以下具體的場景:
- 嚴格模式會使引起靜默失敗(silently fail,注:不報錯也沒有任何效果)的賦值操作拋出異常
- 嚴格模式下的 eval 不再爲上層範圍(surrounding scope,注:包圍eval代碼塊的範圍)引入新變量
- 嚴格模式禁止刪除聲明變量
- 在嚴格模式中一部分字符變成了保留的關鍵字。這些字符包括implements, interface, let, package, private, protected, public, static和yield。在嚴格模式下,你不能再用這些名字作爲變量名或者形參名。
- 嚴格模式下 arguments 和參數值是完全獨立的,非嚴格下修改是會相互影響的
Q:map 和 weekMap 的區別
map
的 key
可以是任意類型,在 map
內部有兩個數組,分別存放 key
和 value
,用下標保證兩者的一一對應,在對 map
操作時,內部會遍歷數組,時間複雜度O(n),其次,因爲數組會一直引用每個鍵和值,回收算法沒法回收處理,可能會導致內存泄露。
相比之下, WeakMap
的鍵值必須是對象,持有的是每個鍵對象的 弱引用
,這意味着在沒有其他引用存在時垃圾回收能正確進行。
const wm1 = new WeakMap();
const o1 = {};
wm1.set(o1, 37); // 當 o1 對象被回收,那麼 WeakMap 中的值也被釋放
Q:String 和 Array 有哪些常用函數
我也不知道爲什麼會有這種筆試題…
- String:
split(): 方法使用指定的分隔符字符串將一個String對象分割成子字符串數組
slice(): 方法提取某個字符串的一部分,並返回一個新的字符串,且不會改動原字符串
substring(): 方法返回一個字符串在開始索引到結束索引之間的一個子集, 或從開始索引直到字符串的末尾的一個子集
- Array:
slice(): 方法返回一個新的數組對象,這一對象是一個由 begin 和 end 決定的原數組的淺拷貝(包括 begin,不包括end)。原始數組不會被改變。
splice(): 方法通過刪除或替換現有元素或者原地添加新的元素來修改數組,並以數組形式返回被修改的內容。此方法會改變原數組。
push(): 方法將一個或多個元素添加到數組的末尾,並返回該數組的新長度。
pop(): 方法從數組中刪除最後一個元素,並返回該元素的值。此方法更改數組的長度。
shift():方法從數組中刪除第一個元素,並返回該元素的值。此方法更改數組的長度。
unshift(): 方法將一個或多個元素添加到數組的開頭,並返回該數組的新長度(該方法修改原有數組)。
Q:判斷數組的幾種方法
這題主要還是考察對原型鏈的理解
Array.isArray()
ES6 apiobj instanceof Array
原型鏈查找obj.constructor === Array
構造函數類型判斷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:閉包的使用場景列舉
閉包:定義在一個函數內部的函數,內部函數持有外部函數內變量的引用,這個內部的函數有自己的執行作用域,可以避免外部污染。
關於閉包的理解,可以說是一千個讀者就有一千個哈姆雷特,找到適合自己理解和講述的就行。
場景有:
- 函數式編程,compose curry
- 函數工廠、單利
- 私有變量和方法,面向對象編程
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 執行時間段。
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:觀察者模式 發佈-訂閱模式 的區別
兩者都是訂閱-通知的模式,區別在於:
觀察者模式:觀察者和訂閱者是互相知道彼此的,是一個緊耦合的設計
發佈-訂閱:觀察者和訂閱者是不知道彼此的,因爲他們中間是通過一個訂閱中心來交互的,訂閱中心存儲了多個訂閱者,當有新的發佈的時候,就會告知訂閱者
設計模式的名詞實在有點多且繞,我畫個簡單的圖:
Q:WebSocket
這個就問到了一次,所以簡單進行了瞭解。
簡單來說,WebSocket 是應用層協議,基於 tcp,與HTTP協議一樣位於應用層,都是TCP/IP協議的子集。
HTTP 協議是單向通信協議,只有客戶端發起HTTP請求,服務端纔會返回數據。而 WebSocket 協議是雙向通信協議,在建立連接之後,客戶端和服務器都可以主動向對方發送或接受數據。
參考資料:
http://www.ruanyifeng.com/blog/2017/05/websocket.html
最後
以上就是 javascript
相關的題目彙總,後續遇到有代表性的題目還會繼續補充。
文章中如有不對的地方,歡迎小夥伴們多多指正。
如果你喜歡探討技術,歡迎添加我微信一起學習探討,大家都是同行,非常期待能與大夥聊技術、聊愛好。
下面是我的微信二維碼,可掃碼添加