Typescript class中的方法和函数类型的属性有何不同?
在ES5中对象是属性名和属性值的集合,对象的属性值可以是基本类型也可以是函数类型,
例如:let obj = { name: ‘jason’, say: function() { console.log(‘hello’) } } // 字面量定义obj对象
在ES6中对象的属性如果是一个函数则有了新的写法,允许省略function关键字
例如:let obj = { name: ‘jason’, say() { console.log(‘hello’) } } // 字面量定义obj对象
两种say的写法在调用时没有任何区别,都可以通过obj.say()正确调用
那么在ts中是否有区别呢???
一、class中的方法和函数
先来看一段代码:
// index.ts
abstract class A {
abstract execute: () => string;
}
class B extends A {
execute() {
return B executed;
}
}
思考一下,上面代码是否会报错??
在上面代码中,抽象类 A 声明了一个抽象的成员 execute,并且它被声明为一个属性(execute: () => string;)。这意味着 A 预期的 execute 是一个函数类型的属性,而不是一个方法。具体来说,这种写法是将 execute 作为一个函数类型的属性,而不是一个实例方法。
而在类 B 中,你定义了 execute 为一个方法(execute() {…})。这就导致了类型不匹配的错误。因为 B 中的 execute 被 TypeScript 解释为一个方法,而不是一个属性。
所以答案是会报错!
确实,方法和函数类型的属性在调用时看起来是相似的,因为它们都可以在实例上被调用。但它们在类型系统中的含义和行为有一些重要的区别。我们可以从以下几个方面来探讨它们之间的不同:
1. 语法上的差异
-
方法(Method):在类中定义的方法是通过常规的方法声明语法进行定义的。例如:
class A { execute(): string { return "Hello, world!"; } }
这里
execute()
是类A
的一个实例方法。 -
函数类型的属性(Function Property):函数类型的属性是通过将方法声明为一个函数类型的属性来定义的。例如:
class A { execute: () => string = () => { return "Hello, world!"; }; }
这里
execute
是类A
的一个属性,它是一个函数类型。它的类型是() => string
,并且在类内部通过赋值来实现。
2. 类中方法与函数类型属性的区别
-
方法:在类的实例中,方法会被作为该类的原型方法(prototype method)来定义。当你调用方法时,它是通过该实例的原型链来查找的。这使得它们可以在类的所有实例中共享。
-
函数类型的属性:函数类型的属性是在实例对象上直接定义的。每个实例会有自己的一份这个属性。因此,
execute
是作为实例的一个成员存在的,而不是共享在原型上的。这意味着每个实例可以拥有自己的execute
函数,且它们不会共享相同的引用。
举个例子:
class A {
execute: () => string = () => "Hello";
}
const a1 = new A();
const a2 = new A();
console.log(a1.execute === a2.execute); // 输出: false
在上面的代码中,每个实例 a1
和 a2
都有各自独立的 execute
函数,因此 a1.execute
和 a2.execute
并不相等。
3. 继承时的差异
当你继承一个类时,方法和函数类型属性的行为会有所不同:
-
继承方法:在继承时,方法会被共享给所有的子类实例,因此如果你在子类中覆盖了父类的方法,它会替换掉原有的实现。
-
继承函数类型的属性:如果在父类中使用了函数类型的属性,那么子类的实例会拥有自己独立的函数实现,并且继承过程中不会自动覆盖父类的函数实现。
举个例子:
class A {
execute: () => string = () => "Hello from A";
}
class B extends A {
execute: () => string = () => "Hello from B";
}
const a = new A();
const b = new B();
console.log(a.execute()); // "Hello from A"
console.log(b.execute()); // "Hello from B"
在这个例子中,A
和 B
都有 execute
函数,但它们分别拥有独立的实现。
4. 调用时的差异
在调用时,方法和函数类型属性的调用方式是一样的:
const a = new A();
console.log(a.execute()); // 调用 execute 方法
无论是方法还是函数类型的属性,调用的语法都是一样的。不过,内部的机制(如原型链与实例属性)有所不同。
5. 类型签名的不同
在 TypeScript 中,方法和函数类型属性的类型签名也有所区别:
-
方法的类型签名:在类的方法声明中,方法的类型是
() => string
或类似的类型,可以根据方法的参数签名进行调整。 -
函数类型属性的类型签名:如果你使用函数类型的属性,TypeScript 会将它视为一个属性,该属性的类型是一个函数类型,而这个函数在类型系统中被看作是一个普通的属性。
这意味着 TypeScript 会区分方法和函数类型的属性,在设计时也会做出不同的类型检查。
总结
虽然在调用时它们看起来很像(都可以直接通过 instance.execute()
来调用),但方法和函数类型属性在内部的工作机制上有所不同:
- 方法 是类的原型的一部分,所有实例共享同一份方法定义。
- 函数类型属性 是实例的一部分,每个实例有自己独立的属性值。
这两者在类型系统、继承行为、以及实例化后如何管理的方面都存在区别。所以即使在调用时相似,背后的设计和实现有所不同。
再来看看开头的那段代码,如果想要ts不报错该如何修改呢?
有两种方法
- 修改 A 中 execute 的声明方式,声明为方法
abstract class A {
abstract execute(): string;
}
class B extends A {
execute() {
return `B executed`;
}
}
这种方式中,A 中的 execute 声明成了一个方法,不再是一个属性。这样,B 中的 execute 就与 A 中的声明匹配了。
- 保持 A 中 execute 为属性类型,修改 B 中的 execute 为属性类型
abstract class A {
abstract execute: () => string;
}
class B extends A {
execute = () => {
return `B executed`;
}
}
这种方式中,将 B 中的 execute 定义为一个属性,并且赋予它一个函数值。这样也可以确保类型一致。
二、字面量类型对象的方法和函数
熟悉了class中的函数和方法,再来看看下面代码:
interface D {
show: boolean;
sleep():string;
say: () => string;
}
const d: D = {
show: true,
sleep: () => 'yes',
say() {
return 'hello world!';
}
}
上面的代码ts会报错吗??
上面代码中,sleep
和 say
似乎在类型声明上确实有不同的表现形式,但 TypeScript 并不会报错,原因在于 TypeScript 允许方法和函数类型的属性在某些情况下可以互换。
为什么没有报错
-
sleep
是一个方法:sleep(): string;
这是一个方法类型的声明,要求
sleep
必须是一个没有参数并返回string
的方法。在d
中,sleep
被声明为一个函数:sleep: () => 'yes',
TypeScript 允许方法的类型声明与函数类型的属性之间的一些自由转换。在这种情况下,
sleep: () => 'yes'
是一个符合() => string
的函数类型的表达式,它也符合方法类型的要求。因此,sleep
作为方法被使用时,TypeScript 认为它是有效的,因为它符合类型声明中的函数签名。 -
say
是一个函数类型的属性:say: () => string;
这是一个函数类型的属性,它要求
say
必须是一个返回string
的函数。然后在d
中,say
被定义为一个方法:say() { return 'hello world!'; }
这里,
say
被定义为一个普通的类方法,这也是 TypeScript 允许的。因为方法say()
在语法上符合() => string
的函数类型,它可以被当作函数类型的属性来处理。
TypeScript 在处理函数类型的属性和方法时,存在一些灵活性。虽然声明方式不同,TypeScript 允许你在对象中使用方法和函数类型的属性互换。原因如下:
-
方法是函数类型的一个特殊形式:在 JavaScript 中,方法本质上是对象的属性,而这个属性的值是一个函数。因此,
say
作为方法,也符合() => string
的签名,所以 TypeScript 允许它作为say
的实现。 -
函数类型的属性也可以是方法:即使
say
在类型声明中是() => string
(函数类型的属性),在d
中,say
作为方法定义同样符合函数类型() => string
的要求。方法say()
被编译器视为返回类型为string
的函数,因此也符合类型() => string
的约束。
关键点
-
方法和函数类型的属性:在 TypeScript 中,方法本质上也是一种函数类型的属性,尤其是在类和接口中。因此,
sleep
和say
都可以是有效的,因为 TypeScript 会允许方法和函数类型的属性在一些情况下进行互换。 -
灵活的类型系统:TypeScript 对方法和函数属性的检查是宽松的,在实践中,它会识别方法和函数类型属性之间的相似性,因此不会在这种情况下抛出错误。
小结
TypeScript 的类型系统允许方法和函数类型属性在某些场景下互换。方法本质上是一个函数类型的属性,只不过它的语法和特性有一些不同。所以即使 sleep
和 say
在声明上有所不同,TypeScript 认为它们都符合接口 D
的要求,因此不会报错。
注意:编辑器会对方法和函数进行区分
- webstorm
- vscode