前言
当函数调用时,会创建执行上下文,this时上下文的一个属性,在函数执行时会用到。this有四种绑定方式:默认绑定、隐式绑定、显示绑定、new绑定。显示绑定时,会用到call()、apply()、bind()函数,那这些函数是怎么实现的呢?让我们手写一下把。
this的解析参考:this全面解析
一、手写call()函数
call()函数的特点:改变this的指向,使用时传递的第一个参数是需要绑定的this,后面可以再传递多个参数。
let obj = {name: 'obj'}
let name = 'window';
function foo(...args) {
console.log(this.name, ...args);
}
foo.call(obj, 1, 2, 3) // obj 1 2 3
根据以上逻辑,可以写出自己的call()函数:
// call 可以传递多个参数,第一个参数是this,立即执行
Function.prototype.myCall = function (context,...args) {
context = context || window;
// this可理解为调用call方法的函数
// fn是新添加的属性,属性值为this(次数的this实际上指的是foo函数,谁调用指向谁)
context.fn = this; // 隐式绑定
// 调用方法
context.fn(...args);
// 删除属性
delete context.fn;
}
let name = 'window'
let obj = {
name: 'obj'
}
function foo(...args) {
console.log(this.name, ...args)
}
// foo.myCall(obj, '1', '2', '3')
上面用到的隐式绑定,可以参考下面的代码:
let name = 'window';
let obj = {
name: 'obj',
foo: function(...args) {
console.log(this.name, ...args);
}
}
obj.foo() // obj
二、手写apply()函数
call()函数的特点:改变this的指向,使用时传递的第一个参数是需要绑定的this,后面可以再传递多个参数, 但是这多个参数需要以数组的方式传递。
let obj = {name: 'obj'}
let name = 'window';
function foo(args) {
console.log(this.name, ...args);
}
foo.apply(obj, [1, 2, 3]) // obj 1 2 3
根据以上逻辑,可以发现跟call()的实现方式是一样的,只是要修改下参数的格式:
Function.prototype.myApply = function (context,args) {
context = context || window;
// this可理解为调用call方法的函数
// fn是新添加的属性,属性值为this
context.fn = this;
// 调用方法
args = args || [];
context.fn(...args);
// 删除属性
delete context.fn;
}
// foo.myApply(obj, [1, 2, 3])
三、手写bind()函数
bind()函数的特点:
1、改变this的指向,使用时传递的第一个参数是需要绑定的this,后面可以再传递多个参数。
2、会返回一个函数,在需要的时候调用。
let obj = {name: 'obj'}
let name = 'window';
function foo(args) {
console.log(this.name, ...args);
}
let bind = foo.bind(obj, [1, 2, 3])
bind() // obj 1 2 3
1、简单版
bind()函数相比于appy()和call()函数,不同的地方在于不会立即执行,可以在适当的时候再调用,因此,手写的bind()应该返回一个函数:
// bind 返回函数,再传入参数
Function.prototype.myBind = function (context,...args1) {
context = context || window;
let self = this
let bindFn = function (...args2) {
let args = [...args1, ...args2];
context.fn = self;
// 调用方法
context.fn(...args);
// 删除属性
delete context.fn;
}
return bindFn;
}
function person(a,b,c) {
console.log(this.name,a, b, c);
}
let obj = {name: 'lisa'}
let bind = person.myBind(obj);
bind(1,3,3); // lisa 1 3 3
2、中级版(不使用ES6)
…扩展运算符是ES6的语言,我们用ES5经典的Array.prototype.slice.call()方法也可以实现:
// bind 返回函数,再传入参数
Function.prototype.myBind = function (context) {
context = context || window;
let self = this
if (typeof self !== "function") { // 加入了对调用函数类型的判断
throw new Error("cannot bind non_function");
}
let args1 = Array.prototype.slice.call(arguments,1);// 将类数组转为数组
let bindFn = function () {
let args2 = Array.prototype.slice.call(arguments);
let args = [...args1, ...args2];
context.fn = self;
// 调用方法
context.fn(...args);
// 删除属性
delete context.fn;
}
return bindFn;
}
function person(a,b,c) {
console.log(this.name,a, b, c);
}
let obj = {name: 'lisa'}
let bind = person.myBind(obj);
bind(1,3,3); // lisa 1 3 3
3、高级版(考虑new)
Function.prototype.myBind = function (obj) {
let that = this;// Person
let arr = Array.prototype.slice.call(arguments, 1);
let newF = function () {
var arr2 = Array.prototype.slice.call(arguments);
let arrSum = arr.concat(arr2);
if (this instanceof newF) {
// this 是newF的实例
that.apply(this, arrSum);
} else {
that.apply(obj, arrSum)
}
}
// 返回的函数与Person链接起来,使实例对象能用到Person函数的属性和方法
newF.prototype = that.prototype;
// 下面三行是上行代码的优化:原型链共享的特性导致上行代码容易修改Person的原型,所以,需要使用new操作转接一下
// let o = function () {}
// o.prototype = that.prototype;
// newF.prototype = new o;
return newF;
}
function person(a,b,c) {
console.log(this.name,a, b, c);
}
person.prototype.name = 'shelly'
let obj = {name: 'lisa'}
let bind = person.myBind(obj);
bind(1,3,3); // lisa 1 3 3
let baz = new bind(1,3,3) // shelly 1 3 3
总结
call()函数和apply函数含义一样,第一个参数都是传递this,不同的地方在于后面的参数形式,前者一个一个传,后者以数组的方式传递。它们调用之后会立即执行。
bind()函数相比于call()、apply(),多返回了一个方法,可以在合适的地方调用,不会立即执行。