Как найти первый элемент массива, соответствующий логическому условию в JavaScript?

220

Мне интересно, есть ли известный, встроенный / элегантный способ найти первый элемент массива JS, соответствующий заданному условию. Эквивалентом AC # будет List.Find .

До сих пор я использовал двухфункциональную комбинацию, подобную этой:

// Returns the first element of an array that satisfies given predicate
Array.prototype.findFirst = function (predicateCallback) {
    if (typeof predicateCallback !== 'function') {
        return undefined;
    }

    for (var i = 0; i < arr.length; i++) {
        if (i in this && predicateCallback(this[i])) return this[i];
    }

    return undefined;
};

// Check if element is not undefined && not null
isNotNullNorUndefined = function (o) {
    return (typeof (o) !== 'undefined' && o !== null);
};

И тогда я могу использовать:

var result = someArray.findFirst(isNotNullNorUndefined);

Но поскольку в ECMAScript так много методов массивов функционального стиля , возможно, что-то уже есть? Я полагаю, что многие люди должны все время реализовывать подобные вещи ...

Якуб П.
источник
6
Там нет встроенного метода, но есть служебные библиотеки, которые приближают эту функциональность, такие как documentcloud.github.com/underscore
kinakuta
Underscore.js выглядит очень красиво! И это найти (). Спасибо!
Якуб П.
1
Просто чтобы вы знали, вы можете уменьшить это: return (typeof (o) !== 'undefined' && o !== null);до этого return o != null;. Они в точности эквивалентны.
Скалы безумия
1
Хорошо знать. Но вы знаете, я не доверяю операторам принуждения, как! = Или ==. Я даже не смог бы легко проверить это, так как мне нужно было бы как-то проверить, что нет другого значения, которое принудительно обнуляется таким образом ... :) Так что мне повезло, что у меня есть библиотека, которая позволяла бы мне вообще убрать эту функцию ... :)
Якуб П.
Я честно скажу, что это довольно элегантное решение. Самая близкая вещь, которую я могу найти, это Array.prototype.some, который пытается найти, удовлетворяет ли какой-либо элемент заданному условию, которое вы передаете ему в форме функции. К сожалению, это возвращает логическое значение вместо индекса или элемента. Я бы порекомендовал ваше решение использовать библиотеку, поскольку библиотеки, как правило, намного больше и содержат вещи, которые вы не будете использовать, и я предпочитаю держать вещи легкими (поскольку вы можете использовать только одну функцию из предоставляемого ею комплекта).
Грэм Робертсон

Ответы:

220

Начиная с ES6 существует нативный findметод для массивов; это останавливает перечисление массива, как только он находит первое совпадение и возвращает значение.

const result = someArray.find(isNotNullNorUndefined);

Старый ответ:

Я должен опубликовать ответ, чтобы остановить эти filterпредложения :-)

поскольку в ECMAScript так много методов массива в функциональном стиле, может быть, есть что-то подобное?

Вы можете использовать someметод Array для итерации массива до тех пор, пока не будет выполнено условие (и затем остановитесь). К сожалению, он вернет только то, было ли условие выполнено один раз, а не по какому элементу (или по какому индексу) оно было выполнено. Поэтому мы должны немного изменить его:

function find(arr, test, ctx) {
    var result = null;
    arr.some(function(el, i) {
        return test.call(ctx, el, i, arr) ? ((result = el), true) : false;
    });
    return result;
}

var result = find(someArray, isNotNullNorUndefined);
Берги
источник
28
Не могу сказать, что полностью понимаю всю неприязнь к фильтру (). Это может быть медленнее, но на самом деле; в большинстве случаев, когда это, вероятно, используется, для начала это небольшой список, и большинство приложений JavaScript не достаточно сложны, чтобы действительно беспокоиться об эффективности на этом уровне. [] .filter (test) .pop () или [] .filter (test) [0] являются простыми, встроенными и читаемыми. Конечно, я имею в виду деловые приложения или веб-сайты, а не интенсивные приложения, такие как игры.
Джош Мак
11
Фильтрующие решения пересекают весь массив / коллекции? Если это так, фильтрация очень неэффективна, потому что она работает по всему массиву, даже если найденное значение является первым в коллекции. some()с другой стороны, возвращается немедленно, что намного быстрее почти во всех случаях, чем фильтрация решений.
Алик Эльзин-килака
@ AlikElzin-kilaka: Да, именно так.
Берги
15
@JoshMc конечно, но имеет смысл не быть безвозмездно неэффективным при публикации решения простой проблемы где-то вроде переполнения стека. Многие люди будут копировать и вставлять код отсюда в служебную функцию, и некоторые из них, в какой-то момент, в конечном итоге будут использовать эту служебную функцию в контексте, где производительность имеет значение, не задумываясь о реализации. Если вы дали им что-то, что имеет эффективную реализацию для начала, вы либо решили проблему с производительностью, которой иначе не было бы, либо сэкономили им кучу времени на диагностику.
Марк Амери
1
@SuperUberDuper: Нет. См. Ответ Марка Эмери ниже.
Берги
105

Начиная с ECMAScript 6, вы можете использовать Array.prototype.findдля этого. Это реализовано и работает в Firefox (25.0), Chrome (45.0), Edge (12) и Safari (7.1), но не в Internet Explorer или ряде других старых или необычных платформ .

Например, выражение ниже оценивается как 106.

[100,101,102,103,104,105,106,107,108,109].find(function (el) {
    return el > 105;
});

Если вы хотите использовать это прямо сейчас, но нуждаетесь в поддержке IE или других неподдерживающих браузеров, вы можете использовать прокладку. Я рекомендую шим es6 . MDN также предлагает шимм, если по какой-то причине вы не хотите использовать весь эс6 шим в своем проекте. Для максимальной совместимости вы хотите использовать es6-shim, потому что в отличие от версии MDN он обнаруживает ошибочные собственные реализации findи перезаписывает их (см. Комментарий, который начинается «Обойти ошибки в Array # find и Array # findIndex» и строки, следующие непосредственно за ней) ,

Марк Эмери
источник
findлучше, чем, filterтак как findостанавливается немедленно, когда находит элемент, соответствующий условию, в то время как filterциклически перебирает все элементы, чтобы получить все соответствующие элементы.
Ань Чан
60

Как насчет использования фильтра и получения первого индекса из полученного массива?

var result = someArray.filter(isNotNullNorUndefined)[0];
Фил Мандер
источник
6
Продолжайте использовать методы es5. var result = someArray.filter (isNotNullNorUndefined) .shift ();
Someyoungideas
Несмотря на то, что я сам проголосовал за поиск ответа выше @Bergi, я думаю, что с деструктуризацией ES6 мы можем немного улучшиться: var [result] = someArray.filter (isNotNullNorUndefined);
Накуль Манчанда
@someyoungideas не могли бы вы объяснить преимущества использования .shiftздесь?
Якубисон
3
@jakubiszon Преимущество использования в shiftтом, что оно «выглядит умным», но на самом деле более запутанно. Кто бы мог подумать, что вызов shift()без аргументов будет таким же, как и первый элемент? Непонятно ИМО. В любом случае доступ к массиву быстрее: jsperf.com/array-access-vs-shift
Джош М.
Кроме того, обозначение в квадратных скобках доступно как для объектов, так и для массивов в ES5 AFAIK, никогда не было предпочтения .shift()перед [0]явно указанным, как это. Несмотря на это, это альтернатива, которую вы можете использовать или нет, но я бы придерживался [0].
SidOfc
15

К настоящему времени должно быть ясно, что JavaScript не предлагает такого решения изначально; Вот два ближайших производных, самый полезный первый:

  1. Array.prototype.some(fn)предлагает желаемое поведение остановки при выполнении условия, но возвращает только наличие элемента; нетрудно применить некоторые хитрости, такие как решение, предложенное ответом Берги .

  2. Array.prototype.filter(fn)[0]делает для отличного однострочного, но наименее эффективным, потому что вы выбрасываете N - 1элементы только для того, чтобы получить то, что вам нужно.

Традиционные методы поиска в JavaScript характеризуются возвращением индекса найденного элемента вместо самого элемента или -1. Это избавляет от необходимости выбирать возвращаемое значение из домена всех возможных типов; индекс может быть только числом, а отрицательные значения недопустимы.

Оба решения выше не поддерживают поиск смещения, поэтому я решил написать это:

(function(ns) {
  ns.search = function(array, callback, offset) {
    var size = array.length;

    offset = offset || 0;
    if (offset >= size || offset <= -size) {
      return -1;
    } else if (offset < 0) {
      offset = size - offset;
    }

    while (offset < size) {
      if (callback(array[offset], offset, array)) {
        return offset;
      }
      ++offset;
    }
    return -1;
  };
}(this));

search([1, 2, NaN, 4], Number.isNaN); // 2
search([1, 2, 3, 4], Number.isNaN); // -1
search([1, NaN, 3, NaN], Number.isNaN, 2); // 3
Разъем
источник
Похоже, наиболее полный ответ. Можете ли вы добавить третий подход в своем ответе?
Mrusful
13

Резюме:

  • Для нахождения первого элемента в массиве, который соответствует логическому условию, мы можем использовать ES6 find()
  • find()расположен Array.prototypeтаким образом, чтобы его можно было использовать в каждом массиве.
  • find()принимает обратный вызов, когда booleanпроверяется условие Функция возвращает значение (не индекс!)

Пример:

const array = [4, 33, 8, 56, 23];

const found = array.find((element) => {
  return element > 50;
});

console.log(found);   //  56

Виллем ван дер Веен
источник
8

Если вы используете, underscore.jsвы можете использовать его findи indexOfфункции, чтобы получить именно то, что вы хотите:

var index = _.indexOf(your_array, _.find(your_array, function (d) {
    return d === true;
}));

Документация:

Мэтт Вэлк
источник
Используйте опцию underscorejs, только если это необходимо, потому что загружать библиотеку только для этого просто не стоит
DannyFeliz
4

По состоянию на ES 2015, Array.prototype.find()обеспечивает эту точную функциональность.

Для браузеров, которые не поддерживают эту функцию, в Mozilla Developer Network предусмотрена поли-заливка (вставлено ниже):

if (!Array.prototype.find) {
  Array.prototype.find = function(predicate) {
    if (this === null) {
      throw new TypeError('Array.prototype.find called on null or undefined');
    }
    if (typeof predicate !== 'function') {
      throw new TypeError('predicate must be a function');
    }
    var list = Object(this);
    var length = list.length >>> 0;
    var thisArg = arguments[1];
    var value;

    for (var i = 0; i < length; i++) {
      value = list[i];
      if (predicate.call(thisArg, value, i, list)) {
        return value;
      }
    }
    return undefined;
  };
}
Кевин Ли Гарнер
источник
2
foundElement = myArray[myArray.findIndex(element => //condition here)];
dotista2008
источник
3
Пример из реальной жизни с условным условием и некоторыми пояснительными словами сделает ваш ответ более ценным и понятным.
SaschaM78
0

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

Использование: (давая значение "Второй")

var defaultItemValue = { id: -1, name: "Undefined" };
var containers: Container[] = [{ id: 1, name: "First" }, { id: 2, name: "Second" }];
GetContainer(2).name;

Реализация:

class Container {
    id: number;
    name: string;
}

public GetContainer(containerId: number): Container {
  var comparator = (item: Container): boolean => {
      return item.id == containerId;
    };
    return this.Get<Container>(this.containers, comparator, this.defaultItemValue);
  }

private Get<T>(array: T[], comparator: (item: T) => boolean, defaultValue: T): T {
  var found: T = null;
  array.some(function(element, index) {
    if (comparator(element)) {
      found = element;
      return true;
    }
  });

  if (!found) {
    found = defaultValue;
  }

  return found;
}
Хенрик
источник
-2

В Javascript нет встроенной функции для выполнения этого поиска.

Если вы используете JQuery, вы можете сделать jQuery.inArray(element,array).

PedroSena
источник
Это тоже сработало бы, хотя я пойду с Underscore, наверное :)
Jakub P.
3
Это не удовлетворяет выводу того, что требуется запрашивающему (нужен элемент с некоторым индексом, а не логическое значение).
Грэм Робертсон
@GrahamRobertson $.inArrayне возвращает логическое значение, а (удивительно!) Возвращает индекс первого соответствующего элемента. Тем не менее, он по-прежнему не выполняет то, о чем просил ОП.
Марк Амери
-2

Менее элегантный способ, который будет обрабатывать throwвсе правильные сообщения об ошибках (на основе Array.prototype.filter), но прекратит повторять первый результат:

function findFirst(arr, test, context) {
    var Result = function (v, i) {this.value = v; this.index = i;};
    try {
        Array.prototype.filter.call(arr, function (v, i, a) {
            if (test(v, i, a)) throw new Result(v, i);
        }, context);
    } catch (e) {
        if (e instanceof Result) return e;
        throw e;
    }
}

Тогда примеры

findFirst([-2, -1, 0, 1, 2, 3], function (e) {return e > 1 && e % 2;});
// Result {value: 3, index: 5}
findFirst([0, 1, 2, 3], 0);               // bad function param
// TypeError: number is not a function
findFirst(0, function () {return true;}); // bad arr param
// undefined
findFirst([1], function (e) {return 0;}); // no match
// undefined

Это работает, заканчивая filterиспользованием throw.

Пол С.
источник