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

本週面試題一覽:javascript

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

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


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

節流函數的做用

節流函數的做用是規定一個單位時間,在這個單位時間內最多隻能觸發一次函數執行,若是這個單位時間內屢次觸發函數,只能有一次生效。html

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

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

節流應用場景

1.按鈕點擊事件github

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 (即全局執行上下文)首先入棧,過程以下:

僞代碼:

//全局執行上下文首先入棧
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做用域,全局做用域]

點擊查看更多

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 的重疊。

根據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>
複製代碼

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>
複製代碼

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>
複製代碼

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>   
複製代碼

根據規則,BFC的區域不會與float box重疊。所以,能夠觸發生成一個新的BFC,以下:

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

點擊查看更多

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} 是可變的。思考下若是想但願一個對象是不可變的,應該用什麼方法?

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
複製代碼

雖而後來知道了爲何,可是想要獲得本身須要的結果,還得整個閉包,我...我作錯了什麼,要這麼對我...

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…

關注公衆號,加入技術交流羣

相關文章
相關標籤/搜索