基于猫狗大战奥特曼,再手写一次apply、call和bind

我心飞翔 分类:javascript

基于猫狗大战奥特曼,再手写一次apply、call和bind~

温故而知新,再看不会你把我头拧下来!

今天刷题的时候看到一个有关 callapply 的奇葩描述,觉得挺有意思的,于是重新把 callapply 的逻辑手写了一遍,温故而知新~

大概是这样的:

  • 猫吃鱼,狗吃肉,奥特曼打小怪兽
  • 狗吃鱼:猫.吃鱼.call(狗, 鱼)
  • 猫打小怪兽:奥特曼.打小怪兽.call(猫, 小怪兽)

这么说还确实有几分道理,下面就通过这个描述重新手写一下 applybind!

前提

首先准备三个对象:奥特曼

let cat = {
  name: "猫",
  eatFish() {
    console.log(`${this.name} 吃鱼中!`);
  },
};
let dog = {
  name: "狗",
  eatMeat() {
    console.log(`${this.name} 吃肉中!`);
  },
};
let ultraman = {
  name: "迪迦",
  fight() {
    console.log(`${this.name} 打小怪兽中!`);
  },
};
 

准备好之后,我们先来实现一下call

call

狗吃鱼 的话需要这样使用:猫.吃鱼.call(狗, 鱼),可以看出来调用 call 的是 上面的 吃鱼 方法,而参数是 ,所以应该是这样使用:

cat.eatFish.call(dog, "狗");
 

对于 call 方法,大概的逻辑是这样的:

  1. 传入的第一个参数被当做上下文,这里是狗
  2. 狗添加一个吃鱼方法,指向猫的吃鱼,也就是猫的this
  3. 狗当然也可以吃各种鱼
  4. 吃完之后,狗删除吃鱼这个方法,因为本不属于它,只是借用

按照上面的逻辑,我们可以这样写:

Function.prototype.defineCall = function (context, ...args) {
  // 不传狗,默认是window
  var context = context || window;
  // 狗添加一个方法,指向猫的吃鱼方法,也就是this
  context.fn = this;
  // 狗可以吃各种鱼,也就是可能有多个参数
  let result = context.fn(...args);
  // 删除狗会吃鱼
  delete context.fn;
  return result;
};
 

这样,一个自定义的 call 基本上就完成啦!现在来测试一下:

cat.eatFish.defineCall(dog, "狗");
ultraman.fight.defineCall(cat, "猫");
// output:
// 狗 吃鱼中!
// 猫 打小怪兽中!
 

现在 可以 吃鱼 了, 可以 打小怪兽 了!

现在我们让狗多吃几种鱼,我们先来简单改一下猫的吃鱼:

let cat = {
  name: "猫",
  eatFish(...args) {
    console.log(`${this.name} 吃鱼中!吃的是:${args}`);
  },
};
 

然后我们再这样调用:

cat.eatFish.defineCall(dog, "三文鱼", "金枪鱼", "鲨鱼");

// output:
// 狗 吃鱼中!吃的是:三文鱼,金枪鱼,鲨鱼
 

这样就可以吃各种鱼了,当然是用arguments 来操作参数也是可以的。

apply

applycall 用法基本类似,区别就在于,第二个参数是数组,我们可以这样写:

Function.prototype.defineApply = function (context, arr) {
  var context = context || window;
  let result;
  context.fn = this;
  if (!arr) {
    // 如果没传参数,就直接执行
    result = context.fn();
  } else {
    //如果有参数就执行
    result = context.fn(...arr);
  }
  delete context.fn;
  return result;
};
 

现在再来调用一下,看看写的对不对:

cat.eatFish.apply(dog, ["狗"]);
ultraman.fight.apply(cat, ["猫"]);

// output:
// 狗 吃鱼中!
// 猫 打小怪兽中!
 

成功!🎉

bind

既然 callapply 都实现了,那稍微有点难度的 bind 也来实现一下好了,毕竟它们是 铁三角 嘛。

我们先来捋一下 bind 都有哪些东西:

  1. bind 也是用来转换 this 的指向的。
  2. bind 不会像它们两个一样立即执行,而是返回了一个绑定 this 的新函数,需要再次调用才可以执行。
  3. bind 支持函数柯里化。
  4. bind 返回的新函数的 this 是无法更改的,callapply 也不可以。

我们一步一步来写,首先写一个最简单的:

Function.prototype.defineBind = function (obj) {
  // 如果不存this,执行期间可能this就指向了window
  let fn = this;
  return function () {
    fn.apply(obj);
  };
};
 

然后给它加上传参的功能,变成这样:

Function.prototype.defineBind = function (obj) {
  //第0位是this,所以得从第一位开始裁剪
  let args = Array.prototype.slice.call(arguments, 1);
  // 如果不存this,执行期间可能this就指向了window
  let fn = this;
  return function () {
    fn.apply(obj, args);
  };
};
 

接着给它加上柯里化:

Function.prototype.defineBind = function (obj) {
  //第0位是this,所以得从第一位开始裁剪
  let args = Array.prototype.slice.call(arguments, 1);
  let fn = this;
  return function () {
    //二次调用我们也抓取arguments对象
    let params = Array.prototype.slice.call(arguments);
    //注意concat的顺序
    fn.apply(obj, args.concat(params));
  };
};
 

现在的 defineBind 差不多已经 初具bind形 了,让它升级成真正的 bind,还有一个细节:

返回的回调函数也可以通过 new 的形式去构造,但是在构造过程中,它的 this 会被忽略,而返回的实例仍然能继承构造函数的构造器属性和原型属性,并且可以正常接收属性(也就是只丢失了 this,其他都是正常的)。

这个意思其实就是让我们自定义 this 的判断和原型继承,所以比较难的来了,先了解一点:构造函数的实例的构造器指向构造函数本身:

function Fn(){};
let o = new Fn();
console.log(o.constructor === Fn);
//true
 

并且在构造函数运行时,内部的 this 是指向实例的(谁调用,this 就指向谁),所以 this.constructor 是指向构造函数的:

function Fn() {
  console.log(this.constructor === Fn); 
  //true
};
let o = new Fn();
console.log(o.constructor === Fn); 
//true
 

那是不是就可以通过改变 this.contructor 的指向来改变原型继承呢?

答案当然是对的!当返回函数作为构造函数的时候,this 指向的应该是实例,当返回函数作为普通函数的时候,this 指向的有应该是当前上下文:

Function.prototype.defineBind = function (obj) {
  let args = Array.prototype.slice.call(arguments, 1);
  let fn = this;
  let bound = function () {
    let params = Array.prototype.slice.call(arguments);
    //通过constructor判断调用方式,为true this指向实例,否则为obj
    fn.apply(this.constructor === fn ? this : obj, args.concat(params));
  };
  //原型链继承
  bound.prototype = fn.prototype;
  return bound;
};
 

这样,一个 bind 基本上就结束了,而且返回的构造函数所产生的实例也不会影响到构造函数。

但是!直接修改实例原型会影响构造函数!

那这个怎么办呢?要是构造函数的原型里啥都没有就好了,这样就不会相互影响了……blablabla……

写一个小例子,用一个中介,让构造函数的原型只能影响到实例,影响不到其他东西:

function Fn() {
  this.name = "123";
  this.sayAge = function () {
    console.log(this.age);
  };
}
Fn.prototype.age = 26;
// 创建一个空白函数Fn1,单纯的拷贝Fn的prototype
let Fn1 = function () {};
Fn1.prototype = Fn.prototype;

let Fn2 = function () {};
Fn2.prototype = new Fn1();
 

给Fn2加了一层 __proto__ 的方式,让Fn2的原型指向了一个实例,而实例的原型是Fn,这样Fn2的改变就不会影响到Fn了(当然通过 __proto__.__proto__ 还是一样能修改)!

Function.prototype.defineBind = function (obj) {
  let args = Array.prototype.slice.call(arguments, 1);
  let fn = this;
  //创建中介函数
  let fn_ = function () {};
  // 上面说的Fn2就是这里的bound
  let bound = function () {
    let params = Array.prototype.slice.call(arguments);
    //通过constructor判断调用方式,为true this指向实例,否则为obj
    fn.apply(this.constructor === fn ? this : obj, args.concat(params));
  };
  fn_.prototype = fn.prototype;
  bound.prototype = new fn_();
  return bound;
};
 

最后再用一个报错润色一下:

Function.prototype.defineBind = function (obj) {
  if (typeof this !== "function") {
    throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
  };
  let args = Array.prototype.slice.call(arguments, 1);
  let fn = this;
  //创建中介函数
  let fn_ = function () {};
  // 上面说的Fn2就是这里的bound
  let bound = function () {
    let params = Array.prototype.slice.call(arguments);
    //通过constructor判断调用方式,为true this指向实例,否则为obj
    fn.apply(this.constructor === fn ? this : obj, args.concat(params));
  };
  fn_.prototype = fn.prototype;
  bound.prototype = new fn_();
  return bound;
};
 

手写 bind 完毕!

最后用狗吃鱼来验证一下:

let cat = {
  name: "猫",
  eatFish(...args) {
    console.log(`${this.name} 吃鱼中!吃的是:${args}`);
  }
};
let dog = {
  name: "狗"
};
cat.eatFish.defineBind(dog, "三文鱼", "金枪鱼")("鲨鱼");

// output:
// 狗 吃鱼中!吃的是:三文鱼,金枪鱼,鲨鱼
 

最后再附上一个es6版本的手写bind,大家可以过一下,还是比较清晰的:

Function.prototype.defineBind = function (context, ...rest) {
  if (typeof this !== "function") {
   throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
  }
  var self = this;
  return function F(...args) {
    if (this instanceof F) {
      return new self(...rest, ...args);
    }
    return self.apply(context, rest.concat(args));
  };
};
 

参考资料:

  • js 手动实现bind方法,超详细思路分析!

我的公众号:道道里的前端栈,分享前端知识,嚼碎的感觉真奇妙~求关注

回复

我来回复
  • 暂无回复内容