05-函数与 Class 中的类型:详解函数重载与面向对象
课程
1
开篇:用正确的方式学习 TypeScript
已学完
学习时长: 7分24秒
2
工欲善其事:打造最舒适的 TypeScript 开发环境
学习时长: 19分14秒
3
进入类型的世界:理解原始类型与对象类型
学习时长: 37分40秒
4
掌握字面量类型与枚举,让你的类型再精确一些
学习时长: 24分53秒
5
函数与 Class 中的类型:详解函数重载与面向对象
学习时长: 50分40秒
6
探秘内置类型:any、unknown、never 与类型断言
学习时长: 34分58秒
7
类型编程好帮手:TypeScript 类型工具(上)
学习时长: 33分34秒
8
类型编程好帮手:TypeScript 类型工具(下)
学习时长: 40分52秒
9
类型编程基石:TypeScript 中无处不在的泛型
学习时长: 43分46秒
10
结构化类型系统:类型兼容性判断的幕后
学习时长: 20分48秒
11
类型系统层级:从 Top Type 到 Bottom Type
学习时长: 45分16秒
12
类型里的逻辑运算:条件类型与 infer
学习时长: 52分44秒
13
内置工具类型基础:别再妖魔化工具类型了!
学习时长: 44分47秒
14
反方向类型推导:用好上下文相关类型
学习时长: 18分37秒
15
函数类型:协变与逆变的比较
学习时长: 21分38秒
16
了解类型编程与类型体操的意义,找到平衡点
学习时长: 4分27秒
17
内置工具类型进阶:类型编程进阶
学习时长: 83分3秒
18
基础类型新成员:模板字符串类型入门
学习时长: 32分58秒
19
类型编程新范式:模板字符串工具类型进阶
学习时长: 69分27秒
20
工程层面的类型能力:类型声明、类型指令与命名空间
学习时长: 53分23秒
21
在 React 中愉快地使用 TypeScript:内置类型与泛型坑位
学习时长: 78分9秒
22
让 ESLint 来约束你的 TypeScript 代码:配置与规则集介绍
学习时长: 57分13秒
23
全链路 TypeScript 工具库,找到适合你的工具
学习时长: 25分13秒
24
说说 TypeScript 和 ECMAScript 之间那些事儿
学习时长: 29分14秒
25
装饰器与反射元数据:了解装饰器基本原理与应用
学习时长: 81分53秒
26
控制反转与依赖注入:基于装饰器的依赖注入实现
学习时长: 69分6秒
27
TSConfig 全解(上):构建相关配置
学习时长: 55分33秒
28
TSConfig 全解(下):检查相关、工程相关配置
学习时长: 76分19秒
29
基于 Prisma + NestJs 的 Node API :前置知识储备
学习时长: 44分50秒
30
基于 Prisma + NestJs 的 Node API :项目开发与基于 Heroku 部署
学习时长: 46分40秒
31
玩转 TypeScript AST:AST Checker 与 CodeMod
学习时长: 85分18秒
32
感谢相伴:是结束,也是开始
学习时长: 3分21秒
33
漫谈篇:面试中的 TypeScript
学习时长: 4分25秒
juejin_logo copyCreated with Sketch.

在前面的入门环节中,我们了解了日常开发中最常用的、基础的变量类型标注,包括原始类型、对象类型、字面量类型与枚举类型。而实际开发中还有一个重要的朋友:函数。函数能够帮助我们进一步抽离与封装代码逻辑,所以掌握函数类型必不可少。如果说函数代表着面向过程的编程,那么 Class 则代表着面向对象的编程,而它也是 ES6 新特性的重要一部分———我们终于可以和各种花式继承告别了。

这一节,我们会介绍函数与 Class 的类型标注,以及一些在 TypeScript 中独有或相比 JavaScript 更加完全的概念,如重载面向对象的编程等。函数部分,我们主要关注其参数类型、返回值类型以及重载的应用。 Class部分,除了类型以外,我们还会学习访问性修饰符、继承、抽象类等来自于面向对象理念的实际使用。

这一节之后,我们就算正式入门 TypeScript 了。此时,你已经掌握了从 JavaScript 迁移到 TypeScript 后的主要技巧,可以开始大胆地在新项目中使用 TypeScript 了。

是不是很期待?让我们赶快开始今天的课程吧!

本节代码见:Function and Class

函数

函数的类型签名

如果说变量的类型是描述了这个变量的值类型,那么函数的类型就是描述了函数入参类型与函数返回值类型,它们同样使用:的语法进行类型标注。我们直接看最简单的例子:

function foo(name: string): number {
  return name.length;
}

在函数类型中同样存在着类型推导。比如在这个例子中,你可以不写返回值处的类型,它也能被正确推导为 number 类型。

在 JavaScript 中,我们称 function name () {} 这一声明函数的方式为函数声明(Function Declaration。除了函数声明以外,我们还可以通过函数表达式(Function Expression,即 const foo = function(){} 的形式声明一个函数。在表达式中进行类型声明的方式是这样的:

const foo = function (name: string): number {
  return name.length
}

我们也可以像对变量进行类型标注那样,对 foo 这个变量进行类型声明:

const foo: (name: string) => number = function (name) {
  return name.length
}

这里的 (name: string) => number 看起来很眼熟,对吧?它是 ES6 的重要特性之一:箭头函数。但在这里,它其实是 TypeScript 中的函数类型签名。而实际的箭头函数,我们的类型标注也是类似的:

// 方式一
const foo = (name: string): number => {
  return name.length
}

// 方式二
const foo: (name: string) => number = (name) => {
  return name.length
}

在方式二的声明方式中,你会发现函数类型声明混合箭头函数声明时,代码的可读性会非常差。因此,一般不推荐这么使用,要么直接在函数中进行参数和返回值的类型声明,要么使用类型别名将函数声明抽离出来

type FuncFoo = (name: string) => number

const foo: FuncFoo = (name) => {
  return name.length
}

如果只是为了描述这个函数的类型结构,我们甚至可以使用 interface 来进行函数声明:

interface FuncFooStruct {
  (name: string): number
}

这时的 interface 被称为 Callable Interface,看起来可能很奇怪,但我们可以这么认为,interface 就是用来描述一个类型结构的,而函数类型本质上也是一个结构固定的类型罢了。

void 类型

在 TypeScript 中,一个没有返回值(即没有调用 return 语句)的函数,其返回类型应当被标记为 void 而不是 undefined,即使它实际的值是 undefined。

// 没有调用 return 语句
function foo(): void { }

// 调用了 return 语句,但没有返回值
function bar(): void {
  return;
}

原因和我们在原始类型与对象类型一节中讲到的:在 TypeScript 中,undefined 类型是一个实际的、有意义的类型值,而 void 才代表着空的、没有意义的类型值。 相比之下,void 类型就像是 JavaScript 中的 null 一样。因此在我们没有实际返回值时,使用 void 类型能更好地说明这个函数没有进行返回操作。但在上面的第二个例子中,其实更好的方式是使用 undefined :

function bar(): undefined {
  return;
}

此时我们想表达的则是,这个函数进行了返回操作,但没有返回实际的值

可选参数与 rest 参数

在很多时候,我们会希望函数的参数可以更灵活,比如它不一定全都必传,当你不传入参数时函数会使用此参数的默认值。正如在对象类型中我们使用 ? 描述一个可选属性一样,在函数类型中我们也使用 ? 描述一个可选参数:

// 在函数逻辑中注入可选参数默认值
function foo1(name: string, age?: number): number {
  const inputAge = age || 18; // 或使用 age ?? 18
  return name.length + inputAge
}

// 直接为可选参数声明默认值
function foo2(name: string, age: number = 18): number {
  const inputAge = age;
  return name.length + inputAge
}

需要注意的是,可选参数必须位于必选参数之后。毕竟在 JavaScript 中函数的入参是按照位置(形参),而不是按照参数名(名参)进行传递。当然,我们也可以直接将可选参数与默认值合并,但此时就不能够使用 ? 了,因为既然都有默认值,那肯定是可选参数啦。

function foo(name: string, age: number = 18): number {
  const inputAge = age || 18;
  return name.length + inputAge
}

在某些情况下,这里的可选参数类型也可以省略,如这里原始类型的情况可以直接从提供的默认值类型推导出来。但对于联合类型或对象类型的复杂情况,还是需要老老实实地进行标注。

对于 rest 参数的类型标注也比较简单,由于其实际上是一个数组,这里我们也应当使用数组类型进行标注:

对于 any 类型,你可以简单理解为它包含了一切可能的类型,我们会在下一节详细介绍。

function foo(arg1: string, ...rest: any[]) { }

当然,你也可以使用我们前面学习的元祖类型进行标注:

function foo(arg1: string, ...rest: [number, boolean]) { }

foo("linbudu", 18, true)

重载

在某些逻辑较复杂的情况下,函数可能有多组入参类型和返回值类型:

function func(foo: number, bar?: boolean): string | number {
  if (bar) {
    return String(foo);
  } else {
    return foo * 599;
  }
}

在这个实例中,函数的返回类型基于其入参 bar 的值,并且从其内部逻辑中我们知道,当 bar 为 true,返回值为 string 类型,否则为 number 类型。而这里的类型签名完全没有体现这一点,我们只知道它的返回值是这么个联合类型。

要想实现与入参关联的返回值类型,我们可以使用 TypeScript 提供的函数重载签名(Overload Signature,将以上的例子使用重载改写:

function func(foo: number, bar: true): string;
function func(foo: number, bar?: false): number;
function func(foo: number, bar?: boolean): string | number {
  if (bar) {
    return String(foo);
  } else {
    return foo * 599;
  }
}

const res1 = func(599); // number
const res2 = func(599, true); // string
const res3 = func(599, false); // number

这里我们的三个 function func 其实具有不同的意义:

  • function func(foo: number, bar: true): string,重载签名一,传入 bar 的值为 true 时,函数返回值为 string 类型。
  • function func(foo: number, bar?: false): number,重载签名二,不传入 bar,或传入 bar 的值为 false 时,函数返回值为 number 类型。
  • function func(foo: number, bar?: boolean): string | number,函数的实现签名,会包含重载签名的所有可能情况。

基于重载签名,我们就实现了将入参类型和返回值类型的可能情况进行关联,获得了更精确的类型标注能力。

这里有一个需要注意的地方,拥有多个重载声明的函数在被调用时,是按照重载的声明顺序往下查找的。因此在第一个重载声明中,为了与逻辑中保持一致,即在 bar 为 true 时返回 string 类型,这里我们需要将第一个重载声明的 bar 声明为必选的字面量类型。

你可以试着为第一个重载声明的 bar 参数也加上可选符号,然后就会发现第一个函数调用错误地匹配到了第一个重载声明。

实际上,TypeScript 中的重载更像是伪重载,它只有一个具体实现,其重载体现在方法调用的签名上而非具体实现上。而在如 C++ 等语言中,重载体现在多个名称一致但入参不同的函数实现上,这才是更广义上的函数重载。

异步函数、Generator 函数等类型签名

对于异步函数、Generator 函数、异步 Generator 函数的类型签名,其参数签名基本一致,而返回值类型则稍微有些区别:

async function asyncFunc(): Promise<void> {}

function* genFunc(): Iterable<void> {}

async function* asyncGenFunc(): AsyncIterable<void> {}

其中,Generator 函数与异步 Generator 函数现在已经基本不再使用,这里仅做了解即可。而对于异步函数(即标记为 async 的函数),其返回值必定为一个 Promise 类型,而 Promise 内部包含的类型则通过泛型的形式书写,即 Promise<T>(关于泛型我们会在后面进行详细了解)。

在函数这一节中,我们主要关注函数的类型标注。因为 TypeScript 中的函数实际上相比 JavaScript 也只是多在重载这一点上,我们需要着重掌握的仍然是类型标注。但在 Class 中,我们的学习重点其实更侧重于其语法与面向对象的编程理念。

Class

类与类成员的类型签名

一个函数的主要结构即是参数、逻辑和返回值,对于逻辑的类型标注其实就是对普通代码的标注,所以我们只介绍了对参数以及返回值地类型标注。而到了 Class 中其实也一样,它的主要结构只有构造函数属性方法访问符(Accessor,我们也只需要关注这三个部分即可。这里我要说明一点,有的同学可能认为装饰器也是 Class 的结构,但我个人认为它并不是 Class 携带的逻辑,不应该被归类在这里。

而对于这些结构的具体意义以及 Class 的入门语法,你可以阅读阮一峰老师的 ES6 标准入门。

属性的类型标注类似于变量,而构造函数、方法、存取器的类型编标注类似于函数:

class Foo {
  prop: string;

  constructor(inputProp: string) {
    this.prop = inputProp;
  }

  print(addon: string): void {
    console.log(`${this.prop} and ${addon}`)
  }

  get propA(): string {
    return `${this.prop}+A`;
  }

  set propA(value: string) {
    this.prop = `${value}+A`
  }
}

唯一需要注意的是,setter 方法不允许进行返回值的类型标注,你可以理解为 setter 的返回值并不会被消费,它是一个只关注过程的函数。类的方法同样可以进行函数那样的重载,且语法基本一致,这里我们不再赘述。

就像函数可以通过函数声明函数表达式创建一样,类也可以通过类声明类表达式的方式创建。很明显上面的写法即是类声明,而使用类表达式的语法则是这样的:

const Foo = class {
  prop: string;

  constructor(inputProp: string) {
    this.prop = inputProp;
  }

  print(addon: string): void {
    console.log(`${this.prop} and ${addon}`)
  }
  
  // ...
}

修饰符

在 TypeScript 中我们能够为 Class 成员添加这些修饰符:public / private / protected / readonly。除 readonly 以外,其他三位都属于访问性修饰符,而 readonly 属于操作性修饰符(就和 interface 中的 readonly 意义一致)。

这些修饰符应用的位置在成员命名前:

class Foo {
  private prop: string;

  constructor(inputProp: string) {
    this.prop = inputProp;
  }

  protected print(addon: string): void {
    console.log(`${this.prop} and ${addon}`)
  }

  public get propA(): string {
    return `${this.prop}+A`;
  }

  public set propA(value: string) {
    this.propA = `${value}+A`
  }
}

我们通常不会为构造函数添加修饰符,而是让它保持默认的 public。在扩展阅读中我们会讲到 private 修饰构造函数的场景。

如果没有其他语言学习经验,你可能不太理解 public / private / protected 的意义,我们简单做个解释。

  • public:此类成员在类、类的实例、子类中都能被访问。
  • private:此类成员仅能在类的内部被访问。
  • protected:此类成员仅能在类与子类中被访问,你可以将类和类的实例当成两种概念,即一旦实例化完毕(出厂零件),那就和类(工厂)没关系了,即不允许再访问受保护的成员

当你不显式使用访问性修饰符,成员的访问性默认会被标记为 public。实际上,在上面的例子中,我们通过构造函数为类成员赋值的方式还是略显麻烦,需要声明类属性以及在构造函数中进行赋值。简单起见,我们可以在构造函数中对参数应用访问性修饰符

class Foo {
  constructor(public arg1: string, private arg2: boolean) { }
}

new Foo("linbudu", true)

此时,参数会被直接作为类的成员(即实例的属性),免去后续的手动赋值。

静态成员

在 TypeScript 中,你可以使用 static 关键字来标识一个成员为静态成员:

class Foo {
  static staticHandler() { }

  public instanceHandler() { }
}

不同于实例成员,在类的内部静态成员无法通过 this 来访问,需要通过 Foo.staticHandler 这种形式进行访问。我们可以查看编译到 ES5 及以下 target 的 JavaScript 代码(ES6 以上就原生支持静态成员了),来进一步了解它们的区别:

var Foo = /** @class */ (function () {
    function Foo() {
    }
    Foo.staticHandler = function () { };
    Foo.prototype.instanceHandler = function () { };
    return Foo;
}());

从中我们可以看到,静态成员直接被挂载在函数体上,而实例成员挂载在原型上,这就是二者的最重要差异:静态成员不会被实例继承,它始终只属于当前定义的这个类(以及其子类)。而原型对象上的实例成员则会沿着原型链进行传递,也就是能够被继承。

而对于静态成员和实例成员的使用时机,其实并不需要非常刻意地划分。比如我会用类 + 静态成员来收敛变量与 utils 方法:

class Utils {
  public static identifier = "linbudu";

  public static makeUHappy() {
    Utils.studyWithU();
    // ...
  }

  public static studyWithU() { }
}

Utils.makeUHappy();

继承、实现、抽象类

既然说到 Class,那就一定离不开继承。与 JavaScript 一样,TypeScript 中也使用 extends 关键字来实现继承:

class Base { }

class Derived extends Base { }

对于这里的两个类,比较严谨的称呼是 基类(Base派生类(Derived。当然,如果你觉得叫父类与子类更容易理解也没问题。关于基类与派生类,我们需要了解的主要是派生类对基类成员的访问与覆盖操作

基类中的哪些成员能够被派生类访问,完全是由其访问性修饰符决定的。我们在上面其实已经介绍过,派生类中可以访问到使用 publicprotected 修饰符的基类成员。除了访问以外,基类中的方法也可以在派生类中被覆盖,但我们仍然可以通过 super 访问到基类中的方法:

class Base {
  print() { }
}

class Derived extends Base {
  print() {
    super.print()
    // ...
  }
}

在派生类中覆盖基类方法时,我们并不能确保派生类的这一方法能覆盖基类方法,万一基类中不存在这个方法呢?所以,TypeScript 4.3 新增了 override 关键字,来确保派生类尝试覆盖的方法一定在基类中存在定义:

class Base {
  printWithLove() { }
}

class Derived extends Base {
  override print() {
    // ...
  }
}

在这里 TS 将会给出错误,因为尝试覆盖的方法并未在基类中声明。通过这一关键字我们就能确保首先这个方法在基类中存在,同时标识这个方法在派生类中被覆盖了。

除了基类与派生类以外,还有一个比较重要的概念:抽象类。抽象类是对类结构与方法的抽象,简单来说,一个抽象类描述了一个类中应当有哪些成员(属性、方法等)一个抽象方法描述了这一方法在实际实现中的结构。我们知道类的方法和函数非常相似,包括结构,因此抽象方法其实描述的就是这个方法的入参类型返回值类型

抽象类使用 abstract 关键字声明:

abstract class AbsFoo {
  abstract absProp: string;
  abstract get absGetter(): string;
  abstract absMethod(name: string): string
}

注意,抽象类中的成员也需要使用 abstract 关键字才能被视为抽象类成员,如这里的抽象方法。我们可以实现(implements)一个抽象类:

class Foo implements AbsFoo {
  absProp: string = "linbudu"

  get absGetter() {
    return "linbudu"
  }

  absMethod(name: string) {
    return name
  }
}

此时,我们必须完全实现这个抽象类的每一个抽象成员。需要注意的是,在 TypeScript 中无法声明静态的抽象成员

对于抽象类,它的本质就是描述类的结构。看到结构,你是否又想到了 interface?是的。interface 不仅可以声明函数结构,也可以声明类的结构:

interface FooStruct {
  absProp: string;
  get absGetter(): string;
  absMethod(input: string): string
}

class Foo implements FooStruct {
  absProp: string = "linbudu"

  get absGetter() {
    return "linbudu"
  }

  absMethod(name: string) {
    return name
  }
}

在这里,我们让类去实现了一个接口。这里接口的作用和抽象类一样,都是描述这个类的结构。除此以外,我们还可以使用 Newable Interface 来描述一个类的结构(类似于描述函数结构的 Callable Interface):

class Foo { }

interface FooStruct {
  new(): Foo
}

declare const NewableFoo: FooStruct;

const foo = new NewableFoo();

总结与预告

在这一节,我们了解了 TypeScript 中的函数与类,它们分别代表了面向过程与面向对象的编程理念。对于函数,我们着重了解其结构体的类型,即参数类型(可选参数与剩余参数)与返回值类型的标注。而对于类,实际上我们了解的更多是新的语法,如访问性修饰符 public / private / protected ,操作修饰符 readonly ,静态成员 static ,抽象类 abstract ,以及 override 等在 JavaScript(ECMAScript)中不存在或实现并不完全的能力。

对于函数与类,你需要更多地实际使用才能掌握得更好。不妨继续对你手上的 JavaScript 进行改造,让函数与类都能披上类型的铠甲,获得完整的类型能力。

在下一节,我们将要接触的就是 JavaScript 中完全没有类似概念的新朋友了,它们是 TypeScript 类型编程最基础的一部分,包括了 any 、 unknown 、never 内置类型,以及类型断言等概念,这些类型工具会是你以后玩转类型编程时最常打交道的一部分。

扩展阅读

私有构造函数

上面说到,我们通常不会对类的构造函数进行访问性修饰,如果我们一定要试试呢?

class Foo {
  private constructor() { }
}

看起来好像没什么问题,但是当你想要实例化这个类时,一行美丽的操作就会出现:类的构造函数被标记为私有,且只允许在类内部访问

那这就很奇怪了,我们要一个不能实例化的类有啥用?摆设吗?

还真不是,有些场景下私有构造函数确实有奇妙的用法,比如像我一样把类作为 utils 方法时,此时 Utils 类内部全部都是静态成员,我们也并不希望真的有人去实例化这个类。此时就可以使用私有构造函数来阻止它被错误地实例化:

class Utils {
  public static identifier = "linbudu";
  
  private constructor(){}

  public static makeUHappy() {
  }
}

或者在一个类希望把实例化逻辑通过方法来实现,而不是通过 new 的形式时,也可以使用私有构造函数来达成目的。

你可能会想到,既然有私有构造函数,那没道理没有受保护的构造函数(protected)啊?还真有。但这里我想留给你自己去探寻,你可以先查找下这么做的意义,再想想,什么场景下我们非用它不可?

SOLID 原则

SOLID 原则是面向对象编程中的基本原则,它包括以下这些五项基本原则。

S,单一功能原则一个类应该仅具有一种职责,这也意味着只存在一种原因使得需要修改类的代码。如对于一个数据实体的操作,其读操作和写操作也应当被视为两种不同的职责,并被分配到两个类中。更进一步,对实体的业务逻辑和对实体的入库逻辑也都应该被拆分开来。

O,开放封闭原则一个类应该是可扩展但不可修改的。即假设我们的业务中支持通过微信、支付宝登录,原本在一个 login 方法中进行 if else 判断,假设后面又新增了抖音登录、美团登录,难道要再加 else if 分支(或 switch case)吗?

enum LoginType {
  WeChat,
  TaoBao,
  TikTok,
  // ...
}

class Login {
  public static handler(type: LoginType) {
    if (type === LoginType.WeChat) { }
    else if (type === LoginType.TikTok) { }
    else if (type === LoginType.TaoBao) { }
    else {
      throw new Error("Invalid Login Type!")
    }
  }
}

当然不,基于开放封闭原则,我们应当将登录的基础逻辑抽离出来,不同的登录方式通过扩展这个基础类来实现自己的特殊逻辑。

abstract class LoginHandler {
  abstract handler(): void
}

class WeChatLoginHandler implements LoginHandler {
  handler() { }
}

class TaoBaoLoginHandler implements LoginHandler {
  handler() { }
}

class TikTokLoginHandler implements LoginHandler {
  handler() { }
}

class Login {
  public static handlerMap: Record<LoginType, LoginHandler> = {
    [LoginType.TaoBao]: new TaoBaoLoginHandler(),
    [LoginType.TikTok]: new TikTokLoginHandler(),
    [LoginType.WeChat]: new WeChatLoginHandler(),

  }
  public static handler(type: LoginType) {
    Login.handlerMap[type].handler()
  }
}

L,里式替换原则一个派生类可以在程序的任何一处对其基类进行替换。这也就意味着,子类完全继承了父类的一切,对父类进行了功能地扩展(而非收窄)。

I,接口分离原则类的实现方应当只需要实现自己需要的那部分接口。比如微信登录支持指纹识别,支付宝支持指纹识别和人脸识别,这个时候微信登录的实现类应该不需要实现人脸识别方法才对。这也就意味着我们提供的抽象类应当按照功能维度拆分成粒度更小的组成才对。

D,依赖倒置原则,这是实现开闭原则的基础,它的核心思想即是对功能的实现应该依赖于抽象层,即不同的逻辑通过实现不同的抽象类。还是登录的例子,我们的登录提供方法应该基于共同的登录抽象类实现(LoginHandler),最终调用方法也基于这个抽象类,而不是在一个高阶登录方法中去依赖多个低阶登录提供方。

留言
Ctrl + Enter
全部评论(86)
后天同学的头像
删除
前端摸鱼师
打卡
点赞
回复
用户9669945973848的头像
删除
函数重载,eslint会报错已使用函数名[流泪]
点赞
回复
happydayqjq的头像
删除
使用 protected 修饰符修饰的构造函数意味着这个类只能被继承而不能被实例化。这种场景通常用于定义抽象基类,它无法被直接实例化,但是可以被其他类继承。
image
点赞
回复
plutoLam的头像
删除
切图仔
当constructor被protected修饰时,可以extends但不能实例化
点赞
回复
嘎嘎🙊的头像
删除
打卡
点赞
回复
你说万象的头像
删除
前端开发工程师
ts中经常出现一些「签名」,这个名词该怎么理解呢
点赞
2
删除
就是声明
点赞
回复
删除
谢谢
就是声明
点赞
回复
朱俊宇的头像
删除
前端开发
这里public和static一起使用是什么意思,取交集还是并集
image
点赞
回复
Zicxxciz的头像
删除
前端工程师 @ ****
打卡
点赞
回复
东东吖的头像
删除
前端工程师
1
点赞
回复
用户2362677620139的头像
删除
打卡
点赞
回复
多吃牛肉减肥的头像
删除
看来在学校搞了点后台还是有用,这些东西还是能用上
点赞
回复
NiceDayAll的头像
删除
前端开发
打卡
点赞
回复
行走的仆累的头像
删除
前端工程师 @ 顺丰科技有限公司
打卡!
点赞
回复
樱桃小锤子的头像
删除
前端 @ 一只转行的咸鱼
打卡打卡
1
回复
慢功夫的头像
删除
前端工程师
第一遍,打卡
点赞
1
删除
终于把函数类型重载的意义搞明白了,也知道了使用场景,赞
1
回复
寿司八哥的头像
删除
Web3D @ 图形起源
打卡,顺便又回顾了SOLID 哈哈
点赞
回复
__小东__的头像
删除
前端 @ 360金融
[呲牙]
点赞
回复
btbrad的头像
删除
搬砖 @ 沪郊某工地
[赞]
点赞
回复
moCha的头像
删除
前端切图仔
class编程有非常大的实践意义,作者能再补充一些示例/细化一下吗?
1
1
删除
+1
2
回复
涛涛_江的头像
删除
打卡
点赞
回复
明溪的头像
删除
抽象类和interface 都可以描述类的结构,两者有什么区别
2
1
删除
(作者)
最重要的区别就是抽象类会存在于运行时,接口不会,其次就是在描述类的结构上抽象类更专业,可以使用抽象方法这样的方式来描述结构,或者也可以提供有具体实现的方法,这都是接口不具备的能力。
5
回复
SPA枸杞泡脚盆的头像
删除
Newable Interface 来描述一个类的结构,这个例子好像不大合适啊,或者说没写完整;
class Foo { }
interface FooStruct {
new(): Foo
}

declare const NewableFoo: FooStruct;

const foo = new NewableFoo();

首先这里new了一个没有初始化的常量NewableFoo,势必报错了。
然后是不是可以举一个结合了interface,new,class constructor这几个概念的例子,
比如这个:
stackoverflow.com
展开
5
回复
沉江河的头像
删除
端水大师 @ 松花江畔
[赞]
点赞
回复
hhhhym的头像
删除
前端搬运工
老师您好,扩展里login类实现,关于LoginType的使用不太理解。LoginType应该只存在于类型空间吧?但为什么却在键里面使用了(值空间)?而且LoginType也没有定义。

然后我修改了一下实现,不知道有没有理解错。盼复
image
点赞
3
删除
(作者)
LoginType 是枚举,是会存在值空间的~
点赞
回复
删除
,,,我的错,没看到前面定义了一个枚举[捂脸]
LoginType 是枚举,是会存在值空间的~
点赞
回复
删除
这块的源码是在哪里看到的?我怎么没找到,求告知
点赞
回复
小民同学1的头像
删除
学到了,私有构造方法
image
5
3
删除
对啊,单例模式
点赞
回复
删除
单列不是只有一个吗,私有构造方法不是不能实例吗?不太懂这个怎么跟单例扯上关系了
对啊,单例模式
点赞
回复
删除
私有构造方法,可以有很多种用法,作者提出的是一种,单例模式也是一种,是两种不同的用法
单列不是只有一个吗,私有构造方法不是不能实例吗?不太懂这个怎么跟单例扯上关系了
点赞
回复
苏州前端_张不皱的头像
删除
前端开发 @ garena
咔咔咔
1
回复
guaishou的头像
删除
点赞
回复
juedui0769的头像
删除
打卡
点赞
回复
糖瓶的头像
删除
前端工程师 @ 🍬
[奋斗]
1
回复
Big_Rice的头像
删除
前端工程师 @ 未知
打卡
点赞
回复
用户5712339034441的头像
删除
虽然 Generator 作为异步解决方案基本不再使用,但是作为遍历的方法还是会使用的[看]
点赞
回复
furfurJiang的头像
删除
前端 @ 程序江
抽象类和接口感觉非常相似,可以说一下应用过程中两者的优劣吗[惊喜]
1
4
删除
(作者)
抽象类是只为了类服务的,并且在运行时也会存在,而接口更多是服务对象类型的结构描述,并且在运行时就被擦除了。理论层面的话,抽象类就是为了描述一个类应该符合怎样的一个抽象结构,比如内部抽象方法的入参与返回值类型,它的本质是一个合法的类。从类型层面说明的话,抽象类是存在于值空间的类型描述与约束,而接口则只存在于值空间。
4
回复
删除
“接口则只存在于值空间” 应该是接口则只存在于类型空间?
抽象类是只为了类服务的,并且在运行时也会存在,而接口更多是服务对象类型的结构描述,并且在运行时就被擦除了。理论层面的话,抽象类就是为了描述一个类应该符合怎样的一个抽象结构,比如内部抽象方法的入参与返回值类型,它的本质是一个合法的类。从类型层面说明的话,抽象类是存在于值空间的类型描述与约束,而接口则只存在于值空间。
1
回复
删除
(作者)
回复
噢噢噢是的,写串了
“接口则只存在于值空间” 应该是接口则只存在于类型空间?
点赞
回复
删除
也是看得愣了一下
噢噢噢是的,写串了
点赞
回复
furfurJiang的头像
删除
前端 @ 程序江
> 静态成员不会被实例继承,它始终只属于当前定义的这个类(以及其子类)
请问这个子类怎么理解?[难过]
点赞
2
删除
(作者)
就是类的子类,extends 这个类的类~
点赞
回复
删除
哦哦是我理解错了,没问题[尬笑]
就是类的子类,extends 这个类的类~
点赞
回复
布罗利_拔都的头像
删除
web前端工程师 @ 北京
这块儿 应该是给 this.prop赋值 不是this.propA(死循环了)
image
点赞
1
删除
(作者)
感谢,已修正~
点赞
回复
CharleeWa的头像
删除
Front-end Engineer @ undefined
daka
点赞
回复
hardmanhongup的头像
删除
同组件封装原则
1
回复
爱吃鱼的桶哥Z的头像
删除
伪 · 全栈打杂攻城狮
打卡~
点赞
回复
ticktock76323的头像
删除
function bar(): undefined {
return;
}
这里的undefined应该是void才对?
image
点赞
2
删除
(作者)
这里其实是想表达如果调用了 return 语句,即使没有真的去返回一个值,也最好使用 undefined 进行标注,这样在你把调用结果作为变量时能获得正确的类型,在上下文类型一节还会有解释
2
回复
删除
已经更新详细说明
这里其实是想表达如果调用了 return 语句,即使没有真的去返回一个值,也最好使用 undefined 进行标注,这样在你把调用结果作为变量时能获得正确的类型,在上下文类型一节还会有解释
1
回复
米笑🤖的头像
删除
文中講到:静态成员不会被子类或者实例继承,它始终只属于当前定义的这个类。
靜態方法不是可以被子類繼承的嗎?
点赞
1
删除
(作者)
感谢,已修正
1
回复
Old_Framer的头像
删除
web前端工程师
这个应该是类的实例的属性。
image
点赞
1
删除
(作者)
已更新
点赞
回复
user8280318367077的头像
删除
function foo(arg1: string, ...rest: any[]) { }
这句为啥不写成
function foo(arg1: string, ...xxx: any[]) { }
点赞
1
删除
不是一样的吗?都是对参数的展开
点赞
回复
Herry的头像
删除
前端开发 @ 腾讯
>比如我会用类 + 静态成员来收敛变量与 utils 方法

用这种方式定义 utils 方法 比用 const utils = { test() {} } 有什么优点么
1
3
删除
(作者)
封装性更好,比如 class UserUtils,方法和属性之间的访问通过 UserUtils.xxxx 而不是 this 或者莫非奇妙来的函数,更直观和更容易定位,还可以搭配装饰器做一些埋点啥的
1
回复
删除
请问这里的收敛是什么意思?
封装性更好,比如 class UserUtils,方法和属性之间的访问通过 UserUtils.xxxx 而不是 this 或者莫非奇妙来的函数,更直观和更容易定位,还可以搭配装饰器做一些埋点啥的
点赞
回复
删除
将一组强相关的方法、变量,收拢到一个类当中,比如 UserService.userType, UserService.addUser
请问这里的收敛是什么意思?
1
回复
zhedream的头像
删除
全栈工程师 @ @zhedream
mark: 2022年6月27日 18点33分
1
回复
占用号的头像
删除
一个派生类可以在程序的任何一处对其基类进行替换。这也就意味着,子类完全继承了父类的一切,对父类进行了功能地扩展(而非收窄)。 应该是功能的扩展吧
点赞
1
删除
(作者)
额。。其实我想表达的就是动词
点赞
回复
万少的头像
删除
头发消失师
可选参数那里应该要修改
image
点赞
1
删除
(作者)
已修正,分了两种情况
点赞
回复
codermao的头像
删除
web实习 @ 小红书
受保护的构造函数(protected)如果要使用,一般会在抽象类中使用吧,我们希望抽象类定义一些子类需要实现的方法和属性,但是抽象类本身并没有实现这些方法,也就是说如果有人实例化的抽象类本身,是并没有特别大的意义的,应该是在子类实现了这些功能以后,允许子类实例化的时候调用抽象类的构造方法[思考]
1
2
删除
应该不一定需要是抽象类,适用场景:不希望这个类被外部实例化,同时保留它可以被扩展的能力(被子类继承,子类可以实例化)[思考]
点赞
回复
删除
不可实例化的基类,其拥有的静态成员变量和方法被子类所共享
应该不一定需要是抽象类,适用场景:不希望这个类被外部实例化,同时保留它可以被扩展的能力(被子类继承,子类可以实例化)[思考]
点赞
回复
codermao的头像
删除
web实习 @ 小红书
关于抽象类哪里,为什么要说是实现一个抽象类,一般来说我们不是一个子类继承一个抽象类吗,在例如java的这种语言中都是继承的[思考]
点赞
3
删除
(作者)
因为 Java 是extends 抽象类,TS 是 implements 抽象类~
点赞
回复
删除
同问,TS不是也可以extends抽象类吗
因为 Java 是extends 抽象类,TS 是 implements 抽象类~
点赞
回复
删除
虽然可以 extends 抽象类,但一般不推荐这么做。实现一个抽象类其实就相当于实现一个接口,抽象类中的所有方法(普通方法、抽象方法)都必须被实现,但继承一个抽象类时只允许实现其中的抽象方法(因为普通方法被视为从抽象类继承了)。同时继承一个抽象类你可以像继承一个普通类那样 super.xxxMethod ,而实现并不行。从这个角度来看我个人认为继承抽象类并不是正确的使用方式。示例 playground:www.typescriptlang.org
同问,TS不是也可以extends抽象类吗
2
回复
codermao的头像
删除
web实习 @ 小红书
其实就是说ts也并没有真正的重载,增加的类型标注,其实就是为了写代码的提示性更好,真实的情况还是需要在最后的实现函数中体现,不然函数的重载也是空谈。[不失礼貌的微笑]
点赞
1
删除
是的,其实还是一个方法通过判断arguments走不同的分支实现的,要是能像java、c#那种可以每个重载方法写自己的代码就好了
点赞
回复
The action has been successful