Вступление
Всем доброго времени суток. Я думаю, вы заметили, что в массивах в JS довольно много хороших встроенных методов. Однако, зачастую, даже их не хватает. Например: мне бы хотелось проверять массивы на пересечение. Конечно, я могу писать так:
checkIntersections(arr1, arr2)
но гораздо удобнее было бы писать вот так:
arr1.checkIntersections(arr2)
Особенно это касается ситуаций, когда функция должна изменять массив, например удалять повторяющиеся элементы. В таком случае нам вообще придется записывать что-то вроде этого:
arr1 = deleteDuplicates(arr1);
Мне такая запись вообще не нравится. Она плохо читаема и не логична. Нужно сделать deleteDuplicates — встроенным методом массива. Но как нам добавить свой метод? Ведь, как многие считают, массив — это неизменяемая часть языка, наподобие операторов. К счастью это не так.
Исследуем массивы
Чтобы понять, как нам взаимодействовать с массивами. Мы для начала должны понять, что из себя представляет сам массив. Для этого давайте выведем его в консоль:
let arr = ["I", "love", "JS"];
console.log(arr);
Вот, что мы увидим:
Как видите, консоль показывает нам, что это массив с 3-я элементами. Но также мы видим, что рядом с ним есть кнопка раскрытия (треугольник). Если мы нажмем на нее, то увидим следующее:
Ничего не напоминает? Это же объект! Мы видим, что его свойствами являются числовые ключи и length. Поэтому мы и записываем: array[0]. Мы обращаемся к свойству объекта array с именем 0. Квадратные скобки нужны, поскольку это имя этого свойства представлено числом. А что это за «Array(3)»? Это строка показывает, экземпляром какого объекта является массив, то есть, с помощью какого класса он создан. Значит, все массивы — это экземпляры класса Array. Обладая этими знаниями, давайте попробуем записать какой-нибудь свой метод.
Добавляем новый метод
Давайте сделаем метод, который будет проверять, есть ли в нашем массиве элемент переданный аргументом. Для этого в цикле переберем все элементы нашего массива и вернем true при обнаружении совпадения. Если по завершении цикла совпадение не будет обнаружено — вернем false. Назовем этот метод checkElement. Итак, чтобы добавить его к массиву, обратимся к свойству prototype класса Array.
Array.prototype.checkElement = function(e) {
};
Теперь пройдемся циклом по всем элементам. Сделать это очень просто:
for (var i = 0; i < this.length; i++) {
this[i];
}
Теперь, на каждом шаге цикла будем выполнять проверку на совпадение переданного элемента с элементом текущей итерации.
if (this[i] === e) {
return true;
}
И наконец, если по завершении цикла совпадений не было обнаружено, вернем false. Вот, что мы получим в итоге.
Array.prototype.checkElement = function(e) {
for (var i = 0; i < this.length; i++) {
if (this[i] === e) {
return true;
}
}
return false;
};
Давайте проверим его в действии.
console.log(arr.checkElement("I"));
console.log(arr.checkElement("not"));
Все работает, но если бы все было так просто, было бы подозрительно.
Исправляем появившуюся ошибку
Давайте попробуем сделать следующее: переберем наш массив с помощью цикла for… in.
for (let i in arr) {
console.log(i);
}
Как видите, произошел косяк: JS стал видит наш новый метод в этом цикле. Разумеется, при использовании такого цикла в коде, это может привести к ошибке. Но почему JS видит наш метод, но не видит например метод push? Давайте вновь обратимся к консоли и выведем все методы нашего массива. Вот так:
console.log(arr.__proto__);
Вот, что мы увидим:
Как видите: встроенный методы отобразились более тусклым текстом чем наш метод. Это означает, что эти методы обладают особым свойством, которое делает их невидимыми для цикла for… in. Но что это за свойство? Если мы прочитаем документацию о технологии Object.defineProperty на MDN (вот она), то заметим там параметр enumerable. Как мы можем узнать, если его значение установить как false, цикл for… in не будет видеть свойство с этим параметром. Давайте так и сделаем, а чтобы не писать миллион одинаковых инструкций, применим ее ко всем методам.
Вот так:
Object.keys(Array.prototype).forEach(method => {
Object.defineProperty(Array.prototype, method, {
enumerable: false,
});
});
Здесь мы получаем массив ключей нашего объекта Array.prototype и, с помощью Object.defineProperty, делаем их невидимыми для цикла for… in. Давайте проверим теперь.
console.log(arr.checkElement("I"));
for (let i in arr) {
console.log(i);
}
Прекрасно! Все работает так, как мы и ожидали.
Итог
Итак, вот код, который у нас получился.
Array.prototype.checkElement = function(e) {
for (var i = 0; i < this.length; i++) {
if (this[i] === e) {
return true;
}
}
return false;
};
Object.keys(Array.prototype).forEach(method => {
Object.defineProperty(Array.prototype, method, {
enumerable: false,
});
});
Как видите, ничего сложного, но может очень сильно облегчить жизнь. Кроме того, можно добавлять кучу других методов под свои нужды.
Я думаю, таким же методом можно изменять и другие технологии в JS. Главное — понять как они устроены.
Sabubu
Абсолютно тупая идея. Вы не думаете о других людях, которым потом придется работать с кодом.
При таком подходе получаются побочные эффекты: вы импортируете модуль, а он добавляет там что-то в прототипе. Как потом в таком коде разбираться?
Не нужно это. Просто сделайте обычную функцию в обычном модуле и не заставляйте людей ломать голову и искать, где вы что переопределили.
rafuck
Даже не в этом дело. Через N лет Emascript X.Y определит в стандартной библиотеке метод с таким же (а, еще хуже, — с похожим названием). И получится… ну… то, что получится.
Alexandroppolus
Хуже всего — если с таким же названием, но чуть другой логикой или набором аргументов.
rafuck
Да. Это я и имел в виду, но, мне кажется, еще хуже, когда названия семантически схожи (или даже схожи с точностью до регистра букв), а логика разная. Это еще сильнее затруднит отладку
OlegPatron92
Почему не написать свой FooBusBassArray объект который будет наследоваться от Array и работать также но будет иметь дополнительные методы. Это проще задокументировать и щадительнее для разработчиков.