为了账号安全,请及时绑定邮箱和手机立即绑定

js6的未来

标签:
JavaScript

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 工具和平台兼容性页面

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消