[[Инженер JavaScript — 8]]

Мы начинаем статью с напоминания читателям о том, что добавление пользовательского типа данных в JavaScript невозможно.

Обзор ES6:

Мы уже видели классы ES6 на нескольких простых примерах. Они использовали обычные функции для облегчения создания объектов, которые могли бы иметь данные и демонстрировали несколько вариантов поведения, используя цепочку [[Prototype]]. Мы также увидели, как можно использовать синтаксис «extends» для достижения единого наследования, когда данные могут быть добавлены несколькими классами/функциями в один и тот же объект, тогда как поведение было добавлено в цепочку [[Prototype]] объекта с использованием нижнего регистра « прототип» функций/классов. Это стандартная идея, используемая в течение многих лет, когда новые объекты, созданные с использованием разных функций конструктора, будут подключаться к нескольким частям цепочки и повторно использовать функции без необходимости переписывать какое-либо поведение/функцию. Ключевое слово this предназначалось для ссылки на объект, который будет вызывать поведение/функцию.

Целью данной статьи является демонстрация аспекта «что» в возможностях класса JavaScript. Читатели, интересующиеся аспектом «как» функций, могут прочитать мою другую статью, где мы подробно обсуждаем это.

Концепция управления доступом к объектам класса:

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

Возможности классов ES2022:

  1. общедоступное свойство дескриптора данных объекта obj.
  2. общедоступные геттеры, сеттеры, свойство доступа, унаследованное объектом obj.
  3. общедоступные методы экземпляра, унаследованные объектом obj.
  4. свойство дескриптора частных данных для объекта obj.
  5. частные геттеры, сеттеры, свойство доступа к объекту obj.
  6. частные методы экземпляра, повторно используемые объектом obj.
  7. общедоступное свойство дескриптора статических данных в классе X.
  8. общедоступные статические геттеры, свойство доступа setters в классе X.
  9. общедоступные статические методы класса X.
  10. частное свойство дескриптора статических данных в классе X.
  11. частные статические геттеры, свойство доступа setters в классе X.
  12. частные статические методы класса X.
  13. Наследование в объектах, созданных с помощью классов
  14. Наследование статических свойств в классах

Пример 1: общедоступные и частные свойства и методы экземпляра

class Person {
  name; // public instance data descriptor property
  place; // public instance data descriptor property
  #books; // private instance data descriptor property
  #agePrivate = '0 years'; // private instance data descriptor property
 
  // public instance constructor method
  constructor(name, place, age) {
    this.name = name;
    this.place = place;
    this.age = age;
    this.#books = [];
  }
  
  // public instance accessor descriptor property
  set age(val) {
    if(val > 0) {
      this.#agePrivate = `${val} years`;
    }
  }
  
  // public instance accessor descriptor property
  get age() {
    return this.#agePrivate;
  }
  
  // private instance method defined once
  #isBookPresent(bookDetail) {
    return this.#books.find((aBook) => 
         this.#sameBook(aBook, bookDetail));
  }
  
  // private instance method defined once
  #sameBook(bookA, bookB) {
    return bookA.author === bookB.author && 
         bookA.bookTitle === bookB.bookTitle;
  }
  
  // public instance method defined once
  removeBook(bookDetail) {
    if(this.#isBookPresent(bookDetail)) {
        this.#books = this.#books.filter((aBook) => 
          !this.#sameBook(aBook, bookDetail));
    }
  }
  
  // public instance method defined once
  addBook(bookDetail) {
    if(!this.#isBookPresent(bookDetail)) {
      this.#books.push(bookDetail);
    }
  }
  
  // public instance method defined once
  greet() {
    console.log(`Hello I am ${this.name}`);
  }
  
  // public instance method defined once
  showAllBooks() {
    console.log("ALL BOOKS: ", this.#books);
  }
}
const prince = new Person("Prince Jha", "Dandeli", 23);
prince.greet();
console.log(prince);
console.log(prince.age);
prince.age = 24;
console.log(prince.age);
// console.log(prince.#agePrivate); // cannot read private filed outside class that defined it
prince.addBook({author: 'Kyle Simpson', bookTitle: 'You Dont Know JavaScript'});
prince.addBook({author: 'Horstman', bookTitle: 'Core Java Volume 1'});
prince.addBook({author: 'Mark Grand', bookTitle: 'Patterns in Java Volume 1'});
prince.showAllBooks();

Единственная разница между общедоступными и частными свойствами заключается в том, что к частным свойствам объекта obj можно получить доступ только из того же класса X, в котором свойство определено, тогда как к любым общедоступным свойствам (собственным или унаследованным) можно получить доступ непосредственно в объекте. object obj из любого места (без ограничений в доступе к ним).

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

Пример 2. Затенение общедоступных методов экземпляра путем добавления свойства к объекту с использованием алгоритма [[Set]].

class A {
  name;
  goodMessage = "Good Morning";
  badMessage = "Bad things to say";
  
  constructor(name) {
    this.name = name;
  }
  
  #sayBadThings() {
      console.log("I am a bad person ",this.name," ", 
          this.badMessage);
    }

  saySomething() {
    console.log("I am a good person ",this.name, " ", 
          this.goodMessage);
  }
  
  changeAttitudeOfPerson() {
    this.saySomething = this.#sayBadThings; // shadow 'saySomething' property
  }
}
const obj = new A("Raju");
const pqr = new A("Shyam");

obj.saySomething();
pqr.saySomething();

// Now shadow using [[Set]] algorithm only the object obj
obj.changeAttitudeOfPerson();

obj.saySomething();
pqr.saySomething();

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

class A {
  name;
  goodMessage = "Good Morning";
  badMessage = "Bad things to say";
  
  constructor(name) {
    this.name = name;
  }

  #sayBadThings() {
      console.log("I am a bad person ",this.name," ", this.badMessage);
    }
  
  #saySomething() {
    console.log("I am a good person ",this.name, " ", this.goodMessage);
  }
  
  changeAttitudeOfPerson() {
    this.#saySomething = this.#sayBadThings;
  }
}
const obj = new A("Raju");
const pqr = new A("Shyam");
obj.changeAttitudeOfPerson(); // Error

Класс, определяющий частное свойство, проверяется во время доступа к свойству.

Пример 4: Механизм контроля доступа в действии.
В основном любой доступ к частному свойству, который появляется внутри класса A, должен быть определен классом A. В приведенном ниже примере мы пытаемся получить доступ к частному свойству #x, которое было добавлено к объекту xyz классом B внутри класса A. JavaScript жалуется, что внутри xyz нет #x, определенного классом A.

class A {
  #x;
  constructor(val) {
    console.log("Constructed in A");
    this.#x = val;
  }
  
  checkEqualX(obj) {
    return this.#x === obj.#x;
  }
}
class B {
  #x;
  constructor(val) {
    console.log("Constructed in B");
    this.#x = val;
  }
}
const obj = new A(20);
const pqr = new A(20);
const xyz = new B(20);

console.log(obj.checkEqualX(pqr)); // true
console.log(obj.checkEqualX(xyz)); // Error, #x defined on xyz is not defined by class A

Пример 5. Попытка доступа к частному свойству, которое не определено в объекте, с помощью класса A запрещена.

class A {
  #x;
  constructor(val) {
    console.log("Constructed in A");
    this.#x = val;
  }
  
  checkXY() {
    console.log("Check #x", this.#x);
    console.log("Check #y", this.#y); // Syntax Error
  }
}

Соображения по выделению памяти:

  1. Поля экземпляра дескриптора общедоступных данных выделяются один раз для каждого объекта, созданного с использованием класса X или его подкласса.
  2. Свойства дескриптора открытого доступа определяются один раз в классе и повторно используются экземплярами. В основном свойства определяются как собственные свойства объекта «прототип» в нижнем регистре, а не экземпляров. Экземпляры наследуют общедоступные геттеры и сеттеры от объектов-прототипов нижнего регистра.
  3. общедоступные методы экземпляра определяются один раз в классе и повторно используются экземплярами. Они определяются как собственные свойства объекта «прототип» в нижнем регистре, а не экземпляров. Экземпляры наследуют общедоступные методы от объектов-прототипов нижнего регистра.
  4. Поля экземпляра дескриптора частных данных выделяются памяти один раз для каждого объекта, созданного с использованием класса X или его подкласса.
  5. частные свойства дескриптора доступа определяются один раз в классе и повторно используются экземплярами.
  6. частные методы экземпляра определяются один раз в классе и повторно используются всеми экземплярами.
  7. статические свойства всегда определяются один раз по отношению к классу.

Пример 6: Использование статических свойств и методов для синглтона.

// Singleton
class App {
  
  static #rating = undefined; // private static data descriptor property
  static #appName; // private static data descriptor property
  static registeredCountryName = "India"; // public static data descriptor property
  
  // public static method
  static showRating() {
    if(!App.#rating) {
      console.log("Not yet rated"); 
    } else {
      console.log("App Rating ", App.#rating);
    }
  }
  
  // public static method
  static rateApplication(val) {
    if(typeof val === 'number' && val > 0 && val <= 5) {
      App.#rating = val;
    console.log("Rated ", val);
    } else {
      console.log(`Could not rate ${val} make sure it is a number > 0 and number <= 5`);
    }
  }
  
  // public static accessor descriptor property
  static get applicationName() {
    return App.#appName;
  }
  
  // public static accessor descriptor property
  static set applicationName(val) {
    App.#appName = `${val} Application`;
  }
}
App.showRating();
App.rateApplication(3.5);
App.showRating();

App.applicationName = "Tech Champs";
console.log(App.applicationName);
console.log(App.registeredCountryName);

Управление доступом в статических частных ресурсах:

Пример 7: Невозможно прочитать частное свойство класса вне класса

class Bat {
  static color = "Brown";
  static #length = 40;
}

console.log(Bat.color);
conole.log(Bat.#length);
// #length private static property should be
// accessed from inside class Bat only

Пример 8: Невозможно прочитать частное статическое свойство, которое не определено внутри класса

class Bat {
  static color = "Brown";
  static #length = 40;
  
  static showLength() {
    console.log("Length of Bat ", Bat.#length);
  }
  
  static showBreadth() {
    console.log("Breadth of Bat ", Bat.#breadth); // Syntax Error
  }
}

console.log(Bat.color);
Bat.showLength();
Bat.showBreadth();

Пример 9. Доступ к статическим закрытым свойствам всегда осуществляется с помощью объекта класса/функции. Объект obj, который используется для доступа к закрытым статическим свойствам класса X, всегда должен относиться к одному и тому же классу X. ( obj === X ). Также такая проверка выполняется во время выполнения при попытке доступа к свойству.

class Animal {
  static #isAnimal = true;
  
  static checkAnimal() {
    return Boolean(this.#isAnimal);
  }
}

class Human extends Animal {}

console.log(Animal.checkAnimal()); // true
console.log(Human.checkAnimal()); // Error , WTF?

В примере 9 функция checkAnimal была вызвана классом Human, который наследуется от класса Animal, но, поскольку функция checkAnimal была вызвана в классе Human, при выполнении функции checkAnimal этот контекст будет Human. Статическое частное свойство определено по отношению к классу Animal, но к нему пытаются получить доступ с помощью объекта функции Human, правила управления доступом не разрешают этого.

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

Наследование объектов в классах ES2022:

Пример 10: Наследование и инкапсуляция

class Person {
  #name;
  #age;
  constructor(name, age) {
    this.#name = name;
    this.#age = age;
  }
  
  getName() {
    return this.#name;  
  }
  
  greet() {
    console.log("Greeting! Hello I am ", this.#name);
  }
}
class Student extends Person {
  #branch;
  
  constructor(name, age, branch) {
    super(name, age);
    this.#branch = branch;
  }
  
  introduce() {
    console.log(`Hello EveryOne, I am ${this.getName()}.`);
    console.log(`I am a student of ${this.#branch} department`);
  }
}
const prince = new Student("Prince Jha", 23, "Computer Science and Engineering");
prince.introduce();
prince.greet();

Пример 11: Наследование, но без инкапсуляции.
Напишем тот же предыдущий пример без приватных полей. Поскольку имя свойства, возраст, ветвь являются свойствами дескриптора данных, непосредственно определяемыми как собственные свойства объекта. Нам больше не нужен метод, подобный getName, для доступа к значению свойства имени объекта. Мы можем просто использовать алгоритм [[Get]] для доступа к свойству.

class Person {
  name;
  age;
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  greet() {
    console.log("Greeting! Hello I am ", this.name);
  }
}
class Student extends Person {
  branch;
  
  constructor(name, age, branch) {
    super(name, age);
    this.branch = branch;
  }
  
  introduce() {
    console.log(`Hello EveryOne, I am ${this.name}.`);
    console.log(`I am a student of ${this.branch} department`);
  }
}
const prince = new Student("Prince Jha", 23, "Computer Science and Engineering");
prince.introduce();
prince.greet();

Наследование статических свойств в классах ES2022:

Статические свойства наследуются с использованием прототипного наследования. Отношение наследования достигается путем соединения ссылки [[Prototype]] класса B с указанием на класс A.

Пример 12: Наследование статических свойств

class A {
  static a = 10;
  static #hiddenA = 20;
  
  static showHiddenARisky() {
    console.log(this === A);
    console.log("a = ", this.a);
    console.log("hidden A ", this.#hiddenA); // this should be A otherwise fails with Error
    console.log("right way hiddenA ", A.#hiddenA);
  }
}
class B extends A {
  static b = 40;
  static #hiddenB = 100;
  
  static showProperties() {
    console.log("a showProperties ", this.a); // works
    console.log("b showProperties ", this.b); // works
    console.log("hidden B", B.#hiddenB); // works
    // console.log(A.#hiddenA); // Error, #hiddenA not defined in class B
    A.showHiddenARisky(); // this is A
    this.showHiddenARisky();
  }
}
console.log(B.b); // own property of class B
console.log(B.a); // inherited from class A
// console.log(B.#hiddenB); // access control, not from outside class
B.showProperties();

Остерегайтесь алгоритма [[Set]] и возможности затенения при наследовании статических свойств

Пример 13: Затенение общедоступных статических свойств дескриптора данных в прототипном наследовании классов

class A {
  static a = 20;
  
  static what(val) {
    this.a = val; // public static property, [[Set]] algorithm
  }
}
class B extends A {
  static b = 50;
}
console.log(B.b); // 100 , own property
console.log(B.a); // 20, inherited property present in class A

B.what(100); // this is B, [[Get]] mechanism
console.log(B.b); // 50
console.log(B.a); // added a property to class B, 100
// shadowing and adding a data descriptor property 
// instead of overwriting the data descriptor property inherited from the chain

console.log(A.a); // still 20

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

Пример 14. Невозможно скрыть статический закрытый метод в классе.

class A {
  static #log() {
    console.log("Original Log");
  }
  
  static changeLog() {
      this.#log = function () {
         console.log("Hello World");
      }
  }
}

A.changeLog(); // Error , cannot shadow private static method