在去年末開始換工做,直到如今算是告了一個段落,斷斷續續的也面試了很多公司,如今回想起來,那段時間經歷了被面試官手撕,被筆試題狂懟,悲傷的時候差點留下沒技術的淚水。javascript
這篇文章我打算把我找工做遇到的各類面試題(每次面試完我都會總結)和我本身複習遇到比較有意思的題目,作一份彙總,年後是跳槽高峯期,也許能幫到一些小夥伴。css
先說下這些題目難度,大部分都是基礎題,由於這段經歷給個人感受就是,無論你面試的是高級仍是初級,基礎的知識必定會問到,甚至會有必定的深度,因此基礎仍是很是重要的。html
我將根據類型分爲幾篇文章來寫:前端
面試總結:javascript 面試點彙總(萬字長文)(已完成) 強烈你們看看這篇,面試中 js 是大頭vue
面試總結:nodejs 面試點彙總(已完成)java
面試總結:瀏覽器相關 面試點彙總(已完成)node
面試總結:css 面試點彙總(已完成)webpack
面試總結:框架 vue 和工程相關的面試點彙總(已完成)git
面試總結:面試技巧篇(已完成)es6
六篇文章都已經更新完啦~
這篇文章是對 javascript
相關的題目作總結,內容有點長,大體算了下,有接近 2W 字,推薦用電腦閱讀,歡迎朋友們先收藏在看。
先看看目錄(這長圖在手機上比較模糊,可點擊圖片看大圖)
原型鏈這東西,基本上是面試必問,並且不是知識點還都是基於原型鏈擴展的,因此咱們先把原先鏈整明白。
咱們看一張網上很是流行的圖
嗯,箭頭有點多且有點繞,不要緊,咱們可逐步分析,咱們從結果倒推結論,這樣更直觀些,看代碼
function person() { this.name = 10 } person.prototype.age = 10 const p = new person()
咱們經過斷點看下 person 這個函數的內容
它是一個自定義的函數類型,看關鍵的兩個屬性 prototype
和 __proto__
,咱們一一分析
對 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__
往上查找還有就是,光看文字的解釋仍是有點費解的,要想深刻理解,仍是須要多動手斷點調試,才能很快理順。
若仍是不太理解實例對象的原型鏈關係,能夠看下一題:解釋構造函數
構造函數與普通函數在編碼上沒有區別,只要能夠經過 new 來調用的就是構造函數。
那麼什麼函數不能夠做爲構造函數呢?
箭頭函數不能夠做爲構造函數。
new
是一個語法糖,對執行的原理一步步拆分並本身寫一個模擬 new 的函數:
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
可看出並不複雜,關鍵點在第二步,設置對象的原型鏈,這也是建立實例對象的核心點。
js 中數據類型分爲兩類,一類是基本數據類型,一類是對象類型。
基本數據類型有:Number String Boolean Null Undefined BigInt Symbol
對象類型: 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
__proto__
逐層向上查找,經過 instanceof 也能夠判斷一個實例是不是其父類型或者祖先類型的實例。有這麼個面試題
function person() { this.name = 10 } console.log(person instanceof person)
結果是 false
,看下 person 函數的原型鏈 person.
proto_ => Function.
proto => Object.
proto => null
,因此在原型鏈上是找不到 person
的
Null
Undefined
String
Number
Boolean
BigInt
Symbol
基本類型:存儲在棧內存中,由於基本類型的大小是固定,在棧內能夠快速查找。
引用類型:存儲在堆內存中,由於引用類型的大小是不固定的,因此存儲在堆內存中,而後棧內存中僅存儲堆中的內存地址。
咱們在查找對象是從棧中查找,那麼可得知,對於基本對象咱們是對它的值進行操做,而對於引用類型,咱們是對其引用地址操做。
var name = 'xiaoming' var name1 = name; // 值拷貝 var obj = {age:10} var obj1 = obj // 引用地址的拷貝,因此這兩個對象指向同一個內存地址,那麼他們實際上是同一個對象
關於函數的傳參是傳值仍是傳引用呢?
不少人說基本類型傳值,對象類型傳引用,但嚴格來講,函數參數傳遞的是值,上圖能夠看出,就算是引用類型,它在棧中存儲的仍是一串內存地址,因此也是一個值。不過我以爲不必過於糾結這句話,理解就行。
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; }
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
包裝對象,只要是爲了便於基本類型調用對象的方法。
包裝對象有三種:String Number Boolean
這三種原始類型能夠與實例對象進行自動轉換,可把原始類型的值變成(包裝成)對象,好比在字符串調用函數時,引擎會將原始類型的值轉換成只讀的包裝對象,執行完函數後就銷燬。
class 也是一個語法糖,本質仍是基於原型鏈,class 語義化和編碼上更加符合面向對象的思惟。
對於 function
能夠用 call apply bind
的方式來改變他的執行上下文,可是 class
卻不能夠,class 雖然本質上也是一個函數,但在轉成 es5 (babel)作了一層代理,來禁止了這種行爲。
Object.keys()
遍歷由於涉及的代碼較多,因此獨立寫一篇文章來總結,傳送門: [js-實現繼承的幾種方式]()
先說下做用域的這個概念,做用域就是變量和函數的可訪問範圍,控制這個變量或者函數可訪問行和生命週期(這個很重要)。
在 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 中沒有塊級做用域的弊端。
其餘小點:
做用域和執行上下文的區別,看下引擎執行腳本的兩個階段
解釋階段: 詞法分析 -> 語法分析 -> 做用域規則肯定
執行階段: 建立執行上下文 -> 執行函數代碼 -> 垃圾回收
參考鏈接:
https://segmentfault.com/a/11...
https://www.cnblogs.com/dolph...
var: 解析器在對js解析時,會將腳本掃描一遍,將變量的聲明提早到代碼塊的頂部,賦值仍是在原先的位置,若在賦值前調用,就會出現暫時性死區,值爲 undefined
let const:不存在在變量提高,且做用域是存在於塊級做用域下,因此這兩個的出現解決了變量提高的問題,同時引用塊級做用域。
注:變量提高的緣由是爲了解決函數互相調用的問題。
其實就是問對 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
訪問器屬性不包含數據值,沒有 value 屬性,有 get set
屬性,經過這兩個屬性來對值進行自定義的讀和寫,能夠理解爲取值和賦值前的攔截器,相關屬性以下:
[[Configurable]]:表示可否經過 delete 刪除屬性從而從新定義屬性,可否修改屬性的特性,或者可否把屬性修改成數據屬性,默認 false
[[Enumerable]]:表示可否經過 for-in 循環返回屬性,默認 false
[[Get]]:在讀取屬性時調用的函數。默認值爲 undefined
[[Set]]:在寫入屬性時調用的函數。默認值爲 undefined
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/dev...
https://developer.mozilla.org...
在 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
(很是感謝評論區夥伴的提醒)
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
是不存在的
結果(爲了辨認,輸出前加了段字符串)
執行過程沒有報錯,說明 arguments
是存在的,那爲啥結果不是預期的 6 呢?
咱們對箭頭函數打斷點看看
arguments 對象看着沒啥問題,傳入的參數也看到了
咱們看看經過數組方式獲取到的值
居然是這些東西,這些是當前腳本執行的模塊信息,並非咱們預期的參數列表
arguments
arguments
,可經過其獲取參數長度,但不能經過改對象獲取參數列表(我也不太懂這個對象的原理,還請知道的夥伴在評論區告知,謝謝)
浮點數的精度丟失不只僅是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/numbe...]
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
是能夠作到四捨五入的,可是僅限於正整數,那麼咱們能夠放大至保留一位小數,計算完成後再縮小倍數。
10 進制轉其餘進制:Number(val).toString([2,8,10,16])
其餘進制轉成10進制:Number.parseInt("1101110",[2,8,10,16])
其餘進制互轉:先將其餘進制轉成 10 進制,在把 10 進制轉成其餘進制
ArrayBuffer: 用來表示通用的、固定長度的原始二進制數據緩衝區,做爲內存區域,能夠存放多種類型的數據,它不能直接讀寫,只能經過視圖來讀寫。
同一段內存,不一樣數據有不一樣的解讀方式,這就叫作「視圖」(view),視圖的做用是以指定格式解讀二進制數據。目前有兩種視圖,一種是 TypedArray
視圖,另外一種是 DataView
視圖,二者的區別主要是字節序,前者的數組成員都是同一個數據類型,後者的數組成員能夠是不一樣的數據類型。
Blob: 也是存放二進制的容器,經過 FileReader
進行轉換。
以前有作過簡單的總結,你們能夠看看:nodejs 二進制與Buffer
畢竟對這塊應用的比較少,推薦一篇文章給你們 二進制數組
這個問題出場率很高呀!常見的有以下幾個:
回調函數
:經過嵌套調用實現Generator
: 異步任務的容器,生成器本質上是一種特殊的迭代器, Generator 執行後返回的是個指針對象,調用對象裏的 next 函數,會移動內部指針,分階段執行 Generator
函數 ,指向 yield
語句,返回一個對象 {value:當前的執行結果,done:是否結束}promise
: 而是一種新的語法糖, Promise 的最大問題是代碼冗餘,經過 then 傳遞執行權,由於需求手動調用 then 方法,當異步函數多的時候,原來的語義變得很不清楚co
: 把 Generator 和 Promise 封裝,達到自動執行async\await
: 目前是es7草案,可經過 bable webpack
等工具提早使用,目前原生瀏覽器支持還不太好。其本質上是語法糖,跟 co 庫同樣,都是對 generator+promise
的封裝,不過相比 co ,語義化更好,能夠像普通函數同樣調用,且大機率是將來的趨勢。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 的弊端很明顯,執行流程管理不方便,異步返回的值須要手動傳遞,編碼上較容易出錯。
Promise 已是 ES6 的規範了,相比 Generator ,設計的更加合理和便捷。
看看Promise的規範:
promise 的每一個操做返回的都是 promise 對象,可支持鏈式調用。經過 then 方法執行回調函數,Promise 的回調函數是放在事件循環中的微隊列。
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) + '"')); } }); }
這是必考題呀,盆友們,這個可閱讀我之前寫的一篇文章,傳送門: js 事件循環
這個東西有點多,能夠看我以前的一篇總結,傳送門:面試官讓我解釋前端模塊化
爲何須要垃圾回收:由於對象須要佔用內存,而內存資源是有限的。
js 會週期性的對不在使用的對象銷燬,釋放內存,關鍵點就在於怎麼識別哪些對象是垃圾。
垃圾對象:對象沒有被引用,或者幾個對象造成循環引用,可是根訪問不到他們,這些都是可回收的垃圾。
垃圾回收的兩種機制:標記清除和引用計數
垃圾收集器在運行的時候會給存儲在內存中的全部變量都加上標記,而後,它會去掉環境中的變量以及被環境中的變量引用的標記,而在此以後再被加上標記的變量將被視爲準備刪除的變量,緣由是環境中的變量已經沒法訪問到這些變量了。
最後。垃圾收集器完成內存清除工做,銷燬那些帶標記的值,並回收他們所佔用的內存空間。
好比說函數中聲明瞭一個變量,就作一個標記,當函數執行完成,退出執行棧,這個變量的標記就變成已使用完。
目前主流瀏覽器採用的是這個策略
跟蹤每一個值被引用的次數,聲明一個變量後,這個變量每被其餘變量引用一次,就加 1 ,若是變量引用釋放了,就減 1,當引用次數爲 0 的時候,對象就被清理。但這個有個循環引用的弊端,因此應用的比較少。
經過在腳本的最頂端放上一個特定語句 "use strict";
整個腳本就可開啓嚴格模式語法。
嚴格模式下有如下好處:
如如下具體的場景:
map
的 key
能夠是任意類型,在 map
內部有兩個數組,分別存放 key
和 value
,用下標保證二者的一一對應,在對 map
操做時,內部會遍歷數組,時間複雜度O(n),其次,由於數組會一直引用每一個鍵和值,回收算法無法回收處理,可能會致使內存泄露。
相比之下, WeakMap
的鍵值必須是對象,持有的是每一個鍵對象的 弱引用
,這意味着在沒有其餘引用存在時垃圾回收能正確進行。
const wm1 = new WeakMap(); const o1 = {}; wm1.set(o1, 37); // 當 o1 對象被回收,那麼 WeakMap 中的值也被釋放
我也不知道爲何會有這種筆試題...
split(): 方法使用指定的分隔符字符串將一個String對象分割成子字符串數組
slice(): 方法提取某個字符串的一部分,並返回一個新的字符串,且不會改動原字符串
substring(): 方法返回一個字符串在開始索引到結束索引之間的一個子集, 或從開始索引直到字符串的末尾的一個子集
slice(): 方法返回一個新的數組對象,這一對象是一個由 begin 和 end 決定的原數組的淺拷貝(包括 begin,不包括end)。原始數組不會被改變。
splice(): 方法經過刪除或替換現有元素或者原地添加新的元素來修改數組,並以數組形式返回被修改的內容。此方法會改變原數組。
push(): 方法將一個或多個元素添加到數組的末尾,並返回該數組的新長度。
pop(): 方法從數組中刪除最後一個元素,並返回該元素的值。此方法更改數組的長度。
shift():方法從數組中刪除第一個元素,並返回該元素的值。此方法更改數組的長度。
unshift(): 方法將一個或多個元素添加到數組的開頭,並返回該數組的新長度(該方法修改原有數組)。
這題主要仍是考察對原型鏈的理解
Array.isArray()
ES6 apiobj instanceof Array
原型鏈查找obj.constructor === Array
構造函數類型判斷Object.prototype.toString.call(obj) === '[object Array]'
toString返回表示該對象的字符串,若這個方法沒有被覆蓋,那麼默認返回 "[object type]"
,其中 type
是對象的類型。須要準確判斷類型的話,建議使用這種方法
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()
閉包:定義在一個函數內部的函數,內部函數持有外部函數內變量的引用,這個內部的函數有本身的執行做用域,能夠避免外部污染。
關於閉包的理解,能夠說是一千個讀者就有一千個哈姆雷特,找到適合本身理解和講述的就行。
場景有:
這題面試官估計是想知道你是否是真的用過 es6 吧
擴展運算符(…)也會調用默認的 Iterator 接口。
擴展運算符主要用在不定參數上,能夠將參數轉成數組形式
function fn(...arg){ console.log(arg) // [ 1, 2, 3 ] } fn(1,2,3)
首先來一句話歸納:進程和線程都是一個時間段的描述,都是對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 執行時間段。
函數式編程的兩個核心:合成和柯里化,以前對函數式編程作過總結,傳送門:【面試官問】你懂函數式編程嗎?
先給面試官簡單說下什麼是遞歸函數:函數內部循環調用自身的就是遞歸函數,若函數沒有執行完畢,執行棧中會一直保持函數相關的變量,一直佔用內存,當遞歸次數過大的時候,就可能會出現內存溢出,也叫爆棧,頁面可能會卡死。
因此爲了不出現這種狀況,能夠採用尾遞歸。
尾遞歸:在函數的最後一步是調用函數,進入下一個函數不在須要上一個函數的環境了,內存空間 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
二者都是訂閱-通知的模式,區別在於:
觀察者模式:觀察者和訂閱者是互相知道彼此的,是一個緊耦合的設計
發佈-訂閱:觀察者和訂閱者是不知道彼此的,由於他們中間是經過一個訂閱中心來交互的,訂閱中心存儲了多個訂閱者,當有新的發佈的時候,就會告知訂閱者
設計模式的名詞實在有點多且繞,我畫個簡單的圖:
這個就問到了一次,因此簡單進行了瞭解。
簡單來講,WebSocket 是應用層協議,基於 tcp,與HTTP協議同樣位於應用層,都是TCP/IP協議的子集。
HTTP 協議是單向通訊協議,只有客戶端發起HTTP請求,服務端纔會返回數據。而 WebSocket 協議是雙向通訊協議,在創建鏈接以後,客戶端和服務器均可以主動向對方發送或接受數據。
參考資料:
http://www.ruanyifeng.com/blo...
以上就是 javascript
相關的題目彙總,後續遇到有表明性的題目還會繼續補充。
文章中若有不對的地方,歡迎小夥伴們多多指正。
若是你喜歡探討技術,歡迎添加我微信一塊兒學習探討,你們都是同行,很是期待能與大夥聊技術、聊愛好。
下面是個人微信二維碼,可掃碼添加