39

javascript 基于原型的面向对象的编程风格很有趣,但在很多情况下,您需要能够从类中创建对象。

例如,在矢量绘图应用程序中,工作区通常在绘图开始时为空:我无法从现有的“线”创建新的“线”。更一般地说,动态创建对象的每种情况都需要使用类。

我已经阅读了很多教程和“Javascript:好的部分”一书,但在我看来,没有办法定义尊重 1)封装和 2)有效成员方法声明的类(我的意思是:成员方法被定义一次,并在每个类实例之间共享)。

为了定义私有变量,使用了闭包:

function ClassA()
{
    var value = 1;
    this.getValue = function()
    {
        return value;
    }
}

这里的问题是,“ClassA”的每个实例都会有自己的成员函数“getValue”的副本,效率不高。

为了有效地定义成员函数,原型被使用:

function ClassB()
{
    this.value = 1;
}

ClassB.prototype.getValue = function()
{
    return this.value;
}

这里的问题是成员变量“value”是公共的。

我认为这个问题不容易解决,因为在对象创建期间需要定义“私有”变量(以便对象可以访问其创建上下文,而不暴露这些值),而基于原型的成员函数定义必须在对象创建之后完成,这样原型才有意义(“this.prototype”不存在,我已经检查过了)。

还是我错过了什么?


编辑 :

首先,感谢您提供有趣的答案。

我只是想为我的初始信息添加一点精确性:

我真正想做的是拥有1)私有变量(封装很好,因为人们只能访问他们需要的东西)和2)有效的成员方法声明(避免复制)。

似乎简单的私有变量声明实际上只能通过javascript中的闭包来实现,这就是我专注于基于类的方法的原因。如果有一种方法可以使用基于原型的方法来实现简单的私有变量声明,那对我来说没问题,我不是一个激烈的基于类的方法支持者。

阅读答案后,似乎简单的解决方案是忘记私有,并使用特殊的编码约定来阻止其他程序员直接访问“私有”变量......

我同意,我的标题/第一句话对我想在这里讨论的问题具有误导性。

4

8 回答 8

31

嘘,过来!想听秘密吗?

经典继承是一种经过测试和尝试的方法。

经常在 JavaScript 中实现它很有用类是一个很好的概念,并且拥有用于在对象很棒之后对我们的世界进行建模的模板。

经典继承只是一种模式。如果它是您的用例所需的模式,那么在 JavaScript 中实现经典继承是完全可以的。

原型继承侧重于共享功能,这很棒恐龙鼓槌很棒),但在某些情况下,您希望共享数据方案而不是功能。这是原型继承根本没有解决的问题。

所以,你是在告诉我课程并不像每个人一直告诉我的那样邪恶?

不,他们不是。JS 社区不赞成的不是类的概念,而是将自己限制为仅用于代码重用的类。就像该语言不强制执行强类型或静态类型一样,它也不强制对象结构的方案。

事实上,该语言的巧妙实现可以将您的普通对象转换为类似于经典继承类的东西。

那么,类在 JavaScript 中是如何工作的

好吧,你真的只需要一个构造函数:

function getVehicle(engine){
    return { engine : engine };
}

var v = getVehicle("V6");
v.engine;//v6

我们现在有一个车辆类。我们不需要使用特殊关键字显式定义 Vehicle 类。现在,有些人不喜欢这样做,习惯了更经典的方式。为此 JS 通过执行以下操作提供(愚蠢的恕我直言)语法糖

function Vehicle(engine){
     this.engine = engine;
}
var v = new Vehicle("V6");
v.engine;//v6

这在很大程度上与上面的示例相同。

那么,我们还缺少什么?

继承和私有成员。

如果我告诉你基本的子类型在 JavaScript 中非常简单怎么办?

JavaScript 的类型概念与我们在其他语言中习惯的不同。在 JS 中成为某种类型的子类型是什么意思?

var a = {x:5};
var b = {x:3,y:3};

的类型是类型的b子类型a吗?假设它是否根据(强)行为子类型(LSP):

<<<< 开始技术部分

还,

所有这些都再次出现,由我们来保存。我们可以随心所欲地紧紧或松散地保持它们,我们不必这样做,但我们肯定可以。

所以事实上,只要我们在实现继承时遵守上面的这些规则,我们就完全实现了强行为子类型化,这是一种非常强大的子类型化形式(见注*)。

>>>>> 结束技术部分

微不足道,人们也可以看到结构子类型成立。

这将如何应用于我们的Car示例?

function getCar(typeOfCar){
    var v = getVehicle("CarEngine");
    v.typeOfCar = typeOfCar;
    return v;
}
v = getCar("Honda");
v.typeOfCar;//Honda;
v.engine;//CarEngine

不太难,是吗?私人会员呢?

function getVehicle(engine){
    var secret = "Hello"
    return {
        engine : engine,
        getSecret : function() {
            return secret;
        }
    };
}

看,secret是一个闭包变量。它是完全“私有的”,它的工作方式与 Java 等语言中的私有不同,但无法从外部访问。

在函数中使用 private 怎么样?

啊! 这是一个很好的问题。

如果我们想在原型上共享的函数中使用私有变量,我们首先需要了解 JS 闭包和函数是如何工作的。

在 JavaScript 中,函数是一流的。这意味着您可以传递函数。

function getPerson(name){
    var greeting = "Hello " + name;
    return {
        greet : function() {
            return greeting;
        }
    };
}

var a = getPerson("thomasc");
a.greet(); //Hello thomasc

到目前为止一切顺利,但是我们可以将绑定到 a 的函数传递给其他对象!这使您可以进行非常松散的解耦,这很棒。

var b = a.greet;
b(); //Hello thomasc

等待!怎么b知道这个人的名字是 thomasc?这就是闭包的魔力。很厉害吧?

您可能会担心性能。让我告诉你我是如何学会停止担心并开始喜欢优化 JIT 的。

实际上,拥有这样的功能副本并不是什么大问题。javascript中的函数都是关于功能的!闭包是一个很棒的概念,一旦你掌握并掌握了它们,你就会发现它是非常值得的,而且对性能的影响真的没有那么大。JS 一天比一天快,不用担心这些性能问题。

如果你觉得很复杂,下面的也很合理。与其他开发人员的共同合同只是说“如果我的变量以_请勿触摸它开头,我们都是同意的成年人”。这看起来像:

function getPerson(name){
    var greeter = {
        greet : function() {
            return "Hello" +greeter._name;
        }
    };
    greeter._name = name;
    return greeter;
}

或者古典风格

function Person(name){
    this._name = name;
    this.greet = function(){
       return "Hello "+this._name;
    }
}

或者,如果您想在原型上缓存函数而不是实例化副本:

function Person(name){
    this._name = name;
}
Person.prototype.greet =  function(){
       return "Hello "+this._name;
}

所以,总结一下:

  • 您可以使用经典的继承模式,它们对于共享数据类型很有用

  • 您还应该使用原型继承,它同样有效,并且在您想要共享功能的情况下更多。

  • TheifMaster几乎做到了。拥有私有私有实际上并不像人们在 JavaScript 中所想的那样有什么大不了的,只要您的代码定义了一个清晰的接口,这根本不会有问题。我们都在这里专注于成年人:)

*聪明的读者可能会想:嗯?你不是在用历史规则欺骗我吗?我的意思是,属性访问没有被封装。

我说不,我不是。即使您没有明确地将字段封装为私有字段,您也可以简单地以不访问它们的方式定义您的合约。经常像 TheifMaster 建议的那样_。另外,我认为只要我们不改变属性访问处理父对象属性的方式,在很多这样的场景中,历史规则并不是什么大问题。再次,这取决于我们。

于 2013-06-09T10:29:54.110 回答
28

我不想气馁,因为您似乎是 StackOverflow 的一个相当新的成员,但是我将不得不面对您说尝试在 JavaScript 中实现经典继承是一个非常糟糕的主意.

注意:当我说在 JavaScript 中实现经典继承是一个坏主意时,我的意思是试图在 JavaScript 中模拟实际的类、接口、访问修饰符等是一个坏主意。尽管如此,作为 JavaScript 设计模式的经典继承还是很有用的,因为它只是原型继承的语法糖(例如,最大最小类)。我一直在我的代码中使用这种设计模式(a la augment)。

JavaScript 是一种原型面向对象的编程语言。不是经典的面向对象编程语言。当然,您可以在 JavaScript 之上实现经典继承,但在您这样做之前,请记住以下事项:

  1. 你违背了语言的精神,这意味着你将面临问题。很多问题——性能、可读性、可维护性等。
  2. 你不需要上课。托马斯,我知道你真的相信你需要上课,但相信我。你没有。

为了你的缘故,我将为这个问题提供两个答案。第一个将向您展示如何在 JavaScript 中进行经典继承。第二个(我推荐)将教你拥抱原型继承。

JavaScript 中的经典继承

大多数程序员都是从尝试在 JavaScript 中实现经典继承开始的。甚至像 Douglas Crockford 这样的 JavaScript 大师也尝试在 JavaScript 中实现经典继承。我也尝试在 JavaScript 中实现经典继承。

首先,我创建了一个名为Clockwork的库,然后创建了augment。但是,我不建议您使用这些库中的任何一个,因为它们违背了 JavaScript 的精神。事实上,当我写这些经典的继承库时,我还是一个业余的 JavaScript 程序员。

我提到这一点的唯一原因是因为每个人在某些时候都是业余爱好者,虽然我更希望你不要在 JavaScript 中使用经典继承模式,但我不能指望你理解为什么原型继承很重要

如果不跌倒几次,您就无法学习如何骑自行车。我相信您仍处于原型继承的学习阶段。您对经典继承的需求就像循环上的训练轮。

然而,辅助轮很重要。如果你想有一些经典的继承库可以让你更舒服地用 JavaScript 编写代码。一个这样的库是jTypes。当您对自己作为 JavaScript 程序员的技能充满信心时,请记住取下训练轮。

注意:我个人一点也不喜欢 jTypes。

JavaScript 中的原型继承

我将本节作为一个里程碑来写给你,这样你准备好学习真正的原型继承时,你可以稍后回来并知道下一步该做什么。

首先下面一行是错误的:

javascript 基于原型的面向对象的编程风格很有趣,但在很多情况下,您需要能够从类中创建对象。

这是错误的,因为:

  1. 您将永远不需要从 JavaScript 中的类创建对象。
  2. 无法在 JavaScript 中创建类。

是的,可以在 JavaScript 中模拟经典继承。但是,您仍然从对象而不是类继承属性。例如,ECMAScript Harmony 类只是原型继承的经典模式的语法糖。

在相同的上下文中,您给出的示例也是错误的:

例如,在矢量绘图应用程序中,工作区通常在绘图开始时为空:我无法从现有的“线”创建新的“线”。更一般地说,动态创建对象的每种情况都需要使用类。

是的,即使工作区一开始是空的,您也可以从现有行创建新行。你需要了解的是,这条线实际上并没有画出来。

var line = {
    x1: 0,
    y1: 0,
    x2: 0,
    y2: 0,
    draw: function () {
        // drawing logic
    },
    create: function (x1, y1, x2, y2) {
        var line = Object.create(this);
        line.x1 = x1;
        line.y1 = y1;
        line.x2 = x2;
        line.y2 = y2;
        return line;
    }
};

现在您可以通过简单地调用来绘制上面的线,line.draw或者您可以从中创建一条新线:

var line2 = line.create(0, 0, 0, 100);
var line3 = line.create(0, 100, 100, 100);
var line4 = line.create(100, 100, 100, 0);
var line5 = line.create(100, 0, 0, 0);

line2.draw();
line3.draw();
line4.draw();
line5.draw();

线line2、和绘制line3时形成一个正方形。line4line5100x100

结论

所以你看到你真的不需要 JavaScript 中的类。对象就足够了。使用函数可以很容易地实现封装。

话虽这么说,如果每个实例都没有自己的一组公共函数,就不能让每个实例的公共函数访问对象的私有状态。

然而,这不是问题,因为:

  1. 你真的不需要私有状态。你可能认为你有,但你真的没有。
  2. 如果您真的想将变量设为私有,那么正如ThiefMaster 所提到的,只需在变量名称前加上下划线,并告诉您的用户不要乱用它。
于 2013-06-09T09:50:53.283 回答
4

好吧,这是我解决这个特定问题的尝试,尽管我认为遵循约定是一种更好的方法,即。用 .作为变量的前缀_。在这里,我只是跟踪数组中的实例,然后可以使用_destroy方法将它们删除。我相信这可以改进,但希望它会给你一些想法:

var Class = (function ClassModule() {

  var private = []; // array of objects of private variables

  function Class(value) {
    this._init();
    this._set('value', value);
  }

  Class.prototype = {

    // create new instance
    _init: function() {
      this.instance = private.length;
      private.push({ instance: this.instance });
    },

    // get private variable
    _get: function(prop) {
      return private[this.instance][prop];
    },

    // set private variable
    _set: function(prop, value) {
      return private[this.instance][prop] = value;
    },

    // remove private variables
    _destroy: function() {
      delete private[this.instance];
    },

    getValue: function() {
      return this._get('value');
    }
  };

  return Class;
}());

var a = new Class('foo');
var b = new Class('baz');

console.log(a.getValue()); //=> foo
console.log(b.getValue()); //=> baz

a._destroy();

console.log(b.getValue()); //=> baz
于 2013-06-09T09:08:28.807 回答
2

您在运行时不需要 private/public 。这些是静态可执行的。任何复杂到足以强制执行私有属性的项目都不会在外部使用,都会有一个构建/预处理步骤,您可以使用它来验证事实。即使是具有 private/public 语法的语言也可以在运行时访问 private 。

至于定义基于类的对象,你使用的构造函数+原型是最简单和最有效的方式。任何一种额外的魔法都会更复杂,性能更低。

尽管您可以缓存prototype,因此您不必一直重复ClassB.prototype.

//in node.js you can leave the wrapper function out
var ClassB = (function() {
    var method = ClassB.prototype;

    function ClassB( value ) {
        this._value = value;
    }

    method.getValue = function() {
        return this._value;
    };

    method.setValue = function( value ) {
        this._value = value;
    };

    return ClassB;
})();

以上不需要任何库,您可以轻松地为其创建宏。

此外,在这种情况下,即使是正则表达式也足以验证“私有”属性是否正确使用。运行/([a-zA-Z$_-][a-zA-Z0-9$_-]*)\._.+/g文件并查看第一个匹配项始终是this. http://jsfiddle.net/7gumy/

于 2013-06-09T12:47:40.770 回答
1

如果您真的想要基于每个实例的私有实体,但仍想继承您的方法,您可以使用以下设置:

var Bundle = (function(){
  var local = {}, constructor = function(){
    if ( this instanceof constructor ) {
      local[(this.id = constructor.id++)] = {
        data: 123
      };
    }
  };
  constructor.id = 0;
  constructor.prototype.exampleMethod = function(){
    return local[this.id].data;
  }
  return constructor;
})();

现在,如果您创建一个new Bundle,则该data值被锁定在内部:

var a = new Bundle(); console.log( a.exampleMethod() ); /// 123

然而,您现在开始争论是否应该在 JavaScript 中真正拥有私有值。据我发现,对于那些可能需要扩展你的代码的人——甚至是你自己——开放访问一切总是更好的。

除了不那么可读或访问“私有”值时笨拙之外,上述模式也有隐藏的缺点。一个事实是,每个单独的实例都Bundle将保留对该local对象的引用。这可能意味着——例如——如果您创建了数千个 Bundle,并删除了除其中一个之外的local所有 Bundle,则不会为所有创建的 Bundle 对其中保存的数据进行垃圾收集。你必须包含一些解构代码来解决这个问题......基本上让事情变得更加复杂。

所以我建议放弃私有实体/属性的想法,无论您决定采用哪种模式......基于对象或构造函数。JavaScript 的好处是所有这些不同的方法都是可能的——它远没有基于类的语言那么清晰——有些人可能会认为这会让事情变得混乱,但我喜欢 JavaScript 带来的快速和富有表现力的自由。

于 2013-06-09T10:43:27.903 回答
1

关于您问题中的这一陈述:

例如,在矢量绘图应用程序中,工作区通常在绘图开始时为空:我无法从现有的“线”创建新的“线”。更一般地说,动态创建对象的每种情况都需要使用类。

您似乎误解了 Javascript 中的对象只能通过克隆现有对象来制作,这会回溯到“好的,但是第一个对象呢?不能通过克隆现有对象来制作的问题,因为有不是任何现有的对象。”

但是,您可以从头开始制作对象,其语法非常简单var object = {}

我的意思是,这是最简单的对象,一个空对象。更有用的当然是一个里面有东西的对象:

var object = {
  name: "Thing",
  value: 0,
  method: function() {
    return true;
  }
}

依此类推,现在您要参加比赛了!

于 2013-06-09T14:39:07.360 回答
1

据我所知,如果没有其他实例影响值,这是不可能的,所以如果它是一个常数,你仍然可以通过将它包装在这样的函数中:

(function( context ) {

    'use strict';

    var SOME_CONSTANT = 'Hello World';

    var SomeObject = function() {};

    SomeObject.prototype.sayHello = function() {
        console.log(SOME_CONSTANT);
    };

    context.SomeObject = SomeObject;

})( this );

var someInstance = new SomeObject();
someInstance.sayHello();

您可以做的最好的事情是注释一个属性不应该通过使用下划线this._value而不是this.value.

请注意,私有函数可以通过将它们隐藏在函数中来实现:

(function( context ) {

    'use strict';

    var SomeObject = function() {};

    var getMessage = function() {
        return 'Hello World';
    };

    SomeObject.prototype.sayHello = function() {
        console.log(getMessage());
    };

    context.SomeObject = SomeObject;

})( this );

var someInstance = new SomeObject();
someInstance.sayHello();

这是 2 个“类”相互扩展和交互的示例:http: //jsfiddle.net/TV3H3/

于 2013-06-09T09:17:22.343 回答
0

有比我更聪明的人回答这个问题,但我想特别指出您刚刚编辑的一个部分 -私有变量部分。

您可以使用闭包来模拟它;一个很棒的构造,它允许函数拥有自己的环境。你可以这样做:

var line = (function() {
  var length = 0;
  return {
    setLen : function (newLen) {
      length = newLen;
    },
    getLen : function () {
      return length;
    }
  };
}());

这会将 line 设置为具有setLenand方法的对象,但是如果不使用这些方法getLen,您将无法手动访问。length

于 2013-06-09T11:57:12.807 回答