Статьи

JavaScript не нуждается в классах

Часто встречается мнение, что наследование прототипов JavaScript слишком сложное и что ему нужны классы, чтобы быть более удобным для пользователя. Этот пост утверждает, что это мнение неверно.

Резюме: Ядро прототипного наследования JavaScript очень простое, оно просто затенено неуклюжим способом создания экземпляров с помощью конструкторов. Поэтому нам не нужны классы, нам просто нужно исправить конструкторы. Продолжайте читать три способа сделать это.

Прототип наследования прост

С одной стороны, JavaScript явно превосходит большинство языков на основе классов — вы можете напрямую создавать объекты:

    var jane = {
        name: "Jane",
        describe: function() {
            return "Person called "+this.name;
        }
    };
    
    console.log(jane.describe()); // Person called Jane

Джейн является объектом, который был создан с помощью
литерала объекта . Имя и описание являются
свойствами . Описание это свойство, значение которого является функцией. Такие функционально-значимые свойства называются
методами . Литерал объекта выглядит как словарь (карта), но это реальный объект. В большинстве языков на основе классов вам нужен класс для его создания. Отсюда и
шаблон синглтона .

Основная идея наследования прототипов невероятно проста , намного проще, чем классы. То, что делает JavaScript настолько сложным, заключается в том, что эта основная идея затеняется при попытке сделать создание экземпляров (данного типа) похожим на Java. Основная идея наследования прототипа такова: объект может указывать на другой объект и, таким образом, делать его своим прототипом . Если свойство не найдено в объекте, поиск продолжается в прототипе (и, если он есть, его прототип и т. Д.). Это позволяет моделировать Джейн и Тарзан как «экземпляры» PersonProto:

    var PersonProto = {
        describe: function () {
            return "Person called "+this.name;
        },
    };
    var jane = {
        __proto__: PersonProto,
        name: "Jane",
    };
    var tarzan = {
        __proto__: PersonProto,
        name: "Tarzan",
    };
    
    console.log(jane.describe()); // Person called Jane

Джейн и Тарзан используют один и тот же прототип PersonProto, который предоставляет метод description () им обоим. Обратите внимание, насколько PersonProto похож на класс.

Конструкторы. Способ создания фабрики для экземпляров PersonProto по умолчанию — через функцию конструктора (short: constructor). Это нормальная функция, которая вызывается через оператор new для создания экземпляра. В следующем коде Person является конструктором. Person.prototype — это тот же объект, что и PersonProto, он становится общим прототипом всех экземпляров Person. Очевидно, конструкторы соответствуют классам в других языках.

    // Constructor: set up the instance
    function Person(name) {
        this.name = name;
    }

    // Prototype: shared by all instances
    Person.prototype.describe = function () {
        return "Person called "+this.name;
    };

    var jane = new Person("Jane");
    console.log(jane instanceof Person); // true

    console.log(jane.describe()); // Person called Jane

Расширяющиеся конструкторы. Вещи становятся неприятными только тогда, когда вы переходите к расширению конструктора, к созданию суб-конструктора через наследование. Давайте создадим Employee как вспомогательный конструктор Person: Сотрудник — это человек, но у него дополнительно есть заголовок, и его метод description () работает по-другому, поскольку он также упоминает заголовок.

    function Employee(name, title) {
        Person.call(this, name);
        this.title = title;
    }
    Employee.prototype = Object.create(Person.prototype);
    Employee.prototype.constructor = Employee;
    Employee.prototype.describe = function () {
        return Person.prototype.describe.call(this)
               + " (" + this.title + ")";
    };

    var jane = new Employee("Jane", "CTO");
    console.log(jane instanceof Person); // true
    console.log(jane instanceof Employee); // true

    console.log(jane.describe()); // Person called Jane (CTO)

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

Решение по наследованию 1: незначительные языковые дополнения

Если вы добавите четыре второстепенные конструкции в JavaScript (как
предложено для ECMAScript.next), тогда все станет намного проще:

  1. Оператор наследования <| (читается как «расширен»):
        var Super = function () { ... }
        var Sub   = Super <| function () { ... }
    

    Конструктор Sub расширяет конструктор Super.

  2. Супер собственность доступ:
        super.describe()
    
  3. Оператор расширения . = Добавляет свойства к объекту вместо его замены:
        var colorPoint = { color: "green" };
        colorPoint .= { x: 33, y: 7 }
    

    После этого colorPoint имеет значение

        { color: "green", x: 33, y: 7 }
    
  4. Укороченный синтаксис метода для литералов объекта:
        {
            method(arg1, arg2) {
                ...
            }
        }
    

    это аббревиатура для

        {
            method: function (arg1, arg2) {
                ...
            }
        }
    

С этими конструкциями предыдущий код выглядит следующим образом:

    function Person(name) {
        this.name = name;
    }
    Person.prototype .= {
        describe() {
            return "Person called "+this.name;
        }
    };
    
    var Employee = Person <| function (name, title) {
        super.constructor(name);
        this.title = title;
    }
    Employee.prototype .= {
        describe() {
            return super.describe() + " (" + this.title + ")";
        }
    };

Это уже вполне пригодно для использования. Остановка здесь будет в порядке, но мы можем сделать немного лучше.

Решение по наследованию 2: образцы объектов

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

    var Person = {
        constructor(name) {
            this.name = name;
        },
        describe() {
            return "Person called "+this.name;
        }
    };
    var Employee = Person <| {
        constructor(name, title) {
            super.constructor(name);
            this.title = title;
        },
        describe() {
            return super.describe() + " (" + this.title + ")";
        }
    };
    
    var jane = new Employee("Jane", "CTO");
    console.log(jane instanceof Employee); // true

Чтобы вышеперечисленное сработало, вам нужно только адаптировать следующие вещи:

  • Оператор наследования <| должен работать и для объектов тоже. Затем он устанавливает прототип объекта.
  • new должен принимать объекты как операнды, а не только функции.
  • instanceof должен разрешать объекты как правую часть, а не только функции.
  • super работает одинаково для образцов функций и объектов: он ищет супер-свойство, начиная с прототипа, в котором находится текущий метод.

Основным недостатком этого подхода является то, что он отличается от используемых в настоящее время примеров функций: теперь прототипы носят имя типа, а не конструкторы. Но можно достичь определенной степени совместимости, обеспечив, чтобы как левая, так и правая части оператора наследования могли быть либо функцией, либо объектом:

    Constructor1 <| Constructor2
    Prototype1   <| Prototype2
    Constructor  <| Prototype
    Prototype    <| Constructor

Основным преимуществом объектных образцов является их простота. Создатели языка программирования Self (который Эйх называет одним из влияний JavaScript) всегда знали об этой простоте. Они хорошо объясняют это в статье Дэвида Унгара «
Организация программ без классов », Крейга Чамберса, Бэй-Вей Чанга, Урса Хольцле (очень рекомендуется, очень читабельно).

Решение по наследованию 3: объявления классов

Несмотря на то, что описанная здесь конструкция называется
объявлением класса , на самом деле это не класс: это синтаксический сахар для конструктора и, следовательно, внутренне преобразованный в функцию. Синтаксис для объявлений классов в настоящее время работает. Это может выглядеть примерно так:

    class Person {
        constructor(name) {
            this.name = name;
        },
        describe() {
            return "Person called "+this.name;
        }
    }
    class Employee extends Person {
        constructor(name, title) {
            super.constructor(name);
            this.title = title;
        },
        describe() {
            return super.describe() + " (" + this.title + ")";
        }
    }

JavaScript имеет долгую историю предложений для классоподобных синтаксических конструкций. Три текущих предложения (в хронологическом порядке):

Естественно, предложение Брендана Эйха будет самым влиятельным, но оно было основано на идеях Ашкенаса. Мое предложение пытается найти баланс между ними, и оно было основано на идеях Аллена Уирфса-Брока об объектных литералах и объектных образцах.

Плюсы и минусы классовых объявлений:

  • Плюсы: они помогают с проблемами юзабилити наследования, особенно, если кто-то из языков на основе классов. И они совместимы с конструкторами, действующими стандартами для экземпляров фабрик.
  • Против: Они не совсем соответствуют прототипной природе языка — в отличие от объектных образцов, которые выглядят практически так же, как объявления классов. Объявления классов скрывают сложности создания экземпляров на основе конструктора, образцы объектов удаляют их.

Множественное наследование

Множественное наследование было явно отклонено для ECMAScript.next, поэтому ни одно из представленных решений не будет поддерживать его. Однако есть
предложение добавить его позже (после ECMAScript.next) в виде черт.

Вывод

По своей сути наследование прототипов JavaScript очень простое. Я хотел бы сохранить эту простоту, сделав объект примерами фабрики экземпляров по умолчанию в JavaScript. Но частичное нарушение обратной совместимости является серьезным препятствием. Следовательно, я все еще был бы удовлетворен вторым лучшим решением: объявлениями класса. Несмотря на то, что они называются классами, они на самом деле являются функциями (что объясняет заголовок этого поста).

Приложение: «JavaScript должен быть заменен»

Роберт Коритник делает достоверное наблюдение в Google+:


Ну, просто посмотрите на количество
решений, которые не делают ничего, кроме как добавить к путанице. Javascript был бы действительно хорош, если бы он поддерживал статические типы. Но вместо расширения Javascript, вероятно, было бы лучше предложить второй язык, который поддерживали бы все браузеры и который был бы ОО и статически типизирован.

Ответ: Вы объединяете три вопроса:

  • Подтип в настоящее время сложен, текущие решения помогают только частично: эта проблема исчезнет в ECMAScript.next, потому что она введет стандартный механизм для подтипирования (так или иначе). Этот механизм будет легко понять — дайте шанс вариантам, описанным в [этом посте]; они выглядят необычно для людей, которые в основном знакомы с классами, но на самом деле они довольно просты.
  • Поддержка статических типов: многое из того, что дает вам статическая типизация, может быть достигнуто с помощью вывода типов. Остальная часть может быть добавлена ​​через тип охранников (которые находятся на палубе для ECMAScript.next.next).
  • Замена JavaScript чем-то лучшим: Google в настоящее время пытается сделать это с помощью Dart . Но как только вы привыкните к JavaScript, чего-то не хватает в менее динамичных языках, таких как Dart. Кроме того, обратная совместимость является огромным аргументом в пользу JavaScript. И, наконец, другие поставщики браузеров вряд ли перейдут на Dart под управлением Google (с открытым исходным кодом или нет). Начинать с нуля — типичная инженерная ошибка, улучшение того, что там происходит, обычно дает лучшие результаты (подход «чем хуже, тем лучше»). ECMAScript.next будет отличным языком — он удаляет большинство причуд JavaScript, но сохраняет свою ловкость.

Вы можете прочитать
остальную часть темы в Google+.

Связанное чтение

  1. Прототипы как классы — введение в наследование JavaScript [подробно объясняет образцы объектов под их прежним названием «прототипы как классы»; включает в себя библиотеку для ECMAScript 5]
  2. Краткая история версий ECMAScript (включая Harmony и ES.next)
  3. ECMAScript.next: обновление «TXJS» от Eich [обзор того, что в настоящее время планируется для ECMAScript.next или ECMAScript 6]

 

 

Источник: http://www.2ality.com/2011/11/javascript-classes.html