【面試篇】寒冬求職季之你必須要懂的原生JS(中)

劉小夕發表於2019-04-22
網際網路寒冬之際,各大公司都縮減了HC,甚至是採取了“裁員”措施,在這樣的大環境之下,想要獲得一份更好的工作,必然需要付出更多的努力。

一年前,也許你搞清楚閉包,this,原型鏈,就能獲得認可。但是現在,很顯然是不行了。本文梳理出了一些面試中有一定難度的高頻原生JS問題,部分知識點可能你之前從未關注過,或者看到了,卻沒有仔細研究,但是它們卻非常重要。

本文將以真實的面試題的形式來呈現知識點,大家在閱讀時,建議不要先看我的答案,而是自己先思考一番。儘管,本文所有的答案,都是我在翻閱各種資料,思考並驗證之後,才給出的(絕非複製貼上而來)。但因水平有限,本人的答案未必是最優的,如果您有更好的答案,歡迎在 issue 中留言。

本文篇幅較長,但是滿滿的都是乾貨!並且還埋伏了可愛的表情包,希望小夥伴們能夠堅持讀完。

寫文超級真誠的小姐姐祝願大家都能找到心儀的工作。

如果你還沒讀過上篇【上篇和中篇並無依賴關係,您可以讀過本文之後再閱讀上篇】,可戳【面試篇】寒冬求職季之你必須要懂的原生JS(上)

【面試篇】寒冬求職季之你必須要懂的原生JS(中)

小姐姐花了近百個小時才完成這篇文章,篇幅較長,希望大家閱讀時多花點耐心,力求真正的掌握相關知識點。

1.說一說JS非同步發展史

非同步最早的解決方案是回撥函式,如事件的回撥,setInterval/setTimeout中的回撥。但是回撥函式有一個很常見的問題,就是回撥地獄的問題(稍後會舉例說明);

為了解決回撥地獄的問題,社群提出了Promise解決方案,ES6將其寫進了語言標準。Promise解決了回撥地獄的問題,但是Promise也存在一些問題,如錯誤不能被try catch,而且使用Promise的鏈式呼叫,其實並沒有從根本上解決回撥地獄的問題,只是換了一種寫法。

ES6中引入 Generator 函式,Generator是一種非同步程式設計解決方案,Generator 函式是協程在 ES6 的實現,最大特點就是可以交出函式的執行權,Generator 函式可以看出是非同步任務的容器,需要暫停的地方,都用yield語句註明。但是 Generator 使用起來較為複雜。

ES7又提出了新的非同步解決方案:async/await,async是 Generator 函式的語法糖,async/await 使得非同步程式碼看起來像同步程式碼,非同步程式設計發展的目標就是讓非同步邏輯的程式碼看起來像同步一樣。

1.回撥函式: callback

//node讀取檔案
fs.readFile(xxx, 'utf-8', function(err, data) {
    //code
});
複製程式碼

回撥函式的使用場景(包括但不限於):

  1. 事件回撥
  2. Node API
  3. setTimeout/setInterval中的回撥函式

非同步回撥巢狀會導致程式碼難以維護,並且不方便統一處理錯誤,不能try catch 和 回撥地獄(如先讀取A文字內容,再根據A文字內容讀取B再根據B的內容讀取C...)。

fs.readFile(A, 'utf-8', function(err, data) {
    fs.readFile(B, 'utf-8', function(err, data) {
        fs.readFile(C, 'utf-8', function(err, data) {
            fs.readFile(D, 'utf-8', function(err, data) {
                //....
            });
        });
    });
});
複製程式碼

2.Promise

Promise 主要解決了回撥地獄的問題,Promise 最早由社群提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise物件。

那麼我們看看Promise是如何解決回撥地獄問題的,仍然以上文的readFile為例。

function read(url) {
    return new Promise((resolve, reject) => {
        fs.readFile(url, 'utf8', (err, data) => {
            if(err) reject(err);
            resolve(data);
        });
    });
}
read(A).then(data => {
    return read(B);
}).then(data => {
    return read(C);
}).then(data => {
    return read(D);
}).catch(reason => {
    console.log(reason);
});
複製程式碼

想要執行程式碼看效果,請戳(小姐姐使用的是VS的 Code Runner 執行程式碼): github.com/YvetteLau/B…

思考一下在Promise之前,你是如何處理非同步併發問題的,假設有這樣一個需求:讀取三個檔案內容,都讀取成功後,輸出最終的結果。有了Promise之後,又如何處理呢?程式碼可戳: github.com/YvetteLau/B…

注: 可以使用 bluebird 將介面 promise化;

引申: Promise有哪些優點和問題呢?

3.Generator

Generator 函式是 ES6 提供的一種非同步程式設計解決方案,整個 Generator 函式就是一個封裝的非同步任務,或者說是非同步任務的容器。非同步操作需要暫停的地方,都用 yield 語句註明。

Generator 函式一般配合 yield 或 Promise 使用。Generator函式返回的是迭代器。對生成器和迭代器不瞭解的同學,請自行補習下基礎。下面我們看一下 Generator 的簡單使用:

function* gen() {
    let a = yield 111;
    console.log(a);
    let b = yield 222;
    console.log(b);
    let c = yield 333;
    console.log(c);
    let d = yield 444;
    console.log(d);
}
let t = gen();
//next方法可以帶一個引數,該引數就會被當作上一個yield表示式的返回值
t.next(1); //第一次呼叫next函式時,傳遞的引數無效
t.next(2); //a輸出2;
t.next(3); //b輸出2; 
t.next(4); //c輸出3;
t.next(5); //d輸出3;
複製程式碼

為了讓大家更好的理解上面程式碼是如何執行的,我畫了一張圖,分別對應每一次的next方法呼叫:

【面試篇】寒冬求職季之你必須要懂的原生JS(中)

仍然以上文的readFile為例,使用 Generator + co庫來實現:

const fs = require('fs');
const co = require('co');
const bluebird = require('bluebird');
const readFile = bluebird.promisify(fs.readFile);

function* read() {
    yield readFile(A, 'utf-8');
    yield readFile(B, 'utf-8');
    yield readFile(C, 'utf-8');
    //....
}
co(read()).then(data => {
    //code
}).catch(err => {
    //code
});

複製程式碼

不使用co庫,如何實現?能否自己寫一個最簡的my_co?請戳: github.com/YvetteLau/B…

PS: 如果你還不太瞭解 Generator/yield,建議閱讀ES6相關文件。

4.async/await

ES7中引入了 async/await 概念。async其實是一個語法糖,它的實現就是將Generator函式和自動執行器(co),包裝在一個函式中。

async/await 的優點是程式碼清晰,不用像 Promise 寫很多 then 鏈,就可以處理回撥地獄的問題。錯誤可以被try catch。

仍然以上文的readFile為例,使用 Generator + co庫來實現:

const fs = require('fs');
const bluebird = require('bluebird');
const readFile = bluebird.promisify(fs.readFile);


async function read() {
    await readFile(A, 'utf-8');
    await readFile(B, 'utf-8');
    await readFile(C, 'utf-8');
    //code
}

read().then((data) => {
    //code
}).catch(err => {
    //code
});
複製程式碼

可執行程式碼,請戳:github.com/YvetteLau/B…

思考一下 async/await 如何處理非同步併發問題的? github.com/YvetteLau/B…

如果你有更好的答案或想法,歡迎在這題目對應的github下留言:說一說JS非同步發展史


2.談談對 async/await 的理解,async/await 的實現原理是什麼?

async/await 就是 Generator 的語法糖,使得非同步操作變得更加方便。來張圖對比一下:

【面試篇】寒冬求職季之你必須要懂的原生JS(中)

async 函式就是將 Generator 函式的星號(*)替換成 async,將 yield 替換成await。

我們說 async 是 Generator 的語法糖,那麼這個糖究竟甜在哪呢?

1)async函式內建執行器,函式呼叫之後,會自動執行,輸出最後結果。而Generator需要呼叫next或者配合co模組使用。

2)更好的語義,async和await,比起星號和yield,語義更清楚了。async表示函式裡有非同步操作,await表示緊跟在後面的表示式需要等待結果。

3)更廣的適用性。co模組約定,yield命令後面只能是 Thunk 函式或 Promise 物件,而async 函式的 await 命令後面,可以是 Promise 物件和原始型別的值。

4)返回值是Promise,async函式的返回值是 Promise 物件,Generator的返回值是 Iterator,Promise 物件使用起來更加方便。

async 函式的實現原理,就是將 Generator 函式和自動執行器,包裝在一個函式裡。

具體程式碼試下如下(和spawn的實現略有差異,個人覺得這樣寫更容易理解),如果你想知道如何一步步寫出 my_co ,可戳: github.com/YvetteLau/B…

function my_co(it) {
    return new Promise((resolve, reject) => {
        function next(data) {
            try {
                var { value, done } = it.next(data);
            }catch(e){
                return reject(e);
            }
            if (!done) { 
                //done為true,表示迭代完成
                //value 不一定是 Promise,可能是一個普通值。使用 Promise.resolve 進行包裝。
                Promise.resolve(value).then(val => {
                    next(val);
                }, reject);
            } else {
                resolve(value);
            }
        }
        next(); //執行一次next
    });
}
function* test() {
    yield new Promise((resolve, reject) => {
        setTimeout(resolve, 100);
    });
    yield new Promise((resolve, reject) => {
        // throw Error(1);
        resolve(10)
    });
    yield 10;
    return 1000;
}

my_co(test()).then(data => {
    console.log(data); //輸出1000
}).catch((err) => {
    console.log('err: ', err);
});
複製程式碼

如果你有更好的答案或想法,歡迎在這題目對應的github下留言:談談對 async/await 的理解,async/await 的實現原理是什麼?


3.使用 async/await 需要注意什麼?

  1. await 命令後面的Promise物件,執行結果可能是 rejected,此時等同於 async 函式返回的 Promise 物件被reject。因此需要加上錯誤處理,可以給每個 await 後的 Promise 增加 catch 方法;也可以將 await 的程式碼放在 try...catch 中。
  2. 多個await命令後面的非同步操作,如果不存在繼發關係,最好讓它們同時觸發。
//下面兩種寫法都可以同時觸發
//法一
async function f1() {
    await Promise.all([
        new Promise((resolve) => {
            setTimeout(resolve, 600);
        }),
        new Promise((resolve) => {
            setTimeout(resolve, 600);
        })
    ])
}
//法二
async function f2() {
    let fn1 = new Promise((resolve) => {
            setTimeout(resolve, 800);
        });
    
    let fn2 = new Promise((resolve) => {
            setTimeout(resolve, 800);
        })
    await fn1;
    await fn2;
}
複製程式碼
  1. await命令只能用在async函式之中,如果用在普通函式,會報錯。
  2. async 函式可以保留執行堆疊。
/**
* 函式a內部執行了一個非同步任務b()。當b()執行的時候,函式a()不會中斷,而是繼續執行。
* 等到b()執行結束,可能a()早就* 執行結束了,b()所在的上下文環境已經消失了。
* 如果b()或c()報錯,錯誤堆疊將不包括a()。
*/
function b() {
    return new Promise((resolve, reject) => {
        setTimeout(resolve, 200)
    });
}
function c() {
    throw Error(10);
}
const a = () => {
    b().then(() => c());
};
a();
/**
* 改成async函式
*/
const m = async () => {
    await b();
    c();
};
m();

複製程式碼

報錯資訊如下,可以看出 async 函式可以保留執行堆疊。

【面試篇】寒冬求職季之你必須要懂的原生JS(中)

如果你有更好的答案或想法,歡迎在這題目對應的github下留言:使用 async/await 需要注意什麼?


4.如何實現 Promise.race?

在程式碼實現前,我們需要先了解 Promise.race 的特點:

  1. Promise.race返回的仍然是一個Promise. 它的狀態與第一個完成的Promise的狀態相同。它可以是完成( resolves),也可以是失敗(rejects),這要取決於第一個Promise是哪一種狀態。

  2. 如果傳入的引數是不可迭代的,那麼將會丟擲錯誤。

  3. 如果傳的引數陣列是空,那麼返回的 promise 將永遠等待。

  4. 如果迭代包含一個或多個非承諾值和/或已解決/拒絕的承諾,則 Promise.race 將解析為迭代中找到的第一個值。

Promise.race = function (promises) {
    //promises 必須是一個可遍歷的資料結構,否則拋錯
    return new Promise((resolve, reject) => {
        if (typeof promises[Symbol.iterator] !== 'function') {
            //真實不是這個錯誤
            Promise.reject('args is not iteratable!');
        }
        if (promises.length === 0) {
            return;
        } else {
            for (let i = 0; i < promises.length; i++) {
                Promise.resolve(promises[i]).then((data) => {
                    resolve(data);
                    return;
                }, (err) => {
                    reject(err);
                    return;
                });
            }
        }
    });
}
複製程式碼

測試程式碼:

//一直在等待態
Promise.race([]).then((data) => {
    console.log('success ', data);
}, (err) => {
    console.log('err ', err);
});
//拋錯
Promise.race().then((data) => {
    console.log('success ', data);
}, (err) => {
    console.log('err ', err);
});
Promise.race([
    new Promise((resolve, reject) => { setTimeout(() => { resolve(100) }, 1000) }),
    new Promise((resolve, reject) => { setTimeout(() => { resolve(200) }, 200) }),
    new Promise((resolve, reject) => { setTimeout(() => { reject(100) }, 100) })
]).then((data) => {
    console.log(data);
}, (err) => {
    console.log(err);
});
複製程式碼

引申: Promise.all/Promise.reject/Promise.resolve/Promise.prototype.finally/Promise.prototype.catch 的實現原理,如果還不太會,戳:Promise原始碼實現

如果你有更好的答案或想法,歡迎在這題目對應的github下留言:如何實現 Promise.race?


【面試篇】寒冬求職季之你必須要懂的原生JS(中)

5.可遍歷資料結構的有什麼特點?

一個物件如果要具備可被 for...of 迴圈呼叫的 Iterator 介面,就必須在其 Symbol.iterator 的屬性上部署遍歷器生成方法(或者原型鏈上的物件具有該方法)

PS: 遍歷器物件根本特徵就是具有next方法。每次呼叫next方法,都會返回一個代表當前成員的資訊物件,具有value和done兩個屬性。

//如為物件新增Iterator 介面;
let obj = {
    name: "Yvette",
    age: 18,
    job: 'engineer',
    [Symbol.iterator]() {
        const self = this;
        const keys = Object.keys(self);
        let index = 0;
        return {
            next() {
                if (index < keys.length) {
                    return {
                        value: self[keys[index++]],
                        done: false
                    };
                } else {
                    return { value: undefined, done: true };
                }
            }
        };
    }
};

for(let item of obj) {
    console.log(item); //Yvette  18  engineer
}
複製程式碼

使用 Generator 函式(遍歷器物件生成函式)簡寫 Symbol.iterator 方法,可以簡寫如下:

let obj = {
    name: "Yvette",
    age: 18,
    job: 'engineer',
    * [Symbol.iterator] () {
        const self = this;
        const keys = Object.keys(self);
        for (let index = 0;index < keys.length; index++) {
            yield self[keys[index]];//yield表示式僅能使用在 Generator 函式中
        } 
    }
};
複製程式碼

原生具備 Iterator 介面的資料結構如下。

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函式的 arguments 物件
  • NodeList 物件
  • ES6 的陣列、Set、Map 都部署了以下三個方法: entries() / keys() / values(),呼叫後都返回遍歷器物件。

如果你有更好的答案或想法,歡迎在這題目對應的github下留言:可遍歷資料結構的有什麼特點?


6.requestAnimationFrame 和 setTimeout/setInterval 有什麼區別?使用 requestAnimationFrame 有哪些好處?

在 requestAnimationFrame 之前,我們主要使用 setTimeout/setInterval 來編寫JS動畫。

編寫動畫的關鍵是迴圈間隔的設定,一方面,迴圈間隔足夠短,動畫效果才能顯得平滑流暢;另一方面,迴圈間隔還要足夠長,才能確保瀏覽器有能力渲染產生的變化。

大部分的電腦顯示器的重新整理頻率是60HZ,也就是每秒鐘重繪60次。大多數瀏覽器都會對重繪操作加以限制,不超過顯示器的重繪頻率,因為即使超過那個頻率使用者體驗也不會提升。因此,最平滑動畫的最佳迴圈間隔是 1000ms / 60 ,約為16.7ms。

setTimeout/setInterval 有一個顯著的缺陷在於時間是不精確的,setTimeout/setInterval 只能保證延時或間隔不小於設定的時間。因為它們實際上只是把任務新增到了任務佇列中,但是如果前面的任務還沒有執行完成,它們必須要等待。

requestAnimationFrame 才有的是系統時間間隔,保持最佳繪製效率,不會因為間隔時間過短,造成過度繪製,增加開銷;也不會因為間隔時間太長,使用動畫卡頓不流暢,讓各種網頁動畫效果能夠有一個統一的重新整理機制,從而節省系統資源,提高系統效能,改善視覺效果。

綜上所述,requestAnimationFrame 和 setTimeout/setInterval 在編寫動畫時相比,優點如下:

1.requestAnimationFrame 不需要設定時間,採用系統時間間隔,能達到最佳的動畫效果。

2.requestAnimationFrame 會把每一幀中的所有DOM操作集中起來,在一次重繪或迴流中就完成。

3.當 requestAnimationFrame() 執行在後臺標籤頁或者隱藏的 <iframe> 裡時,requestAnimationFrame() 會被暫停呼叫以提升效能和電池壽命(大多數瀏覽器中)。

requestAnimationFrame 使用(試試使用requestAnimationFrame寫一個移動的小球,從A移動到B初):

function step(timestamp) {
    //code...
    window.requestAnimationFrame(step);
}
window.requestAnimationFrame(step);

複製程式碼

如果你有更好的答案或想法,歡迎在這題目對應的github下留言:requestAnimationFrame 和 setTimeout/setInterval 有什麼區別?使用 requestAnimationFrame 有哪些好處?


7.JS 型別轉換的規則是什麼?

型別轉換的規則三言兩語說不清,真想哇得一聲哭出來~

【面試篇】寒冬求職季之你必須要懂的原生JS(中)

JS中型別轉換分為 強制型別轉換 和 隱式型別轉換 。

  • 通過 Number()、parseInt()、parseFloat()、toString()、String()、Boolean(),進行強制型別轉換。

  • 邏輯運算子(&&、 ||、 !)、運算子(+、-、*、/)、關係操作符(>、 <、 <= 、>=)、相等運算子(==)或者 if/while 的條件,可能會進行隱式型別轉換。

強制型別轉換

1.Number() 將任意型別的引數轉換為數值型別

規則如下:

  • 如果是布林值,true和false分別被轉換為1和0
  • 如果是數字,返回自身
  • 如果是 null,返回 0
  • 如果是 undefined,返回 NAN
  • 如果是字串,遵循以下規則:
    1. 如果字串中只包含數字(或者是 0X / 0x 開頭的十六進位制數字字串,允許包含正負號),則將其轉換為十進位制
    2. 如果字串中包含有效的浮點格式,將其轉換為浮點數值
    3. 如果是空字串,將其轉換為0
    4. 如不是以上格式的字串,均返回 NaN
  • 如果是Symbol,丟擲錯誤
  • 如果是物件,則呼叫物件的 valueOf() 方法,然後依據前面的規則轉換返回的值。如果轉換的結果是 NaN ,則呼叫物件的 toString() 方法,再次依照前面的規則轉換返回的字串值。

部分內建物件呼叫預設的 valueOf 的行為:

物件 返回值
Array 陣列本身(物件型別)
Boolean 布林值(原始型別)
Date 從 UTC 1970 年 1 月 1 日午夜開始計算,到所封裝的日期所經過的毫秒數
Function 函式本身(物件型別)
Number 數字值(原始型別)
Object 物件本身(物件型別)
String 字串值(原始型別)
Number('0111'); //111
Number('0X11') //17
Number(null); //0
Number(''); //0
Number('1a'); //NaN
Number(-0X11);//-17
複製程式碼

2.parseInt(param, radix)

如果第一個引數傳入的是字串型別:

  1. 忽略字串前面的空格,直至找到第一個非空字元,如果是空字串,返回NaN
  2. 如果第一個字元不是數字符號或者正負號,返回NaN
  3. 如果第一個字元是數字/正負號,則繼續解析直至字串解析完畢或者遇到一個非數字符號為止

如果第一個引數傳入的Number型別:

  1. 數字如果是0開頭,則將其當作八進位制來解析(如果是一個八進位制數);如果以0x開頭,則將其當作十六進位制來解析

如果第一個引數是 null 或者是 undefined,或者是一個物件型別:

  1. 返回 NaN

如果第一個引數是陣列: 1. 去陣列的第一個元素,按照上面的規則進行解析

如果第一個引數是Symbol型別: 1. 丟擲錯誤

如果指定radix引數,以radix為基數進行解析

parseInt('0111'); //111
parseInt(0111); //八進位制數 73
parseInt('');//NaN
parseInt('0X11'); //17
parseInt('1a') //1
parseInt('a1'); //NaN
parseInt(['10aa','aaa']);//10

parseInt([]);//NaN; parseInt(undefined);
複製程式碼

parseFloat

規則和parseInt基本相同,接受一個Number型別或字串,如果是字串中,那麼只有第一個小數點是有效的。

toString()

規則如下:

  • 如果是Number型別,輸出數字字串
  • 如果是 null 或者是 undefined,拋錯
  • 如果是陣列,那麼將陣列展開輸出。空陣列,返回''
  • 如果是物件,返回 [object Object]
  • 如果是Date, 返回日期的文字表示法
  • 如果是函式,輸出對應的字串(如下demo)
  • 如果是Symbol,輸出Symbol字串
let arry = [];
let obj = {a:1};
let sym = Symbol(100);
let date = new Date();
let fn = function() {console.log('穩住,我們能贏!')}
let str = 'hello world';
console.log([].toString()); // ''
console.log([1, 2, 3, undefined, 5, 6].toString());//1,2,3,,5,6
console.log(arry.toString()); // 1,2,3
console.log(obj.toString()); // [object Object]
console.log(date.toString()); // Sun Apr 21 2019 16:11:39 GMT+0800 (CST)
console.log(fn.toString());// function () {console.log('穩住,我們能贏!')}
console.log(str.toString());// 'hello world'
console.log(sym.toString());// Symbol(100)
console.log(undefined.toString());// 拋錯
console.log(null.toString());// 拋錯
複製程式碼

String()

String() 的轉換規則與 toString() 基本一致,最大的一點不同在於 nullundefined,使用 String 進行轉換,null 和 undefined對應的是字串 'null''undefined'

Boolean

除了 undefined、 null、 false、 ''、 0(包括 +0,-0)、 NaN 轉換出來是false,其它都是true.

隱式型別轉換

&& 、|| 、 ! 、 if/while 的條件判斷

需要將資料轉換成 Boolean 型別,轉換規則同 Boolean 強制型別轉換

運算子: + - * /

+ 號操作符,不僅可以用作數字相加,還可以用作字串拼接。

僅當 + 號兩邊都是數字時,進行的是加法運算。如果兩邊都是字串,直接拼接,無需進行隱式型別轉換。

除了上面的情況外,如果運算元是物件、數值或者布林值,則呼叫toString()方法取得字串值(toString轉換規則)。對於 undefined 和 null,分別呼叫String()顯式轉換為字串,然後再進行拼接。

console.log({}+10); //[object Object]10
console.log([1, 2, 3, undefined, 5, 6] + 10);//1,2,3,,5,610
複製程式碼

-*/ 操作符針對的是運算,如果操作值之一不是數值,則被隱式呼叫Number()函式進行轉換。如果其中有一個轉換除了為NaN,結果為NaN.

關係操作符: ==、>、< 、<=、>=

> , <<=>=

  1. 如果兩個操作值都是數值,則進行數值比較
  2. 如果兩個操作值都是字串,則比較字串對應的字元編碼值
  3. 如果有一方是Symbol型別,丟擲錯誤
  4. 除了上述情況之外,都進行Number()進行型別轉換,然後再進行比較。

注: NaN是非常特殊的值,它不和任何型別的值相等,包括它自己,同時它與任何型別的值比較大小時都返回false。

console.log(10 > {});//返回false.
/**
 *{}.valueOf ---> {}
 *{}.toString() ---> '[object Object]' ---> NaN
 *NaN 和 任何型別比大小,都返回 false
 */
複製程式碼

相等操作符:==

  1. 如果型別相同,無需進行型別轉換。
  2. 如果其中一個操作值是 null 或者是 undefined,那麼另一個操作符必須為 null 或者 undefined 時,才返回 true,否則都返回 false.
  3. 如果其中一個是 Symbol 型別,那麼返回 false.
  4. 兩個操作值是否為 string 和 number,就會將字串轉換為 number
  5. 如果一個操作值是 boolean,那麼轉換成 number
  6. 如果一個操作值為 object 且另一方為 string、number 或者 symbol,是的話就會把 object 轉為原始型別再進行判斷(呼叫object的valueOf/toString方法進行轉換)

物件如何轉換成原始資料型別

如果部署了 [Symbol.toPrimitive] 介面,那麼呼叫此介面,若返回的不是基礎資料型別,跑出錯誤。

如果沒有部署 [Symbol.toPrimitive] 介面,那麼先返回 valueOf() 的值,若返回的不是基礎型別的值,再返回 toString() 的值,若返回的不是基礎型別的值, 則丟擲異常。

//先呼叫 valueOf, 後呼叫 toString
let obj = {
    [Symbol.toPrimitive]() {
        return 200;
    },
    valueOf() {
        return 300;
    },
    toString() {
        return 'Hello';
    }
}
//如果 valueOf 返回的不是基本資料型別,則會呼叫 toString, 
//如果 toString 返回的也不是基本資料型別,會丟擲錯誤
console.log(obj + 200); //400
複製程式碼

【面試篇】寒冬求職季之你必須要懂的原生JS(中)

如果你有更好的答案或想法,歡迎在這題目對應的github下留言:JS 型別轉換的規則是什麼?


8.簡述下對 webWorker 的理解?

HTML5則提出了 Web Worker 標準,表示js允許多執行緒,但是子執行緒完全受主執行緒控制並且不能操作dom,只有主執行緒可以操作dom,所以js本質上依然是單執行緒語言。

web worker就是在js單執行緒執行的基礎上開啟一個子執行緒,進行程式處理,而不影響主執行緒的執行,當子執行緒執行完之後再回到主執行緒上,在這個過程中不影響主執行緒的執行。子執行緒與主執行緒之間提供了資料互動的介面postMessage和onmessage,來進行資料傳送和接收。

var worker = new Worker('./worker.js'); //建立一個子執行緒
worker.postMessage('Hello');
worker.onmessage = function (e) {
    console.log(e.data); //Hi
    worker.terminate(); //結束執行緒
};
複製程式碼
//worker.js
onmessage = function (e) {
    console.log(e.data); //Hello
    postMessage("Hi"); //向主程式傳送訊息
};
複製程式碼

僅是最簡示例程式碼,專案中通常是將一些耗時較長的程式碼,放在子執行緒中執行。

如果你有更好的答案或想法,歡迎在這題目對應的github下留言:簡述下對 webWorker 的理解


9.ES6模組和CommonJS模組的差異?

  1. ES6模組在編譯時,就能確定模組的依賴關係,以及輸入和輸出的變數。

    CommonJS 模組,執行時載入。

  2. ES6 模組自動採用嚴格模式,無論模組頭部是否寫了 "use strict"; (嚴格模式有哪些限制?[//連結])

  3. require 可以做動態載入,import 語句做不到,import 語句必須位於頂層作用域中。

  4. ES6 模組中頂層的 this 指向 undefined,ommonJS 模組的頂層 this 指向當前模組。

  5. CommonJS 模組輸出的是一個值的拷貝,ES6 模組輸出的是值的引用。

CommonJS 模組輸出的是值的拷貝,也就是說,一旦輸出一個值,模組內部的變化就影響不到這個值。如:

//name.js
var name = 'William';
setTimeout(() => name = 'Yvette', 200);
module.exports = {
    name
};
//index.js
const name = require('./name');
console.log(name); //William
setTimeout(() => console.log(name), 300); //William
複製程式碼

對比 ES6 模組看一下:

ES6 模組的執行機制與 CommonJS 不一樣。JS 引擎對指令碼靜態分析的時候,遇到模組載入命令 import ,就會生成一個只讀引用。等到指令碼真正執行時,再根據這個只讀引用,到被載入的那個模組裡面去取值。

//name.js
var name = 'William';
setTimeout(() => name = 'Yvette', 200);
export { name };
//index.js
import { name } from './name';
console.log(name); //William
setTimeout(() => console.log(name), 300); //Yvette
複製程式碼

如果你有更好的答案或想法,歡迎在這題目對應的github下留言:ES6模組和CommonJS模組的差異?


10.瀏覽器事件代理機制的原理是什麼?

在說瀏覽器事件代理機制原理之前,我們首先了解一下事件流的概念,早期瀏覽器,IE採用的是事件捕獲事件流,而Netscape採用的則是事件捕獲。"DOM2級事件"把事件流分為三個階段,捕獲階段、目標階段、冒泡階段。現代瀏覽器也都遵循此規範。

【面試篇】寒冬求職季之你必須要懂的原生JS(中)

那麼事件代理是什麼呢?

事件代理又稱為事件委託,在祖先級DOM元素繫結一個事件,當觸發子孫級DOM元素的事件時,利用事件冒泡的原理來觸發繫結在祖先級DOM的事件。因為事件會從目標元素一層層冒泡至document物件。

為什麼要事件代理?

  1. 新增到頁面上的事件數量會影響頁面的執行效能,如果新增的事件過多,會導致網頁的效能下降。採用事件代理的方式,可以大大減少註冊事件的個數。

  2. 事件代理的當時,某個子孫元素是動態增加的,不需要再次對其進行事件繫結。

  3. 不用擔心某個註冊了事件的DOM元素被移除後,可能無法回收其事件處理程式,我們只要把事件處理程式委託給更高層級的元素,就可以避免此問題。

如將頁面中的所有click事件都代理到document上:

addEventListener 接受3個引數,分別是要處理的事件名、處理事件程式的函式和一個布林值。布林值預設為false。表示冒泡階段呼叫事件處理程式,若設定為true,表示在捕獲階段呼叫事件處理程式。

document.addEventListener('click', function (e) {
    console.log(e.target);
    /**
    * 捕獲階段呼叫呼叫事件處理程式,eventPhase是 1; 
    * 處於目標,eventPhase是2 
    * 冒泡階段呼叫事件處理程式,eventPhase是 1;
    */ 
    console.log(e.eventPhase);
    
});
複製程式碼

如果你有更好的答案或想法,歡迎在這題目對應的github下留言:瀏覽器事件代理機制的原理是什麼?


【面試篇】寒冬求職季之你必須要懂的原生JS(中)

11.js如何自定義事件?

自定義 DOM 事件(不考慮IE9之前版本)

自定義事件有三種方法,一種是使用 new Event(), 另一種是 createEvent('CustomEvent') , 另一種是 new customEvent()

  1. 使用 new Event()

獲取不到 event.detail

let btn = document.querySelector('#btn');
let ev = new Event('alert', {
    bubbles: true,    //事件是否冒泡;預設值false
    cancelable: true, //事件能否被取消;預設值false
    composed: false
});
btn.addEventListener('alert', function (event) {
    console.log(event.bubbles); //true
    console.log(event.cancelable); //true
    console.log(event.detail); //undefined
}, false);
btn.dispatchEvent(ev);
複製程式碼
  1. 使用 createEvent('CustomEvent') (DOM3)

要建立自定義事件,可以呼叫 createEvent('CustomEvent'),返回的物件有 initCustomEvent 方法,接受以下四個引數:

  • type: 字串,表示觸發的事件型別,如此處的'alert'
  • bubbles: 布林值: 表示事件是否冒泡
  • cancelable: 布林值,表示事件是否可以取消
  • detail: 任意值,儲存在 event 物件的 detail 屬性中
let btn = document.querySelector('#btn');
let ev = btn.createEvent('CustomEvent');
ev.initCustomEvent('alert', true, true, 'button');
btn.addEventListener('alert', function (event) {
    console.log(event.bubbles); //true
    console.log(event.cancelable);//true
    console.log(event.detail); //button
}, false);
btn.dispatchEvent(ev);
複製程式碼
  1. 使用 new customEvent() (DOM4)

使用起來比 createEvent('CustomEvent') 更加方便

var btn = document.querySelector('#btn');
/*
 * 第一個引數是事件型別
 * 第二個引數是一個物件
 */
var ev = new CustomEvent('alert', {
    bubbles: 'true',
    cancelable: 'true',
    detail: 'button'
});
btn.addEventListener('alert', function (event) {
    console.log(event.bubbles); //true
    console.log(event.cancelable);//true
    console.log(event.detail); //button
}, false);
btn.dispatchEvent(ev);
複製程式碼

自定義非 DOM 事件(觀察者模式)

EventTarget型別有一個單獨的屬性handlers,用於儲存事件處理程式(觀察者)。

addHandler() 用於註冊給定型別事件的事件處理程式;

fire() 用於觸發一個事件;

removeHandler() 用於登出某個事件型別的事件處理程式。

function EventTarget(){
    this.handlers = {};
}

EventTarget.prototype = {
    constructor:EventTarget,
    addHandler:function(type,handler){
        if(typeof this.handlers[type] === "undefined"){
            this.handlers[type] = [];
        }
        this.handlers[type].push(handler);
    },
    fire:function(event){
        if(!event.target){
            event.target = this;
        }
        if(this.handlers[event.type] instanceof Array){
            const handlers = this.handlers[event.type];
            handlers.forEach((handler)=>{
                handler(event);
            });
        }
    },
    removeHandler:function(type,handler){
        if(this.handlers[type] instanceof Array){
            const handlers = this.handlers[type];
            for(var i = 0,len = handlers.length; i < len; i++){
                if(handlers[i] === handler){
                    break;
                }
            }
            handlers.splice(i,1);
        }
    }
}
//使用
function handleMessage(event){
    console.log(event.message);
}
//建立一個新物件
var target = new EventTarget();
//新增一個事件處理程式
target.addHandler("message", handleMessage);
//觸發事件
target.fire({type:"message", message:"Hi"}); //Hi
//刪除事件處理程式
target.removeHandler("message",handleMessage);
//再次觸發事件,沒有事件處理程式
target.fire({type:"message",message: "Hi"});
複製程式碼

如果你有更好的答案或想法,歡迎在這題目對應的github下留言:js如何自定義事件?


12.跨域的方法有哪些?原理是什麼?

知其然知其所以然,在說跨域方法之前,我們先了解下什麼叫跨域,瀏覽器有同源策略,只有當“協議”、“域名”、“埠號”都相同時,才能稱之為是同源,其中有一個不同,即是跨域。

那麼同源策略的作用是什麼呢?同源策略限制了從同一個源載入的文件或指令碼如何與來自另一個源的資源進行互動。這是一個用於隔離潛在惡意檔案的重要安全機制。

那麼我們又為什麼需要跨域呢?一是前端和伺服器分開部署,介面請求需要跨域,二是我們可能會載入其它網站的頁面作為iframe內嵌。

跨域的方法有哪些?

常用的跨域方法

  1. jsonp

儘管瀏覽器有同源策略,但是 <script> 標籤的 src 屬性不會被同源策略所約束,可以獲取任意伺服器上的指令碼並執行。jsonp 通過插入script標籤的方式來實現跨域,引數只能通過url傳入,僅能支援get請求。

實現原理:

Step1: 建立 callback 方法

Step2: 插入 script 標籤

Step3: 後臺接受到請求,解析前端傳過去的 callback 方法,返回該方法的呼叫,並且資料作為引數傳入該方法

Step4: 前端執行服務端返回的方法呼叫

下面程式碼僅為說明 jsonp 原理,專案中請使用成熟的庫。分別看一下前端和服務端的簡單實現:

//前端程式碼
function jsonp({url, params, cb}) {
    return new Promise((resolve, reject) => {
        //建立script標籤
        let script = document.createElement('script');
        //將回撥函式掛在 window 上
        window[cb] = function(data) {
            resolve(data);
            //程式碼執行後,刪除插入的script標籤
            document.body.removeChild(script);
        }
        //回撥函式加在請求地址上
        params = {...params, cb} //wb=b&cb=show
        let arrs = [];
        for(let key in params) {
            arrs.push(`${key}=${params[key]}`);
        }
        script.src = `${url}?${arrs.join('&')}`;
        document.body.appendChild(script);
    });
}
//使用
function sayHi(data) {
    console.log(data);
}
jsonp({
    url: 'http://localhost:3000/say',
    params: {
        //code
    },
    cb: 'sayHi'
}).then(data => {
    console.log(data);
});
複製程式碼
//express啟動一個後臺服務
let express = require('express');
let app = express();

app.get('/say', (req, res) => {
    let {cb} = req.query; //獲取傳來的callback函式名,cb是key
    res.send(`${cb}('Hello!')`);
});
app.listen(3000);
複製程式碼

從今天起,jsonp的原理就要了然於心啦~

【面試篇】寒冬求職季之你必須要懂的原生JS(中)

  1. cors

jsonp 只能支援 get 請求,cors 可以支援多種請求。cors 並不需要前端做什麼工作。

簡單跨域請求:

只要伺服器設定的Access-Control-Allow-Origin Header和請求來源匹配,瀏覽器就允許跨域

  1. 請求的方法是get,head或者post。
  2. Content-Type是application/x-www-form-urlencoded, multipart/form-data 或 text/plain中的一個值,或者不設定也可以,一般預設就是application/x-www-form-urlencoded。
  3. 請求中沒有自定義的HTTP頭部,如x-token。(應該是這幾種頭部 Accept,Accept-Language,Content-Language,Last-Event-ID,Content-Type)
//簡單跨域請求
app.use((req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', 'XXXX');
});
複製程式碼

帶預檢(Preflighted)的跨域請求

不滿於簡單跨域請求的,即是帶預檢的跨域請求。服務端需要設定 Access-Control-Allow-Origin (允許跨域資源請求的域) 、 Access-Control-Allow-Methods (允許的請求方法) 和 Access-Control-Allow-Headers (允許的請求頭)

app.use((req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', 'XXX');
    res.setHeader('Access-Control-Allow-Headers', 'XXX'); //允許返回的頭
    res.setHeader('Access-Control-Allow-Methods', 'XXX');//允許使用put方法請求介面
    res.setHeader('Access-Control-Max-Age', 6); //預檢的存活時間
    if(req.method === "OPTIONS") {
        res.end(); //如果method是OPTIONS,不做處理
    }
});
複製程式碼

更多CORS的知識可以訪問: HTTP訪問控制(CORS)

  1. nginx 反向代理

使用nginx反向代理實現跨域,只需要修改nginx的配置即可解決跨域問題。

A網站向B網站請求某個介面時,向B網站傳送一個請求,nginx根據配置檔案接收這個請求,代替A網站向B網站來請求。 nginx拿到這個資源後再返回給A網站,以此來解決了跨域問題。

例如nginx的埠號為 8090,需要請求的伺服器埠號為 3000。(localhost:8090 請求 localhost:3000/say)

nginx配置如下:

server {
    listen       8090;

    server_name  localhost;

    location / {
        root   /Users/liuyan35/Test/Study/CORS/1-jsonp;
        index  index.html index.htm;
    }
    location /say {
        rewrite  ^/say/(.*)$ /$1 break;
        proxy_pass   http://localhost:3000;
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Credentials' 'true';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
    }
    # others
}
複製程式碼
  1. websocket

Websocket 是 HTML5 的一個持久化的協議,它實現了瀏覽器與伺服器的全雙工通訊,同時也是跨域的一種解決方案。

Websocket 不受同源策略影響,只要伺服器端支援,無需任何配置就支援跨域。

前端頁面在 8080 的埠。

let socket = new WebSocket('ws://localhost:3000'); //協議是ws
socket.onopen = function() {
    socket.send('Hi,你好');
}
socket.onmessage = function(e) {
    console.log(e.data)
}
複製程式碼

服務端 3000埠。可以看出websocket無需做跨域配置。

let WebSocket = require('ws');
let wss = new WebSocket.Server({port: 3000});
wss.on('connection', function(ws) {
    ws.on('message', function(data) {
        console.log(data); //接受到頁面發來的訊息'Hi,你好'
        ws.send('Hi'); //向頁面傳送訊息
    });
});
複製程式碼
  1. postMessage

postMessage 通過用作前端頁面之前的跨域,如父頁面與iframe頁面的跨域。window.postMessage方法,允許跨視窗通訊,不論這兩個視窗是否同源。

話說工作中兩個頁面之前需要通訊的情況並不多,我本人工作中,僅使用過兩次,一次是H5頁面中傳送postMessage資訊,ReactNative的webview中接收此此訊息,並作出相應處理。另一次是可輪播的頁面,某個輪播頁使用的是iframe頁面,為了解決滑動的事件衝突,iframe頁面中去監聽手勢,傳送訊息告訴父頁面是否左滑和右滑。

子頁面向父頁面發訊息

父頁面

window.addEventListener('message', (e) => {
    this.props.movePage(e.data);
}, false);
複製程式碼

子頁面(iframe):

if(/*左滑*/) {
    window.parent && window.parent.postMessage(-1, '*')
}else if(/*右滑*/){
    window.parent && window.parent.postMessage(1, '*')
}
複製程式碼

父頁面向子頁面發訊息

父頁面:

let iframe = document.querySelector('#iframe');
iframe.onload = function() {
    iframe.contentWindow.postMessage('hello', 'http://localhost:3002');
}
複製程式碼

子頁面:

window.addEventListener('message', function(e) {
    console.log(e.data);
    e.source.postMessage('Hi', e.origin); //回訊息
});
複製程式碼
  1. node 中介軟體

node 中介軟體的跨域原理和nginx代理跨域,同源策略是瀏覽器的限制,服務端沒有同源策略。

node中介軟體實現跨域的原理如下:

1.接受客戶端請求

2.將請求 轉發給伺服器。

3.拿到伺服器 響應 資料。

4.將 響應 轉發給客戶端。

不常用跨域方法

以下三種跨域方式很少用,如有興趣,可自行查閱相關資料。

  1. window.name + iframe

  2. location.hash + iframe

  3. document.domain (主域需相同)

如果你有更好的答案或想法,歡迎在這題目對應的github下留言:跨域的方法有哪些?原理是什麼?


13.js非同步載入的方式有哪些?

  1. <script> 的 defer 屬性,HTML4 中新增

  2. <script> 的 async 屬性,HTML5 中新增

<script>標籤開啟defer屬性,指令碼就會非同步載入。渲染引擎遇到這一行命令,就會開始下載外部指令碼,但不會等它下載和執行,而是直接執行後面的命令。

defer 和 async 的區別在於: defer要等到整個頁面在記憶體中正常渲染結束,才會執行;

async一旦下載完,渲染引擎就會中斷渲染,執行這個指令碼以後,再繼續渲染。defer是“渲染完再執行”,async是“下載完就執行”。

如果有多個 defer 指令碼,會按照它們在頁面出現的順序載入。

多個async指令碼是不能保證載入順序的。

  1. 動態插入 script 指令碼
function downloadJS() { 
    varelement = document.createElement("script"); 
    element.src = "XXX.js"; 
    document.body.appendChild(element); 
}
//何時的時候,呼叫上述方法 
複製程式碼
  1. 有條件的動態建立指令碼

如頁面 onload 之後,

如果你有更好的答案或想法,歡迎在這題目對應的github下留言:js非同步載入的方式有哪些?


14.下面程式碼a在什麼情況中列印出1?

//?
if(a == 1 && a == 2 && a == 3) {
    console.log(1);
}
複製程式碼

1.在型別轉換的時候,我們知道了物件如何轉換成原始資料型別。如果部署了 [Symbol.toPrimitive],那麼返回的就是Symbol.toPrimitive的返回值。當然,我們也可以把此函式部署在valueOf或者是toString介面上,效果相同。

//利用閉包延長作用域的特性
let a = {
    [Symbol.toPrimitive]: (function() {
            let i = 1;
            return function() {
                return i++;
            }
    })()
}
複製程式碼

(1). 比較 a == 1 時,會呼叫 [Symbol.toPrimitive],此時 i 是 1,相等。 (2). 繼續比較 a == 2,呼叫 [Symbol.toPrimitive],此時 i 是 2,相等。 (3). 繼續比較 a == 3,呼叫 [Symbol.toPrimitive],此時 i 是 3,相等。

2.利用Object.definePropert在window/global上定義a屬性,獲取a屬性時,會呼叫get.

let val = 1;
Object.defineProperty(window, 'a', {
  get: function() {
    return val++;
  }
});
複製程式碼

3.利用陣列的特性。

var a = [1,2,3];
a.join = a.shift;
複製程式碼

陣列的 toString 方法返回一個字串,該字串由陣列中的每個元素的 toString() 返回值經呼叫 join() 方法連線(由逗號隔開)組成。

因此,我們可以重新 join 方法。返回第一個元素,並將其刪除。

如果你有更好的答案或想法,歡迎在這題目對應的github下留言:下面程式碼a在什麼情況中列印出1?


【面試篇】寒冬求職季之你必須要懂的原生JS(中)
【面試篇】寒冬求職季之你必須要懂的原生JS(中)

15.下面這段程式碼的輸出是什麼?

function Foo() {
    getName = function() {console.log(1)};
    return this;
}
Foo.getName = function() {console.log(2)};
Foo.prototype.getName = function() {console.log(3)};
var getName = function() {console.log(4)};
function getName() {console.log(5)};

Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
複製程式碼

**說明:**一道經典的面試題,僅是為了幫助大家回顧一下知識點,加深理解,真實工作中,是不可能這樣寫程式碼的,否則,肯定會被打死的。

1.首先預編譯階段,變數宣告與函式宣告提升至其對應作用域的最頂端。

因此上面的程式碼編譯後如下(函式宣告的優先順序先於變數宣告):

function Foo() {
    getName = function() {console.log(1)};
    return this;
}
var getName;
function getName() {console.log(5)};
Foo.getName = function() {console.log(2)};
Foo.prototype.getName = function() {console.log(3)};
getName = function() {console.log(4)};
複製程式碼

2.Foo.getName();直接呼叫Foo上getName方法,輸出2

3.getName();輸出4,getName被重新賦值了

4.Foo().getName();執行Foo(),window的getName被重新賦值,返回this;瀏覽器環境中,非嚴格模式,this 指向 window,this.getName();輸出為1.

如果是嚴格模式,this 指向 undefined,此處會丟擲錯誤。

如果是node環境中,this 指向 global,node的全域性變數並不掛在global上,因為global.getName對應的是undefined,不是一個function,會丟擲錯誤。

5.getName();已經拋錯的自然走不動這一步了;繼續瀏覽器非嚴格模式;window.getName被重新賦過值,此時再呼叫,輸出的是1

6.new Foo.getName();考察運算子優先順序的知識,new 無引數列表,對應的優先順序是18;成員訪問操作符 . , 對應的優先順序是 19。因此相當於是 new (Foo.getName)();new操作符會執行建構函式中的方法,因此此處輸出為 2.

7.new Foo().getName();new 帶引數列表,對應的優先順序是19,和成員訪問操作符.優先順序相同。同級運算子,按照從左到右的順序依次計算。new Foo()先初始化 Foo 的例項化物件,例項上沒有getName方法,因此需要原型上去找,即找到了 Foo.prototype.getName,輸出3

8.new new Foo().getName(); new 帶引數列表,優先順序19,因此相當於是 new (new Foo()).getName();先初始化 Foo 的例項化物件,然後將其原型上的 getName 函式作為建構函式再次 new ,輸出3

因此最終結果如下:

Foo.getName(); //2
getName();//4
Foo().getName();//1
getName();//1
new Foo.getName();//2
new Foo().getName();//3
new new Foo().getName();//3
複製程式碼

如果你有更好的答案或想法,歡迎在這題目對應的github下留言:下面這段程式碼的輸出是什麼?


16.實現雙向繫結 Proxy 與 Object.defineProperty 相比優劣如何?

  1. Object.definedProperty 的作用是劫持一個物件的屬性,劫持屬性的getter和setter方法,在物件的屬性發生變化時進行特定的操作。而 Proxy 劫持的是整個物件。

  2. Proxy 會返回一個代理物件,我們只需要操作新物件即可,而 Object.defineProperty 只能遍歷物件屬性直接修改。

  3. Object.definedProperty 不支援陣列,更準確的說是不支援陣列的各種API,因為如果僅僅考慮arry[i] = value 這種情況,是可以劫持的,但是這種劫持意義不大。而 Proxy 可以支援陣列的各種API。

  4. 儘管 Object.defineProperty 有諸多缺陷,但是其相容性要好於 Proxy.

PS: Vue2.x 使用 Object.defineProperty 實現資料雙向繫結,V3.0 則使用了 Proxy.

//攔截器
let obj = {};
let temp = 'Yvette';
Object.defineProperty(obj, 'name', {
    get() {
        console.log("讀取成功");
        return temp
    },
    set(value) {
        console.log("設定成功");
        temp = value;
    }
});

obj.name = 'Chris';
console.log(obj.name);
複製程式碼

PS: Object.defineProperty 定義出來的屬性,預設是不可列舉,不可更改,不可配置【無法delete】

我們可以看到 Proxy 會劫持整個物件,讀取物件中的屬性或者是修改屬性值,那麼就會被劫持。但是有點需要注意,複雜資料型別,監控的是引用地址,而不是值,如果引用地址沒有改變,那麼不會觸發set。

let obj = {name: 'Yvette', hobbits: ['travel', 'reading'], info: {
    age: 20,
    job: 'engineer'
}};
let p = new Proxy(obj, {
    get(target, key) { //第三個引數是 proxy, 一般不使用
        console.log('讀取成功');
        return Reflect.get(target, key);
    },
    set(target, key, value) {
        if(key === 'length') return true; //如果是陣列長度的變化,返回。
        console.log('設定成功');
        return Reflect.set([target, key, value]);
    }
});
p.name = 20; //設定成功
p.age = 20; //設定成功; 不需要事先定義此屬性
p.hobbits.push('photography'); //讀取成功;注意不會觸發設定成功
p.info.age = 18; //讀取成功;不會觸發設定成功
複製程式碼

最後,我們再看下對於陣列的劫持,Object.definedProperty 和 Proxy 的差別

Object.definedProperty 可以將陣列的索引作為屬性進行劫持,但是僅支援直接對 arry[i] 進行操作,不支援陣列的API,非常雞肋。

let arry = []
Object.defineProperty(arry, '0', {
    get() {
        console.log("讀取成功");
        return temp
    },
    set(value) {
        console.log("設定成功");
        temp = value;
    }
});

arry[0] = 10; //觸發設定成功
arry.push(10); //不能被劫持
複製程式碼

Proxy 可以監聽到陣列的變化,支援各種API。注意陣列的變化觸發get和set可能不止一次,如有需要,自行根據key值決定是否要進行處理。

let hobbits = ['travel', 'reading'];
let p = new Proxy(hobbits, {
    get(target, key) {
        // if(key === 'length') return true; //如果是陣列長度的變化,返回。
        console.log('讀取成功');
        return Reflect.get(target, key);
    },
    set(target, key, value) {
        // if(key === 'length') return true; //如果是陣列長度的變化,返回。
        console.log('設定成功');
        return Reflect.set([target, key, value]);
    }
});
p.splice(0,1) //觸發get和set,可以被劫持
p.push('photography');//觸發get和set
p.slice(1); //觸發get;因為 slice 是不會修改原陣列的
複製程式碼

如果你有更好的答案或想法,歡迎在這題目對應的github下留言:實現雙向繫結 Proxy 與 Object.defineProperty 相比優劣如何?


17.Object.is() 與比較操作符 ===== 有什麼區別?

以下情況,Object.is認為是相等

兩個值都是 undefined
兩個值都是 null
兩個值都是 true 或者都是 false
兩個值是由相同個數的字元按照相同的順序組成的字串
兩個值指向同一個物件
兩個值都是數字並且
都是正零 +0
都是負零 -0
都是 NaN
都是除零和 NaN 外的其它同一個數字
複製程式碼

Object.is() 類似於 ===,但是有一些細微差別,如下:

  1. NaN 和 NaN 相等
  2. -0 和 +0 不相等
console.log(Object.is(NaN, NaN));//true
console.log(NaN === NaN);//false
console.log(Object.is(-0, +0)); //false
console.log(-0 === +0); //true
複製程式碼

Object.is 和 ==差得遠了, == 在型別不同時,需要進行型別轉換,前文已經詳細說明。

如果你有更好的答案或想法,歡迎在這題目對應的github下留言:Object.is() 與比較操作符 ===== 有什麼區別?


18.什麼是事件迴圈?Node事件迴圈和JS事件迴圈的差異是什麼?

最後一道題留給大家回答,再寫下去,篇幅實在太長。

針對這道題,後面會專門寫一篇文章~

留下你的答案: 什麼是事件迴圈?Node事件迴圈和JS事件迴圈的差異是什麼?

關於瀏覽器的event-loop可以看我之前的文章:搞懂瀏覽器的EventLoop

【面試篇】寒冬求職季之你必須要懂的原生JS(中)


參考文章:

  1. www.imooc.com/article/386…
  2. es6.ruanyifeng.com/
  3. www.imooc.com/article/725…
  4. www.cnblogs.com/LuckyWinty/…
  5. www.jianshu.com/p/a76dc7e0c…
  6. www.v2ex.com/t/351261

關注小姐姐的公眾號,和小姐姐一起學前端。

【面試篇】寒冬求職季之你必須要懂的原生JS(中)

後續寫作計劃(寫作順序不定)

1.《寒冬求職季之你必須要懂的原生JS》(下)

2.《寒冬求職季之你必須要知道的CSS》

3.《寒冬求職季之你必須要懂的前端安全》

4.《寒冬求職季之你必須要懂的一些瀏覽器知識》

5.《寒冬求職季之你必須要知道的效能優化》

6.《寒冬求職季之你必須要懂的webpack原理》

針對React技術棧:

1.《寒冬求職季之你必須要懂的React》系列

2.《寒冬求職季之你必須要懂的ReactNative》系列

【面試篇】寒冬求職季之你必須要懂的原生JS(中)

本文的寫成耗費了非常多的時間,在這個過程中,我也學習到了很多知識,謝謝各位小夥伴願意花費寶貴的時間閱讀本文,如果本文給了您一點幫助或者是啟發,請不要吝嗇你的贊和Star,您的肯定是我前進的最大動力。github.com/YvetteLau/B…

相關文章