js的类能否继承自null?

发布于
js class extends null

背景

class关键字以及相关的面向对象的编程模式,是es6引入的典型语法之一,这个应该大部分同学应该会用,或者至少八股是背过的,应该没有什么疑问。

但是今天我突发奇想:如果我想要一个“干净”的类,即排除掉Object类所附带的那些toString()isPrototypeOf()等方法的类,那我应该怎么写这个类?

试着写了一下class A extends null,嗯,编译器没有报错;但是深究一下,我发现事情没有想象得那么简单。

参考文章: How and why would I write a class that extends null?

哪些能写、哪些不能写?

首先是最基础的写法,没有任何花里胡哨:

class A extends null {}

这个类本身被正常地声明了,但是它却无法被实例化为对象:

a = new A();
/**
 * chrome 和 node.js 中的运行结果:
 VM524:1 Uncaught TypeError: Super constructor null of A is not a constructor
   at new A (<anonymous>:1:1)
   at <anonymous>:1:5
 */

看起来是对象实例化的时候,super()遇到了问题,可是我们又不能强制不写super,像下面的写法依然无法运行:

class A extends null {
  constructor() {}
}

a = new A();
/**
 VM591:2 Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
   at new A (<anonymous>:2:16)
   at <anonymous>:1:5
 */

上面报错的原因是没有追溯到原型链顶层,因此没有挂上this,于是我们可以尝试按照报错提示,自己return一个作为this:

class B extends null {
  constructor() {
    return Object.create(null);
  }
}

b = new B(); // 运行结果:{}

ok,像上面的写法得到的b对象,确实达到了我们的初衷,也就是得到一个“干净的”对象。但是再深入挖掘一下,会遇到一个很诡异的问题:

b instanceof B; // false

不仅b不是B的实例,同时b也无法访问在B中声明的成员函数,这都是原型链不完整所导致的。

为了解决这个问题,我们可以参照原帖中的回答,用一个特别的原型来实例化对象:

class C extends null {
  constructor() {
    return Object.create(C.prototype);
  }

  call() {
    console.log('hello!');
  }
}

c = new C(); // C {}
c instanceof C; // true
c.call; // function
c.constructor; // function

此时的c对象,只有两个成员方法,一个是C类中声明的方法call,一个是类的原型所携带的constructor,可后者是我们并不想要的东西,是多余的。

底层原因是js的类是由“原型链”的机制来模拟实现的,而不像其他语言那样类本身与对象有本质上的区别(参考)。因此不论如何尝试,都不可能做出一个“完全干净”、同时还能保持“面向对象”功能的对象。

那么,正确的语法是什么?

参考es7规范文档,其中定义了,extends关键字后面跟随的应当是LeftHandSideExpression,中译名左值表达式

熟悉c++的语言的同学应该都了解所谓左值表达式,在js中举个例子,下面的例子会抛出运行时异常:

null = 123;  // Uncaught SyntaxError: Invalid left-hand side in assignment

或者:

function A() {return {}};
A() = 123;  // Uncaught SyntaxError: Invalid left-hand side in assignment

参考阅读:如何理解左值

简而言之,null和函数直接的返回值不能作为左值进行赋值,会抛出运行时错误;但是,虽然es7规范中说extends右侧只能是左值表达式,chrome的实现却可以用null和函数返回值。null的例子前面已经说过了,再看一下函数返回值的写法作为例子:

function A() {
  return Object;
}
class B extends A() {} // 没有抛出异常!
a = new A(); // 没有抛出异常!

结论

不能。

js类的功能的实现必须依赖一些特殊的成员属性/成员方法,因此无法创造出一个完全干净又带有完整面向对象能力的类。“js的类能否继承自null”这个思路也没有实际价值,有其他更好的思路可以替代。

js规范是不支持继承自null的,但实际chrome引擎的实现是部分允许的。