《JavaScript权威指南第7版》第14章 元编程

本章介绍了一些在日常编程中不常用的高级JavaScript特性,但对于编写可重用库的程序员来说,这些特性可能很有价值,并且对于任何想要修改JavaScript对象行为细节的人来说都很有兴趣。

这里描述的许多特性可以粗略地描述为“元编程”:如果常规编程是编写代码来操作数据,那么元编程就是编写代码来操作其他代码。在像JavaScript这样的动态语言中,编程和元编程之间的界限是模糊的,即使使用for/in循环遍历对象属性的简单能力也可能被习惯于更加静态语言的程序员视为“元”。

本章涉及的元编程主题包括:

  • §14.1 控制对象属性的可枚举性、可删除性和可配置性
  • §14.2 控制对象的可扩展性,并创建“密封”和“冻结”对象
  • §14.3 查询和设置对象原型
  • §14.4 使用预定义符号微调类型的行为
  • §14.5 使用模板标签函数创建DSL(领域特定语言)
  • §14.6 用反射方法探测对象
  • §14.7 使用代理控制对象行为

14.1 属性特性 (Property Attributes)

当然,JavaScript对象的属性有名称和值,但是每个属性也有三个关联的特性,这些特性指定了该属性的行为方式以及可以对其执行的操作:

  • writable(可写)特性指定属性的值是否可以更改。
  • enumerable(可枚举)特性指定属性是否可以由for/in循环和Object.keys()方法进行枚举。
  • configurable(可配置)特性指定属性是否可以删除,以及属性的特性是否可以更改。

在对象字面量中或通过对对象的普通赋值定义的属性是可写、可枚举和可配置的。但是JavaScript标准库定义的许多属性都不是。

本节介绍用于查询和设置属性特性的API。此API对库作者特别重要,因为:

  • 它允许他们向原型对象添加方法,并使它们不可枚举,就像内置方法一样。
  • 它允许他们“锁定”他们的对象,定义不能更改或删除的属性。

回想一下§6.10.6,“数据属性”有一个值,“访问器属性”有一个getter和/或setter方法。在本节中,我们将把访问器属性的getter和setter方法视为属性特性。按照这个逻辑,我们甚至可以说数据属性的值也是一个特性。因此,我们可以说一个属性有一个名字和四个特性。数据属性的四个特性是value、writable、enumerable和configurable。访问器属性没有value特性或writable特性:它们的可写性取决于是否有setter。所以访问器属性的四个特性是get、set、enumerable和configurable。

用于查询和设置属性特性的JavaScript方法使用一个称为属性描述符的对象来表示四个特性的集合。属性描述符对象的属性与其所描述的特性的名称相同。因此,数据属性的属性描述符对象具有名为value、writable、enumerable和configurable属性。访问器属性的描述符具有get和set属性,而不是value和writable。可写、可枚举和可配置属性是布尔值,get和set属性是函数值。

要获取指定对象的命名属性的属性描述符,请调用Object.getOwnPropertyDescriptor():

// 返回 {value: 1, writable:true, enumerable:true, configurable:true}
Object.getOwnPropertyDescriptor({
   x: 1}, "x");

// 这是一个具有只读访问器属性的对象
const random = {
   
	get octet() {
    return Math.floor(Math.random()*256); },
};

// 返回 { get: /*func*/, set:undefined, enumerable:true, configurable:true}
Object.getOwnPropertyDescriptor(random, "octet");

// 对于继承属性和不存在的属性返回undefined。
Object.getOwnPropertyDescriptor({
   }, "x") // => undefined; no such prop
Object.getOwnPropertyDescriptor({
   }, "toString") // => undefined; inherited

顾名思义,Object.getOwnPropertyDescriptor()仅适用于自身属性。要查询继承属性的特性,必须显式遍历原型链。(参见§14.3 Object.getPrototypeOf());也可参见§14.6中类似的Reflect.getOwnPropertyDescriptor()函数。)

要设置属性的特性或使用指定的特性创建新属性,请调用Object.defineProperty(),传递要修改的对象、要创建或更改的属性的名称以及属性描述符对象:

let o = {
   }; // 开始时一个属性都没有
// 添加值为1的不可枚举数据属性x。
Object.defineProperty(o, "x", {
   
    value: 1,
    writable: true,
    enumerable: false,
    configurable: true
});

// 检查属性是否存在但不可枚举
o.x // => 1
Object.keys(o) // => []

// 现在修改属性x使其为只读
Object.defineProperty(o, "x", {
    writable: false });
// 尝试更改属性的值
o.x = 2; // 静默失败或在严格模式下抛出TypeError
o.x // => 1

// 属性仍然是可配置的,因此我们可以如下更改其值:
Object.defineProperty(o, "x", {
    value: 2 });
o.x // => 2

// 现在将x从数据属性更改为访问器属性
Object.defineProperty(o, "x", {
    get: function () {
    return 0; } });
o.x // => 0

传递给Object.defineProperty()的属性描述符不必包括所有四个特性。如果要创建新属性,则忽略的特性将被视为false或undefined。如果要修改现有属性,则忽略的特性将保持不变。请注意,此方法更改现有的自身属性或创建新的自身属性,但不会更改继承的属性。另外可以查看§14.6相似的函数Reflect.defineProperty()。

如果要一次创建或修改多个属性,请使用Object.defineProperties()。第一个参数是要修改的对象。第二个参数是一个对象,它将要创建或修改的属性的名称映射到这些属性的属性描述符。例如:

let p = Object.defineProperties({
   }, {
   
    x: {
    value: 1, writable: true, enumerable: true, configurable: true },
    y: {
    value: 1, writable: true, enumerable: true, configurable: true },
    r: {
   
        get() {
    return Math.sqrt(this.x * this.x + this.y * this.y); },
        enumerable: true,
        configurable: true
    }
});
p.r // => Math.SQRT2

这段代码从一个空对象开始,然后添加两个数据属性和一个只读访问器属性。它依赖于这样一个事实:Object.defineProperties()返回修改后的对象(如Object.defineProperty()所做的)。

在§6.2中介绍了Object.create()方法。我们在那里了解到,该方法的第一个参数是新创建的对象的原型对象。此方法还接受第二个可选参数,它与Object.defineProperties()的第二个参数相同。如果将一组属性描述符传递给Object.create(),那么将使用它们向新创建的对象添加属性。

Object.defineProperty()和Object.defineProperties()如果不允许尝试创建或修改属性,则抛出TypeError。如果您试图向不可扩展(见§14.2)对象添加新属性,就会发生这种情况。这些方法可能抛出TypeError的其他原因与特性本身有关。writable特性控制更改value特性的尝试。configurable特性控制更改其他特性的尝试(还指定属性是否可以删除)。然而,这些规则并不完全简单明了。例如,如果不可写属性是可配置的,则可以更改该属性的值。此外,还可以将属性从可写更改为不可写,即使该属性不可配置。以下是完整的规则。调用Object.defineProperty()或Object.defineProperties()时如果试图违反规则,则抛出一个TypeError:

  • 如果对象不可扩展,则可以编辑其现存的自身属性,但不能向其添加新属性。
  • 如果属性不可配置,则无法更改其可配置或可枚举特性。
  • 如果访问器属性不可配置,则不能更改其getter或setter方法,也不能将其更改为数据属性。
  • 如果数据属性不可配置,则不能将其更改为访问器属性。
  • 如果数据属性不可配置,则无法将其可写属性从false更改为true,但可以将其从true更改为false。
  • 如果数据属性不可配置且不可写,则无法更改其值。但是,您可以更改可配置但不可写的属性的值(因为这等同于使其可写,然后更改值,然后将其转换回不可写)。

§6.7描述了Object.assign()函数将属性值从一个或多个源对象复制到目标对象。Object.assign()只复制可枚举属性和属性值,而不复制属性特性。这通常是我们想要的,但它确实意味着,例如,如果一个源对象有一个访问器属性,则是getter函数返回的值被复制到目标对象,而不是getter函数本身。例14-1演示了如何使用Object.getOwnPropertyDescriptor()和Object.defineProperty()以创建Object.assign()的变体,不仅复制属性值,还包括整个属性描述符。

例14-1. 将属性及其特性从一个对象复制到另一个对象

/*
 * 定义一个新的Object.assignDescriptors()函数,与Object.assign()函数类似,
 * 但它将属性描述符从源对象复制到目标对象,而不仅仅是复制属性值。
 * 此函数复制所有自身属性,包括可枚举的和不可枚举的。
 * 因为它复制描述符,所以它从源对象复制getter函数并覆盖目标对象中的setter函数,
 * 而不是调用那些getter和setter。
 *
 * Object.assignDescriptors()传播由Object.defineProperty()引发的任何类型错误。 
 * 如果目标对象被密封或冻结,或者任何源属性试图更改目标对象上现有的不可配置属性,则会发生这种情况。
 *
 * 注意,assignDescriptors属性被Object.defineProperty()添加到对象,
 * 以便可以将这个新函数创建为不可枚举属性,就像Object.assign()函数一样。
 */
Object.defineProperty(Object, "assignDescriptors", {
   
    // 以下3个特性与Object.assign()函数属性特性相同
    writable: true,
    enumerable: false,
    configurable: true,
    // 这个函数是assignDescriptors属性的值
    value: function(target, ...sources) {
   
        for(let source of sources) {
   
            for(let name of Object.getOwnPropertyNames(source)) {
   
                let desc = Object.getOwnPropertyDescriptor(source, name);
                Object.defineProperty(target, name, desc);
            }

            for(let symbol of Object.getOwnPropertySymbols(source)) {
   
                let desc = Object.getOwnPropertyDescriptor(source, symbol);
                Object.defineProperty(target, symbol, desc);
            }
        }
        return target;
    }
});

let o = {
   c: 1, get count() {
   return this.c++;}}; // 使用getter定义对象
let p = Object.assign({
   }, o); // 复制属性值
let q = Object.assignDescriptors({
   }, o); // 复制属性描述符
p.count // => 1: 这只是一个数据属性,所以
p.count // => 1: ...计数器不递增。
q.count // => 2: 当我们第一次复制它的时候增加了一次,
q.count // => 3: ...但是我们复制了getter方法,所以它是递增的。

14.2 对象扩展性

对象的extensible特性指定是否可以向对象添加新属性。默认情况下,普通JavaScript对象是可扩展的,但是您可以使用本节中描述的函数来更改它。

要确定对象是否可扩展,请将其传递给Object.isExtensible()。若要使对象不可扩展,请将其传递给Object.preventExtensions()。完成此操作后,任何向对象添加新属性的尝试都将在严格模式下抛出TypeError,而在非严格模式下静默失败。此外,试图更改不可扩展对象的原型(见§14.3)将始终抛出TypeError。

请注意,一旦将对象设为不可扩展,就无法使其再次可扩展。还请注意Object.preventExtensions()只影响对象本身的扩展性。如果将新属性添加到不可扩展对象的原型中,则不可扩展对象将继承这些新属性。

两个相似的函数,Reflect.isExtensible()和Reflect.preventExtensions(),见§14.6。

extensible特性的目的是能够将对象“锁定”到已知状态,并防止外部篡改。对象的可扩展特性通常与特性的可配置和可写特性结合使用,JavaScript定义了一些函数,可以轻松地将这些特性设置在一起:

  • Object.seal()的工作原理与Object.preventExtensions()相似,但除了使对象不可扩展外,它还使该对象的所有自身属性不可配置。这意味着不能向对象添加新属性,也不能删除或配置现有属性。但是,仍然可以设置可写的现有属性。没有办法打开密封的对象。你可以用Object.isSealed()以确定对象是否已密封。
  • Object.freeze()将对象锁定得更紧。除了使对象不可扩展、属性不可配置外,它还使对象的所有自身的数据属性都是只读的。(如果对象具有带有setter方法的访问器属性,则这些访问器属性不受影响,并且仍然可以通过对属性的赋值来调用它们。)使用Object.isFrozen()以确定对象是否冻结。

理解这一点很重要,Object.seal()和Object.freeze()只影响传递的对象:它们对该对象的原型没有影响。如果您想彻底锁定一个对象,那么您可能还需要密封或冻结原型链中的对象。

Object.preventExtensions(),Object.seal(),和Object.freeze()都返回它们传递的对象,这意味着您可以在嵌套函数调用中使用它们:

// 使用冻结的原型和不可枚举的属性创建密封对象
let o = Object.seal(Object.create(Object
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值