prefácio
Há algum tempo, assisti à transmissão ao vivo e vi o Tio Lobo refutar a "teoria de que o front end está morto". Não sei se o front end está morto ou não. De qualquer forma, o front end não pode receber tanto salário quanto antes; bem, vamos direto ao ponto, Tio Lobo mencionou na sala de transmissão ao vivo que o front end é obrigado a escrever 20 métodos no array.
Passo 1: Classifique
É um pouco difícil escrever 20 métodos de array de uma só vez. Podemos classificar os métodos de array em nossas mentes e agrupar o mesmo tipo de operações em uma categoria. Não é mais fácil escrever dessa maneira?
Adicionar classe de elemento: push, unshift
Excluir classe de elemento: pop, shift, splice
Array para classe de string: toString, join
Traversal class: forEach, reduce, reduceRight, map, filter, some, every
Sorting: sort
Splicing: concat
Index: indexOf, lastIndexOf
Escrevi um 19 completo de uma só vez, mas não é o suficiente para o 20. Parece que não estou qualificado para dizer "o front end está morto". Vamos verificar o que está errado:
Virar:
cópia rasa reversa: fatiar
por que escrever isso? Porque esses são métodos de array definidos em lib.es5.d.ts no vscode
Etapa 2: Implementar a interface do array
O array precisa receber um parâmetro genérico para obter dinamicamente o tipo de elemento no array
interface MyArray<T> {
}
Passo 3: Definição do Método
O primeiro é o método de classe de adição de elemento: push, unshift, não esqueça que eles têm um valor de retorno e o valor de retorno é o comprimento do novo array
push(...args: T[]): number;
unshift(...args: T[]): number;
Excluir método de classe de elemento, os dois primeiros são mais fáceis de escrever e seu valor de retorno é o elemento excluído, mas deve-se observar que a matriz vazia retorna indefinido após a chamada;
pop(): T | undefined;
shift(): T | undefined;
// 错误的写法:splice(start: number, deleteNum: number, ...args: T[]): T[];
Ainda há um problema com a escrita de splice assim, porque apenas o primeiro parâmetro de splice é obrigatório, então você precisa escrever várias declarações
splice(start: number, deleteNum?: number): T[];
splice(start: number, deleteNum: number, ...args: T[]): T[];
Depois, há a classe array-to-string: toString, join, escreva diretamente sem dificuldade
join(param?: string): string;
toString(): string;
Traversal classes: forEach, reduce, reduceRight, map, filter, some, every Escrevemos um por um, o primeiro é o método forEach, neste método geralmente usamos apenas a função callback, mas na verdade existe outro parâmetro que pode especificar o this da função callback
forEach(callbackFn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;
O método de redução pode realizar o acumulador e também é um dos métodos mais usados. A diferença entre reduzirRight e reduzir é que ele percorre da direita para a esquerda
reduce(callbackFn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T;
O método map percorre o array e retorna um novo array, é uma função pura
map(callbackFn: (value: T, index: number, array: T[]) => T, thisArg?: any): T[];
Não repetiremos os seguintes métodos de passagem. Basicamente, todos eles seguem a função de retorno de chamada, este parâmetro de ligação, este modo fixo
Alguns dos métodos a seguir são relativamente simples e, finalmente, as definições de método escritas são resumidas:
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[];
}
A frente é tudo um prelúdio, agora vamos começar o tópico de hoje, esses métodos de arrays de caligrafia.
A quarta etapa implementa esses métodos
Primeiro, modificamos o nome da definição da interface: IMyArray e, em seguida, definimos a classe MyArray para implementar a interface, e o editor injetará automaticamente o método acima
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;
};
Para as seguintes implementações, usamos arrays para implementá-los. Por exemplo, para implementar um dos métodos de travessia, implementamos outros mais complexos, como o reduce. A implementação do reduce é relativamente simples.
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;
};
Em seguida, implemente um método de inversão de matriz reversa, podemos percorrer a primeira metade dos dados e, em seguida, trocar com a segunda metade, respectivamente, completando assim a inversão no local:
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;
};
Quanto aos métodos sort e flat, existem muitas maneiras de implementá-los. Podemos consultar a documentação oficial do V8; na documentação podemos encontrar:
O método de classificação anterior era baseado em quicksort e era um algoritmo de classificação instável. Posteriormente, o V8 migrou a classificação para Torque. tq é uma DSL especial que usa o algoritmo Timsort para obter uma classificação estável. O Timsort pode ser considerado uma classificação de mesclagem estável
Resumo
Começamos com os 20 métodos da matriz, estudamos a definição ts e o uso desses métodos e os simulamos por escrito, e então entendemos seu princípio para o algoritmo de classificação mais complexo. Obviamente, o algoritmo de classificação não é mais o algoritmo de classificação instável implementado pelo quicksort. Agora é um algoritmo de classificação estável e é baseado na classificação por mesclagem, portanto, devemos dominar a classificação por mesclagem; além disso, vale a pena praticar esse método de aprendizado do superficial ao profundo;
Para o título, se o front-end está morto, acho que todos têm a resposta em seus próprios corações;