阅读 1269

实现call、apply、bind

callapplybind本质上都是要改变 this 的指向,在实现过程中一定要时刻搞清楚 this 的指向

首先考虑一下这个场景

function show() {
	console.log(this.name);
}

const person = {
	name: 'white_give'
};
复制代码

如果不用callapplybind,如何让 show 方法里的 this 指向 person 对象呢?

可以像下面这样做

const person = {
	name: 'white_give',
    show: function () {
    	console.log(this.name);
    }
};
复制代码

熟悉 js 的同学可能都知道上面的做法是由 this 的默认绑定改为隐式绑定

其实我们手写实现 call applybind 时也是这个思路

call 的实现

我们还是借用上面定义的 show 方法和 person 对象,不过我们要往 show 方法里面添加一些参数,来验证实现的 call 方法是否有效

function show(...args) {
	console.log(this.name);
    console.log(...args);
}

const person = {
	name: 'white_give'
};

Function.prototype.myCall = function (ctx, ...args) {
	/**
     * this 指向 myCall 函数的调用者
     * ctx 为 this 要指向的对象
     */
	ctx.fn = this; // 相当于给 person 对象增加了一个 fn 属性,属性值是 show 函数
    const result = ctx.fn(...args);
    delete ctx.fn;
    return result;
}
复制代码

所以以上代码的思路就是:

  1. mycall 函数的调用者( show 方法)挂载到传入的 ctxthis 要指向的对象,不是函数内部的 this
  2. 执行挂载的方法
  3. 删除挂载方法的属性

这时我们已经初步实现了 call 方法,但是还有一种情况我们没有涉及到。

思考一下以下代码会输出什么?

show.call(null, 1, 2, 3);
show.myCall(null, 1, 2, 3);

// 下面是以上两条语句的输出结果
// 
// 1 2 3
// Uncaught TypeError: Cannot set property 'fn' of null
复制代码

通过上面的代码发现,我们没有对传入对象为 null 时进行处理,所以最终我们的代码是:

Function.prototype.myCall = function (ctx, ...args) {
	ctx = ctx || window;
	ctx.fn = this;
    const result = ctx.fn(...args);
    delete ctx.fn;
    return result;
}
复制代码

到此为止我们就手动实现了 call 方法,是不是很开心?别着急,我们再来看一下 apply 方法

apply 的实现

applycall 的不同就是,apply 的剩余参数接收的是一个数组

这里我们借用 call 的实现中的 show 函数和 person 对象以及实现 call 方法时的思路

Function.prototype.myApply = function (ctx, args = []) {
	ctx = ctx || window;
    ctx.fn = this;
    const result = ctx.fn(...args);
    delete ctx.fn;
    return result;
}
复制代码

看到这里是不是感觉已经实现了 apply 方法,先别着急,思考一下下面的代码会输出什么?

show.apply(person, 'abc', 'def');
show.myApply(person, 'abc', 'def');

// 下面是以上两条语句的输出结果
// Uncaught TypeError: CreateListFromArrayLike called on non-object
// white_give
// a b c
复制代码

通过上面的代码,我们发现没有对参数类型进行限制。所以最终我们的代码是:

Function.prototype.myApply = function (ctx, args = []) {
	if (args && !Array.isArray(args)) {
        throw ('Uncaught TypeError: CreateListFromArrayLike called on non-object');
    }
	ctx = ctx || window;
    ctx.fn = this;
    const result = ctx.fn(...args);
    delete ctx.fn;
    return result;
}
复制代码

bind 的实现

可能小伙伴们最搞不懂的还是 bind 方法,那我争取把 bind 方法讲明白

首先 bind 方法和 call 方法接收参数的方法相同,且返回的是一个函数

这里我们还是借用 call 的实现中的 show 函数和 person 对象,所以我们 bind 方法的雏形为:

Function.prototype.myBind = function (ctx, ...args) {
	return () => {
    	this.apply(ctx, ...args);
    }
}
复制代码

这样我们可以实现一个简单的 bind 函数,但是原生的 bind 函数具有科里化特征,即下面的代码输出相同的结果:

show.bind(person, 5, 6)(2, 1);  // white_give 5 6 2 1
show.bind(person, 5, 6, 2, 1)(); // white_give 5 6 2 1
show.bind(person)(5, 6, 2, 1); // white_give 5 6 2 1
复制代码

所以我们的代码需要改为:

Function.prototype.myBind = function (ctx, ...args1) {
	return (...args2) => {
    	this.apply(ctx, args1.concat(args2));
    }
}
复制代码

这样就满足了 bind 科里化特征的要求。但是还有最烦人的一点是 bind 返回的函数还可以通过 new 关键字去实例化对象。

所以这就涉及到去判断我们返回的函数是不是有用 new 关键字去实例化对象,如果有的话,返回的函数的 this 的指向就为实例化后的对象,如果没有的话就用我们原来写的逻辑

这里我们给上面的 show 方法的原型对象上增加一个属性,并用 new 关键字来实例化一个对象

show.prototype.info = '这是show方法';

Fcuntion.prototype.myBind = function (ctx, ...args) {
	// do something...
}

const NewBind = show.myBind(person, 5, 6);
const newBind = new NewBind(2, 1);
console.log(newBind.info);
复制代码

接下来我们使用 instanceof 来判断创建的对象是不是用 new 关键字来实例化的对象

Function.prototype.myBind = function (ctx, ...args1) {
	// 用 that 来保存 myBind 的调用者
	const that = this;
    const newFn = function (...args2) {
    	const args = args1.concat(args2);
        // 如果使用 new 关键字,这里的 this 指向的是 newFn 对象
        if (this instanceof newFn) {
        	// 将 myBind 调用者的 this 改为 newFn 对象
        	that.apply(this, args);
        } else {
        	that.apply(ctx, args);
        }
    }
    return newFn;
}
复制代码

到这里我们发现 console.log(newBind.info); 仍然访问不到 info 属性,这是因为还没有将 newFnmyBind 的调用者(show 函数)建立联系,所以我们的代码修改为:

Function.prototype.myBind = function (ctx, ...args1) {
	// 用 that 来保存 myBind 的调用者
	const that = this;
    const newFn = function (...args2) {
    	const args = args1.concat(args2);
        // 如果使用 new 关键字,这里的 this 指向的是 newFn 对象
        if (this instanceof newFn) {
        	// 将 myBind 调用者的 this 改为 newFn 对象
        	that.apply(this, args);
        } else {
        	that.apply(ctx, args);
        }
    }
    newFn.prototype = that.prototype;
    return newFn;
}
复制代码

这样我们就实现了 bind 的功能,但是代码中还有一点不合规范的是我们在代码中直接修改了原型对象,我们可以用原型式继承来进行修改我们的代码,所以最终 bind 的代码为:

Function.prototype.myBind = function (ctx, ...args1) {
  const that = this;
  const o = function () {};
  const newFn = function (...args2) {
    const args = args1.concat(args2);
    if (this instanceof o) {
      that.apply(this, args);
    } else {
      that.apply(ctx, args);
    }
  }
  o.prototype = that.prototype;
  newFc.prototype = new o;
  return newFn;
}
复制代码

其实,理解了 bind 的原理后,就能明白 this 的绑定顺序为什么是 new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定

所以,你现在明白 this 的绑定顺序了吗?

文章分类
前端
文章标签