初衷
对于这个系列,想法源于一次腾讯的面试:
手写一个 Promise,最好能够使用 TypeScript
我最后放弃用 TS ,用纯 JS 完成了题目。
目前来说,大厂面试常出现以下的特点:
- TypeScript 在各大厂的大型项目中应用极为广泛,几乎是进入大厂的必备素质;
- 常出现手写源码的问题; 如果面试中将上述两点结合起来,应该是一个挺加分的表现(也可以加深字自己对 TS 的理解),所以将在本文档种展示各种 TS 源码实现。(都使用 TS 了,代码会使用 ES6 新特性)
本项目代码详见,欢迎各位交流: github.com/zzXiongFan/…
深拷贝
深拷贝需要考虑到以下几个问题:
- 内部递归引用类型
- 循环引用问题
- 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;
}
}
)();
本版本代码具有以下特点:
- 使用闭包解决循环引用的问题;
- 使用
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):
Function.prototype.myCall进行赋值时会报错:Function 的类型定义已经写好,不能直接拓展;thisArg.fn = this;会报错,因为thisArgs并不会被设置为动态对象;- 如何实现泛型的定义。
// 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;
感觉从功能上应该有没有区别的,但是在函数实习实现的时候第二种方法会报错