前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >通过自己实现函数 call,apply,bind 来了解他们的原理

通过自己实现函数 call,apply,bind 来了解他们的原理

作者头像
踏浪
发布2019-11-28 18:03:42
3340
发布2019-11-28 18:03:42
举报
文章被收录于专栏:踏浪的文章踏浪的文章

我们知道函数中的call,apply,bind都是可以修改函数的this指向。关于函数的this指向问题可以转到Javascript this 指向问题这篇文章。

Javascript this 指向问题一篇中我们知道函数运行时候this是取决于调用这个函数的对象。如果一个函数定义在了全局,那么这个this就指向window。

来看一段代码

代码语言:javascript
复制
var name = "tal"
var sex = "boy"

var tal = {
  name: "踏浪",
  sex: "男"
}

function person() {
  console.log(this.name)
  console.log(this.sex)
}
person() // tal boy

上面的person是定义在全局中的一个函数,person()调用的时候相当于是window.person(),是由window调用的,所以,this.namethis.sex都需要在window对象中寻找,即全局变量中找。所以,最后的值会是talboy

再来。我们来调用一下原生的call函数

代码语言:javascript
复制
var name = "tal"
var sex = "boy"

var tal = {
  name: "踏浪",
  sex: "男"
}

function person() {
  console.log(this.name)
  console.log(this.sex)
}
person.call(tal) // 踏浪 男

在调用了call函数以后,最后的值是踏浪 男。得到的是对象tal中的两个属性值。此时函数中的this指向了对象tal。结合上面的例子,直接调用person函数的时候,相当于是window.person()的执行,所以那时this指向了window。那现在既然指向了tal,换言之,就是相当于tal.person()执行了。

call 的实现

所以我们可以这样实现mycall函数

代码语言:javascript
复制
Function.prototype.mycall = function (context) {
  context.fn = this
  context.fn()
  delete context.fn
}

mycall接收一个参数,即一个对象,最终的this指向这个对象。函数内部实现在这个传入的对象中绑定上我们需要执行的这个函数,即context.fn = this一行。最后调用context.fn()。因为我们这样操作修改了传入对象的属性(添加了一个fn属性),所以最后需要删除这个fn属性。这样,第一版的call的实现已经完成。

接下来,原生 call 函数是可以接收参数的

代码语言:javascript
复制
var tal = {
  name: "踏浪",
  sex: "男"
}

function person(age) {
  console.log(age)
  console.log(this.name)
  console.log(this.sex)
}
person.call(tal, 18) // 踏浪 男

那我们自己实现的mycall要想能够接收参数,怎么实现呢

代码语言:javascript
复制
Function.prototype.mycall = function (context) {
  var args = [];
  var len = arguments.length;
  for (var i = 1; i < len; i++) {
    args.push(arguments[i]);
  }
  context.fn = this
  eval('context.fn(' + args.join(",") + ')')
  delete context.fn
}

因为我们需要在 mycall 调用的时候传递参数,而且参数的个数不确定,所以需要使用 arguments 。同时因为第一个参数以及确定了是我们需要的一个对象,this指向这个对象。所以 arguments 需要从 1 开始。我们用一个数组把需要的东西存放起来。

代码语言:javascript
复制
var args = [];
var len = arguments.length;
for (var i = 1; i < len; i++) {
  args.push(arguments[i]);
}

如果是这样的话,我传递过去后数组 args = [18, "成都"] 里面的每一项要怎么传递到 context.fn() 里面并且执行呢?或许你会想到时候 join(",") 方法,OK,没有问题。

代码语言:javascript
复制
var res = args.join(",");
context.fn(res);

这样么?不是。res的值是 18,成都 没问题,但是这就是一个字符串而已,我们要实现传递两个参数,所以,想到了把字符串与函数拼接。即有了 'context.fn(' + args.join() + ')'。这样最后就可以了。但是这只是一个字符串,怎么运行呢?这个时候就需要用到 ES3中的eval 直接传递这个参数进度即可。

上面的 join 方法其实就是把一个数组转换成了字符串,除了 join,你还能想到什么呢?对。可以使用字符转换来实现。

代码语言:javascript
复制
res = "" + args + ""
// or
res = args.toString()

因为每一个隔开的方式是 , 所以可以使用上面的两种,其他的就不行咯。

所以最终的 mycall

代码语言:javascript
复制
Function.prototype.mycall = function (context) {
  var args = [];
  var len = arguments.length;
  for (var i = 1; i < len; i++) {
    args.push(arguments[i]);
  }
  context.fn = this
  eval('context.fn(' + args.join(",") + ')')
  delete context.fn
}

apply的实现

有个实现call的过程,在来实现apply就容易多了。唯一不同的就是apply传递的参数是一个数组,而call是具体的每一项。只需要在参数上面做处理即可。

代码语言:javascript
复制
Function.prototype.myapply = function (context) {
  var args = [];
  var array = arguments[1];
  if ( typeof array !== "object" || !(array instanceof Array) ) {
    throw new Error("The 2'rd args must be Array.")
  }
  for (var i = 0; len = array.length, i < len; i++) {
    args.push('array[' + i + ']');
  }
  context.fn = this
  eval('context.fn(' + args.toString() + ')')
  delete context.fn
}

这里的 arguments[1] 是一个数组了。我们需要对它遍历,并且判断它是不是一个数组。其余的与 call 一样。

bind的实现

原生的bind有两种方式

代码语言:javascript
复制
var tal = {
  name: "踏浪",
  sex: "男"
}

function person(age) {
  console.log(age)
  console.log(this.name)
  console.log(this.sex)
}

person.bind(tal)(18)

// 或者
person.bind(tal, 18)()

所以。使用bind都需要调用两次,而第一次就是返回一个函数。原函数的参数可以在bind中调用,也可以在第二次运行时候调用。所以,根据调用bind时候传递的参数的个数确定最后是返回那种函数,有了下面的这段代码。

代码语言:javascript
复制
Function.prototype.mybind = function (context) {
  var args = [];
  var len = arguments.length;
  for (var i = 1; i < len; i++) {
    args.push(arguments[i]);
  }
  context.fn = this
  if (len === 1) {
    // 如果 mybind 的参数的个数只有一个,那么剩余参数在调用的时候传入
    return function(){
      var sub_args = [];
      for (var i = 0; len = arguments.length, i < len; i++) {
        sub_args.push('arguments[' + i + ']');
      }
      eval('context.fn(' + sub_args.toString() + ')')
      delete context.fn
    }
  } else {
    return function(){
      // 这里不能使用 arguments,这里面的 arguments 是这个 return 函数的而不是最开始的。
      var str = 'context.fn('
      for (var i = 0; len = args.length, i < len; i++) {
        str += "args["+i+"],"
      }
      var newStr = str.replace(/,$/, ")")
      eval(newStr)
      delete context.fn
    }
  }
}

总结

通过自己实现 call, apply, bind 这三种方法,能够更深刻的理解到这三个函数的原理,同时涉及到的只是点也多:this指向,arguments类数组,每一个对象都要的toSting方法(另一个是valueOf),eval方法的使用(不是滥用,webpack中就使用了这个方法),函数对象可以使用 delete 删除(使用var 定义的无法使用delete删除)。或许你已经明白了,但是代码种东西,还是自己动动手,印象更深刻。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019-08-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • call 的实现
  • apply的实现
  • bind的实现
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档