JavaScript的元系统

393 阅读6分钟
原文链接: aimingoo.github.io

JavaScript的元系统

本文是对在第四届FEDay中分享的《无类继承:JavaScript面向对象的根基》的进一步讨论。也是对开源项目@aimingoo/metameta的解析。

本文是一个系列,包括:

  • JavaScript的元系统(本文)
  • JavaScript中创建原子的几种方法 - 在这里
  • 元类型系统是对JavaScript内建概念的补充 - 在这里

以及相关下载:

  • 资源分享:第四届FEDay讲演主题 - 在这里

ECMAScript中只有两处提及到“Meta”这个概念,一处是说明ECMAScript的规范类型(a specification type)是用于描述和实现语言类型(language types)的元值(meta-values),另一处则是唯一被称为“元属性(Meta Property)”的new.target

所以ECMAScript中是没有所谓“元系统(Meta system)”或“元类型系统(Meta type system)”。我们在这里先定义一个称为“原子(Atom)”的东西,并基于此来构建起一个完整的JavaScript元系统。

原子(atom)

定义:原子是JavaScript中的对象的最小单元,它是对象但不继承自Object();以原子为原型的对象也会被称为原子对象。

JavaScript中的对象就是一个属性包(properties bag, or a collection of properties),一个属性包为空集时,它必然是对象的最小形态。因此一个没有原型,且自有属性集为空的对象,必然是一个原子。

原子可以用ES5兼容的语法创建出来:

var atom = Object.create(null);

1

也可以通过将一般对象的原型置为null来得到一个原子:

var atom = Object.setPrototypeOf(new Object, null);

1

并且,在ECMAScript中有三个内建/原生对象是原子的:

function isAtom(x) {
    switch (typeof x) {
        case 'object':
        case 'function': return !(x instanceof Object);
    }
    return false;
}

// modules in es6
import * as namespace from './your-module.js';

console.log(isAtom(null));
console.log(isAtom(Object.prototype));
console.log(isAtom(namespace));

1
2
3
4
5
6
7
8
9
10
11
12
13
14

在同一个运行环境中,可以并存多个原子,以及由原型指向原子的、原型继承的对象系统。所有这些原子以及衍生的对象系统都是互不相等、没有交集的。

> Object.create(null) === Object.create(null)
false

1
2

因此,JavaScript原生的、由Object()派生或创建的对象、类,在本质上也是上述“对象系统”之一。但是,

  • 作为唯一特例,Object()所属的对象系统称为“原生对象系统”,以区别于后来创建的其它原子对象系统。

并且,

  • 作为唯一特例,null值是一个原子(注:原子在ECMAScript约定的ECMAScript language types中不是对象,但在JavaScript自身的类型检查(typeof)中它是对象)。

NOTE(2018.08.28): 修正了一处关于arguments的错误,确认arguments对象不是原子。thanks for hebaby @github

元(meta)

定义:能产生原子(atom)的一个过程称为元(meta)。

推论:原子的构造器(Atom)与元(meta)是等义的。

由于atom对象的构造器通常记为Atom(),所以从概念上它与“元(meta)”是等义的,在实际使用中我们也并不明确地区分二者。

meta可以是一个函数,也可以是一个类,甚至也可以是一个代理对象(proxy)、箭头函数(arrow functions)或方法(methods)。——在概念定义中,我们只约定了“meta是一个过程”,并没有强调atom是它构建出来的,亦或只是它的调用结果。

在开源项目中Metameta(@aimingoo/metameta)中,meta是以ES6的语法声明的一个Atom类:

class Atom extends null {
    constructor() {
        return Object.create(new.target.prototype);
    }
}

1
2
3
4
5

任何情况下,我们用该meta都可以产生新的原子对象:

> isAtom(new Atom)
true

> new Atom === new Atom
false

1
2
3
4
5

元类型(Meta,Meta types)

定义:所有元(meta)的类型称为元类型(Meta types)

在JavaScript中,一个数据所对应的类型可以用它的构造器来标示,亦即是Meta();并且这也意味着Meta()作为构造器产生的实例是元(meta)。亦即是说,Meta()应当是一个“返回meta过程”的过程。

在ES6的语法中,可以简单地在函数中返回一个“类声明(class definitions)”来得到一个字面量风格的类。因此在Metameta中声明了MetaMeta()类来作为元类型的祖先类:

// Meta's super
class MetaMeta extends null {
  constructor(base=Atom) { // Atom() by default, NOTE: 声明在上例
    return Object.setPrototypeOf(class extends new.target {}, base);
  }
...

1
2
3
4
5
6

NOTE(2018.09.09): 修正了一处关于MetaMeta()类的错误,这是因为在本文中为了简化Meta()类的描述而直接暴露了MetaMeta的实现,但又没有处理相关的逻辑所导致的。需要注意的是,在Metameta项目中该类的实现与本文是略有区别的。thanks for nextdoorUncleLiu @github

所以现在,我们就可以通过如下的方法来得到一个原子了:

// Atom与meta是同义的
> Atom2 = meta = new MetaMeta

// 创建一个原子
> atom = new Atom2

// 检测
> isAtom(atom)
true

1
2
3
4
5
6
7
8
9

基于原子的继承性

我们之所以要用class来声明Atom和MetaMeta,是为了简单地得到面向对象的继承性。亦即是说,当我们想要派生一个新的原子对象类型的时候,可以简单地通过扩展上述的系统来得到它的构造器。例如:

class MyAtomObject extends new MetaMeta {
    get description() {
        return 'i am an atom.';
    }
}

var x = new MyAtomObject;
console.log(x.description);

1
2
3
4
5
6
7
8

在这个例子中,new MetaMeta直接创建了一个Atom,而MyAtomObject则派生自该Atom,因此它的实例自然是atom。并且,基于ES6的类声明语法,MyAtomObject也可以具有自己的存取器成员、对象方法,或者类方法。

基于元的继承性

从MetaMeta也可以基于元类型进行派生,由此我们可以实现“元类(Meta class)类型”。

定义:元类(Meta class)是一个产生类(class)的过程。

从定义上来说,简单的元类可以写成:

function SimpleMetaClass() {
    return class {};
}

1
2
3

当然,由于在MetaMeta中“元类型”本身就是基于类实现的——亦即是它本来就是一个“返回类”的过程,因此它只需要简单的一层概念抽象就可以实现“元类”类型了。如下:

// “元(Meta)”类型
class Meta extends MetaMeta { ... }

// “元类(MetaClass)”类型
class MetaClass extends Meta { ... }

1
2
3
4
5

之所以让Meta派生自MetaMeta(),主要目的是为了得到一层super声明,以确保Meta()以及它的类方法(static methods)之于它的super是词法上下文绑定的。而“元类(MetaClass)”则用于派生一层类型声明,以便让MetaClass()能拥有自己的类方法,例如MetaClass.isClassOf()

现在,我们已经在Meta上实现了一层派生,我们也可以实现更多层的派生,以通过“类类型”的方法来得到更多的构造器——换言之,我们可以产生更多的类,它们都可以作为更多的“不同的对象系统的”祖先类。我们可以让JavaScript中出现多个完全不同的、与Object()所代表的“原生对象系统”并列的对象系统。

如前所述的——它们相互独立,没有交集。例如:

// “元类(MetaClass)”产生类
var ObjectEx = new MetaClass;

// 基于ObjectEx可以派生一个“独立的、不同的”对象系统
class MyObjectEx extends ObjectEx {};

// 可以用类似的方法来派生更多这样的对象系统
class MyObjectPlus extends new MetaClass {
    ...
};

1
2
3
4
5
6
7
8
9
10

接下来,你可以检测它们的类属关系:

> ObjectEx.isClassOf(MyObjectEx)
true

> MetaClass.isClassOf(ObjectEx)
true

1
2
3
4
5

或使用ECMAScript内置方法检测原子:

> (new MyObjectEx) instanceof ObjectEx
true

> (new MyObjectEx) instanceof MyObjectPlus
false

1
2
3
4
5