模拟实现apply/call/bind

473 阅读3分钟

call()apply()的区别在于,call()方法接受的是若干个参数的列表,而apply()方法接受的是一个包含多个参数的数组

call

需满足:

1.改变this的指向。

2.新对象可以执行该函数。

3.考虑this为null和undefined时,this指向window。 this为基本类型时,原生的call会用Object自动转换。

4.能传入参数。

Function.prototype.call1 = function (context) {
	context = context ? Object(context) : window // 实现3
	// 模拟传入的对象中有一个调用该对象的函数
	// 作用是为了改变函数的作用域指向该对象
	context.fn = this
	
	//接收参数,若有。
	let args = [...arguments].slice(1) // 第0个为this
	let result = context.fn(...args) // 执行fn
	delete context.fn //删除fn
	
	return result
}

测试一下:

var value = 111;

var obj = {
    value: 999
}

function bar(name, age) {
    console.log(this.value);
    return {
        value: this.value,
        name: name,
        age: age
    }
}

function foo() {
    console.log(this);
}

bar.call1(null); // 111
foo.call1(520); // Number {520, fn: ƒ}

bar.call1(obj, 'jarva', 3);
// 999
// {
//   age: 3
//	 name: "jarva"
//	 value: 999
//  }

apply

与call实现基本一致

Function.prototype.apply1 = function (context) {
    context = context ? Object(context) : window
    context.fn = this
  
    let result;
    if (arguments[1]) {
		result = context.fn(...arguments[1])  
    } else {
        result = context.fn()
    }
      
    delete context.fn
    return result;
}

bind

bind() 方法会创建一个新函数,当这个新函数被调用时,它的 this 值是传递给 bind() 的第一个参数,传入bind方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。bind返回的绑定函数也能使用 new 操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

bind 方法与 call / apply 最大的不同就是前者返回一个绑定上下文的函数,而后两者是直接执行了函数。

需满足:

1.指定this。

2.返回函数。

3.传入参数。

4.对new类型时,需要忽略this。

现在先考虑前三个条件,实现如下:

Function.prototype.bind1 = function(context) {
	let _this = this // 记住当前作用域,指向调用者。

	let args = Array.prototype.slice.call(arguments, 1) // 去掉第一个this参数
	// let args = [].slice.call(arguments, 1)
	// let args = [...arguments].slice(1)
	
	return function () {
        // 因为 bind 可以实现类似这样的代码 fn.bind(obj, 1)(2) 
	    // 所以要合并返回参数
		// let bindArgs = Array.prototype.slice.call(arguments);
		let bindArgs = [...arguments]
		return _this.apply(context, args.concat(bindArgs)) // 指定this。
	}
}

测试用例

var value = 111

var foo = {
    value: 999
};

function bar(name, age) {
    return {
		value: this.value,
		name: name,
		age: age
    }
}

var bindFoo = bar.bind1(foo, "Jack")
bindFoo(20);
//{ age: 20, name: "Jack", value: 999 }

一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

现在来实现完整的bind模拟。

Function.prototype.bind2 = function(context) {

	// 如果调用的不是函数,要抛出异常
	if (typeof this !== 'function') {
	    throw new Error("Function.prototype.bind - what is trying to be bound is not callable")
	}
	let _this = this // 记住当前作用域,指向调用者。

	let args = Array.prototype.slice.call(arguments, 1) // 去掉第一个this参数
	// let args = [].slice.call(arguments, 1)
	// let args = [...arguments].slice(1)
	var fn = function () {
        // 因为 bind 可以实现类似这样的代码 fn.bind(obj, 1)(2) 
	    // 所以要合并返回参数
		// let bindArgs = Array.prototype.slice.call(arguments)
		let bindArgs = [...arguments]
		// 当作为构造函数时,this 指向实例,此时 this instanceof fBound 结果为 true,可以让实例获得来自绑定函数的值
		return _this.apply(this instanceof fn ? this : context, args.concat(bindArgs)) // 指定this。
	}
	// 还要考虑修改返回函数的prototype为绑定函数的prototype,
	// 使得实例可以继承原型的值。 
	// 为了修改fn.prototype时不影响原型的值,使用ES5的 Object.create()方法生成一个新对象
	fn.prototype = Object.create(this.prototype)
	return fn
}	

测试一下

// 测试用例
var value = 2;
var foo = {
    value: 1
};
function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
bar.prototype.friend = 'kevin';

var bindFoo = bar.bind2(foo, 'Jack'); // bind2
var obj = new bindFoo(20); // 返回正确
// undefined
// Jack
// 20

obj.habit; // 返回正确
// shopping

obj.friend; // 返回正确
// kevin

obj.__proto__.friend = "Kitty"; // 修改原型

bar.prototype.friend; // kevin

收工~