JS中无处不在的元编程

Posted 哇喔WEB

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JS中无处不在的元编程相关的知识,希望对你有一定的参考价值。

什么是元编程?

维基百科:

元编程 (meta programming)是一种编程技术,编写出来的计算机程序能够将其他程序作为数据来处理。

意味着可以编写出这样的程序:它能够读取、生成、分析或者转换其它程序,甚至在运行时修改程序自身(反射)。

元编程中的 的概念可以理解为 程序 本身,元编程关注以下的一点或几点:

  • 1、运行时修改语言结构,这种现象被称为 反射编程反射

    • 自省:代码检视自己;
    • 自我修改:代码修改自己;
    • 调解:代码修改默认的语言行为而使其他代码受影响;
  • 2、生成代码;

一、自省

代码能够自我检查、访问内部属性,获得代码的底层信息!

// 访问对象自身属性
var users = {
    \'Tom\': 32,
    \'Bill\': 50,
    \'Sam\': 65
};

Object.keys(users).forEach(name => {
    const age = users[name];
    console.log(`User ${name} is ${age} years old!`);
});

// 输出结果
User Tom is 32 years old!
User Bill is 50 years old!
User Sam is 65 years old!

自省在平时的业务开发中很常见,这也是元编程技术的一种使用!

二、自我修改

顾名思义,代码可以修改自身属性或者其他底层信息!

let a = 1;
if (a == 1 && a == 2 && a == 3) {
    console.log("元编程");
}

上述代码在正常情况下是无论如何也无法满足条件输出,

因为一个值不可能同时满足等于1、2、3;但是,利用元编程就可以实现:

// 修改自身
let a = {
    [Symbol.toPrimitive]: ((i) => () => ++i)(0);
}

if (a == 1 && a == 2 && a == 3) {
    console.log("元编程");
}

// 输出 \'元编程\'

Symbol.toPrimitive 是一个内置的 Symbol 值,它是作为对象的函数值属性存在的;

在对象转换为原始值的时候会被调用,初始值为1,调用一次+1,就可以满足a == 1 && a == 2 && a == 3

上述函数变形为:

let a = {
    [Symbol.toPrimitive]: (function (i){
        return function (){
            return ++i
        }
    })(0)
}

if (a == 1 && a == 2 && a == 3) {
    console.log("元编程");
}

在开发过程中自我修改应该要尽力避免,可以想象:正在使用一个数据的同时又在修改这个数据,后容易造成不可预期的错误!

三、调解

代码修改默认的语言行为而使其他代码受影响,最明显的体现为改变其它对象的语义!

在元编程中,调解的概念类似于包装、捕获、拦截。

Object.defineProperty()就是典型的 调解 的运用:

var sun = {};
Object.defineProperty(sun, \'rises\', {
    value: true,
    configurable: false,
    writable: false,
    enumerable: false
});

console.log(\'sun rises\', sun.rises);
sun.rises = false;
console.log(\'sun rises\', sun.rises);

// 输出
sun rises true
sun rises true

上面例子中,新创建了一个普通对象 sun,之后通过Object.defineProperty 改变了它的语义:为其定义了一个不可写的 rises 属性。

四、Reflect & Proxy

MDN:

从ECMAScript 2015 开始,javascript 获得了 ProxyReflect 对象的支持,允许你拦截并定义基本语言操作的自定义行为(例如,属性查找,赋值,枚举,函数调用等)。借助这两个对象,你可以在 JavaScript 元级别进行编程。
// Proxy的handler 和 Reflect 对象 13 个方法

.apply()                                                        // 对一个函数进行调用操作, 和 Function.prototype.apply() 功能类似
.construct()                                                // 对构造函数进行 new 操作,相当于执行 new target(...args)
.get()                                                            // 获取对象身上某个属性的值,类似于 target[name]。
.has()                                                            // 判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同
.ownKeys()                                                    // 返回一个包含所有自身属性(不包含继承属性)的数组,类似于 Object.keys()
.set()                                                            // 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true
.setPrototypeOf()                                        // 设置对象原型的函数. 返回一个 Boolean, 如果更新成功,则返回true。
.defineProperty()                                        // Object.defineProperty() 类似
.deleteProperty()                                        // 作为函数的delete操作符,相当于执行 delete target[name]。
.getOwnPropertyDescriptor()                    //对象中存在该属性,则返回对应的属性描述符,类似于 Object.getOwnPropertyDescriptor()
.getPrototypeOf()                                        // 类似于 Object.getPrototypeOf()。
.isExtensible()                                            // 类似于 Object.isExtensible()
.preventExtensions()                                // 类似于 Object.preventExtensions()。返回一个Boolean。

1、Reflect

Reflect 是一个内置对象,它提供了可拦截 JavaScript 操作的方法。

该方法和Proxy的handler类似,但 Reflect 方法并不是一个函数对象,因此它是不可构造的。

Reflect的所有属性和方法都是静态的(就像Math对象);

Reflect.has() 为例,与 in 运算符对比,检测一个对象是否存在特定属性:

Reflect.has(Object, "assign");     // true
"assign" in Object;                         // true

2、Proxy

在 ECMAScript 6 中引入的 Proxy 对象可以拦截某些操作并实现自定义行为。

例如获取一个对象上的属性:

let handler = {
  get: function(target, name){
    return name in target ? target[name] : 42;
}};

let p = new Proxy({}, handler);
p.a = 1;

console.log(p.a, p.b); // 1, 42

Proxy 对象定义了一个目标(这里是一个空对象)和一个实现了 get 劫持的 handler 对象。

代理的对象在获取未定义的属性时不会返回 undefined,而是返回 42。

五、生成代码

js中使用元编程技术生成代码最常见的函数 eval():函数会将传入的字符串当做 JavaScript 代码进行执行。

let str = "function sayHello(){console.log(\'hello\')}";
eval(str);
sayHello();

// 输出
hello

六、总结

元编程是当你将程序的逻辑转向关注它自身(或者它的运行时环境)时进行的编程,要么为了调查它自己的结构,要么为了修改它。

元编程的主要价值是扩展语言的普通机制来提供额外的能力

参考

  • MDN:元编程
  • JavaScript 元编程
  • 浅谈es6中的元编程

以上是关于JS中无处不在的元编程的主要内容,如果未能解决你的问题,请参考以下文章

VSCode自定义代码片段——JS中的面向对象编程

VSCode自定义代码片段9——JS中的面向对象编程

深刻理解Python中的元类(metaclass)

深刻理解Python中的元类(metaclass)

深刻理解Python中的元类(metaclass)

深刻理解Python中的元类(metaclass)