深入 ES6:类

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 的重量。人们希望能够轻松地执行以下操作

  1. 向对象添加普通函数属性。
  2. 向对象添加生成器函数属性。
  3. 向对象添加普通访问器函数属性。
  4. 将上述任何一种作为您在完成的对象上使用 [] 语法添加的。我们将这些称为*计算属性名*。

以前无法执行其中的一些操作。例如,无法使用对 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 中,它们目前并不存在于类定义中。 不过好消息是! 我和其他参与规范流程的人一样,强烈支持在类语法中安装 staticconst 值。 实际上,这个问题已经在规范会议上提出来了! 我认为我们可以在将来期待更多关于这方面的讨论。

  • 好吧,即使如此,这些也棒极了! 我现在能使用它们吗? – 不完全是。 有 polyfill 选项(尤其是 Babel),因此你今天就可以开始尝试使用它们。 不幸的是,它们要过一段时间才能在所有主流浏览器中实现原生支持。 我已经在 Firefox 的 Nightly 版本 中实现了我们今天讨论的所有内容,并且在 Edge 和 Chrome 中已经实现了,但默认情况下未启用。 不幸的是,看起来 Safari 目前还没有实现。

  • Java 和 C++ 有子类化和 super 关键字,但是这里没有提到。 JS 有这个吗? – 有! 但是,这将是另一篇文章的讨论内容。 稍后请回来查看关于子类化的更新,我们将进一步讨论 JavaScript 类的强大功能。

如果没有 Jason OrendorffJeff Walden 的指导和大量的代码审查责任,我将无法实现类。

下周,Jason Orendorff 从一周的假期中回来,将开始讨论 letconst

关于 Eric Faust

更多 Eric Faust 的文章…


40 条评论

  1. Cris Mooney

    这项工作做得很好,目标是经验丰富的 JavaScript 程序员,而不是那些来自已经存在类的语言的程序员,并且假设这些能力对于 JavaScript 来说都是 100% 的新内容。 编译器在这方面可能会有所帮助(如果有人查看 https://babel.node.org.cn/repl 这样的输出),但这是我第一次看到有人花时间承认并写出如何才能实现与类相关的功能,然后如此干净地引导到新的语法支持,以及为什么即使是这部分语法也是有价值的(并且与传统语法相关),即使没有添加“新功能” — 所有这些价值甚至在达到“extends”之前就已存在。 这是一个逻辑性很强的演示,写得很棒,并带有智能的预测问答,应该鼓励采用新功能,并尊重现有的语言。 谢谢!

    2015 年 7 月 23 日 下午 07:46

  2. Jovica Aleksic

    我发现使用 babel 时可以很好地实现实例变量这一点值得注意。 我明白这个系列是关于 es6 特性的,而属性初始化器只是 es7 早期的一个提案。 但是,从语法上讲,它们确实可以与 es6 类一起使用,就像一样,允许你编写非常简洁的代码。 我猜大多数 es6 开发者都会使用 babel。
    因此,在配置 babel 使用 stage=0 后,你可以这样写

    class Foo {
    bar = ‘some value’;
    static bar = ‘some static value’;
    }

    结果与以下相同

    class Foo {
    constructor() {
    this.bar = ‘some value’;
    }
    }
    Foo.bar = ‘some static value’;

    2015 年 7 月 23 日 下午 10:45

    1. Eric Faust

      感谢你关于 Babel 的提示! 这很有用。 实际上,这正是我个人喜欢的语法,另外还添加了 |const| 作为实例变量的另一个修饰符。

      当然,你可以在构造函数中添加实例变量,但是对于静态变量,事后添加它们真的很糟糕。 这正是我们刚刚努力消除的东西!

      从我的角度来看,梦想是编写一些可以这样表达的东西

      class Life {
      static const ANSWER = 42;
      }

      并且将其安装在 Life 构造函数上,使其不可写不可配置(至少是!)。 我们将看看 es7 提案能取得什么进展。

      2015 年 7 月 24 日 下午 11:26

  3. Phil Ricketts

    这看起来很棒。

    我一直都在思考 ES6 的作用,作为一名为浏览器编写大量分布式项目团队的 javascript 代码的人,我决定只专注于编写良好的 ES5,原因如下
    * 浏览器中的 ES6 支持存在性能问题(因为它还没有经过 ES5 随着时间的推移而进行的所有优化)。
    * 编译是额外的创作步骤(+ 浏览器还没有完全准备好)。
    * 编译可能会产生奇怪的效果,因为代码是在通过编译器解释后在浏览器中运行的。
    * 支持浏览器中 ES6 的多个实现会导致类似 CSS 的支持差异,这将不可避免地导致由于支持差异而带来的额外开销。
    * 开发者的人体工程学对我来说不是最重要的 — 编写良好的 ES5 是。

    2015 年 7 月 23 日 下午 11:12

    1. Miguel Camba

      那是不准确的。逐点分析

      “浏览器中的 ES6 支持存在性能问题”。 虽然这是真的,但你不应该担心,因为 A) 这很可能永远不会成为你的瓶颈 B) 你会将代码编译成 ES5,所以即使 ES6 非常慢,也不会影响你。

      “编译是额外的创作步骤(+ 浏览器还没有完全准备好)”。 确实,这是一个额外的创作步骤。 不好,但目前是必要的,但是如果你要编译,为什么还要提到浏览器支持呢?

      “编译可能会产生奇怪的效果,因为代码是在通过编译器解释后在浏览器中运行的”。 虽然我认为可以在浏览器中加载编译器并在客户端进行编译,但我认为没有人这样做。 人们在“编译时”编译代码,因此浏览器永远不会收到 ES6。 而且编译非常可靠。 有时可读性可能比手工制作的 JS 差一些,但除此之外就没有其他问题了。

      “支持浏览器中 ES6 的多个实现会导致类似 CSS 的支持差异”。 同样,由于你要编译,所以这不是问题。 没有 ES6 特定的差异,只有我们都知道和喜欢的那些差异。

      “开发者的人体工程学对我来说不是最重要的 — 编写良好的 ES5 是”。 我个人不同意,但这是主观的,所以没什么好争论的。

      2015 年 7 月 28 日 上午 04:02

  4. bogus

    如何在 ES6 类中进行混合?

    2015 年 7 月 23 日 下午 13:03

    1. Eric Faust

      我看到了一些关于 |mixin| 关键字提案的传言,但我不记得发生了什么。

      没有理由让正常的基于 extend() 的方法不能适用于类,它只是没有直接内置到语法中。

      从提出一个涵盖此问题空间中所有内容的语法的角度来看,这是一个非常糟糕的答案,但至少它比以前好了一些。

      2015 年 7 月 24 日 下午 11:29

      1. Christian Martin

        你也可以使用装饰器作为混合。

        2015 年 7 月 24 日 下午 12:10

    2. Jason Orendorff

      使用混合的一种方法是
      class MyClass extends mix(MixinClass1, MixinClass2, ...) { ... }

      但是标准没有定义这个 mix() 函数; 你必须自己编写它。 我认为它主要需要创建一个原型对象,将所有混合类的所有功能结合起来,然后创建一个简单的构造函数并将原型附加到它。 这并不难:几行代码,使用 Object.assign()

      换句话说,你基本上是用 JS 构建了自己的混合支持,就像你对 ES1-5 做的那样。 这是可能的,因为 ES6 类使用了完全相同的简单且可破解的底层模型。 在完成之后,它看起来更漂亮!

      2015 年 7 月 30 日 上午 08:18

  5. Eric Elliott

    我喜欢类语法,但 ES6 中的语义真的有问题。

    使用 `class` 会迫使你从所有调用站点使用 `new`,这使得以后将实现切换为工厂更加困难(这是一种常见的重构,它包含在 Martin Fowler 的开创性 [重构目录](https://refactoring.martinfowler.com.cn/) 中)。

    至于继承和 super(你很快就会讨论,很明显),[这些都是不好的想法](https://medium.com/javascript-scene/the-two-pillars-of-javascript-ee6f3281e7f3),在我看来,它们根本不适合 JavaScript。

    装饰器和特征对于 ES7 来说要好得多。 正如著名的四人帮所说,“优先选择对象组合而不是类继承”。

    或者 [直接使用 stamp](https://github.com/stampit-org/stampit)。 stamp 社区最近一直非常活跃,我们一直在努力创建一个 [stamp 的开放规范](https://github.com/stampit-org/stamp-specification)。 如果能够在原生 JavaScript 中添加对它的支持,那就太好了。

    2015 年 7 月 23 日 下午 15:59

    1. Jeff Walden

      Java 之类的语言会让你有点困惑。:-) 在这些语言中,构造函数**不能**作为工厂方法——它们必须返回由它们创建的任何 `this`。

      相比之下,在 JS 中,使用 `new` 调用的函数可以返回任何对象,而不仅仅是默认的 `this`。

      class X { constructor() { return new String(‘ohai’); } };
      assert(new X() instanceof String);

      在调用站点强制使用 `new` 不会阻止你将它们用作工厂。

      我强烈同意继承在很多情况下都是个坏主意。特别是当从 JS 和 DOM 中的大部分基本功能继承时。但是,继承在 ES5 中如此多的类实现中几乎无处不在,这表明添加继承是值得的,即使它会被滥用。我脑海中一直有个想法,想写一篇帖子来反对使用继承,或者至少建议谨慎使用,但我不知道什么时候能写出来。

      2015 年 7 月 24 日 下午 4:53

      1. Eric Elliott

        我希望它很简单,但事实并非如此。

        返回任何非构造函数实例的东西都是对 API 调用者的重大更改。如果所有代码都在你的城堡中,那就没问题了,但如果是库/公共 API,你将无法解决。有关更深入的讨论,请参阅此处:https://medium.com/javascript-scene/how-to-fix-the-es6-class-keyword-2d42bb3f4caf

        2015 年 7 月 24 日 下午 6:55

        1. Eric Elliott

          我在这篇帖子中澄清了一些关于 JavaScript 继承的误解:https://medium.com/javascript-scene/common-misconceptions-about-inheritance-in-javascript-d5d9bab29b0a

          查看有关 `new` 和 `instanceof` 的部分。

          2015 年 7 月 24 日 下午 7:06

        2. Jeff Walden

          如果人们依赖于返回值类型,那么它来自 `new` 还是来自经典工厂方法都无关紧要。经典的 Java `Point` 池示例,从 `Point.create(int x, int y)` 方法返回缓存的实例,可以在 JavaScript 中轻松地为 `new Point(x, y)` 完成。

          function Point(x, y) { if (!Point._cache) Point._cache = {}; var cache = Point._cache; if (!(x in cache)) cache[x] = {}; if (y in cache[x]) return cache[x][y]; this.x = x; this.y = y; cache[x][y] = this; }

          转换为类语法留作练习。

          由你决定构造的函数是否只返回它自身的实例。只要它只返回它自身的实例,就没有“对 API 调用者的重大更改”。

          2015 年 7 月 25 日 下午 4:03

  6. Dmitry Soshnikov

    写得很好!

    > 分号是怎么回事?——为了“使事物看起来更像传统的类”。

    我认为在这种情况下,我们需要在全球范围内保持一致的风格,而不是根据“传统类”(传统来自哪里? BTW?)引入多种风格。ES6 规范对此没有说明分号,因此你只是在每个方法定义之后添加了无用的空语句。

    2015 年 7 月 24 日 下午 12:10

    1. Eric Faust

      > ES6 规范对此没有说明分号,因此你只是在每个方法定义之后添加了无用的空语句。

      这并不完全正确。规范非常明确地允许在类定义中使用分号,并且非常明确地不允许使用任意语句。

      请参阅 http://www.ecma-international.org/ecma-262/6.0/#sec-class-definitions 以了解完整的语法。ClassElement 产生的最后一行是我们要关注的。

      确实,那里的分号不会计算任何值,所以从这个意义上说你是对的。如帖子中所述,分号不是必需的。我几乎是凭着肌肉记忆加上了它们,因为我来自 C++ 背景。

      2015 年 7 月 24 日 上午 12:26

  7. Dmitry Soshnikov

    > 规范非常明确地允许使用分号

    哦,有意思,谢谢!是的,我的错,我不记得这个变化了(可能是最近的)。好吧,那,是的,我预测将来我们将会看到一些不一致的风格:) 或者,我们将仍然提出一个单一的风格(无论是否使用分号),并且将由 linter 来维护一种风格。

    2015 年 7 月 24 日 下午 4:03

  8. Dmitry Soshnikov

    此外,如果它们能像对象字面量一样用逗号分隔,那就真的会更好。现在我们有了巨大的不一致:在对象字面量中,方法用逗号分隔(与任何其他属性一样),而在类中:要么什么都没有,要么用分号:) 可惜,ES6 规范已经发布了。

    2015 年 7 月 24 日 下午 4:15

    1. Allen Wirfs-Brock

      我们考虑了所有这些可能性,包括在对象字面量中允许使用分号作为分隔符的可能性。

      逗号对于未来的扩展来说过于限制。例如,如果在类主体中添加类似 let/const(或 private/protected)的声明,我们希望能够在其中使用逗号分隔的标识符列表。因此,我们决定在类主体中使用语句式分号分隔符。

      我倾向于也允许在对象字面量中使用分号作为分隔符,但我们无法在 TC39 中达成共识。也许将来会有...

      2015 年 7 月 24 日 下午 5:32

    2. Simon Speich

      我也希望它用逗号分隔...

      2015 年 7 月 27 日 上午 2:25

  9. PhistucK

    它已经在 Chrome 中实现并默认启用。它只需要严格模式,所以请在开发者工具中尝试一下(我尝试了 Chrome 43)——
    (function () { “use strict”; class a {} }())
    它不会抛出语法错误。
    据我所知,Edge 也默认启用了它。也许它也需要严格模式。

    2015 年 7 月 25 日 上午 11:10

  10. Neil Stansbury

    不幸的是,这又是一种对 JS 的 Java 化,主要来自那些似乎难以理解原型和万物皆对象的人。

    我一直觉得缺少的是静态原型继承声明——一个接受对象数组以进行组合的 `__proto__` 的形式化。

    “`
    RedCircle.prototype = {
    __proto__ : [Shape, Compass, Crayon]
    }
    “`

    2015 年 7 月 27 日 上午 3:45

    1. Jason Orendorff

      > 这又是一种对 JS 的 Java 化

      除了什么之外,还有“更多”?

      对我来说,ES6 类根本不像 Java。由 ES6 类声明创建的底层构造函数和原型与你在 ES1-5 中手动创建的完全相同。

      简洁的、声明性的语法如何将一个好主意变成一个坏主意?

      > 主要来自那些似乎难以理解原型和万物皆对象的人。

      我想知道你指的是哪些人。ES6 类是由 Allen Wirfs-Brock 设计和支持的,他的面向对象信条是货真价实的。他曾在 Smalltalk 上工作过。其他 TC39 成员(他们在确定这种简单设计之前,对许多奇特的 `class` 设计进行了辩论)也是重量级人物。Mark Miller 领导了 E 编程语言 的团队。Tom van Cutsem 设计了代理(所以我想他知道原型是什么)。Brendan Eich 最初将原型继承引入 JS 中。

      这些人并非全能,但声称他们“难以理解” JS 的前提是愚蠢的。

      许多对 ES6 类的反对意见似乎是“但其他程序员不会以我不喜欢的方式使用它?”或者,“其他程序员不会将它作为逃避学习我关心的东西的借口?”如果经验可以作为指南,你可以放松一下。影响很小,Java 式 API 设计成为 JS 中的首选做法的危险并不存在。

      2015 年 7 月 30 日 上午 9:17

      1. Eric Elliott

        “由 ES6 类声明创建的底层构造函数和原型与你在 ES1-5 中手动创建的完全相同。”

        它唯一相似之处是,如果你试图在 JavaScript 中模拟经典继承。这本身就是一个坏主意。

        https://medium.com/javascript-scene/the-two-pillars-of-javascript-ee6f3281e7f3

        2015 年 7 月 30 日 上午 10:47

  11. Dominik

    在 ES5 示例中,设置新的原型对象时,应该恢复构造函数属性

    Circle.prototype = {
    area: function area() {
    return Math.pow(this.radius, 2) * Math.PI;
    },
    constructor: Circle
    };

    这通常与 ES6 类的行为相匹配,因为类的 constructor() 方法会自动建立这种关系。但是,ES6 似乎更加严格,因为生成的 Circle.prototype.constructor 属性是不可写、不可枚举且不可配置的。

    2015 年 7 月 27 日 上午 6:48

  12. simonleung

    ES6 类不是 ES5 的进步,而是 ES5 构造函数的替代方案,并且专门用于创建对象。

    对于类 C,
    C 也具有原型属性
    C.prototype.constrcutor===C;

    但是,
    C 不能“重新声明”,否则会引发 TypeError。
    C.prototype 不能被新的对象“替换”。
    不能直接调用 C,即 C();
    C 的 constructor() 中的“return …” 将被忽略。

    2015 年 7 月 28 日 上午 9:52

    1. Jeff Walden

      我认为你最后一点不正确。类构造函数可以返回与它通常返回的 `this` 不同的值。这在 SpiderMonkey 中运行良好,例如 `new (class C { constructor() { return new String("hi"); } })()` 是一个 String 对象。根据我对 ES6 的理解,14.5.14 步骤 12 使用 14.3.8 中的通用 DefineMethod 操作创建构造函数,该操作用于定义所有其他方法,这些方法的返回值当然不会被忽略。或者我遗漏了什么?

      2015 年 7 月 28 日 上午 10:04

      1. simonleung

        好吧,我的错。
        只有当构造函数根据规范 6.1.7.3 返回非对象值时,它才会被忽略。
        这同样适用于 Function 构造函数。

        2015 年 7 月 28 日 上午 11:12

  13. Cris Mooney

    在思考了“类”语法替代方案之后,对于某些(主要是来自其他地方)来说,更好的代码组织方式,这篇文章中的以下评论脱颖而出

    > 无法在定义函数时将属性(和原型)添加到函数上。

    问题:既然这里似乎有很多人参与了新的规范,有人能解释一下为什么“函数”的语法不能得到增强以支持这一点,而不是引入一种实际上是竞争的语法吗?

    虽然通过提供熟悉感来欢迎来自其他环境的人是有价值的,但这也有不利的一面,它可以通过鼓励使用替代的竞争语法来隐藏核心当前语言功能,从而阻碍人们对语言基本原理的理解。

    我感觉我看到很多从其他语言转到 JavaScript 的新手,他们为了使用新的“class”语法而疯狂地学习,却不理解 JavaScript 本身已经存在的机制。这种思维方式只是用另一种不完整的方法替代了另一种不完整的方法,这是新手最常做的事,而不是去融合两种方法的优点来找到更好的解决方案。

    我希望有人能解释一下,为什么我们找不到一个创造性的、兼容的解决方案来解决“在定义函数时无法向其添加属性”这个关键问题。这样我们就可以保留语言本身并向新用户展示它的强大功能,同时提供让新手能够理解和组织代码的方式,并且简洁程度几乎相同。这到底是妥协、懒惰、缺乏远见,还是真的存在不可逾越的原因?

    2015 年 7 月 29 日 下午 7:53

    1. Cris Mooney

      题外话:我想澄清一下“妥协、懒惰、缺乏远见”:我和任何努力工作的人一样,经常会遇到这些问题。对于任何努力工作的人来说,它们都是 100% 可以被接受的,除非这些问题是长期存在的。任何努力工作的人,尤其是那些专门从事 JavaScript 开发的人,肯定都很聪明也很勤奋,他们不可能把所有事情都“做对”(即使是“正确”在很多情况下也是主观的)。这些回应是可以接受的,也是值得尊重的,我的本意也是如此。那些不能接受这些回应的人,是因为他们过分的自尊心和不安全感,其他人不应该对此负责。真正的科学家既能接受成功也能接受失败,因为失败能提高清晰度。我只是想理解“为什么”,这样我才能更好地理解和工作。

      2015 年 7 月 29 日 上午 8:43

      1. Jason Orendorff

        好吧,我认为你抓住了问题的关键部分,但它并不是特别重要。

        对我来说,class 语法有两个主要优点:

        1. ES1-5 创建类的做法是程序化的,而且很冗长。声明式代码是好的,简洁也是好的。

        我在演示幻灯片上放了一个完整的 ES6 类,而相应的 ES5 代码太长了。(这次演讲在这里:https://github.com/jorendorff/game-playing/blob/master/talk.md

        当然,我们通常不会为了适应幻灯片而写代码。但对一种代码阅读受众有益的东西,对其他受众也同样有益。

        2. 存在一致性和正确性的优势:使用新的 class 语法更容易确保细节正确,尤其是在子类化时。

        我喜欢方法不可枚举。我喜欢“静态”方法实际上是“类方法”(以 Smalltalk 的意义)并且可以被子类继承。我喜欢 .prototype 和 .constructor 被自动连接。

        这个功能的添加并非为了“提供熟悉感”,而是为了铺设现有的牛路径。牛不需要被牵引,它们已经在这条路上行走了几十年。

        2015 年 7 月 29 日 下午 3:50

        1. Neil Stansbury

          Jason,我认为这才是真正的危险——把方钉塞进圆孔,并将传统的 OO 范式应用于 JavaScript,而它们并不匹配。

          正如你所知,在 ES1-5 中,无论是字面上还是比喻上,都没有“类”的概念,即使是 ES6 的 class 声明实际上也不过是语法糖。

          “class”声明并没有提供关于不可变原型链的概念,任何认为自己的 JS “类”与 Java 或 C++ 相似的人都会大吃一惊。

          在 ES6 之前,ES 中唯一真正的“痛点”是构造函数与原型分别声明,而“extends”提供了一种声明式的形式化,用 __proto__ 来实现原型链。

          除此之外,ES6 class 在底层只是提供了一种简洁性。ES5 已经可以做 class 声明可以做的一切,所以我认为这正是为了“提供熟悉感”给非 ES 用户,因为这条牛路径在 ES 中很久以前就已经被铺好了。

          最糟糕的是,它掩盖了 JavaScript 底层原型概念,并提供了一套期望,而这些期望要么不真实,要么充其量只是一些近似。最近我亲眼目睹了这方面的问题,一个由前 AS3 开发人员组成的团队迁移到 TypeScript,他们对传统的“继承”与原型“委托”概念感到困惑。

          我相信 ES6 会取得巨大成功——但 ES5 已经成功了,我个人很伤心看到“过时”的 OO 概念污染了组合与委托优于继承的理念,我担心这不仅仅是 JavaScript 强大武器库中的另一种模式。我怀疑在未来几年,我们会看到大量“Java-script”。

          我认为这篇文章很有意思:http://christianalfoni.github.io/javascript/2015/01/01/think-twice-about-classes.html

          2015 年 7 月 29 日 下午 8:15

          1. Jason Orendorff

            直奔主题

            > 除此之外,ES6 class 在底层只是提供了一种简洁性。

            我已经说过我对这方面的问题(“声明式代码是好的,简洁也是好的”, “存在一致性和正确性的优势”, “铺设现有的牛路径”)。

            ES6 class “隐藏”了底层模型的说法对我来说有点奇怪。这并非什么秘密。

            所有声明式语法和 API 都会隐藏幕后的某些程序性内容……但声明式语法和 API 仍然更好。阻止复杂性反复出现在你的代码中是一件好事。

            如果我们今天要添加对象字面量,人们可能会提出所有相同的抱怨。“这隐藏了所有幕后的 Object.defineProperty() 调用!现在没人会理解属性属性!没人会理解对象可以具有任何你想要的原型!这使用属性赋值已经可以实现。看看我的三行玩具函数,它从数组构建类似于对象字面量的东西——这太好了!这将迫使每个人都使用自己的属性来完成所有操作。”

            但对象字面量实际上非常好用。

            > 我认为这篇文章很有意思:…

            关于 Christian 的文章,我想说几点。我希望这两点都能成为好消息!

            第一,Christian 似乎认为 class 是对其他所有内容的排他性替代,而不是可以与他已知技巧结合使用的构建块。后续文章机会!

            第二,类并没有将开发人员“锁定”在一个编程模型中。如果你使用过任何比 Java 和 Ruby 更新的语言,你就会知道,那些不需要所有东西都是类的语言,实际上并没有一种“所有东西必须是类”的文化。C++ 不是。Python 不是。F# 也不是。JavaScript 也不会。

            ES6 类是一个方便的工具。我喜欢使用它们。就像所有好的东西一样,适度使用才是最好的。

            2015 年 7 月 30 日 上午 10:28

          2. Cris Mooney

            我应该先查一下“Kowtow”的拼写。但我感谢你对我失误的创造性回应,所以我也会接受我的错误。

            我们这里存在一个“class 大锤”问题,即“class”这个词语带来的势头和人类的自然忠诚,被扔进了一个已经拥有有趣对象范式的竞技场。虽然 Jason 可能声称最好的东西会得到使用,但事实并非如此。当然,他可能很出色,可以超越人类。但独特的人不能代表一般人。

            实际上,我们会看到大量程序员出于熟悉感而只使用新的、高曝光度的“class”,而不会考虑他们不理解的选项。容易做的事情是大多数人会做的事情。而且,流行并不总是等于聪明。JavaScript 的成功在很大程度上源于它本身,而事实是,这可能会被稀释和丢失。

            这就是“Kowtow”发挥作用的地方:我目前并不支持简单地将方孔转换成圆孔——我也喜欢圆孔,并认为许多没有使用过圆孔的人也会大吃一惊。当语言发生改变时,应该有比“我的牛路径就是这样”更令人信服的理由。相反,保留原始路径的优点,并兼容地添加另一个路径的优点。在我几十年的编程生涯中,我看到了太多用 Y 简单地替换 X,而不是努力找到一个新的 Z。术语“class”的权重太大,除了替换之外,它不会做任何其他事情,至少在短期内是这样。

            我们似乎都同意,使用对象和继承形式进行组织是有用的,而且我们中的许多人认为“new”,“.prototype”和“defineProperty”语法在这方面比较弱。

            但是,“class”并不是组织/沟通的唯一替代方法。我们有 Object.create() 和返回字面量的函数,它们也可以清晰、简洁,而且可以说更具进步意义。我认为这些方法可以进行更少的修改,从而改进语言,同时促使人们欣赏语言的独特价值。就像对 inline 支持 defineProperty 功能的增强一样,一些更小的更改可以添加所需的增强功能,提高清晰度和简洁性,而不会鼓励过度的“class”浪潮。

            进一步谈论这些增强功能,虽然我不认同不可变继承原型链至关重要的观点,但我支持它作为一项有用的功能,可以方便地访问,并在适用时使用,以支持那些对它着迷的人。但这不应该是默认选项,也不应该成为首要选择,因为这会促进过度使用。

            所以,虽然我理解这些变化已经到来,但我仍然不清楚为什么选择了这条路。我仍然持观望态度,不太可能使用它,但我仍然好奇,是什么原因阻止了一条更具进步意义的适度之路。

            对我来说,这仍然看起来像是那些喜欢 class 的人,除非它看起来完全符合他们的熟悉程度,否则他们不会满意。是的,改变是值得的,可以使其更紧凑,并增加更多功能,但它们不需要偏离得太远。

            我只考虑过“格式/简洁”方面的可能性,我相信其他人可以做得更好,但请考虑/批评我在 https://jsfiddle.net/0tw153g3/ 中的初步思考。有些人会觉得我的示例/测试显而易见且微不足道,其他人可能会受到启发而产生新的想法。无论如何,这只是一个良好的开端,就像其他人所做的那样,我认为语言中的一些微调已经足够了,不需要完全抛弃 JavaScript 中经常被低估的价值。也许这就是 Erik Elliott 在 https://github.com/stampit-org/stampit 中改进的地方,但我想看看我的想法,然后再被其他特定实现所影响。

            最后我想提醒大家,无论是 class 语法还是其他语法,实际定义的语法/结构,应该只占任何需要大量组织的严肃程序的一小部分。关于我们想要计算机实际执行的操作的逻辑指令(我敢说程序性)仍然是那些对某种语言投入大量精力的人的主要目的。所以,我认为,在这个特定领域中的简洁和简单,有点被夸大了,不需要引入这样一个有争议、负荷过重、高曝光度的词语。

            至少,这就是我目前的想法,直到我被一些不那么“我非常喜欢类”的理由所启发。我仍然没有看到任何理由不去添加人们喜欢 class 的东西,而不是使用大锤。

            感谢你的谈话!我保证现在会闭嘴了。

            2015 年 7 月 30 日 下午 12:48

  14. PhistucK

    @Cris Mooney –
    我认为你加入游戏太晚了,因为规范已经最终确定了…… :( 你有大约两年的时间向相关标准机构提交反馈(你提交了吗)……

    无论如何,可配置性问题(我假设你指的是这个,比如,{configurable: false, enumerable: true} 或者类似的)可以用 ECMAScript 7 (2016?) 装饰器解决,如果它们被接受的话 -
    https://github.com/wycats/javascript-decorators
    它可能也能解决其他类似问题。

    2015 年 7 月 29 日 上午 8:18

  15. Cris Mooney

    @PhistucK,我并不是来改变规范的,只是想弄清楚自己错过了什么。我知道很多非常聪明的人参与其中,我不相信他们会错过像我这样的另一个厨师在厨房里。

    我真的很想知道我的问题是否有效,希望听到一些能帮助我以及可能帮助其他人理解为什么做出这些选择,以便我们能更全面地加入其中。

    @Eric Elliott 和其他人提出了很好的观点,我知道像 @Allen Wirfs-Brock 这样的其他人也表示他们有深刻的内部知识。我不想听到“太晚了”,但既然这似乎是一个互动和教育性的论坛,我希望听到他们帮助我理解我还没有考虑到的东西。

    切线:你只能假设我指的是“没有办法在定义函数时将属性添加到函数上”,以及(在这个教育性网页调查中)“类”的明显解决方案,因为这是我写的内容。

    2015 年 7 月 29 日 上午 8:30

  16. simonleung

    如果你不提类继承,引入类语法是毫无意义的,而类继承在 ES5 构造函数中是缺失的。

    2015 年 7 月 29 日 上午 9:58

  17. yunpeng

    FYI:在文章中,dfferentiate =》differentiate,:-)

    2015 年 8 月 6 日 下午 8:39

    1. Havi Hoffman [编辑]

      @yunpeng 谢谢,观察得很仔细。已修复。:-)

      2015 年 8 月 7 日 上午 11:37

  18. markg

    非常有用的文章,谢谢。在代码示例中,你可以把行长缩短一点,这样你就不用来回滚动才能阅读注释了。该栏很窄。

    2015 年 8 月 8 日 下午 2:07

本文的评论已关闭。