Header:
本篇不讨论具体用法,着重研究实现原理,全篇采用es6代码
call、apply、bind三个方法存在于Function.prototype上方法,所以所有函数都能调用该三个方法。三个方法的主要作用都在于改变函数执行上下文this,以达到不同的目的。
call、apply
因为俩者只是在参数传递上不一样,所以拿在一起研究,且也不模拟apply实现
原生call,apply使用:
const obj1 = {
value: 9
};
const showValue = function() {
console.log(this.value);
};
// 执行call/apply函数改写context
showValue.call(obj1); // 输出 9
showValue(); // undefined
分析执行过程:
-
函数调用call/apply方法
说明call,apply方法存在于Function原型上,也就是Function.prototype上,那我们模拟时也在Function.prototype上添加call2方法
Function.prototype.call2 = function() {}; -
传入想要改写成的上下文对象
说明call/apply的第一个参数接受为一个对象
Function.prototype.call2 = function(context) {}; -
语句执行,输出结果1
this.value的值为1,说明this指向obj1,也就是说这个时候showValue函数是obj1的属性,则在call2函数内部,就应该将this(this代表showValue函数)添加到obj1上,然后直接执行。
Function.prototype.call2 = function(context) { context.fn = this; context.fn(); }; -
再次执行 showValue() 函数,输出结果undefined
在第三步我们已经将obj1上面添加了属性showValue函数,而在再次执行时,this.value结果却不是预期的1,则可知在call内部添加obj1属性showValue并执行后,就将该属性从obj1中删除。
Function.prototype.call2 = function(context) { context.fn = this; context.fn(); delete context.fn; };
完整代码(注意:此处没有添加各种情况判断代码,如context=null||undefined||基本类型等):
const obj1 = {
value: 9
};
const showValue = function() {
console.log(this.value);
};
// 执行call/apply函数改写context
showValue.call(obj1); // 输出 9
showValue(); // undefined
// 模拟代码
// 完善参数传递等其他部分
Function.prototype.call2 = function(context, ...args) {
context.fn = this;
const res = context.fn(...args);
delete context.fn;
return res;
};
// 执行call2函数改写context
showValue.call2(obj1); // 输出 9
showValue(); // undefined
bind
原生bind使用:
1. 简单使用
const obj = { title: 'obj-title' };
const showTitle = function() {
console.log(this.title);
};
const showObjTitle = showTitle.bind(obj);
showObjTitle(); // obj-title;
1.1 分析执行过程
-
函数调用bind函数,传入context对象。
发现bind()
返回一个函数。Function.prototype.bind2 = function(context) { return function() {}; }; -
调用showObjTitle()函数,函数输出
obj-title函数输出了第一步绑定的obj中的title,那么说明在返回的函数内部执行了call/apply类似的功能代码,那么次数就意味着可以直接用call/apply代替函数内的功能代码。
Function.prototype.bind2 = function(context) { const fn = this; return function() { return fn.apply(context); }; };
2. 作为柯里化函数使用
const obj = { content: 'obj-content' };
const showTitle = function(title, content) {
let con = content;
if (this.content) con += this.content;
console.log(title, ':::', con);
};
const showObjTitle = showTitle.bind(obj, 'bind-title');
showObjTitle('-bind-content'); // bind-title ::: -bind-contentobj-content
2.1 分析执行过程
-
调用bind函数时传入第二个参数'bind-title'
bind函数可以接受第二个参数
Function.prototype.bind2 = function(context, arg1) { const fn = this; return function() { return fn.apply(context); }; }; -
调showObjTitle时传入了一个参数'-bind-content'
bind函数返回的函数可以接受参数
Function.prototype.bind2 = function(context, arg1) { const fn = this; return function(arg2) { return fn.apply(context); }; }; -
调用showObjTitle()函数,函数输出
obj-title:::obj-content-bind-content;最后可以看出前面分别在bind时和调用showObjTitle时传入的参数都被fn按顺序接收到了,所以由此看出,在返回的函数内部要将前面俩次传入的参数相加并传入到fn内部。
Function.prototype.bind2 = function(context, arg1) { const fn = this; const argTemp = arg1 === undefined ? [] : [arg1]; return function(arg2) { return fn.apply(context, arg2 !== undefined ? [ ...argTemp, arg2 ] : argTemp); }; };
3. 使用new关键字使用(函数的构造调用)
const obj = { title: 'obj-title', content: 'obj-content', time: '1999-9-9' };
const News = function(title, content) {
this.title = title;
this.content = content;
this.time = this.time || '2019-9-9';
};
News.prototype.getContent = function() {
console.log(this.content);
};
const DogNews = News.bind(obj, 'dog');
const dn = new DogNews('cute');
console.log(dn.title); // dog
console.log(dn.time); // 2019-9-9
dn.getContent(); // cute
3.1 分析执行过程
-
使用new关键字调用了DogNews方法,语句返回了一个实例,并在下面的语句中输出了传入的参数。
可以看出因为
执行new关键字,导致前面bind的context失效,取而代之的this则是agentConstructor的一个实例,那么这里就需要做判断处理,从而兼容不使用new的情况。Function.prototype.bind2 = function(context, arg1) { const fn = this; const argTemp = arg1 === undefined ? [] : [arg1]; const agentConstructor = function(arg2) { const argsOfAll = arg2 !== undefined ? [ ...argTemp, arg2 ] : argTemp; if (this instanceof agentConstructor) { fn.apply(this, argsOfAll); } return fn.apply(context, argsOfAll); }; }; return agentConstructor; -
dn.getContent()语句输出了cute。
getContent是
News原型上的方法,而通过new DogNews('cute')语句返回的DogNews实例也能够访问到是News原型上的方法,那么则说明在bind时返回的函数上的原型跟News原型发生了关系。但是如果直接使agentConstructor.prototype = News.prototype的话,在agentConstructor原型发生改变时,也将会影响News的原型,那么此处就要再使用一个不对外公开的空函数进行承接。Function.prototype.bind2 = function(context, arg1) { const fn = this; const argTemp = arg1 === undefined ? [] : [arg1]; const TempFn = function() {}; const agentConstructor = function(arg2) { const argsOfAll = arg2 !== undefined ? [ ...argTemp, arg2 ] : argTemp; if (this instanceof agentConstructor) { fn.apply(this, argsOfAll); } return fn.apply(context, argsOfAll); }; }; TempFn.prototype = fn.prototype; agentConstructor.prototype = new TempFn(); return agentConstructor; }
完整代码:
Function.prototype.bind2 = function(context, arg1) {
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
const fn = this;
const argTemp = arg1 === undefined ? [] : [arg1];
const TempFn = function() {};
const agentConstructor = function(arg2) {
const argsOfAll = arg2 !== undefined ? [ ...argTemp, arg2 ] : argTemp;
if (this instanceof agentConstructor) {
fn.apply(this, argsOfAll);
}
return fn.apply(context, argsOfAll);
};
};
TempFn.prototype = fn.prototype;
agentConstructor.prototype = new TempFn();
return agentConstructor;
}
结束语
至此,关于call/apply/bind的简单原理实现就完成了.该文章主要用于学习梳理。