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

简介: 基于猫狗大战奥特曼,再手写一次apply、call和bind~温故而知新,再看不会你把我头拧下来!今天刷题的时候看到一个有关 call 和 apply 的奇葩描述,觉得挺有意思的,于是重新把 call 和 apply 的逻辑手写了一遍,温故而知新~

网络异常,图片无法展示
|

基于猫狗大战奥特曼,再手写一次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));
  };
};
目录
相关文章
|
存储 算法 C++
【C++从0到王者】第三十一站:map与set(上)
【C++从0到王者】第三十一站:map与set
54 0
|
存储 编译器 C++
【C++从0到王者】第三十一站:map与set(下)
【C++从0到王者】第三十一站:map与set
51 0
|
6月前
|
C++
[C++/PTA] 狗的继承
[C++/PTA] 狗的继承
79 0
|
机器学习/深度学习
来自猫猫的深情告白--基于PaddleGAN精准唇形合成模型实现光棍节猫猫表白视频
来自猫猫的深情告白--基于PaddleGAN精准唇形合成模型实现光棍节猫猫表白视频
336 0
来自猫猫的深情告白--基于PaddleGAN精准唇形合成模型实现光棍节猫猫表白视频
|
前端开发
前端百题斩【015】——快速手撕call、apply、bind
前端百题斩【015】——快速手撕call、apply、bind
前端百题斩【015】——快速手撕call、apply、bind
|
机器学习/深度学习 数据采集 人工智能
拯救单身狗:这个对象生成器帮你看看未来对象长啥样
不知道自己未来的老婆 or 老公长什么样?来,我们先用 AI 预测出一个。
114 0
拯救单身狗:这个对象生成器帮你看看未来对象长啥样
|
安全
我是個 9 岁的女孩,我已經站上 TechCrunch 黑客马拉松展示我的 APP:Super Fun Kid Time
在美国科技博客TechCrunch主办的 TechCrunch Disrupt Hackathon中, 出现了一位年仅9岁的小女孩工程师Alexandra Jordan,现在正就读四年级的她,并不是跟着爸爸去玩的,而是为了发表自己的项目产品。
145 0
我是個 9 岁的女孩,我已經站上 TechCrunch 黑客马拉松展示我的 APP:Super Fun Kid Time
|
C# C++
由猫捉老鼠想起的——关于继承、接口和引用
用C#写了几年的网站,虽然感觉上没什么问题了,但是对于基础知识一直都是模模糊糊的,最近几天重新学习了一下基础知识,感受颇深。对于类、封装、继承、多态、接口等有了新的认识。我想说说我的想法,请大家看看对不对。
844 0
|
机器学习/深度学习 算法
由你定义吃鸡风格!CycleGAN,你的自定义风格转换大师
了解 CycleGAN 的图像风格转换并探索其在在游戏图形模块中的应用。
2094 0
|
机器学习/深度学习 算法 计算机视觉
py4CV例子1猫狗大战和Knn算法
1、什么是猫狗大战;数据集来源于Kaggle(一个为开发商和数据科学家提供举办机器学习竞赛、托管数据库、编写和分享代码的平台),原数据集有12500只猫和12500只狗,分为训练、测试两个部分。 2、什么是Knn算法: K最近邻(k-Nearest Neighbor,KNN)基本思想:如果一个样本在特征空间中的k个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别。
1590 0