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

優雅手撕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;
    }

打完收工