Изучение исходного кода, несомненно, может изменить траекторию вашей карьеры разработчика. Даже заглянув под поверхность всего на один уровень, вы сможете выделиться среди большинства среднестатистических разработчиков.
Это первый шаг к мастерству!
Вот личная история: в моей текущей работе в стартапе AI/ML команда не могла понять, как перенести данные Neo4j с сервера на фронтенд для визуализации, и они сделали презентацию через 12 часов. Меня пригласили в качестве фрилансера, и вы могли ясно видеть панику. Проблема была в том, что данные, возвращаемые Neo4j, не были в правильном формате, ожидаемом инструментом визуализации neo4jd3 .
Представьте себе: Neo4jd3 ожидает треугольник, а Neo4j возвращает квадрат. Это несовместимое несоответствие прямо здесь!
Скоро мы, возможно, займемся графовыми данными с JavaScript и Neo4j в Mastered ! Это изображение вызывает ностальгию.
Было только два варианта: переделать весь бэкэнд Neo4j или изучить исходный код Neo4jd3, выяснить ожидаемый формат, а затем создать адаптер для преобразования квадрата в треугольник.
neo4jd3 <- adapter <- Neo4j
Мой мозг по умолчанию переключился на чтение исходного кода, и я создал адаптер: neo4jd3-ts .
import createNeoChart, { NeoDatatoChartData } from "neo4jd3-ts";
Адаптер — NeoDatatoChartData
, а все остальное — история. Я принял этот урок близко к сердцу, и при каждой возможности я спускаюсь на уровень ниже в каждом инструменте, который я использую. Это стало настолько распространенным, что иногда я даже не читаю документацию.
Этот подход кардинально изменил мою карьеру. Все, что я делаю, похоже на магию. Через несколько месяцев я руководил критически важными миграциями серверов и проектами, и все потому, что я сделал шаг к источнику.
Вот о чем эта серия: не довольствоваться API, а выходить за рамки, учиться воссоздавать эти инструменты. Вырваться из колеи в этом мире хайпа вокруг ИИ — вот что делает разработчика ценным за пределами среднего!
В этой серии статей я планирую изучить популярные библиотеки и инструменты JavaScript, а также вместе разобраться, как они работают и какие закономерности мы можем у них изучить, по одному инструменту за раз.
Поскольку я в основном работаю бэкенд-разработчиком (да, полного цикла, но 90% времени занимаюсь бэкендом), лучшего инструмента для начала, чем Express.js, не найти.
Я предполагаю, что у вас есть опыт программирования и хорошее понимание основ программирования! Вас можно отнести к продвинутому новичку.
Будет очень сложно и утомительно пытаться изучать/преподавать исходный код, одновременно обучая основам. Вы можете присоединиться к серии, но будьте готовы к тому, что будет сложно. Я не могу охватить все, но я постараюсь сделать все, что смогу.
Эта статья написана до Express по следующей причине: я решил рассмотреть очень маленькую библиотеку, от которой зависит Express, merge-descriptors , которая на момент написания статьи имела 27 181 495 загрузок и содержала всего 26 строк кода.
Это даст нам возможность создать структуру и позволит мне познакомить вас с основами объектов, которые имеют решающее значение при создании модулей JavaScript.
Прежде чем продолжить, убедитесь, что в вашей системе есть исходный код Express и merge-descriptors . Таким образом, вы сможете открыть его в IDE, и я смогу указать вам с помощью номеров строк, где мы ищем.
Express — это мощная библиотека. Мы рассмотрим все, что сможем, в нескольких статьях, прежде чем перейдем к другому инструменту.
Откройте исходный код Express в IDE, желательно с указанием номеров строк, перейдите в папку lib
и откройте файл express.js
, файл входа.
В строке 17 представлена наша первая библиотека:
var mixin = require('merge-descriptors');
Использование в строках 42 и 43:
mixin(app, EventEmitter.prototype, false); mixin(app, proto, false);
Прежде чем мы даже рассмотрим, что здесь происходит, нам нужно сделать шаг назад и поговорить об объектах в JavaScript, за пределами структуры данных. Мы обсудим композицию, наследование, прототипы и миксины — название этой статьи.
Закройте исходный код Express и создайте где-нибудь новую папку, чтобы продолжить изучение этих важнейших основ объектов.
Объект — это инкапсуляция данных и поведения, лежащая в основе объектно-ориентированного программирования (ООП) . Интересный факт: почти все в JavaScript является объектом.
const person = { // data name: "Jane", age: 0, // behavior grow(){ this.age += 1; } };
Все, что находится между открывающимися и закрывающимися фигурными скобками в объекте person
, называется собственными свойствами объекта . Это важно.
Собственные свойства — это те, которые относятся непосредственно к объекту. name
, age
и grow
— это собственные свойства person
.
Это важно, поскольку каждый объект JavaScript имеет свойство prototype
. Давайте закодируем указанный выше объект в функциональный план, чтобы иметь возможность динамически создавать объекты person
.
function createNewPerson(name, age){ this.name = name; this.age = age; } createNewPerson.prototype.print = function(){ console.log(`${this.name} is ${this.age}`); }; const john = new createNewPerson("John", 32);
Прототип — это то, как объекты JavaScript наследуют свойства и методы от других объектов. Разница между Own Properties
и Prototype
заключается в доступе к свойству объекта:
john.name; // access
JavaScript сначала будет искать в Own Properties
, так как они имеют высокий приоритет. Если свойство не найдено, он рекурсивно ищет в собственном prototype
объекта, пока не найдет null и не выдаст ошибку.
Объект-прототип может наследовать от другого объекта через свой собственный прототип. Это называется цепочкой прототипов.
console.log(john.hasOwnProperty('name')); // true console.log(john.hasOwnProperty('print')); // false, it's in the prototype
Однако print
работает на john
:
john.print(); // "John is 32"
Вот почему JavaScript определяется как язык, основанный на прототипах. С прототипами мы можем сделать больше, чем просто добавить свойства и методы, такие как наследование.
"Hello world" наследования — это объект млекопитающего. Давайте воссоздадим его с помощью JavaScript.
// our Mammal blueprint function Mammal(name) { this.name = name; } Mammal.prototype.breathe = function() { console.log(`${this.name} is breathing.`); };
В JavaScript внутри объекта Object
есть статическая функция:
Object.create();
Он создает объект аналогично {}
и new functionBlueprint
, но разница в том, что create
может принимать прототип в качестве параметра для наследования.
// we use a cat blueprint function here (implemented below) Cat.prototype = Object.create(Mammal.prototype); // correction after we inherited all the properties Cat.prototype.constructor = Cat;
Теперь Cat
будет метод breathe
, найденный в Mammal
, но важно знать, что Cat
указывает на Mammal
как на свой прототип.
Mammal Blueprint : Сначала мы определяем функцию Mammal
и добавляем метод breathe
к ее прототипу.
Наследование Cat : Мы создаем функцию Cat
и устанавливаем Cat.prototype
в Object.create(Mammal.prototype)
. Это делает прототип Cat
наследуемым от Mammal
, но изменяет указатель constructor
на Mammal
.
Исправление конструктора : Мы исправляем Cat.prototype.constructor
, чтобы он указывал обратно на Cat
, гарантируя, что объект Cat
сохраняет свою идентичность, наследуя методы от Mammal
. Наконец, мы добавляем метод meow
к Cat
.
Такой подход позволяет объекту Cat
получать доступ как к методам Mammal
(например, breathe
), так и к методам его собственного прототипа (например, meow
).
Нам нужно это исправить. Давайте создадим полный пример:
function Cat(name, breed) { this.name = name; this.breed = breed; } Cat.prototype = Object.create(Mammal.prototype); // cat prototype pointing to mammal // correction after we inherited all the properties Cat.prototype.constructor = Cat; // we are re-pointing a pointer, the inherited properties are still there Cat.prototype.meow = function() { console.log(`${this.name} is meowing.`); };
Чтобы понять Cat.prototype.constructor = Cat
, вам нужно знать об указателях. Когда мы наследуем от Mammal
с помощью Object.create
, он меняет указатель нашего прототипа Cat
на Mammal
, что неправильно. Мы по-прежнему хотим, чтобы наш Cat
был отдельным индивидуумом, несмотря на наличие родителя Mammal
.
Вот почему нам нужно это исправить.
В этом примере Cat
наследуется от Mammal
с помощью цепочки прототипов. Объект Cat
может получить доступ к методам breathe
и meow
.
const myCat = new Cat("Misty", "Ragdoll"); myCat.breathe(); // Misty is breathing. myCat.meow(); // Misty is meowing.
Мы можем создать собаку, также унаследовав ее от млекопитающего:
function Dog(name, breed) { this.name = name; this.breed = breed; } Dog.prototype = Object.create(Mammal.prototype); Dog.prototype.constructor = Dog; Dog.prototype.bark = function() { console.log(`${this.name} is barking.`); }; const myDog = new Dog('Buddy', 'Golden Retriever'); myDog.breathe(); // Buddy is breathing. myDog.bark(); // Buddy is barking.
Мы создали базовое классическое наследование, но почему это важно? Я думал, мы рассмотрим исходный код!
Да, верно, но прототипы — это ядро построения эффективных и гибких модулей, помимо наследования. Даже простые, хорошо написанные модули напичканы объектами-прототипами. Мы просто закладываем основы.
Альтернативой наследованию является композиция объектов, которая грубо берет два или более объектов и объединяет их вместе, образуя «супер»-объект.
Миксины позволяют объектам заимствовать методы из других объектов без использования наследования. Они удобны для обмена поведением между несвязанными объектами.
Именно это и делает наше первое исследование: библиотека merge-descriptors
мы обещали рассмотреть в первую очередь.
Мы уже видели, где и как это используется в Express. Теперь мы знаем, что это для композиции объектов.
В строке 17 представлена наша первая библиотека:
var mixin = require('merge-descriptors');
Использование в строках 42 и 43:
mixin(app, EventEmitter.prototype, false); mixin(app, proto, false);
Используя имеющиеся у нас знания, мы уже можем сделать вывод, что mixin
объединяет EventEmitter.prototype
и proto
в объект с именем app
.
Мы вернемся к app
, когда начнем говорить об Express.
Это весь исходный код merge-descriptors
:
'use strict'; function mergeDescriptors(destination, source, overwrite = true) { if (!destination) { throw new TypeError('The `destination` argument is required.'); } if (!source) { throw new TypeError('The `source` argument is required.'); } for (const name of Object.getOwnPropertyNames(source)) { if (!overwrite && Object.hasOwn(destination, name)) { // Skip descriptor continue; } // Copy descriptor const descriptor = Object.getOwnPropertyDescriptor(source, name); Object.defineProperty(destination, name, descriptor); } return destination; } module.exports = mergeDescriptors;
С самого начала всегда обращайте внимание на то, как используется функция и какие параметры она принимает:
// definition mergeDescriptors(destination, source, overwrite = true) // usage var mixin = require('merge-descriptors'); mixin(app, EventEmitter.prototype, false); mixin(app, proto, false);
App
— это наш пункт назначения. Мы знаем, что mixin
означает композицию объектов. Грубо говоря, этот пакет компонует исходный объект в целевой объект с возможностью перезаписи.
Перезапись, по предположению, происходит, если app
(адресат) имеет точно такое же свойство, как и источник, при true
перезаписи, в противном случае оставить это свойство нетронутым и пропустить.
Мы знаем, что объекты не могут иметь одно и то же свойство дважды. В парах ключ-значение (объектах) ключи должны быть уникальными.
В Express перезапись выполняется false
.
Ниже приведены основные правила обслуживания, всегда обрабатывайте ожидаемые ошибки:
if (!destination) { throw new TypeError('The `destination` argument is required.'); } if (!source) { throw new TypeError('The `source` argument is required.'); }
Вот тут начинается самое интересное: строка 12.
for (const name of Object.getOwnPropertyNames(source)) {
Из вышесказанного мы знаем, что означает OwnProperty
, поэтому getOwnPropertyNames
явно означает получение ключей собственных свойств.
const person = { // data name: "Jane", age: 0, // behavior grow() { this.age += 1; } }; Object.getOwnPropertyNames(person); // [ 'name', 'age', 'grow' ]
Он возвращает ключи в виде массива, и мы перебираем эти ключи в следующем примере:
for (const name of Object.getOwnPropertyNames(source)) {
Далее проверяется, имеют ли пункт назначения и источник тот же ключ, по которому мы в данный момент просматриваем цикл:
if (!overwrite && Object.hasOwn(destination, name)) { // Skip descriptor continue; }
Если overwrite ложно, пропустите это свойство; не перезаписывайте. Именно это и делает continue
— он переводит цикл на следующую итерацию и не запускает код ниже, который является следующим кодом:
// Copy descriptor const descriptor = Object.getOwnPropertyDescriptor(source, name); Object.defineProperty(destination, name, descriptor);
Мы уже знаем, что означает getOwnProperty
. Новое слово — descriptor
. Давайте проверим эту функцию на нашем собственном объекте person
:
const person = { // data name: "Jane", age: 0, // behavior grow() { this.age += 1; } }; Object.getOwnPropertyDescriptor(person, "grow"); // { // value: [Function: grow], // writable: true, // enumerable: true, // configurable: true // }
Он возвращает нашу функцию grow
в качестве значения, а следующая строка не требует пояснений:
Object.defineProperty(destination, name, descriptor);
Он берет наш дескриптор из источника и записывает его в место назначения. Он копирует собственные свойства источника в наш объект назначения как свои собственные свойства.
Давайте рассмотрим пример на примере нашего объекта person
:
const val = { value: function isAlien() { return false; }, enumerable: true, writable: true, configurable: true, }; Object.defineProperty(person, "isAlien", val);
Теперь person
должно быть определено свойство isAlien
.
Подводя итог, можно сказать, что этот часто загружаемый модуль копирует собственные свойства из исходного объекта в целевой с возможностью перезаписи.
Мы успешно завершили работу над нашим первым модулем на этом уровне исходного кода , и нас ждет еще более интересное.
Это было введение. Мы начали с изучения основ, необходимых для понимания модуля, и, как побочный продукт, понимания шаблонов в большинстве модулей, то есть композиции объектов и наследования. Наконец, мы прошлись по модулю merge-descriptors
.
Этот шаблон будет преобладать в большинстве статей. Если я считаю, что есть необходимые основы для освещения, мы рассмотрим их в первом разделе, а затем рассмотрим исходный код.
К счастью, merge-descriptors
используется в Express, который является нашим фокусом в качестве стартового исследования исходного кода. Так что ждите больше статей об исходном коде Express.js, пока мы не почувствуем, что у нас достаточно хорошо получилось использовать Express, а затем переключимся на другой модуль или инструмент, например Node.js.
Что вы можете сделать в это время в качестве испытания, так это перейти к тестовому файлу в merge descriptors, прочитать весь файл. Чтение исходного кода самостоятельно важно, попробуйте выяснить, что он делает и тестирует, затем сломайте его, да и исправьте его снова или добавьте больше тестов!
Если вас интересует более эксклюзивный практический и объемный контент для совершенствования навыков программирования, вы можете найти больше информации на Ko-fi .