優雅手撕bind函式(面試官常問)

bigname22發表於2020-11-02


優雅手撕bind函式


前言:
  • 為什麼面試官總愛讓實現一個bind函式?
  • 他想從bind中知道些什麼?
  • 一個小小的bind裡面內有玄機?
    今天來刨析一下實現一個bind要懂多少相關知識點,也方便我們將零碎的知識點串聯起來。

? 看完有用的同學記得點個贊再走,您的鼓勵-我莫大的動力

看完能學到什麼

  • 實現bind
  • new原理

本文章的敘事步驟

  • bind函式作用
  • 模擬bind的要點
  • 實現思路
  • new函式特殊情況(this&父原型)

-------------人工分割線-------------

bind函式的作用

返回一個能夠改變this指向的函式。

模擬bind的要點

  • 改變this指向
  • 返回函式

實現思路

建立一個待返回的函式,函式內部利用call/apply改變指向,call/apply的引數從arguments中獲取。

實現程式碼如下:

  Function.prototype.myBind = function () {
        let exeFunc = this;
        let beThis = arguments[0];
        let args = [].slice.call(arguments ,1);
        return function () {
            exeFunc.apply(beThis,args);
        }
    }

來份資料測試一下:

	let other = {
        name: 'other'
    }
	let obj = {
        name: 'obj',
        getName : function (age,height) {
            console.log(this.name);
            console.log('年齡' + age);
            console.log('身高' + height);
        }
    }
    obj.getName.myBind(other, 14, 200)();

測試結果正常。列印的是other

還挺簡單的是吧!但考點通常不止如此。接著看:

function Person() {
        this.name = 'person';
        this.getName = function (age, height) {
            console.log(this.name);
            console.log('age:' + age, 'height:' + height);
        }
    }

這個時候:

let PersonMyBind = Person.myBind(window);
let per3 = new PersonMyBind();
per3.getName();

思考一下會列印person嗎?

答案:實際上per3是一個空物件。

new函式特殊情況-this

那麼為什麼會出現這樣的錯誤。這就牽扯到關於new的知識:
如果不太明白的可便宜看下這篇文章
這是一段關於new的模擬程式碼

function New (constructFunc) {
	// 生命中間物件,最後作為返回的例項,相當於let obj = New(Obj); => obj = res
	var res = {};
	if(constructFunc.prototype !== null) {
		// 將例項的原型指向建構函式的原型
		res.__proto__ = constructFunc.prototype;
	}
	// 重點重點 ret為該建構函式執行的結果,將建構函式的this改為執行res
	var ret = constructFunc.apply(res, Array.prototype.slice.call(arguments, 1));
	// 如果建構函式有返回值,則直接返回
	if((typeof rest === "object" || typeof ret === "function") && ret !== null) {
		return ret;
	}
	// 否則返回該例項
	return res;
} 

其中,下面一行程式碼就是導致我們寫的bind不能如願以償將name、getName屬性建立到物件的致命原因,且聽我細細道來:

var ret = constructFunc.apply(res, Array.prototype.slice.call(arguments, 1));

當我們執行Person.myBind()的時候,我的得到的返回結果是一個函式:function () {exeFunc.apply(beThis,args);},來個圖明顯一點。
在這裡插入圖片描述
那麼當這一行程式碼執行時:

var ret = constructFunc.apply(res, Array.prototype.slice.call(arguments, 1));

來張圖來看清new Person與new PersonMyBind()的區別:
在這裡插入圖片描述
在知道產生這種現象的原因之後我們該如何解決?其實非常簡單,如果是new的情況:

	let resultFunc = function () {
            exeFn.apply(this, args) // 這裡傳入的是this物件,對應著new過程中的res
        }

所以這個時候問題就是該如何區分new Person()和Person()!答案還是在new的實現原理中找答案,我們可以找到上面new的模擬程式碼中的這一行:

	// 將例項的原型指向建構函式的原型
	res.__proto__ = constructFunc.prototype;

也就是說在執行

	let resultFunc = function () {
			// 此時的this__proto__等於Person.prototype
            exeFn.apply(this, args)
        }

此時的this.__proto__等於Person.prototype,利用這一特性就ok了。
升級我們的myBind

 Function.prototype.myBind = function () {
        if(typeof this !== 'function') {
            throw new Error('呼叫者必須為function型別');
        }
        let exeFn = this; // this 為待執行函式
        let currentThis = arguments[0]; // 待指定的this
        let args = [].slice.call(arguments,1); // 剩餘的都作為引數傳遞
        let resultFunc = function () {
           // 區分new呼叫與普通呼叫
            exeFn.apply(this.__proto__=== resultFunc.prototype ? this : currentThis, args)
        }
        return resultFunc;
    }

new函式特殊情況-父原型

到這裡還沒結束,我們還要解決Person加入有父原型的情況,在知道上面的知識點後解決這個也非常easy
再升級一版:

    Function.prototype.myBind = function () {
        if(typeof this !== 'function') {
            throw new Error('呼叫者必須為function型別');
        }
        let exeFn = this; // this 為待執行函式
        let currentThis = arguments[0]; // 待指定的this
        let args = [].slice.call(arguments,1); // 剩餘的都作為引數傳遞
        let resultFunc = function () {
            // 區分new呼叫跟普通呼叫
            exeFn.apply(this.__proto__=== resultFunc.prototype ? this : currentThis, args)
        }
        // 維持原來函式的父原型
        if (this.prototype) {
            resultFunc.prototype = this.prototype;
        }
        return resultFunc;
    }

打完收工

相關文章