2019秋招知識盲點總結
開始
儘管秋招還沒有拿到offer(好難過),但是一些知識點還是要總結的,既然自己選了這條路,那就一定要堅定不移的走下去......
注意 new
運算子的優先順序
function Foo() { return this; } Foo.getName = function () { console.log('1'); }; Foo.prototype.getName = function () { console.log('2'); }; new Foo.getName(); // -> 1 new Foo().getName(); // -> 2
通過這段程式碼可以看出:new Foo()
的優先順序高於 new Foo
.
對於程式碼1 來說:是將 Foo.getName
當成一個建構函式來執行,執行建構函式所以輸出為1.
對於程式碼2來說:通過 new Foo()
建立了一個Foo的例項,通過例項訪問其原型鏈上的 方法所以輸出為2.
注意非匿名的立即執行函式
var foo = 1; // 有名立即執行函式 (function foo(){ foo = 1; console.log(foo); })(); // 執行這段程式碼會輸出什麼呢? // -> ƒ foo() { foo = 10 ; console.log(foo) } // 再去訪問 foo 的值 foo // -> 1
當JS執行器遇到非匿名的立即執行函式時,會建立一個輔助的特定物件,然後將函式名稱作為這個物件的屬性,因此行數內部才可以訪問到 foo
,但這個值是隻讀的,所以對其重新賦值不會生效,所以列印結果還是這個函式,並且外部的值也沒有發生改變。
關於物件的深拷貝
-
可以使用
JSON.stringify
和JSON.parse
這個兩個方法優點:簡單
缺點:會忽略掉
undefined
; 不能序列化函式 ; 不能解決迴圈引用的物件function clone(obj) { return JSON.parse(JSON.stringify(obj)); }
-
使用遞迴迴圈賦值的方式
優點:可以處理
undefined
缺點:實現相對麻煩,效率不高
function clone(obj) { if(!obj || typeof obj !== 'object') { return; } var _obj = obj.constructor === Object ? {} : []; for(let key in obj) { if(typeof obj[key] === 'object') { _obj[key] = clone(obj[key]); } else { _obj[key] = obj[key]; } } return _obj; } // 或者 function clone(obj) { if(!obj || typeof obj !== 'object') throw new TypeError('params typeError'); let _obj = obj.constructor === Object ? {} : []; Object.getOwnPropertyNames(obj).forEach(name => { if(typeof obj[name] === 'object') { _obj[name] = clone(obj[name]); } else { _obj[name] = obj[name]; } }); return _obj; }
-
使用內建
MessageChannel
物件優點:是內建函式中處理深拷貝效能最快的
缺點:不能處理函式(會報錯)
function clone(obj) { return new Promise(resolve => { let {port1, port2} = new MessageChannel(); port2.onmessage = ev => resolve(ev.data); port1.postMessage(obj); }); } clone(obj).then(console.log);
關於async/await
, promise
非同步執行順序
想解決這個問題,就必須知道 `await` 做了什麼?
剛開始以為await
會一直等待表達執行的執行結果,之後才會執行後面的程式碼。實際上await
是一個讓出執行緒的標誌(遇到await
會立即返回一個pending
狀態的promise)。await後面的函式會先執行一遍,然後就會跳出整個async
函式來執行後面js程式碼。等本輪事件迴圈執行完又跳回到async
函式中等待await後面表示式的返回值,如果返回值為非promise
則繼續執行async後面的程式碼,否則將promse
加入佇列。
且看一道面試題(分析程式碼執行 順序):
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function () {
console.log("settimeout");
},0);
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log("script end");
OK,那接下來具體分析執行過程:
首先輸出 "script start" ,然後立即將定時器加入非同步事件佇列。執行 async1() ,輸出 "async1 start" ,進入 async2() ,輸出 "async2" ,跳出整個 async1() 函式來執行後面js程式碼,執行promise執行器中的內容,輸出 "promise1" ,執行resolve()回撥,將then函式中的內容加入非同步事件佇列,接著輸出 "script end" 。回到 async1() 的await等待 async2() 函式的返回值,因為返回值是一個promise例項,將promise加入非同步事件佇列。此時的同步程式碼執行完畢,輪詢並從佇列拿出程式碼放入主執行緒執行,所以輸出 "promise2" ,繼續執行 async1() 中的後續內容,輸出 "async1 end" ,最後取出定時器中的內容並執行,輸出 "settimeout" 。
綜上所述:
script start
async1 start
async2
promise1
script end
promise2
async1 end
settimeout
那麼再看一個例子應該會簡單很多:
function testFunc() {
console.log("testFunc..."); // 2
return "testFunc";
}
async function testAsync() {
console.log("testAsync..."); // 7
return Promise.resolve("hello testAsync");
}
async function foo() {
console.log("test start..."); // 1
const v1 = await testFunc();
connsole.log('hello world.'); // 5
console.log(v1); // 6 testFunc
const v2 = await testAsync();
console.log(v2); // 9 hello testAsync
}
foo();
var promise = new Promise(resolve => {
console.log("promise start.."); // 3
resolve("promise");
});
promise.then(val => console.log(val)); // 8 promise
console.log("test end..."); // 4
防抖和節流
- 防抖:如果使用者多次呼叫且間隔小於wait值,那麼就會被轉化為一次呼叫。
- 節流:多次執行函式轉化為,每隔一定時間(wait)呼叫函式 。
一個簡單的防抖函式:
function debounce(func, wait) {
let timer = null;
return function(...params) {
// 如果定時器存在則清除
if(timer){
clearTimeout(timer);
}
// 重新開始定時執行
timer = setTimeout(() => {
func.apply(this, params);
}, wait);
}
}
缺點:只能在最後執行,不能立即被執行,在某些情況下不適用。
改進...
function debounce(func, wait, immediate) {
let timer, context, args;
// 定時器
let later = function() {
return setTimeout(() => {
timer = null;
if(!immediate) {
func.apply(context, args);
}
}, wait);
}
return function(...params) {
if(!timer) {
timer = later();
// immediate 為 true,則立即執行
// 否則 快取上下文 和 引數
if(immediate) {
func.apply(this, params);
} else {
context = this;
args = params;
}
} else {
clearTimeout(timer);
timer = later();
}
}
}
一個簡單的節流函式:
// 節流函式
// 快速的多次執行,轉化為等待wait時間再去執行
function throttle(func, wait) {
var timer = null;
var context = null;
return function(...args) {
context = this;
if(!timer) {
timer = setTimeout(function() {
timer = null;
func.apply(context, args);
}, wait);
}
}
}
// 如果想讓第一次呼叫立即執行也非常簡單
僅需要將 func.apply(context, args) 提到定時器外邊即可。
節流函式除了可以使用定時器實現以外,當然也可以有其他方式:
// 第一次呼叫會被立即執行
function throttle(func, wait) {
var prev = 0;
var context = null;
return function(...args) {
var now = +new Date();
context = this;
if(now -prev > wait) {
func.apply(context,args);
prev = now;
}
}
}
call、apply和bind
怎麼去模擬一個call函式呢?
思路:call一個非常重要的作用就是改變上下文環境也就是this,我們可以給使用者傳入的上下文物件上新增一個函式,通過這個上下文物件去執行函式,然後將這個函式刪除,返回結果就可以了。
Function.prototype.myCall = function(context, ...args) {
context = context || window;
// 給上下文物件上新增這個函式
context.fn = this;
// 通過這個上下文物件去執行函式
let result = context.fn(...args);
// 將這個函式刪除
delete context.fn;
return result;
}
call既然都實現了,那麼apply也是類似的,只不過傳入的引數是一個數組而已。
Function.prototype.myApply = function(context, arr) {
context = context || window;
arr = arr || [];
let type = {}.toString.call(arr).slice(8,-1);
if(type !== 'Array')
throw new TypeError('CreateListFromArrayLike called on non-object');
context.fn = this;
let result = context.fn(...arr);
delete context.fn;
return result;
}
模擬bind函式,bind函式應該返回一個新的函式。
Function.prototype.myBind = function(context, ...args) {
// 儲存當前的函式
let func = this;
return function F(...params) {
if(this instanceof F) {
return new func(...args, ...params);
}
return func.apply(context,[...args,...params]);
}
}
陣列降維
function flattenDeep(arr) {
if(!Array.isArray(arr))
return [arr];
return arr.reduce((prev,cur) => {
return [...prev, ...flattenDeep(cur)];
},[]);
}
flattenDeep([1, [[2], [3, [4]], 5]]);
棧的壓入和彈出
輸入兩個整數序列,第一個序列表示棧的壓入順序,請判斷第二個序列是否可能為該棧的彈出順序。假設壓入棧的所有數字均不相等。例如序列1,2,3,4,5是某棧的壓入順序,序列4,5,3,2,1是該壓棧序列對應的一個彈出序列,但4,3,5,1,2就不可能是該壓棧序列的彈出序列。
function IsPopOrder(pushV,popV){
if(pushV.length === 0) return false;
var stack = []; // 模擬棧
for(var i = 0, j = 0; i < pushV.length;){
stack.push(pushV[i]);
i += 1;
// 壓入棧的值需要被彈出
while(j < popV.length && stack[stack.length-1] === popV[j]){
stack.pop();
j++;
if(stack.length === 0) break;
}
}
return stack.length === 0;
}
利用棧模擬佇列
思路:
- 對棧A新增資料。
- 如果棧B為空,迴圈將棧A中內容彈出放入棧B,並彈出棧B最後一項
- 如果棧B不為空,則直接彈出棧B的最後一項
var stackA = [];
var stackB = [];
function push(node){
stackA.push(node);
}
function pop(){
if(!stackB.length){
while(stackA.length){
stackB.push(stackA.pop());
}
}
return stackB.pop();
}
Fetch和ajax之間的區別
fetch
- Fetch API是基於Promise設計的
- 容易同構(前後端運行同一套程式碼)
- 語法簡潔,更加語義化
- 原生支援率不高,可以用polyfill相容IE8+瀏覽器
fetch(url).then(function(response){
return response.json();
}).then(function(data){
console.log(data);
}).catch(function(err){
console.log(err);
});
ajax
- 設計粗糙,不關注分離原則
- 基於事件的非同步模型,不夠友好
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'json';
xhr.onload = function(){
console.log(xhr.response);
}
xhr.onerror = function(){
console.log('error');
}
xhr.send();
Fetch常見坑
- fetch請求預設是不帶cookie的,需要設定
fetch(url, {credentials: 'include'})
- 伺服器返回400,500錯誤碼時不會reject,只有網路錯誤導致不能完成時,才會reject。
- IE8, 9 的 XHR 不支援 CORS 跨域。
歸併排序
function merge(left, right) {
var temp = [];
while(left.length && right.length) {
if(left[0] < right[0])
temp.push(left.shift());
else
temp.push(right.shift());
}
return temp.concat(left,right);
}
function mergeSort(arr) {
if(arr.length === 1)
return arr;
var mid = (arr.length/2)|0;
var left = arr.slice(0,mid);
var right = arr.slice(mid);
return merge(mergeSort(left), mergeSort(right));
}
箭頭函式的this原理
this
指向固定化,並不是因為箭頭函式內部有繫結 this
的機制,實際原因是箭頭函式沒有自己的 this
,導致內部的this就是外層程式碼的 this
,也正是因為沒有 this
,所以箭頭函式不能用作建構函式。
js相關尺寸
BFC原理
- 在BFC垂直方向元素邊距會發生重疊
- 不會與浮動元素的box重合
- 獨立的容器,裡外互不影響
-
浮動元素參與計算
自定義事件
var content = document.querySelector('.content');
// 自定義事件
var evt = new Event('custom');
var customEvt = new CustomEvent('customEvt', {
// 通過這個屬性傳遞引數
detail: {
name: 'tom',
age: 12
}
});
content.addEventListener('custom', (e) => {
console.log('自定義事件被觸發,無引數...');
console.log(e);
});
content.addEventListener('customEvt', (e) => {
console.log('自定義事件被觸發,有引數...');
console.log(e);
console.log(e.detail);
});
// 點選時觸發這個自定義事件
content.addEventListener('click', (e) => {
content.dispatchEvent(evt);
content.dispatchEvent(customEvt);
});
變數提升
var foo = 3; // 不在同一個作用域
function hoistVariable() {
// 內部變數提升導致 foo 的初始值為undefined
// 所以 foo = 5;
var foo = foo || 5;
console.log(foo); // 5
}
hoistVariable();
上邊的比較簡單,看一個函式和變數同名,關於變數提升的小問題。
var a = 6;
function b(){
console.log(a); // @1
var a = 4;
function a(){
alert(4);
}
console.log(a); //@2
}
b();
- 因為JavaScript中的函式是一等公民,函式宣告的優先順序最高(高於變數提升),會被提升至當前作用域最頂端,所以在
@1
輸出的是function a(){alert(4);}
- 接下來執行
a=4;
這一句,重新對a
進行賦值。 - 函式已被提升,所以不考慮,所以在
@2
這裡自然會輸出4
。
如果還不能理解?且看預編譯後的程式碼:
var a;
a = 6;
function b(){
var a;
a = function a(){ // 函式先提升
alert(4);
}
console.log(a); // @1
a = 4;
console.log(a); // @2
}
b(); // 結果已經非常明瞭了
POST和GET的區別
- POST對請求引數的長度沒有限制,而GET如果請求引數過長,會被瀏覽器截斷。
- GET請求引數會直接暴露在URL上,所以不適合用來傳遞敏感資訊。
- GET請求可以被瀏覽器主動快取,而POST請求不可以,除非手動設定。
- GET請求在瀏覽器回退時是無害的,而POST會再次提交請求。
- GET請求產生的URL可以被瀏覽器快取,而POST不可以。
通訊類
1. 前後端如何通訊?
- ajax
- WebSocket
- CORS
2. 如何建立ajax?
// 建立xhr物件
var xhr = XMLHttpRequest ? new XMLHttpRequest() : new window.ActiveXObject('Microsoft.XMLHTTP');
// GET 請求
xhr.open('GET',url,true);
xhr.send();
// POST 請求
xhr.open('POST',url,true);
// 表單資料 , 也可以提交json資料,相應的content-Type: application/json
xhr.setRequestHeader('content-Type', 'application/x-www-from-urlencoded');
xhr.send(dataArr.join('&'));
xhr.onload = function() {
if(xhr.status === 200 || xhr.status === 304) {
var data = xhr.responseText;
// 拿到資料
} else {
// 出問題了
}
}
3. 跨域通訊的幾種方式?
-
JSONP:利用
script
標籤的跨域能力,服務返回一個js函式呼叫,資料作為函式的一個引數來傳遞。var script = document.createElement('script'); script.type = 'text/javascript'; script.src = url; // 跨域地址 document.head.appendChild(script); //有能耐把我這輩子都安排了,不然少他媽扯淡。 setTimeout(function() { document.head.removeChild(script); script = null; }); // 接收資料 function jsonpCallback(data) { console.log(data); }
-
WebSocket:不受同源政策限制。
var ws = new WebSocket('wss://echo.websocket.org'); ws.onopen = function(e) { ws.send('hello...'); } ws.onmessage = function(e) { var data = e.data; } ws.onclose = function() { console.log('close...'); }
-
Hash:利用
location.hash
來傳值。 缺點:資料直接暴露在url中,大小、型別都有限制。1、父窗體可以把資訊寫在子窗體的href的hash上,子視窗通過監聽hashchange事件獲取資訊。
2、子窗體改變父窗體的hash值,那麼就要藉助第三個子窗體,第三個子窗體是第二個子窗體的子窗體。(第三個子窗體要與父窗體同源)
3、第二個子窗體把資訊設定在第三個子窗體的hash值上,然後第三個子窗體改變父窗體的hash值,從而實現跨域。
// 父窗體 var son = document.getElementByTagName('iframe'); son.src = son.src + '#' + data; // 子窗體 window.onhashchange = function() { var data = window.location.hash; }
-
postMessage :
語法:window.postMessage(msg,targetOrigin)
// 視窗A 傳送 BWindow.postMessage('傳送的資料', 'http://B.com'); // 視窗B 接收 window.addEventListener('message', (event) => { event.origin: // http://A.com event.source; // AWindow event.data; // '傳送的資料' });
-
CORS: 跨域資源共享。
fetch(url, { method: 'get', // 頭資訊配置 }).then(() => {});
安全類
- CSRF,跨站請求偽造(Cross-site request forgery)
防禦措施:
Token驗證(請求必須攜帶Token)
Referer 驗證(驗證請求的來源是否可信)
- XSS(cross-site scripting 跨站指令碼攻擊)
原理:注入指令碼
防禦措施:對使用者的輸入做驗證
渲染機制類
1. 什麼是DOCTYPE及作用?
用來宣告文件型別和DTD規範的。
DTD(document type definition)文件型別定義,是一系列的語法規則,用來宣告XML或(X)HTML的檔案型別,瀏覽器會使用它來決定文件型別,決定使用何種協議來解析,以及切換瀏覽器模式。
2. 常見的doctype有哪些?
- HTML5 <!DOCTYPE html>
- HTML4.01 Strict 嚴格模式 (不包含展示性或棄用的元素)
- HTML4.01 Transitional 傳統(寬鬆)模式(包含展示性或棄用的元素)
頁面效能類
提升頁面效能的方式?
- 資源壓縮合並,減少HTTP請求
-
非核心程式碼非同步載入
非同步載入方式:
- 動態指令碼載入
- defer (HTML解析完順序執行)
- async (載入完立即執行)
- 利用瀏覽器快取
- 使用CDN
-
預解析DNS
<meta http-equiv="x-dnns-prefetch-control" content="on"> <link rel="dns-prefetch" href="//host_name_to_prefetch.com"
錯誤監控
- 前端錯誤分類:程式碼(執行)錯誤 資源載入錯誤
- 錯誤捕獲方式
執行錯誤捕獲:(1)try...catch (2)window.onerror
資源載入錯誤 :(1)object.onerror(資源錯誤不冒泡) (2)performance.getEntries() (3)Error事件捕獲(在事件流捕獲階段處理錯誤)
- 跨域js執行錯誤也是可以捕獲的,捕獲的錯誤:script error
-
上報錯誤原理
利用Image物件上報
// 利用Image標籤上報錯(簡單、不需要藉助其他庫) (new Image()).src = 'http://www.baidu.com/test?error=xxx';
二分法查詢
function binarySearch(arr,val,leftIndex,rightIndex) {
if(leftIndex > rightIndex){ return; }
var midIndex = (leftIndex + rightIndex) / 2 | 0;
var midVal = arr[midIndex];
if(val > midVal) {
return binarySearch(arr,val,midIndex+1,rightIndex);
}else if(val < midVal) {
return binarySearch(arr,val,leftIndex,midIndex-1);
}else{
return midIndex;
}
}