来吧,一起对 TypeScript 扫盲吧!

对于前端小伙伴来说,TypeScript 肯定都不陌生,但本人之前一直对 TypeScript 了解的不多,这次决定全面学习一下 TypeScript 并总结成博客文章

废话不多说,咱直接就开始吧 👊

TypeScript 概览

  1. TypeScript 是什么?

    简单理解就是 TypeScript 是增加了类型约束的 JavaScript,并且可以被编译成原生 JavaScript。

  2. 为什么需要 TypeScript?

    a. 与弱类型的 JS 结合,在编译期间增强类型检查,提前发现可能的缺陷

    b. 通过强类型约束可以放心地进行多人协作开发,保证项目的可维护性

    c. 与代码编辑器集成,提供自动补全、引用跳转等实用功能,提升开发效率

基本用法

下面来看看 TypeScript 的基本用法

基本类型

简单类型介绍

对于简单类型呢,就是 string、number、boolean、undefined 和 null,比较基础:

typescript 复制代码
const str: string = 'hello';
const num: number = 1;
const isAfternoon: boolean = true;
let result: undefined = undefined;
let variable: null = null;
自动推断类型

在某些场景,ts 是可以自己推断出类型的,比如:

  • 初始化赋值的时候
typescript 复制代码
let myName = 'Daniel Yang';

myName = 123; // 让我们看看将数字类型赋值给 myName 会发生什么?

duang~ ts 发出了报错:👇

  • 对函数的返回值
typescript 复制代码
function greet(name: string) {
    return `Hi, My name is ${name}.`;
}

ts 会自动推断出返回值类型:

  • 存在比较明显的上下文推断
typescript 复制代码
const arr = [1, 2, 3];

在 map 方法中 ts 能推断出遍历元素的类型:

在这些场景下由于 ts 能推断出具体类型,所以是可以省略类型注释的,还能减少代码的长度😬

特别的类型

下面介绍一些特别的类型

1. any

在 ts 里 有一个很特殊的 any 类型,对于不知道具体类型 或者就是不想写类型的情况 ,可以使用 any 来声明

不过这样会导致 ts 对该变量禁用检查,丢失掉 ts 该有的作用,所以需要避免过度使用 any

2. unknown

unknown 代表着任意的值,它和 any 非常像,但由于对 unknown 进行任意操作都是不合法的,所以它比直接使用 any 更安全

typescript 复制代码
function fnWithAny(a: any) {
    a.b(); // ✅ it's OK.
}
    
function fnWithUnknown(a: unknown) {
    a.b(); // ❌ error: a is of type 'unknown'.
}

3. never

never 意味着永远不会发生,就像那年秋天,咳咳,扯远了。。。🤷‍♂️

对于抛出异常会提前终止执行的函数来说,适合对其返回类型声明为 never:

typescript 复制代码
function fail(): never {
    throw new Error('oops')
}

看起来好像没啥用🤐

但其实 never 非常适合用于防止对联合类型有遗漏使用的情况,例如:

typescript 复制代码
type Shape = 'circle' | 'square';

let shape: Shape;

switch (shape) {
    case 'circle':
        // some logic
        break;
    case 'square':
        // some logic
        break;
    default: // 按照正常逻辑是走不到 default 分支的
        const val: never = shape; // 此时 shape 为 never 类型
        break;
}

有意思的地方来了,如果有一天大家对 Shape 增加了新类型 star,但是忘记去新增 switch 的 case 分支,此时 default 分支里 ts 会报错导致代码编译不通过,将这个遗漏 case 分支的隐患暴露出来!

4. void

void 意味着函数没有返回值或不返回任何明确的值:

typescript 复制代码
function noop1(): void {
    console.log('noop')
}
    
function noop2(): void {
    console.log('Just nothing.');
    return;
}

复杂类型

接下来咱们来看下如何在 ts 里给复杂对象添加类型声明

首先来认识一下 typeinterface 关键字

1. type 类型别名

在 ts 里,我们可以使用 type 关键词来给任意类型添加命名,这样可以方便引用和复用:

typescript 复制代码
// 添加 Point 的类型别名
type Point = {
    x: number;
    y: number;
}

function printCoord(pt: Point) {
    console.log("coordinate's x and y is: ", pt.x, pt.y);
}

同时我们可以使用 & 符号将多个 type 进行组合:

typescript 复制代码
type Animal = {
    name: string;
    eat: () => void;
}

type DogAction = {
    bark: () => void;
    walk: () => void;
}

type Dog = Animal & DogAction; // 组合

let dog: Dog;
dog.walk();
2. interface 接口类型

interface 是另一种用来声明对象类型的方式:

typescript 复制代码
interface Point {
    x: number;
    y: number;
}

function printCoord(pt: Point) {
    console.log("coordinate's x and y is: ", pt.x, pt.y);
}

我们可以使用 extends 关键字对 interface 进行继承:

typescript 复制代码
interface Animal {
    name: string;
    eat: () => void;
}

interface Dog extends Animal { // 继承
    bark: () => void;
    walk: () => void;
}

let dog: Dog;
dog.walk();

既然有两种类型声明的方式,那么问题来了,typeinterface 有啥区别呢?🤔

type 和 interface 的区别

typeinterface 主要有以下几个区别:

  1. interface 只能声明对象类型,但 type 除了对象类型以外,还可以声明简单类型和 union 联合类型

    typescript 复制代码
    // 对象类型
    interface Info {
        name: string;
        desc: string;
    }
    type Info = {
        name: string;
        desc: string;
    }
    
    // type 还可以声明简单类型和联合类型
    type name = string;
    type value = string | number;
  2. interface 的重复声明可以合并,然而 type 不能重复声明:

    typescript 复制代码
    // interface 可以重复声明,声明的属性会进行合并
    interface Info {
        name: string;
    }
    
    interface Info {
        desc: string;
    }
    
    type Info = {
        name: string;
    }
    
    type Info = { // ❌ Error: type 不能重复声明
        desc: string;
    }
  3. type 和 interface 实现类型扩展的方式不同

    type 通过 & 符号进行类型合并,而 interface 通过 extends 关键词实现继承

    typescript 复制代码
    interface A {
        a: string;
    }
    
    interface B extends A {
        b: number;
    }
    
    🤗 // interface B => { a: string; b: number; }
    
    type A = {
        a: string;
    }
    
    type B = A & {
        b: number;
    }
    
    🤗 // type B => { a: string; b: number; }
3. 对象

讲完了类型声明的方式,我们来看看在 ts 里如何对对象进行类型声明,如下所示👇:

typescript 复制代码
interface Info {
    name: string;
    desc: string;
}

同时我们可以用 ?readonly 修饰符来修饰对象属性:

  • ? 是可选修饰符,意味着该属性可以不赋值

    typescript 复制代码
    type Info = {
        name: string;
        phone?: string; // phone => string | undefined
    }
  • readonly 是只读修饰符,表示该属性初始化后不能再次修改

    typescript 复制代码
    type Info = {
        readonly name: string;
    }
    
    let info: Info = {
        name: 'Daniel'
    }
    info.name = 'Tom'; // ❌ Error: Cannot assign to 'name' because it is a read-only property.

在使用可选属性前需要检查属性是否存在,否则 ts 会产生报错提示:

typescript 复制代码
function printName(obj: { first: string, last?: string }) {
    console.log(obj.last.toUpperCase()); // ❌ Error: 'obj.last' is possibly 'undefined'.

    if (obj.last !== undefined) {
        console.log(obj.last.toUpperCase()); // ✅ OK.
    }

    console.log(obj.last?.toUpperCase()); // 或者使用 JS 的 ?. 语法糖 ✅
}

对于 readonly 来说虽然不会真的改变属性的性质,但会在编译期的类型检查期间禁止属性的重新写入:

typescript 复制代码
function doSomething(obj: { readonly message: string }) {
    obj.message = 'hello'; // ❌ Error: Cannot assign to 'message' because it is a read-only property.
}

readonly 修饰符与 const 声明挺类似的,它并不意味着属性的值完全不能修改,而是指不能再重新更新属性的引用:

typescript 复制代码
type PersonalInfo = {
    readonly baseInfo: { // baseInfo 是一个对象
        name: string;
        gender: string;
        age: number;
    }
}

function getPersonalInfo(person: PersonalInfo) {
    person.baseInfo.age ++; // ✅ 可以更新它的属性值

    person.baseInfo = { // ❌ 但不能更新它的引用
        name: 'Yang',
        //...
    }
}
4. 数组

对数组来说,它的类型声明有两种方式,以字符串数组为例:

  • string[]
  • Array<string>

这两种写法的结果没有区别,只是第二种是泛型 U<T> 的写法,我们稍后再详细介绍泛型😌

与对象属性一样,我们也可以将数组声明为只读数组,同样有两种方式:

  • ReadonlyArray<string>
  • readonly string[]

这样使得数组内容不可更改:

typescript 复制代码
const arr: readonly string[] = ['apple', 'banana'];

arr[2] = 'orange'; // ❌ Error: Index signature in type 'readonly string[]' only permits reading.
arr1.push('orange'); // ❌ Error: Property 'push' does not exist on type 'readonly string[]'
5. 函数

对函数来说,需要声明类型的地方有 函数参数函数返回值,例如:

typescript 复制代码
function getMax(a: number, b: number): string {
    return `The max is ${Math.max(a, b)}`;
}

同样地,我们也可以声明可选参数和只读参数:

typescript 复制代码
function fixed(n: number, digit?: number) {
    if (digit !== undefined) {
        return n.toFixed(digit);
    }
    return n.toFixed();
}

function fn(obj: { readonly a: number }) {
    console.log('obj.a is: ', obj.a);

    obj.a ++; // ❌ Error: Cannot assign to 'a' because it is a read-only property.
}

常用类型

接下来介绍几种常用的类型

union 联合类型

联合类型是将两个以上的类型组合起来的形式,表示某个值可以是其中任意一个类型:

typescript 复制代码
function printId(id: number | string) {
    console.log('Your ID is: ', id);
}

printId(101); // ✅ OK.
printId('202'); // ✅ OK.
printId({ id: 303 }); // ❌ Error: Argument of type '{ id: number; }' is not assignable to parameter of type 'string | number'.

除了类型联合外,咱们还可以联合具体的值,这样在代码编辑器里还能方便地增加提示:

typescript 复制代码
function printText(s: string, alignment: 'left' | 'right' | 'center') {}

💣 需要注意的是,在 ts 里使用联合类型时,只有当某个属性是所有类型所共有的才可以直接用

比如某个联合类型是 string | number,如果直接使用只存在于 string 类型上的属性和方法是会喜提报错的 🙃:

typescript 复制代码
function print(val: string | number) {
    if (typeof val.split === 'function') { // ❌ Error: Property 'split' does not exist on type 'number'.
        console.log(val.split(''));
    }
}

咱就是说在使用某一类型特有的属性之前,需要通过明确的类型判断让 ts 知道变量具体的类型,这样就能正常使用类型所对应的属性和方法了

偶总结了下 => 至少有以下 几种方式 可以用来更明确地判断变量的类型:

  1. 使用 typeof 操作符

    typescript 复制代码
    function padLeft(padding: number | string, input: string) {
        if (typeof padding === 'number') { // 使用 typeof 明确变量的类型
            return ''.repeat(padding) + input;
        }
        return `${padding}${input}`;
    }
  2. 使用 in 操作符

    typescript 复制代码
    type Fish = { swim: () => void };
    type Bird = { fly: () => void };
    
    function move(animal: Fish | Bird) {
        if ('swim' in animal) { // 检查 swim 是否存在于 animal 原型链上,即是否为 Fish 类型
            animal.swim();
        } else {
            animal.fly();
        }
    }
  3. 使用 instanceof 操作符

    typescript 复制代码
    function logValue(x: Date | string) {
        if (x instanceof Date) { // 是否为 Date 类型实例
            console.log(x.toUTCString());
        } else {
            console.log(x);
        }
    }
  4. 使用自定义类型预测方法

    除了使用 JS 本身的语言能力来做,咱也可以自定义一些类型判断方法

    比如我们需要判断一个变量究竟是 Fish 类型还是 Bird 类型,可以这样写:

    typescript 复制代码
    function isFish(pet: Fish | Bird): pet is Fish {
        return (pet as Fish).swim !== undefined; // 验证下 pet 变量上是否存在 swim 属性
    }

    然后放在条件判断里就好了:

    typescript 复制代码
    if (isFish(pet)) {
        pet.swim();
    } else {
        pet.fly();
    }
enum 枚举

enum 枚举是 ts 在 js 语法之外新增的特性,它允许咱们定义一组命名常量,比如:

typescript 复制代码
enum NumericDirection {
    Up, // 默认从 0 开始,后面的变量如果没有赋值则继续加 1
    Down, // 1
    Left, // 2
    Right, // 3
}

enum StringDirection {
    UP = 'UP',
    Down = 'Down',
    Left = 'Left',
    Right = 'Right',
}

简单来说就是:

  • 数字类型的枚举默认值为 0,后面的成员如果没有赋值则继续累加 1

  • 字符类型的枚举必须要赋值

枚举成员也可以是混合类型,例如这样:

typescript 复制代码
enum MixedType {
    A, // 0
    B: 'B'
}

比较有意思的是枚举其实是真实的对象,所以在代码里可以作为值直接使用:

typescript 复制代码
enum Response {
    NO, // 0
    YES, // 1
}

// 作为类型的值
function handleResponse(type: Response, message: string) {}
handleResponse(Response.YES, 'success')

// 传递给函数参数
function fn(n: number) {
    if (n === Response.YES) {}
}

fn(Response.NO);

如果是这样,那问题又来了: ts 的枚举和 js 的对象有什么区别呢? 👻

emm... 枚举与对象主要有两点不同:

  • 数字类型的枚举会生成 反向映射,可以通过枚举的值获取到对应的键 key:

    typescript 复制代码
    enum NumericEnum {
        LEFT = 1,
        RIGHT = 2,
    }
    
    NumericEnum[NumericEnum.LEFT]; // 'LEFT'
    NumericEnum[1]; // 'LEFT'
    
    // 让我们打印下 NumericEnum 的 key
    for (const key of Object.keys(NumericEnum)) console.log(key)
    // '1'
    // '2'
    // 'LEFT'
    // 'RIGHT'
    // 。。。不是很明白为什么要这样设计?🤷‍♂️
  • 枚举成员是只读类型

    typescript 复制代码
    NumericEnum['LEFT'] = 3; // ❌ Error: Cannot assign to 'LEFT' because it is a read-only property.
Tuple 元组

介绍完枚举,我们来认识下 Tuple 元组

这名字听起来很高大上,但其实。。。它就是数组而已

不过在元组里可以混合着不同类型,比如: pair: [string, number] 这样子,它就属于元组

由于元组一般是知道元素数量和对应的类型,所以 ts 可以对元组的下标访问是否越界和具体元素的操作是否合法做检查:

typescript 复制代码
function doSomething(pair: [string, number]) {
    console.log('first value is: ', pair[0]); // ✅ It's OK.
    console.log('third value is: ', pair[2]); // ❌ Error: Tuple type '[string, number]' of length '2' has no element at index '2'.
    console.log(pair[1].split('-')); // ❌ Error: Property 'split' does not exist on type 'number'.
}

为什么说是一般呢,因为元组里可以有可选元素和扩展元素,它们会造成元组的实际长度不确定

  • 可选元素:咱可以在元素类型后面增加 ? 表示其为可选元素,需要注意可选元素只能出现在队尾

    typescript 复制代码
    type TupleArray = [number, string, boolean?];
    const arr1: TupleArray = [1, '2']; // ✅ OK.
    const arr2: TupleArray = [1, '2', true]; // ✅ OK.
  • 扩展元素:和 js 语法一样,咱可以用在类型前添加 ... 表示它是一个扩展元素:

    typescript 复制代码
    type StringNumberBooleans = [string, number, ...boolean[]]; // 表示前两个元素分别是字符和数字类型,剩下的元素都是布尔类型
    type StringBooleansNumber = [string, ...boolean[], number]; // 表示第一个和最后一个元素分别是字符和数字类型,中间的元素都是布尔类型
    type BooleansStringNumber = [...boolean[], string, number]; //  表示最后两个元素分别是字符和数字类型,前面的元素都是布尔类型

进阶用法

恭喜你,能看到这里的人都是大佬,下面让我们来学一些 ts 的进阶用法😎

函数

函数重载

如果某个函数能够以不同的参数数量和参数类型来调用,那在 ts 里该如何对该函数进行类型声明呢?

答案是 => 我们可以 定义多个函数签名

比如我们要写一个展示日期的方法,该方法可以接收一个数字类型的时间戳参数具体年、月、日三个参数,那么可以这样写函数的类型声明:

typescript 复制代码
// 函数签名
function makeDate(timestamp: number): Date;
function makeDate(year: number, month: number, day: number): Date;

// 函数实现
function makeDate(yOrTimestamp: number, month?: number, day?: number): Date {
    if (month !== undefined && day !== undefined) {
        return new Date(yOrTimestamp, month, day);
    }
    return new Date(yOrTimestamp);
}

// 函数调用
const d1 = makeDate(123456789); // ✅ OK.
const d2 = makeDate(2023, 7, 30); // ✅ OK.
const d3 = makeDate(2016, 10); // ❌ Error: No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.

⚠️不过需要强调的是,如果能用 union 联合类型声明的,就不要用重载来声明,否则会把简单问题复杂化

比如我们需要写一个返回字符串或数组长度的方法,假设使用重载来进行类型声明:

typescript 复制代码
// 函数签名
function len(s: string): number;
function len(arr: any[]): number;

// 函数实现
function len(x: any) {
    return x.length;
}

在普通调用下没有问题:

typescript 复制代码
len('hello'); // ✅ OK.
len([1, 2, 3]); // ✅ OK.

但如果像下面这样调用,ts 就会报错:

typescript 复制代码
len(Math.random() > 0.5 ? 'hello' : [1, 2, 3]); // ❌ Error: 

因为此时参数类型在编译时没法确定,不能单独匹配任意一个函数签名:

但冷静下来想一想 🤔,在这种参数数量和返回值类型都相同的情况下,直接使用 union 联合类型不香吗:

typescript 复制代码
function len(x: string | any[]): number {
    return x.length;
}

len(Math.random() > 0.5 ? 'hello' : [1, 2, 3]); // ✅ OK.

完美解决 (o゜▽゜)o☆[BINGO!]

函数泛型

接下来我们来了解下 ts 里一个比较重要的概念: 泛型

泛型是用来描述同一类型在多个值之间的关联性🌟

比如某个方法需要返回数组参数的第一个元素,虽然可以像这样写类型声明:

typescript 复制代码
function getFirstElement(arr: any[]) {
    return arr[0];
}

但这样会导致方法的返回值是 any 类型,有点简单粗暴,表达不了返回值和参数数组的关系

如果返回值的类型能明确地与数组的元素类型关联上就好了😌

此时我们就可以使用 泛型 来满足这个需求,如下:

typescript 复制代码
function getFirstElement<Type>(arr: Type[]): Type {
    return arr[0];
}

See? ! 通过在函数签名处添加一个类型参数 Type 并用在参数列表和返回值声明里,我们就在它们俩之间建立了联系

现在当我们调用函数时,返回值的类型将会与数组元素的类型一致:

Great!🥳

同时我们还可以使用 extends 关键字 对泛型增加限制

比如我们需要实现一个 在两元素中返回 length 属性最大的那个元素 方法:

typescript 复制代码
function getLonger<Type extends { length: number }>(a: Type, b: Type): Type {
    if (a.length > b.length) {
        return a;
    }
    return b;
}

这样就限制了该泛型必须具有 number 类型的 length 属性:

typescript 复制代码
getLonger(10, 20); // ❌ Error: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.
getLonger([10], [20]); // ✅ OK.

对象

索引签名 index signature

在实际项目中会存在这样一种情况: 咱不知道一个类型里所有的属性值,但巧的是咱知道属性 key 和对应值的类型

此时就可以用索引签名来进行类型声明

比如可以这样声明一个下标是数字、值是字符串的对象:

typescript 复制代码
interface StringArray {
    [index: number]: string;
}

const myArray: StringArray = getStringArray();
myArrsy[0]; // type: string;

But 只有 stringnumbersymbol 可以用作对象 key 的类型,这也符合 JS 语言中对象 key 类型的范围

如果对象的属性有不同类型,我们可以用 union 联合类型来声明值的类型:

typescript 复制代码
interface NumberOrStringDic {
    [key: string]: number | string;
    length: number;
    name: string; // ✅ It's OK.
}

最后,我们也可以给索引签名增加 readonly 前缀来防止属性被重新赋值:

typescript 复制代码
interface ReadonlyStringArray {
    readonly [index: number]: string;
}

const myArray: ReadonlyStringArray = getReadonlyStringArray();
myArray[0] = 'Daniel'; // ❌ Error: Index signature in type 'ReadonlyStringArray' only permits reading.
对象泛型

与函数一样,对象也存在泛型声明 🤪

假设有这样一个对象 Box,它有一个包含任意类型的 content 属性,讲道理我们可以这样声明:

typescript 复制代码
interface Box {
    content: any;
}

这没有问题,但使用 any 会导致 ts 对 content 属性移除了类型检查,比如:

typescript 复制代码
const box: Box = {
    content: 'string type'
}

box.content(); // 字符串不能直接作为方法调用,但此时 ts 没有及时给出报错🙁

在这种情况下我们就可以对 Box 对象进行泛型声明

可以这样理解下面的声明: Box 的 Type 就是 content 属性的类型

typescript 复制代码
interface Box<Type> {
    content: Type;
}

然后重点是 我们在引用 Box 类型的时候需要给出 Type 的具体类型,例如:

typescript 复制代码
const box: Box<string> = {
    content: 'string value'
}

这样 ts 会明确知道 box.contentstring 类型,从对 box.content 的调用做出准确的检查: 🤗

另外我们还可以用 type 来声明泛型:

typescript 复制代码
type Box<Type> = {
    content: Type;
}

同时因为 type 不仅可以声明对象类型,我们还能用 type 来声明一些泛型的辅助类型,例如:

typescript 复制代码
type OrNull<T> = T | null;

type OneOrMany<T> = T | T[];

type OneOrManyOrNull<T> = OrNull<OneOrMany<T>>;

// 应用
type OneOrManyOrNullStrings = OneOrManyOrNull<string>;

实用工具类型

文章的最后咱们来认识一些实用的工具类型吧 🔨

1. Partial<Type>

返回一个与 Type 属性相同但全被设为可选的新类型:

typescript 复制代码
interface Todo {
    title: string;
    desc: string;
}

let optionalTodo: Partial<Todo>;
/**
   {
       title?: string;
       desc?: string;
   }
*/
2. Required<Type>

与 Partial 相反,返回一个与 Type 属性相同但全被设为必填的新类型:

typescript 复制代码
interface Info {
    name?: string;
    age?: number;
}

let requiredInfo: Required<Info>;
/**
    {
       name: string;
       age: number;
    }
*/
3. Pick<Type, Keys>

从 Type 里挑出指定的 Keys 来构造一个新类型:

typescript 复制代码
interface Todo {
    title: string;
    desc: string;
    completed: boolean;
}

type TodoPreview = Pick<Todo, 'title' | 'completed'>;
/**
    {
       title: string;
       completed: boolean;
    }
*/
4. Omit<Type, Keys>

与 Pick 相反,从 Type 里移除掉指定的 Keys 来构造一个新类型:

typescript 复制代码
interface Todo {
    title: string;
    desc: string;
    completed: boolean;
}

type TodoPreview = Omit<Todo, 'desc' | 'completed'>;
/**
    {
       title: string;
    }
*/
5. Extract<UnionType, ExtractedMembers>

取 UnionType 和 ExtractedMembers 的交集来构造一个新类型:

typescript 复制代码
type T0 = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // T0: 'a'

type T1 = Extract<string | number | (() => void), Function>; // T1: () => void
6. Exclude<UnionType, ExcludedMembers>

从 UnionType 里移除掉 ExtractedMembers 存在的类型来构造一个新类型:

typescript 复制代码
type T0 = Exclude<'a' | 'b' | 'c', 'a'>; // T0: 'b' | 'c'

type T1 = Exclude<string | number | (() => void), Function>; // T1: string | number
7. NonNullable<Type>

从 Type 里移除掉 undefinednull 来构造一个新类型:

typescript 复制代码
type T0 = NonNullable<string | number | undefined | null>; // T0: string | number

参考文档

相关推荐
腾讯TNTWeb前端团队5 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰8 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪8 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪9 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy9 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom10 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom10 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom10 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom10 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom10 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试