如何最好地继承原生JavaScript对象?(特别是String)

18

我长期浏览stackoverflow,但第一次参与。如果我错过了任何礼节细节,请告诉我!

此外,我已经搜遍了高低之处,包括这个网站,但我没有找到一个清晰简洁的解释,能够明确我正在寻找的内容。如果我只是错过了它,请指引我正确的方向!

好的,我想扩展一些本地JavaScript对象,比如Array和String。然而,我不想实际扩展它们,而是创建从中继承的新对象,然后修改这些对象。

对于Array,这样做是有效的:

var myArray = function (n){
    this.push(n);

    this.a = function (){
        alert(this[0]);
    };
}

myArray.prototype = Array.prototype;

var x = new myArray("foo");
x.a();

然而,对于String类型,这种方法不起作用:

var myString = function (n){
    this = n;

    this.a = function (){
        alert(this);
    };
}

myString.prototype = String.prototype;

var x = new myString("foo");
x.a();

我也尝试过:

myString.prototype = new String();

现在,在尝试研究这个问题时,我发现以下代码可以正常工作:

var myString = function (n){
    var s = new String(n);

    s.a = function (){
        alert(this);
    };

    return s;
}

var x = myString("foo");
x.a();
然而,这对我来说几乎感觉像是“作弊”。就像,我应该使用“真正”的继承模型,而不是这个快捷方式。
所以,我的问题是:
1)您能告诉我从String继承方面我做错了什么吗?(最好附上一个可行的示例...)
2)在“真正”的继承示例和“捷径”示例之间,您能否列举任何一种方法比另一种方法更具明显的优点或劣势?或者只是在功能上有些差异?(因为它们最终看起来都是一样的...)
谢谢大家!
编辑:
感谢所有评论/回答。我认为@CMS的信息是最好的,因为:
1)他通过指出在自己的字符串对象中部分重新定义String可以使其正常工作来回答了我的String继承问题。(例如覆盖toString和toValue) 2)从Array继承的新对象具有自己的限制,这些限制并不立即可见,也无法通过部分重新定义Array来解决。
从上述2点出发,我得出结论:JavaScript的可继承性声称仅适用于您自己创建的对象,并且当涉及到原生对象时,整个模型会崩溃。(这可能是为什么你找到的90%的例子都是Pet->Dog或Human->Student,而不是String->SuperString)。这可以通过@chjj的答案来解释,这些对象实际上应该是原始值,尽管JS中的所有内容似乎都是对象,并且应该是100%可继承的。
如果这个结论完全错误,请更正我。如果它是准确的,那么我相信这对任何人来说都不是新闻 - 但再次感谢您所有的评论。我想我现在有一个选择:
要么继续使用寄生继承(我现在知道的第二个示例名称),并尝试减少其可能的内存使用影响,要么像@davin、@Jeff或@chjj建议的那样为自己假定义或完全重新定义这些对象(这似乎是一种浪费)。
@CMS-将您的信息编译成答案,我会选择它。

哎呀 - 忘了一件事。在第一个字符串继承示例(“真正的”继承示例)中,我还尝试了一些类似于“this.constructor.apply(this, arguments);”的东西,试图在myString的构造函数中调用String的构造函数,但没有成功。 - encoder
如果你搜索“JavaScript继承”,在这里的SO上有很多帖子讨论了你所问的许多问题。 - jfriend00
1
@encoder:对于数组,你的例子似乎可以工作,但是存在几个问题,例如当你设置一个数组索引时,length属性不会自动更新,并且当你给它赋值时,它不会影响对象的其他属性(例如 x.length = 0; //空数组)。这比看起来更加复杂。请查看这篇文章 - Christian C. Salvadó
不确定编辑是否会触发电子邮件提醒,因此我在评论中说明我编辑了原帖。 - encoder
我知道这是一个老问题,但我认为处理本地对象的实际方法是通过扩展本地对象本身,通过原型添加方法和属性(例如向String.prototype添加方法)而不是对其进行子类化。现在经常使用此方法作为polyfill来使新方法与旧实现兼容,并且还可以用于创建自己的方法(具有命名空间冲突注意事项)。 - jfriend00
5个回答

13

这种做法非常简单但存在缺陷:

var MyString = function() {};

MyString.prototype = new String();

你所要求的在JavaScript中是不寻常的,因为通常情况下,你不会将它们视为字符串对象,而是作为原始值的“string”类型来处理。此外,字符串根本不可变。你可以通过指定.toString方法使任何对象表现得像字符串一样:

var obj = {};
obj.toString = function() {
  return this.value;
};
obj.value = 'hello';

console.log(obj + ' world!');

但显然它将没有任何字符串方法。你可以通过几种方式实现继承。其中一种是JavaScript最初应该使用的“原始”方法,就像你和我上面发布的那样,或者:

var MyString = function() {};
var fn = function() {};
fn.prototype = String.prototype;
MyString.prototype = new fn();

这允许在不调用构造函数的情况下向原型链中添加内容。

ES5的方法是:

MyString.prototype = Object.create(String.prototype, {
  constructor: { value: MyString }
});

非标准的,但最方便的方式是:

MyString.prototype.__proto__ = String.prototype;

所以,最终,你可以做的是这样:

var MyString = function(str) {
  this._value = str;
};

// non-standard, this is just an example
MyString.prototype.__proto__ = String.prototype;

MyString.prototype.toString = function() {
  return this._value;
};
继承的字符串方法可能会使用该方法,我不确定。我认为可能会,因为有一个toString 方法。这取决于它们在特定JS引擎内部的实现方式。但也可能不会。你需要自己定义。再次强调,你所要求的非常奇怪。
你也可以尝试直接调用父构造函数:
var MyString = function(str) {
  String.call(this, str);
};

MyString.prototype.__proto__ = String.prototype;

但这样做有点靠不住。

你所想要实现的目标可能并不值得这么做。我敢打赌,有更好的方法来达到你使用它的目的。

如果你想要一个绝对可靠的方法:

// warning, not backwardly compatible with non-ES5 engines

var MyString = function(str) {
  this._value = str;
};

Object.getOwnPropertyNames(String.prototype).forEach(function(key) {
  var func = String.prototype[key];
  MyString.prototype[key] = function() {
    return func.apply(this._value, arguments);
  };
});

这将对this._value应用柯里化到每个String方法。这很有趣,因为你的字符串将是可变的,不像真正的JavaScript字符串。

你可以这样做:

    return this._value = func.apply(this._value, arguments);

这将增添一种有趣的动态效果。如果你想要它返回你的字符串而不是一个本地字符串:

    return new MyString(func.apply(this._value, arguments));

或者简单地说:

    this._value = func.apply(this._value, arguments);
    return this;

有几种方法可以解决它,具体取决于您想要的行为。

此外,您的字符串不会像JavaScript字符串那样具有长度或索引,解决此问题的方法是在构造函数中添加:

var MyString = function(str) {
  this._value = str;
  this.length = str.length;
  // very rough to instantiate
  for (var i = 0, l = str.length; i < l; i++) {
    this[i] = str[i];
  }
};

非常不规范的方法。根据实现方式,你可能只需要在那里调用构造函数以添加索引和长度。如果您想使用ES5,则还可以使用获取器获取长度。

但是,再次强调,您在这里想要做的事情绝不是理想的方法。它会变得缓慢且不必要。


1
关于 Object.keys(String.prototype),它将找不到任何内容,所有内置构造函数的属性都是不可枚举的,您需要使用 Object.getOwnPropertyNames - Christian C. Salvadó

5

这行不是有效的:

this = n;

this不是有效的左值(lvalue)。这意味着你不能将值分配给由this引用的值。在JavaScript中这是无效的。如果你这样做,你的示例代码就可以正常工作:

var myString = function (n){
    this.prop = n;

    this.a = function (){
        alert(this.prop);
    };
}

myString.prototype = new String; // or String.prototype;

var x = new myString("foo");
x.a();

关于你的解决方法,你应该意识到你所做的只是创建一个字符串对象,并增加一个函数属性,然后调用它。这里 没有 发生继承。

例如,如果你在我的示例中执行 x instanceof myString,它将评估为 true,但在你的示例中不是这样,因为函数 myString 不是一种类型,它只是一个普通的函数。


1
你需要在你的myString.prototype对象上覆盖String.prototype中的valueOftoString方法,因为这两个方法是“非通用”的,它们只能在“真实”的字符串上工作,并且在类型转换时由内部使用,例如当你使用任何String.prototype方法(如x.charAt(0);)时,在其他情况下(例如x + 'bar'),如果你不覆盖它们,所有这些方法和操作都将无法使用,它们都会抛出一个TypeError - Christian C. Salvadó
@CMS,这是一个非常好的观点。在这些玩具示例中可能没有太多事情可做,因为没有什么能够产生直观上的意义,也许只需要使用存根。 - davin

1

我会考虑创建一个新对象,使用本地对象作为后备字段,并手动重新创建本地对象的函数。以下是一些非常粗糙、未经测试的示例代码...

var myString = {
    _value = ''
    ,substring:_value.substring
    ,indexOf:_value.indexOf
}

现在,我确定这不会按预期工作。但我认为通过一些调整,它可以类似于从String继承的对象。


1

在构造函数中无法分配 this

this=n 是一个错误

你的 myarray 只是本地数组的别名 - 你对 myarray.prototype 所做的任何更改都会影响到 Array.prototype。


0

ECMAScript6标准允许直接从本地对象的构造函数继承。

class ExtendedNumber extends Number
{
    constructor(value)
    {
        super(value);
    }

    add(argument)
    {
        return this + argument;
    }
}

var number = new ExtendedNumber(2);
console.log(number instanceof Number); // true
console.log(number + 1); // 4
console.log(number.add(3)); // 5
console.log(Number(1)); // 1

在ECMAScript5中,可以像这样继承:

function ExtendedNumber(value)
{
    if(!(this instanceof arguments.callee)) { return Number(value); }

    var self = new Number(value);

    self.add = function(argument)
    {
        return self + argument;
    };

    return self;
}

然而,生成的对象并不是原始的:

console.log(number); // ExtendedNumber {[[PrimitiveValue]]: 2}

但是你可以通过扩展实际使用的原型来扩展原始类型:

var str = "some text";
var proto = Object.getPrototypeOf(str);
proto.replaceAll = function(substring, newstring)
{
    return this.replace(new RegExp(substring, 'g'), newstring);
}

console.log(str.replaceAll('e', 'E')); // somE tExt

将强制转换为面向类的符号表示法有点棘手:

function ExtendedString(value)
{
    if(this instanceof arguments.callee) { throw new Error("Calling " + arguments.callee.name + " as a constructor function is not allowed."); }
    if(this.constructor != String) { value = value == undefined || value == null || value.constructor != String ? "" : value; arguments.callee.bind(Object.getPrototypeOf(value))(); return value; }

    this.replaceAll = function(substring, newstring)
    {
        return this.replace(new RegExp(substring, 'g'), newstring);
    }
}

console.log(!!ExtendedString("str").replaceAll); // true

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