Libx

元编程

Word count: 2,278Reading time: 8 min
2018/10/24 Share

在1024这一天,先开个坑叭。

什么是原编程(Mata Programming)

很早之前就接触了元编程的一些东西,但是并没有什么很好的理解,只是觉得这个名字好牛逼,很玄幻🐸。主要还是在实际的coding中并没有直接的感触,最近又看到了相关的概念所以来简单的写点东西。

不准确的翻译

在《你不知道的JS-ES6与未来》有这样的解释:元编程是针对程序本身的行为进行操作的编程。换句话说,它是为你程序的编程而进行的编程。 是的,很拗口。也有更详细些的这样的解释:元编程(笼统地说)是所有关于一门语言的底层机制,而不是数据建模或者业务逻辑那些高级抽象。如果程序可以被描述为 “制作程序”,元编程就能被描述为 “让程序来制作程序”。你可能已经在日常编程中不知不觉地使用到了元编程。

在接触了相关的概念之后,觉得似乎和想象中的不太一样,所以想到是否存在翻译的误解,首先从元这个字来理解好像有些问题,在中文的环境下元这个字在理解中有这样的理解:基本:单元,元件,元气(精气,根本),元素,元音。 而在概念中好像并不是这种意思,看到了有人解释这个meta的意思然后就有些理解了:

Meta- 这个前缀在希腊语中的本意是「在…后,越过…的」,类似于拉丁语的 post-,比如 metaphysics 就是「在物理学之后」,这个词最开始指一些亚里士多德的著作,因为它们通常排序在《物理学》之后。但西方哲学界在几千年中渐渐赋予该词缀一种全新的意义:关于某事自身的某事。比如 meta-knowledge 就是「关于知识本身的知识」,meta-data 就是「关于数据的数据」,meta-language 就是「关于语言的语言」,而 meta-programming 也是由此而来,是「关于编程的编程」。
弄清了词源和字面意思,可知大陆将 meta- 这个前缀译为「元」并不恰当。台湾译为「后设」,稍微好一点点,但仍旧无法望文生义。也许「自相关」是个不错的选择,「自相关数据」、「自相关语言」、「自相关编程」——但是好像又太罗嗦了。

这个翻译似乎有些问题。

这里举一个例子:

例如,如果你为了调查对象a和另一个对象b之间的关系 —— 它们是被[[Prototype]]链接的吗? —— 而使用a.isPrototypeOf(b),这通常称为自省,就是一种形式的元编程。宏(JS中还没有) —— 代码在编译时修改自己 —— 是元编程的另一个明显的例子。使用for..in循环枚举一个对象的键,或者检查一个对象是否是一个“类构造器”的 实例,是另一些常见的元编程任务。

所关注的内容

元编程关注以下的一点或几点:代码检视自己,代码修改自己,或者代码修改默认的语言行为而使其他代码受影响。简单的来概括,元编程中关注的方面: 代码生成(Code Generation) 反射(Reflection)

元编程的目标是利用语言自身的内在能力使你其他部分的代码更具描述性,表现力,和/或灵活性。由于元编程的 元 的性质,要给它一个更精确的定义有些困难。理解元编程的最佳方法是通过代码来观察它。
ES6在JS已经拥有的东西上,增加了几种新的元编程形式/特性。

Symbols 实现了的反射(Reflection within implementation)—— 你将 Symbols 应用到你已有的类和对象上去改变它们的行为。
Reflect 通过自省(introspection)实现反射(Reflection through introspection) —— 通常用来探索非常底层的代码信息。
Proxy 通过调解(intercession)实现反射(Reflection through intercession) —— 包裹对象并通过自陷(trap)来拦截对象行为。

Name

来看一个函数名的问题:这似乎是一个非常简单的不值一提的问题,但是JS就是这样,他会给你非常多的惊喜,答案会有些令人诧异地模糊。考虑如下代码:

function daz() {}
var obj = {
foo: function() {},
bar: function baz() {},
bam: daz,
zim() {}
};

在这前一个代码段中,“obj.foo()的名字是什么?”有些微妙。是”foo”,””,还是undefined?那么obj.bar()呢 —— 是”bar”还是”baz”?obj.bam()称为”bam”还是”daz”?obj.zim()呢?

另外,作为回调被传递的函数呢?就像:

function foo(cb) {
// `cb()` 的名字是什么?
}
foo( function(){
// 我是匿名的!
} );

在程序中函数可以被好几种方法所表达,而函数的“名字”应当是什么并不总是那么清晰和明确。更重要的是,我们需要区别函数的“名字”是指它的name属性 —— 是的,函数有一个叫做name的属性 —— 还是指它词法绑定的名称,比如在function bar() { .. }中的bar。
词法绑定名称是你将在递归之类的东西中所使用的:

function foo(i) {
if (i < 10) return foo( i * 2 );
return i;
}

name属性是你为了元编程而使用的,所以它才是我们在这里的讨论中所关注的。
产生这种用困惑是因为,在默认情况下一个函数的词法名称(如果有的话)也会被设置为它的name属性。实际上,ES5(和以前的)语言规范中并没有官方要求这种行为。name属性的设置是一种非标准,但依然相当可靠的行为。在ES6中,它已经被标准化。

推断

但如果函数没有词法名称,name属性会怎么样呢?现在在ES6中,有一个推断规则可以判定一个合理的name属性值来赋予一个函数,即使它没有词法名称可用。
考虑如下代码:

var abc = function() {};
abc.name; // "abc"

如果我们给了这个函数一个词法名称,比如abc = function def() { .. },那么name属性将理所当然地是”def”。但是由于缺少词法名称,直观上名称”abc”看起来很合适。
这里是在ES6中将会(或不会)进行名称推断的其他形式:

(function(){ .. }); // name:
(function*(){ .. }); // name:
window.foo = function(){ .. }; // name:
class Awesome {
constructor() { .. } // name: Awesome
funny() { .. } // name: funny
}
var c = class Awesome { .. }; // name: Awesome
var o = {
foo() { .. }, // name: foo
*bar() { .. }, // name: bar
baz: () => { .. }, // name: baz
bam: function(){ .. }, // name: bam
get qux() { .. }, // name: get qux
set fuz() { .. }, // name: set fuz
["b" + "iz"]:
function(){ .. }, // name: biz
[Symbol( "buz" )]:
function(){ .. } // name: [buz]
};
var x = o.foo.bind( o ); // name: bound foo
(function(){ .. }).bind( o ); // name: bound
export default function() { .. } // name: default
var y = new Function(); // name: anonymous
var GeneratorFunction =
function*(){}.__proto__.constructor;
var z = new GeneratorFunction(); // name: anonymous

name属性默认是不可写的,但它是可配置的,这意味着如果有需要,你可以使用Object.defineProperty(..)来手动改变它。

元属性

在“new.target”中,我们引入了一个ES6的新概念:元属性。正如这个名称所暗示的,元属性意在以一种属性访问的形式提供特殊的元信息,而这在以前是不可能的。
在new.target的情况下,关键字new作为一个属性访问的上下文环境。显然new本身不是一个对象,这使得这种能力很特殊。然而,当new.target被用于一个构造器调用(一个使用new调用的函数/方法)内部时,new变成了一个虚拟上下文环境,如此new.target就可以指代这个new调用的目标构造器。
这是一个元编程操作的典型例子,因为它的意图是从一个构造器调用内部判定原来的new的目标是什么,这一般是为了自省(检查类型/结构)或者静态属性访问。
举例来说,你可能想根据一个构造器是被直接调用,还是通过一个子类进行调用,来使它有不同的行为:

class Parent {
constructor() {
if (new.target === Parent) {
console.log( "Parent instantiated" );
}
else {
console.log( "A child instantiated" );
}
}
}
class Child extends Parent {}
var a = new Parent();
// Parent instantiated
var b = new Child();
// A child instantiated

这里有一个微妙的地方,在Parent类定义内部的constructor()实际上被给予了这个类的词法名称(Parent),即便语法暗示着这个类是一个与构造器分离的不同实体。

Symbol

待续

Proxy & Reflect

写在最后

从对匿名函数的函数名推断,到告诉你一个构造器是如何被调用的元属性,你可以前所未有地在程序运行期间来调查它的结构。通用Symbols允许你覆盖固有的行为,比如将一个对象转换为一个基本类型值的强制转换。代理可以拦截并自定义各种在对象上的底层操作,而且Reflect提供了模拟它们的工具。

参考文章:
[译]ES6 中的元编程 系列
You-Dont-Know-JS
MDN-元编程

CATALOG
  1. 1. 什么是原编程(Mata Programming)
  2. 2. 不准确的翻译
  3. 3. 所关注的内容
  4. 4. Name
    1. 4.1. 推断
    2. 4.2. 元属性
  5. 5. Symbol
  6. 6. Proxy & Reflect
  7. 7. 写在最后