JavaScript进阶之模拟call,apply和bind

35次阅读

共计 7326 个字符,预计需要花费 19 分钟才能阅读完成。

原文:https://zhehuaxuan.github.io/… 作者:zhehuaxuan
目的
本文主要用于理解和掌握 call,apply 和 bind 的使用和原理,本文适用于对它们的用法不是很熟悉,或者想搞清楚它们原理的童鞋。好,那我们开始!在 JavaScript 中有三种方式来改变 this 的作用域 call,apply 和 bind。我们先来看看它们是怎么用的,只有知道怎么用的,我们才能来模拟它。
Function.prototype.call()
首先是 Function.prototype.call(),不熟的童鞋请猛戳 MDN,它是这么说的:call()允许为不同的对象分配和调用属于一个对象的函数 / 方法。也就是说:一个函数,只要调用 call()方法,就可以把它分配给不同的对象。
如果还是不明白,不急!跟我往下看,我们先来写一个 call()函数最简单的用法:
function source(){
console.log(this.name); // 打印 xuan
}
let destination = {
name:”xuan”
};
console.log(source.call(destination));
上述代码会打印出 destination 的 name 属性,也就是说 source()函数通过调用 call(),source()函数中的 this 对象可以分配到 destination 对象中。类似于实现 destination.source()的效果,当然前提是 destination 要有一个 source 属性
好,现在大家应该明白 call()的基本用法,我们再来看下面的例子:
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
}
let destination = {
name:”xuan”
};
console.log(source.call(destination,18,”male”));
打印效果如下:

我们可以看到可以 call()也可以传参,而且是以参数, 参数,… 的形式传入。
上述我们知道 call()的两个作用:
1. 改变 this 的指向 2. 支持对函数传参

我们看到最后还还输出一个 undefined,说明现在调用 source.call(…args)没有返回值。
我们给 source 函数添加一个返回值试一下:
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
// 添加一个返回值对象
return {
age:age,
gender:gender,
name:this.name
}
}
let destination = {
name:”xuan”
};
console.log(source.call(destination,18,”male”));
打印结果:

果不其然!call()函数的返回值就是 source 函数的返回值,那么 call()函数的作用已经很明显了。
这边再总结一下:

改变 this 的指向
支持对函数传参
函数返回什么,call 就返回什么。

模拟 Function.prototype.call()
根据 call()函数的作用,我们下面一步一步的进行模拟。我们先把上面的部分代码摘抄下来:
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
// 添加一个返回值对象
return {
age:age,
gender:gender,
name:this.name
}
}
let destination = {
name:”xuan”
};
上面的这部分代码我们先不变。现在只要实现一个函数 call1()并使用下面方式
console.log(source.call1(destination));
如果得出的结果和 call()函数一样,那就没问题了。
现在我们来模拟第一步:改变 this 的指向。
假设我们 destination 的结构是这样的:
let destination = {
name:”xuan”,
source:function(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
// 添加一个返回值对象
return {
age:age,
gender:gender,
name:this.name
}
}
}
我们执行 destination.source(18,”male”); 就可以在 source()函数中把正确的结果打印出来并且返回我们想要的值。
现在我们的目的更明确了:给 destination 对象添加一个 source 属性,然后添加参数执行它。
所以我们定义如下:
Function.prototype.call1 = function(ctx){
ctx.fn = this; //ctx 为 destination this 指向 source 那么就是 destination.fn = source;
ctx.fn(); // 执行函数
delete ctx.fn; // 在删除这个属性
}
console.log(source.call1(destination,18,”male”));
打印效果如下:

我们发现 this 的指向已经改变了,但是我们传入的参数还没有处理。
第二步:支持对函数传参。我们使用 ES6 语法修改如下:
Function.prototype.call1 =function(ctx,…args){
ctx.fn = this;
ctx.fn(…args);
delete ctx.fn;
}
console.log(source.call1(destination,18,”male”));
打印效果如下:
参数出现了,现在就剩下返回值了,很简单,我们再修改一下:
Function.prototype.call1 =function(ctx,…args){
ctx.fn = this || window; // 防止 ctx 为 null 的情况
let res = ctx.fn(…args);
delete ctx.fn;
return res;
}
console.log(source.call1(destination,18,”male”));
打印效果如下:

现在我们实现了 call 的效果!
模拟 Function.prototype.apply()
apply()函数的作用和 call()函数一样,只是传参的方式不一样。apply 的用法可以查看 MDN,MDN 这么说的:apply() 方法调用一个具有给定 this 值的函数,以及作为一个数组(或类似数组对象)提供的参数。
apply()函数的第二个参数是一个数组,数组是调用 apply()的函数的参数。
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
return {
age:age,
gender:gender,
name:this.name
}
}
let destination = {
name:”xuan”
};
console.log(source.apply(destination,[18,”male”]));

效果和 call()是一样的。既然只是传参不一样,我们把模拟 call()函数的代码稍微改改:
Function.prototype.apply1 =function(ctx,args=[]){
ctx.fn = this || window;
let res = ctx.fn(…args);
delete ctx.fn;
return res;
}
console.log(source.apply1(destination,[18,’male’]));
执行效果如下:

apply()函数的模拟完成。
Function.prototype.bind()
对于 bind()函数的作用,我们引用 MDN,bind()方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this 对象,之后的一序列参数将会在传递的实参前传入作为它的参数。我们看一下代码:
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
return {
age:age,
gender:gender,
name:this.name
}
}
let destination = {
name:”xuan”
};
var res = source.bind(destination,18,”male”);
console.log(res());
console.log(“==========================”)
var res1 = source.bind(destination,18);
console.log(res1(“male”));
console.log(“==========================”)
var res2 = source.bind(destination);
console.log(res2(18,”male”));
打印效果如下:

我们发现 bind 函数跟 apply 和 call 有两个区别:
1.bind 返回的是函数,虽然也有 call 和 apply 的作用,但是需要在调用 bind()时生效 2.bind 中也可以添加参数

明白了区别,下面我们来模拟 bind 函数。
模拟 Function.prototype.bind()
和模拟 call 一样,现摘抄下面的代码:
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
// 添加一个返回值对象
return {
age:age,
gender:gender,
name:this.name
}
}
let destination = {
name:”xuan”
};
然后我们定义一个函数 bind1,如果执行下面的代码能够返回和 bind 函数一样的值,就达到我们的目的。
var res = source.bind1(destination,18);
console.log(res(“male”));
首先我们定义一个 bind1 函数,因为返回值是一个函数,所以我们可以这么写:
Function.prototype.bind1 = function(ctx,…args){
var that = this;// 外层的 this 指向通过变量传进去
return function(){
// 将外层函数的参数和内层函数的参数合并
var all_args = […args].concat([…arguments]);
// 因为 ctx 是外层的 this 指针,在外层我们使用一个变量 that 引用进来
return that.apply(ctx,all_args);
}
}
打印效果如下:

这里我们利用闭包,把外层函数的 ctx 和参数 args 传到内层函数,再将内外传递的参数合并,然后使用 apply()或 call()函数,将其返回。
当我们调用 res(“male”)时,因为外层 ctx 和 args 还是会存在内存当中,所以调用时,前面的 ctx 也就是 source,args 也就是 18,再将传入的 ”male” 跟 18 合并 [18,’male’],执行 source.apply(destination,[18,’male’]); 返回函数结果即可。bind() 的模拟完成!
但是 bind 除了上述用法,还可以有如下用法:
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
// 添加一个返回值对象
return {
age:age,
gender:gender,
name:this.name
}
}
let destination = {
name:”xuan”
};
var res = source.bind1(destination,18);
var person = new res(“male”);
console.log(person);
打印效果如下:
我们发现 bind 函数支持 new 关键字,调用的时候 this 的绑定失效了,那么 new 之后,this 指向哪里呢?我们来试一下,代码如下:
function source(age,gender){
console.log(this);
}
let destination = {
name:”xuan”
};
var res = source.bind(destination,18);
console.log(new res(“male”));
console.log(res(“male”));

执行 new 的时候,我们发现虽然 bind 的第一个参数是 destination,但是 this 是指向 source 的。

不用 new 的话,this 指向 destination。
好,现在再来回顾一下我们的 bind1 实现:
Function.prototype.bind1 = function(ctx,…args){
var that = this;
return function(){
// 将外层函数的参数和内层函数的参数合并
var all_args = […args].concat([…arguments]);
// 因为 ctx 是外层的 this 指针,在外层我们使用一个变量 that 引用进来
return that.apply(ctx,all_args);
}
}
如果我们使用:
var res = source.bind(destination,18);
console.log(new res(“male”));
如果执行上述代码,我们的 ctx 还是 destination,也就是说这个时候下面的 source 函数中的 ctx 还是指向 destination。而根据 Function.prototype.bind 的用法,这时 this 应该是指向 source 自身。
我们先把部分代码抄下来:
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
// 添加一个返回值对象
return {
age:age,
gender:gender,
name:this.name
}
}
let destination = {
name:”xuan”
};
我们改一下 bind1 函数:
Function.prototype.bind1 = function (ctx, …args) {
var that = this;//that 肯定是 source
// 定义了一个函数
let f = function () {
// 将外层函数的参数和内层函数的参数合并
var all_args = […args].concat([…arguments]);
// 因为 ctx 是外层的 this 指针,在外层我们使用一个变量 that 引用进来
var real_ctx = this instanceof f ? this : ctx;
return that.apply(real_ctx, all_args);
}
// 函数的原型指向 source 的原型,这样执行 new f()的时候 this 就会通过原型链指向 source
f.prototype = this.prototype;
// 返回函数
return f;
}
我们执行
var res = source.bind1(destination,18);
console.log(new res(“male”));
效果如下:

已经达到我们的效果!
现在分析一下上述实现的代码:
// 调用 var res = source.bind1(destination,18)时的代码分析
Function.prototype.bind1 = function (ctx, …args) {
var that = this;//that 肯定是 source
// 定义了一个函数
let f = function () {
… // 内部先不管
}
// 函数的原型指向 source 的原型,这样执行 new f()的时候 this 就会指向一个新家的对象,这个对象通过原型链指向 source,这正是我们上面执行 apply 的时候需要传入的参数
//f.prototype==>source.prototype
f.prototype = this.prototype;
// 返回函数
return f;
}
f()函数的内部实现分析:
//new res(“male”)相当于运行 new f(“male”);下面进行函数的运行态分析
let f = function () {
console.log(this);// 这个时候打印 this 就是一个_proto_指向 f.prototype 的对象, 因为 f.prototype==>source.prototype,所以 this._proto_==>source.prototype
// 将外层函数的参数和内层函数的参数合并
var all_args = […args].concat([…arguments]);
// 正常不用 new 的时候 this 指向当前调用处的 this 指针(在全局环境中执行,this 就是 window 对象);使用 new 的话这个 this 对象的原型链上有一个类型是 f 的原型对象。
// 那么判断一下,如果 this instanceof f,那么 real_ctx=this, 否则 real_ctx=ctx;
var real_ctx = this instanceof f ? this : ctx;
// 现在把真正分配给 source 函数的对象传入
return that.apply(real_ctx, all_args);
}
至此 bind()函数的模拟实现完毕!如有不对之处,欢迎拍砖!您的宝贵意见是我写作的动力,谢谢大家。

正文完
 0