Как перейти к предыдущему/следующему элементу массива

Скажем, у нас есть список целых чисел:

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

Ответы

Ответ 1

Если вы хотите сохранить список как 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.

Ответ 2

Обычно я рекомендую не добавлять вещи в 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).

Ответ 3

Аспект 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);
}