优雅手撕bind函数

255 阅读3分钟


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

打完收工