前端面試高頻出現的八個基礎JS概念

數據類型

Javascript 世界裏將數據類型分爲了兩種:原始數據類型引用數據類型前端

共有如下八種數據類型:NumberStringBooleanNullUndefinedObjectSymbol(ES6)BigInt(ES10)面試

原始數據類型:StringNumberBooleanNullUndefinedSymbolBigInt
引用數據類型:Object數組

不一樣類型的存儲方式:
原始數據類型:原始數據類型的值在內存中佔據固定大小,保存在棧內存中。
引用數據類型:引用數據類型的值是對象,在棧內存中只是保存對象的變量標識符以及對象在堆內存中的儲存地址,其內容是保存中堆內存中的。微信

varletconst 有什麼區別

  1. var 具備變量提高性質,letconst 沒有。

所謂 變量提高 指的是 JS 在預編譯階段,函數和以 var 聲明的變量會被 JS 引擎提高至當前做用域頂端。markdown

// 編譯前
function sayName() {
    console.log(name);
    var name = '橙某人';
}
// 編譯後
function sayName() {
    var name;
    console.log(name);
    name = '橙某人';
}
複製代碼
  1. var 不具塊級做用域,letconst 具備塊級做用域性質。
// 例子一
{
    var a = 1;
    let b = 2;
    const c = 3;
}
console.log(a); // 1
console.log(b); // 報錯
console.log(c); // 報錯
// 例子二
function fn() {
    if(true) {
        var a = 1;
        let b = 2;
        const c = 3;
    }
    console.log(a); // 1
    console.log(c); // 報錯
    console.log(c); // 報錯
}
fn()
複製代碼

暫時性死區:JS 引擎在預編譯代碼的時候,若是遇到 var 聲明會將它提高至當前做用域頂端,若是遇到 letconst 會將它們放入暫時性死區(Temporal Dead Zone),簡稱 TDZ, 它的性質是會造成一個封閉的做用域,任何訪問 TDZ 中的變量就會報錯。只有在執行過變量的聲明語句後,變量纔會從 TDZ 中移除,才能進行正常的變量訪問。閉包

  1. var 能重複聲明,letconst 重複聲明會報錯。
  2. var 全局聲明變量會掛載在 windowletconst 不會。
var a = a;
console.log(window.a); // 1

let b = 2;
console.log(window.b); // undefined

const c = 3;
console.log(window.c); // undefined
複製代碼

瞭解一下 JS 的工做過程,有利於更好的理解問題哦:app

JS 引擎的執行過程分爲三個階段:語法分析階段、預編譯階段、執行階段。

語法分析階段:檢查書寫的 JS 語法有沒有錯誤,如是否少寫個'{'等。
預編譯階段:分爲全局預編譯、局部預編譯。函數

全局預編譯:post

  1. 建立GO對象。
  2. 找變量聲明,將變量聲明做爲GO對象的屬性名,並賦值undefined。
  3. 找全局裏的函數聲明,將函數名做爲GO對象的屬性名,值賦予函數體。

局部預編譯:學習

  1. 建立一個AO對象。
  2. 找形參和變量聲明,將形參和變量聲明做爲AO對象的屬性名,值爲undefined。
  3. 將實參和形參統一。
  4. 在函數體裏找函數聲明,將函數名做爲AO對象的屬性名,值賦予函數體。

執行階段:從上到下,逐行執行,變量賦值階段也在此完成。

判斷數據類型的四種方式

  1. typeof,區分不了細緻的 Object 類型,如 ArrayDateRegExp都只是返回 object
console.log(typeof 1); // number
console.log(typeof '1'); // string
console.log(typeof true); // boolean
console.log(typeof null); // object, null 在 typeof 下被標記爲 object, 這是JS一個歷史bug了
console.log(typeof undefined); // undefined
console.log(typeof {}); // object
console.log(typeof Symbol()); // symbol
console.log(typeof 1n); // bigint
複製代碼

建立一個 BigInt 類型的方式有兩種:在一個整數字面量後面加 n 或者調用 BigInt 函數。
const a = BigInt(1);
const b = 1n;

  1. Object.prototype.toString.call(),基本能知足對各類數據類型的判斷。
console.log(Object.prototype.toString.call(1)); // [object Number]
console.log(Object.prototype.toString.call('1')); // [object String]
console.log(Object.prototype.toString.call(true)); // [object Boolean]
console.log(Object.prototype.toString.call(null)); // [object Null]
console.log(Object.prototype.toString.call(undefined)); // [object Undefined]
console.log(Object.prototype.toString.call({})); // [object Object]
console.log(Object.prototype.toString.call(Symbol())); // [object Symbol]
console.log(Object.prototype.toString.call(1n)); // [object BigInt]
複製代碼

它對於其餘一些內置引用類型的判斷也很適合。

console.log(Object.prototype.toString.call([])); // [object Array]
console.log(Object.prototype.toString.call(new Date())); // [object Date]
console.log(Object.prototype.toString.call(/a/g)); // [object RegExp]
console.log(Object.prototype.toString.call(function(){})); // [object Function]
console.log(Object.prototype.toString.call(new Error())); // [object Error]
console.log(Object.prototype.toString.call(Math)); // [object Math]
console.log(Object.prototype.toString.call(JSON)); // [object JSON]
function fn() {
    console.log(Object.prototype.toString.call(arguments)); // [object Arguments]
}
fn();
複製代碼
  1. constructor,是根據原型鏈原來來判斷的,由於每個實例對象均可以經過 constructor 來訪問它的構造函數。
console.log((1).constructor === Number);
console.log('1'.constructor === String);
console.log(true.constructor === Boolean);
// console.log(null.constructor);
// console.log(undefined.constructor);
console.log({}.constructor === Object);
console.log(Symbol().constructor === Symbol);
console.log(1n.constructor === BigInt);
複製代碼

因爲 undefinednull 是無效的對象,並不具有 constructor 並且 null 是做爲原型鏈的末端結尾。

  1. instanceof,內部機制是經過檢查構造函數的原型對象(prototype)是否出如今被檢測對象的原型鏈上來判斷的。
console.log(1 instanceof Number); // false
console.log('1' instanceof String); // false
console.log(true instanceof Boolean); // false
console.log({} instanceof Object); // true
console.log(function(){} instanceof Function); // true
複製代碼

instanceof 強調的是對擁有原型鏈的引用類型對象的判斷,因此對於原生數據類型就一籌莫展(字面量方式建立的不能檢測)。咱們對於它更多的是在這種場景中使用:

function Person(){};
function Student(){};
var p = new Person();
console.log(p instanceof Person); // true
console.log(p instanceof Student); // false
複製代碼

如何快速判斷一個 window 類型

這是小編在一次面試中面試官問個人題目,當時很懵,由於這個問題是創建在我剛答完上面說過的 「判斷數據類型的四種方式」 的時候,當時我隨口就說了分別能利用 Object.prototype.toString.call()constructorinstanceof 三種方式來判斷。

console.log(Object.prototype.toString.call(window)); // [object Window]
console.log(window.constructor === Window); // true
console.log(window instanceof Window); // true
複製代碼

面試官一臉嚴肅的表情問我「除了這三種,還有嗎?」

我......心想...還有嗎???這還不夠嗎?腦殼想不出來東西了,我傻笑着抓着腦殼說:「暫時我沒想到其餘方式了,這還有其餘快的方式嗎?求指教」。

面試官:「很簡單,window 它有一個 window屬性指向自身,能夠利用這個特性來判斷。」

console.log(window.window === window); // true
複製代碼

好傢伙,細,真的細。

image.png

閉包

一個萬年常考題,MDN 對閉包的定義爲:

閉包是指那些可以訪問自由變量的函數。

??? 懵逼?會不會有小夥伴學了好久的閉包,也常用,但就是死活說不清楚它是啥,能幹什麼?在網上關於它的文章,那是數不勝數,基本一搜索就一大把,各篇文章說得五花八門。

但看了那麼多文章學習,你又是否學會了呢?在我這,我以爲學會一個知識點,就是你能用大白話把它說出來,而不是背它的概念,只有這樣才能說明本身消化了,剩下的就是不斷去應用增強印象就能夠了,慢慢就記牢了。

此次不對閉包深究了,沒意思,就簡單說說幾個問答題。

面試官:說說你對閉包的理解?
這是一個客觀性很是強的問法,咱們直追本質,面試官想知道什麼呢?他其實只是想聽聽你對閉包是否有本身的我的理解,對於概念的他天天不知道聽過多少次了。

答:咱們知道函數內部能直接讀取函數外部定義的變量,可是函數外部沒法讀取函數內部的變量,閉包在我看來,就是提供一種訪問函數內部變量的橋樑。

面試官:說說閉包的好處和壞處?

答:使用閉包的好處是提供了訪問函數內部變量的方式與避免形成全局變量污染。使用閉包的壞處是會形成內存消耗大,濫用閉包會致使內存泄露,形成頁面奔潰。

面試官:你能說說爲何使用閉包會形成內存泄露嗎?

答:函數的做用域鏈是在函數定義的時候建立的,在函數運行完成,銷燬的時候消亡,這時它內部的變量就應該被銷燬,內存被回收,可是閉包能讓其繼續延續下去,不被垃圾回收機制回收。因爲變量都是維護在內存中的,這些變量數據就會一直佔用着內存,最後超載使用內存,形成內存泄露。

面試官:手寫一個閉包的例子吧?
手寫例子通常也是必不可少的,閉包的例子很是很是的多,咱們只要記住一些簡單、容易記住的小例子防身,我以爲就能夠了。
一道經典的閉包例子:

var inputs = document.getElementsByTagName('input');
for(var i = 0;i<inputs.length;i++) {
    (function(i) {
        inputs[i].onclick = function() {
            console.log(i)
        }
    })(i)
}
複製代碼

防抖節流

這也是一個老題目了,就在我寫這篇文章的前兩天,恰好就被問過。(T_T) 這是兩個容易記混淆的概念,這裏我想了兩個例子,你且看看妥不穩當。

相信各位對王者榮耀(趕忙上號,峽谷見!!!)不陌生了,咱們直接來看:

  • 防抖:有點像英雄在回城的時候,每次點擊回城要必定時間才能傳送,在這個時間內若是移動英雄,則要從新開始。
  • 節流:有點像射手英雄,無論你按得多快,只要攻速沒增長,也只會一下一下射出攻擊而已。

嘿嘿,這兩個例子如何?有沒有幫你記住了他倆呢?(  ̄ ▽ ̄)

防抖(debounce)

在觸發事件後 N 秒後才執行函數,若是在 N 秒內又觸發了函數,則從新進行計時。

  • 應用場景:
    輸入框進行輸入實時搜索、頁面觸發resize事件的時候。

  • 手寫:

    function debounce(fn, wait) {
        var timer = null
        return () => {
            clearTimeout(timer);
            timer = setTimeout(fn, wait)
        }
    }
    複製代碼

節流(throttle)

在規定的一個單位時間內,只觸發一次函數,若是單位時間內觸發屢次函數,只有一次生效。

  • 應用場景:
    頁面滾動事件。

  • 手寫:

    function throttle(fn, wait) {
        var timer = null;
        return () => {
            if(!timer) {
                timer = setTimeout(() => {
                    timer = null;
                    fn()
                }, wait)
            }
        }
    }
    複製代碼

上面列舉了兩個的簡單手寫過程,如今去面試,基本都有手寫代碼題了,這都成爲一個潮流了,這致使了不少時候咱們要記住不少代碼的書寫過程,這真的讓人難受想哭(︶︿︶)。
我本身的方法是,我會記住最簡單的代碼結構,剔除那些非主流的功能設計,只留一個能實現主要功能的代碼構架就行,剩下的若是真遇到手寫代碼的時候,再本身慢慢推導出來就行。(雖然不少時候可能也推導不出來,哈哈哈)

原型與原型鏈

  • 原型

什麼是原型呢?它是一個對象,咱們也稱它爲原型對象,代碼中用 prototype 來表示。

  • 原型鏈

那什麼又是原型鏈呢?原型與原型層層相連接的過程即爲原型鏈。

原型與原型鏈 在前端是一個很是基礎的 JS 概念了,相信你也或多或少會有所聽過了,咱們且來看看下面這張圖你是否看得懂:

image.png

要理解好 原型與原型鏈 咱們要記住五個很重要的東西,這是咱們每次回顧它們的時候都要想起來的:

  • JS 把對象(除null)分爲普通對象與函數對象,無論是什麼對象都會有一個 __proto__ 屬性。
  • 函數對象還會有一個 prototype 屬性,也就是說函數默認擁有 __proto__ 屬性與 prototype 屬性。
  • 普通對象的 __proto__ 屬性與函數對象的 prototype 屬性都會指向它們對應的原型對象。
  • 函數對象另外一個 __proto__ 屬性會指向 Function.prototype 原型,原型鏈的末端爲 null
  • 原型對象 它會擁有一個 constructor 屬性指向它的構造函數。

通常面試聊到 原型與原型鏈 能講清楚這五個點基本也就及格了,那麼如何記住這些東西?

答案:畫圖,按着本身的理解,本身畫兩天你就印象深入,不騙你,略略略。

如何推導出這個圖,能夠看看小編前面寫過這篇文章:看完,你會發現,原型、原型鏈原來如此簡單!

call與apply與bind

call/apply/bind 的面試題無非逃不過的就是手寫代碼實現了(︶︿︶),咱們就不聊它的應用了,簡單聊聊它會涉及的問題不是。

面試官:它們三者有什麼做用?

答:它們三者的主要做用都是爲了改變函數的 this 指向,目前大部分關於它們的應用都是圍繞這一點來進行的。

面試官:那它們之間有什麼區別?

答: 有三點不一樣:

  1. 參數不一樣,callbind 參數是經過一個一個傳遞的, apply 只接收一個參數而且是以數組的形式傳遞,後續參數是無效的。
  2. 執行時機不一樣,callapply 當即調用當即執行,bind 調用後會返回一個函數,調用該函數才執行。
  3. bind 返回的函數能做爲構造函數使用,傳遞的參數依然有效,但綁定的 this 將失效。

(記憶apply 傳遞的參數爲數組,能夠根據開頭 a 等同於 Array 記憶哦(-^〇^-))

有時候適當的總結會讓面試官很舒心,你上面嗶哩啪啦講一大堆,講完本身也忘了,對比你直接說: 「有xx點不一樣,第一點是...第二點是...」 ,相信後者的方式更能博得面試官的好感。

面試官:手寫實現bind()方法吧?
只是能說出它們三者的做用區別,並不能讓咱們脫穎而出,只有理解夠深咱們才能卷得過別人,手寫必不可少!特別是 bind的,大廠的面試中基本是高頻出現。

關於 callapply 的實現你能夠看小編以前看的文章瞭解一下。 Call與Apply函數的分析及手寫實現

bind 的實現以下:

Function.prototype.myBind = function(context) {
    // 保存原函數
    var _fn = this;
    // 獲取第一次傳遞的參數
    var params = Array.prototype.slice.call(arguments, 1);

    var Controller = function() {}; 

    // 返回的函數, 可能會被看成 new調用
    var bound = function() {  
        // 獲取第二次傳遞的參數
        var secondParams = Array.prototype.slice.call(arguments);
        /** 考慮返回的bound函數是被當成 普通的調用 仍是 new調用:
         *  new調用: 綁定的 this 失效, bound函數中的this指向自身
         *  普通的調用: 正常改變執行函數的 this 指向, 把它指向 context
         */
        var targetThis = this instanceof Controller ? this : context;
        return _fn.apply(targetThis, params.concat(secondParams));
    }
    /**
     * 1. 返回的函數應該具備原函數的原型。
     * 2. 修改返回函數的原型不能影響原函數的原型。
     */ 
    Controller.prototype = _fn.prototype;
    bound.prototype = new Controller();
    return bound;
}
複製代碼

測試代碼:

function fn(val1, val2, val3) {console.log(this, val1, val2, val3)}
var res = fn.myBind(obj, 1, 2)
res(3); // {name: "橙某人"} 1 2 3
複製代碼

深淺拷貝

若是你學過一些前端知識,知道棧空間與堆空間,那麼我相信你對於理解這個概念必定沒啥問題了,涉及這個知識點的面試題通常可能會是代碼層面上,如:實現一個淺拷貝函數?或者遞歸實現一下深拷貝函數等。

淺拷貝

淺拷貝操做會建立一個新對象,這個對象有着原始對象屬性值的一份精確拷貝,若是屬性是基本類型,拷貝的就是基本數據類型的值,若是屬性是引用類型,拷貝的就是內存地址。

  • 手寫
    function copy(original) {
        var o = {};
        for(var key in original) {
            o[key] = original[key];
        }
        return o;
    }
    複製代碼

深拷貝

深拷貝操做會將一個對象從內存中完整拷貝一份出來,從堆內存中開闢一個新的區域放新對象,且修改新對象不會影響原對象。

  • 手寫
    function deepCopy(original) {
        if(typeof original !== 'object') return;
        var o = {}
        for(let key in original) {
            o[key] = typeof original[key] === 'object' ? deepCopy(original[key]) : original[key]
        }
        return o;
    }
    複製代碼

若是你瞭解深淺拷貝的基本概念,相信上面的代碼對你沒什麼難度的。(^▽^)

面試官:你以爲賦值與淺拷貝有什麼區別? (這是在網上衝浪的時候偶然看到的一題)

  1. 把一個對象賦值給另外一個新變量時,賦值的是該對象在棧中的地址,兩個對象指向的是同一個堆空間。
  2. 淺拷貝是從新在堆空間中建立一塊空間,拷貝後的基本數據類型不相互影響,拷貝後的對象引用類型會相互影響。

(說白了就是:是否在堆內存中建立新空間。)

New關鍵字

關於 new 關鍵字相信用法你已經很是熟了。

function Person() {};
var p = new Person();
複製代碼

可是,我須要你記住它幹了三件事件:

  1. 新建了一個空對象並返回。
  2. 新對象的原型(__proto__)指向構造函數的原型(prototype)。
  3. 構造函數的 this 指向新對象。

記住了這三件事情基本也就不用擔憂和它相關的面試題了,即便是讓你來模擬它的實現,也是很是簡單的。

function myNew(constructor) {
    // 1. 建立新對象
    const newObject = new Object();
    // 2. 改變新對象的原型指向
    newObject.__proto__ = constructor.prototype;
    // 3. 構造函數的 this 指向新對象
    constructor.apply(newObject, Array.prototype.slice.call(arguments, 1))

    return newObject;
}
複製代碼

是否是徹底沒有難度? 更多詳情

微信圖片_20210112181033.jpg

柯里化函數

在數學和計算機科學中,柯里化是一種將使用多個參數的一個函數轉換成一系列使用一個參數的函數的技術。

這概念很懵吧?沒錯,我也懵。不過沒有關係,咱們記住它的例子就行,它的例子頗有特色,看完,你再來回頭想一想可能就能懂了噢。

咱們先來看一個很簡單的三個數累加示例:

function add(a, b, c) {
    return a + b + c;
}
console.log(add(1, 2, 3)); // 6
複製代碼

這是一個很簡單的操做,但有時咱們但願 add 函數可以靈活一點,像是這樣子的形式也能實現:

console.log(add(1, 2)(3)); // 6
console.log(add(1)(2, 3)); // 6 
console.log(add(1)(2)(3)); // 6
console.log(add(4, 5, 6)); // 15
複製代碼

這個時候就會用到柯里化的概念了,下面咱們來直接看它的代碼,帶很詳細的解釋,實際的代碼沒有多少行的,徹底沒有負擔的,哈哈。

/**
 * 柯里化函數: 延遲接收參數, 延遲執行, 返回一個函數繼續接收剩餘的參數
 * call: 收集參數
 * apply: 注入參數, 參數變成了數組, 借用apply能依次注入參數
 */ 
function curry(fn) {
    // 獲取原函數的參數長度
    var argsLength = fn.length;

    return function curried() {
        // 獲取調用 curried 函數的參數
        var args1 = Array.prototype.slice.call(arguments);
        // 判斷收集的參數是否知足原函數的參數長度: 知足-調用原函數返回結果  不知足-繼續柯里化(遞歸)
        if(args1.length >= argsLength) {
            // 調用原函數返回結果
            return fn.apply(this, args1);
        }else {
            // 不知足繼續返回一個函數收集參數
            return function() {
                var args2 = Array.prototype.slice.call(arguments);
                // 繼續柯里化
                return curried.apply(this, args1.concat(args2));
            }
        }
    }
}
複製代碼

測試代碼

function add(a, b, c) {
    return a + b + c;
}
var newAdd = curry(add);
console.log(newAdd(1, 2, 3)); // 6
console.log(newAdd(1, 2)(3)); // 6
console.log(newAdd(1)(2, 3)); // 6
console.log(newAdd(1)(2)(3)); // 6
console.log(newAdd(4, 5, 6)); // 15
複製代碼

柯里化函數在我看來就是,延遲接收參數,延遲執行,返回一個函數繼續接收剩餘的參數,當函數參數接收知足條件的時候,就執行原函數返回結果。

至此,本篇文章就寫完啦,撒花撒花。

image.png

但願本文對你有所幫助,若有任何疑問,期待你的留言哦。 老樣子,點贊+評論=你會了,收藏=你精通了。

相關文章
相關標籤/搜索