Какие методы можно использовать для определения класса в JavaScript, и каковы их компромиссы?

686

Я предпочитаю использовать ООП в крупных проектах, таких как тот, над которым я сейчас работаю. Мне нужно создать несколько классов в JavaScript, но, если я не ошибаюсь, есть по крайней мере несколько способов сделать это. Каков будет синтаксис и почему это будет сделано таким образом?

Я хотел бы избежать использования сторонних библиотек - по крайней мере, на первых порах.
В поисках других ответов я нашел статью « Объектно-ориентированное программирование на JavaScript», часть I: Наследование - Doc JavaScript, в которой обсуждается объектно-ориентированное программирование на JavaScript. Есть ли лучший способ сделать наследство?

Karim
источник
примечание: это дубликат stackoverflow.com/questions/355848
Джейсон С
3
Лично мне нравится объявлять членов класса внутри тела функции. Я использую технику «исправления», чтобы создать замыкание, чтобы оно больше походило на класс. У меня есть подробный пример в моем блоге: ncombo.wordpress.com/2012/12/30/…
Джон
Я перенес большую часть функциональности ООП C ++ в JavaScript с простым и естественным синтаксисом. Смотрите мой ответ здесь: stackoverflow.com/a/18239463/1115652
В JavaScript нет классов. Но если вы хотите имитировать поведение класса в JS, вы можете. Подробности смотрите в: symfony-world.blogspot.com/2013/10/…
ducin

Ответы:

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. Вы захотите проверить хотя бы Prototype и jQuery .

Решить, какой из них «лучший», - отличный способ начать священную войну с переполнением стека. Если вы начинаете большой JavaScript-проект, то определенно стоит изучить популярную библиотеку и делать это по-своему. Я опытный парень, но Stack Overflow, похоже, склоняется к jQuery.

Поскольку существует только «один способ сделать это», без каких-либо зависимостей от внешних библиотек, то, как я написал, в значительной степени так.

Триптих
источник
48
Но он не работает как язык X, где я узнал один верный способ, которым вещь, которая используется для создания экземпляров объектов, должна работать :(
Эрик Реппен
2
Согласно developer.mozilla.org/en-US/docs/Web/JavaScript/… свойства также должны быть добавлены к прототипу ("Person.prototype.name = '';")
DaveD
1
@DaveD - возможно, так и было, но, похоже, больше нет?
Кирен Джонстон
6
JQuery даже не предоставляет никакого способа создания функциональности, подобной классу ??? (Все классы, которые у него есть, являются классами CSS). Вы должны удалить его из этой части ответа.
Берги
7
Со второй половины 2015 года был выпущен новый стандарт EcmaScript 6, поэтому я предлагаю сделать это по-новому (намного чище и проще
DevWL
213

Лучший способ определить класс в JavaScript - это не определять класс.

Шутки в сторону.

Существует несколько разновидностей объектно-ориентированных, некоторые из них:

  • ОО на основе классов (впервые представлен Smalltalk)
  • ОО на основе прототипа (впервые представленный Self)
  • мультиметодная ОО (впервые представленная CommonLoops, я думаю)
  • ОО на основе предикатов (без понятия)

И, вероятно, другие, о которых я не знаю.

JavaScript реализует ОО на основе прототипов. В ОО на основе прототипов новые объекты создаются путем копирования других объектов (вместо создания экземпляров из шаблона класса), а методы живут непосредственно в объектах, а не в классах. Наследование осуществляется посредством делегирования: если у объекта нет метода или свойства, он проверяется на его прототипах (то есть на объекте, из которого он был клонирован), затем на прототипах прототипа и так далее.

Другими словами: нет классов.

У JavaScript на самом деле есть хорошая настройка этой модели: конструкторы. Вы можете не только создавать объекты путем копирования существующих, но и создавать их, так сказать, «из воздуха». Если вы вызываете функцию с newключевым словом, эта функция становится конструктором, и thisключевое слово будет указывать не на текущий объект, а на вновь созданный «пустой» объект. Таким образом, вы можете настроить объект так, как вам нравится. Таким образом, конструкторы JavaScript могут взять на себя одну из ролей классов в традиционной ОО на основе классов: выступать в качестве шаблона или проекта для новых объектов.

Теперь, JavaScript очень мощный язык, поэтому довольно легко реализовать класс на основе системы OO внутри JavaScript , если вы хотите. Тем не менее, вы должны делать это только в том случае, если вам это действительно нужно, а не только потому, что так делает Java.

Йорг Миттаг
источник
«Если вы вызываете функцию с новым ключевым словом, эта функция становится конструктором, и ключевое слово this будет указывать не на текущий объект, а на вновь созданный« пустой »объект». Если вы вызываете функцию без ключевого слова new, это будет ссылаться на контекст вызова, по умолчанию глобальный объект (окно). В строгом режиме по умолчанию используется undefined. call, apply и bind принимает контекст вызова в качестве первого параметра. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Элиас Хасле
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), но вы можете использовать эти функции сейчас с таким транспайлером, как Babel .

Ресурсы

Дол
источник
56

Я предпочитаю использовать Даниэля X. Мура {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);
        });
      }
    },
  };
}

Пример наличия класса person включает в себя связываемый модуль.

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!

Раскрытие: я Даниэль X. Мур, и это мое {SUPER: SYSTEM}. Это лучший способ определить класс в JavaScript.

Даниэль Х Мур
источник
@DanielXMoore "Переменные экземпляра совместно используются отдельными экземплярами класса" Это не переменные экземпляра, а статические переменные / переменные класса.
JAB
2
@JAB Это неверно, статические / классовые переменные являются общими для всех экземпляров класса. Каждый экземпляр имеет свои собственные переменные экземпляра.
Даниэль Икс Мур
(Другими словами, используя нормальное значение термина «переменная экземпляра», является ли переменная единичной или нет, ортогональна уровню доступности переменной.)
JAB
2
Вы звучали почти как супергерой, претендующий на звание лучшего xD
Dadan
Простой подход к определению класса Javascript с использованием объектов javascript: wapgee.com/story/i/203
Ильяс
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'});
liammclennan
источник
Это очень элегантный способ построения полезной структуры объекта без необходимости что-либо импортировать. Я использовал систему классов Ресига, но мне это может понравиться больше. Спасибо.
Тим Сколлик
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
@Justin: Пожалуйста, дайте мне знать, что не является действительным?
Amol M Kulkarni
При изучении этих обозначений я также наткнулся на this.set (). Например: this.set ('port', 3000). Я думаю, это используется для установки свойства порта для объекта. Если это так, почему мы не используем напрямую: {port: 3000}. Есть ли документация, где я могу получить более подробную информацию.
адитьях
24

Я думаю, вам следует прочитать прототипное наследование Дугласа Крокфорда в JavaScript и классическое наследование в JavaScript .

Примеры с его страницы:

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;
});

Я также рекомендую его видео: Расширенный JavaScript .

Вы можете найти больше видео на его странице: http://javascript.crockford.com/ В книге Джона Рейсига вы найдете много примеров с сайта Дугласа Крокфора.

Ярек
источник
25
Это только я? Как, черт возьми, это более элегантно? Я бы назвал определения функций фактическими 'strings'именами многих вещей, но элегантность - не одно из них ...
fgysin восстановил Монику
4
@JAB, но отражение - это исключение, а не правило. С помощью вышеуказанного метода вы должны объявить все ваши методы со строками.
Кирк Волл
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 находится на чем-то похожем на первый прототип метода

annakata
источник
Мне это нравится. Я чаще всего использую стандартный синтаксис JS, потому что мне не нравится идея копирования функций в каждый экземпляр объекта. Я всегда скучал по красоте самодостаточного решения, и это решает его довольно хорошо.
Лукаш Коржибски
1
Не уверен, но я понял, что определение функции-прототипа внутри области действия (что-то вроде замыкания) приводит к утечке памяти, так как сборщик мусора не может попасть туда в экземпляре этих классов.
Санн
15

Если вы идете по простому, вы можете полностью избежать «нового» ключевого слова и просто использовать фабричные методы. Иногда я предпочитаю это, потому что мне нравится использовать 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» является началом своего рода конструктора, я полагаю?
Карим
Только что увидел комментарий Триптиха. Понимаю. Таким образом, вы можете просто сделать что-то вроде «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

Причина в thatтом, что это thisможет быть связано с чем-то другим, если вы дадите метод в качестве обработчика события, поэтому вы сохраните значение во время создания экземпляра и будете использовать его позже.

Изменить: это определенно не лучший способ, просто простой способ. Я тоже жду хороших ответов!

orip
источник
1
Это = эта конструкция не нужна здесь. Кроме того, методы add () и avg () будут скопированы для каждого «экземпляра» класса Foo, а не разделены между ними.
Триптих
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 . Он будет иметь внутренние приватные поля, называемые toggle и text . Он также будет иметь эти открытые элементы: поля подсчитывают и номер ; свойства toggle , text и numberLength ; методы incrementNumbersByCount и настройки .

Шаблон сворачивания полностью детализирован здесь: Шаблон сворачивания Javascript

intrepidis
источник
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());

tponthieux
источник
2

MooTools (My Object-Oriented Tools) сосредоточен на идее классов . Вы даже можете расширить и реализовать с наследованием.

Когда освоено, это делает для смешно многоразового, мощного JavaScript.

Райан Флоренс
источник
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.

Roland
источник
0

JavaScript является объектно-ориентированным , но он радикально отличается от других языков ООП, таких как Java, C # или C ++. Не пытайтесь понять это так. Выкинь это старое знание и начни заново. JavaScript нуждается в другом мышлении.

Я бы посоветовал получить хорошее руководство или что-то по этому вопросу. Я сам нашел ExtJS Tutorials лучшим для меня, хотя я не использовал фреймворк ни до, ни после прочтения. Но это дает хорошее объяснение того, что есть в мире JavaScript. Извините, похоже, что этот контент был удален. Вот ссылка на копию archive.org вместо этого. Работает сегодня. :П

Vilx-
источник
2
Объектно-ориентированный? Я думал, что это было функционально .
Питер Мортенсен
Ссылка "ExtJS Tutorials" не работает.
Питер Мортенсен
Я думаю, что было бы более объяснительным объяснить, что функции в javascript являются объектами, и правила области скобок javascript делают каждый функциональный блок инкапсулирующим.
Миббит
-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();

Авинаш Маурья
источник