使用 TypeScript 手写源码秘籍

1,137 阅读6分钟

初衷

对于这个系列,想法源于一次腾讯的面试:

手写一个 Promise,最好能够使用 TypeScript

我最后放弃用 TS ,用纯 JS 完成了题目。

目前来说,大厂面试常出现以下的特点:

  1. TypeScript 在各大厂的大型项目中应用极为广泛,几乎是进入大厂的必备素质;
  2. 常出现手写源码的问题; 如果面试中将上述两点结合起来,应该是一个挺加分的表现(也可以加深字自己对 TS 的理解),所以将在本文档种展示各种 TS 源码实现。(都使用 TS 了,代码会使用 ES6 新特性)

本项目代码详见,欢迎各位交流: github.com/zzXiongFan/…

深拷贝

深拷贝需要考虑到以下几个问题:

  1. 内部递归引用类型
  2. 循环引用问题
  3. TS: 能够正确识别类型(泛型)

看代码:

export const deepCp = (
  function () {
    // 使用 Map 记录,避免循环引用
    let cp_rec = new Map();
    // 使用 unknown 类型进行类型约束
    return function <T extends unknown>(root: T): T {
      // 首先判断对象类型: 直接返回基本类型
      if(cp_rec.has(root)) return cp_rec.get(root);
      if(typeof root !== 'object' || root === null) return root;
      // ES6 新特性,判断数组
      if(Array.isArray(root)) {
        return root.map(item => deepCp(item)) as T;
      }
      const obj: T = {} as T;
      for(let key in root) {
        obj[key] = deepCp(root[key]);
      }
      cp_rec.set(root, obj);
      return obj;
    }
  }
)();

本版本代码具有以下特点:

  1. 使用闭包解决循环引用的问题;
  2. 使用 unknown 类型,对函数内部的类型进行约束,具体用法查看此文章

call/apply

call/apply 感觉是今年手写源码最常出现的问题了,今年面试中,美团和字节各出现过一次,call/apply 的 TypeScript 稍微有点没有必要,看看官方的类型提示:

看起来好像也用了泛型,虽然感觉完全没有必要,不过为了完整模拟,就参考官方声明文件的写法进行实现。首先先贴一个 JS 版本的 call 的实现。

// 使用了 ES6 ... 语法
Function.prototype.myCall = function(thiArg, ...args) {
    thisArg.fn = this;
    let res = thisArg.fn(...args);
    delete thisArg.fn;
    return res;
}

单独把 JS 版本的实现拿出来的目的为了说明其转换到 TypeScript 中可能出现的问题,对比最终实现代码查看(读者可以自行实现看看是否存在以下问题,不要使用any):

  1. Function.prototype.myCall进行赋值时会报错:Function 的类型定义已经写好,不能直接拓展;
  2. thisArg.fn = this;会报错,因为thisArgs并不会被设置为动态对象;
  3. 如何实现泛型的定义。
// src/common.d.ts
// 解决上述问题 1 和 3
// 类型声明文件:此处对 Function 类型进行拓展, TS 会自动将声明的接口合并
declare interface Function {
  myCall: <T, A extends any[], R>(this: (this: T, ...args: A) => R, thisArg: T, ...args: A) => R;
  myApply: <T, A extends any[], R>(this: (this: T, ...args: A) => R, thisArg: T, args?: A) => R;
}
// src/02-call-apply.ts
// 采用混入的方式,注册到原型
export const MixIn = function () {
  Function.prototype.myCall = function <T, A extends any[], R>(this: (this: T, ...args: A) => R, thisArg: T, ...args: A): R {
    // 解决问题 2: 用 Object.assign 将属性复制到目标内部, 使用 交叉类型 进行约束
    let temp: T & {fn ?: typeof this} = Object.assign(thisArg, { fn: this});
    let res = temp.fn(...args);
    // 此处 temp 和 thisArg 具有相同的引用
    delete temp.fn;
    return res;
  }
  Function.prototype.myApply = function <T, A extends any[], R>(this: (this: T, ...args: A) => R, thisArg: T, args?: A): R {
    let temp: T & {fn ?: typeof this} = Object.assign(thisArg, { fn: this});
    let res = temp.fn(...args as A);
    delete temp.fn;
    return res;
  }
}

此处针对 call/apply 给出了一种 TypeScript 的实现,其中需要注意原型方法的类型定义问题(对 Function 的扩展及方法本身 this 的声明),这种实现方式还是有点不太优雅,但是能取得和原生方法几乎一样的效果,欢迎大家给出更完美的实现方法。

bind

bind方法:

会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN )

同样的,首先看看纯 JS 的实现:

Function.prototype.mybind = function(ctx, ...args) {
  if(typeof this !== 'function') throw Error('not a function');
  let fn = this;
  let rec_args = args;
  let resFn = function(...args) {
    // 确保 new 调用的时候的正确性
    return fn.apply(this instanceof resFn? this : ctx, rec_args.concat(args));
  }
  
  // 保证原型链的传递
  resFn.prototype = Object.create(this.prototype);

  return resFn;
}

其实 bind 的 Typescript 实现挺麻烦而且没有必要,但是在整个实现中,可以对 TS泛型和重载 能有更深的了解,看看官方的 bind 的类型设计: 可以看到,官方的 bind 有5种重载,分别针对不传入额外的参数,传入1-4个参数,传入更多参数的类型判断情况,下面实现这类的方法的重载接口定义:

// 提取 This 的类型:若不存在 this 参数,则为 unknown
type ThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any ? U : unknown;
// 返回 去除函数对象的 this 参数后的 函数类型
type OmitThisParameter<T> = unknown extends ThisParameterType<T> ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T;

declare interface Function {
  // bind 定义 传入参数个数从0 - 4 个分别进行定义
  // 当 bind 仅传入 thisArgs 参数时,需要将函数类型的定义传递下去
  mybind<T>(this: T, thisArg: ThisParameterType<T>): OmitThisParameter<T>;
  // bind 传入 1 - 4 个参数时的类型传递
  mybind<T , A0, A extends any, R>(this: (this: T, arg0: A0, ...args: A) => R, thisArg: T, arg0: A0): (...args: A) => R;
  mybind<T , A0, A1, A extends any, R>(this: (this: T, arg0: A0, arg1: A1, ...args: A) => R, thisArg: T, arg0: A0, arg1: A1): (...args: A) => R;
  mybind<T , A0, A1, A2, A3, A extends any, R>(this: (this: T, arg0: A0, arg1: A1, arg2: A2, arg3: A3, ...args: A) => R, thisArg: T, arg0: A0, arg1: A1, arg2: A2, arg3: A3,): (...args: A) => R;
  mybind<T , A0, A1, A2, A extends any, R>(this: (this: T, arg0: A0, arg1: A1, arg2: A2, ...args: A) => R, thisArg: T, arg0: A0, arg1: A1, arg2: A2): (...args: A) => R;
  // mybind<T , AX extends any[], R>(this: (this: T, ...args: AX) => R, thisArgs: T, ...args: AX): (...args: AX) => R;
  mybind<T, AX, R>(this: (this: T, ...args: AX[]) => R, thisArg: T, ...args: AX[]): (...args: AX[]) => R;
}

这个地方,泛型的定义相对而言比较复杂,最复杂的是第一种,需要反解出函数的返回类型,并剔除 this 参数,由此实现5种重载的定义,下面看看 bind 实体的定义,泛型和重载定义好了, bind 本体其实和 TS 本身的实现并没有区别:

export const MixIn = function () {
  // 直接使用通用类型进行 实现
  Function.prototype.mybind = function <T, AX, R>(this: (this: T, ...args: AX[]) => R, thisArg: T, ...args: AX[]): (...args: AX[]) => R {
    // 使用 自执行函数 构建闭包
    let rec_args = args;
    let ctx = this;
    let resFn: (...args: AX[]) => R = function(...args: AX[]): R {
      // 这个地方实在是绕不开了
      // @ts-ignore
      return ctx.apply(this instanceof resFn ? this : thisArg, [...rec_args, ...args]);
    }

    resFn.prototype = Object.create(this.prototype);
    return resFn
  }
}

new

有了前面的 apply/call 基础,实现 new 的 TypeScript 的写法相当容易,唯一有坑的地方就是如何写 new 的类型定义?直接上代码:

// 提取 函数泛型中的 参数类型
type FuncParameterType<T> = T extends (...args: infer A) => any ? A : unknown;
// 此处使用了 ReturnType 做返回值判断,满足 构造函数返回对象时的问题。
type INew = <T extends (...args: any[]) => any>(func: T, ...args: FuncParameterType<T>) => ReturnType<T> extends (Object | Function) ? ReturnType<T> : Object; 
export const myNew:INew = function (func, ...args) {
  // 进行原型链的关联,确保 new 后的结果延续原型链
  let res: {__proto__?: typeof func.prototype} = {};
  res.__proto__ = func.prototype;
  let func_res =  func.apply(res, args);
  // 对返回值进行判断
  if(func_res instanceof Object || func_res instanceof Function) {
    return func_res;
  }
  return res;
}

以上就是 new 的 TS 写法,在类型定义中有一个问题,希望有缘人能给解答下,下面这两种泛型的写法,有什么不一样吗:

// 方法1:
type FuncParamsType<T> = T extends (...args: infer A) => any ? A : unknown;
type IFunc1<T> = (func: T, ...args: FuncParamsType<T>) => ReturnType<T> extends (Object | Function) ? ReturnType<T> : Object;

// 方法2:
type Ifunc2<A extends any[], R> = (func: (...args: A) => R, ...args: A) => R extends (Object | Function) ? R : Object;

感觉从功能上应该有没有区别的,但是在函数实习实现的时候第二种方法会报错