Вопрос:
Скажем, у нас есть список целых чисел:
var fibonacci = [1,1,2,3,5,8,13,21];
Я хочу иметь возможность получить следующий и предыдущий элемент (просто для перемещения указателя элемента без изменения массива) следующим образом (например, может пройти без прототипа, чтобы переопределить интерфейс Array, но почему бы и нет):
fibonacci.prev(); // returns false fibonacci.next(); // returns 1 fibonacci.next(); // returns 1 fibonacci.next(); // returns 2 fibonacci.next(); // returns 3 fibonacci.next(); // returns 5 fibonacci.next(); // returns 8 fibonacci.prev(); // returns 5 fibonacci.next(); // returns 8 fibonacci.next(); // returns 13 fibonacci.next(); // returns false Лучший ответ:
Если вы хотите сохранить список как Array, вам придется изменить его [[prototype]], чтобы он выглядел как итеративная коллекция:
Array.prototype.next = function() { return this[++this.current]; }; Array.prototype.prev = function() { return this[—this.current]; }; Array.prototype.current = 0;
Теперь каждый Array будет иметь методы prev и next и свойство current, которое указывает на “текущие” элементы. Оговорка: свойство current может быть изменено, что приводит к непредсказуемым результатам.
Post scriptum: я не рекомендую делать prev и next return false, когда индекс выходит за пределы диапазона. Если вы действительно этого хотите, вы можете изменить методы на что-то вроде:
Array.prototype.next = function() { if (!((this.current + 1) in this)) return false; return this[++this.current]; };
ОБНОВЛЕНИЕ в середине 2016 года
Я обновляю этот ответ, потому что кажется, что он все еще получает мнения и голоса. Я должен был уточнить, что данный ответ является доказательством концепции и в целом продление прототипа родных классов является плохой практикой, и его следует избегать в производственных проектах.
В частности, это не так много, потому что это будет беспорядок с циклами for…in, которые всегда следует избегать для массивов, и это определенно плохая практика для итерации через их элементы, а также потому, что с IE9 мы можем надежно сделать это вместо этого
Object.defineProperty(Array.prototype, «next», { value: function() { return this[++this.current]; }, enumerable: false });
Основная проблема заключается в том, что расширение родных классов не является будущим, т.е. может случиться так, что ECMA представит метод next для массивов, который, вероятно, будет несовместим с вашей реализацией. Это уже произошло даже с очень распространенными структурами JS – последний случай был расширение массива MooTools contains, которое привело ECMA, чтобы изменить имя на includes (плохой ход, IMO, поскольку у нас уже есть contains в DOMTokenList, такие как Element.classList).
Если не сказать, что вы не должны распространять собственные прототипы, но вы должны знать, что делаете. Первый совет, который я могу вам дать, – это выбрать имена, которые не будут сталкиваться с будущими стандартными расширениями, например. myCompanyNext вместо next. Это будет стоить вам некоторой элегантности кода, но заставит вас спать.
Еще лучше, в этом случае вы можете эффективно расширить класс Array:
function MyTraversableArray() { if (typeof arguments[0] === «number») this.length = arguments[0]; else this.push.apply(this, arguments); this.current = 0; } MyTraversableArray.prototype = []; MyTraversableArray.prototype.constructor = MyTraversableArray; MyTraversableArray.prototype.next = function() { return this[++this.current]; }; MyTraversableArray.prototype.prev = function() { return this[—this.current]; };
В ES6, кроме того, проще расширить родные классы:
class MyTraversableArray extends Array { next() { return this[++this.current]; } }
Увы, у транспилеров трудное время с расширениями родного класса, и Babel удалил его поддержку. Но это потому, что они не могут точно воспроизвести некоторые виды поведения, которые не имеют никакого влияния в нашем случае, поэтому вы можете придерживаться вышеуказанного старого кода ES3.
Ответ №1
Обычно я рекомендую не добавлять вещи в Array.prototype из-за количества действительно плохого JavaScript там. Например, если вы установили Array.protoype.next = function () {}, и у кого-то есть следующий код, тогда возникает проблема:
var total = 0, i, myArr = [0,1,2]; for(i in myArr) { total += myArr[i]; } total; //is probably «3next»
Это плохое использование циклов for-in вызывает тревогу. Поэтому вы просите о проблемах, добавив прототип Array. Тем не менее, довольно легко создать оболочку, чтобы сделать то, что вы хотите сделать:
var iterifyArr = function (arr) { var cur = 0; arr.next = (function () { return (++cur >= this.length) ? false : this[cur]; }); arr.prev = (function () { return (—cur < 0) ? false : this[cur]; }); return arr; }; var fibonacci = [1, 1, 2, 3, 5, 8, 13]; iterifyArr(fibonacci); fibonacci.prev(); // returns false fibonacci.next(); // returns 1 fibonacci.next(); // returns 1 fibonacci.next(); // returns 2 fibonacci.next(); // returns 3 fibonacci.next(); // returns 5 fibonacci.next(); // returns 8 fibonacci.prev(); // returns 5 fibonacci.next(); // returns 8 fibonacci.next(); // returns 13 fibonacci.next(); // returns false
Несколько примечаний:
Прежде всего, вы, вероятно, захотите вернуть undefined вместо false, если вы пройдете мимо конца. Во-вторых, поскольку этот метод скрывает cur с использованием замыкания, у вас нет доступа к нему в вашем массиве. Таким образом, вы можете иметь метод cur() для захвата текущего значения:
//Inside the iterifyArr function: //… arr.cur = (function () { return this[cur]; }); //…
Наконец, ваши требования неясны о том, как далеко до конца поддерживается “указатель”. Возьмем следующий код, например (предполагается, что fibonacci установлен как указано выше):
fibonacci.prev(); //false fibonacci.prev(); //false fibonacci.next(); //Should this be false or 1?
В моем коде это будет false, но вы можете захотеть, чтобы он был 1, и в этом случае вам пришлось бы сделать пару простых изменений в моем коде.
О, и потому, что функция возвращает arr, вы можете “итерировать” массив в той же строке, что и вы его определяете, например:
var fibonacci = iterifyArr([1, 1, 2, 3, 5, 8, 13]);
Это может сделать вещи немного чище для вас. Вы также можете reset итератор путем повторного вызова iterifyArr в вашем массиве, или вы можете легко написать метод для reset (просто установите cur на 0).
Ответ №2
Аспект next теперь встроен в массивы, потому что с ES2015 массивы являются итерабельными, что означает, что вы можете получить итератор для них, у которого есть метод next (но продолжайте читать для части “prev” ):
const a = [1, 2, 3, 4, 5]; const iter = a[Symbol.iterator](); let result; while (!(result = iter.next()).done) { console.log(result.value); }
Итераторы идут только вперед, но не в обоих направлениях. И, конечно, вы обычно не используете итератор явно, вы обычно используете его как часть некоторой итерационной конструкции, например for-of:
const a = [1, 2, 3, 4, 5]; for (const value of a) { console.log(value); }
Вы можете легко дать себе двухсторонний итератор:
-
Выполняя автономную функцию, которая принимает массив и возвращает итератор, или
-
Подклассификация Array и переопределение итератора в подклассе или
-
Заменив итератор по умолчанию Array на свой собственный (просто убедитесь, что он работает точно так же, как и по умолчанию при переходе!)
Вот пример с подклассом:
class MyArray extends Array { // Define the iterator function for this class [Symbol.iterator]() { // `index` points at the next value we’ll return let index = 0; // Return the iterator return { // `next` returns the next next: () => { const done = index >= this.length; const value = done ? undefined : this[index++]; return { value, done }; }, // `prev` returns the previous prev: () => { const done = index == 0; const value = done ? undefined : this[—index]; return { value, done }; } }; } } // Demonstrate usage: const a = new MyArray(«a», «b»); const i = a[Symbol.iterator](); console.log(«next», JSON.stringify(i.next())); console.log(«next», JSON.stringify(i.next())); console.log(«next», JSON.stringify(i.next())); console.log(«prev», JSON.stringify(i.prev())); console.log(«prev», JSON.stringify(i.prev())); console.log(«prev», JSON.stringify(i.prev())); console.log(«next», JSON.stringify(i.next()));.as-console-wrapper { max-height: 100% !important; }