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


686

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

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


примечание: это дубликат 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'});

Это очень элегантный способ построения полезной структуры объекта без необходимости что-либо импортировать. Я использовал систему классов Ресига, но мне это может понравиться больше. Спасибо.
— Тим Сколлик

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 находится на чем-то похожем на первый прототип метода


Мне это нравится. Я чаще всего использую стандартный синтаксис 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может быть связано с чем-то другим, если вы дадите метод в качестве обработчика события, поэтому вы сохраните значение во время создания экземпляра и будете использовать его позже.

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


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


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

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.


0

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

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


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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.