今天我们再来看看“元”编程。那么,元编程中的“元”代表什么呢?“元”有“之上”或“超出一般限制”的意思。
在 JavaScript 中,我们可以把元编程的功能分为几类:第一类是查找和添加对象属性相关的功能;第二类是创建 DSL 这样的特定领域语言;第三类就是可以作为代理用于对象的装饰器。今天,就让我们一一来看一下。
对象属性的属性
1. 对象属性的设置
我们都知道 Javascript 对象的属性包含了名称和值。但是另外,我们需要了解的是,每个属性本身也有三个相关的属性,它们分别为可写属性(writable)、可枚举属性(enumerable)以及可配置属性(configurable)。
这三个属性指定了该属性的行为方式,以及我们可以使用它们做什么。这里的可写属性指定了属性的值是否可以更改;可枚举属性指定了属性是否可以由 for/in 循环和 Object.keys() 方法枚举;可配置属性指定了是否可以删除属性或更改属性的属性。
那么对属性的查询和设置有什么用呢?
这对于很多第三方库的开发者来说是很重要的,因为它允许开发者向原型对象中添加方法,而且也可以像标准库中的很多内置方法一样,将它们设置成不可枚举;同时,它也可以允许开发者“锁定”对象,让属性定义无法被更改或删除。
我们可以将属性分为两类,一类是“数据属性”,一类是“访问器属性”。我们把值、getter、setter 都看做是值的话,那么,数据属性就包含了值、可写、可枚举和可配置这 4 个属性;而访问器属性则包含了 get、set、可枚举和可配置这 4 个属性。其中可写、可枚举和可配置属性是布尔值,get 和 set 属性是函数值。
object.getOwnPropertyDescriptor(): 获取对象属性的属性描述,顾名思义,这个方法只适用于获取对象自身的属性
Object.getPrototypeOf():查询继承属性的属性,就需要通过 Object.getPrototypeOf() 的方法来遍历原型链
Object.defineProperty():如果要设置属性的属性或者使用指定的属性创建新属性的时候用到。
2. 对象的可延展性
除了对对象的属性的获取和设置外,我们对对象本身也可以设置它的可延展性。我们可以通过 Object.isExtensible() 让一个对象可延展,同样的,我们也可以通过 Object.preventExtensions() 将一个对象设置为不可延展。
不过这里需要注意的是,一旦我们把一个对象设置为不可延展,我们不仅不可以在对象上再设置属性,而且我们也不可以再把对象改回为可延展。还有就是这个不可延展只影响对象本身的属性,对于对象的原型对象上的属性来说,是不受影响的。
对象的可延展性通常的作用是用于对象状态的锁定。通常我们把它和属性设置中的可写属性和可配置属性结合来使用。在 JavaScript 中,我们可以通过 Object.seal() 把不可延展属性和不可配置属性结合;通过 Object.freeze() 我们可以把不可延展、不可配置和不可写属性结合起来。
3. 对象的原型对象
前面,我们介绍的 Object.freeze() 、 Object.seal() 和属性设置的方法一样,都是仅作用于对象本身的,都不会对对象的原型造成影响。
通过 new 创建的对象会使用创建函数的原型值作为自己的原型,通过 Object.create() 创建的对象会使用第一个参数作为对象的原型。
Object.getPrototypeOf() 来获取对象的原型;
通过 isPrototypeOf() 我们可以判断一个对象是不是另外一个对象的原型;
通过Object.setPrototypeOf() 修改一个对象的原型;不过有一点需要注意的是,通常在原型已经设置后,就很少被改变了,使用 Object.setPrototypeOf() 有可能对性能产生影响。
用于 DSL 的模版标签
我们知道,在 JavaScript 中,在反引号内的字符串被称为模板字面量。当一个值为函数的表达式,并且后面跟着一个模板字面量时,它会变成一个函数被调用,我们将它称之为“带标签的模板字面量”。
为什么我们说定义一个新的标签函数,用于标签模板字面量可以被当做是一种元编程呢?因为标签模板通常用于定义 DSL,也就是域特定语言,这样定义新的标签函数就如同向 JavaScript 中添加了新的语法。标签模板字面量已被许多前端 JavaScript 库采用。GraphQL 查询语言通过使用 gql `` 标签函数,可以使查询被嵌入到 JavaScript 代码中。Emotion 库使用 css `` 标签函数,使 CSS 样式同样可以被嵌入到 JavaScript 中。
当函数表达式后面有模板字面量时,该函数将被调用。
当我们想将一个值安全地插入到 HTML 字符串中时,模版会非常得有用。我们拿 html`` 为例,在使用标签构建最终字符串之前,标签会对每个值执行 HTML 转义。
function html(str, ...val) {
var escaped = val.map(v => String(v)
.replace("&", "&")
.replace("'", "'"));
var result = str[0];
for(var i = 0; i < escaped.length; i++) {
result += escaped[i] + str[i+1];
}
return result;
}
var operator = "&";
html`<b>x ${operator} y</b>` // => "<b>x & y</b>"
下面,我们再来看看 Reflect 对象。Reflect 并不是一个类,和 Math 对象类似,它的属性只是定义了一组相关的函数。ES6 中添加的这些函数都在一个命名空间中,它们模仿核心语言的行为,并且复制了各种预先存在于对象函数的特性。
尽管 Reflect 函数没有提供任何新功能,但它们确实将这些功能组合在一个 API 中方便使用。比如,我们在上面提到的对象属性的设置、可延展性以及对象的原型对象在 Reflect 中都有对应的方法,如 Reflect.set()、Reflect.isExtensible() 和 Reflect.getPrototypeOf(),等等。下面,我们会看到 Reflect 函数集与 Proxy 的处理程序方法集也可以一一对应。
Proxy 和 Reflect
在 ES6 和更高版本中提供的 Proxy 类可以算是 JavaScript 中最强大的元编程功能了。它允许我们编写改变 JavaScript 对象基本行为的代码。我们在前面提到的 Reflect API 是一组函数,它使我们可以直接访问 JavaScript 对象上的一组基本操作。当我们创建 Proxy 对象时,我们指定了另外两个对象,目标对象和处理程序对象。
var target = {
message1: "hello",
message2: "world",
};
var handler = {};
var proxy = new Proxy(target, handler);
生成的代理对象没有自己的状态或行为。无论何时对其执行操作(读取属性、写入属性、定义新属性、查找原型、将其作为函数调用),它都会将这些操作分派给处理程序对象或目标对象。代理对象支持的操作与 Reflect API 定义的操作相同。Proxy 的工作机制是,如果 handler 是空的,那么代理对象只是一层透明的装饰器。所以在上面的例子中,如果我们执行代理,那么它返回的结果就是目标对象上本来自有的属性。
console.log(proxy.message1); // hello
console.log(proxy.message2); // world
通常,我们会把 Proxy 和 Reflect 结合起来使用,这样的好处是,对于我们不想自定义的部分,我们可以使用 Reflect 来调用对象内置的方法。
const target = {
message1: "hello",
message2: "world",
};
const handler = {
get(target, prop, receiver) {
if (prop === "message2") {
return "Jackson";
}
return Reflect.get(...arguments);
},
};
var proxy = new Proxy(target, handler);
console.log(proxy.message1); // hello
console.log(proxy.message2); // Jackson
此文章为2月Day22学习笔记,2月最后一天,内容来源于极客时间《Jvascript进阶实战课》,大家共同进步💪💪
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。