Подтвердить что ты не робот

Являются ли классы es6 просто синтаксическим сахаром для прототипа в javascript?

После игры с ES6 мне действительно понравился новый синтаксис и доступные функции, но у меня есть вопрос о классах.

являются ли новые классы ES6 просто синтаксическим сахаром для старого прототипа? или там больше происходит здесь за кулисами? то есть:

class Thing {
   //... classy stuff
  doStuff(){}
}

vs:

var Thing = function() {
  // ... setup stuff
};

Thing.prototype.doStuff = function() {}; // etc
4b9b3361

Ответ 1

Да, возможно, но некоторые из синтаксического сахара имеют зубы.

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

Функция конструктора классов имеет обычный объект-прототип, из которого экземпляры класса наследуют свойства в обычном режиме JavaScript. В этот прототип добавляются методы экземпляра, определенные в классе.

ES6 не предоставляет средства для объявления значений свойств по умолчанию класса экземпляра класса (т.е. значений, которые не являются методами) внутри тела класса, которые должны храниться в прототипе и унаследованы. Чтобы инициализировать значение экземпляра, вы можете либо установить их как локальные, не наследуемые свойства в конструкторе, либо вручную добавить их в объект класса prototype вне определения класса так же, как и для обычных конструкторских функций. (Я не утверждаю, что по существу или иным образом создаю унаследованные свойства для классов JavaScript).

Статические методы, объявленные внутри тела класса, добавляются как свойства функции конструктора классов. Избегайте использования имен методов статического класса, которые конкурируют со стандартными функциональными свойствами и методами, унаследованными от Function.prototype, такими как call, apply или length.

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

Некоторые интересные вещи возникают, когда вы расширяете класс:

  • Свойство объекта prototype конструктора расширенного класса автоматически создается прототипом объекта prototype расширяемого класса. Это не является особенно новым, и эффект можно дублировать с помощью Object.create.

  • функция конструктора расширенного класса (объект) автоматически прототипируется в функции конструктора расширенного класса, а не Function. Хотя может быть возможно воспроизвести эффект на обычную конструкторскую функцию с помощью Object.setPrototypeOf или даже childClass.__proto__ = parentClass, это будет чрезвычайно необычная практика кодирования и часто рекомендуется в документации JavaScript.

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

Я считаю, что наивно думать, что декларации и выражения класса останутся неизменными во всех будущих версиях ECMA Script, и будет интересно узнать, происходят ли и когда происходят события. Вероятно, стало причудой ассоциировать "синтаксический сахар" с классами, введенными в ES6 (ECMA-262, стандартная версия 6), но лично я стараюсь избегать его повторения.

Ответ 2

  Являются ли новые классы ES6 просто синтаксическим сахаром для старого образца прототипа?

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

Источник

В следующем примере краткого кода показано, как функции в class устанавливаются для объекта prototype.

class Thing {
   someFunc() {}
}

console.log("someFunc" in Thing.prototype); // true

Ответ 3

Да. Но они более строгие.

В ваших примерах есть два основных отличия.

Прежде всего, с синтаксисом класса вы не можете инициализировать экземпляр без new.

class Thing{}
Thing() //Uncaught TypeError: Class constructor Thing cannot be invoked without 'new'

var Thing = function() {
  if(!(this instanceof Thing)){
     return new Thing();
  }
};
Thing(); //works

Второй, классы, определенные с помощью синтаксиса класса, являются блочными. Он похож на определение переменных с ключевым словом let.

class Thing{}
class Thing{} //Uncaught SyntaxError: Identifier 'Thing' has already been declared

{
    class Thing{}
}
console.log(Thing); //Uncaught ReferenceError: Thing is not defined

Изменить

Как упоминается в своем комментарии @zeroflagL, объявления классов также не поднимаются.

console.log(Thing) //Uncaught ReferenceError: Thing is not defined
class Thing{}

Ответ 4

Нет, классы ES6 - это не просто синтаксический сахар для прототипа.

Хотя во многих местах можно прочитать об обратном, и хотя на первый взгляд это кажется правдой, все становится сложнее, когда вы начинаете копаться в деталях.

Я не был полностью удовлетворен существующими ответами. Проведя дополнительные исследования, я классифицировал особенности классов ES6 в своем уме:

  1. Синтаксический сахар для стандартного псевдоклассического наследования ES5.
  2. Синтаксический сахар для улучшения псевдоклассического паттерна наследования доступен, но непрактичен или необычен в ES5.
  3. Синтаксический сахар для улучшений шаблона псевдоклассического наследования, недоступного в ES5, но который может быть реализован в ES6 без синтаксиса класса.
  4. Функции, которые невозможно реализовать без синтаксиса class, даже в ES6.

(Я попытался сделать этот ответ как можно более полным, и в результате он стал довольно длинным. Тем, кто более заинтересован в хорошем обзоре, следует взглянуть на traktor53s answer.)


Итак, позвольте мне шаг за шагом "десагар" (и, насколько это возможно), объявления классов ниже, чтобы проиллюстрировать происходящее:

// Class Declaration:
class Vertebrate {
    constructor( name ) {
        this.name = name;
        this.hasVertebrae = true;
        this.isWalking = false;
    }

    walk() {
        this.isWalking = true;
        return this;
    }

    static isVertebrate( animal ) {
        return animal.hasVertebrae;
    }
}

// Derived Class Declaration:
class Bird extends Vertebrate {
    constructor( name ) {
        super( name )
        this.hasWings = true;
    }

    walk() {
        console.log( "Advancing on 2 legs..." );
        return super.walk();
    }

    static isBird( animal ) {
        return super.isVertebrate( animal ) && animal.hasWings;
    }
}

1. Синтаксический сахар для стандартного псевдоклассического наследования ES5

В своей основе классы ES6 действительно предоставляют синтаксический сахар для стандартного образца псевдоклассического наследования ES5.

Объявления классов/выражения

В фоновом режиме объявление класса или выражение класса создаст функцию конструктора с тем же именем, что и у класса, так что:

  1. Внутреннее свойство [[Construct]] конструктора ссылается на блок кода, присоединенный к методу класса constructor().
  2. Методы classe определены в свойстве constructors prototype (пока мы не включаем статические методы).

Используя синтаксис ES5, первоначальное объявление класса, таким образом, примерно эквивалентно следующему (без учета статических методов):

function Vertebrate( name ) {           // 1. A constructor function containing the code of the class constructor method is defined
    this.name = name;
    this.hasVertebrae = true;
    this.isWalking = false;
}

Object.assign( Vertebrate.prototype, {  // 2. Class methods are defined on the constructor prototype property
    walk: function() {
        this.isWalking = true;
        return this;
    }
} );

Исходное объявление класса и приведенный выше фрагмент кода приведут к следующему:

console.log( typeof Vertebrate )                                    // function
console.log( typeof Vertebrate.prototype )                          // object

console.log( Object.getOwnPropertyNames( Vertebrate.prototype ) )   // [ 'constructor', 'walk' ]
console.log( Vertebrate.prototype.constructor === Vertebrate )      // true
console.log( Vertebrate.prototype.walk )                            // [Function: walk]

console.log( new Vertebrate( 'Bob' ) )                              // Vertebrate { name: 'Bob', hasVertebrae: true, isWalking: false }

Производные объявления/выражения класса

В дополнение к вышесказанному, объявления производного класса или выражения производного класса также устанавливают наследование между свойствами конструкторов prototype и используют синтаксис super так, что:

  1. Свойство prototype дочернего конструктора наследуется от свойства prototype родительского конструктора.
  2. Вызов super() равносилен вызову родительского конструктора с this, привязанным к текущему контексту.
    • Это только приблизительное приближение к функциональности, предоставляемой super(), которая также установит неявный параметр new.target и вызовет внутренний метод [[Construct]] (вместо метода [[Call]]). Вызов super() полностью обессилен в разделе 3.
  3. super[method]() вызывает сумму для вызова метода на родительском объекте prototype с this, привязанным к текущему контексту (мы пока не включаем статические методы).
    • Это только приблизительное значение вызовов super[method](), которые не зависят от прямой ссылки на родительский класс. super[method]() вызовы будут полностью реплицированы в разделе 3.

Используя синтаксис ES5, первоначальное объявление производного класса, таким образом, примерно эквивалентно следующему (без учета статических методов):

function Bird( name ) {
    Vertebrate.call( this,  name )                          // 2. The super() call is approximated by directly calling the parent constructor
    this.hasWings = true;
}

Bird.prototype = Object.create( Vertebrate.prototype, {     // 1. Inheritance is established between the constructors' prototype properties
    constructor: {
        value: Bird,
        writable: true,
        configurable: true
    }
} );

Object.assign( Bird.prototype, {                            
    walk: function() {
        console.log( "Advancing on 2 legs..." );
        return Vertebrate.prototype.walk.call( this );        // 3. The super[method]() call is approximated by directly calling the method on the parent prototype object
    }
})

Исходное объявление производного класса и приведенный выше фрагмент кода приведут к следующему:

console.log( Object.getPrototypeOf( Bird.prototype ) )      // Vertebrate {}
console.log( new Bird("Titi") )                             // Bird { name: 'Titi', hasVertebrae: true, isWalking: false, hasWings: true }
console.log( new Bird( "Titi" ).walk().isWalking )          // true

2. Синтаксический сахар для улучшения псевдоклассического паттерна наследования, доступный, но непрактичный или необычный в ES5

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

Объявления классов/выражения

Объявление класса или выражение класса дополнительно настроят следующим образом:

  1. Весь код внутри объявления класса или выражения класса выполняется в строгом режиме.
  2. Классические методы classs определены в самом конструкторе.
  3. Все методы класса (статические или нет) не перечисляются.
  4. Свойство прототипа конструкторов недоступно для записи.

Используя синтаксис ES5, первоначальное объявление класса, таким образом, более точно (но все еще только частично) эквивалентно следующему:

var Vertebrate = (function() {                              // 1. Code is wrapped in an IIFE that runs in strict mode
    'use strict';

    function Vertebrate( name ) {
        this.name = name;
        this.hasVertebrae = true;
        this.isWalking = false;
    }

    Object.defineProperty( Vertebrate.prototype, 'walk', {  // 3. Methods are defined to be non-enumerable
        value: function walk() {
            this.isWalking = true;
            return this;
        },
        writable: true,
        configurable: true
    } );

    Object.defineProperty( Vertebrate, 'isVertebrate', {    // 2. Static methods are defined on the constructor itself
        value: function isVertebrate( animal ) {            // 3. Methods are defined to be non-enumerable
            return animal.hasVertebrae;
        },
        writable: true,
        configurable: true
    } );

    Object.defineProperty( Vertebrate, "prototype", {       // 4. The constructor prototype property is defined to be non-writable:
        writable: false 
    });

    return Vertebrate
})();
  • NB 1. Если окружающий код уже работает в строгом режиме, конечно, нет необходимости переносить все в IIFE.

  • NB 2: Хотя в ES5 можно было без проблем определять статические свойства, это не было распространенным явлением. Причиной этого может быть то, что установление наследования статических свойств было невозможно без использования тогдашнего нестандартного свойства __proto__.

Теперь первоначальное объявление класса и приведенный выше фрагмент кода также приведут к следующему:

console.log( Object.getOwnPropertyDescriptor( Vertebrate.prototype, 'walk' ) )      
// { value: [Function: walk],
//   writable: true,
//   enumerable: false,
//   configurable: true }

console.log( Object.getOwnPropertyDescriptor( Vertebrate, 'isVertebrate' ) )    
// { value: [Function: isVertebrate],
//   writable: true,
//   enumerable: false,
//   configurable: true }

console.log( Object.getOwnPropertyDescriptor( Vertebrate, 'prototype' ) )
// { value: Vertebrate {},
//   writable: false,
//   enumerable: false,
//   configurable: false }

Производные объявления/выражения класса

В дополнение к вышесказанному, объявления производного класса или выражения производного класса также будут использовать синтаксис super, так что:

  1. Вызовы super[method]() внутри статических методов равносильны вызову метода в родительском конструкторе с this, привязанным к текущему контексту.
    • Это только приблизительное значение вызовов super[method](), которые не зависят от прямой ссылки на родительский класс. Вызовы super[method]() в статических методах не могут полностью имитироваться без использования синтаксиса class и перечислены в разделе 4.

Используя синтаксис ES5, первоначальное объявление производного класса, таким образом, более точно (но все еще только частично) эквивалентно следующему:

function Bird( name ) {
    Vertebrate.call( this,  name )
    this.hasWings = true;
}

Bird.prototype = Object.create( Vertebrate.prototype, {
    constructor: {
        value: Bird,
        writable: true,
        configurable: true
    }
} );

Object.defineProperty( Bird.prototype, 'walk', {
    value: function walk( animal ) {
        return Vertebrate.prototype.walk.call( this );
    },
    writable: true,
    configurable: true
} );

Object.defineProperty( Bird, 'isBird', {
    value: function isBird( animal ) {
        return Vertebrate.isVertebrate.call( this, animal ) && animal.hasWings;    // 1. The super[method]() call is approximated by directly calling the method on the parent constructor
    },
    writable: true,
    configurable: true
} );

Object.defineProperty( Bird, "prototype", {
    writable: false 
});

Теперь первоначальное объявление производного класса и приведенный выше фрагмент кода также приведут к следующему:

console.log( Bird.isBird( new Bird("Titi") ) )  // true

3. Синтаксический сахар для улучшения псевдоклассической модели наследования, недоступной в ES5

Классы ES6 дополнительно обеспечивают улучшения шаблона псевдоклассического наследования, которые недоступны в ES5, но могут быть реализованы в ES6 без использования синтаксиса классов.

Объявления классов/выражения

Характеристики ES6, найденные в других местах, также делятся на классы, в частности:

  1. Объявления классов ведут себя как объявления let - они не инициализируются при подъеме и оказываются во временной мертвой зоне до объявления. (связанный вопрос)
  2. Имя класса ведет себя как привязка const внутри объявления класса - его нельзя перезаписать в методе класса, попытка сделать это приведет к TypeError.
  3. Конструкторы классов должны вызываться с помощью внутреннего метода [[Construct]], а TypeError вызывается, если они вызываются как обычные функции с внутренним методом [[Call]].
  4. Методы класса (за исключением метода constructor()), статические или нет, ведут себя как методы, определенные через синтаксис краткого метода, то есть:
    • Они могут использовать ключевое слово super через super.prop или super[method] (это потому, что им присваивается внутреннее свойство [[HomeObject]]).
    • Их нельзя использовать в качестве конструкторов - у них нет свойства prototype и внутреннего свойства [[Construct]].

Используя синтаксис ES6, первоначальное объявление класса, таким образом, еще более точно (но все еще только частично) эквивалентно следующему:

let Vertebrate = (function() {                      // 1. The constructor is defined with a let declaration, it is thus not initialized when hoisted and ends up in the TDZ
    'use strict';

    const Vertebrate = function( name ) {           // 2. Inside the IIFE, the constructor is defined with a const declaration, thus preventing an overwrite of the class name
        if( typeof new.target === 'undefined' ) {   // 3. A TypeError is thrown if the constructor is invoked as an ordinary function without new.target being set
            throw new TypeError( 'Class constructor ${Vertebrate.name} cannot be invoked without 'new'' );
        }

        this.name = name;
        this.hasVertebrae = true;
        this.isWalking = false;
    }

    Object.assign( Vertebrate, {
        isVertebrate( animal ) {                    // 4. Methods are defined using the concise method syntax
            return animal.hasVertebrae;
        },
    } );
    Object.defineProperty( Vertebrate, 'isVertebrate', {enumerable: false} );

    Vertebrate.prototype = {
        constructor: Vertebrate,
        walk() {                                    // 4. Methods are defined using the concise method syntax
            this.isWalking = true;
            return this;
        },
    };
    Object.defineProperty( Vertebrate.prototype, 'constructor', {enumerable: false} );
    Object.defineProperty( Vertebrate.prototype, 'walk', {enumerable: false} );

    return Vertebrate;
})();
  • NB 1: Хотя экземпляры и статические методы определены в кратком синтаксисе метода, ссылки super не будут работать так, как ожидается в статических методах. Действительно, внутреннее свойство [[HomeObject]] не копируется Object.assign(). Правильная установка свойства [[HomeObject]] в статических методах потребует от нас определения конструктора функции с использованием литерала объекта, что невозможно.

  • NB 2: чтобы предотвратить вызов конструкторов без ключевого слова new, аналогичные меры предосторожности могут быть уже реализованы в ES5 с использованием оператора instanceof. Хотя они не охватывали все случаи (см. этот ответ).

Теперь первоначальное объявление класса и приведенный выше фрагмент кода также приведут к следующему:

Vertebrate( "Bob" );                                                    // TypeError: Class constructor Vertebrate cannot be invoked without 'new'
console.log( Vertebrate.prototype.walk.hasOwnProperty( 'prototype' ) )  // false
new Vertebrate.prototype.walk()                                         // TypeError: Vertebrate.prototype.walk is not a constructor
console.log( Vertebrate.isVertebrate.hasOwnProperty( 'prototype' ) )    // false
new Vertebrate.isVertebrate()                                           // TypeError: Vertebrate.isVertebrate is not a constructor

Производные объявления/выражения класса

В дополнение к вышесказанному, для объявления производного класса или выражения производного класса также будет выполняться следующее:

  1. Дочерний конструктор наследуется от родительского конструктора (то есть производные классы наследуют статические члены).
  2. Вызов super() в конструкторе производного класса равносилен вызову внутреннего метода [[Construct]] родительского конструктора с текущим значением new.target и привязке контекста this к возвращенному объекту.

Используя синтаксис ES6, первоначальное объявление производного класса, таким образом, более точно (но все еще только частично) эквивалентно следующему:

let Bird = (function() {
    'use strict';

    const Bird = function( name ) {
        if( typeof new.target === 'undefined' ) {
            throw new TypeError( 'Class constructor ${Bird.name} cannot be invoked without 'new'' );
        }

        const that = Reflect.construct( Vertebrate, [name], new.target );   // 2. super() calls amount to calling the parent constructor [[Construct]] method with the current new.target value and binding the 'this' context to the returned value (see NB 2 below)
        that.hasWings = true;
        return that;
    }

    Bird.prototype = {
        constructor: Bird,
        walk() {   
            console.log( "Advancing on 2 legs..." );
            return super.walk();                                            // super[method]() calls can now be made using the concise method syntax (see 4. in Class Declarations / Expressions above)
        },
    };
    Object.defineProperty( Bird.prototype, 'constructor', {enumerable: false} );
    Object.defineProperty( Bird.prototype, 'walk', {enumerable: false} );

    Object.assign( Bird, {
        isBird: function( animal ) {
            return Vertebrate.isVertebrate( animal ) && animal.hasWings;    // super[method]() calls can still not be made in static methods (see NB 1 in Class Declarations / Expressions above)
        }
    })
    Object.defineProperty( Bird, 'isBird', {enumerable: false} );

    Object.setPrototypeOf( Bird, Vertebrate );                              // 1. Inheritance is established between the constructors directly
    Object.setPrototypeOf( Bird.prototype, Vertebrate.prototype );

    return Bird;
})();   
  • NB 1: Так как Object.create() можно использовать только для установки прототипа нового нефункционального объекта, настройка наследования между самими конструкторами может быть реализована только в ES5 путем манипуляции с нестандартным тогда __proto__ недвижимость.

  • NB 2: невозможно имитировать эффект super() с использованием контекста this, поэтому нам пришлось явно возвращать другой объект that из конструктора.

Теперь первоначальное объявление производного класса и приведенный выше фрагмент кода также приведут к следующему:

console.log( Object.getPrototypeOf( Bird ) )        // [Function: Vertebrate]
console.log( Bird.isVertebrate )                    // [Function: isVertebrate]

4. Функции, которые невозможно реализовать без синтаксиса class

Классы ES6 дополнительно предоставляют следующие функции, которые невозможно реализовать вообще без фактического использования синтаксиса class:

  1. Внутреннее свойство [[HomeObject]] статических методов класса указывает на конструктор класса.
    • Нет возможности реализовать это для обычных функций конструктора, так как для этого потребуется определить функцию через литерал объекта (см. также раздел 3 выше). Это особенно проблематично для статических методов производных классов, использующих ключевое слово super, например, наш метод Bird.isBird().

Возможно частично обойти эту проблему, если родительский класс известен заранее.


Заключение

Некоторые особенности классов ES6 являются просто синтаксическим сахаром для стандартного образца псевдоклассического наследования ES5. Однако классы ES6 также имеют функции, которые могут быть реализованы только в ES6, и некоторые дополнительные функции, которые невозможно воспроизвести в ES6 (т.е. без использования синтаксиса классов).

Глядя на вышесказанное, я думаю, что было бы справедливо сказать, что классы ES6 являются более краткими, более удобными и более безопасными в использовании, чем шаблон псевдоклассического наследования ES5. В результате они также становятся менее гибкими (см., Например, этот вопрос).


Примечания стороны

Стоит отметить еще несколько особенностей классов, которые не нашли места в приведенной выше классификации:

  1. super() является допустимым синтаксисом в конструкторах производных классов и может вызываться только один раз.
  2. Попытка доступа к this в конструкторе производного класса до вызова super() приводит к ReferenceError.
  3. super() должен вызываться в конструкторе производного класса, если из него явно не возвращен объект.
  4. eval и arguments не являются допустимыми идентификаторами классов (хотя они являются действительными идентификаторами функций в нестрогом режиме).
  5. Производные классы устанавливают метод constructor() по умолчанию, если он не предоставлен (соответствует constructor( ...args ) { super( ...args ); }).
  6. Невозможно определить свойства данных в классе с помощью объявления класса или выражения класса (хотя вы можете добавить свойства данных в класс вручную после его объявления).

Дополнительные ресурсы

  • Николас Закас, глава Общие сведения о классах ES6 в книге "Основные сведения о ES6", - лучшее из написанных мной классов ES6.
  • В блоге 2ality Акселя Раушмайера есть очень тщательная запись о классах ES6.
  • На игровой площадке объекта есть отличное видео, объясняющее образец псевдоклассического наследования (и сравнение его с синтаксисом класса).
  • Вавилон - это отличное место для самостоятельного изучения вещей.

Ответ 5

Короткий ответ: да.:) Но там гораздо больше - проверка типа "переполнения времени" - это нечто совершенно иное, чем то, что вы будете использовать для использования ванили ECMA/JavaScript.

Изменить: Извините, я полностью не понял вопрос OP, почему-то я интерпретировал этот вопрос как о TypeScript.:/Возможно, я должен удалить этот ответ?

Ответ 6

Это полностью синтаксический сахар. Что нового в прототипическом наследовании в ES6 является переопределение свойства __proto__ объектов. __proto__ теперь легален и что как подкласс класса стал возможен с помощью JS.

Ответ 7

Да, почти.

С помощью es6 вы можете расширить класс Function и класс Array, в es5 вы не можете иметь такое же поведение: расширение функции не делает вызываемый объект и расширение массива не наследует свойство auto.length в es5

Для остальных логика и классы прототипов в JavaScript одинаковы

Являются ли классы es6 действительно семантическим сахаром?