「面試重點」聊一聊JS中call、apply、bind裏的當心思

引子

面試的重點難點的坑來啦!~/(ㄒoㄒ)/~~不出意外,this在ES5中是比較頭疼和讓初學者恐懼的一塊,儘管在 ES6 中可能會極大避免 this 產生的錯誤,可是爲了前端初學者可以在使用上可以將call,apply,bind等容易混淆的this指向問題,最好仍是瞭解一下call、apply、bind 三者的區別,以及它們在底層中是如何來實現的~前端

call、apply、bind它們 究竟藏在哪裏

全部函數能調call、apply.bind的方法前提是function是Function的實例,而Function.prototype上面有這三個方法es6

Function.prototype = {
    call
    apply
    bind
}
複製代碼

call / apply / bind 的使用

call / apply

用法:第一個參數就是改變的this指向,寫誰就是誰(特殊:非嚴格模式下,傳遞null/undefined指向的也是window)
區別:執行函數,傳遞的參數方式有區別,call是一個個傳遞,apply是把須要傳遞的參數放到數組中總體傳遞
面試

func.call([context],10,20);
func.apply([context],[10,20])
複製代碼

bind

用法:bind不是當即執行函數,屬於預先改變this和傳遞一些內容 => "柯里化思想"
區別:call/apply都是改變this的同時直接將函數執行,而bind須要手動執行
數組

let obj = {
    fn(x, y) {
        console.log(this, x, y);
    }
}

obj.fn.call();              // window 嚴格模式下: undefined
obj.fn.call(null);          // ...
obj.fn.call(undefined);     // ...
obj.fn.call(window, 10, 20);  // window
obj.fn.apply(window, [10, 20]);  // window
複製代碼

例:在1秒鐘以後,執行fn函數,讓其函數裏的this變爲window

錯誤寫法:
setTimeout(obj.fn.call(window, 10, 20));
複製代碼

緣由:
fn.call()自動執行,執行以後將結果(window)賦值給setTimeout再讓瀏覽器執行,顯然是錯誤的,因setTimeout第一個參數應爲要執行的函數,而非window等表達式瀏覽器

正確寫法:
setTimeout(obj.fn.bind(window, 10, 20));
複製代碼

call apply bind的實現

實現Function.prototype.bind(柯里化函數思想)

注:重寫bind須要在Function.prototype定義,由於是Function原型上的方法
柯里化思想:一個大函數裏面返回一個小函數,返回的小函數供外面調取使用,在執行大函數執行時造成的執行上下文不能銷燬,造成閉包,保護大函數裏面的變量,等到anonymous(下文提到)執行時,再調取大函數裏面的變量
基礎版bash

~ function anonymous(proto) {
    // context: bind更改以後的this指向
    function bind(context) {
        // context may be null or undefined
        if (context == undefined) {
            context = window;
        }
        
        <!--arguments { 0:context, 1:10, 2:20, length:3}-->
        <!--獲取傳遞的實參集合-->
        var args = [].slice.call(arguments, 1);
        
        須要最終執行的函數(例: obj.fn)
        var _this = this;
        
        <!--bind()執行會返回一個新函數-->
        return function anonymous() {
             _this.apply(context, args);
        }
        
        proto.bind = bind;
    }
}(Function.prototype);

let obj = {
    fn(x, y) {
        console.log(this, x, y);
    }
}
複製代碼

如今bind原理懂了以後,咱們來回顧一下這個題
回顧:在1秒鐘以後,執行fn函數,讓其函數裏的this變爲window
bind結合setTimeout實現
原理:
一、1s以後先執行bind的返回結果anonymous
二、在anonymous中再把須要執行的obj.fn執行,把以前存儲的context/args傳遞給函數閉包

setTimeout(obj.fn.bind(window, 10, 20));
setTimeout(anonymous, 1000);  
複製代碼

完整版
app

// document.body.onclick = obj.fn.bind(window, 10, 20);
document.body.onclick = anonymous;
複製代碼

:給當前元素的某個事件行爲綁定方法,當事件觸發執行完這個方法以後,方法中有一個默認事件對象ev(MouseEvent),ev做爲anonymous的形參對象anonymous(ev),由於最終執行的是obj.fn,因此爲了方便拿到ev
函數

~ function anonymous(proto) {
    // context: bind更改以後的this指向
    function bind(context) {
        // context may be null or undefined
        if (context == undefined) {
            context = window;
        }
        
        <!--arguments { 0:context, 1:10, 2:20, length:3}-->
        <!--獲取傳遞的實參集合-->
        var args = [].slice.call(arguments, 1);
        
        須要最終執行的函數(例: obj.fn)
        var _this = this;
        
        <!--bind()執行會返回一個新函數-->
        return function anonymous(ev) {
            args.push(ev);
             _this.apply(context, args);
        }
        
        proto.bind = bind;
    }
}(Function.prototype);

let obj = {
    fn(x, y,ev) {
        console.log(this, x, y,ev);
    }
};
複製代碼

因爲anonymous不必定綁給誰,因此不必定有ev,但也還有多是其餘東西,因此...性能

...
...
    return function anonymous() {
        var amArg = [].slice.call(arguments, 0);
        args = args.concat(amArg);
         _this.apply(context, args);
    }
    
    proto.bind = bind;
複製代碼

bind核心邏輯(es6寫法)

function bind (context = window, ...args) {
    return (...amArg) => {
        args = args.concat(amArg);
        _this.apply(context, args);
    }
}
複製代碼

經測試:apply在傳遞多個參數的狀況下,性能不如call,故改寫call

function bind (context = window, ...args) {
    return (...amArg) => {
        args = args.concat(amArg);
        _this.call(context, ...args);
    }
}
複製代碼

es6實現Function.prototype.call/apply

obj.fn.call(window, 10, 20)爲例

原理:context.$fn = this

步驟:

一、把當前函數(要更改的函數obj.fn),做爲context一個屬性,賦給this
二、context.&fn(),this天然指向context
三、防止對象屬性被竄改,及時delete context.$fn
四、call()執行以後應返回一個function,賦值給result
PS:(若是在面試的時候想寫詳細點能夠限定context數據類型爲引用類型,排除掉基本類型的可能)

~ function anonymous(proto) {
    // 只有當context不傳,或傳undefined時,纔是window
    function call(context = window, ...args) {
    // 因此應該null狀況考慮進去
        context === null ? context = window : null;
        let type = typeof context;
        if (type !== "object" && type !== "function" && type !== "symbol"){
            // => 基本類型值
            switch(type) {
                case 'number':
                    context = new Number(context);
                    break;
                case 'string':
                    context = new String(context);
                    break;
                case 'boolean':
                    context = new Boolean(context);
                    break;
            }
        };
        
        <!--必須保證context是引用類型-->
        <!--this是call以前要執行的函數(obj.fn)-->
        // 關鍵步驟
        context.$fn = this;
        let result = context.$fn(...args);
        delete context.$fn;
        return result;
    }
    proto.call = call; 
    
    function apply(context = window, args) {
        context.$fn = this;
        let result = context.$fn(...args);
        delete context.$fn;
        return result;
    }
    proto.apply = apply; 
}(Function.prototype);

let obj = {
    fn(x, y) {
        console.log(this, x, y);
    }
};

obj.fn.call(window,10,20);  // Window {parent: Window, opener: null, top: Window, length: 0, frames: Window, …} 10 20
obj.fn.call(1,10,20);   // Number {1, $fn: ƒ} 10 20
obj.fn.call(true,10,20);  // Boolean {true, $fn: ƒ} 10 20

obj.fn.apply(true,[10,20]); // Boolean {true}__proto__: Boolean[[PrimitiveValue]]: true (2) [10, 20] undefined
複製代碼

強化練習

call的無限調用

function call(context = window, ...args) {
    // 必須保證context是引用類型
    context.$fn = this;
    let result = context.$fn(...args);
    delete context.$fn;
    return result;
}

call 引用類型 堆地址AAAFFF000
複製代碼
function fn1() { console.log(1); }
function fn2() { console.log(2); }
fn1.call(fn2);             // 執行的是fn1 => 1
fn1.call.call(fn2);        // 最終讓fn2執行 => 2 (包括多個call)
Function.prototype.call(fn1);
Function.prototype.call.call(fn1);
複製代碼

fn1.call.call(fn2);

一、先讓最後一個call執行,
最後一個call中的this是fn1.call,context是fn2
    this => fn1.call => AAAFFF000
    context => fn2
    args => []
最後一個call開始執行
    fn2.$fn = AAAFFF000 
    result = fn2.$fn(...[]) (AAAFFF000) 執行,

接着讓call第二次執行
    this => fn2
    context => undefined
    args => []
    undefined.$fn = fn2
    result = undefined.$fn => (fn2())
    
最終讓fn2執行
複製代碼

寫在最後

  • 文中若有錯誤,歡迎在評論區指正,若是這篇文章幫到了你,歡迎點贊和關注
相關文章
相關標籤/搜索