可以使用哪些技术在JavaScript中定义类,它们的权衡是什么?


686

我喜欢在目前正在从事的大型项目中使用OOP。我需要用JavaScript创建几个类,但是,如果我没有记错的话,至少有两种方法可以做到这一点。语法是什么,为什么要用这种方式呢?

我想避免使用第三方库-至少在一开始。
在寻找其他答案时,我找到了文章“ 使用JavaScript进行面向对象的编程”,第一部分:继承-文档JavaScript,该文章讨论了JavaScript中的面向对象的编程。有更好的继承方法吗?



3
就个人而言,我喜欢在函数体内声明类成员。我使用“修复此问题”技术来创建闭包,使其表现得更像类。我有我的博客上详细示例:ncombo.wordpress.com/2012/12/30/...
乔恩·

我使用简单自然的语法将大多数C ++ OOP功能移植到了JavaScript。在这里看到我的答案:stackoverflow.com/a/18239463/1115652

JavaScript中没有类。但是,如果您想在JS中模拟类的行为,则可以。详情请见:symfony-world.blogspot.com/2013/10/…–
ducin

Answers:


743

这是无需使用任何外部库即可完成的方法:

// Define a class like this
function Person(name, gender){

   // Add object properties like this
   this.name = name;
   this.gender = gender;
}

// Add methods like this.  All Person objects will be able to invoke this
Person.prototype.speak = function(){
    alert("Howdy, my name is" + this.name);
};

// Instantiate new objects with 'new'
var person = new Person("Bob", "M");

// Invoke methods like this
person.speak(); // alerts "Howdy, my name is Bob"

现在,真正的答案要复杂得多。例如,JavaScript中没有类。JavaScript使用prototype基于的继承方案。

此外,还有许多流行的JavaScript库,它们具有自己的样式,它们近似于JavaScript中的类功能。您将至少要签出PrototypejQuery

确定其中哪一个是“最佳”是在Stack Overflow上发起一场神圣战争的好方法。如果您正着手进行一个较大的JavaScript大型项目,那么绝对值得学习流行的库并按自己的方式去做。我是一个原型人,但是Stack Overflow似乎倾向于jQuery。

至于只有“一种方法”,而没有对外部库的任何依赖,我写的方式就差不多了。


48
但是它不能像X语言那样工作,在X语言中,我学会了一种用于制作对象实例的事物应该起作用的真实方法:(
Erik Reppen

2
根据developer.mozilla.org/zh-CN/docs/Web/JavaScript/…的属性也应添加到原型中(“ Person.prototype.name =”;“)
DaveD

1
@DaveD-也许可以了,但是现在似乎不再了。
Kieren Johnstone 2014年

6
jQuery甚至不提供任何创建类功能的方法?(它拥有的所有类都是CSS类)您应该将其从答案的那一部分中删除。
Bergi 2014年

7
从2015年下半年开始发布EcmaScript 6新标准,所以我建议采用新方法(更加简洁
DevWL

213

在JavaScript中定义类的最佳方法是不定义类。

说真的

有几种不同的面向对象风格,其中一些是:

  • 基于类的OO(由Smalltalk首次引入)
  • 基于原型的OO(由Self首次引入)
  • 基于多方法的OO(我认为是CommonLoops首次引入的)
  • 基于谓词的面向对象(不知道)

可能还有其他我不认识的人。

JavaScript实现了基于原型的OO。在基于原型的OO中,通过复制其他对象(而不是从类模板实例化)来创建新对象,并且方法直接存在于对象中而不是类中。继承是通过委托完成的:如果对象没有方法或属性,则在其原型(即从其克隆的对象)上查找该对象,然后在该原型的原型上进行查找,依此类推。

换句话说:没有类。

JavaScript实际上对该模型做了一个很好的调整:构造函数。可以说,不仅可以通过复制现有对象来创建对象,还可以“凭空”构造它们。如果使用new关键字调用一个函数,则该函数将成为构造函数,而该this关键字将不会指向当前对象,而是指向一个新创建的“空”对象。因此,您可以根据自己的喜好配置对象。这样,JavaScript构造函数就可以在传统的基于类的OO中担当类的角色之一:充当新对象的模板或蓝图。

现在,JavaScript是一种非常强大的语言,因此,如果需要,在JavaScript中实现基于类的OO系统非常容易。但是,只有在确实有需要时才应该这样做,而不仅仅是因为Java就是这样做的。


“如果使用new关键字调用一个函数,则该函数将成为构造函数,而this关键字将不会指向当前对象,而是指向一个新创建的“空”对象。” 如果调用不带new关键字的函数,则它将引用调用上下文,默认情况下是全局对象(窗口)。在严格模式下,未定义是默认设置。调用,应用和绑定将调用上下文作为第一个参数。developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/...
埃利亚斯Hasle城

83

ES2015课程

在ES2015规范中,您可以使用类语法,它只是原型系统的基础。

class Person {
  constructor(name) {
    this.name = name;
  }
  toString() {
    return `My name is ${ this.name }.`;
  }
}

class Employee extends Person {
  constructor(name, hours) {
    super(name);
    this.hours = hours;
  }
  toString() {
    return `${ super.toString() } I work ${ this.hours } hours.`;
  }
}

好处

主要优点是静态分析工具发现更容易针对此语法。对于其他来自基于类的语言的人,使用该语言作为多语言也更容易。

注意事项

警惕其当前的局限性。要获得私有属性,必须使用Symbols或WeakMaps。在将来的版本中,很可能会将类扩展为包括这些缺少的功能。

支持

浏览器支持是不是现在(通过IE除外几乎每个人都支持)很不错,但你可以像现在transpiler使用这些功能巴贝尔

资源资源


56

我更喜欢使用Daniel X. Moore的{SUPER: SYSTEM}。这是一门提供诸如真实实例变量,基于特征的继承,类层次结构和配置选项之类的好处的学科。下面的示例说明了真实实例变量的使用,我认为这是最大的优势。如果您不需要实例变量并且只对公共或私有变量感到满意,那么可能会有更简单的系统。

function Person(I) {
  I = I || {};

  Object.reverseMerge(I, {
    name: "McLovin",
    age: 25,
    homeState: "Hawaii"
  });

  return {
    introduce: function() {
      return "Hi I'm " + I.name + " and I'm " + I.age;
    }
  };
}

var fogel = Person({
  age: "old enough"
});
fogel.introduce(); // "Hi I'm McLovin and I'm old enough"

哇,它本身并不是真的很有用,但是请看一下添加子类:

function Ninja(I) {
  I = I || {};

  Object.reverseMerge(I, {
    belt: "black"
  });

  // Ninja is a subclass of person
  return Object.extend(Person(I), {
    greetChallenger: function() {
      return "In all my " + I.age + " years as a ninja, I've never met a challenger as worthy as you...";
    }
  });
}

var resig = Ninja({name: "John Resig"});

resig.introduce(); // "Hi I'm John Resig and I'm 25"

另一个优点是具有基于模块和特征的继承的能力。

// The Bindable module
function Bindable() {

  var eventCallbacks = {};

  return {
    bind: function(event, callback) {
      eventCallbacks[event] = eventCallbacks[event] || [];

      eventCallbacks[event].push(callback);
    },

    trigger: function(event) {
      var callbacks = eventCallbacks[event];

      if(callbacks && callbacks.length) {
        var self = this;
        callbacks.forEach(function(callback) {
          callback(self);
        });
      }
    },
  };
}

具有人员类的示例包括可绑定模块。

function Person(I) {
  I = I || {};

  Object.reverseMerge(I, {
    name: "McLovin",
    age: 25,
    homeState: "Hawaii"
  });

  var self = {
    introduce: function() {
      return "Hi I'm " + I.name + " and I'm " + I.age;
    }
  };

  // Including the Bindable module
  Object.extend(self, Bindable());

  return self;
}

var person = Person();
person.bind("eat", function() {
  alert(person.introduce() + " and I'm eating!");
});

person.trigger("eat"); // Blasts the alert!

披露:我是Daniel X. Moore,这是我的{SUPER: SYSTEM}。这是在JavaScript中定义类的最佳方法。


@DanielXMoore“实例变量在类的各个实例之间共享”那些不是实例变量,它们是静态/类变量。
2013年

2
@JAB是不正确的,静态/类变量在类的所有实例之间共享。每个实例都有其自己的实例变量。
Daniel X Moore

(换句话说,使用术语“实例变量”的通常含义,无论变量是否为一个,都正交于变量的可访问性级别。)
JAB

2
您几乎听起来像是获得最佳xD的超级英雄
丹丹(Dandan)2016年

一种使用javascript对象定义Javascript类的简单方法:wapgee.com/story/i/203
Ilyas karim

41
var Animal = function(options) {
    var name = options.name;
    var animal = {};

    animal.getName = function() {
        return name;
    };

    var somePrivateMethod = function() {

    };

    return animal;
};

// usage
var cat = Animal({name: 'tiger'});

这是一种无需导入任何东西即可构建可用对象结构的非常优雅的方法。我使用的是Resig的课程系统,但我可能会更喜欢。谢谢。
Tim Scollick 2013年

29
这种方法的问题在于,每次创建新的Animal实例时,它都会重新定义功能,而不是仅使用原型定义一次。
贾斯汀

33

以下是迄今为止我在javascript中使用过的创建对象的方法

范例1:

obj = new Object();
obj.name = 'test';
obj.sayHello = function() {
    console.log('Hello '+ this.name);
}

范例2:

obj = {};
obj.name = 'test';
obj.sayHello = function() {
    console.log('Hello '+ this.name);
}
obj.sayHello();

范例3:

var obj = function(nameParam) {
    this.name = nameParam;
}
obj.prototype.sayHello = function() {
    console.log('Hello '+ this.name);
}

示例4:Object.create()的实际好处。请参考[此链接]

var Obj = {
    init: function(nameParam) {
        this.name = nameParam;
    },
    sayHello: function() {
        console.log('Hello '+ this.name);
    }
};
var usrObj = Object.create(Obj);  // <== one level of inheritance

usrObj.init('Bob');
usrObj.sayHello();

示例5(自定义的Crockford's Object.create):

Object.build = function(o) {
   var initArgs = Array.prototype.slice.call(arguments,1)
   function F() {
      if((typeof o.init === 'function') && initArgs.length) {
         o.init.apply(this,initArgs)
      }
   }
   F.prototype = o
   return new F()
}
MY_GLOBAL = {i: 1, nextId: function(){return this.i++}}  // For example

var userB = {
    init: function(nameParam) {
        this.id = MY_GLOBAL.nextId();
        this.name = nameParam;
    },
    sayHello: function() {
        console.log('Hello '+ this.name);
    }
};
var bob = Object.build(userB, 'Bob');  // Different from your code
bob.sayHello();


使用ES6 / ES2015随时更新答案

一个类的定义如下:

class Person {
    constructor(strName, numAge) {
        this.name = strName;
        this.age = numAge;
    }

    toString() {
        return '((Class::Person) named ' + this.name + ' & of age ' + this.age + ')';
    }
}

let objPerson = new Person("Bob",33);
console.log(objPerson.toString());

1
@贾斯汀:请让我知道什么无效?
Amol M Kulkarni 2015年

在研究这些符号时,我也偶然发现了this.set()。例如:this.set('port',3000)。我的猜测是,这用于设置对象的端口属性。如果是这样,为什么我们不直接使用:{port:3000}。有没有可以获取更多详细信息的文档。
adityah

24

我想你应该读道格拉斯Crockford的原型继承在JavaScriptJavaScript中的经典传承

他页面上的示例:

Function.prototype.method = function (name, func) {
    this.prototype[name] = func;
    return this;
};

影响?它将允许您以更优雅的方式添加方法:

function Parenizor(value) {
    this.setValue(value);
}

Parenizor.method('setValue', function (value) {
    this.value = value;
    return this;
});

我还推荐他的视频: Advanced JavaScript

您可以在他的页面上找到更多视频:http : //javascript.crockford.com/在John Reisig的书中,您可以从Douglas Crockfor的网站上找到许多示例。


25
只有我吗?这到底如何优雅?我会以实际的'strings'名称来调用函数定义,但是很多事情都不是优雅的……
fgysin恢复Monica

4
@JAB,但是反射是例外,不是规则。使用上述方法,您必须使用字符串声明所有方法
Kirk Woll 2013年

16

因为我不会接受YUI / Crockford的工厂计划,并且因为我喜欢保持事物的独立性和可扩展性,所以这是我的选择:

function Person(params)
{
  this.name = params.name || defaultnamevalue;
  this.role = params.role || defaultrolevalue;

  if(typeof(this.speak)=='undefined') //guarantees one time prototyping
  {
    Person.prototype.speak = function() {/* do whatever */};
  }
}

var Robert = new Person({name:'Bob'});

理想情况下,typeof测试是在第一个方法原型上进行的


我喜欢。我最经常使用JS的标准语法,因为我不喜欢将函数复制到每个对象实例中的想法。我总是想念自包含解决方案的美丽,这可以很好地解决它。
Lukasz Korzybski

1
不确定,但我知道在函数范围内(某种程度上是闭包)定义原型函数会导致内存泄漏,因为这些类的实例中垃圾回收器无法到达那里。
桑(Sanne)

15

如果您只是为了简单起见,则可以完全避免使用“ new”关键字,而只需使用工厂方法即可。有时候,我更喜欢这样做,因为我喜欢使用JSON创建对象。

function getSomeObj(var1, var2){
  var obj = {
     instancevar1: var1,
     instancevar2: var2,
     someMethod: function(param)
     {  
          //stuff; 
     }
  };
  return obj;
}

var myobj = getSomeObj("var1", "var2");
myobj.someMethod("bla");

不过,我不确定大型对象的性能会受到什么影响。


不需要obj.instancevar1 = var1行,因为内部对象可以访问getSomeObj()的参数。
三联画

哇。那使我的大脑受伤,但是它有一定的优雅。我想,“ obj.instancevar1 = var1”部分是某种构造函数的开始吗?
卡里姆

刚刚看到Triptych的评论。我知道了。因此,您可以执行“ instancevar1:var1”之类的操作,在其中实例化内部对象。
卡里姆

确实...当您使用{}定义对象时,它可以访问当前作用域内的变量。
山姆

10
通过这种方法,您失去了继承的能力,并且由于您没有使用obj.prototype.something,因此每次使用对象时都在定义函数=更多的内存和更慢的内存。
一些

12
var Student = (function () {
    function Student(firstname, lastname) {
        this.firstname = firstname;
        this.lastname = lastname;
        this.fullname = firstname + " " + lastname;
    }

    Student.prototype.sayMyName = function () {
        return this.fullname;
    };

    return Student;
}());

var user = new Student("Jane", "User");
var user_fullname = user.sayMyName();

这就是TypeScript将带有构造函数的类编译为JavaScript的方式。


10

简单的方法是:

function Foo(a) {
  var that=this;

  function privateMethod() { .. }

  // public methods
  that.add = function(b) {
    return a + b;
  };
  that.avg = function(b) {
    return that.add(b) / 2; // calling another public method
  };
}

var x = new Foo(10);
alert(x.add(2)); // 12
alert(x.avg(20)); // 15

原因thatthis如果将方法作为事件处理程序提供则可以绑定到其他对象,因此可以在实例化过程中保存该值并在以后使用。

编辑:这绝对不是最好的方法,只是一种简单的方法。我也在等待好的答案!


1
that = this构造在这里不是必需的。同样,将为Foo类的每个“实例”复制add()和avg()方法,而不是在它们之间共享。
三联画

1
在这种情况下是否有必要(排序),但不是您提供的简单情况。
三联画

9

您可能想使用折叠模式创建类型:

    // Here is the constructor section.
    var myType = function () {
        var N = {}, // Enclosed (private) members are here.
            X = this; // Exposed (public) members are here.

        (function ENCLOSED_FIELDS() {
            N.toggle = false;
            N.text = '';
        }());

        (function EXPOSED_FIELDS() {
            X.count = 0;
            X.numbers = [1, 2, 3];
        }());

        // The properties below have access to the enclosed fields.
        // Careful with functions exposed within the closure of the
        // constructor, each new instance will have it's own copy.
        (function EXPOSED_PROPERTIES_WITHIN_CONSTRUCTOR() {
            Object.defineProperty(X, 'toggle', {
                get: function () {
                    var before = N.toggle;
                    N.toggle = !N.toggle;
                    return before;
                }
            });

            Object.defineProperty(X, 'text', {
                get: function () {
                    return N.text;
                },
                set: function (value) {
                    N.text = value;
                }
            });
        }());
    };

    // Here is the prototype section.
    (function PROTOTYPE() {
        var P = myType.prototype;

        (function EXPOSED_PROPERTIES_WITHIN_PROTOTYPE() {
            Object.defineProperty(P, 'numberLength', {
                get: function () {
                    return this.numbers.length;
                }
            });
        }());

        (function EXPOSED_METHODS() {
            P.incrementNumbersByCount = function () {
                var i;
                for (i = 0; i < this.numbers.length; i++) {
                    this.numbers[i] += this.count;
                }
            };
            P.tweak = function () {
                if (this.toggle) {
                    this.count++;
                }
                this.text = 'tweaked';
            };
        }());
    }());

该代码将为您提供一个名为myType的类型。它将具有称为切换文本的内部私有字段。它还将具有以下暴露的成员:字段数字;属性切换文本数字长度 ; 方法crementNumbersByCount调整

折叠模式在此处进行了详细说明: Javascript折叠模式


3

为@liammclennan的答案编写代码。

var Animal = function (args) {
  return {
    name: args.name,

    getName: function () {
      return this.name; // member access
    },

    callGetName: function () {
      return this.getName(); // method call
    }
  };
};

var cat = Animal({ name: 'tiger' });
console.log(cat.callGetName());



2

具有继承的基于对象的类

var baseObject = 
{
     // Replication / Constructor function
     new : function(){
         return Object.create(this);   
     },

    aProperty : null,
    aMethod : function(param){
      alert("Heres your " + param + "!");
    },
}


newObject = baseObject.new();
newObject.aProperty = "Hello";

anotherObject = Object.create(baseObject); 
anotherObject.aProperty = "There";

console.log(newObject.aProperty) // "Hello"
console.log(anotherObject.aProperty) // "There"
console.log(baseObject.aProperty) // null

简单,贴心,可以完成。


1

一个基地

function Base(kind) {
    this.kind = kind;
}

一类

// Shared var
var _greeting;

(function _init() {
    Class.prototype = new Base();
    Class.prototype.constructor = Class;
    Class.prototype.log = function() { _log.apply(this, arguments); }
    _greeting = "Good afternoon!";
})();

function Class(name, kind) {
    Base.call(this, kind);
    this.name = name;
}

// Shared function
function _log() {
    console.log(_greeting + " Me name is " + this.name + " and I'm a " + this.kind);
}

行动

var c = new Class("Joe", "Object");
c.log(); // "Good afternoon! Me name is Joe and I'm a Object"

1

根据三联画的示例,这甚至可能更简单:

    // Define a class and instantiate it
    var ThePerson = new function Person(name, gender) {
        // Add class data members
        this.name = name;
        this.gender = gender;
        // Add class methods
        this.hello = function () { alert('Hello, this is ' + this.name); }
    }("Bob", "M"); // this instantiates the 'new' object

    // Use the object
    ThePerson.hello(); // alerts "Hello, this is Bob"

这只会创建一个对象实例,但是如果您想在类中封装一堆变量和方法的名称,它仍然很有用。通常,构造函数将没有“ Bob,M”参数,例如,如果方法将被调用到具有自己数据的系统(例如数据库或网络)。

我对JS还是太陌生,所以看不到为什么不使用它prototype


0

JavaScript是面向对象的,但与Java,C#或C ++ 等其他OOP语言完全不同。不要试图那样理解它。把旧知识扔掉,重新开始。JavaScript需要不同的想法。

我建议您获得一本好的手册或有关该主题的内容。我自己发现ExtJS教程最适合我,尽管我在阅读框架之前或之后都没有使用过该框架。但这确实对JavaScript世界中的什么给出了很好的解释。抱歉,该内容似乎已被删除。这是指向archive.org复制的链接。今天工作。:P


2
面向对象?我认为它可以正常工作
Peter Mortensen

“ ExtJS教程”链接已断开。
Peter Mortensen

我认为解释javascript中的函数是对象会更具解释性,而javascript的方括号规则将每个功能块封装起来。
mibbit

-1

//new way using this and new
function Persons(name) {
  this.name = name;
  this.greeting = function() {
    alert('Hi! I\'m ' + this.name + '.');
  };
}

var gee=new Persons("gee");
gee.greeting();

var gray=new Persons("gray");
gray.greeting();

//old way
function createPerson(name){
 var obj={};
 obj.name=name;
 obj.greeting = function(){
 console.log("hello I am"+obj.name);
 }; 
  return obj;
}

var gita=createPerson('Gita');
gita.greeting();

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.