js中的bind、apply、call的原生实现及其思考

·  阅读 613

一、引言

  说起这三个函数,可能大部分人都觉得,这有啥,我用的贼熟练。对,没错,这是js中十分常见的三个改变this指向的函数,但是里面其实还是有一些细节的地方值得大家注意。下面我们先简要介绍一下这三个函数。

二、函数的用法

  其实这三个函数的用法挺简单的,所以我就不过多介绍了,只是简要的介绍一下。 apply函数接受两个参数,第一个是要绑定给this的值,第二个是一个数组。call函数和apply类似,call函数接受的是一个参数列表。bind函数则和前面两个函数有少许区别,bind函数返回的是一个新函数,需要在使用的时候手动调用一下,而其他两个函数是在使用时立即执行。下面是一些简单的例子

let obj1 = {
  name:"obj1"
}

let obj2 = {
  name:"obj2"
}

global.name = "global";

function f(arg1, arg2) {
  console.log(arg1 + " " + arg2 + " " + this.name);
}

f(1, 2);
//1 2 global
f.apply(obj1, [1, 2]);
//1 2 obj1
f.call(obj1, 1, 2);
//1 2 obj1
let newF = f.bind(obj2, 1, 2);
newF();
//1 2 obj2
复制代码

三、进一步思考

  对于三种函数的用法我相信即使之前没接触过这三种函数的同学在简单看了例子之后也能大概掌握了,但是你有没有想过下面这中情况:

let obj1 = {
  name:"obj1"
}

let obj2 = {
  name:"obj2"
}

global.name = "global";

function f(arg1, arg2) {
  console.log(this.name);
}

let newF = f.bind(obj1).bind(obj2).bind(this);
复制代码

  假设我们使用一次bind会改变this的指向,但是我们在后续继续使用bind,是不是就会继续改变指向,那这个newF的输出就会变成global?我们来测试一下:

let newF = f.bind(obj1).bind(obj2).bind(this);
newF();
//obj1
复制代码

  居然输出了obj1,我不是在后续又改变了他的指向么,为什么没用?这个问题等我们手动实现了这三个函数后就会有了答案,下面我们尝试自己实现这几个函数。

四、call、apply函数实现

  其实这几个函数实现起来比较简单,我们直接在传入的obj对象上挂载一个方法,在调用这个方法,就相当于改变了this的指向。随后接受返回值,将方法从对象上删除,最后返回这个返回值,一个简单的call函数就已经实现了。

Function.prototype.MyCall = function (obj) {
  obj._f = this;
  let res = obj._f();
  delete obj._f;
  return res;
}
复制代码

  我们来试着使用一下

let demo1 = {
  name:"demo1"
}

function f() {
  return this.name;
}

console.log(f.MyCall(demo1))
//demo1
复制代码

  可以看到我们已经成功改变了this的指向,不要忘了他应该还要接受一个参数列表,我们继续完善:

Function.prototype.MyCall = function (obj, ...rest) {
  obj._f = this;
  let res = obj._f(...rest);
  delete obj._f;
  return res;
}
复制代码

  其实也非常简单,我们使用es6中的语法很容易就可以完成这个功能,再来看一下效果

let demo1 = {
  name:"demo1"
}

function f(arg1, arg2) {
  return arg1 +" "+arg2+" "+this.name;
}

console.log(f.MyCall(demo1, 1, 2))
//1 2 demo1
复制代码

  好了,一个简单版的call函数就已经实现了,我们下面依葫芦画瓢来实现apply函数

Function.prototype.MyApply = function (obj, list) {
  obj._f = this;
  let res = obj._f(...list);
  delete obj._f;
  return res;
}

Function.prototype.MyCall = function (obj, ...rest) {
  obj._f = this;
  let res = obj._f(...rest);
  delete obj._f;
  return res;
}

let demo1 = {
  name:"demo1"
}

function f(arg1, arg2) {
  return arg1 +" "+arg2+" "+this.name;
}

console.log(f.MyApply(demo1, [1, 2]))
//1 2 demo1

复制代码

  这样我们就实现来apply和call两个函数,这两个函数现在其实还很不完善,因为没有做任何错误捕获的处理,我们放到后面再说,现在我们先完成bind函数。

五、bind函数的实现

  其实bind函数的实现非常简单,直接使用我们之前实现的call函数即可

Function.prototype.MyBind = function (obj, ...rest) {
  return () => this.MyCall(obj, ...rest);
}
let newF = f.MyBind(demo1, 1, 2);
console.log(newF());
1 2 demo1
复制代码

  注意我们使用了箭头函数来保证this指向我们想要的对象。其实到这里我们已经把三个函数全部实现了,那么回到我们开始时的问题,为什么我连续绑定多个this只有第一个有效。想一想这三个方法的实现,我想你应该有了答案。如果我们连续绑定多个this值,那么其实是相当于下面这种结果:

// f.bind(obj1).bind(obj2)
//等同于下面这样
obj1 = {
  name: "obj1",
  _f:function (arg1, arg2) {
    console.log(arg1+" "+arg2+" "+this.name);
  }
}

obj2 = {
  name: "obj2",
  _f:function (arg1, arg2) {
    obj1._f(arg1, arg2);
  }
}

obj2._f(1, 2);
//1 2 obj1
复制代码

  你看似重新绑定了this值,但是其实最后执行的还是obj1身上的函数,所以还是会输出obj1的name值。这就是我对于bind连续绑定为什么不能生效的理解,如果有什么错误的地方还希望大家指出,因为自己也不能保证我的理解全部都是正确的。好了,到这里我们已经把文章开头的问题说明白了,下一步我们来把我们实现的函数真正的完善一下。

六、apply、call、bind函数的完善

  首先我们看下面的例子

Function.prototype.MyCall = function (obj, ...rest) {
  obj._f = this;
  let res = obj._f(...rest);
  delete obj._f;
  return res;
}

let demo1 = {
  name:"demo1",
  _f:function () {
    console.log("这是原本就有的f函数");
  }
}

function f(arg1, arg2) {
  return arg1 +" "+arg2+" "+this.name;
}

demo1._f();
//这是原本就有的f函数
console.log(f.MyCall(demo1, 1, 2));
//1 2 demo1
demo1._f();
//报错 demo1._f is not a function
复制代码

  尴尬的事情出现了,我们居然把原本的_f函数删掉了,为了避免这个问题,我们使用es6的symbol对象来解决这个问题

Function.prototype.MyCall = function (obj, ...rest) {
  let _f = Symbol();
  obj[_f] = this;
  let res = obj[_f](...rest);
  delete obj[_f];
  return res;
}

let demo1 = {
  name:"demo1",
  _f:function () {
    console.log("这是原本就有的f函数");
  }
}

function f(arg1, arg2) {
  return arg1 +" "+arg2+" "+this.name;
}

demo1._f();
//这是原本就有的f函数
console.log(f.MyCall(demo1, 1, 2));
//1 2 demo1
demo1._f();
//这是原本就有的f函数
复制代码

  每个symbol值都是唯一的,不会和其他值发生冲突,所以也确保了不会影响的对象上的任何属性。 再一个问题就是如果传入的obj为undefin或者null,我们把它指向全局,如果传入的对象是基本类型,我们使用Object函数对其进行处理。

Function.prototype.MyCall = function (obj, ...rest) {
  if(obj === null || typeof obj === "undefined") {
    obj = global;
  }
  else if(typeof obj != "object") {
    obj = Object(obj);
  }
  let _f = Symbol();
  obj[_f] = this;
  let res = obj[_f](...rest);
  delete obj[_f];
  return res;
}
复制代码

  对于apply我们同样做这样的处理。最后就是bind,值得注意的是,bind返回的函数是可以当作构造函数的。所以对于bind函数,我们返回一个新的F函数,在F函数被调用时,判断一下当前的this的构造函数是否是F,如果是,那么证明这是使用了new函数来调用,所以我们应该将this就设置为当前的this。否则把this设置为obj对象。对代码进行进一步修改如下:

Function.prototype.MyBind = function (obj, ...rest) {
  if(obj === null || typeof obj === "undefined") {
    obj = global;
  }
  else if(typeof obj != "object") {
    obj = Object(obj);
  }
  let that = this;
  return F = function () {
    if(this instanceof F) {
      return that.MyCall(this, ...rest);
    }

    return that.MyCall(obj, ...rest);
  }
}

Function.prototype.MyApply = function (obj, list) {
  if(obj === null || typeof obj === "undefined") {
    obj = global;
  }
  else if(typeof obj != "object") {
    obj = Object(obj);
  }
  let _f = Symbol();
  obj[_f] = this;
  let res = obj[_f](...list);
  delete obj[_f];
  return res;
}

Function.prototype.MyCall = function (obj, ...rest) {
  if(obj === null || typeof obj === "undefined") {
    obj = global;
  }
  else if(typeof obj != "object") {
    obj = Object(obj);
  }
  let _f = Symbol();
  obj[_f] = this;
  let res = obj[_f](...rest);
  delete obj[_f];
  return res;
}
复制代码

  这就是我根据自己的理解实现的bind、call以及apply,代码中肯定还存在许多不完善的地方,但是基本的实现思路应该是没错的,欢迎大家交流讨论。github地址:github.com/klx-buct/bi…

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改