什么是一种好的简约JavaScript继承方法?

6
我正在重写一个JavaScript项目,并希望能够使用面向对象的方法来组织当前代码的混乱。主要问题是这个JavaScript应该作为小部件在第三方网站内运行,我不能让它与其他网站可能使用的JavaScript库发生冲突。
因此,我正在寻找一种在JavaScript中编写“类似”的继承方式,具有以下要求:
  1. 没有外部库或会与外部库冲突的内容(这排除了从外部库复制&粘贴的可能性)。
  2. 极简主义——我不希望支持代码超过几行,并且我不希望开发人员每次定义新类或方法时都需要大量样板代码。
  3. 应允许动态扩展父对象,以便子对象看到更改(原型)。
  4. 应允许构造函数链接。
  5. 应允许使用super类型调用。
  6. 仍然应该感觉像JavaScript。
最初,我尝试使用简单的原型链:
function Shape(x,y) {
  this.x = x;
  this.y = y;

  this.draw = function() {
    throw new Error("Arbitrary shapes cannot be drawn");
  }
}

function Square(x,y,side) {
  this.x = x;
  this.y = y;
  this.side = side;

  this.draw = function() {
    gotoXY(this.x,this.y); lineTo(this.x+this.side, this.y); ...
  }
}
Square.prototype = new Shape();

这解决了需求1、2和6,但它不允许超级调用(新函数覆盖父函数)、构造函数链接和动态扩展父类不会为子类提供新的方法。

欢迎提出任何建议。


你可以调用超类方法。如果你在Triangle中定义draw,你就可以通过Triangle.prototype.draw获得超级绘制。 - Eldar Djafarov
问题在于“super”方法无法访问当前对象的字段 - this是原型而不是drawthis。我可以做Square.prototype.draw.apply(this, arguments),但这很笨拙,而且我通常不喜欢方法通过名称调用它们的容器类(它们应该在任何地方使用this)。 - Guss
这个怎么处理:this.constructor.prototype.draw.apply(this, arguments) - Eldar Djafarov
@Eldar:这样做不行,因为覆盖原型会使“constructor”无法使用;而且这种方式也不是特别简洁。 - Christoph
  1. Square是一个构造函数,Square.prototype是它的原型。重写原型与构造函数无关。 [this.constructor] 等于 [Square]。
  2. 这一点没有什么复杂的。这是原型继承,它就是这样工作的。你有构造函数,你有原型。 this.constructor.prototype.constructor.prototype...draw.apply(this, arguments)
- Eldar Djafarov
显示剩余2条评论
6个回答

5
我建议使用以下模式,该模式利用clone函数从原型继承而不是实例继承:
function Shape(x, y) {
    this.x = x;
    this.y = y;
}

Shape.prototype.draw = function() {
    throw new Error('Arbitrary shapes cannot be drawn');
};

function Square(x,y,side) {
    Shape.call(this, x, y); // call super constructor
    this.side = side;
}

// inherit from `Shape.prototype` and *not* an actual instance:
Square.prototype = clone(Shape.prototype);

// override `draw()` method
Square.prototype.draw = function() {
    gotoXY(this.x,this.y); lineTo(this.x+this.side, this.y); // ...
};

重要的是方法存在于原型中(出于性能原因,这也应该是这样),这样您就可以通过调用超类的方法来调用它们。
SuperClass.prototype.aMethod.call(this, arg1, arg2);

通过一些 语法糖, 你可以让JS看起来像一个经典的基于类的语言:

var Shape = Class.extend({
    constructor : function(x, y) {
        this.x = x;
        this.y = y;
    },
    draw : function() {
        throw new Error('Arbitrary shapes cannot be drawn');
    }
});

var Square = Shape.extend({
    constructor : function(x, y, side) {
        Shape.call(this, x, y);
        this.side = side
    },
    draw : function() {
        gotoXY(this.x,this.y); lineTo(this.x+this.side, this.y); // ...
    }
});

@Guss:在我看来,使用实例进行继承是一种不好的做法,因为它会运行超类的构造函数;根据构造函数实际执行的操作,这可能会产生不良的副作用;在子类的构造函数中显式调用超类的构造函数更加清晰,因为这样只有在您真正想创建新实例时才会运行初始化代码。 - Christoph
Christoph的解决方案本质上就是Zakas在他的JavaScript书中所称的“寄生组合继承”(第179-181页)。请注意,我与其无关 ;)哦,为什么不从实例继承呢?Christoph是正确的,但具体来说,您必须调用两次构造函数!1)构造函数窃取调用2)原型分配。他的“克隆”在Zakas示例中被称为inheritPrototype(SubType,SuperType)。 - Rob
继承原型(sub,sup){var proto = createObjectHelper(sup.prototype);prot.constructor = sub;sub.prototype = proto;} - Rob
我在这里有几乎所有流行继承模式的可工作代码示例:http://github.com/roblevintennis/Testing-and-Debugging-JavaScript - Rob
@Christoph 哇,好棒的答案。我在2017年也遇到了同样的问题,不想使用糖(译注:指语法糖)。 但是,您的 clone 函数与仅使用 this.prototype = Shape; 之类的东西有什么区别呢?优缺点是什么?我错过了什么吗? - DanilGholtsman
显示剩余4条评论

4
道格拉斯·克罗克福德在Javascript的经典继承和原型继承方面都有很好的文章,这应该是一个不错的起点。classical prototypal

3
我熟悉Crockford的文章,但不喜欢它们。他要求大量模板代码(请看你原型继承链接中构造对象的第一个示例),或者需要几十行代码来实现“语法糖”。有时两者兼备。而且,由于各种“uber”和“beget”等内容,结果看起来一点也不像JavaScript——目的是让经过训练的JavaScript开发人员可以查看代码并立即理解其含义。 - Guss
很难不使用一些样板代码来尝试实现所需的功能。公平地说:每个经过训练的JavaScript开发人员都应该熟悉Crockford的工作 :) - cllpse

1

好的,使用JavaScript复制类/实例式系统的技巧是你只能在实例上使用原型继承。所以你需要能够创建一个仅用于继承的“非实例”实例,并且有一个与构造函数本身分离的初始化方法。

这是我在添加装饰之前使用的最小系统,将一个特殊的一次性值传递到构造函数中,以使其构造一个对象而不进行初始化:

Function.prototype.subclass= function() {
    var c= new Function(
        'if (!(this instanceof arguments.callee)) throw(\'Constructor called without "new"\'); '+
        'if (arguments[0]!==Function.prototype.subclass._FLAG && this._init) this._init.apply(this, arguments); '
    );
    if (this!==Object)
        c.prototype= new this(Function.prototype.subclass._FLAG);
    return c;
};
Function.prototype.subclass._FLAG= {};

new Function() 的使用是避免在 subclass() 上形成不必要的闭包的一种方法。如果您喜欢,可以将其替换为更漂亮的 function() {...} 表达式。

使用相对干净,并且通常类似于 Python 风格的对象,只不过语法略显笨拙:

var Shape= Object.subclass();
Shape.prototype._init= function(x, y) {
    this.x= x;
    this.y= y;
};
Shape.prototype.draw= function() {
    throw new Error("Arbitrary shapes cannot be drawn");
};

var Square= Shape.subclass();
Square.prototype._init= function(x, y, side) {
    Shape.prototype._init.call(this, x, y);
    this.side= side;
};
Square.prototype.draw= function() {
    gotoXY(this.x, this.y);
    lineTo(this.x+this.side, this.y); // ...
};

猴子补丁内置函数(Function)有点可疑,但使其易于阅读,而且没有人会想要在 Function 上使用 for...in


function() {...} 在其所在的函数作用域内形成闭包,保留对该函数中参数和局部变量的引用。而 new Function() 则避免了这种情况。不过无论哪种方式都没有太大影响,因为闭包中并没有什么重量级的内容。 - bobince
1
当然可以。我只是不确定你为什么要使用“重型”Function,而普通的function(){}已经足够了。 - kangax
Function的调用会更耗费资源,但由于缺少作用域框架,返回的对象会更加轻量级。 - bobince

1

在研究这个问题时,我发现最常见的模式是在Mozilla开发者网络上描述的。我已经更新了他们的示例,包括对超类方法的调用,并在警报消息中显示日志:

// Shape - superclass
function Shape() {
  this.x = 0;
  this.y = 0;
}

// superclass method
Shape.prototype.move = function(x, y) {
  this.x += x;
  this.y += y;
  log += 'Shape moved.\n';
};

// Rectangle - subclass
function Rectangle() {
  Shape.call(this); // call super constructor.
}

// subclass extends superclass
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

// Override method
Rectangle.prototype.move = function(x, y) {
  Shape.prototype.move.call(this, x, y); // call superclass method
  log += 'Rectangle moved.\n';
}

var log = "";
var rect = new Rectangle();

log += ('Is rect an instance of Rectangle? ' + (rect instanceof Rectangle) + '\n'); // true
log += ('Is rect an instance of Shape? ' + (rect instanceof Shape) + '\n'); // true
rect.move(1, 1); // Outputs, 'Shape moved.'
alert(log);

  1. 在您提出这个问题的五年时间里,浏览器对继承的支持似乎有所改善,因此我认为您不需要外部库。
  2. 这是我见过的最简单的技术,我不知道您是否认为它太冗长。
  3. 它使用了原型,如您所请求的那样,因此将新方法添加到父级应该也会向子对象提供它们。
  4. 您可以在示例中看到构造函数链接。
  5. 超类型调用也在示例中。
  6. 我不确定它是否感觉像JavaScript,您必须自己决定。

看起来不错,感觉很像JavaScript,但是有相当多的样板代码 :-/ 不过,这是一个好的解决方案。 - Guss

0
你可以使用Crockford在他的书《JavaScript the good parts》中提出的函数式编程模式。这个想法是使用闭包来创建私有字段,并使用特权函数来访问这些字段。以下是符合你6个要求之一的解决方案:
    var people = function (info) {
    var that = {};
    // private
    var name = info.name;
    var age = info.age;
    // getter and setter
    that.getName = function () {
        return name;
    };
    that.setName = function (aName) {
        name = aName;
    };
    that.getAge = function () {
        return age;
    };
    that.setAge = function (anAge) {
        age = anAge;
    };
    return that;
};

var student = function (info) {
    // super
    var that = people(info);
    // private
    var major = info.major;
    that.getMajor = function () {
        return major;
    };
    that.setMajor = function (aMajor) {
        major = aMajor;
    };
    return that;
};

var itStudent = function (info) {
    // super
    var that = student(info);
    var language = info.language;
    that.getLanguage = function () {
        return language;
    };
    that.setLanguage = function (aLanguage) {
        language = aLanguage;
    };
    return that;
};

var p = person({name : "Alex", age : 24});
console.debug(p.age); // undefined
console.debug(p.getAge()); // 24

var s = student({name : "Alex", age : 24, major : "IT"});
console.debug(s.getName()); // Alex
console.debug(s.getMajor()); // IT

var i = itStudent({name : "Alex", age : 24, major : "IT", language : "js"});
console.debug(i.language); // Undefined
console.debug(i.getName()); // Alex
console.debug(i.getMajor()); // IT
console.debug(i.getLanguage()); // js

我不喜欢这种函数式语法,一方面它不像面向对象的代码(因此当你试图让受过经典面向对象培训的程序员与你合作时会有负面影响),另一方面你也无法享受到原型继承的优势,无法使用混入模式。 - Guss

-1

我也受到了Crockford的启发,但是我使用“构造函数”和他所谓的“功能继承”取得了良好的经验。你的情况可能有所不同。

更新:抱歉,我忘记了:你仍然需要用一个superior方法来增强Object,以便获得对超级方法的良好访问。这可能不适合你。

var makeShape = function (x, y) {
    that = {};
    that.x = x;
    that.y = y;
    that.draw = function() {
        throw new Error("Arbitrary shapes cannot be drawn");
    }
    return that;
};

var makeSquare = function (x, y, side) {
    that = makeShape(x, y);
    that.side = side;
    that.draw = function() {
        gotoXY(that.x,that.y); lineTo(that.x+that.side, that.y); ...
    }
    return that;
};

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