hzzzzzzzq / Blog

这是我记录博客的地方,希望能多写一些技术博客,JS、ES、React、Vue等
14 stars 0 forks source link

深入理解JavaScript系列(9):bind 函数 #50

Open hzzzzzzzq opened 2 years ago

hzzzzzzzq commented 2 years ago

bind

bind 函数与 call,apply 的区别是什么呢?

具体对照,可以看我上面的一篇文章,《JavaScript 深入之 call 和 apply 函数》

我们来看 bind 的特点

第一版 - 返回函数

我们来看一下 bind 执行的返回效果吧:

let obj = {
  name: 'hzzzzzzzq',
  age: 18,
};
function log() {
  console.log(this.name, this.age);
}
let print = log.bind(obj);
print();
// hzzzzzzzq 18

由此,我们来写文吗的第一版代码。

Function.prototype.myBind = function (context) {
  let self = this;
  return function () {
    return self.apply(context);
  };
};

我们来看看结果,是不是一样了呢?

let obj = {
  name: 'hzzzzzzzq',
  age: 18,
};
function log() {
  console.log(this.name, this.age);
}
let print = log.myBind(obj);
print();
// hzzzzzzzq 18

这时候,我们得到了一个返回值,并且绑定。

第二版 - 传入参数

我们来看看,不仅在 bind 函数中可以传入参数,其返回的函数也可以传入参数。 我们看下面的例子:

let obj = {
  value: 1,
};
function log(name, age) {
  console.log(this.value, name, age);
}
let print = log.bind(obj, 'hzzzzzzzq');
print(18);
// 1 hzzzzzzzq 18

我们怎么加入参数呢?

  1. 我们要获取调用 bind 函数时除了第一个绑定对象以外的参数
  2. 我们要获取内部返回函数的参数
Function.prototype.myBind = function (context) {
  let self = this;
  const args = [].slice.call(arguments, 1); // 获取从第二个开始的全部参数
  return function () {
    const subArgs = [].slice.call(arguments); // 获取 bind 返回函数的内部参数
    return self.apply(context, args.concat(subArgs));
  };
};

第二版写完了。

第三版 - 作为构造函数使用的绑定函数

接下来就是构造函数啦,最难的部分了。

绑定函数自动适应于使用 new 操作符去构造一个由目标函数创建的新实例。当一个绑定函数是用来构建一个值的,原来提供的 this 就会被忽略

我们来举个例子,看一下。

var value = 2;
let obj = {
  value: 1,
};
function log(name, age) {
  this.sport = 'playing';
  console.log(this.value, name, age);
}
let print = log.bind(obj, 'hzzzzzzzq');
let obj2 = new print('18');
// undefined hzzzzzzzq 18
console.log(obj2.sport); // playing

从上面结果,我们可以看出,使用 new 进行创建新实例时,this 指向绑定的 obj 已经失效了,返回 undefined

但是为什么不会指向全局变量呢?其实就是 new 操作,让现在的 this 指向了 obj2

可以来看看 new 操作是怎么实现的,参考我的这篇文章 - 《JavaScript 深入之 new 操作》

Function.prototype.myBind = function (context) {
  let self = this;
  const args = [].slice.call(arguments, 1); // 获取从第二个开始的全部参数
  const resultFn = function () {
    const subArgs = [].slice.call(arguments); // 获取调用时传入的参数
    return self.apply(
      this instanceof resultFn ? this : context,
      args.concat(subArgs) // 拼接参数
    );
  };
  resultFn.prototype = this.prototype;
  return resultFn;
};

肯定有人会对这句代码有疑问 this instanceof resultFn ? this : context,我们来解释一下。

这里我们根据 thisresultFn 进行 instanceof 来判断是构造函数还是普通函数调用。

上面的代码真的实现了吗?

好像还不够,存在问题,那就是返回函数的值修改时,也会导致原函数值的修改。

let bindObj = log.myBind(null);
bindObj.prototype.name = 'hzzzzzzzq';
console.log(obj.prototype.name); // hzzzzzzzq

第四版 - 汇总

我们来汇总一下,我们实现 bind 函数的步骤。

我们还会对上面的问题进行优化 - 就是通过一个额外的函数进行中转。

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况
  2. 保存当前函数的引用,获取其余传入参数值
  3. 创建一个函数返回
  4. 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 thisapply 调用,其余情况都传入指定的上下文对象
Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
    throw new Error('只有函数可以调用 myBind');
  }
  const self = this;
  const args = [].slice.call(arguments, 1);
  const fn = function () {}; // 中转函数
  const resultFn = function () {
    const subArgs = [].slice.call(arguments); // 获取调用时传入的参数
    return self.apply(
      this instanceof fn ? this : context,
      args.concat(subArgs) // 拼接参数
    );
  };
  fn.prototype = self.prototype;
  resultFn.prototype = new fn();
  return resultFn;
};