【Step-By-Step】高頻面試題深入解析 / 週刊02

劉小夕發表於2019-06-03

本週面試題一覽:

  • 節流(throttle)函式的作用是什麼?有哪些應用場景,請實現一個節流函式
  • 說一說你對JS執行上下文棧和作用域鏈的理解?
  • 什麼是BFC?BFC的佈局規則是什麼?如何建立BFC?
  • let、const、var 的區別有哪些?
  • 深拷貝和淺拷貝的區別是什麼?如何實現一個深拷貝?

更多優質文章可戳: github.com/YvetteLau/B…


6. 節流(throttle)函式的作用是什麼?有哪些應用場景,請實現一個節流函式。(2019-05-27)

節流函式的作用

節流函式的作用是規定一個單位時間,在這個單位時間內最多隻能觸發一次函式執行,如果這個單位時間內多次觸發函式,只能有一次生效。

舉例說明:小明的媽媽和小明約定好,如果小明在週考中取得滿分,那麼當月可以帶他去遊樂場玩,但是一個月最多隻能去一次。

這其實就是一個節流的例子,在一個月的時間內,去遊樂場最多隻能觸發一次。即使這個時間週期內,小明取得多次滿分。

節流應用場景

1.按鈕點選事件

2.拖拽事件

3.onScoll

4.計算滑鼠移動的距離(mousemove)

節流函式實現

利用時間戳實現

function throttle (func, delay) {
    var lastTime = 0;
    function throttled() {
        var context = this;
        var args = arguments;
        var nowTime = Date.now();
        if(nowTime > lastTime + delay) {
            func.apply(context, args);
            lastTime = nowTime;
        }
    }
    //節流函式最終返回的是一個函式
    return throttled; 
}
複製程式碼

利用定時器實現

function throttle(func, delay) {
    var timeout = null;
    function throttled() {
        var context = this;
        var args = arguments;
        if(!timeout) {
            timeout = setTimeout(()=>{
                func.apply(context, args);
                clearTimeout(timeout);
                timeout=null
            }, delay);
        }
    }
    return throttled;
}
複製程式碼

時間戳和定時器的方式都沒有考慮最後一次執行的問題,比如有個按鈕點選事件,設定的間隔時間是1S,在第0.5S,1.8S,2.2S點選,那麼只有0.5S和1.8S的兩次點選能夠觸發函式執行,而最後一次的2.2S會被忽略。

組合實現,允許設定第一次或者最後一次是否觸發函式執行

function throttle (func, wait, options) {
    var timeout, context, args, result;
    var previous = 0;
    if (!options) options = {};

    var later = function () {
        previous = options.leading === false ? 0 : Date.now() || new Date().getTime();
        timeout = null;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
    };

    var throttled = function () {
        var now = Date.now() || new Date().getTime();
        if (!previous && options.leading === false) previous = now;
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            result = func.apply(context, args);
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
            // 判斷是否設定了定時器和 trailing
            timeout = setTimeout(later, remaining);
        }
        return result;
    };

    throttled.cancel = function () {
        clearTimeout(timeout);
        previous = 0;
        timeout = context = args = null;
    };

    return throttled;
}
複製程式碼

使用很簡單:

btn.onclick = throttle(handle, 1000, {leading:true, trailing: true});
複製程式碼

點選檢視更多

7. 說一說你對JS執行上下文棧和作用域鏈的理解?(2019-05-28)

在開始說明JS上下文棧和作用域之前,我們先說明下JS上下文以及作用域的概念。

JS執行上下文

執行上下文就是當前 JavaScript 程式碼被解析和執行時所在環境的抽象概念, JavaScript 中執行任何的程式碼都是在執行上下文中執行。

執行上下文型別分為:

  • 全域性執行上下文
  • 函式執行上下文
  • eval函式執行上下文(不被推薦)

執行上下文建立過程中,需要做以下幾件事:

  1. 建立變數物件:首先初始化函式的引數arguments,提升函式宣告和變數宣告。
  2. 建立作用域鏈(Scope Chain):在執行期上下文的建立階段,作用域鏈是在變數物件之後建立的。
  3. 確定this的值,即 ResolveThisBinding

作用域

作用域負責收集和維護由所有宣告的識別符號(變數)組成的一系列查詢,並實施一套非常嚴格的規則,確定當前執行的程式碼對這些識別符號的訪問許可權。—— 摘錄自《你不知道的JavaScript》(上卷)

作用域有兩種工作模型:詞法作用域和動態作用域,JS採用的是詞法作用域工作模型,詞法作用域意味著作用域是由書寫程式碼時變數和函式宣告的位置決定的。(witheval 能夠修改詞法作用域,但是不推薦使用,對此不做特別說明)

作用域分為:

  • 全域性作用域
  • 函式作用域
  • 塊級作用域

JS執行上下文棧(後面簡稱執行棧)

執行棧,也叫做呼叫棧,具有 LIFO (後進先出) 結構,用於儲存在程式碼執行期間建立的所有執行上下文。

規則如下:

  • 首次執行JavaScript程式碼的時候,會建立一個全域性執行的上下文並Push到當前的執行棧中,每當發生函式呼叫,引擎都會為該函式建立一個新的函式執行上下文並Push當前執行棧的棧頂。
  • 當棧頂的函式執行完成後,其對應的函式執行上下文將會從執行棧中Pop出,上下文的控制權將移動到當前執行棧的下一個執行上下文。

以一段程式碼具體說明:

function fun3() {
    console.log('fun3')
}

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();
複製程式碼

Global Execution Context (即全域性執行上下文)首先入棧,過程如下:

【Step-By-Step】高頻面試題深入解析 / 週刊02

虛擬碼:

//全域性執行上下文首先入棧
ECStack.push(globalContext);

//執行fun1();
ECStack.push(<fun1> functionContext);

//fun1中又呼叫了fun2;
ECStack.push(<fun2> functionContext);

//fun2中又呼叫了fun3;
ECStack.push(<fun3> functionContext);

//fun3執行完畢
ECStack.pop();

//fun2執行完畢
ECStack.pop();

//fun1執行完畢
ECStack.pop();

//javascript繼續順序執行下面的程式碼,但ECStack底部始終有一個 全域性上下文(globalContext);
複製程式碼

作用域鏈

作用域鏈就是從當前作用域開始一層一層向上尋找某個變數,直到找到全域性作用域還是沒找到,就宣佈放棄。這種一層一層的關係,就是作用域鏈。

如:

var a = 10;
function fn1() {
    var b = 20;
    console.log(fn2)
    function fn2() {
        a = 20
    }
    return fn2;
}
fn1()();
複製程式碼

fn2作用域鏈 = [fn2作用域, fn1作用域,全域性作用域]

【Step-By-Step】高頻面試題深入解析 / 週刊02

點選檢視更多

8. 什麼是BFC?BFC的佈局規則是什麼?如何建立BFC?(2019-05-29)

什麼是BFC

BFC 是 Block Formatting Context 的縮寫,即塊格式化上下文。我們來看一下CSS2.1規範中對 BFC 的說明。

Floats, absolutely positioned elements, block containers (such as inline-blocks, table-cells, and table-captions) that are not block boxes, and block boxes with 'overflow' other than 'visible' (except when that value has been propagated to the viewport) establish new block formatting contexts for their contents.

浮動、絕對定位的元素、非塊級盒子的塊容器(如inline-blocks、table-cells 和 table-captions),以及overflow的值不為visible(該值已傳播到視區時除外)為其內容建立新的塊格式上下文。

因此,如果想要深入的理解BFC,我們需要了解以下兩個概念:

1.Box

2.Formatting Context

Box

Box 是 CSS 佈局的物件和基本單位,頁面是由若干個Box組成的。

元素的型別 和 display 屬性,決定了這個 Box 的型別。不同型別的 Box 會參與不同的 Formatting Context。

Formatting Context

Formatting Context 是頁面的一塊渲染區域,並且有一套渲染規則,決定了其子元素將如何定位,以及和其它元素的關係和相互作用。

Formatting Context 有 BFC (Block formatting context),IFC (Inline formatting context),FFC (Flex formatting context) 和 GFC (Grid formatting context)。FFC 和 GFC 為 CC3 中新增。

BFC佈局規則

  • BFC內,盒子依次垂直排列。
  • BFC內,兩個盒子的垂直距離由 margin 屬性決定。屬於同一個BFC的兩個相鄰Box的margin會發生重疊【符合合併原則的margin合併後是使用大的margin】
  • BFC內,每個盒子的左外邊緣接觸內部盒子的左邊緣(對於從右到左的格式,右邊緣接觸)。即使在存在浮動的情況下也是如此。除非建立新的BFC。
  • BFC的區域不會與float box重疊。
  • BFC就是頁面上的一個隔離的獨立容器,容器裡面的子元素不會影響到外面的元素。反之也如此。
  • 計算BFC的高度時,浮動元素也參與計算。

如何建立BFC

  • 根元素
  • 浮動元素(float 屬性不為 none)
  • position 為 absolute 或 fixed
  • overflow 不為 visible 的塊元素
  • display 為 inline-block, table-cell, table-caption

BFC的應用

1.防止 margin 重疊

<style>
    .a{
        height: 100px;
        width: 100px;
        margin: 50px;
        background: pink;
    }
</style>
<body>
    <div class="a"></div>
    <div class="a"></div>
</body>
複製程式碼

兩個div直接的 margin 是50px,發生了 margin 的重疊。

【Step-By-Step】高頻面試題深入解析 / 週刊02

根據BFC規則,同一個BFC內的兩個兩個相鄰Box的 margin 會發生重疊,因此我們可以在div外面再巢狀一層容器,並且觸發該容器生成一個 BFC,這樣 <div class="a"></div> 就會屬於兩個 BFC,自然也就不會再發生 margin 重疊

<style>
    .a{
        height: 100px;
        width: 100px;
        margin: 50px;
        background: pink;
    }
    .container{
        overflow: auto; /*觸發生成BFC*/
    }
</style>
<body>
    <div class="container">
        <div class="a"></div>
    </div>    
    <div class="a"></div>
</body>
複製程式碼

【Step-By-Step】高頻面試題深入解析 / 週刊02

2.清除內部浮動

<style>
    .a{
        height: 100px;
        width: 100px;
        margin: 10px;
        background: pink;
        float: left;
    }
    .container{
        width: 120px;
        border: 2px solid black;
    }
</style>
<body>
    <div class="container">
        <div class="a"></div>
    </div>
</body>
複製程式碼

【Step-By-Step】高頻面試題深入解析 / 週刊02

container 的高度沒有被撐開,如果我們希望 container 的高度能夠包含浮動元素,那麼可以建立一個新的 BFC,因為根據 BFC 的規則,計算 BFC 的高度時,浮動元素也參與計算。

<style>
    .a{
        height: 100px;
        width: 100px;
        margin: 10px;
        background: pink;
        float: left;
    }
    .container{
        width: 120px;
        display: inline-block;/*觸發生成BFC*/
        border: 2px solid black; 
    }
</style>
複製程式碼

【Step-By-Step】高頻面試題深入解析 / 週刊02

3.自適應多欄佈局

<style>
    body{
        width: 500px;
    }
    .a{
        height: 150px;
        width: 100px;
        background: pink;
        float: left;
    }
    .b{
        height: 200px;
        background: blue;
    }
</style>
<body>
    <div class="a"></div>
    <div class="b"></div>
</body>   
複製程式碼

【Step-By-Step】高頻面試題深入解析 / 週刊02

根據規則,BFC的區域不會與float box重疊。因此,可以觸發生成一個新的BFC,如下:

<style>
.b{
    height: 200px;
    overflow: hidden; /*觸發生成BFC*/
    background: blue;
}
</style>
複製程式碼

【Step-By-Step】高頻面試題深入解析 / 週刊02

點選檢視更多

9. let、const、var 的區別有哪些?(2019-05-30)

宣告方式 變數提升 暫時性死區 重複宣告 塊作用域有效 初始值 重新賦值
var 不存在 允許 不是 非必須 允許
let 不會 存在 不允許 非必須 允許
const 不會 存在 不允許 必須 不允許

1.let/const 定義的變數不會出現變數提升,而 var 定義的變數會提升。

a = 10;
var a; //正常
複製程式碼
a = 10;
let a; //ReferenceError
複製程式碼

2.相同作用域中,let 和 const 不允許重複宣告,var 允許重複宣告。

let a = 10;
var a = 20;
//丟擲異常:SyntaxError: Identifier 'a' has already been declared
複製程式碼

3.cosnt 宣告變數時必須設定初始值

const a;//SyntaxError: Missing initializer in const declaration
複製程式碼

4.const 宣告一個只讀的常量,這個常量不可改變。

這裡有一個非常重要的點即是:複雜資料型別,儲存在棧中的是堆記憶體的地址,存在棧中的這個地址是不變的,但是存在堆中的值是可以變得。有沒有相當常量指標/指標常量~

const a = 20;
const b = {
    age: 18,
    star: 500
}
複製程式碼

一圖勝萬言,如下圖所示,不變的是棧記憶體中 a 儲存的 20,和 b 中儲存的 0x0012ff21(瞎編的一個數字)。而 {age: 18, star: 200} 是可變的。思考下如果想希望一個物件是不可變的,應該用什麼方法?

【Step-By-Step】高頻面試題深入解析 / 週刊02

5.let/const 宣告的變數僅在塊級作用域中有效。而 var 宣告的變數在塊級作用域外仍能訪問到。

{
    let a = 10;
    const b = 20;
    var c = 30;
}
console.log(a); //ReferenceError
console.log(b); //ReferenceError
console.log(c); //30
複製程式碼

在 let/const 之前,最早學習JS的時候,也曾被下面這個問題困擾:

期望: a[0]() 輸出 0 , a[1]() 輸出 1 , a[2]() 輸出 2 , ...

var a = [];
for (var i = 0; i < 10; i++) {
    a[i] = function () {
        console.log(i);
    };
}
a[6](); // 10
複製程式碼

【Step-By-Step】高頻面試題深入解析 / 週刊02

雖然後來知道了為什麼,但是想要得到自己需要的結果,還得整個閉包,我...我做錯了什麼,要這麼對我...

var a = [];
for (var i = 0; i < 10; i++) {
    a[i] = (function(j){
        return function () {
            console.log(j);
        }
    })(i)
}
a[6](); // 6
複製程式碼

有了 let 之後,終於不要這麼麻煩了。

var a = [];
for (let i = 0; i < 10; i++) {
    a[i] = function () {
        console.log(i);
    };
}
a[6](); // 6
複製程式碼

美滋滋,有沒有~

美是美了,但是總得問自己為什麼吧~

var i 為什麼輸出的是 10,這是因為 i 在全域性範圍內都是有效的,相當於只有一個變數 i,等執行到 a[6]() 的時候,這個 i 的值是什麼?請大聲說出來。

再看 let , 我們說 let 宣告的變數僅在塊級作用域內有效,變數i是let宣告的,當前的 i 只在本輪迴圈有效,所以每一次迴圈的 i 其實都是一個新的變數。有興趣的小夥伴可以檢視 babel 編譯後的程式碼。

6.頂層作用域中 var 宣告的變數掛在window上(瀏覽器環境)

var a = 10;
console.log(window.a);//10
複製程式碼

7.let/const有暫時性死區的問題,即let/const 宣告的變數,在定義之前都是不可用的。如果使用會丟擲錯誤。

只要塊級作用域記憶體在let命令,它所宣告的變數就“繫結”(binding)這個區域,不再受外部的影響。

var a = 10;
if (true) {
  a = 20; // ReferenceError
  let a;
}
複製程式碼

在程式碼塊內,使用 let/const 命令宣告變數之前,該變數都是不可用的,也就意味著 typeof 不再是一個百分百安全的操作。

console.log(typeof b);//undefined

console.log(a); //ReferenceError
let a = 10;
複製程式碼

點選檢視更多

10. 深拷貝和淺拷貝的區別是什麼?如何實現一個深拷貝?(2019-05-31)

深拷貝和淺拷貝是針對複雜資料型別來說的。

深拷貝

深拷貝複製變數值,對於非基本型別的變數,則遞迴至基本型別變數後,再複製。 深拷貝後的物件與原來的物件是完全隔離的,互不影響,對一個物件的修改並不會影響另一個物件。

淺拷貝

淺拷貝是會將物件的每個屬性進行依次複製,但是當物件的屬性值是引用型別時,實質複製的是其引用,當引用指向的值改變時也會跟著變化。

可以使用 for inObject.assign、 擴充套件運算子 ...Array.prototype.slice()Array.prototype.concat() 等,例如:

let obj = {
    name: 'Yvette',
    age: 18,
    hobbies: ['reading', 'photography']
}
let obj2 = Object.assign({}, obj);
let obj3 = {...obj};

obj.name = 'Jack';
obj.hobbies.push('coding');
console.log(obj);//{ name: 'Jack', age: 18,hobbies: [ 'reading', 'photography', 'coding' ] }
console.log(obj2);//{ name: 'Yvette', age: 18,hobbies: [ 'reading', 'photography', 'coding' ] }
console.log(obj3);//{ name: 'Yvette', age: 18,hobbies: [ 'reading', 'photography', 'coding' ] }
複製程式碼

可以看出淺拷貝只最第一層屬性進行了拷貝,當第一層的屬性值是基本資料型別時,新的物件和原物件互不影響,但是如果第一層的屬性值是複雜資料型別,那麼新物件和原物件的屬性值其指向的是同一塊記憶體地址。來看一下使用 for in 實現淺拷貝。

let obj = {
    name: 'Yvette',
    age: 18,
    hobbies: ['reading', 'photography']
}
let newObj = {};
for(let key in obj){
    newObj[key] = obj[key]; 
    //這一步不需要多說吧,複雜資料型別棧中存的是對應的地址,因此賦值操作,相當於兩個屬性值指向同一個記憶體空間
}
console.log(newObj);
//{ name: 'Yvette', age: 18, hobbies: [ 'reading', 'photography' ] }
obj.age = 20;
obj.hobbies.pop();
console.log(newObj);
//{ name: 'Yvette', age: 18, hobbies: [ 'reading' ] }

複製程式碼

深拷貝實現

1.深拷貝最簡單的實現是: JSON.parse(JSON.stringify(obj))

let obj = {
    name: 'Yvette',
    age: 18,
    hobbies: ['reading', 'photography']
}
let newObj = JSON.parse(JSON.stringify(obj));//newObj和obj互不影響
obj.hobbies.push('coding');
console.log(newObj);//{ name: 'Yvette', age: 18, hobbies: [ 'reading', 'photography' ] }
複製程式碼

JSON.parse(JSON.stringify(obj)) 是最簡單的實現方式,但是有一點缺陷:

1.物件的屬性值是函式時,無法拷貝。

let obj = {
    name: 'Yvette',
    age: 18,
    hobbies: ['reading', 'photography'],
    sayHi: function() {
        console.log(sayHi);
    }
}
let newObj = JSON.parse(JSON.stringify(obj));
console.log(newObj);//{ name: 'Yvette', age: 18, hobbies: [ 'reading', 'photography' ] }
複製程式碼

2.原型鏈上的屬性無法獲取

function Super() {

}
Super.prototype.location = 'NanJing';
function Child(name, age, hobbies) {
    this.name = name;
    this.age = age;
}
Child.prototype = new Super();

let obj = new Child('Yvette', 18);
console.log(obj.location); //NanJing
let newObj = JSON.parse(JSON.stringify(obj));
console.log(newObj);//{ name: 'Yvette', age: 18}
console.log(newObj.location);//undefined;原型鏈上的屬性無法獲取
複製程式碼

3.不能正確的處理 Date 型別的資料

4.不能處理 RegExp

5.會忽略 symbol

6.會忽略 undefined

let obj = {
    time: new Date(),
    reg: /\d{3}/,
    sym: Symbol(10),
    name: undefined
}

let obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); //{ time: '2019-06-02T08:16:44.625Z', reg: {} }
複製程式碼

2.實現一個 deepClone 函式

  1. 如果是基本資料型別,直接返回
  2. 如果是 RegExp 或者 Date 型別,返回對應型別
  3. 如果是複雜資料型別,遞迴。
function deepClone(obj) { //遞迴拷貝
    if(obj instanceof RegExp) return new RegExp(obj);
    if(obj instanceof Date) return new Date(obj);
    if(obj === null || typeof obj !== 'object') {
        //如果不是複雜資料型別,直接返回
        return obj;
    }
    /**
     * 如果obj是陣列,那麼 obj.constructor 是 [Function: Array]
     * 如果obj是物件,那麼 obj.constructor 是 [Function: Object]
     */
    let t = new obj.constructor();
    for(let key in obj) {
        //如果 obj[key] 是複雜資料型別,遞迴
        if(obj.hasOwnProperty(key)){//是否是自身的屬性
            t[key] = deepClone(obj[key]);
        }
    }
    return t;
}
複製程式碼

測試:

function Super() {

}
Super.prototype.location = 'NanJing';
function Child(name, age, hobbies) {
    this.name = name;
    this.age = age;
    this.hobbies = hobbies;
}
Child.prototype = new Super();

let obj = new Child('Yvette', 18, ['reading', 'photography']);
obj.sayHi = function () {
    console.log('hi');
}
console.log(obj.location); //NanJing
let newObj = deepClone(obj);
console.log(newObj);//
console.log(newObj.location);//NanJing 可以獲取到原型鏈上的屬性
newObj.sayHi();//hi 函式屬性拷貝正常
複製程式碼

3.迴圈引用

前面的deepClone沒有考慮迴圈引用的問題,例如物件的某個屬性,是這個物件本身。

function deepClone(obj, hash = new WeakMap()) { //遞迴拷貝
    if(obj instanceof RegExp) return new RegExp(obj);
    if(obj instanceof Date) return new Date(obj);
    if(obj === null || typeof obj !== 'object') {
        //如果不是複雜資料型別,直接返回
        return obj;
    }
    if (hash.has(obj)) {
        return hash.get(obj);
    }
    /**
     * 如果obj是陣列,那麼 obj.constructor 是 [Function: Array]
     * 如果obj是物件,那麼 obj.constructor 是 [Function: Object]
     */
    let t = new obj.constructor();
    hash.set(obj, t);
    for(let key in obj) {
        //如果 obj[key] 是複雜資料型別,遞迴
        if(obj.hasOwnProperty(key)){//是否是自身的屬性
            if(obj[key] && typeof obj[key] === 'object') {
                t[key] = deepClone(obj[key], hash);
            }else{
                t[key] = obj[key];
            }
            
        }
    }
    return t;
}
複製程式碼

測試程式碼:

const obj1 = {
    name: 'Yvetet',
    sayHi: function(){
        console.log('Hi')
    },
    time: new Date(),
    info: {

    }
    
}
obj1.circle = obj1;
obj1.info.base = obj1;
obj1.info.name = obj1.name;

console.log(deepClone(obj1));
複製程式碼

點選檢視更多

參考文章:

[1] www.ecma-international.org/ecma-262/6.…

[2] 【譯】理解 Javascript 執行上下文和執行棧

[3] css-tricks.com/debouncing-…

[4] github.com/mqyqingfeng…

[5] www.cnblogs.com/coco1s/p/40…

[6] www.cnblogs.com/wangfupeng1…

[7] www.w3.org/TR/2011/REC…

[8] github.com/mqyqingfeng…

謝謝各位小夥伴願意花費寶貴的時間閱讀本文,如果本文給了您一點幫助或者是啟發,請不要吝嗇你的贊和Star,您的肯定是我前進的最大動力。github.com/YvetteLau/B…

關注公眾號,加入技術交流群

【Step-By-Step】高頻面試題深入解析 / 週刊02

相關文章