- 深入理解TypeScript
- (澳)Basarat Ali Syed
- 2026字
- 2025-02-22 19:07:18
3.1 类
在JavaScript中类作为“一等公民”的重要原因在于以下3点。
● 提供一个有用的抽象化结构。
● 为开发者提供使用类的一致方法,而不是每个框架(Ember.js、React.js等)都有自己的版本。
● 熟知面向对象的开发者已经对类有所了解。
最终,JavaScript开发者可以使用class了,这里展示一个基本的类Point。

这个类会在ES5中生成如下代码。

这是一个常用的传统的JavaScript类的模式,现在它被当作第一等语言结构,
1.继承(extends)
在TypeScript中的类,与其他语言一样,支持使用extends关键字实现单继承,如下所示。

如果在一个类中拥有构造器,那么你必须在这个构造器中调用父级构造器(TypeScript将会指明这一点)。这能够确保你可以使用this来设置(获取)一些变量。在调用super之后,你可以添加任何想在构造器中添加的表达式(在这里是成员z)。
注意:你可以很轻易地改写父类中的成员(这里修改了add),而你的成员仍然可以使用父类的函数(super.函数名)。
2.静态(static)
TypeScript类支持静态属性,静态属性会被所有的实例共享。放置(和访问)它们的合适位置在类本身上,如下所示。

你可以拥有静态成员和静态函数。
3.访问修饰符
TypeScript支持访问修饰符public、private、protected,它们决定了成员的可访问性,如下所示。

如果未指定访问修饰符,则默认是public的,因为它符合JavaScript方便的特性。
注意:在运行时(在编译后的JavaScript代码中),这些没有任何意义,但是如果你没有正确地使用它们的话,在编译时会抛出错误,示例如下。

跟之前一样,这些访问修饰符同时在成员属性和成员函数上有效。
4.抽象(abstract)
abstract也可以被认为是一个访问修饰符,之所以单独介绍它,是因为与前面提到的修饰符不同,abstract可以作用在类及类的任何成员上。拥有一个abstract修饰符意味着该函数不能直接被调用,并且子类必须实现这个功能。
● abstract类不能直接被实例化,用户必须创建一个类来继承abstract类。
● abstract成员不能直接被访问,子类必须实现这个功能。
5.构造器(constructor)
构造器(constructor)是可选的,类不是必须要有一个构造器,示例如下。

可以使用构造器来定义成员变量,即在类中拥有一个成员,并在构造器中初始化它,示例如下。

这是一种普遍的形式,TypeScript为这种方式提供了一个缩写,你可以在成员中加一个修饰符前缀,它会在类上自动声明,并且从构造器中复制过去。因此上面的例子可以改成如下的形式(注意public x:number)。

6.属性初始化
这是TypeScript(实际上是ES7)提供的一个特性,你可以在类的构造器外初始化类中的任何成员,这在需要提供一个默认值时比较有用(注意,numbers=[])。

7.IIFE发生了什么
在JavaScript中,类编译后的代码的示例如下。

这段代码被包含在立即调用函数表达式(Immediately-Invoked Function Expression,简称IIFE)中。

之所以这么做,与继承有关。IIFE允许TypeScript使用变量_super来捕获基类,示例如下。

注意:IIFE允许TypeScript通过变量_super来轻松地捕获基类Point,这种方式在类体中经常被使用。
8.__extends
当在TypeScript中使用类的继承时,它们会生成如下代码。

在这里d表示派生类,b表示基类。这个函数做了两件事情。
● 将基类的静态成员复制到子类,例如,for(var p in b)if(b.hasOwn Property(p))d [p]=b [p]。
● 设置子类函数的原型,用来选择性地查找父类proto上的成员,例如,实际上d.prototype.__proto__=b.prototype。
而对于下面的两个问题,有一种奇怪的现象,人们很少去理解第一个问题,但很多人却在第二个问题上挣扎。下面我将按顺序来解释它们。
1)d.prototype.__proto__=b.prototype
在指导了很多人之后,我发现这样解释是最简单的:首先,我会解释__extends中的代码如何等同于简单的d.prototype.__proto__=b.prototype,然后再解释为什么这样做很重要。为了便于理解这些事情,你必须首先了解下面的内容。
●__proto__.
● prototype.
● new在函数调用时对其内部this的影响。
● new对__proto__和prototype的影响。
JavaScript中的所有对象都包含一个__proto__成员。这个成员在较老版本的浏览器中不能被获取(有些文档将这个属性称为[[prototype]])。它的存在有一个目的:如果在对象上找不到某个属性,如obj.property,那么它会在obj.__proto__.property上查找;如果仍然没有找到,则会在obj.__proto__.__proto__.property上继续查找;直到属性被找到,或者当__proto__为null时,才会停止查找。这解释了JavaScript支持“开箱即用”的原型继承的原因。具体示例如下(你可以将它们运行在Chrome或Node.js中)。

现在你应该已经理解了__proto__,另一个有用的事实是JavaScript中所有的函数都有一个prototype属性,而在prototype上有一个指向函数本身的构造器属性,示例如下。

现在,让我们看看new对被调用函数内部的this的影响。基本上,被调用函数中的this都将指向从函数返回的新对象。这样,就很容易看出你是否改变了函数内部的this的属性了。

现在,你需要知道的事情是:在函数上调用new,会将函数的prototype分配给一个新创建对象(该对象是函数调用返回的)的__proto__。运行下面这些代码,你就能够完全理解了。

好了,现在我们继续看__extends,我给下面的代码行编了号。

倒着看这个函数,从第3行d.prototype=new__()可以看出d.prototype={__proto__:__.prototype}(基于前面讲的new对prototype和__proto__的影响)。结合上一行代码(即第2行),可以得到d.prototype={__proto__:b.prototype}。
但是,等等,我们想要的是d.prototype.__proto__,即只改变__proto__,而保存原始的d.prototype.constructor,这就是第1行的意义。这里,我们事实上拥有d.prototype={__proto__:__.prototype,constructor:d}。因此,自从我们修复了d.prototype.constructor,唯一真正被改变是__proto__,即d.prototype.__proto__=b.prototype。
2)d.prototype.__proto__=b.prototype的重要性
d.prototype.__proto__=b.prototype的重要性在于,它允许你将成员函数添加到子类,并从基类继承其他函数,示例如下。

bird.fly将会在bird.__proto__fly上查找成员,(记住new可以让bird.__proto__指向Bird.prototype),而bird.walk(一个被继承的成员)将会在bird.__proto__.__proto__.walk上查找,因为bird.__proto__==Bird.prototype和bird.__proto__.__proto__==Animal.prototype。