JavaScript进阶之模拟call,apply和bind

原文:zhehuaxuan.github.io/2019/02/26/…
做者:zhehuaxuanjavascript

目的

在JavaScript中有三种方式来改变this的做用域callapplybind。它们在前端开发中颇有用。好比:继承,React的事件绑定等,本文先讲用法,再讲原理,最后本身模拟,旨在对这块内容有系统性掌握。前端

Function.prototype.call()

MDN中对call()解释以下:
call()容许为不一样的对象分配和调用属于一个对象的函数/方法
也就是说:一个函数,只要调用call()方法,就能够把对象以参数传递给函数。java

若是仍是不明白,不急!咱们先来写一个call()函数最简单的用法:git

function source(){
    console.log(this.name); //打印 xuan
}
let destination = {
    name:"xuan"
};
console.log(source.call(destination));
复制代码

上述代码会打印出destinationname属性,也就是说source()函数经过调用call()source()函数中的this<=>destination对应起来。相似于实现destination.source()的效果github

好,明白基本用法,再来看下面的例子:数组

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自己没有返回任何值,故undefined
复制代码

打印效果以下:闭包

咱们能够看到call()支持传参,并且是以arg1,arg2,...的形式传入。咱们看到最后还还输出一个undefined,说明如今调用source.call(…args)没有返回值。app

咱们如今给source函数添加返回值:ide

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()函数的做用总结以下:

  1. 改变this的指向
  2. 支持对函数传参
  3. 调用call的函数返回什么,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"));
复制代码

打印效果以下:

参数出现了。 第三步:
调用call的函数返回什么,call返回什么

咱们再修改一下:

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;
    args = args || [];
    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的做用,可是须要在调用函数时生效

2.bind中也能够添加参数

注:bind还支持new语法,下面会展开。
咱们先根据上述2点区别来模拟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]);
        //apply改变ctx的指向
        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.bind(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 = 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()函数的模拟实现完毕!若有不对之处,欢迎拍砖!您的宝贵意见是我写做的动力,谢谢你们。

相关文章
相关标签/搜索