-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
TypeScript 之 Class(上) #232
Comments
return s.toLowercse() === "ok"; 这里拼写错误“toLowerCase” |
hh,不是拼写错误,认真看看上下文!! |
class Point { |
强迫症,在 implements 部分的例子中,NameChecker 缺了个 |
在箭头函数那里的第三条,不能在派生中使用super.getName,因为原型链中并没有入口可以获取该基类方法,刚开始不能理解这句话。 后来写了下代码,如下 class MyClass1 {
name = "MyClass1";
getName = () => {
return this.name;
};
constructor() {
this.name = "MyClass1";
// MyClass1.prototype.getName = this.getName;
}
}
class Derived1 extends MyClass1 {
testGetName = () => {
// TypeError: (intermediate value).getName is not a function.
// 此时的getName其实是实例的函数,在原型链上并不存在
// 但是可以通过给原型添加该属性,来使得super.getName()可用
return super.getName();
};
}
const d = new Derived1();
console.log(d.testGetName()); 发现确实不行,想了下才意识到,在class中的方法是会被添加到MyClass1.prototype上的,而属性则会作为实例的属性,在构造时进行添加,也就是添加到实例上去的,原型上并不存在这个属性,可不就是没有入口可以获取该基类方法么。 因此,如果想绕过这个限制,就可以在构造函数中给原型添加这个属性来实现super调用。也就是添加上面注释掉的 MyClass1.prototype.getName = this.getName, 这段代码 不知道理解有没有错误,欢迎大家指教。 |
类(Classes)
TypeScript 完全支持 ES2015 引入的
class
关键字。和其他 JavaScript 语言特性一样,TypeScript 提供了类型注解和其他语法,允许你表达类与其他类型之间的关系。
类成员(Class Members)
这是一个最基本的类,一个空类:
这个类并没有什么用,所以让我们添加一些成员。
字段(Fields)
一个字段声明会创建一个公共(public)可写入(writeable)的属性:
注意:类型注解是可选的,如果没有指定,会隐式的设置为
any
。字段可以设置初始值(initializers):
就像
const
、let
和var
,一个类属性的初始值会被用于推断它的类型:--strictPropertyInitialization
strictPropertyInitialization 选项控制了类字段是否需要在构造函数里初始化:
注意,字段需要在构造函数自身进行初始化。TypeScript 并不会分析构造函数里你调用的方法,进而判断初始化的值,因为一个派生类也许会覆盖这些方法并且初始化成员失败:
如果你执意要通过其他方式初始化一个字段,而不是在构造函数里(举个例子,引入外部库为你补充类的部分内容),你可以使用明确赋值断言操作符(definite assignment assertion operator)
!
:readonly
字段可以添加一个
readonly
前缀修饰符,这会阻止在构造函数之外的赋值。构造函数(Constructors)
类的构造函数跟函数非常类似,你可以使用带类型注解的参数、默认值、重载等。
但类构造函数签名与函数签名之间也有一些区别:
Super 调用(Super Calls)
就像在 JavaScript 中,如果你有一个基类,你需要在使用任何
this.
成员之前,先在构造函数里调用super()
。忘记调用
super
是 JavaScript 中一个简单的错误,但是 TypeScript 会在需要的时候提醒你。方法(Methods)
类中的函数属性被称为方法。方法跟函数、构造函数一样,使用相同的类型注解。
除了标准的类型注解,TypeScript 并没有给方法添加任何新的东西。
注意在一个方法体内,它依然可以通过
this.
访问字段和其他的方法。方法体内一个未限定的名称(unqualified name,没有明确限定作用域的名称)总是指向闭包作用域里的内容。Getters / Setter
类也可以有存取器(accessors):
TypeScript 对存取器有一些特殊的推断规则:
get
存在而set
不存在,属性会被自动设置为readonly
从 TypeScript 4.3 起,存取器在读取和设置的时候可以使用不同的类型。
索引签名(Index Signatures)
类可以声明索引签名,它和对象类型的索引签名是一样的:
因为索引签名类型也需要捕获方法的类型,这使得并不容易有效的使用这些类型。通常的来说,在其他地方存储索引数据而不是在类实例本身,会更好一些。
类继承(Class Heritage)
JavaScript 的类可以继承基类。
implements
语句(implements
Clauses)你可以使用
implements
语句检查一个类是否满足一个特定的interface
。如果一个类没有正确的实现(implement)它,TypeScript 会报错:类也可以实现多个接口,比如
class C implements A, B {
注意事项(Cautions)
implements
语句仅仅检查类是否按照接口类型实现,但它并不会改变类的类型或者方法的类型。一个常见的错误就是以为implements
语句会改变类的类型——然而实际上它并不会:在这个例子中,我们可能会以为
s
的类型会被check
的name: string
参数影响。实际上并没有,implements
语句并不会影响类的内部是如何检查或者类型推断的。类似的,实现一个有可选属性的接口,并不会创建这个属性:
extends
语句(extends
Clauses)类可以
extend
一个基类。一个派生类有基类所有的属性和方法,还可以定义额外的成员。覆写属性(Overriding Methods)
一个派生类可以覆写一个基类的字段或属性。你可以使用
super
语法访问基类的方法。TypeScript 强制要求派生类总是它的基类的子类型。
举个例子,这是一个合法的覆写方法的方式:
派生类需要遵循着它的基类的实现。
而且通过一个基类引用指向一个派生类实例,这是非常常见并合法的:
但是如果
Derived
不遵循Base
的约定实现呢?即便我们忽视错误编译代码,这个例子也会运行错误:
初始化顺序(Initialization Order)
有些情况下,JavaScript 类初始化的顺序会让你感到很奇怪,让我们看这个例子:
到底发生了什么呢?
类初始化的顺序,就像在 JavaScript 中定义的那样:
这意味着基类构造函数只能看到它自己的
name
的值,因为此时派生类字段初始化还没有运行。继承内置类型(Inheriting Built-in Types)
在 ES2015 中,当调用
super(...)
的时候,如果构造函数返回了一个对象,会隐式替换this
的值。所以捕获super()
可能的返回值并用this
替换它是非常有必要的。这就导致,像
Error
、Array
等子类,也许不会再如你期望的那样运行。这是因为Error
、Array
等类似内置对象的构造函数,会使用 ECMAScript 6 的new.target
调整原型链。然而,在 ECMAScript 5 中,当调用一个构造函数的时候,并没有方法可以确保new.target
的值。 其他的降级编译器默认也会有同样的限制。对于一个像下面这样的子类:
你也许可以发现:
undefined
,所以调用sayHello
会导致错误instanceof
失效,(new MsgError()) instanceof MsgError
会返回false
。我们推荐,手动的在
super(...)
调用后调整原型:不过,任何
MsgError
的子类也不得不手动设置原型。如果运行时不支持Object.setPrototypeOf
,你也许可以使用__proto__
。不幸的是,这些方案并不会能在 IE 10 或者之前的版本正常运行。解决的一个方法是手动拷贝原型中的方法到实例中(就比如
MsgError.prototype
到this
),但是它自己的原型链依然没有被修复。成员可见性(Member Visibility)
你可以使用 TypeScript 控制某个方法或者属性是否对类以外的代码可见。
public
类成员默认的可见性为
public
,一个public
的成员可以在任何地方被获取:因为
public
是默认的可见性修饰符,所以你不需要写它,除非处于格式或者可读性的原因。protected
protected
成员仅仅对子类可见:受保护成员的公开(Exposure of protected members)
派生类需要遵循基类的实现,但是依然可以选择公开拥有更多能力的基类子类型,这就包括让一个
protected
成员变成public
:这里需要注意的是,如果公开不是故意的,在这个派生类中,我们需要小心的拷贝
protected
修饰符。交叉等级受保护成员访问(Cross-hierarchy protected access)
不同的 OOP 语言在通过一个基类引用是否可以合法的获取一个
protected
成员是有争议的。在 Java 中,这是合法的,而 C# 和 C++ 认为这段代码是不合法的。
TypeScript 站在 C# 和 C++ 这边。因为
Derived2
的x
应该只有从Derived2
的子类访问才是合法的,而Derived1
并不是它们中的一个。此外,如果通过Derived1
访问x
是不合法的,通过一个基类引用访问也应该是不合法的。看这篇《Why Can’t I Access A Protected Member From A Derived Class?》,解释了更多 C# 这样做的原因。
private
private
有点像protected
,但是不允许访问成员,即便是子类。因为
private
成员对派生类并不可见,所以一个派生类也不能增加它的可见性:交叉实例私有成员访问(Cross-instance private access)
不同的 OOP 语言在关于一个类的不同实例是否可以获取彼此的
private
成员上,也是不一致的。像 Java、C#、C++、Swift 和 PHP 都是允许的,Ruby 是不允许。TypeScript 允许交叉实例私有成员的获取:
警告(Caveats)
private
和protected
仅仅在类型检查的时候才会强制生效。这意味着在 JavaScript 运行时,像
in
或者简单的属性查找,依然可以获取private
或者protected
成员。private
允许在类型检查的时候,通过方括号语法进行访问。这让比如单元测试的时候,会更容易访问private
字段,这也让这些字段是弱私有(soft private)而不是严格的强制私有。不像 TypeScript 的
private
,JavaScript 的私有字段(#
)即便是编译后依然保留私有性,并且不会提供像上面这种方括号获取的方法,这让它们变得强私有(hard private)。当被编译成 ES2021 或者之前的版本,TypeScript 会使用 WeakMaps 替代
#
:如果你需要防止恶意攻击,保护类中的值,你应该使用强私有的机制比如闭包,
WeakMaps
,或者私有字段。但是注意,这也会在运行时影响性能。TypeScript 系列
TypeScript 系列文章由官方文档翻译、重难点解析、实战技巧三个部分组成,涵盖入门、进阶、实战,旨在为你提供一个系统学习 TS 的教程,全系列预计 40 篇左右。点此浏览全系列文章,并建议顺便收藏站点。
微信:「mqyqingfeng」,加我进冴羽唯一的读者群。
如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。
The text was updated successfully, but these errors were encountered: