剑客
关注科技互联网

面向 JavaScript 开发人员的 ECMAScript 6 指南,第 3 部分: JavaScript 中的类

系列内容:

此内容是该系列 4
部分中的第 #
部分: 面向 JavaScript 开发人员的 ECMAScript 6 指南,第 3 部分

http://www.ibm.com/developerworks/cn/views/web/libraryview.jsp?sort_by=&show_abstract=true&show_all=&search_flag=&contentarea_by=Web+development&search_by=%E9%9D%A2%E5%90%91+JavaScript+%E5%BC%80%E5%8F%91%E4%BA%BA%E5%91%98%E7%9A%84+ECMAScript+6+%E6%8C%87%E5%8D%97&topic_by=-1&type_by=%E6%89%80%E6%9C%89%E7%B1%BB%E5%88%AB&ibm-search=%E6%90%9C%E7%B4%A2

敬请期待该系列的后续内容。

此内容是该系列的一部分: 面向 JavaScript 开发人员的 ECMAScript 6 指南,第 3 部分

敬请期待该系列的后续内容。

在第 2 部分 中,您学习了 ECMAScript 6 中的函数增强,包括新的箭头和生成器函数。将函数元素集成到 JavaScript 代码中意味着要重新考虑某些因素,但变化没有您想象的那么大。事实上,在多年来提出的所有变化中,ECMAScript 6 中最有争议的新元素或许就是面向对象的元素。

JavaScript 中一直缺少传统的基于类的语法,但 ECMAScript 6 改变了这种状况。在本期文章中,将学习如何在 JavaScript 中定义类和属性,以及如何使用原型链向 JavaScript 程序引入继承。

关于本系列

ECMAScript 6 于 2015 年 6 月被采纳,是第一个为某种语言而编写的 JavaScript 标准,它不只有助于将现代 Web 功能相结合,而且为现代 Web 提供了强大的支持。在本系列 中,编程语言导师 Ted Neward 将介绍会成为您最喜欢的 Web 开发工具中的标准的新功能和语法,还将展示如何恰当地将它们引入您自己的代码中。

对象简史

JavaScript 最初被设想和宣传为 Java 的轻量型版本,所以它通常被认为是一种面向对象的传统语言。得益于 new
关键词,它似乎在语法上类似于过去常常在 Java 或 C++ 中看到的语法。

事实上,JavaScript 不是基于类的环境,而是一个基于对象的环境。如果您不熟悉或仅偶尔参与面向对象的开发,JavaScript 可能对您无关紧要,但理解它们的区别仍然很重要。在基于对象的环境中,不存在类。每个对象是从另一个现有对象克隆而来的,而不是来自类。当克隆一个对象时,会保持对其原型对象的隐式引用。

在基于对象的环境中工作有其优势,但在没有基于类的概念(比如属性和继承)的情况下能执行的操作上也存在局限。ECMAScript 技术委员会曾经试图将面向对象的元素集成到 JavaScript 中,而不牺牲它的独特风格。在 ECMAScript 6 中,该委员会最终找到了实现途径。

类定义

从一开始就采用 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;
      }
    }

    let ted = new Person("Ted", "Neward", 45);
    console.log(ted);

构造函数是一个 特殊函数
,会在构造过程中被调用。任何作为 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);

尽管该语言委员的目的显然是让 JavaScript 开发人员能够编写更加传统的面向类的代码,但他们会还想支持 ECMAScript 目前所具有的灵活性和开放性。理想情况下,这意味着开发人员能各取所长。

属性和封装

无法公开和维护其状态的类不是一个真正的类。因此,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()
函数来检索同一个列表。

现在有一个有趣的问题:如果对象本身上没有 firstName
lastName
age
的 getter 和 setter 函数,那么如何在没有严格的解释器能力的情况下解析类似 “ ted.firstName
” 的表达式?

答案既简单又优雅: ted
Person
的实例)保留了与它的类 Person
的原型链接。

原型链

从最初开始,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 库的一些增强,包括显式声明和使用模块的新语法。

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址