ES6 深入是一个系列,介绍了 ECMAScript 标准第 6 版(简称 ES6)中添加到 JavaScript 编程语言中的新功能。
今天,我们从本系列之前文章中的复杂性中得到了一些喘息的机会。我们没有看到新的以前从未见过的使用生成器编写代码的方法;没有无所不能的代理对象,它们提供了对 JavaScript 语言内部算法工作的钩子;没有新的数据结构可以省去自己动手实现解决方案的必要。相反,我们将讨论针对一个老问题进行的语法和习惯用法清理:JavaScript 中的对象构造函数创建。
问题
假设我们想要创建面向对象设计原则中最典型的例子:圆形类。想象一下,我们正在为一个简单的画布库编写一个圆形。除其他事项外,我们可能想知道如何执行以下操作
- 将给定的圆形绘制到给定的画布上。
- 跟踪已创建的所有圆形的总数。
- 跟踪给定圆形的半径以及如何强制不变量对其值的限制。
- 计算给定圆形的面积。
当前的 JS 习惯用法表明,我们应该首先将构造函数创建为函数,然后将我们可能希望添加到函数本身的任何属性添加进去,然后将该构造函数的prototype
属性替换为一个对象。这个prototype
对象将包含由我们的构造函数创建的实例对象应该开始的所有属性。即使对于一个简单的例子,在您完成所有键入操作时,最终还是会有很多样板代码
<pre>
function Circle(radius) {
this.radius = radius;
Circle.circlesMade++;
}
Circle.draw = function draw(circle, canvas) { /* 画布绘制代码 */ }
Object.defineProperty(Circle, "circlesMade", {
get: function() {
return !this._count ? 0 : this._count;
},
set: function(val) {
this._count = val;
}
});
Circle.prototype = {
area: function area() {
return Math.pow(this.radius, 2) * Math.PI;
}
};
Object.defineProperty(Circle.prototype, "radius", {
get: function() {
return this._radius;
},
set: function(radius) {
if (!Number.isInteger(radius))
throw new Error("圆形半径必须为整数。");
this._radius = radius;
}
});
</pre>
代码不仅繁琐,而且不直观。它要求对函数的工作方式以及各种已安装属性如何传递到已创建的实例对象上有一个非平凡的理解。如果这种方法看起来很复杂,别担心。本文的重点是展示一种更简单的方法来编写能够完成所有这些操作的代码。
方法定义语法
在第一次尝试清理此问题时,ES6 提供了一种用于向对象添加特殊属性的新语法。虽然向上面的 Circle.prototype
添加 area
方法很容易,但添加 radius
的 getter/setter 对似乎要重得多。随着 JS 朝着更面向对象的方法发展,人们开始对设计更清洁的方法来向对象添加访问器感兴趣。我们需要一种新的方法来添加对象的“方法”,就像它们是用 obj.prop = method
添加的那样,而不需要 Object.defineProperty
的重量。人们希望能够轻松地执行以下操作
- 向对象添加普通函数属性。
- 向对象添加生成器函数属性。
- 向对象添加普通访问器函数属性。
- 将上述任何一种作为您在完成的对象上使用
[]
语法添加的。我们将这些称为*计算属性名*。
以前无法执行其中的一些操作。例如,无法使用对 obj.prop
的赋值来定义 getter 或 setter。因此,必须添加新的语法。您现在可以编写类似于以下代码的代码
<pre>
var obj = {
// 方法现在不使用 function 关键字添加,而是使用属性的名称作为函数的名称。
// method(args) { ... },
// 要创建一个作为生成器的函数,只需像往常一样添加一个 '*'。
*genMethod(args) { ... },
// 访问器现在可以使用 |get| 和 |set| 内联。您只需内联定义函数。不过,没有生成器。
// 请注意,以这种方式安装的 getter 必须没有参数
get propName() { ... },
// 请注意,以这种方式安装的 setter 必须恰好有一个参数
set propName(arg) { ... },
// 要处理上面情况 (4),现在可以在任何可以放名称的地方使用 [] 语法!这可以使用符号、调用函数、连接字符串或
// 任何其他计算结果为属性 ID 的表达式。虽然我在此处将其显示为方法,但这种语法也适用于访问器或生成器。
[functionThatReturnsPropertyName()] (args) { ... }
使用这种新语法,我们现在可以重写上面的代码片段
area() {
get radius() {
set radius(radius) {
};
</pre>
严格来说,这段代码与上面的代码片段并不完全相同。对象文字中的方法定义将被安装为可配置的和可枚举的,而第一个代码片段中安装的访问器将是不可配置的和不可枚举的。在实践中,这很少被注意到,为了简洁起见,我在上面省略了可枚举性和可配置性。
<pre>
function Circle(radius) {
this.radius = radius;
Circle.circlesMade++;
}
Circle.draw = function draw(circle, canvas) { /* 画布绘制代码 */ }
Object.defineProperty(Circle, "circlesMade", {
get: function() {
return !this._count ? 0 : this._count;
},
set: function(val) {
this._count = val;
}
});
Circle.prototype = {
尽管如此,它正在变得更好,对吧?不幸的是,即使配备了这种新的方法定义语法,我们也无法为 Circle
的定义做太多的事情,因为我们还没有定义函数。没有办法在定义函数时将属性放到函数上。
return Math.pow(this.radius, 2) * Math.PI;
},
类定义语法
return this._radius;
},
尽管这更好,但它仍然不能满足想要在 JavaScript 中获得更清洁的面向对象设计解决方案的人们的需求。他们争辩说,其他语言有一种用于处理面向对象设计的结构,这种结构被称为*类*。
if (!Number.isInteger(radius))
throw new Error("圆形半径必须为整数。");
this._radius = radius;
}
};
</pre>
说得有道理。那么,让我们添加类。
我们想要一个系统,它允许我们将方法添加到命名的构造函数中,并将方法添加到其 .prototype
中,以便它们出现在类的已构建实例上。由于我们有我们花哨的新方法定义语法,所以我们应该绝对使用它。然后,我们只需要一种方法来区分所有类实例的通用化以及哪些函数特定于给定实例。在 C++ 或 Java 中,该关键字为 static
。这似乎与任何其他方法一样好。让我们使用它。
现在,如果有一种方法可以指定一堆方法中的一个作为被调用作为构造函数的函数,那就很有用了。在 C++ 或 Java 中,它的名称将与类相同,没有返回值类型。由于 JS 没有返回值类型,并且为了向后兼容,我们仍然需要一个 .constructor
属性,因此让我们将该方法命名为 constructor
。
将它们放在一起,我们可以像预期的那样重写我们的 Circle 类
class Circle {
constructor(radius) {
this.radius = radius;
Circle.circlesMade++;
<pre>
static draw(circle, canvas) {
// 画布绘制代码
this.radius = radius;
Circle.circlesMade++;
};
static get circlesMade() {
return !this._count ? 0 : this._count;
};
static set circlesMade(val) {
return !this._count ? 0 : this._count;
};
this._count = val;
this._count = val;
};
尽管如此,它正在变得更好,对吧?不幸的是,即使配备了这种新的方法定义语法,我们也无法为 Circle
的定义做太多的事情,因为我们还没有定义函数。没有办法在定义函数时将属性放到函数上。
return Math.pow(this.radius, 2) * Math.PI;
};
类定义语法
return this._radius;
};
尽管这更好,但它仍然不能满足想要在 JavaScript 中获得更清洁的面向对象设计解决方案的人们的需求。他们争辩说,其他语言有一种用于处理面向对象设计的结构,这种结构被称为*类*。
if (!Number.isInteger(radius))
throw new Error("圆形半径必须为整数。");
this._radius = radius;
};
}
</pre>
area() {
return Math.pow(this.radius, 2) * Math.PI;
-
get radius() {
-
return this._radius;
-
set radius(radius) {
-
if (!Number.isInteger(radius))
-
throw new Error("圆形半径必须为整数。");
-
this._radius = radius;
-
那么,上面关于可枚举性和其他内容的那些小把戏是怎么回事呢? – 人们希望能够在对象上安装方法,但是当枚举对象的属性时,只能获取对象的附加数据属性。 这是有道理的。 因此,在类中安装的方法是可配置的,但不可枚举。
-
等等… 什么…? 我的实例变量在哪里?
static
常量呢? – 你抓到我了。 在 ES6 中,它们目前并不存在于类定义中。 不过好消息是! 我和其他参与规范流程的人一样,强烈支持在类语法中安装static
和const
值。 实际上,这个问题已经在规范会议上提出来了! 我认为我们可以在将来期待更多关于这方面的讨论。 -
好吧,即使如此,这些也棒极了! 我现在能使用它们吗? – 不完全是。 有 polyfill 选项(尤其是 Babel),因此你今天就可以开始尝试使用它们。 不幸的是,它们要过一段时间才能在所有主流浏览器中实现原生支持。 我已经在 Firefox 的 Nightly 版本 中实现了我们今天讨论的所有内容,并且在 Edge 和 Chrome 中已经实现了,但默认情况下未启用。 不幸的是,看起来 Safari 目前还没有实现。
-
Java 和 C++ 有子类化和
super
关键字,但是这里没有提到。 JS 有这个吗? – 有! 但是,这将是另一篇文章的讨论内容。 稍后请回来查看关于子类化的更新,我们将进一步讨论 JavaScript 类的强大功能。
如果没有 Jason Orendorff 和 Jeff Walden 的指导和大量的代码审查责任,我将无法实现类。
下周,Jason Orendorff 从一周的假期中回来,将开始讨论 let 和 const。
40 条评论