Основы JavaScript: классы и наследование

Java внешний интерфейс JavaScript React.js
Основы JavaScript: классы и наследование

предисловие

   Прежде всего, приглашаю всех подписаться на меняБлог на гитхабе, что можно рассматривать как небольшое поощрение для меня, ведь у меня нет денег, чтобы писать вещи, и я могу продолжать на своем собственном энтузиазме и всеобщем поощрении.

   Давно ничего не писал.По разным причинам в последнее время не находил времени зацикливаться на своем писании.Похоже на бег.Однажды расслабившись,трудно снова взяться за дело. В последнее время мне хочется успокоиться и написать что-нибудь снова, а выбор темы стал для меня головной болью.В последнее время я периодически испытываю головокружение от проблемы наследования JavaScript на работе.Я понимаю, что даже если много знаний очень простой, его нужно часто пересматривать и практиковать, иначе даже знакомые вещи будут часто заставлять вас чувствовать себя незнакомыми, поэтому выберите такую ​​​​самую базовую статью, как начало этого года.   

Добрый

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

var person = {
    name: "MrErHu", 
    sayName: function(){
        alert(this.name);
    }
};

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

function createObject(name){
    return {
        "name": name,
        "sayName": function(){
            alert(this.name);
        }
    }
}

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

function Person(name, age, job){
    this.name = name;
    this.sayName = function(){
        alert(this.name);
    };    
}

var obj = new Person();
obj.sayName();

мы знаемnewОператор выполнит следующие четыре шага:   

  1. Создать совершенно новый объект
  2. новые внутренние свойства объекта[[Prototype]](неформальный атрибут__proto__) подключается к прототипу конструктора
  3. Конструкторthisпривяжет новый объект
  4. Если функция не возвращает никакого другого объекта, тоnewВызовы функций в выражениях автоматически возвращают этот новый объект

   Таким образом, мы можем выполнять суждение о типах объектов, сгенерированных конструктором. Однако есть проблема с режимом простого конструктора, то есть методы каждого объекта независимы друг от друга, а функция по сути является объектом, поэтому это вызовет много траты памяти. рассмотрениеnewТретий шаг оператора, мы заново генерируем внутренние свойства объекта[[Prototype]]будет подключен к прототипу конструктора, поэтому, используя эту возможность, мы можем смешиватьШаблон конструктораа такжережим прототипа, чтобы решить вышеуказанную проблему.

function Person(name, age, job){
    this.name = name;
}

Person.prototype = {
    constructor : Person,
    sayName : function(){
        alert(this.name);
    }
}

var obj = new Person();
obj.sayName();

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

Person.prototype.sayName = function(){
    alert(this.name);
}

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

Object.defineProperty(Person, "constructor", {
    configurable: false,
    enumerable: false,
    writable: true,
    value: Person
});

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

Person('MrErHu');
console.log(window.name); //MrErHu

   Однако мы вступили в эру ES6, и все меняется. ES6 реализует для нас концепцию классов в JavaScript. Приведенный выше код можно реализовать с помощью краткого класса.   

class Person {
    constructor(name){
        this.name = name;
    }
    
    sayName(){
        alert(this.name);
    }
}

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

let person = new Person('MrErHu');
person.sayName(); //MrErHu

   Мы видим, что в классеconstructorФункция принимает на себя функцию предыдущего конструктора, и здесь можно инициализировать свойства экземпляра в классе. метод классаsayNameЭто эквивалентно тому, что мы определили в прототипе конструктора ранее. По сути, в ES6 классы — это просто синтаксический сахар для функций:   

typeof Person  //"function"

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

let person = new Person('MrErHu')
class Person { //...... } 

Использование    выше неверно. Таким образом, класс больше похож на функциональное выражение.

   Во-вторых, весь код в объявлении класса автоматически запускается в строгом режиме, и класс нельзя вывести из строгого режима. Эквивалентно всему коду в объявлении класса, который выполняется в режиме "строгого использования".

   Кроме того, все методы в классе неперечислимы.

   Наконец, классы нельзя вызывать напрямую, они должны бытьnewвызов оператора. На самом деле у функций есть внутренние свойства[[Constructor]]а также[[Call]], Конечно, к этим двум методам нельзя получить доступ извне, и они существуют только в движке JavaScript. Когда мы вызываем функцию напрямую, мы фактически вызываем внутреннее свойство[[Call]], все, что он делает, это напрямую выполняет тело функции. когда мы проходимnewКогда оператор вызывается, он фактически вызывает внутреннее свойство[[Constructor]], все, что он делает, это создает новый объект экземпляра и выполняет функцию над объектом экземпляра (привязкаthis) и, наконец, возвращает новый экземпляр объекта. Потому что класс не содержит внутренних свойств[[Call]], поэтому его нельзя вызвать напрямую. Кстати, могу упомянуть, что в ES6мета-свойство new.target    

Так называемыймета-атрибутозначаетнеобъектные свойства, может предоставить нам некоторую дополнительную информацию.new.targetЯвляется одним из метаболических, когда вызов[[Constructor]]имущество,new.targetто естьnewцель оператора, если вызов[[Call]]Атрибуты,new.targetто естьundefined. На самом деле это свойство очень полезно, например, мы можем определитьnewФункция, вызываемая оператором:

function Person(){
    if(new.target === undefined){
        throw('该函数必须通过new操作符调用');
    }
}

   Или мы можем создать функцию в JavaScript, аналогичную виртуальной функции в C++:

class Person {
  constructor() {
    if (new.target === Person) {
      throw new Error('本类不能实例化');
    }
  }
}

  

наследовать

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

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

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

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

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

var square = new Square(3);

console.log(square.getArea());
console.log(square instanceof Square);
console.log(square instanceof Rectangle);

   Первый подклассSquareЧтобы создать родительский классRectangleсвойства, мы находимся вSquareв функцииRectangle.call(this, length, length)Вызывается метод, цель создать свойства родительского класса в подклассе, чтобы наследовать метод родительского класса, даемSquareПрисваивается новый прототип. кроме как черезObject.createобразом, вы также должны были видеть следующий путь:   

Square.prototype = new Rectangle();
Object.defineProperty(Square.prototype, "constructor", {
    value: Square,
    enumerable: false,
    writable: false,
    configurable: false
});

  Object.createНовый метод в ES5 для создания нового объекта. Созданный объект наследует прототип другого объекта, а некоторые свойства можно указать при создании нового объекта.Object.createСпособ указания атрибутов такой же, какObject.definePropertyТочно так же все они представлены в виде дескрипторов атрибутов. Поэтому видно, что поObject.createа такжеnewПринципиально нет разницы в способе реализации наследования.       Но ES6 может сильно упростить этапы наследования:

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

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

   Мы видим, что реализовать наследование классов через ES6 очень просто.Squareвызвать конструкторsuperЕго цель — вызвать конструктор родительского класса. конечно вызовsuperФункция необязательна, если вы укажете конструктор по умолчанию, он будет вызван автоматическиsuperфункцию и передать все параметры.       Мало того, наследование классов ES6 дает больше новых возможностей, прежде всегоextendsЛюбой тип выражения может быть унаследован, если выражение в конечном итоге возвращает наследуемую функцию (то есть,extendsможет унаследовать[[Constructor]]функции внутренних свойств , таких какnullи генераторные функции, стрелочные функции не обладают этим свойством, поэтому они не могут быть унаследованы). Например:

class A{}
class B{}

function getParentClass(type){
    if(//...){
        return A;
    }
    if(//...){
        return B;
    }
}

class C extends getParentClass(//...){
}

   Вы можете видеть, что мы добились динамического наследования с помощью приведенного выше кода и можем наследовать разные классы в соответствии с различными условиями суждения.      Наследование ES6 отличается от наследования классов, реализованного в ES5. ES5 заключается в том, чтобы сначала создать экземпляр подкласса, а затем создать свойства родительского класса на основе экземпляра подкласса. ES6 как раз наоборот: сначала создается экземпляр родительского класса, а затем расширяются свойства дочернего класса на основе экземпляра родительского класса. Используя это свойство, мы можем делать то, чего не может 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";
colors.length  // 0

colors.length = 0;
colors[0]  // "red"

   Как видите, унаследовано от нативных объектовArrayизMyArrayв случаеlengthне так оригинальноArrayэкземпляр класса Также есть возможность динамически отражать количество элементов в массиве или путем измененияlengthсвойство, тем самым изменяя данные в массиве. Причина в том, что традиционный способ реализации наследования массива заключается в том, чтобы сначала создать подкласс, а затем расширить свойства и методы родительского класса на основе подкласса, поэтому нет связанного метода наследования, но ES6 может легко достичь это:

class MyArray extends Array {
  constructor(...args) {
    super(...args);
  }
}

var arr = new MyArray();
arr[0] = 12;
arr.length // 1

arr.length = 0;
arr[0] // undefined

   Мы можем видеть сквозьextendsосуществленныйMyArrayМассив, созданный классом, можно использовать как собственный массив, используяlengthСвойства отражают изменения массива и изменяют элементы массива. Мало того, в ES6 мы можем использоватьSymbol.speciesСвойства позволяют нам изменить возвращаемый тип экземпляра методов, унаследованных от собственных объектов, когда мы наследуем от собственных объектов. Например,Array.prototype.sliceвернулся быArrayэкземпляр типа, установивSymbol.speciesсвойство, мы можем заставить его возвращать пользовательский тип объекта:   

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

let items = new MyArray(1,2,3,4);
subitems = items.slice(1,3);

subitems instanceof MyArray; // true

   Последнее замечание,extendsРеализация наследования может наследовать статические функции-члены родительского класса, например:   

class Rectangle{
    // ......
    static create(width, height){
        return new Rectangle(width, height);
    }
}

class Square extends Rectangle{
    //......
}

let rect = Square.create(3,4);
rect instanceof Square; // true