JavaScript 模拟 call 的实现


作者:Seiya

时间:2019年08月08日


call 方法


call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

它提供新的 this 值给当前调用的函数/方法。我们可以使用 call 来实现继承:写一个方法,然后让另外一个新的对象来继承它(而不是在新对象中再写一次这个方法)。


注意:

该方法的语法和作用与 apply() 方法类似,只有一个区别,就是 call() 方法接受的是一个参数列表,而 apply() 方法接受的是一个包含多个参数的数组。



模拟实现

若要模拟 call 方法的实现,需要注意两点:

  1. 修改 this 的指向;

  2. 执行调用的函数;


同时,我们需要解决实现 call 方法过程中的三个问题:

  • 问题一:call 方法的整体思路;

  • 问题二:解决参数传递的问题;

  • 问题三:解决 this 参数为 null 的问题,以及函数返回值的问题;


我们拿下面的例子来模拟 call 方法的实现:

var foo = {	value: 1 }
function bar() { console.log(this.value) }

bar.callFn(foo)		// 1


问题一:call 方法的整体思路

  • 第一步:将函数设为对象的属性;

  • 第二步:执行该函数;

  • 第三步:删除该函数;


根据上面的思路,我们可以得到初步代码:

Function.prototype.callFn = function(context) {
	context.fn = this
	context.fn()
	delete context.fn
}


问题二:解决参数传递的问题

我们可以从 Arguments 对象中取值,取出第二个到最后一个参数,然后放到一个数组里。具体实现如下:

Function.prototype.callFn = function(context) {
	context.fn = this

	var args = []
	for(var i = 1, len = arguments.length; i < len; i++) {
		args.push('argument[' + i + ']')
	}

	/* 使用 eval 函数拼一个函数,解决参数传递的问题 */
	eval('context.fn(' + args + ')')
	delete context.fn
}

也可通过使用 ES6 中提供的新特性实现:

Function.prototype.callFn = function(context) {
	context.fn = this

	var args = [...arguments].slice(1)

	context.fn(...arg)
	delete context.fn
}

当然,我们也可以这样实现:

Function.prototype.callFn = function(context, ...args) {
	context.fn = this

	args = args ? args : []

	context.fn(...args)
	delete context.fn
}


问题三:解决 this 参数为 null 的问题,以及函数返回值的问题

Function.prototype.callFn = function(context) {
	/* 判断 context 是否为 null */
	var context = context || window
	context.fn = this

	var args = []
	for(var i = 1, len = arguments.length; i < len; i++) {
		args.push('argument[' + i + ']')
	}

	/* 接受函数执行的返回值 */
	var result = eval('context.fn(' + args + ')')
	delete context.fn

	return result
}


最终版


Function.prototype.callFn = function(context, ...args) {
	var context = context || window
	args = args ? args : []

	/* 给 context 新增一个独一无二的属性以免覆盖原有属性 */
	const fn = Symbol()
	context.fn = this

	/* 通过隐式绑定的方式调用函数 */
	var result = context.fn(...args)
	delete context.fn

	return result
}
最后更新时间: 2019-8-9 13:40:40