js6的未来
JavaScript 中的类
类定义
从一开始就采用 class 关键字可能是最容易的实现途径。如下所示,此关键字表示一个新 ECMAScript 类的定义:
1. 定义新类
class Person
{
}
let p = new Person();
空类本身不是很有趣。毕竟,每个人都有姓名和年龄,Person 类应该反映出这一点。在构造类实例时,通过引入构造函数来添加这些细节:
2. 构造类实例
class Person
{
constructor(firstName, lastName, age)
{
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
}
构造函数是一个特殊函数,会在构造过程中被调用。任何作为 new 运算符的一部分而传递给 type 的参数都被传递给构造函数。但是不要误解:constructor 仍然是 ECMAScript 函数。您可以利用它类似 JavaScript 的灵活参数,以及隐式的 arguments 参数,就象这样:
3. 灵活的参数和隐式参数
class Person
{
constructor(firstName, lastName, age)
{
console.log(arguments);
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
}
let ted = new Person("Ted", "Neward", 45);
console.log(ted);
let cher = new Person("Cher");
console.log(cher);
let r2d2 = new Person("R2", "D2", 39, "Astromech Droid");
console.log(r2d2);
属性和封装
ECMAScript 6 现在允许开发人员定义伪装为字段的属性函数。这为我们设定了 ECMAScript 中的各种封装风格。
考虑 Person 类。firstName、lastName 和 age 作为成熟的属性是合理的,我们将它们定义如下:
4. 定义属性
class Person
{
constructor(firstName, lastName, age)
{
console.log(arguments);
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
get firstName() { return this._firstName; }
set firstName(value) { this._firstName = value; }
get lastName() { return this._lastName; }
set lastName(value) { this._lastName = value; }
get age() { return this._age; }
set age(value) { this._age = value; }
}
请注意 getter 和 setter(根据 ECMAScript 规范中的官方规定)是如何引用字段名称的,字段名称添加了一条下划线作为前缀。这意味着 Person 现在有 6 个函数和 3 个字段 — 每个属性有 2 个函数和 1 个字段。不同于其他语言,ECMAScript 中的property 语法不会在创建属性时静默地引入后备存储字段。(后备存储 是存储数据的地方 — 换句话说,是实际字段本身。)
属性不需要逐个地直接反映类的内部状态。事实上,属性的封装性质很大程度上是为了部分或完整地隐藏内部状态:
5. 封装隐藏状态
class Person
{
// ... as before
get fullName() { return this._firstName + " " + this._lastName; }
get surname() { return this._lastName; }
get givenName() { return this._firstName; }
}
请注意,属性语法没有消除您直接获取字段的能力。您仍然可以使用熟悉的 ECMAScript 原理,枚举一个对象来获得它的内部结构:
6. 枚举一个对象
for (let m in ted) {
console.log(m,ted[m]);
// prints
// "_firstName,Ted"
// "_lastName,Neward"
// "_age,45"
}
还可以使用 Object 定义的 getAllPropertyNames() 函数来检索同一个列表。
原型链
从最初开始,JavaScript 就保留着从一个对象到另一个对象的原型链。您可能认为,原型链类似于 Java 或 C++/C# 中的继承,但两种技术之间只有一个真正的相似点:当 JavaScript 需要解析一个没有直接包含在对象上的符号时,它会沿原型链查找可能的匹配值。
这不太容易理解,所以我要再说明一下。想象您使用旧式 JavaScript 样式定义了一个非常简单的对象:
7. 旧式 JavaScript 对象
var obj = {};
现在,您需要获取该对象的字符串表示。通常,toString() 方法会为您完成这项工作,但 obj 上没有定义该函数,事实上,它之上什么都没有定义。该代码不仅能运行,还会返回结果:
8. 结果字符串
var obj = {};
console.log(obj.toString()); // prints "[object Object]"
当解释器寻找 toString 作为 obj 对象上的名称时,它没有找到匹配值。它没有立即找到该对象的原型对象,所以它在原型中搜索 toString。如果仍然没有找到匹配值,那么它会查找原型的原型,依此类推。在这种特定情况下,obj 的原型(Object对象)上定义了一个 toString。
现在让我们返回到 Person 类。您应该很清楚具体的情形:对象 ted 有一个对对象Person 的原型引用,Person 拥有方法对 firstName、lastName 和 age,它们被定义为 getter 和 setter。当使用一个 getter 或 setter 时,该语言会尊重原型,代表 ted 实例本身来执行它。
Person 类上定义的所有方法均如此,您在我们添加新方法时就会看到
9. 将一个方法添加到 Person
class Person
{
// ... as before
getOlder() {
return ++this.age;
}
}
新方法允许以 Person 为原型的实例优雅地老化,如下所示:
10. 沿原型链查找
ted.getOlder();
console.log(ted);
// prints Person { _firstName: 'Ted', _lastName: 'Neward', _age: 46 }
getOlder 方法是在 Person 对象上定义的,所以在调用 ted.getOlder() 时,解释器会沿原型链从 ted 查找到 Person。然后它会找到该方法并执行它。
对于大多数 Java 或 C++/C# 开发人员,可能需要一段时间才能习惯类实际上是对象的概念。对于 Smalltalk 开发人员,始终会遇到这种情况,所以他们想知道是什么耽误了我们其余人这么长时间。如果有助于您更快地解释该概念,可以尝试将 ECMAScript 中的类视为类型对象:为提供类型定义的外观而存在的对象实例。
原型继承
作为一种模式,“跟随原型链” 使 ECMAScript 6 的继承规则非常容易理解。如果您创建一个扩展另一个类的类,很容易想到在派生类上调用该实例方法时发生的情况。
11. 调用实例方法
class Author extends Person
{
constructor(firstName, lastName, age, subject)
{
super(firstName, lastName, age);
this.subject = subject;
}
get subject() { return this._subject; }
set subject(value) { this._subject = value; }
writeArticle() {
console.log(this.firstName,"just wrote an article on",this.subject);
}
}
let mark = new Author("Mark", "Richards", 55, "Architecture");
mark.writeArticle();
实例本身首先会处理调用。如果失败,那么它会检查类型对象(在本例中为Author)。接下来,将会检查类型对象的 “扩展” 对象 (Person),依此类推,直到返回到最初的类型对象,该对象始终是 Object。
此外,从清单 11 中的 Author 构造函数可以看到,关键字 super 显然会在原型链中向上调用给定方法的原型版本。在本例中,调用了构造函数,让 Person 构造函数有机会执行发挥自己的作用。如果仅跟随原型链,那么原理很简单。
我对原型委托使用得越多,就越欣赏此解决方案的优雅之处。所有方面都遵循一个概念,“旧规则” 仍在发挥其作用。如果希望以元对象方式继续使用 ECMAScript 对象,在对象本身上添加和删除方法,您仍然可以这么做:
12. 旧式对象委托
mark.favoriteLanguage = function() {
return "Java";
}
mark.favoriteBeverage = function() {
return "Scotch";
}
console.log(mark.firstName,"prefers writing", mark.subject,
"using",mark.favoriteLanguage(),"and",mark.favoriteBeverage());
在我看来,新的基于类的语法很容易掌握;在本例中,会让您使用 Java 并一直使用同一种语言。
静态属性和字段
如果不考虑回避 对面向对象的讨论,任何面向对象的讨论都是不完整的。当开始在代码中使用类时,知道如何处理全局变量和/或函数至关重要。在大多数语言中,这些变量和函数被认为是静态的(或整体式的),如果您喜欢使用概模式。
ECMAScript 6 没有隐式配备静态属性或字段,但根据我们上面的讨论和对 ECMAScript 对象的工作原理的一些了解,不难想象可以如何实现静态值:
13. 引入静态值
class Person
{
constructor(firstName, lastName, age)
{
console.log(arguments);
// Just introduce a new field on Person itself
// if it doesn't already exist; otherwise, just
// reference the one that's there
if (typeof(Person.population === 'undefined'))
Person.population = 0;
Person.population++;
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
// ... as before
}
因为 Person 类实际上是一个对象,所以 ECMAScript 中的静态字段实质上是 Person类型对象上的字段。因此,尽管没有显式的语法来定义静态字段,但可以直接在类型对象上引用字段。在上面的示例中,Person 构造函数首先检查 Person 是否已有一个 population 字段。如果没有,它会将 population 设置为 0,隐式地创建该字段。如果有一个 population 字段,那么它会递增该值。
因此,沿原型链一直到 Person 的任何实例都可以引用 population 字段,无论是直接引用还是按名称引用 Person 类(或类型对象),后者是首选方法:
14. 引用类
console.log(Person.population);
console.log(ted.population);
定义字段很容易,但 ECMAScript 6 规范使定义静态方法变得有点复杂。要定义静态方法,需要在类声明中使用 static 关键字来定义函数:
15. 定义静态方法
class Person
{
// ... as before
static haveBaby() {
return Person.population++;
}
}
同样地,可以通过实例或通过类本身来调用静态方法。您可能会发现,如果始终通过类名称调用静态方法,很容易跟踪在何处定义了什么对象。
结束语
ECMAScript 技术委员会在其发展过程中遇到了一些严峻的挑战,但这些挑战都没有向 JavaScript 引入类那么艰难。目前,似乎新语法获得了成功,满足了大多数面向对象的开发人员的期望,而且从整体上讲没有丢弃 ECMAScript 的基础原则。
该委员会没有集成 TypeScript 等语言中提供的稳健的静态类型检查,但这从来都不是他们考虑的目标。值得称赞的是,该委员会没有试图强迫这么做,至少在这一轮改进中没有这么做。
请关注本系列的最后一期文章!我们将探索 ECMAScript 6 库的一些增强,包括显式声明和使用模块的新语法。
ECMAScript 6 中的新功能
ECMAScript 6 工具和平台兼容性页面
共同学习,写下你的评论
评论加载中...
作者其他优质文章