最近看到有一篇文章總結了一些前端的面試題,面向的物件應該是社招中初、中級的前端,感覺有一定的參考價值,因此開一個帖子嘗試解答這些問題,順便當做自己的面試題積累。
JavaScript基礎
1、宣告提前類問題
在網上找到一篇文章,裡面有一道面試題,考察了包括變數定義提升、this指標指向、運算子優先順序、原型、繼承、全域性變數汙染、物件屬性及原型屬性優先順序等許多知識點,而就其中宣告提前相關的知識,我覺得也十分有參考價值:
function Foo() {
getName = function () { alert (1); };
return this;
}
Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
var getName = function () { alert (4);};
function getName() { alert (5);}
// 請寫出以下輸出結果:
Foo.getName();
getName(); // 宣告提前
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
複製程式碼
這道題的答案是:2、4、1、1、2、3、3。
這裡考察宣告提前的題目在程式碼中已經標出,這裡宣告getName方法的兩個語句:
var getName = function () { alert (4) };
function getName() { alert (5) }
複製程式碼
實際上在解析的時候是這樣的順序:
function getName() { alert (5) }
var getName;
getName = function () { alert (4) };
複製程式碼
如果我們在程式碼中間再加兩個斷點:
getName(); // 5
var getName = function () { alert (4) };
getName(); // 4
function getName() { alert (5) }
複製程式碼
在第一次getName時,function的宣告和var的宣告都被提前到了第一次getName的前面,而getName的賦值操作並不會提前,單純使用var的宣告也不會覆蓋function所定義的變數,因此第一次getName輸出的是function宣告的5; 而第二次getName則是發生在賦值語句的後面,因此輸出的結果是4,所以實際程式碼的執行順序是這樣:
function getName() { alert (5) }
var getName;
getName(); // 5
getName = function () { alert (4) };
getName(); // 4
複製程式碼
2、瀏覽器儲存
localStorage,sessionStorage和cookie的區別
共同點:都是儲存在瀏覽器端、僅同源可用的儲存方式
- 資料儲存方面
- cookie資料始終在同源的http請求中攜帶(即使不需要),即cookie在瀏覽器和伺服器間來回傳遞。cookie資料還有路徑(path)的概念,可以限制cookie只屬於某個路徑下
- sessionStorage和localStorage不會自動把資料傳送給伺服器,僅在本地儲存。
- 儲存資料大小
- 儲存大小限制也不同,cookie資料不能超過4K,同時因為每次http請求都會攜帶cookie、所以cookie只適合儲存很小的資料,如會話標識。
- sessionStorage和localStorage雖然也有儲存大小的限制,但比cookie大得多,可以達到5M或更大
- 資料儲存有效期
- sessionStorage:僅在當前瀏覽器視窗關閉之前有效;
- localStorage:始終有效,視窗或瀏覽器關閉也一直儲存,本地儲存,因此用作持久資料;
- cookie:只在設定的cookie過期時間之前有效,即使視窗關閉或瀏覽器關閉
- 作用域不同
- sessionStorage不在不同的瀏覽器視窗中共享,即使是同一個頁面;
- localstorage在所有同源視窗中都是共享的;也就是說只要瀏覽器不關閉,資料仍然存在
- cookie: 也是在所有同源視窗中都是共享的.也就是說只要瀏覽器不關閉,資料仍然存在
3、跨域
不久我寫了一個帖子,對同源策略及各種跨域的方式進行了總結:什麼是跨域,為什麼瀏覽器會禁止跨域,及其引起的發散性學習
4、Promise的使用及原理
Promise是ES6加入的新特性,用於更合理的解決非同步程式設計問題,關於用法阮一峰老師在ECMAScript 6 入門中作出了詳細的說明,在此就不重複了。
上面這篇文章則是對Promise的原理進行的詳細的說明,在這裡,我提取最簡單的Promise實現方式來對Promise的原理進行說明:
function Promise(fn) {
var value = null,
callbacks = []; // callbacks為陣列,因為可能同時有很多個回撥
this.then = function (onFulfilled) {
callbacks.push(onFulfilled);
};
function resolve(value) {
callbacks.forEach(function (callback) {
callback(value);
});
}
fn(resolve);
}
複製程式碼
首先,then
裡面宣告的單個或多個函式,將被推入callbacks
列表,在Promise例項呼叫resolve
方法時遍歷呼叫,並傳入resolve
方法中傳入的引數值。
以下,使用一個簡單的例子來對Promise的執行流程進行分析:
functionm func () {
return new Promise(function (resolve) {
setTimeout(function () {
resolve('complete')
}, 3000);
})
}
func().then(function (res) {
console.log(res); // complete
})
複製程式碼
func
函式的定義是返回了一個Promise例項,宣告例項時傳入的回撥函式加入了一個resolve
引數(這個resolve
引數在Promise中的fn(resolve)
定義中獲取resolve
的函式實體),回撥中執行了一個非同步操作,在非同步操作完成的回撥中執行了resolve
函式。
再看執行步驟,func
函式返回了一個Promise例項,例項則可以執行Promise建構函式中定義的then
方法,then
方法中傳入的回撥則會在resolve
(即非同步操作完成後)執行,由此實現了通過then
方法執行非同步操作完成後回撥的功能。
5、JavaScript事件迴圈機制
原文中貼出的文章具有很大參考價值,先貼個連結:詳解JavaScript中的Event Loop(事件迴圈)機制。
JavaScript是一種單執行緒、非阻塞的語言,這是由於它當初的設計就是用於和瀏覽器互動的:
- 單執行緒:
JavaScript
設計為單執行緒的原因是,最開始它最大的作用就是和DOM
進行互動,試想一下,如果JavaScript
是多執行緒的,那麼當兩個執行緒同時對DOM
進行一項操作,例如一個向其新增事件,而另一個刪除了這個DOM
,此時該如何處理呢?因此,為了保證不會 發生類似於這個例子中的情景,JavaScript
選擇只用一個主執行緒來執行程式碼,這樣就保證了程式執行的一致性。 - 非阻塞:當程式碼需要進行一項非同步任務(無法立刻返回結果,需要花一定時間才能返回的任務,如I/O事件)的時候,主執行緒會掛起(
pending
)這個任務,然後在非同步任務返回結果的時候再根據一定規則去執行相應的回撥。而JavaScript
實現非同步操作的方法就是使用Event Loop。
setTimeout(function () {
console.log(1);
});
new Promise(function(resolve,reject){
console.log(2)
resolve(3)
}).then(function(val){
console.log(val);
})
複製程式碼
下面通過一段程式碼來分析這個問題,首先setTimeout
和Promise
中的then
回撥都是非同步方法,而new Promise
則是一個同步操作,所以這段程式碼應該首先會立即輸出2
;JavaScript
將非同步方法分為了marco task
(巨集任務:包括setTimeout
和setInterval
等)和micro task
(微任務:包括new Promise
等),在JavaScript
的執行棧中,如果同時存在到期的巨集任務和微任務,則會將微任務先全部執行,再執行第一個巨集任務,因此,兩個非同步操作中then
的回撥會率先執行,然後才執行setTimeout
的回撥,因此會依次輸出3、1,所以最終輸出的結果就是2、3、1。
6、ES6作用域及let和var的區別
這個問題阮一峰老師在ECMAScript 6 入門中的let 和 const 命令
章節對這個問題作出了詳細的說明,下面提取一些我認為關鍵的點進行講解。
ES6引入了使用{}
包裹的程式碼區域作為塊級作用域的宣告方式,其效果與ES5中function
宣告的函式所生成的函式作用域具有相同的效果,作用域外部不能訪問作用域內部宣告的函式或變數,這樣的宣告在ES6中對於包括for () {}
、if () {}
等大括號包裹的程式碼塊中都會生效,生成一個單獨的作用域。
ES6新增的let
宣告變數的方式相比var
具有以下幾個重要特點:
let
宣告的變數只在作用域內有效,如下方程式碼,if
宣告生成了一個塊級作用域,在這個作用域內宣告的變數在作用域外部無法訪問,假如訪問會產生錯誤:
if (true) {
let me = 'handsome boy';
}
console.log(me); // ReferenceError
複製程式碼
let
宣告的變數與var
不同,不會產生變數提升,如下方程式碼,在宣告之前輸出程式碼,會產生錯誤:
// var 的情況
console.log(foo); // 輸出undefined
var foo = 2;
// let 的情況
console.log(bar); // ReferenceError
let bar = 2;
複製程式碼
let
的宣告方式不允許重複宣告,如重複宣告會報錯,而var
宣告變數時,後宣告的語句會對先宣告的語句進行覆蓋:
// 報錯
function func() {
let a = 10;
var a = 1;
}
// 報錯
function func() {
let a = 10;
let a = 1;
}
複製程式碼
- 只要塊級作用域記憶體在
let
命令,它所宣告的變數就“繫結”(binding
)這個區域,不再受外部的影響,這個特性稱為暫時性死區
。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
複製程式碼
7、閉包
待補充
8、原型及原型鏈
待補充
9、瀏覽器的迴流與重繪 (Reflow & Repaint)
瀏覽器在接收到 html
與 css
後,渲染的步驟是:html
經過渲染生成 DOM
樹, css
經過渲染生成 css
渲染樹,兩者再經過結合,生成 render tree
,瀏覽器就可以根據 render tree
進行畫面繪製。
如果瀏覽器從伺服器接收到了新的 css
,需要更新頁面時,需要經過什麼操作呢?這就是迴流 reflow
與重繪 repaint
。由於瀏覽器在重新渲染頁面時會先進行 reflow
再進行 repaint
,因此,迴流必將引起重繪,而重繪不一定會引起迴流。
重繪:當前元素的樣式(背景顏色、字型顏色等)發生改變的時候,我們只需要把改變的元素重新的渲染一下即可,重繪對瀏覽器的效能影響較小。發生重繪的情形:改變容器的外觀風格等,比如 background:black
等。改變外觀,不改變佈局,不影響其他的 DOM
。
迴流:是指瀏覽器為了重新渲染部分或者全部的文件而重新計算文件中元素的位置和幾何構造的過程。
因為迴流可能導致整個 DOM
樹的重新構造,所以是效能的一大殺手,一個元素的迴流導致了其所有子元素以及 DOM
中緊隨其後的祖先元素的隨後的迴流。下面貼出會觸發瀏覽器 reflow
的變化:
- 頁面首次渲染
- 瀏覽器視窗大小發生改變
- 元素尺寸或位置發生改變
- 元素內容變化(文字數量或圖片大小等等)
- 元素字型大小變化
- 新增或者刪除可見的DOM元素
- 啟用CSS偽類(例如::hover)
- 查詢某些屬性或呼叫某些方法
優化方案:
CSS
- 避免使用
table
佈局。 - 儘可能在
DOM
樹的最末端改變class
。 - 避免設定多層內聯樣式。
- 將動畫效果應用到
position
屬性為absolute
或fixed
的元素上。 - 避免使用
CSS
表示式(例如:calc()
)。
JavaScript
- 避免頻繁操作樣式,最好一次性重寫
style
屬性,或者將樣式列表定義為class
並一次性更改class
屬性。 - 避免頻繁操作
DOM
,建立一個documentFragment
,在它上面應用所有DOM
操作,最後再把它新增到文件中。 - 也可以先為元素設定
display: none
,操作結束後再把它顯示出來。因為在display
屬性為none
的元素上進行的DOM
操作不會引發迴流和重繪。 - 避免頻繁讀取會引發迴流/重繪的屬性,如果確實需要多次使用,就用一個變數快取起來。
- 對具有複雜動畫的元素使用絕對定位,使它脫離文件流,否則會引起父元素及後續元素頻繁迴流。
10、JS物件的深複製
一般的思路就是遞迴解決,對不同的資料型別做不同的處理:
function deepCopy (obj) {
let result = {}
for (let key in obj) {
if (obj[key] instanceof Object || obj[key] instanceof Array) {
result[key] = deepCopy(obj[key])
} else {
result[key] = obj[key]
}
}
return result
}
複製程式碼
這個只能複製內部有陣列、物件或其他基礎資料型別的物件,假如有一些像RegExp
、Date
這樣的複雜物件複製的結果就是一個{}
,無法正確進行復制,因為沒有對這些特殊物件進行單獨的處理。若要參考對複雜物件進行復制,可以參考lodash
中陣列深複製方法_.cloneDeep()
的實現方案,下面這篇文章對陣列深複製的方法進行了詳細的解析,有一定參考價值:
jerryzou.com/posts/dive-…
另外如果要複製的物件資料結構較為簡單,沒有複雜物件的資料,那麼可以用最簡便的方法:
let cloneResult = JSON.parse(JSON.stringify(targetObj))
複製程式碼
11、JS運算精度丟失
此前轉載了一篇文章,對JavaScript運算精度丟失的原因及解決方案都有比較詳細的說明: blog.csdn.net/qq_35271556…
瀏覽器相關
1、瀏覽器從載入到渲染的過程,比如輸入一個網址到顯示頁面的過程
載入過程:
- 瀏覽器根據 DNS 伺服器解析得到域名的 IP 地址
- 向這個 IP 的機器傳送 HTTP 請求
- 伺服器收到、處理並返回 HTTP 請求
- 瀏覽器得到返回內容
渲染過程:
- 根據 HTML 結構生成 DOM 樹
- 根據 CSS 生成 CSSOM
- 將 DOM 和 CSSOM 整合形成 RenderTree
- 根據 RenderTree 開始渲染和展示
- 遇到
<script>
時,會執行並阻塞渲染
2、瀏覽器快取機制
參考文章:segmentfault.com/a/119000001…
3、效能優化
參考文章:blog.csdn.net/na_sama/art…
Vue
1、元件間通訊方式
Vue的官方文件對元件間的通訊方式做了詳細的說明:cn.vuejs.org
父元件向子元件傳輸
- 最常用的方式是在子元件標籤上傳入資料,在子元件內部用
props
接收:
// 父元件
<template>
<children name="boy"></children>
</template>
<script>
// 子元件:children
export default {
props: {
name: String
}
}
</script>
複製程式碼
- 還可以在子元件中用
this.$parent
訪問父元件的例項,不過官方文件有這樣一段文字,很好的說明了$parent
的意義:節制地使用$parent
和$children
—— 它們的主要目的是作為訪問元件的應急方法。更推薦用props
和events
實現父子元件通訊。
子元件向父元件傳輸
- 一般在子元件中使用
this.$emit('eventName', 'data')
,然後在父元件中的子元件標籤上監聽eventName
事件,並在引數中獲取傳過來的值。
// 子元件
export default {
mounted () {
this.$emit('mounted', 'Children is mounted.')
}
}
複製程式碼
<template>
<children @mounted="mountedHandle"></children>
</template>
<script>
// 父元件
export default {
methods: {
mountedHandle (data) {
console.log(data) // Children is mounted.
}
}
}
</script>
複製程式碼
- 與
$parent
一樣,在父元件中可以通過訪問this.$children
來訪問元件的所有子元件例項。
非父子元件之間的資料傳遞
-
對於非父子元件間,且具有複雜元件層級關係的情況,可以通過
Vuex
進行元件間資料傳遞: vuex.vuejs.org/zh/ -
在
Vue 1.0
中常用的event bus
方式進行的全域性資料傳遞,在Vue 2.0
中已經被移除,官方文件中有說明:$dispatch
和$broadcast
已經被棄用。請使用更多簡明清晰的元件間通訊和更好的狀態管理方案,如:Vuex
。
2、雙向繫結原理
blog.seosiwei.com/detail/35 blog.seosiwei.com/detail/36 blog.seosiwei.com/detail/37