JavaScript call、apply、bind详解与异同

一、call

Ⅰ、call 函数的简介:

1、call函数的用法

fn.call(obj,1,2);

A、call() 函数中的第一个参数表示:想让 this 指向的对象 (obj);
B、call() 函数中的第二及以后参数表示:传进去的实参;

2、call 函数的功能:

其一、让函数立执行;
其二、可改变 this 的指向;
其三、可实现继承问题;

Ⅱ、call 函数如何调用:

1、call() 函数的调用过程:

var obj = { a: 1 };
    function fn(x, y) {  // 每个函数都会有 call 方法;
      console.log(this);
      return x + y;
    }

    console.log(fn.call(null, 1, 2)); 
    // 此时的输出结果为:Window, 3;
    // 若此时的 call() 函数中的第一个参数未传值,或传值为 null 时,此时的 fn 中的 this 指向为:Window; 
                                    
    console.log(fn.call(obj, 1, 2)); 
    // 此时的输出结果为:{a: 1}, 3;
    // 若此时的 call() 函数中的第一个参数非空而是 obj ,那么此时的 fn 中的 this 指向为:obj; 

2、call() 函数的调用结果展示为: 

Ⅲ、模拟 call 函数的实现原理;

1、对 call 函数原理的模拟 (即:实现自己的 myCall 方法) ;


	Function.prototype.myCall = function (context) {  
	// 其中 context 参数是指:改变 this 指向,而待指向的的参数值(如:obj);
	// 此时是在 Function 的原型链上来添加 myCall() 函数, 那么此时该函数中的 this 指向的是:调用该 myCall() 方法的实例或函数;
	
      // 处理参数(即:实参); 
      var args = [...arguments].slice(1);  
      // 将 arguments 的值从第二个截取后,放到 args 的数组里;
      // 该操作是将传入的实参值转化成数组,以待后面使用;

      context = context || window;   
      // 当传的值为 null 或没传值时(均为:false),此时 context 的值为:Window;      
      // 当传值时(为:true),此时的 context 的值为:context;

     
      context.fn = this; 
       // 此时 this 指向的是:将来调用 myCall 方法的那个函数;
       // 而又由于 context 调用 fn 这个属性是指向 this, 因此此时的 this 就指向了 context;  那么此时就完成了 this 指向 context 的目的;
       // 该操作是:让将来调用 myCall 方法的那个函数,封装到 context 的一个属性中去(即:context.fn);
       // 那么此时的 context.fn 就代表的是调用 myCall 方法的那个函数,它是一个函数;

      var r = context.fn(...args); 
      // 该操作是:调用 context.fn() 函数,也就是调用了 '调用 myCall' 的函数;
      // 并传参为:'...args', 也就是将 args 数组中的值展开作为参数传入函数中;
      // 此时也就完成了,将 this 的指向为:context;     且还运行了 '调用 myCall' 的函数,并将参数传进去; (即:已经实现了 call() 函数的功能); 

      return r;
      // 然后再将操作后的值返回;
    }

    var obj = { a: 1 };
    function fn(x, y) {  
      console.log(this);
      return x + y;
    }


    var sum = fn.myCall(obj, 1, 2);
    console.log(sum); 
    // 此时的输出结果为:'{a: 1, fn: ƒ}', 3;   
    // 输出结果都正确,只是 context 中添加一个 fn 属性,但添加一个属性也是没大影响的;
    // 注意:此时并没有模拟 this 指向 Window 的情况;

2、模拟的 myCall() 函数的调用结果展示为:

二、apply

Ⅰ、改变this指向

这是网上一个常见的例子:

var person = {
    fullName: function() {
        return this.firstName + " " + this.lastName;
    }
}
var person1 = {
    firstName: "Bill",
    lastName: "Gates",
}
person.fullName.apply(person1);  // 将返回 "Bill Gates"

如何理解?可以这么理解,person有个方法fullName调用this.firstNamethis.lastName这两个变量,this指向person这个对象,但它是没有这两个变量的。

apply可以改变调用apply方法的函数(这里指的就是person.fullName这个函数)的this指向,现在person.fullName.apply(person1)就让this指向person1了,fullName方法就可以访问到这两个值,就成功输出。

Ⅱ、将数组入参变为一般入参 

这里说一点,就是这个作用就是apply和call的最大区别了。

就是apply的第二个参数接受的是数组,call不是。

接收数组有什么用?

比如当一个函数入参是非数组,而你目前拥有的是数组,你不想处理数组再进行入参的输入,你就可以使用apply。

例子:

Math.max(1,2,3)//3
Math.max([1,2,3])//报错
Math.max.apply(null,[1,2,3])//3

需要注意的是这里的第一个值为null时,

在 “JavaScript 严格模式”下则它将成为被调用函数的所有者(对象)也就是没改变指向,在“非严格”模式下,它成为全局对象

因为我们只是测试第二个入参作用,因此,第一个入参null就用来占位,就算改变了指向我们也没有用到它。

以下的例子可以加深一下你对apply和call区别的理解。

apply:

var person = {
  fullName: function(city, country) {
    return this.firstName + " " + this.lastName + "," + city + "," + country;
  }
}
var person1 = {
  firstName:"John",
  lastName: "Doe"
}
person.fullName.apply(person1, ["Oslo", "Norway"]);

call:

var person = {
  fullName: function(city, country) {
    return this.firstName + " " + this.lastName + "," + city + "," + country;
  }
}
var person1 = {
  firstName:"John",
  lastName: "Doe"
}
person.fullName.call(person1, "Oslo", "Norway");

三、bind

Function.prototype.bind()

 bind()方法主要就是将函数绑定到某个对象,bind()会创建一个函数,函数体内的this对象的值会被绑定到传入bind()中的第一个参数的值,例如:f.bind(obj),实际上可以理解为obj.f(),这时f函数体内的this自然指向的是obj;

例:

  var a = {
    b: function() {
      var func = function() {
        console.log(this.c);
      }
      func();
    },
    c: 'hello'
  }
  a.b(); 
  console.log(a.c); 

那么答案是什么呢?undefined和hello,大家可以打印看下,这个就是因为fun()这个函数执行的时候他的函数上下文为window,而a.b()的这个函数的执行的时候函数上下文this为a对象是什么意思呢?

  var a = {
    b: function() {
      console.log(this.c); //hello  
      var func = function() {
        console.log(this.c); //undefined
      }
      func();
    },
    c: 'hello'
  }
  a.b(); 

看了上面的例子和console.log()的结果大家应该是知道了这个函数上下文大概是这么一回事了把?什么还是不会好把看这里,这里有关于this指向的问题的解析那么问题来了当我们希望func()他的输出的值就是为hrllo怎么办

方法一:改变this的值

  var a = {
    b: function() {
      var _this = this; // 通过赋值的方式将this赋值给that
      var func = function() {
        console.log(_this.c);
      }
      func();
    },
    c: 'hello'
  }
  a.b(); // hello
  console.log(a.c); // hello

方法二:绑定this的值发生改变

// 使用bind方法一
  var a = {
    b: function() {
      var func = function() {
        console.log(this.c);
      }.bind(this);
      func();
    },
    c: 'hello'
  }
  a.b(); // hello
  console.log(a.c); // hello
 
// 使用bind方法二
  var a = {
    b: function() {
      var func = function() {
        console.log(this.c);
      }
      func.bind(this)();
    },
    c: 'hello'
  }
  a.b(); // hello
  console.log(a.c); // hello

这里我们以a.b()的形式去执行a对象中的b这个函数,是经过对象a的所以当我们来执行a对象中b函数的时候找this就会先找到a对象所以在a对象中的b这个函数中的this为a对象,所以这个时候bind,绑定的this也就是为a对象了

    c = 10;
    var a = {
      b: function () {
        console.log(this);
        var func = function () {
          console.log(this.c);
        }.bind(this);
        func();
      },
      c: 'hello'
    }
    var d = a.b;
    d();

这里我们以d()的形式去执行a对象中的b这个函数吗,因为d()的执行的时候由于没人应用this默认为window,所以在a对象中的b这个函数中的this为window,所以不这个时候bind,绑定的this也就是为window了

bind()的原生实现分步解析

1:通过call,吧arguments生成一个真正的数组

Array.prototype.slice.call(arguments)是用来将参数由类数组转换为真正的数组;
[].slice.call(arguments)是用来将参数由类数组转换为真正的数组;

上面两中方法的结果一样

解析:其实就是arguments他是一个类数组(对象)他没slice的方法 ,通过上面的Array.prototype.slice写法可以让他添加slice的方法,为什么可以呢?

slice()没参数默认从key0到最后一项,浅拷贝,不影响原数组

因为其实arguments也是一个对象,就相当与是在arguments中执行了slice()函数,那么不要参数的情况下就是根据key下标会返回一个完整的数组从而达到了生成了一个新的数组的效果,arguments对象中是有length属性和0.1.2.这样的属性的而slice就是会根据这些0.1.2的这些key(也叫属性值的东西)放回成为一个数组

Ⅲ、原生bing实现

    Function.prototype.myBind = function () {
      if (typeof this !== 'function') throw 'caller must be a function'
      let self = this // 这里是关键 用来和new出来的比较原型来判断是否为new出来的  当前函数对象
      let context = arguments[0]
      let args = Array.prototype.slice.call(arguments, 1) // 旧:参数
      let fn = function () {
        let fnArgs = Array.prototype.slice.call(arguments) // 新:参数
        // bind 函数的参数 + 延迟函数的参数
        // 用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
        self.apply(this instanceof self ? this : context, args.concat(fnArgs))
      }
      fn.prototype = Object.create(self.prototype) // 维护原型
      return fn
    }

关键点

1.bing是影响不到new构造函数过程中this为新构造出来的那个对象的定义

当一个绑定函数是用来构建一个值的,原来提供的 this 就会被忽略。 ---MDN

代码:this instanceof self

this:对myBing出来的函数所用者的指向

self:使用myBing()的那个函数

instanceof:用来检查构造函数的prototype属性是否出现在某一个实例对象的原型链上

2.由于返回的是新定义出来的函数,所以原型要记得绑定 

3.Object.create()  专门用来绑定__proto__的

分析

这里帮忙分析下子在用new流程的时候为什么要---(this instanceof self)

var YAxisPoint = Point.MyBind(null, 0/*x*/);
 
var axisPoint = new YAxisPoint(5);

我们以上面代码为例子,所以可以得出

1. self:Point 函数

我们使用关键词new的时候在执行YAxisPoint函数,那么YAxisPoint 函数中的this就是我们new出来的实例了

2..this = 新出来的实例对象

由于this等于新new出来的实例其实this instanceof self不应该为true的,因为新创建的函数和引用myBind的函数(self函数)完全无瓜葛

但是,fn.prototype = Object.create(self.prototype),导致fn构造函数new出来的实例指向的prototype和引用myBind的函数(self函数)的prototype是一样的,所以this instanceof self === true

四、 call、apply、bind三者的异同

共同点 :

  • 三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefined或null,则默认指向全局window

不同点:

  • call 和 apply 会调用函数, 并且改变函数内部this指向.
  • call 和 apply传递的参数不一样,call传递参数使用逗号隔开,apply使用数组传递,且apply和call是一次性传入参数,而bind可以分为多次传入
  • bind是返回绑定this之后的函数

应用场景

  1. call 经常做继承

  2. apply经常跟数组有关系. 比如借助于数学对象实现数组最大值最小值

  3. bind 不调用函数,但是还想改变this指向. 比如改变定时器内部的this指向

以上内容搬运相关学习内容的优秀创作者文章,详解可见:

call内容: 什么是 call 函数? call 函数的用法? call 函数的实现原理(实例演示)_call函数-CSDN博客

apply内容:js apply()用法详解-CSDN博客

bind内容:js中bind()使用详情_js bind-CSDN博客 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值