Найти в Дзене
ZDG

Наследование в Javascript: Долгая история

Оглавление

Тема с Юнити пока отложена, т.к. там надо проект кодить руками, на что нет времени. А просто поговорить – пожалуйста. Поэтому сегодня поговорим про JаvaScript и наследование в нём. Картинок тоже делать не буду.

Вы, вероятно, уже знакомы с наследованием. Оно обычно выглядит так:

class A extends B

Ключевое слово тут "extends", то есть "расширяет". Или так, в стиле Python:

class A (B)

В JavaScript, однако, дело обстоит совершенно иначе. Постойте, скажете вы, ведь в JavaScript мы тоже пишем:

class A extends B

Но это лишь относительно недавно появившийся синтаксический сахар, который работает ещё и не во всех браузерах. Он скрывает под собой ад, который творится на самом деле. Именно поэтому я не пользуюсь такой записью. Она более удобна, но вызывает иллюзии, которых быть не должно.

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

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

Данные и код

Мне с самого начала представлялось, что код это некая жёстко фиксированная структура, которая воплощает логику. А данные это то, что можно изменять или перемещать.

Если использовать образы, то код это мясорубка, а данные это мясо. Мы засовываем данные (мясо) в код (мясорубку), и код совершает над ними какие-то действия. Какие – зависит от самого устройства кода, так же как работа мясорубки зависит от её конструкции. Прикрути в ней что-то не так – и работать она не будет. То же самое и с кодом.

И вот на выходе получается фарш, то есть уже обработанные данные. Этот фарш можно засунуть в другое устройство (другой код), и получить на выходе например колбасу.

В этой логике всё работает понятно. Код это жёсткая конструкция, которая проектируется и строится заранее, а данные путешествуют по этой конструкции и видоизменяются.

При этом, если в мясорубку засунуть вместо мяса гвозди, она наверно сломается. Так же сломается и код, если ему дать неверные данные. Аналогия весьма близкая. Вернёмся мы к ней позже, когда наступит время.

In-place объекты

Каждая переменная в программе это по сути мини-объект, который состоит из одного свойства. Например,

var a = 5;

Здесь слово var сигнализирует о том, что надо выделить кусочек памяти. Буква "a" говорит, что этот кусочек памяти мы будем называть "a". А число 5 это то, что мы будем там хранить.

Мы можем объявить несколько переменных, например,

var x = 5;
var y = 10;
var z = 0;

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

var obj = { x: 5, y: 10, z: 0 };

Вот мы и создали объект obj. У него есть свойства x, y, z, но так как это больше не отдельные переменные, обращаться к ним нужно так: obj.x, obj.y, obj.z.

Мы создали его прямо на лету, то есть не использовали никаких ранее созданных заготовок. Из пустого места появились свойства x, y, z. Если мы захотим создать ещё один такой же объект, то надо будет написать то же самое ещё раз.

Впрочем, нам в любом случае пришлось бы это сделать, так как мы же хотим присвоить свойствам x, y, z какие-то значения? Значит, пришлось бы их всё равно писать:

var obj2 = { x: 10, y: 15, z: 20 };

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

var obj = { x: 1, y: 2, z: 3, test: function() { return true; }};

И теперь мы можем обратиться к методу объекта так же, как к свойству объекта:

obj.test();

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

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

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

Класс – это инструкция по изготовлению объекта. Например, как выглядят классы во многих языках:

class A {
var x;
var y;
var z;
function test() { return true; }
}

Не вникая в конкретный синтаксис, мы можем догадаться, что данный класс A описывает определённую структуру объекта. А именно, у этого объекта должны быть свойства x, y, z и метод test().

Далее, когда мы захотим создать объект(ы) с такими свойствами и c таким методом, мы уже не будем описывать их свойства, а просто напишем:

var obj = new A();
var obj2 = new A();

Здесь нужно обратить особое внимание на new и A(). По сути это означает "новый объект класса A", но нюансы кроются в деталях.

Слово new нужно конкретно для того, чтобы создать объект-заготовку, то есть выделить память и минимально её оформить. Этот объект ещё безымянен и пуст, у него нет никаких свойств. А вот дальше написано A(), и это значит: примени к этой заготовке конструктор класса A. Eщё точнее: конструктор класса это функция, которая заполняет пустой объект нужными свойствами. Вызов функции пишется со скобками, поэтому мы пишем A(), хотя в разных языках разные правила.

При объявлении класса A мы нигде не определяли функцию-конструктор A(), потому что она существует по умолчанию (точнее, язык понимает, что делать в таких случаях).

Когда мы вызываем конструктор, то говорим ему: на тебе пустой объект, сделай нам из него такой объект, какой умеешь. Конструктор смотрит: а от какого я класса? Если от класса A, то я добавлю в объект свойства и методы из описания класса A. Если от класса B, то свойства и методы из класса B:

var a = new A();
var b = new B();

Повторим:

  • var – выделить память под новую переменную
  • a – назвать эту память "a"
  • new – выделить память под пустой объект
  • A() – применить к этому объекту конструктор класса A
  • и поместить указатель на готовый объект в переменную a

Ещё раз обратите внимание: свойства и методы объекта создаются только конструктором, до этого это просто пустой объект. А конструктор использует описание класса как инструкцию для строительства объекта.

В следующей части посмотрим, как эта система работает в JavaScript, и почему там легко запутаться.

Читайте дальше: