使用Object.defineProperty来覆盖只读属性

6
在我们的NodeJS应用程序中,我们通过扩展默认的Error对象来定义自定义错误类:
"use strict";
const util = require("util");

function CustomError(message) {
    Error.call(this);
    Error.captureStackTrace(this, CustomError);
    this.message = message;
}

util.inherits(CustomError, Error);

这使得我们可以使用throw CustomError("Something");,正确显示堆栈跟踪,并且instanceof Errorinstanceof CustomError都能正常工作。
但是,在API中返回错误(通过HTTP),我们需要将错误转换为JSON格式。在Error对象上调用JSON.stringify()会导致结果为"{}",这对于消费者来说显然不够描述性。
为了解决这个问题,我想重写CustomError.prototype.toJSON(),返回一个包含错误名称和消息的对象字面量。然后,JSON.stringify()将只对该对象进行字符串化处理,一切都将正常工作:
// after util.inherits call

CustomError.prototype.toJSON = () => ({
    name    : "CustomError",
    message : this.message
});

然而,我很快就发现这会抛出一个 TypeError: Cannot assign to read only property 'toJSON' of Error 。这也许是有道理的,因为我在尝试写入原型。所以我改变了构造函数:

function CustomError(message) {
    Error.call(this);
    Error.captureStackTrace(this, CustomError);
    this.message = message;
    this.toJSON = () => ({
        name    : "CustomError",
        message : this.message
    });
}

我原本期望的是这样,CustomError.toJSON函数会被使用,而Error中的CustomError.prototype.toJSON则会被忽略。

然而,这只会在对象构造时引发错误:Cannot assign to read only property 'toJSON' of CustomError

接下来,我尝试从文件中删除"use strict";,有点解决了问题,因为不再抛出错误,但JSON.stringify()根本没有使用toJSON()函数。

此时,我已经万分绝望,开始尝试随机的方法。最终,我使用Object.defineProperty()代替直接赋值给this.toJSON

function CustomError(message) {
    Error.call(this);
    Error.captureStackTrace(this, CustomError);
    this.message = message;
    Object.defineProperty(this, "toJSON", {
        value: () => ({
            name    : "CustomError",
            message : this.message
        })
    });

这个代码完美地运行了。在严格模式下,没有出现任何错误,并且JSON.stringify()返回了{"name:" CustomError", "message": "Something"},就像我想要的一样。

所以虽然它现在按照我想要的方式工作,但我仍然想知道:

  1. 为什么它能正常工作?我期望它等同于this.toJSON = ...,但显然并不是这样。
  2. 它应该像这样工作吗?也就是说,依赖这种行为是否安全?
  3. 如果不是,我应该如何正确地覆盖toJSON方法?(如果可能的话)

1
请看一下这里我的回答,看看是否是你所询问的内容。谢谢! - Qantas 94 Heavy
@Qantas94Heavy 这确实是非常奇怪的行为,但至少解释了为什么我无法正常地分配属性。从你评论中链接的文章中,我仍然不明白的是:Object.defineProperty是否按设计工作(即是否覆盖只读原型属性),还是这是我不应该依赖的意外行为? - tkers
我遇到了一个 TypeError: 无法重新定义属性: xxxx 的错误。 - undefined
3个回答

3
为什么这样做可以成功? Object.defineProperty 只是定义一个属性(或者修改已存在并可配置的属性的特性)。它不像赋值操作 this.toJSON = … 那样关心继承,并且不会检查是否存在继承来的 setter 或者不可写属性。
这种方法是否安全?能否依赖这种行为?
是的,这种方法是安全的。你甚至可以在原型上使用它。
对于你实际的用例,在最新的 node.js 版本中,使用一个继承自 Error 的 ES6 类会得到最好的结果。

1

我注意到你使用了箭头函数,因此我会假设你可以访问ES6,这意味着你也可以访问类。

你可以简单地扩展Error类。例如:

class CustomError extends Error {
    toJSON() {
        return {
            name: 'CustomError',
            message: this.message
        };
    }
}

好的观点,看起来更清晰!不过,为什么这样做(以及使用Object.defineProperty)有效,而尝试设置this.toJSON却无效呢? - tkers
我其实也不太确定,因为在 Node 4.x 上无法重现。 - Ben Fortune

0

如果你确实需要在<template>元素中使用组件或其他React功能,你可以通过猴子补丁HTMLTemplateElement的一些DOM方法来修复React以在content中呈现。

HTMLTemplateElement.prototype.appendChild = function (child) {
  return this.content.appendChild(child);
}

HTMLTemplateElement.prototype.removeChild = function (child) {
  return this.content.removeChild(child);
}

Object.defineProperty(HTMLTemplateElement.prototype, "firstChild", {
  get() {
    return this.content.firstChild;
  }
})

在这之后,我能够在模板元素内呈现任何内容。使用appendChildremoveChild是必要的,因为它们将用于渲染,但如果您没有使用水合作用,则可能不需要firstChild补丁。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接