1. 程式人生 > >2019秋招知識盲點總結

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.stringifyJSON.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相關尺寸

iCtsdx.png

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值,從而實現跨域。

    iPQsr6.png

    // 父窗體
    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)

    iP1PXt.png

    防禦措施:

    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請求
  • 非核心程式碼非同步載入

    非同步載入方式:

    1. 動態指令碼載入
    2. defer (HTML解析完順序執行)
    3. 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;
    }
}