Spec-Zone.ru › JavaScript

extends

Базовый уровень Широко доступно

Эта функция хорошо зарекомендовала себя и работает на многих устройствах и версиях браузеров. Она доступна во всех браузерах с марта 2016 года.

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

Попробуйте

class DateFormatter extends Date {
  getFormattedDate() {
    const months = [
      "Jan",
      "Feb",
      "Mar",
      "Apr",
      "May",
      "Jun",
      "Jul",
      "Aug",
      "Sep",
      "Oct",
      "Nov",
      "Dec",
    ];
    return `${this.getDate()}-${months[this.getMonth()]}-${this.getFullYear()}`;
  }
}

console.log(new DateFormatter("August 19, 1975 23:15:30").getFormattedDate());
// Expected output: "19-Aug-1975"

Синтаксис

class ChildClass extends ParentClass { /* … */ }
ParentClass
Выражение, которое вычисляется в конструкторскую функцию (включая класс) или null.

Описание

Ключевое слово extends может использоваться для создания подклассов как пользовательских классов, так и встроенных объектов.

Любой конструктор, который можно вызвать с new и имеет свойство prototype, может быть кандидатом на роль родительского класса. Оба условия должны выполняться — например, связанные функции и Proxy могут быть сконструированы, но у них нет свойства prototype, поэтому их нельзя сделать подклассами.

function OldStyleClass() {
  this.someProperty = 1;
}
OldStyleClass.prototype.someMethod = function () {};

class ChildClass extends OldStyleClass {}

class ModernClass {
  someProperty = 1;
  someMethod() {}
}

class AnotherChildClass extends ModernClass {}

Свойство prototype объекта ParentClass должно быть Object или null, но на практике вы редко будете беспокоиться об этом, поскольку неконструируемый prototype в любом случае не ведет себя должным образом. (Он игнорируется оператором new.)

function ParentClass() {}
ParentClass.prototype = 3;

class ChildClass extends ParentClass {}
// Uncaught TypeError: Class extends value does not have valid prototype property 3

console.log(Object.getPrototypeOf(new ParentClass()));
// [Object: null prototype] {}
// Not actually a number!

extends устанавливает прототип как для ChildClass, так и для ChildClass.prototype.

Прототип ChildClass Прототип ChildClass.prototype
Пункт extends отсутствует Function.prototype Object.prototype
extends null Function.prototype null
extends ParentClass ParentClass ParentClass.prototype
class ParentClass {}
class ChildClass extends ParentClass {}

// Allows inheritance of static properties
Object.getPrototypeOf(ChildClass) === ParentClass;
// Allows inheritance of instance properties
Object.getPrototypeOf(ChildClass.prototype) === ParentClass.prototype;

Правая часть extends не обязательно должна быть идентификатором. Вы можете использовать любое выражение, которое вычисляется в конструктор. Это часто полезно для создания миксинов. Значение this в выражении extends — это this, окружающий определение класса, а обращение к имени класса является ReferenceError, потому что класс еще не инициализирован. await и yield работают как ожидается в этом выражении.

class SomeClass extends class {
  constructor() {
    console.log("Base class");
  }
} {
  constructor() {
    super();
    console.log("Derived class");
  }
}

new SomeClass();
// Base class
// Derived class

Хотя базовый класс может возвращать что угодно из своего конструктора, производный класс должен возвращать объект или undefined, иначе будет выброшена ошибка TypeError.

class ParentClass {
  constructor() {
    return 1;
  }
}

console.log(new ParentClass()); // ParentClass {}
// The return value is ignored because it's not an object
// This is consistent with function constructors

class ChildClass extends ParentClass {
  constructor() {
    super();
    return 1;
  }
}

console.log(new ChildClass()); // TypeError: Derived constructors may only return object or undefined

Если конструктор родительского класса возвращает объект, этот объект будет использоваться в качестве значения this для производного класса при дальнейшей инициализации полей класса. Этот трюк называется "переопределение возвращаемого значения", которое позволяет полям производного класса (включая приватные) определяться на несвязанных объектах.

Создание подклассов встроенных объектов

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

Вот некоторые вещи, которые можно ожидать при расширении класса:

  • При вызове статического фабричного метода (например, Promise.resolve() или Array.from()) из подкласса, возвращаемый экземпляр всегда является экземпляром подкласса.
  • При вызове метода экземпляра, который возвращает новый экземпляр (например, Promise.prototype.then() или Array.prototype.map()) из подкласса, возвращаемый экземпляр всегда является экземпляром подкласса.
  • Методы экземпляра пытаются делегировать минимальному набору примитивных методов, где это возможно. Например, для подкласса Promise, переопределение then() автоматически вызывает изменение поведения catch(); или для подкласса Map, переопределение set() автоматически вызывает изменение поведения конструктора Map().

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

  • Первое требует, чтобы статический метод читал значение this, чтобы получить конструктор для создания возвращаемого экземпляра. Это означает, что [p1, p2, p3].map(Promise.resolve) выбрасывает ошибку, потому что this внутри Promise.resolve является undefined. Способ исправить это — вернуться к базовому классу, если this не является конструктором, как это делает Array.from(), но это все равно означает, что базовый класс имеет специальную обработку.
  • Второе требует, чтобы метод экземпляра читал this.constructor, чтобы получить конструкторскую функцию. Однако new this.constructor() может нарушить устаревший код, потому что свойство constructor является записываемым и конфигурируемым и никак не защищено. Поэтому многие копирующие встроенные методы вместо этого используют свойство [Symbol.species] конструктора (которое по умолчанию просто возвращает this, сам конструктор). Однако [Symbol.species] позволяет выполнять произвольный код и создавать экземпляры произвольного типа, что представляет собой угрозу безопасности и сильно усложняет семантику создания подклассов.
  • Третье приводит к явным вызовам пользовательского кода, что затрудняет реализацию многих оптимизаций. Например, если конструктор Map() вызывается с итерируемым элементом из x элементов, то он должен явно вызвать метод set() x раз вместо простого копирования элементов во внутреннее хранилище.

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

Расширение null

extends null был разработан для упрощения создания объектов, не наследующих от Object.prototype. Однако из-за нерешенных вопросов о том, следует ли вызывать super() внутри конструктора, невозможно сконструировать такой класс на практике с использованием любой реализации конструктора, которая не возвращает объект. Комитет TC39 работает над повторным включением этой функции.

new (class extends null {})();
// TypeError: Super constructor null of anonymous class is not a constructor

new (class extends null {
  constructor() {}
})();
// ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

new (class extends null {
  constructor() {
    super();
  }
})();
// TypeError: Super constructor null of anonymous class is not a constructor

Вместо этого вам нужно явно возвращать экземпляр из конструктора.

class NullClass extends null {
  constructor() {
    // Using new.target allows derived classes to
    // have the correct prototype chain
    return Object.create(new.target.prototype);
  }
}

const proto = Object.getPrototypeOf;
console.log(proto(proto(new NullClass()))); // null

Примеры

Использование extends

Первый пример создает класс под названием Square из класса под названием Polygon. Этот пример взят из этого живого демо (исходный код).

class Square extends Polygon {
  constructor(length) {
    // Here, it calls the parent class' constructor with lengths
    // provided for the Polygon's width and height
    super(length, length);
    // Note: In derived classes, super() must be called before you
    // can use 'this'. Leaving this out will cause a reference error.
    this.name = "Square";
  }

  get area() {
    return this.height * this.width;
  }
}

Расширение обычных объектов

Классы не могут расширять обычные (неконструируемые) объекты. Если вы хотите наследовать от обычного объекта, сделав все его свойства доступными для унаследованных экземпляров, вы можете вместо этого использовать Object.setPrototypeOf():

const Animal = {
  speak() {
    console.log(`${this.name} makes a noise.`);
  },
};

class Dog {
  constructor(name) {
    this.name = name;
  }
}

Object.setPrototypeOf(Dog.prototype, Animal);

const d = new Dog("Mitzie");
d.speak(); // Mitzie makes a noise.

Расширение встроенных объектов

Этот пример расширяет встроенный объект Date. Этот пример взят из этого живого демо (исходный код).

class MyDate extends Date {
  getFormattedDate() {
    const months = [
      "Jan", "Feb", "Mar", "Apr", "May", "Jun",
      "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
    ];
    return `${this.getDate()}-${months[this.getMonth()]}-${this.getFullYear()}`;
  }
}

Расширение Object

Все объекты JavaScript по умолчанию наследуют от Object.prototype, поэтому запись extends Object на первый взгляд кажется избыточной. Единственное отличие от полного отсутствия extends заключается в том, что сам конструктор наследует статические методы от Object, такие как Object.keys(). Однако, поскольку ни один статический метод Object не использует значение this, нет смысла наследовать эти статические методы.

Конструктор Object() обрабатывает сценарий создания подкласса особым образом. Если он вызывается неявно через super(), он всегда инициализирует новый объект с new.target.prototype в качестве своего прототипа. Любое значение, переданное super(), игнорируется.

class C extends Object {
  constructor(v) {
    super(v);
  }
}

console.log(new C(1) instanceof Number); // false
console.log(C.keys({ a: 1, b: 2 })); // [ 'a', 'b' ]

Сравните это поведение с пользовательской оберткой, которая не обрабатывает создание подкласса особым образом:

function MyObject(v) {
  return new Object(v);
}
class D extends MyObject {
  constructor(v) {
    super(v);
  }
}
console.log(new D(1) instanceof Number); // true

Species

Вы можете захотеть возвращать объекты Array в вашем производном классе массива MyArray. Шаблон "species" позволяет переопределить конструкторы по умолчанию.

Например, при использовании методов, таких как Array.prototype.map(), которые возвращают конструктор по умолчанию, вы хотите, чтобы эти методы возвращали родительский объект Array, а не объект MyArray. Символ Symbol.species позволяет сделать это:

class MyArray extends Array {
  // Overwrite species to the parent Array constructor
  static get [Symbol.species]() {
    return Array;
  }
}

const a = new MyArray(1, 2, 3);
const mapped = a.map((x) => x * x);

console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array); // true

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

Миксины

Абстрактные подклассы или миксины — это шаблоны для классов. Класс может иметь только один суперкласс, поэтому множественное наследование от инструментальных классов, например, невозможно. Функциональность должна предоставляться суперклассом.

Функция, принимающая суперкласс в качестве входных данных и возвращающая подкласс, расширяющий этот суперкласс, может использоваться для реализации миксинов:

const calculatorMixin = (Base) =>
  class extends Base {
    calc() {}
  };

const randomizerMixin = (Base) =>
  class extends Base {
    randomize() {}
  };

Класс, использующий эти миксины, может быть написан так:

class Foo {}
class Bar extends calculatorMixin(randomizerMixin(Foo)) {}

Избегание наследования

Наследование — это очень сильная связь в объектно-ориентированном программировании. Это означает, что все поведение базового класса по умолчанию наследуется подклассом, что не всегда желательно. Например, рассмотрим реализацию ReadOnlyMap:

class ReadOnlyMap extends Map {
  set() {
    throw new TypeError("A read-only map must be set at construction time.");
  }
}

Оказывается, ReadOnlyMap не является конструируемым, потому что конструктор Map() вызывает метод экземпляра set().

const m = new ReadOnlyMap([["a", 1]]); // TypeError: A read-only map must be set at construction time.

Мы можем обойти это, используя приватный флаг, чтобы указать, конструируется ли экземпляр. Однако более серьезная проблема с этим дизайном заключается в том, что он нарушает принцип подстановки Барбары Лисков, который гласит, что подкласс должен быть заменяемым своим суперклассом. Если функция ожидает объект Map, она также должна иметь возможность использовать объект ReadOnlyMap, что здесь приведет к сбою.

Наследование часто приводит к проблеме "круг-эллипс", поскольку ни один тип не включает идеально поведение другого, хотя они имеют много общих черт. В общем, если нет очень веской причины использовать наследование, лучше использовать композицию. Композиция означает, что класс имеет ссылку на объект другого класса и использует этот объект только как деталь реализации.

class ReadOnlyMap {
  #data;
  constructor(values) {
    this.#data = new Map(values);
  }
  get(key) {
    return this.#data.get(key);
  }
  has(key) {
    return this.#data.has(key);
  }
  get size() {
    return this.#data.size;
  }
  *keys() {
    yield* this.#data.keys();
  }
  *values() {
    yield* this.#data.values();
  }
  *entries() {
    yield* this.#data.entries();
  }
  *[Symbol.iterator]() {
    yield* this.#data[Symbol.iterator]();
  }
}

В этом случае класс ReadOnlyMap не является подклассом Map, но он по-прежнему реализует большинство тех же методов. Это означает большее дублирование кода, но также означает, что класс ReadOnlyMap не сильно связан с классом Map и легко не сломается, если класс Map изменится, избегая семантических проблем встроенного наследования. Например, если класс Map добавляет новый вспомогательный метод (например, getOrInsert()), который не вызывает set(), это приведет к тому, что класс ReadOnlyMap больше не будет доступен только для чтения, если последний не будет обновлен соответствующим образом для переопределения getOrInsert(). Более того, объекты ReadOnlyMap вообще не имеют метода set, что более точно, чем выбрасывание ошибки во время выполнения.

Спецификации

Спецификация
ECMAScript® 2027 Language Specification
# sec-class-definitions

Совместимость с браузерами

Настольные компьютеры Мобильные Сервер
Chrome Edge Firefox Opera Safari Chrome Android Firefox for Android Opera Android Safari on iOS Samsung Internet WebView Android WebView on iOS Bun Deno Node.js
extends
49
42–49Требуется строгий режим.
13
45
36
29–36Требуется строгий режим.
9
49
42–49Требуется строгий режим.
45
36
29–36Требуется строгий режим.
9
5.0
4.0–5.0Требуется строгий режим.
49
42–49Требуется строгий режим.
9
1.0.0
1.0
6.0.0

См. также

  • Руководство по использованию классов
  • Классы
  • constructor
  • class
  • super

© 2005–2025 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/extends

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API