用TS写出20个数组方法的声明

news2025/1/11 18:38:41

前言

前段时间看直播看到狼叔直播驳斥”前端已死论“,前端死没死不知道,反正前端是拿不到以前那么多工资了;好,进入正题,狼叔在直播间提到要求前端写出20个数组上的方法,这确实不太简单,但是只写出方法没有什么意义,我们今天来写20个数组方法的声明;这要求我们对于每一个方法的每一个参数用法都了解透彻;

第一步:分门别类

一口气写出20个数组方法有点难度,我们可以在脑海里对数组方法进行分类,同一类操作归为一类,这样写是不是更加简单了呢?

  1. 添加元素类:push、unshift
  2. 删除元素类:pop、shift、splice
  3. 数组转字符串类:toString、join
  4. 遍历类:forEach、reduce、reduceRight、map、filter、some、every
  5. 排序:sort
  6. 拼接:concat
  7. 索引:indexOf、lastIndexOf

一口气写了整整19个,就是不够那20个,看来我不够资格说”前端已死“,来查一查差哪些:

  1. 翻转:reverse
  2. 浅拷贝:slice

为什么写这些?因为这些是vscode中lib.es5.d.ts中定义的数组方法

第二步:实现数组接口

数组需要接收一个泛型参数,用来动态获取数组中元素类型

interface MyArray<T> {
    
}
复制代码

第三步:方法定义

首先是元素添加类方法:push、unshift,千万不要忘了他们有返回值,返回值是新数组的length

  push(...args: T[]): number;
  unshift(...args: T[]): number;
复制代码

删除元素类方法,前两个比较好写,它们的返回值都是删除的那个元素,但是需要注意的是空数组调用后返回undefined;

 pop(): T | undefined;
 shift(): T | undefined;
 /**错误的写法:splice(start: number, deleteNum: number, ...args: T[]): T[];**/
复制代码

splice这样写还有问题,因为splice只有第一个参数是必传,这样就需要写多个声明了

 splice(start: number, deleteNum?: number): T[];
 splice(start: number, deleteNum: number, ...args: T[]): T[];
复制代码

然后是数组转字符串类:toString、join,没有难度直接写

 join(param?: string): string;
 toString(): string;
复制代码

遍历类:forEach、reduce、reduceRight、map、filter、some、every 我们一个一个地来写,首先是forEach方法,这个方法我们常用的就只有回调函数,但是其实还有一个参数可以指定回调函数的this

forEach(callbackFn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;
复制代码

reduce这个方法可以实现累加器,也是我们最常用的方法之一,reduceRight与reduce的区别就在于它是从右往左遍历

 reduce(callbackFn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T;
复制代码

map方法遍历数组并且会返回一个新的数组,它是一个纯函数

  map(callbackFn: (value: T, index: number, array: T[]) => T, thisArg?: any): T[];
复制代码

后面的一些遍历方法我们就不再赘述,基本上都遵从回调函数,this绑定参数,这种固定模式

后面的一些方法都比较简单,最后把写好的方法定义都汇总起来:

interface MyArray<T> {
  length: number;
  // 数组添加元素
  push(...args: T[]): number;
  unshift(...args: T[]): number;

  // 数组删除元素
  pop(): T | undefined;
  shift(): T | undefined;
  splice(start?: number, deleteNum?: number): T[];
  splice(start: number, deleteNum?: number): T[];
  splice(start: number, deleteNum: number, ...args: T[]): T[];

  // 数组索引
  indexOf(item: T): number;
  lastIndexOf(item: T): number;

  // 数组遍历
  forEach(callbackFn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;
  reduce(callbackFn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T;
  reduceRight(callbackFn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T;
  some(callbackFn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean;
  every(callbackFn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean;
  map(callbackFn: (value: T, index: number, array: T[]) => T, thisArg?: any): T[];

  //   数组与字符串
  join(param?: string): string;
  toString(): string;
  toLocalString(): string;

  //   数组排序
  sort(callbackFn: (a: T, b: T) => number): T[];

  // 数组扁平化
  flat(deepSize: number): T[];

  //   数组的拼接
  concat(...args: T[]): T[];

  //   数组的拷贝
  slice(start?: number, end?: number): T[];

  //   数组翻转
  reverse(): T[];
}
复制代码

前面都是前奏,现在开始今天的正题,手写数组的这些方法。

第四步 实现这些方法

首先我们修改一下接口定义的名称:IMyArray,然后定义MyArray类实现该接口,编辑器会自动将上面的方法注入

class MyArray<T> implements IMyArray<T> {

}
复制代码

先实现push方法:

push(...args: T[]): number {
    const len = args.length;
    for (let i = 0; i < len; i++) {
      this[this.length++] = args[i];
    }
    return this.length;
}
复制代码

其实我们实现的是一个类数组,只不过含有数组的所有方法,这里经常会使用类数组来考察对push的理解,比如这道题:

const obj = {  
    0:1,  
    3:2,  
    length:2,  
    push:[].push  
}  
obj.push(3);
复制代码

然后实现一个splice,注意splice是一个原地修改数组的方法,所以我们不能借助额外的空间实现,这里我们还是使用Array.prototype.splice的方式来实现,类数组不能通过length属性删除元素

Array.prototype.splice = function splice(start: number, deleteNum = 1, ...rest: any[]) {
  if (start === undefined) {
    return [];
  }

  const that = this;
  let returnValue: any[] = [];
  // 将begin到end的元素全部往前移动
  function moveAhead(begin: number, end: number, step: number) {
    const deleteArr: any[] = [];
    // 可以从前往后遍历
    for (let i = begin; i < end && i + step < end; i++) {
      if (i < begin + step) {
        deleteArr.push(that[i]);
      }
      that[i] = that[i + step];
    }
    return deleteArr;
  }
  function pushAtIdx(idx: number, ...items: any[]) {
    const len = items.length;
    const lenAfter = that.length;
    // 在idx处添加len个元素,首先需要把所有元素后移len位,然后替换中间那些元素
    for (let i = idx; i < idx + len; i++) {
      if (i < lenAfter) {
        that[i + len] = that[i];
      }

      if (i - idx < len) {
        that[i] = items[i - idx];
      }
    }
  }
  if (deleteNum >= 1) {
    returnValue = moveAhead(Math.max(start, 0), that.length, deleteNum);
    that.length -= deleteNum;
  }

  pushAtIdx(start, ...rest);
  return returnValue;
};
复制代码

后面的实现我们都是用数组来实现,比如实现其中某一个遍历的方法,我们就实现比较复杂的比如reduce,reduce的实现比较简单

Array.prototype.reduce = function <T>(
  callbackFn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T,
  initialValue?: T
): T {
  // reduce如果有初始值那么以初始值开始计算,如果没有初始值那么用数组第一项作为初始值
  let startIndex = 0;
  const len = this.length;
  let ans = initialValue;
  if (initialValue === undefined) {
    ans = this[0];
    startIndex = 1;
  }

  for (let i = startIndex; i < len; i++) {
    ans = callbackFn(ans, this[i], i, this);
  }
  return ans;
};
复制代码

然后再实现一个reverse数组翻转方法,我们可以遍历前一半的数据,然后分别与后面一半进行交换,这样就完成了原地翻转:

Array.prototype.reverse = function () {
  const len = this.length;
  const that = this;
  function swap(a, b) {
    const tmp = that[a];
    that[a] = that[b];
    that[b] = tmp;
  }
  for (let i = 0; i < len >> 1; i++) {
    swap(i, len - i - 1);
  }
  return this;
};
复制代码

至于sort和flat方法这些都有很多实现方式,我们可以参考一下V8官方的文档;从文档中我们可以发现:

之前的sort方法是基于快排,并且是一种不稳定的排序算法,后来V8将sort迁移到了Torque,tq是一种特殊的DSL,利用Timsort算法实现了稳定的排序,Timsort可以看成一种稳定的归并排序

总结

我们先从数组的20个方法为切入点,研究了这些方法的ts定义,用法,顺便手写模拟了一下它们,然后对于比较复杂的sort算法我们了解了一下它的原理,显然sort算法已经不是那个以前用快排实现的不稳定的排序算法了,现在是一种稳定的排序算法,并且基于归并排序,所以归并排序我们一定要掌握好;另外这种由浅入深的学习方法,值得大家去实践;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/444602.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(补)4.13每日一题

给定一个字符串 s &#xff0c;请你找出其中不含有重复字符的 最长子串 的长度。 题目连接&#xff1a;https://leetcode.cn/problems/longest-substring-without-repeating-characters/ 解题 开始我把这个题目想简单了&#xff0c;我想的是输入一个字符串&#xff0c;从第一…

Day946.厂商定制的Android系统为什么也要解耦? -系统重构实战

厂商定制的Android系统为什么也要解耦&#xff1f; Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于厂商定制的Android系统为什么也要解耦&#xff1f;的内容。 一、Android 系统架构 AOSP&#xff0c;全称是 Android Open Source Project&#xff0c;中文译为“An…

永磁同步电机流频比(I/F)控制及Matlab/Simulink仿真分析

文章目录 前言一、流频比I/F控制原理二、永磁同步电机I/F控制系统Matlab/Simulink仿真分析2.1.仿真电路分析2.1.1 I/F控制算法2.1.2 电流环2.1.3 输出处理2.1.4 主电路 2.2 仿真结果分析 总结 前言 本章节采用流频比I/F控制方法驱动永磁同步电机的转动&#xff0c;首先分析流频…

传统机器学习(二)逻辑回归算法(一)

传统机器学习(二)逻辑回归算法(一) 1.1 算法概述 1.1.1 逻辑回归及其梯度推导 ​ 线性回归的任务&#xff0c;就是构造一个预测函数来映射输入的特征矩阵x和标签值y的线性关系&#xff0c;而构造预测函数的核心就是找出模型的参数,著名的最小二乘法就是用来求解线性回归中参…

J-Link不能连接目标MCU几点常见原因

J-Link是嵌入式软件最常用的工具之一&#xff0c;但是&#xff0c;在使用这个工具时&#xff0c;也会遇到各种各样的问题。 J-Link的连接 使用J-Link&#xff0c;首先第一步硬件连接&#xff0c;确认J-Link和PC机之间是否连接正常&#xff0c;并确认上位机能和 J-Link 建立正…

ZedGraph 绘制动态曲线

文章目录 前言&#xff1a;开发环境&#xff1a;1 下载ZedGraph 控件并设置图形界面2 功能实现3 需求升级4 小结 话不多数&#xff0c;先上一个效果图&#xff1a; 前言&#xff1a; 需要采集一些设备的数据以图表的形式展示出来&#xff0c;研究数据的走向是否平稳&#xff0…

mac Homebrew方式安装 activemq

两种方式安装 activemq 一、通过Homebrew管理安装 1. 确保homebrew可用 查看brew版本 brew -v 如果报错&#xff0c;则可能是未启用brew&#xff0c;需要安装或更新 更新并重新查看是否安装成功 brew update brew -v 2. 安装 activemq&#xff1a;下载activemq前 会先下载相…

Direct3D 12——纹理——寻址模式

可将经过常数插值或线性插值的纹理定义为一个返回向量值的函数T&#xff08;u, v&#xff09; &#xff08;r,g,b,a&#xff09;&#xff0c;即给 定纹理坐标&#xff08;u,v&#xff09;∈[0,1]^2,则上述纹理函数T将返回颜色&#xff08;r,g, b, a&#xff09;。 Direct3D允许…

MLCC周期性分析:当前时点处于周期反转前夜

MLCC是电子工业大米&#xff0c;供需波动导致行业成周期性波动 MLCC是最常用的被动元器件之一&#xff0c;终端下游涵盖消费电子、家电、汽车、通信等。在5g、汽车电子、智能硬件的推动下&#xff0c;MLCC行业需求稳步增长。供给端来看&#xff0c;中国大陆厂商合计市场份额不…

MFC加载动态gif图片文件C++语言,基于MFC的动画播放控件

MFC加载动态gif图片&#xff0c;使用VS2015环境 一、将下载的PictureEx.h和PictureEx.cpp放在工程文件的目录下&#xff0c;动态gif图片放在工程文件的res文件夹下&#xff1b;&#xff08;GIF动图下载 https://icons8.com/preloaders/en/search/move&#xff09; &#xff08…

企业级VUE前端项目各目录文件的作用

概述 本文项目是基于Vue CLI3构建工具&#xff08;基于 webpack)生成的脚手架项目。Vue CLI 现已处于维护模式&#xff0c;VUE官方推荐使用 create-vue&#xff08;基于 Vite&#xff09;构建工具。 vue-cli2.0与3.0在目录结构方面&#xff0c;有明显的不同,vue-cli3.0移除了…

Linux性能优化实战

1. TCP/IP报文详解 TCP/IP 定义了电子设备如何连入因特网&#xff0c;以及数据如何在它们之间传输的标准。协议采用了4层的层级结构&#xff0c;每一层都呼叫它的下一层所提供的协议来完成自己的需求。TCP负责发现传输的问题&#xff0c;一有问题就发出信号&#xff0c;要求重…

根据cadence设计图学习硬件知识day04了解一些芯片

1.PI3PCIE3212 &#xff08;双向信道多路复用器/多路分解器开关&#xff09; PI3PCIE3212是PCIe Gen3.0、8Gbps、4对2差分&#xff0c;PCI ExpressR 3.0性能&#xff0c;8.0Gbps 双向信道多路复用器/多路分解器开关。由于其低的位对位偏斜&#xff0c;高的通道对通道噪声隔离…

邂逅Node.js开发

目录&#xff1a; 1 Node.js是什么&#xff1f; 2 Node的应用场景 3 Node安装和管理 4 JavaScript代码执行 5 Node的输入和输出 6 Node的全局对象 node命令是可以直接运行js脚本的,在某文件夹底下只要有js文件&#xff0c;就可以通过命令提示符运行该js文件。格式是 &…

简单聊聊煤炭行业的数字化和可持续发展

煤在普通人的心目中是一种能引起复杂感情的东西。我们喜欢它在冬天给我们带来温暖&#xff0c;我们不喜欢它因为它黢黑黢黑的&#xff0c;沾在身上特别黑&#xff0c;看起来脏兮兮的。在笔者的记忆中&#xff0c;小时候煤可是生活的必需品。 小时候在冬天的河北必须要生炉子&a…

电源常识-纹波-EMI

1、纹波﹔纹波就是一个直流电压中的交流成分。直流电压本来应该是一个固定的值&#xff0c;但是很多时候它是通过交流电压整流、滤波后得来的&#xff0c;如图1,由于滤波不彻底&#xff0c;就会有剩余的交流成分&#xff0c;即使采用电池供电也会因负载的波动而产生波纹。事实上…

FreeRTOS 任务相关 API 函数

FreeRTOS 中用于创建和删除任务的 API 函数如下表所示&#xff1a; 1. 函数 xTaskCreate() 此函数用于使用动态的方式创建任务&#xff0c;任务的任务控制块以及任务的栈空间所需的内存&#xff0c; 均由 FreeRTOS 从 FreeRTOS 管理的堆中分配&#xff0c;若使用此函数&#x…

聚焦慕思欧洲设计中心,用设计谱写健康睡眠新篇章

4月20日&#xff0c;在意大利米兰&#xff0c;多位欧洲顶尖设计师齐聚ADI博物馆&#xff0c;共同见证“梦享之美”——慕思欧洲设计中心暨设计国际梦之队成立发布会的盛大召开。慕思此次发布会特地选定在米兰国际家具展期间&#xff0c;而这是公认的世界三大家具展之一&#xf…

DF竞赛平台携手嬴彻科技与清华大学智能产业研究院,助力自动驾驶挑战赛圆满落幕!

由DataFountain竞赛平台&#xff08;简称DF平台&#xff09;提供办赛支持的「首届“嬴彻-清华AIR杯”自动驾驶挑战赛&#xff1a;决策规划算法」已圆满落幕。作为一场前沿性自动驾驶类比赛&#xff0c;本次大赛立足“高速道路”和“城市道路”两大真实场景&#xff0c;选择“半…

SEO文章批量生成

SEO文章生成器 想必大部分人对于 SEO 这个词不会陌生&#xff0c;它是指一系列的优化策略&#xff0c;目的是让网站能够在搜索引擎上更容易地被检索&#xff0c;并获得更多的流量和曝光度。但是&#xff0c;SEO 的优化并非易事&#xff0c;尤其对于那些没有相关技术知识和经验…