前言
因爲一些特殊原因,剛入職半年又重新找工作,發現之前的面試題已經忘了,因此在此記錄並不斷更新。
面試題不是靠死記硬背,而是在短時間儘量多的備戰可能問到的題目,答案則是需要理解更深層的原理,回答時才能夠從容不迫,這便需要花費更多的時間。共勉!希望每個人面試都能有個好結果~
CSS部分
元素水平垂直居中
1、margin 已知高寬
1)margin爲auto
.box {
background-color: #FF8C00;
width: 300px;
height: 300px;
position: relative;
}
.content {
background-color: #F00;
width: 100px;
height: 100px;
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
margin: auto;
}
2)margin爲指定值
.box {
background-color: #FF8C00;
width: 300px;
height: 300px;
position: relative;
}
.content {
background-color: #F00;
width: 100px;
height: 100px;
position: absolute;
left: 50%;
top: 50%;
margin: -50px 0 0 -50px;
}
2、transform 未知高寬
.box {
background-color: #FF8C00;
width: 300px;
height: 300px;
position: relative;
}
.content {
background-color: #F00;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}
3、flex佈局 未知高寬
.box {
background-color: #FF8C00;
width: 300px;
height: 300px;
display: flex;
justify-content: center;
align-items: center;
}
.content {
background-color: #F00;
}
4、table-cell佈局
.box {
background-color: #FF8C00;
width: 300px;
height: 300px;
display: table;
}
.content {
background-color: #F00;
display: table-cell;
vertical-align: middle;
text-align: center;
}
.inner {
background-color: #000;
display: inline-block;
width: 20%;
height: 20%;
}
css優先級算法
css屬於層疊樣式,會來自於父組件的繼承樣式以及自己的特殊聲明樣式,那麼如何判斷其樣式的優先級呢,就需要有一定的計算方式。
各選擇器優先級爲:!important>id選擇器>class、屬性選擇器或僞類>元素和僞元素選擇器>*
其權重通過0,0,0,0的方式進行計算,其中:
1、ID選擇器的特殊性,其值0,1,0,0;
2、類選擇器、屬性選擇器或僞類,加0,0,1,0。
3、元素和僞元素,加0,0,0,1。
4、通配選擇器*
對特殊性沒有貢獻,即0,0,0,0。
5、最後比較特殊的一個標誌!important(權重),它沒有特殊性值,但它的優先級是最高的,爲了方便記憶,可以認爲它的特殊性值爲1,0,0,0,0。
例如以下規則的選擇器優先級算法爲:
a{color: yellow;} /*特殊性值:0,0,0,1*/
div a{color: green;} /*特殊性值:0,0,0,2*/
demo a{color: black;} /*特殊性值:0,0,1,1*/
demo input[type="text"]{color: blue;} /*特殊性值:0,0,2,1*/
demo *[type="text"]{color: grey;} /*特殊性值:0,0,2,0*/
#demo a{color: orange;} /*特殊性值:0,1,0,1*/
div#demo a{color: red;} /*特殊性值:0,1,0,2*/
當然,如果優先級計算結果一致,則後聲明的覆蓋先聲明的樣式。
清除浮動
1、額外標籤法
clear:both;
缺點:需要新增多餘的標籤,語義化差。
2、父級添加overflow屬性
overflow: hidden;
缺點:內容增多的時候容易造成不會自動換行導致內容被隱藏掉,無法顯示要溢出的元素。
3、after僞元素清除浮動
.clearfix:after{/*僞元素是行內元素 正常瀏覽器清除浮動方法*/
content: "";
display: block;
height: 0;
clear:both;
visibility: hidden;
}
.clearfix{
*zoom: 1;/*ie6清除浮動的方式 *號只有IE6-IE7執行,其他瀏覽器不執行*/
}
缺點:ie6-7不支持僞元素:after,使用zoom:1觸發hasLayout。
全局reset
JS及ES6部分
new操作符的機制
new 運算符創建一個用戶定義的對象類型的實例或具有構造函數的內置對象的實例。new 關鍵字會進行如下的操作:
1、創建一個空的簡單JavaScript對象(即{});
2、鏈接該對象(即設置該對象的構造函數)到另一個對象 ;
3、將步驟1新創建的對象作爲this的上下文 ;
4、如果該函數沒有返回對象,則返回this。
其代碼原理如下:
function newCreate(Base, ...args){
let obj = {}
obj.__proto__ = Base.prototype
let result = Base.call(obj, ...args)
return result instanceof Object ? result : obj
}
閉包理解
閉包是大部分筆試面試都會問的問題。網上一堆關於閉包問題的解釋,在此只記錄我所學習到的內容。
閉包是函數和聲明該函數的詞法環境的組合,指有權訪問另一函數作用域下的變量的函數。
詞法環境
js沒有塊級作用域概念,函數作用域是在其函數原型的作用域中一層層往上檢索是否有該變量,也就是檢索詞法環境。如下:
function init() {
var name = "Mozilla"; // name 是一個被 init 創建的局部變量
function displayName() { // displayName() 是內部函數,一個閉包
alert(name); // 使用了父函數中聲明的變量
}
displayName();
}
init();
init()
創建了一個局部變量 name
和一個名爲 displayName()
的函數。displayName()
是定義在 init()
裏的內部函數,僅在該函數體內可用。displayName()
內沒有自己的局部變量,然而它可以訪問到外部函數的變量,所以 displayName()
可以使用父函數 init()
中聲明的變量 name
。但是,如果有同名變量 name
在 displayName()
中被定義,則會使用 displayName()
中定義的 name
。
閉包
function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
運行這段代碼和之前的 init()
示例的效果完全一樣。其中的不同 — 也是有意思的地方 — 在於內部函數 displayName()
在執行前,被外部函數返回。
第一眼看上去,也許不能直觀的看出這段代碼能夠正常運行。在一些編程語言中,函數中的局部變量僅在函數的執行期間可用。一旦 makeFunc()
執行完畢,我們會認爲 name
變量將不能被訪問。然而,因爲代碼運行得沒問題,所以很顯然在 JavaScript 中並不是這樣的。
這個謎題的答案是,JavaScript中的函數會形成閉包。 閉包是由函數以及創建該函數的詞法環境組合而成。**這個環境包含了這個閉包創建時所能訪問的所有局部變量。**在我們的例子中,myFunc
是執行 makeFunc
時創建的 displayName
函數實例的引用,而 displayName
實例仍可訪問其詞法作用域中的變量,即可以訪問到 name
。由此,當 myFunc
被調用時,name
仍可被訪問,其值 Mozilla 就被傳遞到alert
中。
閉包缺點
1、內存泄漏
由於閉包會使得函數中的變量都被保存在內存中,內存消耗很大,所以不能濫用閉包,否則會造成網頁的性能問題,在IE中可能導致內存泄露。解決方法是,在退出函數之前,將不使用的局部變量全部刪除。
2、引用參數會修改父函數的值
閉包會在父函數外部,改變父函數內部變量的值。所以,如果你把父函數當作對象(object)使用,把閉包當作它的公用方法(Public Method),把內部變量當作它的私有屬性(private value),這時一定要小心,不要隨便改變父函數內部變量的值。
Promise
Promise面試題內容較多,暫時不整理,獨立一文整理。
finally
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(function(value) {
P.resolve(callback()).then(function(){
return value;
});
},
function (reason) {
P.resolve(callback()).then(function() {
throw reason;
});
});
};
高階函數
高階函數源自於函數式編程,是函數式編程的基本技術。JS中函數可以被賦值給變量,被變量引用,因此可以作爲參數傳遞給函數:
/**
* 數值轉換
* @param {Number} val 要被處理的數值
* @param {Function} fn 處理輸入的val
* @return {Number || String}
*/
const toConvert = function(val, fn) {
return fn(val);
};
const addUnitW = function(val) {
return val + 'W';
};
toConvert(123.1, Math.ceil); // 124
toConvert(123.1, addUnitW); // "123.1W"
另外,JS的回調函數同樣是以實參形式傳入其他函數中,這也是高階函數(在函數式編程中回調函數被稱爲 lambda表達式):
[1, 2, 3, 4, 5].map(d => d ** 2); // [1, 4, 9, 16, 25]
// 以上,等同於:
const square = d => d ** 2;
[1, 2, 3, 4, 5].map(square); // [1, 4, 9, 16, 25]
跨域請求
何爲跨域
瀏覽器處於安全考慮,設計了同源策略,同源策略限制了從同一個源加載的文檔或腳本如何與來自另一個源的資源進行交互。這是一個用於隔離潛在惡意文件的重要安全機制。
如果兩個頁面的協議,端口(如果有指定)和主機都相同,則兩個頁面具有相同的源,否則則爲跨域。
解決方法
1、JSONP
js加載並沒有跨域問題,因此可以添加script標籤訪問外部請求:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script type='text/javascript'>
// 後端返回直接執行的方法,相當於執行這個方法,由於後端把返回的數據放在方法的參數裏,所以這裏能拿到res。
window.jsonpCb = function (res) {
console.log(res)
}
</script>
<script src='http://localhost:9871/api/jsonp?msg=helloJsonp&cb=jsonpCb' type='text/javascript'></script>
</body>
</html>
缺點:1)從外部加載代碼很危險;2)確定請求是否成功並不容易
2、CORS
這個只需要後端配置就行:
ctx.set('Access-Control-Allow-Origin', '*')
3、Node中間件代理
通過Node的中間層代理,轉發請求
var httpProxy = require('http-proxy');
var proxy = httpProxy.createProxyServer({
target: 'http://localhost:9871/', //接口地址
//創建一個代理,從以上路徑獲取數據。
});
4、Nginx代理
通過nginx代理,轉發前端的請求。
server{
# 監聽9099端口
listen 9099;
# 域名是localhost
server_name localhost;
#凡是localhost:9099/api這個樣子的,都轉發到真正的服務端地址http://localhost:9871
location ^~ /api {
proxy_pass http://localhost:9871;
}
}
其中3、4兩點,同源策略是瀏覽器的限制,http協議並不會有此限制,因此通過後端轉發並不存在跨域的問題。
5、圖像ping
缺點:只能獲取Get請求,且只是單向請求,無法獲取響應文本。
async原理
async 函數的實現原理,就是將 Generator 函數和自動執行器,包裝在一個函數裏。
async function fn(args) {
// ...
}
// 等同於
function fn(args) {
return spawn(function* () {
// ...
});
}
async等同於聲明瞭個自執行的函數,參數爲Generator函數。
spawn
函數的實現
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) { // 若已完成,則返回結果
return resolve(next.value);
}
// 若未執行結束,則繼續執行之後的內容
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
其基本過程爲:
返回一個Promise對象,對象內:
聲明自執行函數step,若函數結果done未完成,則繼續遞歸執行step函數。
bind的實現
實現原理:
1、判斷自身是否爲函數,不是則拋出異常;
2、指定this所屬以及函數的參數;
3、返回一個函數,該函數返回調用this主函數的函數;
4、指定函數的原型鏈
MDN給出的polyfill方法:
if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function() {},
fBound = function() {
// this instanceof fBound === true時,說明返回的fBound被當做new的構造函數調用
return fToBind.apply(this instanceof fBound
? this
: oThis,
// 獲取調用時(fBound)的傳參.bind 返回的函數入參往往是這麼傳遞的
aArgs.concat(Array.prototype.slice.call(arguments)));
};
// 維護原型關係
if (this.prototype) {
// 當執行Function.prototype.bind()時, this爲Function.prototype
// this.prototype(即Function.prototype.prototype)爲undefined
fNOP.prototype = this.prototype;
}
// 下行的代碼使fBound.prototype是fNOP的實例,因此
// 返回的fBound若作爲new的構造函數,new生成的新對象作爲this傳入fBound,新對象的__proto__就是fNOP的實例
fBound.prototype = new fNOP();
return fBound;
};
}
Class原理
Class類
ES6使用class定義類:
class Parent {
constructor(name,age){
this.name = name;
this.age = age;
}
speakSomething(){
console.log("I can speek chinese");
}
}
經過babel轉碼之後爲:
var _createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function (Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
}();
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Parent = function () {
function Parent(name, age) {
_classCallCheck(this, Parent);
this.name = name;
this.age = age;
}
_createClass(Parent, [{
key: "speakSomething",
value: function speakSomething() {
console.log("I can speek chinese");
}
}]);
return Parent;
}();
通過ES6創建的類,是不允許你直接調用的。在ES5中,構造函數是可以直接運行的,比如Parent()
。但是在ES6就不行。我們可以看到轉碼的構造函數中有_classCallCheck(this, Parent)
語句,這句話是防止你通過構造函數直接運行的。你直接在ES6運行Parent()
,這是不允許的,ES6中拋出Class constructor Parent cannot be invoked without 'new'
錯誤。轉碼後的會拋出Cannot call a class as a function.
能夠規範化類的使用方式。
轉碼中的_createClass
方法,它調用Object.defineProperty
方法去給新創建的Parent
添加各種屬性。defineProperties(Constructor.prototype, protoProps)
是給原型添加屬性。如果你有靜態屬性,會直接添加到構造函數defineProperties(Constructor, staticProps)
上。
ES6繼承
require和import的區別
prototype和__proto__區別
isArray原理
isArray用於判斷傳遞的值是否爲數組。使用方式:
// 下面的函數調用都返回 true
Array.isArray([]);
Array.isArray([1]);
Array.isArray(new Array());
// 鮮爲人知的事實:其實 Array.prototype 也是一個數組。
Array.isArray(Array.prototype);
// 下面的函數調用都返回 false
Array.isArray();
Array.isArray({});
Array.isArray(null);
Array.isArray(undefined);
Array.isArray(17);
Array.isArray('Array');
Array.isArray(true);
Array.isArray(false);
Array.isArray({ __proto__: Array.prototype });
其他判斷數組方式
1、instanceof
let a = [];
a instanceof Array; //true
let b = {};
b instanceof Array; //false
缺點:不能夠檢驗iframes對象裏的數組
2、constructor
let a = [1,3,4];
a.constructor === Array;//true
缺點:與instanceof一樣,無法檢驗iframes裏的數組
3、Object.prototype.toString.call()
let a = [1,2,3]
Object.prototype.toString.call(a) === '[object Array]';//true
該方法對iframes裏數組一樣適用
isArray vs instanceof
檢驗數組時,isArray更優於instanceof,因爲Array.isArray能夠檢測iframes。
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[window.frames.length-1].Array;
var arr = new xArray(1,2,3); // [1,2,3]
// Correctly checking for Array
Array.isArray(arr); // true
// Considered harmful, because doesn't work though iframes
arr instanceof Array; // false
isArray實現原理
若不存在改方法,可使用以下方式實現:
if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}
事件輪詢機制
函數防抖
var timer = null;
document.onscroll = function () {
window.clearTimeout(timer);
timer = setTimeout(function() {
console.log('防抖');
}, 300);
}
函數節流
var canRun = true;
document.onscroll = function () {
if(!canRun) return ;
canRun = false;
setTimeout(function () {
console.log('節流')
canRun = true;
}, 300);
}
數組去重
數組隨機排序
Jsonp封裝
Vue部分
MVVM原理
DIFF算法
詳情可看文章:
詳解vue的diff算法
虛擬DOM渲染過程
把模板編譯爲render函數
實例進行掛載, 根據根節點render函數的調用,遞歸的生成虛擬dom
利用DIFF 算法對比虛擬dom,渲染到真實dom
組件內部data發生變化,組件和子組件引用data作爲props重新調用render函數,生成虛擬dom, 返回到步驟3
詳情可看文章:
vue核心之虛擬DOM(vdom)
vue生成AST源碼解析
computed和watch的區別
MVC和MVVM的區別
v-bind和v-model的區別
vue的生命週期
vue路由鉤子
vue路由鉤子理解
路由間跳轉
路由間參數傳遞
vue組件間傳遞參數
vue中event bus的實現
keep-alive作用和原理
類似於緩存代理模式
vuex的理解
vuex數據響應原理
vue插槽slot原理解析
webpack部分
webpack打包原理
webpack熱部署原理
webpack和gulp的區別
其他內容
http請求
http優化
https工作原理
WebSocket
常見的web攻擊方式
SQL注入
XSS攻擊
CSRF攻擊
DOS攻擊
常見設計模式
TCP三次握手和四次揮手
TCP位碼,有6種標示:
SYN(synchronous建立聯機)
ACK(acknowledgement 確認)
PSH(push傳送)
FIN(finish結束)
RST(reset重置)
URG(urgent緊急)
Sequence number(順序號碼)
Acknowledge number(確認號碼)
三次握手
第一次握手:主機A發送位碼爲syn=1,隨機產生seq number=x的數據包到服務器,客戶端進入SYN_SEND狀態,等待服務器的確認;主機B由SYN=1知道,A要求建立聯機;
第二次握手:主機B收到請求後要確認聯機信息,向A發送ack number(主機A的seq+1),syn=1,ack=1,隨機產生seq=y的包,此時服務器進入SYN_RECV狀態;
第三次握手:主機A收到後檢查ack number是否正確,即第一次發送的seq number+1,以及位碼ack是否爲1,若正確,主機A會再發送ack number(主機B的seq+1),ack=1,主機B收到後確認seq值與ack=1則連接建立成功。客戶端和服務器端都進入ESTABLISHED狀態,完成TCP三次握手。
四次揮手
第一次揮手:主機1(可以使客戶端,也可以是服務器端),設置Sequence Number和Acknowledgment Number,向主機2發送一個FIN報文段;此時,主機1進入FIN_WAIT_1狀態;這表示主機1沒有數據要發送給主機2了;
第二次揮手:主機2收到了主機1發送的FIN報文段,向主機1回一個ACK報文段,Acknowledgment Number爲Sequence Number加1;主機1進入FIN_WAIT_2狀態;主機2告訴主機1,我也沒有數據要發送了,可以進行關閉連接了;
第三次揮手:主機2向主機1發送FIN報文段,請求關閉連接,同時主機2進入CLOSE_WAIT狀態;
第四次揮手:主機1收到主機2發送的FIN報文段,向主機2發送ACK報文段,然後主機1進入TIME_WAIT狀態;主機2收到主機1的ACK報文段以後,就關閉連接;此時,主機1等待2MSL後依然沒有收到回覆,則證明Server端已正常關閉,那好,主機1也可以關閉連接了。
函數式編程
函數式編程思想
1、聲明式編程
命令式主張告訴編譯器“如何”做,聲明式告訴編譯器“做什麼”,如何做的部分(獲得數組長度,循環遍歷每一項)被抽象到高階函數中,forEach就是這樣一個內置函數。
// 命令式方式
var array = [0, 1, 2, 3]
for(let i = 0; i < array.length; i++) {
array[i] = Math.pow(array[i], 2)
}
array; // [0, 1, 4, 9]
// 聲明式方式
[0, 1, 2, 3].map(num => Math.pow(num, 2))
2、純函數
純函數是對相同輸入返回相同輸出的函數,不依賴(包含)任何外部變量,所以也不會產生改變外部環境變量的副作用。
3、引用透明
所有函數對於相同的輸入都將返回相同的值(函數只依賴參數的輸入,不依賴於其他全局數據,即函數內部沒有全局引用),這使並行代碼和緩存(用值直接替換函數的結果)成爲可能。
// 非引用透明
var counter = 0
function increment() {
return ++counter
}
// 引用透明
var increment = (counter) => counter + 1
4、不可變性
不可變數據是指那些創建後不能更改的數據。與許多其他語言一樣,JavaScript 裏有一些基本類型(String,Number 等)從本質上是不可變的,但是對象就是在任意的地方可變。
考慮一個簡單的數組排序代碼:
var sortDesc = function(arr) {
return arr.sort(function(a, b) {
return a - b
})
}
var arr = [1, 3, 2]
sortDesc(arr) // [1, 2, 3]
arr // [1, 2, 3]
重要的概念
1、 柯里化 curry
function currying(fn, ...args) {
if (args.length >= fn.length) {
return fn(...args)
}
return function (...args2) {
return currying(fn, ...args, ...args2)
}
}
2、組合函數 compose
const compose = (a, b)=>(c)=>a(b(c))
3、部分應用 partial
function partial(fn, ...args) {
return fn.bind(null, ...args)
}