手写call、apply及bind函数

2022/8/12 javascript

call、apply 及 bind 函数内部实现是怎么样的?

首先从以下几点考虑如何实现这几个函数

  • 不传入第一个参数,那么上下文默认为 window
  • 改变 this 指向,让新的对象可以执行改函数,并能接受参数。

# 先来手撸 call

call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

Function.prototype.qlCall = function (context) {
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  if (!context) {
    context = typeof window !== "undefined" ? window : global;
  }

  /**暴露处理 context 有可能传的不是对象*/
  context = Object(context);
  /**使用 Symbol 值命名 防止与context中的属性名冲突 */
  const fnName = Symbol("key");
  context[fnName] = this;
  const args = [...arguments].slice(1);
  const result = context[fnName](...args);
  delete context[fnName];

  return result;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

分析 call 的实现原理

  • 首先 context 为可选参数,如果不传的话默认上下文为 window | global(兼容 node.js)

  • 接下来就是 通过 Object构造函数创建一个包装类对象,避免传入的context不是对象。

  • 使用 Symbol值值命名需要被调用的函数名称防止与 context 中的属性名冲突。

  • 将调用的函数赋值给 context[ fnName ]

  • 因为call可以传入对个参数作为调用函数的参数,所以使用数组的 slice方法 浅拷贝传入的多个参数。

  • 最后调用函数并使用展开运算符入参,之后再删除context上的函数

# 手撸 apply

apply() 方法调用一个具有给定 this 值的函数,以及以一个数组(或一个类数组对象)的形式提供的参数。

Function.prototype.qlApply = function (context) {
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  if (!context) {
    context = typeof window !== "undefined" ? window : global;
  }

  /**暴露处理 context 有可能传的不是对象*/
  context = Object(context);
  /**使用 Symbol 值命名 防止与context中的属性名冲突 */
  const fnName = Symbol("key");
  context[fnName] = this;

  /**call 和 apply 的区别就是处理参数 */
  let result;
  if (arguments[1]) {
    result = context[fnName](...arguments[1]);
  } else {
    result = context[fnName]();
  }
  delete context[fnName];
  return result;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

分析 apply 的实现原理

  • 基本和 call 的实现是一样的,除了处理参数和call有区别

  • call 接受一个参数列表,但 apply 接受一个参数的单数组

# 手撸 bind

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

bind的实现对比其他两个函数略微地复杂了一点,因为bind需要返回一个函数,需要判断一些边界问题

比如 我们可能会 通过 new Func()调用返回的函数,或者是普通的 func() 调用。

我们先分析一下 new 执行的过程中会发现什么:

  1. 自动创建一个新对象
  2. 新对象把它的[[prototype]]关联到构造函数的原型上
  3. 将构造函数中的 this 绑定到新建的对象上
  4. 根据构造函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理。
function qlNew(func, ...args) {
  if (typeof func !== "function") {
    throw "func must be a function";
  }
  // 1.创建一个新对象
  // const obj = {};
  // 2.新对象原型指向构造函数原型对象
  // obj.__proto__ = func.prototype;

  /**
   * 1,2步还可以更简化处理
   */
  let obj = Object.create(func.prototype);
  /** Object.create 实际模拟如下
   * let Ctor = function () {}
   * Ctor.prototype = func.prototype
   * Ctor.prototype = func.prototype
   * let obj = new Ctor()
   */

  // 3.将构建函数的this指向新对象
  let result = func.apply(obj, args);
  // 4.根据返回值判断
  const flag =
    (typeof result === "Object" && result !== null) ||
    typeof result === "function";

  return flag ? result : obj;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Function.prototype.qlBind = function (context, ...args) {
  if (typeof this !== "function") {
    throw new TypeError("bind must be called on a function");
  }

  const executeBound = function (
    sourceFunc,
    boundFunc,
    context,
    callingContext,
    args
  ) {
    if (!(callingContext instanceof boundFunc)) {
      // 调用方式不是 new Func 的形式就直接调用 sourceFunc,并且给对应的参数即可
      return sourceFunc.apply(context, args);
    } else {
      // 下面就是处理 new Func() 的调用形式了
      const self = Object.create(sourceFunc.prototype);
      const result = sourceFunc.apply(self, args);
      const flag =
        (typeof result === "Object" && result !== null) ||
        typeof result === "function";

      return flag ? result : obj;
    }
  };

  const func = this;
  const bound = function (...innerArgs) {
    /**
     * 因为 bind 的普通调用的传参可能是这样的 func.bind(obj, 1)(2) 所以入参是需要
     * args.concat(...innerArgs) 这样就能将两边的参数拼接起来了。
     */
    return executeBound(func, bound, context, this, args.concat(...innerArgs));
  };

  return bound;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Last Updated: 2022/8/28 下午3:19:34