4

通常人们会这样写代码:

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

Shape.prototype.move = function(x, y) {
  this.x += x;
  this.y += y;
}

但是,我试图想出一种方法来在原型上定义函数,而不用将函数定义与构造函数分开。这是我得到的:

Object.prototype.method = function(name, func) {
  if (typeof(this.constructor.prototype[name]) === 'undefined') {
    this.constructor.prototype[name] = func
  }
}

这使您可以执行以下操作:

function Shape() {
  this.x = 0;
  this.y = 0;

  this.method('move', function(x, y) {
    this.x += x;
    this.y += y;
  })
}

而且无论您创建多少次形状,该功能都只会定义一次。

我知道增强Object.prototype不是一种好的做法。但除此之外,这种方法还有什么缺点吗?

编辑:

约翰提出了一个很好的观点;我应该使method不可枚举。这是修改后的版本:

Object.defineProperty(Object.prototype, 'method', {
    value: function(name, func) {
        if (typeof(this.constructor.prototype[name]) === 'undefined') {
            this.constructor.prototype[name] = func
        }
    },
    enumerable: false
})
4

4 回答 4

7

让我们实际比较这两种方式,看看哪一种更快:http: //jsperf.com/traditional-oop-vs-derek-s-oop-variant

如您所见,您的方法比传统方法慢得多。原因是:

  1. 您的构造函数做的事情超出了要求。因此,如果您创建多个实例,那么创建实例的额外成本就会增加。
  2. 正如@Alxandr 提到的,method每次您无缘无故地创建一个新实例时,您都会创建一个新的匿名函数。它只会有用一次,之后就会浪费处理能力。
  3. 您正在调用一个函数来检查prototype构造函数是否具有给定方法,prototype如果没有,则将该方法添加到。这似乎是不必要的。您无需创建函数即可为您执行此操作。恕我直言,函数调用只是额外的开销。

既然你要求批评:

我知道增强Object.prototype不是一种好的做法。但除此之外,这种方法还有什么缺点吗?

除了非常缓慢之外,您的方法还受到以下影响:

  1. 难以理解。您可能会发现这种方法很直观。但是,阅读您的代码的人肯定会想知道该函数的this.method作用。他们需要阅读 的定义Object.prototype.method才能完全理解您的代码。
  2. 不直观。正如我之前提到的,prototype在构造函数中定义属性是没有意义的。它只需要一次,之后它就会变成额外的行李。最好将构造函数逻辑和prototype属性分开。
  3. 它可能会导致意外行为。正如@basilikum 指出的那样,如果您从不调用构造函数,那么prototype将永远不会设置属性。当您尝试访问prototype. 例如,从prototype没有属性继承属性时,将继承直到调用基构造函数。

我相信您的目标是将构造函数和prototype属性封装到一个实体中:

但是,我试图想出一种方法来在原型上定义函数,而不用将函数定义与构造函数分开。

是否有捷径可寻?让我们看看,JavaScript 是一种原型面向对象的语言。因此,我们应该更多地关注prototype而不是构造函数

上图取自以下答案:https ://stackoverflow.com/a/8096017/783743

这张图向我们展示了:

  1. 每个构造函数都有一个名为的属性prototype,它指向构造函数的原型对象。
  2. 每个原型都有一个名为的属性constructor,它指向原型对象的构造函数。
  3. 我们从构造函数创建一个实例。然而,实例实际上继承自prototype,而不是构造函数。

这是非常有用的信息。传统上,我们总是先创建一个构造函数,然后再设置它的prototype属性。然而,这些信息告诉我们,我们可以先创建一个原型对象,然后constructor在其上定义属性。

例如,传统上我们可以这样写:

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

Shape.prototype.move = function(x, y) {
    this.x += x;
    this.y += y;
};

然而,使用我们新发现的知识,我们可能会写出相同的东西:

var shape = {
    constructor: function () {
        this.x = 0;
        this.y = 0;
    },
    move: function (x, y) {
        this.x += x;
        this.y += y;
    }
};

这两个示例中包含的信息是相同的。然而,我们需要一些额外的脚手架来使第二个示例工作。特别是我们需要做:

var Shape = shape.constructor;
Shape.prototype = shape;

这不是一个大问题。我们只需创建一个函数来为我们执行此操作:

function defclass(prototype) {
    var constructor = prototype.constructor;
    constructor.prototype = prototype;
    return constructor;
}

现在我们可以定义Shape如下:

var Shape = defclass({
    constructor: function () {
        this.x = 0;
        this.y = 0;
    },
    move: function (x, y) {
        this.x += x;
        this.y += y;
    }
});

如您所见,封装在 JavaScript 中很容易实现。您需要做的就是侧身思考。然而,继承是一个不同的问题。你需要做更多的工作来实现继承。

希望这有帮助。

于 2013-07-27T02:27:55.800 回答
3

首先,您声明“该函数将只定义一次”,但是,这不是真正的 AFAIK。每次你构造一个新Shape的,匿名函数

function(x,y) { this.x += x; this.y += y }

get 被重新定义(除非编译器/解释器足够聪明,可以优化它,因为它没有被使用)。然后它被传递给方法Object.prototype.method,它选择忽略它(因为方法已经定义),并且只是留在内存中进行垃圾收集。在循环中运行它(如果编译器/解释器无法优化它),将导致无缘无故地创建很多函数。

此外,正如 Johan 所说,乱扔垃圾Object.prototype可能会导致其他代码停止工作,因为突然for(x in anything)会显示您的新功能method

如果您想将构造函数的声明和它的方法“组合”在一起,您可以创建如下内容:

function mangle(ctor, methods) {
    for(x in methods) {
        if(methods.hasOwnProperty(x)) {
            ctor.prototype[x] = methods[x];
        }
    }
    return ctor;
}

然后你会这样称呼它:

var Shape = mangle(function() {
    // moved default-values to prototype
    // so they don't have to be created
    // if they aren't set to something
    // else than the default.
}, {
    x: 0,
    y: 0,
    move: function(x, y) {
        this.x += x;
        this.y += y;
    }
});

或者你可以坚持正常的设计,或者使用像 Prototype 或 MooTools 这样的“类”提供库。有很多选项可供选择。

于 2013-07-22T06:50:28.027 回答
1

Alxandr 已经说得最多了,但我想指出您方法的另一个缺点。

仅当您至少调用一次构造函数时,该属性move才会存在。当涉及到继承时,这可能很糟糕。以此为例:

function Shape() {
  this.x = 0;
  this.y = 0;

  this.method(this, 'move', function(x, y) {
    this.x += x;
    this.y += y;
  });
}
function Triangle() {
}

Triangle.prototype = Object.create(Shape.prototype);

var t = new Triangle();
t.move(); //gives you an error because move is not defined

如您所见,您从未调用过的构造函数,Shape因此move还不存在。通常你会在Shape构造函数内部调用构造Triangle函数并且你的代码可以正常工作,但可能会有你不想这样做的情况。

所以我建议坚持原来的模式。当你习惯它时,它实际上并没有那么难看。

于 2013-07-22T07:08:18.260 回答
0

如果您或将来的某个人尝试对object扩展的对象进行循环,则结果可能是不可预测的并且会中断,因为object现在包含未知属性。一个意想不到的小问题!

于 2013-07-22T06:44:04.137 回答