經典大廠前端面試題(含解析)函數篇(二)

金三銀四就要到了。整理整理面試題。前端

整篇文章都與JS的函數相關。面試

從函數的定義開始

  1. 每一個函數實際上都是一個 Function 對象,即: (function(){}).constructor === Function
  2. 函數是 頭等對象/一等公民
    1. 函數能夠像任何其餘對象同樣具備屬性和方法
    2. 能夠賦值給變量(函數表達式
    3. 能夠做爲參數傳遞給函數(高階函數
    4. 能夠做爲另外一個函數的返回值(閉包

定義函數的方式有 4 種:算法

  1. new Function(str);
  2. 函數表達式 var fn = function() {}
  3. 函數聲明 function fn() {}
  4. 箭頭函數 var fn = () => {}

PS:new Function 聲明的對象是在函數建立時解析的,故比較低效chrome

什麼是閉包?

MDN的定義:函數與對其狀態即詞法環境的引用共同構成閉包(closure)。也就是說,閉包可讓你從內部函數訪問外部函數做用域編程

在JavaScript,函數在每次建立時生成閉包。waht????(MDN說的...設計模式

小紅書上的更好理解一點:閉包是指有權訪問另一個函數做用域中的變量的函數數組

也就是說,這就是閉包:安全

function saySomething(){
        var name = 'mokou';
        return function () {
            console.log(name);
        }
    }

    var say = saySomething()
    say()
複製代碼

閉包產生的緣由?

根據 JS 的垃圾回收機制(不提新生代和老生代),根據可達性算法:不可達就會被回收。bash

什麼是不可達?簡單來講:內存中沒有在內存中存放引用(即:沒有指針指向堆)就視爲不可達。(不懂堆棧的能夠看下上一篇JS基礎篇)微信

上面案例代碼中:saySomething 方法的返回值的引用存在了 say 變量中,因此可達,故:引用不會被銷燬,從而產生閉包。

說一個閉包的使用場景?

案例一:請求出錯的提示框(多個請求同時出錯通常都只有一個提示框)

實現思路:使用傳說中的設計模式 單例模式

如下是單例模式的實現:

const Singleton = (function() {
        var _instance;
        return function(obj) {
            return _instance || (_instance = obj);
        }
    })();

    var a = new Singleton({x: 1});
    var b = new Singleton({y: 2});

    console.log(a === b);
複製代碼

PS:上例還有一個優勢:_instance 是私有的,外部不能更改(保證安全無污染/可信)

案例二:解決 varfor + setTimeout 混合場景中的BUG

BUG 展現:

for (var i=1; i<=5; i++) {
        setTimeout(function() {
            console.log(i);
        }, i*300 );
    }
複製代碼

上例會打印:6 6 6 6 6

由於 var 是函數做用域(緣由1),而 setTimeout 是異步執行(緣由2),因此:當 console.log 執行的時候 i 已經等於 6 了(BUG產生)

在沒有 letconst 的年代,經常使用的解決方式就是閉包

for (var i = 1; i <= 5; i++) {
        (function(j) {
            setTimeout(function() {
                console.log(j);
            }, j*300);
        })(i);
    }
複製代碼

閉包的缺點?

缺點:

  1. 性能考量:閉包在處理速度和內存消耗方面對腳本性能具備負面影響(多執行了一個函數,多了一個內存指向)
  2. 可能內存溢出。(好比:在閉包中的 addEventListener 沒有被 removeEventListener

函數表達式和函數聲明的區別?

主要區別在

  1. 函數聲明被提高到了函數定義(能夠在函數聲明以前使用)
  2. 函數表達式要根據定義的方式進行判斷
    • 經過 var 定義:有變量聲明提高
    • 經過 let 和 const 定義:沒有變量提高

什麼是變量提高?

JavaScript 中,函數及變量(經過var方式)的聲明都將被提高到函數的最頂部。

案例:如下會輸出什麼結果?

var name = 'zmz';

    function say(){
        var name;
        console.log(name);

        var name = 'mokou';
        console.log(name);
    }

    say();
複製代碼

答案是:先輸出 undefined 再輸出 mokou

由於在函數 say 內部也聲明瞭一個 name(是經過 var)聲明的,因此會聲明提高,可是未賦值,因此首先輸出的是 undefined,以後是正常流程,輸出 mokou

PS:因爲 var 的變量提高很不友好,因此在 ES6 中添加了 letconst (本章主要講函數,暫略。)

函數定義和變量同名會怎麼樣?

在生成執行上下文時,會有兩個階段。

  1. 建立的階段(具體步驟是建立 VO),JS 解釋器會找出須要提高的變量和函數,而且給他們提早在內存中開闢好空間,函數的話會將整個函數存入內存中,變量只聲明而且賦值爲 undefined
  2. 代碼執行階段:咱們能夠直接提早使用。

在提高的過程當中:函數定義優先於變量提高,變量在執行階段纔會被真正賦值。

舉例

console.log(typeof a === 'function')

    var a = 1;
    function a() {}

    console.log(a == 1);
複製代碼

上例會打印 true true

說一下箭頭函數?

箭頭函數式 ES6 標準

  1. 箭頭函數的的this,就是定義時所在的對象
  2. 一旦綁定了上下文,就不可改變(call、apply、bind 都不能改變箭頭函數內部 this 的指向)
let obj = {
        x () {
            let y = () => {
                console.log(this === obj);
            }
            y();    // true
            // call、apply、bind 都不能改變箭頭函數內部 this 的指向
            y.call(window); // true
            y.apply(window); // true
            y.bind(window)(); // true
            // 同時,被bind綁定過的方法,也是不可變的,(不會再次被 bind、call、apply改變this的指向)
        }
    }
複製代碼
  1. 因爲this指向問題,因此:箭頭函數不能看成構造函數,不能使用new命令
  2. 箭頭函數沒有 arguments,須要手動使用 ...args 參數代替
  3. 箭頭函數不能用做 Generator 函數

那其餘函數的 this 指向問題呢?

  1. 以函數的形式調用(this指向 window)
function fn () {
        console.log(this, 'fn');
        function subFn () {
            console.log(this, 'subFn');
        }
        subFn(); // window
    }
    fn(); // window
複製代碼
  1. 以方法的形式調用 (this指向 調用函數的對象)
var x = 'abc';
    var obj = {
        x: 123,
        fn: function () {
            console.log(this.x);
        }
    }
    obj.fn(); //  123
    var fn = obj.fn;
    fn(); // abc
複製代碼
  1. callapplybind 的形式調用(更改指向,箭頭函數除外)
  2. 以構造函數調用,(指向實例)
  • new的實例是 構造函數中return的對象 || this
// 構造函數中有 return對象 的狀況
    function A() {
        return {
            a : 1
        }
    }
    A.prototype.say = function () {
        console.log(this, 'xx')
    }
    var a = new A();
    // a = {a: 1}
    // a.say === undefined
    // 構造函數中 沒有return對象 的狀況
    function A() {
        // 能夠手動 return this
    }
    A.prototype.say = function () {
        console.log(this, 'xx')
    }
    var a = new A();
    a.say();
    // A {} "xx"
複製代碼

call 和 apply 的不一樣?

  1. 入參不一樣
  2. 性能差別(call比apply快不少)

性能測試:如下測試環境爲 chrome v73

function work(a, b, c) {}

    for (var j = 0; j < 5; j++) {
        console.time('apply');
        for (var i = 0; i < 1000000; i++) {
            work.apply(this, [1, 2, 3]);
        }
        console.timeEnd('apply');

        console.time('call');
        for (var i = 0; i < 1000000; i++) {
            work.call(this, 1, 2, 3);
        }
        console.timeEnd('call');
    }
    
    /*
        // apply: 69.355224609375ms
        // call: 8.7431640625ms

        // apply: 57.72119140625ms
        // call: 4.146728515625ms

        // apply: 50.552001953125ms
        // call: 4.12890625ms

        // apply: 50.242919921875ms
        // call: 4.720947265625ms

        // apply: 49.669921875ms
        // call: 4.054931640625ms
    */
複製代碼

測試結果: call 比 apply快 10倍(大約是這樣的)

緣由:.apply 在運行前要對做爲參數的數組進行一系列檢驗和深拷貝,.call 則沒有這些步驟

怎麼實現 call ?

實現思路

  1. 前置知識點:
    1. 當函數以方法的形式調用時,this指向被調用的對象
    2. 函數的參數是值傳遞
    3. 引用類型可寫
  2. myCall 的第一個參數(暫命名爲that)做爲 被調用的對象
  3. that上添加一個方法(方法名隨意,暫命名fn
  4. 經過 that[fn](...args) 調用方法(此時this指向爲that
  5. 刪除掉第3步添加的方法

具體代碼

Function.prototype.myCall = function(that, ...args) {
        let func = this;
        let fn = Symbol("fn");

        that[fn] = func;
        let res = that[fn](...args);
        delete that[fn];

        return res;
    }
複製代碼

測試一下

function say(x,y,z) {
        console.log(this.name, x, y, z)
    }
    
    say.myCall({name: 'mokou'}, 1, 2, 3)

    // 打印 mokou 1 2 3
複製代碼

怎麼實現一個 bind ?

實現思路

  1. bind 只改變 this 指向,不執行函數,那麼能夠用閉包來實現
  2. 具體更改 this指向的問題能夠借用 call 實現
Function.prototype.myBind = function(that) {
        if (typeof this !== 'function') {
            throw new TypeError('Error')
        }
        const _fn = this;
        return function(...args) {
            _fn.call(that, ...args)
        }
    }
複製代碼

測試一下:

function say(x,y,z) {
        console.log(this.name, x, y, z)
    }

    const testFn = say.myBind({name: 'mokou'})
    testFn(1, 2, 3);

    // 打印 mokou 1 2 3
複製代碼

說一下尾遞歸?

PS: 這個小題是半搬運的 @阮一峯 老師的博客

尾遞歸就是:函數最後單純return函數,尾遞歸來講,因爲只存在一個調用記錄,因此永遠不會發生"棧溢出"錯誤。

ES6出現的尾遞歸,能夠將複雜度O(n)的調用記錄,換爲複雜度O(1)的調用記錄

測試:不使用尾遞歸

function Fibonacci (n) {
        if ( n <= 1 ) {return 1};
        // return 四則運算
        return Fibonacci(n - 1) + Fibonacci(n - 2);
    }
    Fibonacci(10) // 89
    Fibonacci(100) // 超時
    Fibonacci(100) // 超時
複製代碼

測試:使用尾遞歸

function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
        if( n <= 1 ) {return ac2};
        return Fibonacci2 (n - 1, ac2, ac1 + ac2);
    }
    Fibonacci2(100) // 573147844013817200000
    Fibonacci2(1000) // 7.0330367711422765e+208
    Fibonacci2(10000) // Infinity
複製代碼

蹦牀函數(協程):解決遞歸棧溢出問題,將函數變成循環

function trampoline(f) {
        while (f && f instanceof Function) {
            f = f();
        }
        return f;
    }
複製代碼

尾遞歸的優化:

function tco(f) {
        var value;
        var active = false;
        var accumulated = [];

        return function accumulator() {
            accumulated.push(arguments);
            // 除了第一次執行,其餘的執行都是爲了傳參
            if (!active) { // 很重要,若是不使用 active關閉後續進入, sum函數超過會溢出
                // 在第一次進入進入遞歸優化時激活,關閉後續進入
                active = true;
                // 有參數就執行
                while (accumulated.length) {
                    // 調用f,順便清除參數
                    value = f.apply(this, accumulated.shift());
                    // 因爲while中又調用 f,f調用sum,而後sum在執行時給accumulated塞了一個參數
                    // 因此 while循環會在sum返回結果前一種執行,直到遞歸完成
                }
                active = false;
                return value;
            }
        };
    }

    var sum = tco(function(x, y) {
        if (y > 0) {
            // 此時的sum是accumulator
            // 執行sum等於給accumulator傳參
            return sum(x + 1, y - 1)
        }
        else {
            return x
        }
    });

    sum(1, 100000)
複製代碼

for in、 for of、forEach 各自的特色是什麼?

  1. for in 遍歷的是對象的可枚舉屬性
  2. for of 遍歷的是對象的迭代器屬性
  3. forEach 只能遍歷數組,且不能中斷(break等無效)

手寫一個防抖函數?

防抖函數:

function debounce(fn, wait) {
        let timer = null;
        return (...args) => {
            clearTimeout(timer);
            timer = setTimeout(() => {
                fn.apply(this, args);
            }, wait);
        }
    }
複製代碼

使用場景:輸入框校驗

手寫一個節流函數

節流函數

function throttle(fn, wait = 300) {
        let flag = true;
        return (...args) => {
            if (!flag) return;
            flag = false;
            setTimeout(() => {
                fn.apply(this, args);
                flag = true;
            }, wait);
        }
    }
複製代碼

使用場景:

  1. 延遲防抖函數:onscroll 時觸發的事件
  2. 當即執行防抖函數:按鈕的點擊事件(某種狀況下 once函數 更合適)

說一下 Class ?

ES6 的 class 能夠看做只是一個語法糖,它的絕大部分功能,ES5 均可以作到,新的class寫法只是讓對象原型的寫法更加清晰、更像面向對象編程的語法而已。

Class 和 function 有什麼不一樣

  1. 類沒有變量提高,
new B();
    class B {}
    // Uncaught ReferenceError: B is not defined
複製代碼
  1. 類的全部方法,都不可枚舉
class A {
        constructor() {
            this.x = 1;
        }
        static say() {
            return 'zmz';
        }
        print() {
            console.log(this.x);
        }
    }
    Object.keys(A); // []
    Object.keys(A.prototype); // []
複製代碼
  1. 類的的全部方法,沒有原型對象prototype
接例2
    console.log(A.say.prototype); // undefined
    console.log(new A().print.prototype); // undefined
複製代碼
  1. 類不能直接使用,必須使用 new 調用。
接例2
    A();
    // Uncaught TypeError: Class constructor A cannot be invoked without 'new'
複製代碼
  1. 類內部啓用嚴格模式
class B {
        x = 1
    }
    // Uncaught SyntaxError: Identifier 'B' has already been declared
複製代碼

ES5 怎麼實現繼承?

須要完成功能

  1. 繼承 構造屬性
  2. 繼承 原型方法
  3. 糾正構造器

主流繼承方案

function Parent () {
        this.name = 'mokou';
    }
    
    function Child() {
        Parent5.call(this);
        this.age = '18';
    }

    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;
複製代碼

繼承優化(參考 Babel 的降級方案)

function inherits(subClass, superClass) {
        subClass.prototype = Object.create(superClass && superClass.prototype, {
            constructor: {
                value: subClass,
                writable: true,
                configurable: true
            }
        });
        if (superClass) Object.setPrototypeOf(subClass, superClass);
    }
複製代碼

說一下 new 的過程?

  1. 建立一個空對象
  2. 新對象的__proto__指向構造函數的 prototype
  3. 綁定 this,指向構造方法
  4. 返回新對象

詳細代碼

function myNew() {
        var obj = new Object()
        var Con = [].shift.call(arguments)
        obj.__proto__ = Con.prototype
        var result = Con.apply(obj, arguments)
        return typeof result === 'object' ? result : obj
    }
複製代碼

說一下原型鏈吧?

  1. 對象都有 __proto__, 它是一個訪問器屬性,指向了咱們不能直接訪問到的內部屬性 [[prototype]]
  2. 函數都有 prototype,每一個實例對象的 __proto__ 指向它的構造函數的 prototype
    • son.__proto__ === Son.prototype
  3. 屬性查找會在原型鏈上一層一層的尋找屬性
    • Son.prototype.__proto__ === Parent.prototype
  4. 層層向上直到一個對象的原型對象爲 nullnull 沒有原型,並做爲這個原型鏈中的最後一個環節。
    • son.__proto__.__proto__........ === null

舉例:

class Parent {}
    class Son extends Parent{}
    
    const log = console.log;
    
    const son = new Son();
    const parent = new Parent();
    
    log(son.constructor === Son)
    log(son.__proto__ === son.constructor.prototype)
    
    log(son.__proto__ === Son.prototype)
    log(Son.prototype.__proto__ === Parent.prototype)
    log(Parent.prototype.__proto__ === Object.prototype)
    log(Object.prototype.__proto__ === null)
    
    log(son.__proto__.__proto__.__proto__.__proto__ === null)

    log(Son.constructor === Function)
    log(Son.__proto__ === Parent)
    
    log(Parent.constructor === Function)
    log(Parent.__proto__ === Object.__proto__)
複製代碼

PS:因爲 __proto__ 的性能問題和兼容性問題,不推薦使用。

推薦

  • 使用 Object.getPrototypeOf 獲取原型屬性
  • 經過 Object.setPrototypeOf 修改原型屬性
  • 經過 Object.create() 繼承原型

PS: for inObject.keys 會調用原型 屬性

  • 不調用不可枚舉屬性
  • isPrototypeOf 和 hasOwnProperty

說一下 靜態屬性/方法 ?

靜態屬性/方法:就是不須要實例化類,就能直接調用的 屬性/方法。

綜合上面ParentSon的例子

不論是 sonSon仍是Parent,它們都是對象,因此均可以直接賦值,也能在__proto__上賦值

因此靜態屬性/方式直接賦值就能夠了

Parent.x = 1
    Parent.__proto__.x =2

    console.log(Parent.x)  // 1
    console.log(Parent.__proto__.x) // 2
複製代碼

若是使用 ES6的 Class 定義一個類

class A {
        constructor() {
            this.x = 1;
        }
        static say() {
            console.log('zmz');
        }
        print() {
            console.log(this.x);
        }
    }

    A.say()
複製代碼

後期規劃

  1. 近期:Promise 相關、函數式編程相關
  2. 中期:源碼相關
  3. 預計代碼層面寫的差很少了會寫軟件工程相關

關注微信號"前端進階課" 回覆"Vue"、"React"、"KOA"、"全棧" 有相關課程贈送哦。文章將不按期發送閱讀紅包,敬請期待

相關文章
相關標籤/搜索