【问题标题】:Extending core types without modifying prototype在不修改原型的情况下扩展核心类型
【发布时间】:2011-10-31 18:54:11
【问题描述】:

如何在不修改原型的情况下扩展核心 JavaScript 类型(字符串、日期等)?例如,假设我想用一些方便的方法创建一个派生字符串类:

function MyString() { }
MyString.prototype = new String();
MyString.prototype.reverse = function() {
  return this.split('').reverse().join('');
};
var s = new MyString("Foobar"); // Hmm, where do we use the argument?
s.reverse();
// Chrome - TypeError: String.prototype.toString is not generic
// Firefox - TypeError: String.prototype.toString called on incompatible Object

错误似乎源自 String 基方法,在这种情况下可能是“拆分”,因为它的方法被应用于某些非字符串对象。但是如果我们不能应用到非字符串对象上,那么我们真的可以自动重用它们吗?

[编辑]

显然,我的尝试在很多方面都存在缺陷,但我认为这表明了我的意图。经过一番思考,我们似乎无法重用任何 String 原型对象的函数,除非在 String 上显式调用它们。

是否可以像这样扩展核心类型?

【问题讨论】:

  • 只需使用 defineProperty 扩展 String.prototype 并确保将 enumerable 设置为 false 并配置为 true

标签: javascript prototypal-inheritance


【解决方案1】:

2 年后:在全局范围内改变任何东西都是一个糟糕的主意

原文:

在 ES5 浏览器中扩展原生原型存在一些“错误”是 FUD。

Object.defineProperty(String.prototype, "my_method", {
  value: function _my_method() { ... },
  configurable: true,
  enumerable: false,
  writeable: true
});

但是,如果您必须支持 ES3 浏览器,那么人们在字符串上使用 for ... in 循环就会出现问题。

我的意见是,您可以更改原生原型,并且应该停止使用任何写得不好会破坏的代码

【讨论】:

  • +1 了解我的意图和顾虑并教会我一些新东西!扩展原型是最简单的方法,我很高兴看到有一种安全的、基于标准的方法来做到这一点。
  • @maerics 只是为了说明这一点。您必须设置configurable: true。如果您不这样做,那么当我们需要时,我们就无法从原型中删除您的扩展。这真是令人沮丧。 See this question
  • 这不是最好的主意。 A. 这没有回答问题 B. 如果您扩展本机对象的原型,那么您的代码将成为“写得不好的代码”。覆盖本机方法是最糟糕的主意,我曾经遇到过这种情况,不要犯同样的错误。如果其他项目不依赖于您的项目,它就足够安全了,但是一旦您发布代码,它就会污染一个新的命名空间。这很糟糕,原因与全局变量不好的原因相同。
  • @BT 同意了。在全局范围内改变任何东西是一个糟糕的主意
  • @Raynos 为什么改变全局范围是一个糟糕的主意?你能帮我理解这个吗?我看到很多人说这是一个坏主意,它留下的问题比答案多,或者有时你会听到(弃用的坏主意)我的意思是为什么与什么一样重要。
【解决方案2】:

更新:即使这段代码也没有完全扩展原生的String 类型(length 属性不起作用)。

Imo 可能不值得遵循这种方法。要考虑的事情太多,您必须投入太多时间来确保它完全正常工作(如果它确实可以)。 @Raynos provides another interesting approach.

尽管如此,还是有这样的想法:


似乎除了真正的字符串之外,您不能在任何其他东西上调用String.prototype.toString。您可以覆盖此方法:

// constructor
function MyString(s) {
    String.call(this, s); // call the "parent" constructor
    this.s_ = s;
}

// create a new empty prototype to *not* override the original one
tmp = function(){};
tmp.prototype = String.prototype;
MyString.prototype = new tmp();
MyString.prototype.constructor = MyString;

// new method
MyString.prototype.reverse = function() {
  return this.split('').reverse().join('');
};

// override 
MyString.prototype.toString = function() {
  return this.s_;
};

MyString.prototype.valueOf = function() {
  return this.s_;
};


var s = new MyString("Foobar");
alert(s.reverse());

如您所见,我还必须覆盖 valueOf 才能使其工作。

但是:我不知道这些是否是您必须重写的唯一方法,而对于其他内置类型,您可能必须重写其他方法。一个好的开始是使用ECMAScript specification 并查看方法的规范。

例如String.prototype.split算法的第二步是:

S 成为调用 ToString 的结果,将 this 值作为其参数。

如果一个对象被传递给ToString,那么它基本上调用了这个对象的toString方法。这就是为什么当我们覆盖 toString 时它会起作用。

更新:不起作用s.length。因此,尽管您可能能够使 方法 工作,但其他属性似乎更棘手。

【讨论】:

  • 另外,这意味着 MyString 的实例将是对象,而不是基元,因此如果将其用作基元,则可能具有特殊的行为。
  • 不完美,但最好的答案之一。现在,您可以为 .length 使用特殊的 getter,并为字符访问使用代理,例如 str[2]
【解决方案3】:

首先,在这段代码中:

MyString.prototype = String.prototype;   
MyString.prototype.reverse = function() {
    this.split('').reverse().join('');
};

变量MyString.prototypeString.prototype 都引用同一个对象!分配给一个就是分配给另一个。当您将reverse 方法放入MyString.prototype 时,您也在将其写入String.prototype。所以试试这个:

MyString.prototype = String.prototype;   
MyString.prototype.charAt = function () {alert("Haha");}
var s = new MyString();
s.charAt(4);
"dog".charAt(3);

最后两行提醒,因为它们的原型是同一个对象。你确实扩展了String.prototype

现在关于您的错误。您在 MyString 对象上调用了 reverse。这个方法在哪里定义?在原型中,与String.prototype 相同。你覆盖了reverse。它做的第一件事是什么?它在目标对象上调用split。现在的问题是,为了让String.prototype.split 工作,它必须调用String.prototype.toString。例如:

var s = new MyString();
if (s.split("")) {alert("Hi");}

此代码生成错误:

TypeError: String.prototype.toString is not generic

这意味着String.prototype.toString 使用字符串的内部表示来做它的事情(即返回其内部原始字符串),并且不能应用于共享字符串原型的任意目标对象。所以当你调用split 时,split 的实现说“哦,我的目标不是字符串,让我调用toString”,但随后toString 说“我的目标不是字符串,我不是通用的”所以它抛出了TypeError

如果你想进一步了解 JavaScript 中的泛型,可以查看this MDN section on Array and String generics

至于让这个工作没有错误,请参阅 Alxandr 的回答。

至于扩展确切的内置类型,例如StringDate 等等,而不改变它们的原型,你真的不需要,不创建包装器、委托或子类。但是这样就不允许像

这样的语法
d1.itervalTo(d2)

其中d1d2 是内置Date 类的实例,您没有扩展其原型。 :-) JavaScript 为这种方法调用语法使用原型链。它就是这样。很好的问题...但是这是您的想法吗?

【讨论】:

  • 感谢您的反馈,我已经更新了我的问题以解决这些问题(我将对其进行注释)。
  • 不,我的也不行。我只是想解释一下,如果您实际上 可以 扩展字符串对象,这就是这样做的方式。但是因为字符串对象上的函数在字符串的内部表示上运行,所以这是不可能的。
  • @maerics:我已经更新了我的答案,以便对您更新的问题给出明确的答案,而不仅仅是理论上的答案。
  • @Alxandr 我认为你是对的,它不能完成。原型只是在 JS 中“扩展”的手段。不过,Felix King 在他的回答中有一些有趣的方法,但如果你拒绝添加到内置的原型中,最终你必须有一个新的类。
【解决方案4】:

你在这里只弄错了一个地方。 MyString.prototype 不应该是 String.prototype,应该是这样的:

function MyString(s) { }
MyString.prototype = new String();
MyString.prototype.reverse = function() {
  this.split('').reverse().join('');
};
var s = new MyString("Foobar");
s.reverse();

[编辑]

为了更好地回答您的问题,不应该是不可能的。 如果你看看这个:https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/constructor 它解释说你不能改变布尔、整数和字符串的类型,因此它们不能被“子类化”。

【讨论】:

  • 嗯,是的,我想是的。但是,我认为您不能像那样扩展字符串对象。原因,您不能将“this”对象设置为等于字符串。所以我认为你将很难扩展字符串对象。但如果可以的话,这将是 js 的方式。
  • 除非您的代码抛出相同的错误。无论如何,这无关紧要,因为 String 的实例无论如何都会有 String 原型;它只是在原型链上节省了额外的一跳,对吗?
  • 不,好吧,当您更改 MyString.prototype 时,您也将更改字符串原型。
  • 在 FF 中我仍然收到错误 String.prototype.toString called on incompatible Object。无论如何,你对s 没有做任何事情。
  • 好吧,你也不是。如前所述,我认为您无法在不修改其原型的情况下扩展字符串对象。
【解决方案5】:

我认为基本的答案是你可能不能。你能做的就是 Sugar.js 所做的——创建一个类似对象的对象并从中扩展:

http://sugarjs.com/

Sugar.js 是关于原生对象扩展的,它们扩展 Object.prototype。

【讨论】:

    猜你喜欢
    • 2019-05-06
    • 2011-03-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-01-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多