深入理解apply,call,bind及源码实现

本文详细解析JavaScript中this指向的原理及call、apply、bind方法的区别与实现。重点介绍了bind方法的功能,包括创建新函数、函数柯里化以及如何处理作为构造函数的情况。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

对于前面2个 apply,call大家应该非常熟悉了,都可以改变this指向,都可以传参数,但是bind的话很多人可能觉得和它们没有什么区别,估计用bind也用的少,下面我来一一分析下各自的实现原理:

1.call方法

1.第一个参数是this指向的对象。
2.使用的单个参数进行传递。
3.用于确定了函数的形参有多少个的时候用。

举个例子:

       var name = '李四'
        var b = {name:'张三'};
        function a(n){
            console.log(this.name+n+'岁了')
        }
        a.call(b,'18');//'张三18岁了'
        a.call(null,'18');//'李四18岁了'

我们可以这样理解:把a方法放到b里面,然后我在b的环境下执行a(如果b没有值,那就相当再window环境下执行a,this是指向window的),相当b.a(18)或者a(18),然后我再b里面删除a方法。知道原理了,那么我们按照上面的例子来自己封装下call方法:

    Function.prototype.call = function (obj) {
            // 当call的第一个参数没有或者是null的时候,this的指向是window
            var obj= obj || window;
            // 把a方法放进里面
            obj.fn = this;
            // 用于存储call后面的参数
            var args = [];
            var len = arguments.length;
            // 这里是为了将一个函数的参数传入到另外一个函数执行
            for (var i = 1; i < len; i++) {
                args.push('arguments[' + i + ']');
            };
            // 在eval的环境下 args数组会变成一个一个参数字符串(默认是会调用Array.toString())
            var result = eval('obj.fn(' + args + ')');
            // 删除b里面的a方法
            delete obj.fn;
            // 因为函数可能有返回值,所以把结果也返回出去给他们
            return result;
        };
2.apply方法(和call没啥区别)

1.第一个参数是this指向的对象。
2.使用的参数是数组进行传递。
3.用于确定了函数的形参的个数不确定的情况下使用。
原理我就不累赘了,和call没有什么区别,只有传递的形参不用,apply传递的一定要是是数组,那么我们就可以在传参的进行判断下。我们来模拟下代码的实现:

   Function.prototype.apply = function (obj, arr) {
      // 当apply的第一个参数是null的时候,this的默认指向是window
      var obj = obj || window;
      // 把该函数挂载到对象上
      obj.fn = this;
      //判断有没有传值
      if (!arr) {
          result = obj.fn();
      } else {
          //判断传入的是不是数组,不是的话抛出异常
          if (!Array.isArray(arr)) {
              throw new Error('上传的必须是数组');
          };
          var args = [];
          // 用于存储apply后面的参数
          for (var i = 0; i < arr.length; i++) {
              args.push('arr[' + i + ']');
          };
          // 这里的args默认是会调用Array.toString()方法的
          var result = eval('obj.fn(' + args + ')');
      }
      // 删除函数
      delete obj.fn;
      // 因为函数可能有放回值,所以把结果也返回出去给他们
      return result;
        }
2.bind方法

这里重要讲下bind,bind和call和apply还是有点区别的。
1.bind会创建一个函数(称为绑定函数),创建一个新函数而不执行,这是bind和call与apply方法的一个重要差别,call和apply这两个方法都会立即执行函数,返回的是函数执行后的结果。而bind函数只创建一个新函数而不执行。
2.函数的柯里化(使用一个闭包返回一个函数),柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
下面我们来慢慢模拟一下bind的实现(首选返回一个函数):

  Function.prototype.bind = function(obj){
        var self =this;
        //第一个参数为它运行时的this,应该取第二个之后的参数
        var args =Array.prototype.slice.call(arguments,1);
        //返回一个新函数闭包
        var newFn = function(){
            self.apply(obj,args);
        };
        return newFn;
    };
    //例子:
    var name = '李四'
    var b = {
        name: '张三',
    };

    function a(age) {
        console.log(this.name + age + '岁了')
    }
    var p =a.bind(b,'18');
    p();//"张三18岁了"

和预想的一样,bind得底部实现还是apply方法,很稳!!,有没有发现啥问题,我只在返回的新函数里面传了第2个参数,调用的时候都没传参数,这是个问题!然后补充一下:

 Function.prototype.bind = function(obj){
        var self =this;
        //第一个参数为它运行时的this,应该取第二个之后的参数
        var args =Array.prototype.slice.call(arguments,1);
        //返回一个新函数闭包
        var newFn = function(){
            self.apply(obj,args.concat(Array.prototype.slice.call(arguments)));
        };
        return newFn;
    };
    //例子改下参数
    
    var name = '李四'
    var b = {
        name: '张三',
    };

    function a(age,sex) {
        console.log(this.name + age + '岁了,性别'+sex);
    }
    var p =a.bind(b,'18');
    p('男');//张三18岁了,性别男

和预想的一样!可以接受2个参数,实现了函数的柯里化。是不是还觉得哪里有问题?对的,函数才能使用bind!这里需要判断一下,再改下代码:

   Function.prototype.bind = function(obj){
        var self =this;
        if(typeof this !=='function'){
            throw new Error('只有函数才可以调用bind');
        };
        //第一个参数为它运行时的this,应该取第二个之后的参数
        var args =Array.prototype.slice.call(arguments,1);
        //返回一个新函数闭包
        var newFn = function(){
            self.apply(obj,args.concat(Array.prototype.slice.call(arguments)));
        };
        return newFn;
    };

好了,到这里的话,感觉好像代码实现的差不多,但是这里有个重点,当我们把创建出来的新函数当做构造函数的时候,官方文档有这么一句话:“说明绑定过后的函数被new实例化之后,需要继承原函数的原型链方法,且绑定过程中提供的this被忽略(继承原函数的this对象),但是参数还是会使用。” 看上去不是很明白,其实主要是说,当new完之后 新创建出来的实例要继承原函数的原型,并且this指向新创建出来的实例对象(绑定的this将失效)。首先实现下原理继承:

    Function.prototype.bind = function(obj){
        var self =this;
        if(typeof this !=='function'){
            throw new Error('只有函数才可以调用bind');
        };
        //第一个参数为它运行时的this,应该取第二个之后的参数
        var args =Array.prototype.slice.call(arguments,1);
        //返回一个新函数闭包
        var newFn = function(){
            self.apply(obj,args.concat(Array.prototype.slice.call(arguments)));
        };
        //继承原函数的原型
        newFn.prototype = this.prototype;
        return newFn;
    };

如果这样写的话是有问题的,如果我改变了新创建出来的函数的原型同样也修改了原函数的原型,所以这里需要写一个过渡函数(如果不懂继承的同学,可以点击这里)。

   Function.prototype.bind = function(obj){
        var self =this;
        if(typeof this !=='function'){
            throw new Error('只有函数才可以调用bind');
        };
        //第一个参数为它运行时的this,应该取第二个之后的参数
        var args =Array.prototype.slice.call(arguments,1);
        //返回一个新函数闭包
        var newFn = function(){
            self.apply(obj,args.concat(Array.prototype.slice.call(arguments)));
        };
        //过渡函数
        var f = function(){};
        f.prototype = this.prototype;
        newFn.prototype = new f();
        return newFn;
    };

接下来需要判断this的值,判断到底是不是new出来的实例,通过instanceof 判断。简单解释下 instanceof ,假如a instanceof b,意思是a是不是b的实例,或者这样说:b的prototype是否在a的原型链上。再改下代码:

    Function.prototype.bind = function(obj){
        var self =this;
        if(typeof this !=='function'){
            throw new Error('只有函数才可以调用bind');
        };
        //第一个参数为它运行时的this,应该取第二个之后的参数
        var args =Array.prototype.slice.call(arguments,1);
        //返回一个新函数闭包
        var newFn = function(){
            self.apply(this instanceof f ?  this : obj ,args.concat(Array.prototype.slice.call(arguments)));
        };
        //过渡函数
        var f = function(){};
        f.prototype = this.prototype;
        newFn.prototype = new f();
        return newFn;
    };

到这里代码就修改的差不多了,最后拿这段代码“this instanceof f ? this : obj ”分析下,这里的意思是如果是new出来的,this指向它实例出来的对象,如果不是,那么this是指向绑定的obj对象。如果obj没有值,不传值或者是null,那么这个this是指向window的,有些同学代码在这里做了下个判断 “this instanceof f ? this : obj || this”,这样写的话,this不一定都指向window的,比如看下面这个例子:

    var name = '张三';
    var foo = {
        name:'李四',
        fn: fn.bind(null) //如果在这里执行,this是指向widonw的
    };

    function fn() {
        console.log(this.name);
    }
	//foo这里调用fn,所以在这里把this的指向改变了,this===foo;
    foo.fn() //'张三'

所以说这个判断是不对的。好了,到这里就差不多已经说完了(如果有不对之处,欢迎指正,不胜感激!!!),本来还想说下bind的几个难点,怕写的太多都看不下去了,还是下一篇再说吧,欢乐的时光总是过得特别快,又到时候和大家讲拜拜!!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值