资讯专栏INFORMATION COLUMN

JavaScript进阶之模拟call,apply和bind

CoderBear / 1870人阅读

摘要:模拟和模拟一样,现摘抄下面的代码添加一个返回值对象然后我们定义一个函数,如果执行下面的代码能够返回和函数一样的值,就达到我们的目的。

原文:https://zhehuaxuan.github.io/...  
作者:zhehuaxuan
目的

本文主要用于理解和掌握callapplybind的使用和原理,本文适用于对它们的用法不是很熟悉,或者想搞清楚它们原理的童鞋。
好,那我们开始!
在JavaScript中有三种方式来改变this的作用域callapplybind。我们先来看看它们是怎么用的,只有知道怎么用的,我们才能来模拟它。

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));

上述代码会打印出destinationname属性,也就是说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函数跟applycall有两个区别:

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")时,因为外层ctxargs还是会存在内存当中,所以调用时,前面的ctx也就是sourceargs也就是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()函数的模拟实现完毕!如有不对之处,欢迎拍砖!您的宝贵意见是我写作的动力,谢谢大家。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/102537.html

相关文章

  • 进阶3-3期】深度解析 call apply 原理、使用场景及实现

    摘要:之前文章详细介绍了的使用,不了解的查看进阶期。不同的引擎有不同的限制,核心限制在,有些引擎会抛出异常,有些不抛出异常但丢失多余参数。存储的对象能动态增多和减少,并且可以存储任何值。这边采用方法来实现,拼成一个函数。 之前文章详细介绍了 this 的使用,不了解的查看【进阶3-1期】。 call() 和 apply() call() 方法调用一个函数, 其具有一个指定的 this 值和分...

    godlong_X 评论0 收藏0
  • 进阶3-4期】深度解析bind原理、使用场景及模拟实现

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

    guyan0319 评论0 收藏0
  • javasscript - 收藏集 - 掘金

    摘要:跨域请求详解从繁至简前端掘金什么是为什么要用是的一种使用模式,可用于解决主流浏览器的跨域数据访问的问题。异步编程入门道典型的面试题前端掘金在界中,开发人员的需求量一直居高不下。 jsonp 跨域请求详解——从繁至简 - 前端 - 掘金什么是jsonp?为什么要用jsonp?JSONP(JSON with Padding)是JSON的一种使用模式,可用于解决主流浏览器的跨域数据访问的问题...

    Rango 评论0 收藏0
  • 进阶 6-2 期】深入高阶函数应用柯里化

    摘要:引言上一节介绍了高阶函数的定义,并结合实例说明了使用高阶函数和不使用高阶函数的情况。我们期望函数输出,但是实际上调用柯里化函数时,所以调用时就已经执行并输出了,而不是理想中的返回闭包函数,所以后续调用将会报错。引言 上一节介绍了高阶函数的定义,并结合实例说明了使用高阶函数和不使用高阶函数的情况。后面几部分将结合实际应用场景介绍高阶函数的应用,本节先来聊聊函数柯里化,通过介绍其定义、比较常见的...

    stackvoid 评论0 收藏0
  • JavaScript深入bind模拟实现

    摘要:也就是说当返回的函数作为构造函数的时候,时指定的值会失效,但传入的参数依然生效。构造函数效果的优化实现但是在这个写法中,我们直接将,我们直接修改的时候,也会直接修改函数的。 JavaScript深入系列第十一篇,通过bind函数的模拟实现,带大家真正了解bind的特性 bind 一句话介绍 bind: bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数...

    FingerLiu 评论0 收藏0

发表评论

0条评论

CoderBear

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<