如何以及为什么要编写一个继承null的类?

71
JavaScript的class syntax,在ES6中添加,显然使得扩展null合法:
class foo extends null {}

new foo();

通过一些搜索发现,在ES Discuss上有人建议这样的声明应该被视为错误;然而,其他评论者则认为这样的声明应该保持合法,理由是:

某些人可能想创建一个具有 {__proto__: null} 原型的类

最终,对这个争论的那一方获胜了。
我对这个假设使用案例感到很困惑。首先,虽然这种类的声明是合法的,但似乎以这种方式声明的类无法实例化。在 Node.js 或 Chrome 中尝试实例化上述的 `foo` 类会给出一个非常愚蠢的错误信息:

TypeError: function is not a function

而在 Firefox 中进行相同操作会得到:

TypeError: function () {
} is not a constructor

在这个特性的 MDN 当前示例中,为类定义一个构造函数也没有帮助;如果我尝试实例化这个类:
class bar extends null {
  constructor() {}
}

new bar();

然后Chrome/Node告诉我:

ReferenceError: this未定义

而Firefox告诉我:

ReferenceError: |this|在bar类构造函数中未初始化使用

这是什么疯狂的事情?为什么这些扩展了null的类不能被实例化?既然它们不能被实例化,为什么规范中还故意保留了创建它们的可能性?为什么一些MDN作者认为这值得记录呢?这个特性有什么可能的用途?


1
constructor返回Object.create(null)作为替代方案。这显然可以避免自动调用super。https://jsfiddle.net/w7y2mbcd/ 但我猜它实际上不是该类的成员。 - user1106925
2
也许可以返回 Object.create(bar.prototype)。https://jsfiddle.net/w7y2mbcd/1/ - user1106925
1
我认为有时候人们只是想要一个干净的对象。在我们有 Map 之前,这更加重要,因此他们会使用 Object.create(null) 作为通用映射来保存不预先知道的任意属性,以便你可以保证任何属性查找都来自于与对象的交互而不是默认继承。现在似乎不那么重要了,但另一方面可能仍然存在一些用例。虽然不知道它们是什么。 - user1106925
在我看来,如果没有父构造函数,最好不要绕过一些麻烦的步骤来调用super。但是我也没有认真思考过这个问题。 - user1106925
1
就此而言,规范指出extends后面可以跟任何LeftHandSideExpression,这基本上意味着任何可以放在=左边的东西都可以。这包括像foo()null这样的东西。现在,null = 42;foo() = 42都没有意义,但它们是有效的,并且会抛出运行时错误。另一方面,class Foo extends bar() {}是有意义的。编辑:嗯,foo() = 42null = 42似乎是早期错误,几乎像语法错误:-/ - Felix Kling
显示剩余4条评论
4个回答

31

编辑(2021年):TC39,负责规定JavaScript标准的组织,仍未解决这个问题。在浏览器可以始终如一地实现它之前,这个问题需要解决。您可以在此处跟踪最新的努力。


原始回答

创建这些类是有意为之的,但Chrome和Firefox存在缺陷。这里是Chrome的bug,这里是Firefox的bug。Safari上运行良好(至少在主分支上)。

以前规范中有一个bug使得它们无法实例化,但已经修复了一段时间。(还有一个相关的问题,但那不是你看到的问题。)

使用情况与Object.create(null)大致相同。有时您想要的是不继承于Object.prototype的东西。


那么假设我想要连接每个对象的原型,但是除了我使用 extend nullObject.create(null) 创建的一个对象? - Stefan Rein
2
看起来关于 extends null 的规范已经改变了,因此Chrome不会修复这个错误 - Decade Moon
显然这个问题还没有解决。他们为什么花这么长时间? - user3840170

15
回答第二部分:
我对这个假设的使用情况无法理解。
这样,你的对象在其原型链中就不会有Object.prototype。
class Hash extends null {}
var o = {};
var hash = new Hash;
o["foo"] = 5;
hash["foo"] = 5;
// both are used as hash maps (or use `Map`).
hash["toString"]; // undefined
o["toString"]; // function

据我们所知,undefined 实际上不是一个函数。在这种情况下,我们可以创建对象而不担心调用不存在的字段。

这是一种常见的模式,通过 Object.create(null) 实现,并且在许多代码库中都很常见,包括像 Node.js 这样的大型项目。

(注意:new class Foo extends null {} 创建的对象原型为空对象,而 Object.create(null) 创建的对象原型为 null)


new class extends null {}Object.create(null)不是同义词。前者(嗯,如果我们假装它实际上能工作的话)有自己的原型(只是它不继承自Object.prototype),而后者则没有。 - user3840170
@user3840170 是的,你说得对。这个答案是在“班级改革”规范变更之前发布的,我会进行更新。 - Benjamin Gruenbaum
等等,实际上在我的回答中哪里说它们是一样的? - Benjamin Gruenbaum
在最后一段中,“这是通过Object.create(null)的常见模式”意味着它们是等价的。 - user3840170

4
事实上,确实可以构建一个扩展null的类,如下所示:

class bar extends null {
    constructor() {
        super() //required
    }
}

//super is not a function
//change the super class (for static methods only)
Object.setPrototypeOf(bar, class {});

let instance = new bar();
//instances still have null prototype
console.log(instance.toString); //undefined


在类体内部,也可以写成 static { Object.setPrototypeOf(this, class {}); }。如果你提到为什么 Object.create 的解决方法效果不太好,这个答案也会更好。 - undefined
如果你希望delete bar.prototype.constructor后它成为空对象,你可以这样做。 - undefined

1
这个回答并没有解答“如何使 class extends null”起作用,但是解释了如何创建一个空原型类。
如果你希望你的类原型具有一个 null 原型,只需将原型的原型设置为 null 即可。
如果你希望你的类是完全空的(instanceof 仍然可以工作),你还可以删除 prototype.constructor
class NullProtoRecord {
  static {
    Object.setPrototypeOf(this.prototype, null);
    // @ts-ignore
    delete this.prototype.constructor;
  }
}
console.log(new NullProtoRecord());
// NullProtoRecord {}
console.log('is NullProtoRecord:', new NullProtoRecord() instanceof NullProtoRecord);
// is NullProtoRecord: true 
console.log('is Object:', new NullProtoRecord() instanceof Object);
// is Object: false 
console.log('all keys:', allKeys(new NullProtoRecord()));
// all keys: [] 

function allKeys(o) {
  if (!o) return [];
  return [...Object.getOwnPropertyNames(o), ...allKeys(Object.getPrototypeOf(o))];
}

enter image description here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接