Подобни на класове структури в ECMAScript 5

В ECMAScript 5 и по рано, не е имало класове, най-близкият еквивалент е създаването на конструктор и след това възлагане на методи на неговия прототип, този подход обикновено се нарича създаване на потребителски тип. Например:

function PersonType(name) {
    this.name = name; 
}
 PersonType.prototype.sayName = function() { 
    console.log(this.name); 
};

let person = new PersonType("Nicholas");
    person.sayName(); // outputs "Nicholas"

console.log(person instanceof PersonType); // true
console.log(person instanceof Object);    // true

В този код, PersonType е функция конструктор, която създава едно свойство наречено name. Метода sayName() се възлага на прототипа, така същата функция се споделя от всички случаи на прототипа на PersonType обекта. След това, се създава нова инстанция на PersonType с помощта на оператора new. Полученият person обект се счита за инстанция на PersonType и на Object чрез прототипното наследяване.

Същият основен модел е в основата на много от имитиращите класове библиотеки в JavaScript и това е до началото на класовете в ECMAScript 6.

Декларации на класове

Декларациите на класове започват с ключовата дума class, последвана от името на класа. Останалата част от синтаксиса изглежда подобно на кратките методи за обект без да се изисква запетая между тях. Например, това е една проста декларация на клас:

class PersonClass {

// еквивалентно на конструктора PersonType
     constructor(name) {
        this.name = name;
	}

    // еквивалентно на PersonType.prototype.sayName
     sayName() { 
	    console.log(this.name);
	} 
}

let person = new PersonClass("Nicholas"); 
person.sayName();     // outputs "Nicholas" 
	
console.log(person instanceof PersonClass);    // true 
console.log(person instanceof Object);         // true 

console.log(typeof PersonClass);                     // "function" 
console.log(typeof PersonClass.prototype.sayName);   // "function"
				

Декларацията на класа PersonClass се държи доста подобно на PersonType от предишния пример. Вместо да дефинира функция, като конструктор, декларацията на класа ви позволява да определите конструктора директно в класа, използвайки метод наречен constructor. Тъй като, методите на класа използват краткия синтаксис, няма нужда да се използва ключовата дума function. Всички други имена на методи са без особено значение, така че може да добавите толкова методи, колкото искате.

info
Собствените свойства, са свойства, които се случват само в инстанцията, а не в прототипа и могат да се създават само вътре в клас конструктор или метод. В предишния пример, name е собствено свойство. Препоръчително е да се създадат всички възможни собствени свойства в рамките на функцията конструктор, така че да има едно единствено място, което е отговорно за всички тях.

Интересен аспект на декларациите на класове е, че те са само syntactic sugar на върха на съществуващите потребителски типoве декларации. Декларацията на PersonClass всъщност създава функция, която има поведението на constructor метода, поради което typeof PersonClass е "function". По същия начин, метода sayName() завършва, като метод на PersonClass.prototype. Този пример е подобен на връзката между sayName() и PersonType.prototype в предишния пример. Тези сходства ви позволяват да смесвате потребителски типове и класове без да се притеснявате твърде много за какво ги използвате.

Защо да използваме клас синтаксис

Въпреки приликите има няколко важни разлики между класове и потребителски типове, които трябва да имате в предвид:

  1. Клас декларациите, за разлика от декларациите на функции, не се издигат. Декларациите на класове действат, като let декларации и така съществуват в темпоралната мъртва зона, докато изпълнението не достигне декларацията.
  2. Целия код вътре в клас декларацията, минава в strict mode автоматично. Няма начин да се откажете от strict mode вътре в класовете.
  3. Всички методи са non-enumerable. Това е значителна промяна от потребителските типове, където трябва да използвате Object.defineProperty() за да направите методите non-enumerable.
  4. Всички методи нямат вътрешен метод [[Construct]] и така хвърлят грешка ако опитате да ги извикате с new.
  5. Извикването на клас конструктора без new хвърля грешка
  6. Опита да замените името на класа в рамките на клас метода хвърля грешка.

С всичко това в предвид, декларацията на PersonClass от предишния пример е директно еквивалентна на следния код, който не използва синтаксиса за клас:

// директен еквивалент на PersonClass
let PersonType2 = (function() {
  "use strict";

  const PersonType2 = function(name) {

    // уверете се, че функцията се извиква с new
    if (typeof new.target === "undefined") {
        throw new Error("Constructor must be called with new."); 
  }

  this.name = name; 
}

Object.defineProperty(PersonType2.prototype, "sayName", {
  value: function(){

      // уверете се, че метода не се извиква с new
      if (typeof new.target !== "undefined") {
        throw new Error("Method cannot be called with new.");  
      } 
       console.log(this.name); 	
    }, 
    enumerable: false,
    writable: true,
    configurable: true 
  });

    return PersonType2; 
}());
				

Първото нещо, което се забелязва в този код е, че има две декларации на PersonType2: let декларацията във външния обхват и const декларацията вътре в IIFE. Това е начина, по който методите на класа са защитени от презаписване на името на класа, докато на кода от вън е позволено да го направи. Функцията конструктор се проверява от new.target за да се гарантира, че тя се извиква с new, в противен случай се хвърля грешка. На следващо място sayName() метода се определя на non-enumerable и също така се проверява от new.target за да се гарантира, че не се извиква с new. Последната стъпка е да се върне функцията конструктор.

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

Постоянни имена на класове

Името на класа се указва с използването на const, но само в рамките на самия клас. Това означава, че можете да замените името на класа извън този клас, но не и вътре в метода на класа. Например:

class Foo {
  constructor() { 
    Foo = "bar";  // хвърля грешка ако бъде изпълнено				
  } 
}

// но това е добре
 Foo = "baz";
			

В този код, Foo вътре в конструктора е с отделно обвързване от Foo извън класа. Вътрешния Foo се определя, все едно, че е const декларация и така не може да бъде презаписан. Ще бъде хвърлена грешка, ако конструктора се опита да замени Foo с някаква стойност. Външния Foo се определя, все едно, че е let декларация и така неговата стойност може да бъде презаписана по всяко време.

Клас изрази

Класове и функции са сходни по това, че те имат две форми: декларации и изрази. Декларацията на функции и класове започва с подходяща дума ( function или class съответно) последвана от идентификатор. Функциите имат израз форма, която не изисква идентификатор след function и по същия начин, класовете имат израз форма, която не изисква идентификатор след class. Тези class expressions са предназначени да бъдат използвани в декларации на променливи или подаване към функции, като аргументи.

A Basic Class Expression

Ето един клас израз еквивалент на предишните PersonClass примери, последван от някакъв код, който го използва:

let PersonClass = class {

    // еквивалентно на PersonType конструктора
    constructor(name) {
       this.name = name;
    }

    // еквивалентно на PersonType.prototype.sayName 
    sayName() { 
       console.log(this.name);
    } 
};

let person = new PersonClass("Nicholas");
person.sayName();     // outputs "Nicholas" 
 
console.log(person instanceof PersonClass);    // true 
console.log(person instanceof Object);         // true 
 
console.log(typeof PersonClass);         // "function" 
console.log(typeof PersonClass.prototype.sayName);//"function"
				

Този пример показва, че клас изразите не изискват идентификатор след class. Освен синтаксиса, клас изразите са абсолютно равностойни на декларациите на класове

В анонимни клас изрази, както в предишния пример, PersonClass.name е празен string. При използване на декларация на клас, PersonClass.name ще бъде "PersonClass".

info
Използването на декларации за класове или клас изрази е най-вече въпрос на стил. За разлика от декларациите за функции и функционални изрази, декларациите на клас и клас изрази не се издигат, така избора няма голямо влияние върху поведението по време на изпълнение на кода. Единствената съществена разлика е, че анонимни клас изрази имат name свойство, което е празен string, докато декларации на класа винаги имат name свойство равно на името на класа (например, PersonClass.name е "PersonClass", когато се използва при декларация на клас).

Именуване на клас израз

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

let PersonClass = class PersonClass2 {

   // еквивалентно на PersonType конструктора 
   constructor(name) {
	this.name = name; 
   }

   // еквивалентно на PersonType.prototype.sayName 
   sayName() { 
	   console.log(this.name); 
   } 
}; 

console.log(typeof PersonClass); // "function" 
console.log(typeof PersonClass2); // "undefined"
				

В този пример, на клас израза се дава име: PersonClass2. Идентификатора PersonClass2 съществува само в рамката на дефиницията на класа, така че може да се използва вътре от методите на класа (като sayName()). Извън този клас, typeof PersonClass2 е "undefined", защто PersonClass2 не съществува там. За да разберете, защо се случва това, погледнете еквивалентната декларация, която не използва класове:

// директен еквивалент на PersonClass име на клас израз
let PersonClass = (function() { 
   "use strict";

   const PersonClass2 = function(name) {

    // уверете се, че функцията се извиква с new
    if (typeof new.target === "undefined") {
       throw new Error("Constructor must be called with new.");
    }

   this.name = name;
  }

  Object.defineProperty(PersonClass2.prototype, "sayName", { 
     value: function() { 
     // уверете се, че функцията се извиква с new
     if (typeof new.target !=== "undefined") {
	throw new Error("Method must be called with new.");
     }
     console.log(this.name); 
   }, 
   enumerable: false, 
   writable: true,
   configurable: true 
  });

  return PersonClass2; 
}());
				

Създаване на клас израз с име леко променя това, което се случва в JavaScript машината. За декларация на клас, външното обвързване (дефинирано с let) има същото име, като вътрешното обвързване (дефинирано с const). Клас изразите използват името си, като const дефиниция, така че в този случай PersonClass2 е определен за употреба само в рамките на класа.

Докато поведението на клас изрази е различно от това на функционалнте изрази, все още има много прилики между двете. И двете могат да се използват, като стойности, което отваря много възможности.

Класовете, като First-Class Citizens

В програмирането, се казва за нещо, че е first-class citizen, когато то може да се използва, като стойност, което означава, че може да бъде прехвърлено във функция, върнато от функция и присвоено от променлива. В JavaScript функциите са first-class citizen (понякога наричани само първокласни функции) и това е само част от това, което прави JavaScript уникален език.

ECMAScript 6 продължава тази традиция, като прави класовете first-class citizen. Това позволява класовете да бъдат използвани по много различни начини. Например, те могат да бъдат вкарани във функции, като аргументи:

function createObject(classDef) {
    return new classDef(); 
}

let obj = createObject(class { 
   sayHi() { 
     console.log("Hi!"); 
   } 
});
obj.sayHi();     // "Hi!"
				

В този пример, функцията createObject() се извиква с анонимен клас израз, като аргумент, създава инстанция на този клас с new и връща инстанция. Променливата obj след това съхранява върнатата инстанция.

Друго интересно приложение на клас изрази е да създават singletons, като незабавно се позовават на клас конструктора. За да направите това, трябва да използвате new с израза на класа и да включите скоби на края. Например:

let person = new class PersonClass {
   constructor(name) {
      this.name = name; 
   } 
   sayName() { 
      console.log(this.name); 
   } 

}("Nicholas");

person.sayName(); // "Nicholas"
				

Тука, анонимния клас израз се създава и след това се изпълнява незабавно. Този модел позволява използването на синтаксиса на класа за създаване на singletons без да излизате от препратката на класа за проверка (не забравяйте, че PersonClass създава обвързване вътре в класа, не отвън). Скобите в края са индикатор за извикването на функцията и също така позволяват подаване на аргумент.

Примерите в тази глава са фокусирани, доколкото може, върху класове с методи. Но можете да създадете accessor свойства на класове със синтаксис подобен за обект.

Accessor свойства

Докато собствените свойства трябва да бъдат създадени в клас конструктора, класовете ви позволяват за определите accessor свойства на прототипа. За създаване на getter, трябва да изпозвате ключовата дума get последвана от интервал, последван от идентификатор. За създаване на setter, трябва да използвате ключовата дума set. Например:

class CustomHTMLElement { 
    constructor(element) {
        this.element = element; 
    } 

    get html() {
        return this.element.innerHTML; 
    } 
	
    set html(value) {
        this.element.innerHTML = value; 
    }
}

var descriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype, "html"); 

console.log("get" in descriptor); // true
console.log("set" in descriptor); // true 
console.log(descriptor.enumerable); // false
				

В този пример, класа CustomHTMLElement прави обвивка около съществуващ DOM елемент. Той има getter и setter за html, като делегати на innerHTML метода на самия елемент. Това accessor свойство се създава, като non-enumerable, точно както всеки друг метод ще бъде създаден в CustomHTMLElement.prototype. Еквивалент на предишния пример с non-class ще бъде представен, като:

// директен еквивалент на предишния пример
let CustomHTMLElement = (function() {
    "use strict";

    const CustomHTMLElement = function(element) {

      // уверете се, че функцията се извиква с new
      if (typeof new.target === "undefined") {
        throw new Error("Constructor must be called with new.");
      }

      this.element = element; 
  }

  Object.defineProperty(CustomHTMLElement.prototype, "html", { 
    enumerable: false,
    configurable: true, 
    get: function() {
      return this.element.innerHTML; 
    },
    set: function(value) {
      this.element.innerHTML = value; 
    } 
  });

  return CustomHTMLElement; 
}());
				

Както при предишните примери, това показва колко много код се спести чрез използване на класове вместо еквиваленти на non-class. Дефинирането само на html свойството за достъп е почти с размера на еквивалента на декларацията на класа.

Изчисляване на имена

Клас методите и accessor свойствата могат да имат изчислени имена. Вместо използването на идентификатор, можем да използваме квадратни скоби около израза, което е същия синтаксис, както за изчислените имена на обект. Например:

let methodName = "sayName";

class PersonClass {  
    constructor(name) {
      this.name = name; 
    } 
    [methodName]() { 
      console.log(this.name); 
    } 
};

let me = new PersonClass("Nicholas"); 
me.sayName();    // "Nicholas"
				

Тази версия на PersonClass използва променлива да зададе името на метода вътре в своята дефиниция. String "sayName" се възлага на променливата methodName и след това methodName се използва за деклариране на метода. Метода sayName() по късно се достъпва директно.

Accessor свойствата могат да използват изчислени имена по същия начин, като този:

let propertyName = "html";

class CustomHTMLElement {
    constructor(element) {
      this.element = element; 
    } 

    get [propertyName]() {
      return this.element.innerHTML; 
    } 
	
    set [propertyName](value) {
      this.element.innerHTML = value; 
    }
}
				

Тука getter и setter за html са определени с помощта на променливата propertyName. Mожете да получите достъп до свойството с помощта на .html, което засяга само определението.

Видяхте, че има много прилики между класове и обекти за методи, accessor свойства и изчисляване на имена. Има още една прилика за покриване: генератори.

Генератор методи

Когато въведохме генератори в Глава 8, вие научихте как да дефинирате генератор на даден обект, като поставите символа звезда (*) до името на метода. Същия синтаксис работи и за класове, както и че позволява всеки един метод да бъде генератор. Ето един пример:

class MyClass { 
    *createIterator() {
        yield 1;
        yield 2;
        yield 3; 
    }
}

let instance = new MyClass();
let iterator = instance.createIterator();
				

Този код създава клас наречен MyClass с метод - генератор наречен createIterator(). Метода връща итератор, чиито стойности са кодирани в генератора. Генератор методите са полезни, когато имате обект, който представлява сбор от стойности и искате да обхождате тези стойности лесно. arrays, sets и maps имат множество генератор методи за отчитане на различните начини, по които програмистите взаимодействат с техните позиции.

Докато методите на генератора са полезни, много по-полезно е да се определят default итератори за вашия клас, ако класа представлява колекция от стойности. Можете да определите default итератор за един клас с помощта на Symbol.iterator за определяне на метод - генератор, като например:

class Collection { 
    constructor() {
        this.items = []; 
    } 
    *[Symbol.iterator]() {
        yield *this.items.values(); 
    } 
}

var collection = new Collection(); 
collection.items.push(1); 
collection.items.push(2);
collection.items.push(3);

for(let x of collection) { 
    console.log(x); 
} 
// Output: 
// 1 
// 2 
// 3
				

Този пример използва изчислено име за метода на генератора, който делегира итератора values() към this.items. Всеки клас, който управлява колекция от стойности трябва да включва default итератор, защото за да работят някои специфични операции за колекции изисква да имат итератор. По този начин всички случаи на Collection може да се използват директно в for-of цикъл или с оператора spread.

Добавянето на методи и accessor свойства на прототипа на класа е полезно, когато искате те да се появяват в инстанциите на обекта. Ако от друга страна искате методи и accessor свойства на самия клас, тогава ще трябва да използвате статични членове.

Статични членове

Друг общ модел в ECMAScript 5 и по-рано е добавяне на допълнителни методи директно в конструктора за симулиране на статични членове. Например:

function PersonType(name) {
    this.name = name; 
}

// статичен метод 
PersonType.create = function(name) {
    return new PersonType(name); 
};

// инстанция на метод 
PersonType.prototype.sayName = function() { 
    console.log(this.name); 
};

var person = PersonType.create("Nicholas");
				

Този код създава factory метод с PersonType.create(). В други програмни езици factory метода се счита статичен метод, тъй като той не зависи от инстанцията на PersonType за своите данни. ECMAScript 6 класовете опростяват създаването на статични членове с помощта на static нотация преди името на метод или accessor свойство. Например, тук е клас еквивалент на последния пример:

class PersonClass {

    // еквивалентно на PersonType конструктора 
    constructor(name) {
        this.name = name; 
    }

    // еквивалентно на PersonType.prototype.sayName 
    sayName() {
        console.log(this.name); 
    }

    // еквивалентно на PersonType.create
    static create(name) {
        return new PersonClass(name); 
    } 
}

let person = PersonClass.create("Nicholas");
				

Дефиницията на PersonClass има един единствен статичен метод наречен create(). Синтаксиса на метода е същия, както за sayName() с изключение на ключовата дума static. Можете да използвате ключовата дума static върху всеки метод или accessor свойство в рамките на един клас. Единственото ограничение е, че не можете да използвате static в дефиницията на конструктора.

worning
Статичните членове не са достъпни от инстанциите. Достъп до статичните членове винаги трябва да се получи от класа директно.

Наследяване с производни класовете

Друг проблем с потребителските типове перди ECMAScript 6, беше обширния процес необходим за изпълнение на наследяването. За правилно наследяване, трябват няколко стъпки. Например:

function Rectangle(length, width) {
    this.length = length;
    this.width = width; 
} 

Rectangle.prototype.getArea = function() {
    return this.length * this.width; 
};

function Square(length) { 
    Rectangle.call(this, length, length); 
} 

Square.prototype = Object.create(Rectangle.prototype, { 
    constructor: {
        value: Square,
        enumerable: true, 
        writable: true,
        configurable: true 
    } 
});

var square = new Square(3); 

console.log(square.getArea());     // 9
console.log(square instanceof Square);   // true 
console.log(square instanceof Rectangle);    // true
				

Тука, Square наследява от Rectangle и за да го направи, трябва да презапише Square.prototype с нов обект създаден от Rectangle.prototype, както и да извика Rectangle.call() метода. Тези стъпки често се бъркат от начинаещите в изучаването на езика, но са и източник на грешки и за опитни програмисти.

Класовете правят наследяването по-лесно с помощта на познатата ключова дума extends за уточняване на функциите, от която трябва да наследи класа. Прототипите се настройват автоматично и можете да получите достъп до базовия клас конструктор, използвайки super() метода. Ето ECMAScript 6 еквивалента на предишния пример:

class Rectangle { 
    constructor(length, width) {
        this.length = length;
        this.width = width; 
    } 
	
    getArea() {
    return this.length * this.width; 
    } 
}

class Square extends Rectangle { 
    constructor(length) {
        // също, като Rectangle.call(this, length, length)
        super(length, length); 
    } 
}

var square = new Square(3); 

console.log(square.getArea());     // 9
console.log(square instanceof Square);  // true 
console.log(square instanceofRectangle);  // true
				

В този пример, класа Square наследява от Rectangle с помощта на ключовата дума extends. Square конструктора използва super() за да извика Rectangle конструктора с определени аргументи. Имайте в предвид, че за разлика от версията на кода в ECMAScript 5 идентификатора Rectangle се използва само в рамките на декларацията на класа (след extends).

Класове, които наследяват от други класове са посочени, като производни класове. Използването на super() е изискване за производните класове, ако посочите конструктор; ако не го направите, ще възникне грешка. Ако решите да не използвате конструктор, тогава super() се извиква автоматично, с всички аргументи при създаването на нова инстанция на класа. Например, следващите два класа са идентични:

class Square extends Rectangle {
    // няма конструктор 
}

// е еквивалентно на

class Square extends Rectangle { 
    constructor(...args) {
        super(...args); 
    } 
}
				

Втория клас в този пример показва еквивалент на конструктор по подразбиране за всички производни класове. Всички аргументи се подават към конструктора на базовия клас. В този случай функционалността не е съвсем правилна, защото на Square конструктора му трябва само един аргумент и затова е най-добре да се определят ръчно в конструктора.

worning
Има няколко неща, които трябва да имате в предвид, когато използвате super():
  1. Може да използвате super() само в производен клас. Ако се опитате да го използвате в не-производен клас (клас който не използва extends) или функция, ще бъде хвърлена грешка.
  2. Трябва да извикате super() преди this в конструктора. Тъй като super() е отговорен за инициализацията на this, опита за достъп до this преди извикването на super() ще доведе до грешка.
  3. Единственият начин да се избегне извикване на super() е да се върне обект от клас конструктора.

Сянка на методите на класа

Методите на производните класове са винаги сянка на методите със същото име на базовия клас. Например можем да добавим getArea() към Square с цел да предефинираме тази функционалност:

class Square extends Rectangle { 
    constructor(length) {
        super(length, length); 
    }

    // презаписване и сянката на Rectangle.prototype.getArea() 
    getArea() {
        return this.length * this.length;
    } 
}
					

В този код, getArea() се определя, като част от Square и следователно Rectangle.prototype.getArea() метода вече няма да се извиква с всички случаи на Square. Разбира се винаги може да решите да извикате от базовия клас версията на метода с помощта на super.getArea(), също като:

class Square extends Rectangle { 
    constructor(length) {
        super(length, length); 
    }

    // презаписване, сянка и извикване на Rectangle.prototype.getArea() 
    getArea() {
        return super.getArea(); 
    }
}
					

Използването на super по този начин, е обсъдено в Глава 4 (виж "Easy Prototype Access със super референция"). Стойността this е автоматично настроена правилно, така че да можете да направите един прост метод за извикване.

Наследяване на статични членове

Ако базовият клас има статични членове, тогава тези статични членове също са на разположение на производния клас. Това наследяване работи подобно на други езици, но е нова концепция за JavaScript. Ето един пример:

class Rectangle { 
    constructor(length, width) {
        this.length = length;
        this.width = width; 
    } 
	
    getArea() {
        return this.length * this.width; 
    }

    static create(length, width) {
        return new Rectangle(length, width);
    } 
}

class Square extends Rectangle { 
    constructor(length) {

        // също, като Rectangle.call(this, length, length)
        super(length, length); 
    }
}

var rect = Square.create(3, 4); 

console.log(rect instanceof Rectangle);   // true 
console.log(rect.getArea());              // 12 
console.log(rect instanceof Square);      // false
					

В този код, новия static create() метод се добавя към Rectangle класа. Чрез наследяване този метод е достъпен, като Square.create() метода се държи по същия начин, както Rectangle.create() метода.

Производни класове от изрази

Може би най-мощният аспект на производните класове в ECMAScript 6 е възможността да се извлече клас от израз. Можете да използвате extends с всеки израз, толкова дълго, колкото израза реши, че е функция с [[Construct]] и прототип. Например:

function Rectangle(length, width) {
    this.length = length;
    this.width = width; 
} 

Rectangle.prototype.getArea = function() {
    return this.length * this.width; 
};

class Square extends Rectangle { 
    constructor(length) {
    super(length, length); 
    } 
}

var x = new Square(3); 
console.log(x.getArea());              // 9 
console.log(x instanceof Rectangle);   // true
					

Този пример определя Rectangle в ECMAScript 5 стил на конструктора, докато Square е клас. Тъй като Rectangle има [[Construct]] и прототип, Square класа все още може да наследява директно от него.

Приемане на всякакъв тип на израз след extends позволява някои мощни възможности, като например, динамично определяне, на това какво наследява. Например:

function Rectangle(length, width) {
    this.length = length;
    this.width = width; 
} 

Rectangle.prototype.getArea = function() {
    return this.length * this.width; 
};

function getBase() {
    return Rectangle; 
}

class Square extends getBase() { 
    constructor(length) {
    super(length, length); 
    } 
}

var x = new Square(3); 
console.log(x.getArea());              // 9 
console.log(x instanceof Rectangle);   // true
					

Тука, функцията getBase() се извиква директно, като част от декларацията на класа. Тя връща Rectangle, което прави този пример функционално еквивалентен на предишния. И тъй като можем да определим базовия клас динамично, означава, че е възможно да се създадат различни подходи за наследяване. Например, можете ефективно да създадете mixins:

let SerializableMixin = { 
    serialize() {
        return JSON.stringify(this); 
    } 
};

let AreaMixin = { 
    getArea() {
        return this.length * this.width; 
    } 
};

function mixin(...mixins) {
    var base = function() {};
    Object.assign(base.prototype, ...mixins);
    return base; 
}

class Square extends mixin(AreaMixin, SerializableMixin) { 
    constructor(length) {
        super();
        this.length = length;
        this.width = length;
    } 
}

var x = new Square(3); 
console.log(x.getArea());            // 9 
console.log(x.serialize());          // "{"length":3,"width":3}"
					

В този пример, mixins се използват вместо класическо наследяване. Функцията mixin() взема произволен брой аргументи, които представляват mixin обекти. Той създава функция наречена base и възлага свойства на всеки mixin обект към прототипа. Функцията след това се връща, така Square може да използва extends. Имайте в предвид, че тъй като extends все още се използва, вие трябва да извикате super() в конструктора.

В този случай, Square има както getArea() от AreaMixin така и serialize от SerializableMixin. Това се постига чрез прототипното наследяване. Функцията mixin() динамично наследява прототипа на новата функция с всички собствени свойства на всеки mixin. (Имайте в предвид, че ако няколко mixins имат едно и също свойство, само последното добавено свойство ще остане.)

worning
Въпреки, че всеки израз може да се използва след extends, не всички изрази дават валиден клас. По-конкретно следните типове изрази предизвикват грешка: В тези случаи, всеки опит да се създаде нова инстанция на класа ще доведе до грешка, защото няма [[Construct]] за извикване.

Наследяване от вградени модули

Почти толкова дълго, от когато има arrays в JavaScript, програмистите са искали да създават свои собствени специални видове arrays, чрез наследяване. Обаче, в ECMAScript 5 и по-рано, това не е било възможно. Опитите да се използва класическо наследяване не са водели до функциониращ код. Например:

// вградено поведение в array
var colors = []; 
colors[0] = "red"; 
console.log(colors.length);     // 1 

colors.length = 0; 
console.log(colors[0]);     // undefined

// опити за наследяване на array в ES5

function MyArray() {
    Array.apply(this, arguments); 
} 

MyArray.prototype = Object.create(Array.prototype, { 
    constructor: { 
        value: MyArray, 
        writable: true,
        configurable: true, 
        enumerable: true 
    }
});

var colors = new MyArray(); 
colors[0] = "red"; 
console.log(colors.length);     // 0 

colors.length = 0; 
console.log(colors[0]);        // "red"
					

В изхода на console.log в края на кода, може да видите, че използването на класическата форма на наследяване в JavaScript води до неочаквано поведение в резултатите на array. Дължината length и цифровото свойство в копието на MyArray не реагират по същия начин, както във вградения array, защото тази функционалност не се покрива от Array.apply() или чрез възлагане на прототипа.

Една от целите на ECMAScript 6 класовете е да се позволи наследяване от всички вградени модули. За да се постигне това, модела на наследяването на класове е малко по-различен от модела на класическото наследяване в ECMAScript 5 и по-рано, по два начина:

  • В класическото наследяване на ECMAScript 5, стойността this се създава първо от производните типове (например, MyArray) и след това се извиква базовия тип конструктор ( Array.apply()). Това означава, че this започва, като инстанция на MyArray и след това се украсява с допълнителни свойства от Array.
  • В базираното на класове наследяване на ECMAScript 6 стойността на this се създава първо от базовия ( Array) и след това се модифицира от конструктора на производния клас ( MyArray). Резултата е, че this започва с всички вградени функции на основата и правилно приема всички функции свързани с него.

Следния базиран на клас специален array, работи както се очаква

class MyArray extends Array {
    // празно }

var colors = new MyArray(); 

colors[0] = "red"; 
console.log(colors.length);    // 1 
colors.length = 0; 
console.log(colors[0]);        // undefined
					

В този пример, MyArray наследява директно от Array и следователно работи, като Array. Взаимодействието с цифровите свойства актуализира length свойството и манипулирането на length свойството актуализира цифровите свойства. Това означава, че не само може правилно да наследява от Array, но и да създаде свои собствени производни класове от arrays, както и да наследява от други вградени модули. С цялата тази допълнителна функционалност, ECMAScript 6 и производните класове ефективно отстраняват последния специален случай на наследяване от вградени модули, но този случай е все още на етап проучване.

Symbol.species свойства

Интересен аспект на наследяване от вградени модули е, че всеки метод, който връща инстанция на вграден модул автоматично, ще върне инстанция на производния клас. Така, че ако имаме производен клас MyArray, който наследява от Array методи, като slice(), ще се върне, като инстанция на MyArray. Например:

class MyArray extends Array {
    // празно 
}

let items = new MyArray(1, 2, 3, 4), 
    subitems = items.slice(1, 3); 
	
console.log(items instanceof MyArray);      // true 
console.log(subitems instanceof MyArray);   // true
					

В този код, slice() метода се връща, като инстанция на MyArray. Метода slice() е наследен от Array и се връща, като инстанция на Array нормално. Зад кулисите на сцената, това е Symbol.species свойство, който прави тази промяна.

Well-known Symbol.species се използва за определяне на статично свойство за достъп, което връща функция. Тази функция е конструктор, която за да се използва,като инстанция на клас, трябва да бъде създадена вътре в инстанцията на метода (вместо да се използва конструктора). Има няколко вградени типове, които имат дефиниран Symbol.species:

  • Array
  • ArrayBuffer (обсъден в Chapter 10)
  • Map
  • Promise
  • RegExp
  • Set
  • Typed Arrays (обсъдени в Chapter 10)

Всеки един от тези типове имат по подразбиране Symbol.species свойство, което връща this, което означава, че свойството винаги ще върне функция конструктор. Ако ви се налага да изпълнявате тези функции на потребителски клас, кодът ще изглежда така:

// няколко вградени типове използват видове подобни на this
class MyClass {
    static get [Symbol.species]() {
        return this; 
    } 
    constructor(value) {
        this.value = value; 
    } 
    clone() {
        return new this.constructor[Symbol.species](this.value); 
    }
}
					

В този пример, well-known Symbol.species се използва за да зададете статично свойство за достъп до MyClass. Имайте в предвид, че има само едни getter без setter, защото не е възможно да се промени вида на един клас. Всяко извикване на this.constructor[Symbol.species] връща MyClass. Метода clone() използва това определение, за да върне нова инстанция, вместо директно използване на MyClass, което позволява на производните класове да заменят тази стойност. Например:

class MyClass {
    static get [Symbol.species]() {
        return this; 
    } 
	
    constructor(value) {
        this.value = value; 
    } 
	
    clone() {
        return new this.constructor[Symbol.species](this.value); 
    }
}

class MyDerivedClass1 extends MyClass {
    // празно 
}

class MyDerivedClass2 extends MyClass {
    static get [Symbol.species]() {
        return MyClass; 
    }
}

let instance1 = new MyDerivedClass1("foo"),
    clone1 = instance1.clone(),
    instance2 = new MyDerivedClass2("bar"),
    clone2 = instance2.clone(); 
	
console.log(clone1 instanceof MyClass);             // true 
console.log(clone1 instanceof MyDerivedClass1);     // true 
console.log(clone2 instanceof MyClass);             // true 
console.log(clone2 instanceof MyDerivedClass2);     // false
					

Тука, MyDerivedClass1 наследява от MyClass и не променя Symbol.species свойството. Когато се извика clone(), той връща инстанцията на MyDerivedClass1, защото this.constructor[Symbol.species] връща MyDerivedClass1. Класа MyDerivedClass2 наследява от MyClass и overrides Symbol.species да върне MyClass. Когато се извика clone() върху инстанцията на MyDerivedClass2, върнатата стойност е инстанция на MyClass. Използвайки Symbol.species, за всеки производен клас може да се определи, какъв тип стойност трябва да върне, когато метода връща инстанция.

Например Array използва Symbol.species, за да каже на класа да използва методите, които връща array. В един клас, получен от Array можете да определите типа на обекта, върнат от наследените методи, като например:

class MyArray extends Array {
    static get [Symbol.species]() {
        return Array; 
    }
}

let items = new MyArray(1, 2, 3, 4),
    subitems = items.slice(1, 3); 
	
console.log(items instanceof MyArray);       // true 
console.log(subitems instanceof Array);      // true 
console.log(subitems instanceof MyArray);    // false
					

Този код overrides Symbol.species на MyArray, който наследява Array и всички наследени методи, които връщат arrays ще се използват, като инстанция на Array вместо на MyArray.

Като, цяло трябва да използвате Symbol.species свойството, когато искате да използвате this.constructor в клас метод. По този начин, позволява на получените класове да заменят типа на връщане лесно. Освен това, ако създавате производни класове от клас, който има дефиниран Symbol.species, не забравяйте да използвате тази стойност вместо тази на конструктора.

Използване на new.target в клас конструктори

В Глава 3, учихме за new.target и как неговата стойност се променя в зависимост от това как се извиква една функция. Можете също да използвате new.target в клас конструктор, за да определите, как се позовава един клас. В прост случай, new.target е равен на функцията конструктор за класа, както в този пример:

class Rectangle { 
    constructor(length, width) { 
        console.log(new.target === Rectangle);
        this.length = length;
        this.width = width; 
    }
} 

// new.target is Rectangle
var obj = new Rectangle(3, 4);     // outputs true
					

В този код, можем да видим, че new.target е еквивалентен на Rectangle, когато се извика new Rectangle(3, 4). Тъй като, клас конструктора не може да се извика без new, new.target винаги е дефиниран вътре в клас конструктора. Въпреки това, стойността не може винаги да е една и съща. Помислете над този код:

class Rectangle { 
    constructor(length, width) { 
        console.log(new.target === Rectangle);
        this.length = length;
        this.width = width; 
    }
}

class Square extends Rectangle { 
    constructor(length) {
        super(length, length) 
    } 
} 

// new.target is Square
var obj = new Square(3);       // outputs false
					

Тука, Square извиква Rectangle конструктора, така че new.target е равна на Square, когато се извика конструктора на Rectangle. Това е важно, защото дава на всеки конструктор способноста да променя поведението си, въз основа на това как се извиква. Например, можете да създадете абстрактен базов клас (такъв, който не може да бъде инстанция директно) с помощта на new.target:

// абстрактен базов клас
class Shape { 
    constructor() {
        if(new.target === Shape) {
            throw new Error("This class cannot be instantiated directly.") 
        } 
    } 
}

class Rectangle extends Shape { 
    constructor(length, width) {
        super();
        this.length = length;
        this.width = width; 
    } 
}

var x = new Shape();  // хвърля грешка

var y = new Rectangle(3, 4); // няма грешка						
console.log(y instanceof Shape); // true
					

В този пример, клас конструктора Shape хвърля грешка, когато new.target е Shape, което означава, че new Shape() винаги хвърля грешка. Въпреки това, можете да използвате Shape, като базов клас, това е което Rectangle прави. Извикването на super() изпълнява Shape конструктора и new.target е равно на Rectangle, затова конструктора продължава без грешка.

info
Понеже класа не може да бъде извикан без new, new.target никога не е undefined в рамките на клас конструктор.