Como encontrar o primeiro elemento da matriz que corresponde a uma condição booleana no JavaScript?

218

Gostaria de saber se existe uma maneira conhecida, embutida / elegante de encontrar o primeiro elemento de uma matriz JS que corresponda a uma determinada condição. AC # equivalente seria List.Find .

Até agora, eu tenho usado uma combinação de duas funções como esta:

// 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);
};

E então eu posso usar:

var result = someArray.findFirst(isNotNullNorUndefined);

Mas como existem muitos métodos de matriz de estilo funcional no ECMAScript , talvez já exista algo como esse? Eu imagino que muitas pessoas precisam implementar coisas assim o tempo todo ...

Jakub P.
fonte
6
Não há um construído no método, mas existem bibliotecas de utilidade que aproximada essa funcionalidade, tal como documentcloud.github.com/underscore
kinakuta
Underscore.js parece muito bom mesmo! E tem find (). Obrigado!
Jakub P.
1
Só para você saber, você pode reduzir isso: return (typeof (o) !== 'undefined' && o !== null);até isso return o != null;. Eles são exatamente equivalentes.
falésias de insanidade
1
Bom saber. Mas você sabe, desconfio dos operadores de coação como! = Ou ==. Eu nem seria capaz de testá-lo facilmente, pois precisaria verificar de alguma forma que não há outro valor que seja coagido a nulo dessa maneira ... :) Então, como sou sortuda por ter uma biblioteca que permite me para remover essa função por completo ... :)
Jakub P.
Honestamente, tenho que dizer que essa é uma solução bastante elegante. A coisa mais próxima que posso encontrar é Array.prototype.some, que tenta descobrir se algum elemento satisfaz uma determinada condição que você passa para ele na forma de uma função. Infelizmente, isso retorna um booleano em vez do índice ou do elemento. Eu recomendaria sua solução sobre o uso de uma biblioteca, pois as bibliotecas tendem a ser muito maiores e contêm itens que você não usará e prefiro manter as coisas leves (já que você pode usar apenas a única função fora do conjunto que ele fornece).
Graham Robertson

Respostas:

217

Desde o ES6, existe o findmétodo nativo para matrizes; isso para de enumerar a matriz depois de encontrar a primeira correspondência e retornar o valor.

const result = someArray.find(isNotNullNorUndefined);

Resposta antiga:

Tenho que postar uma resposta para interromper essas filtersugestões :-)

Como existem muitos métodos de matriz de estilo funcional no ECMAScript, talvez já exista algo como esse?

Você pode usar o somemétodo Array para iterar a matriz até que uma condição seja atendida (e depois pare). Infelizmente, ele retornará apenas se a condição foi atendida uma vez, não por qual elemento (ou em qual índice) foi atendida. Então, temos que corrigi-lo um pouco:

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);
Bergi
fonte
28
Não posso dizer que entendo completamente toda a antipatia dirigida a filter (). Pode ser mais lento, mas na realidade; na maioria dos casos em que isso provavelmente é usado, é uma pequena lista para começar, e a maioria dos aplicativos JavaScript não é complicada o suficiente para realmente se preocupar com a eficiência nesse nível. [] .filter (teste) .pop () ou [] .filter (teste) [0] são simples, nativos e legíveis. Claro que estou falando de aplicativos de negócios ou sites, não aplicativos intensivos, como jogos.
Josh Mc
11
As soluções de filtro atravessam toda a matriz / coleções? Nesse caso, a filtragem é muito ineficiente, porque é executada em toda a matriz, mesmo que o valor encontrado seja o primeiro da coleção. some()por outro lado, retorna imediatamente, o que é muito mais rápido em quase todos os casos do que as soluções de filtragem.
AlikElzin-kilaka 15/07
@ AlikElzin-kilaka: Sim, exatamente.
Bergi 15/07/2015
15
@ JoshMc, com certeza, mas faz sentido não ser gratuito ineficiente ao postar uma solução para um problema simples em algum lugar como Stack Overflow. Muitas pessoas copiam e colam o código daqui em uma função de utilitário, e algumas delas, em algum momento, acabam usando essa função de utilitário em um contexto em que o desempenho é importante sem pensar na implementação. Se você deu a eles algo que possui uma implementação eficiente, você resolveu um problema de desempenho que eles não teriam ou economizou um tempo de desenvolvimento para diagnosticá-lo.
Mark Amery
1
@SuperUberDuper: Não. Veja a resposta de Mark Amery abaixo.
Bergi 27/08/16
104

A partir do ECMAScript 6, você pode usar Array.prototype.findpara isso. Isso é implementado e funciona no Firefox (25.0), Chrome (45.0), Edge (12) e Safari (7.1), mas não no Internet Explorer ou em várias outras plataformas antigas ou incomuns .

Por exemplo, a expressão abaixo é avaliada como 106.

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

Se você quiser usar isso agora, mas precisar de suporte para o IE ou outros navegadores não compatíveis, use um calço. Eu recomendo o es6-shim . O MDN também oferece um calço se, por algum motivo, você não quiser colocar todo o es6-shim em seu projeto. Para obter compatibilidade máxima, você deseja o es6-shim, porque, ao contrário da versão MDN, ele detecta implementações nativas de bugs finde as substitui (consulte o comentário que começa em "Como solucionar bugs na matriz # find e Array # findIndex" e nas linhas imediatamente a seguir) .

Mark Amery
fonte
findé melhor do que filterdesde que findpara imediatamente quando encontra um elemento correspondente à condição, enquanto filterpercorre todos os elementos para fornecer todos os elementos correspondentes.
Anh Tran
59

Que tal usar filtro e obter o primeiro índice da matriz resultante?

var result = someArray.filter(isNotNullNorUndefined)[0];
Phil Mander
fonte
6
Continue usando os métodos es5. var resultado = someArray.filter (isNotNullNorUndefined) .shift ();
someyoungideas
Embora eu tenha sido votado por encontrar a resposta acima na @ Bergi, acho que com a desestruturação do ES6 podemos melhorar um pouco acima: var [result] = someArray.filter (isNotNullNorUndefined);
Nakul Manchanda
@someyoungideas você poderia explicar o benefício de usar .shiftaqui?
precisa saber é o seguinte
3
@jakubiszon O benefício do uso shifté que "parece inteligente", mas na verdade é mais confuso. Quem pensaria que chamar shift()sem argumentos seria o mesmo que pegar o primeiro elemento? Não está claro IMO. De qualquer forma, o acesso à matriz é mais rápido: jsperf.com/array-access-vs-shift
Josh M.
Além disso, a notação de colchete está disponível para objetos e matrizes no ES5 AFAIK, nunca vi uma preferência .shift()sobre [0]explicitamente declarada dessa maneira. Apesar disso, é uma alternativa que você pode optar por usar ou não [0].
SidOfc 16/03
15

Já deve estar claro que o JavaScript não oferece essa solução nativamente; Aqui estão as duas derivadas mais próximas, as mais úteis primeiro:

  1. Array.prototype.some(fn)oferece o comportamento desejado de parar quando uma condição é atendida, mas retorna apenas se um elemento está presente; não é difícil aplicar alguns truques, como a solução oferecida pela resposta de Bergi .

  2. Array.prototype.filter(fn)[0]cria um ótimo one-liner, mas é o menos eficiente, porque você joga fora os N - 1elementos apenas para conseguir o que precisa.

Os métodos de pesquisa tradicionais em JavaScript são caracterizados pelo retorno do índice do elemento encontrado em vez do próprio elemento ou -1. Isso evita ter que escolher um valor de retorno do domínio de todos os tipos possíveis; um índice pode ser apenas um número e valores negativos são inválidos.

As duas soluções acima também não oferecem suporte à pesquisa de deslocamento, então decidi escrever o seguinte:

(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
Ja͢ck
fonte
Parece a resposta mais abrangente. Você pode adicionar uma terceira abordagem na sua resposta?
precisa saber é o seguinte
13

Resumo:

  • Para encontrar o primeiro elemento em uma matriz que corresponda a uma condição booleana, podemos usar o ES6 find()
  • find()está localizado Array.prototypepara que possa ser usado em todas as matrizes.
  • find()recebe um retorno de chamada onde uma booleancondição é testada. A função retorna o valor (não o índice!)

Exemplo:

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

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

console.log(found);   //  56

Willem van der Veen
fonte
8

Se você estiver usando, underscore.jspoderá usar as funções finde indexOfpara obter exatamente o que deseja:

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

Documentação:

Matt Woelk
fonte
Use a opção underscorejs apenas se é necessário porque carregar uma biblioteca apenas para isso não vale apenas isso
DannyFeliz
4

A partir do ES 2015, Array.prototype.find()fornece essa funcionalidade exata.

Para navegadores que não suportam esse recurso, a Mozilla Developer Network forneceu um polyfill (colado abaixo):

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;
  };
}
Kevin Lee Garner
fonte
2
foundElement = myArray[myArray.findIndex(element => //condition here)];
dotista2008
fonte
3
Um exemplo da vida real com uma cláusula de condição e algumas palavras explicativas tornaria sua resposta mais valiosa e compreensível.
precisa saber é o seguinte
0

Eu me inspirei em várias fontes na internet para derivar a solução abaixo. Queria levar em consideração algum valor padrão e fornecer uma maneira de comparar cada entrada para uma abordagem genérica que isso resolve.

Uso: (dando valor "Segundo")

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

Implementação:

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;
}
Henrik
fonte
-2

Não há função interna no Javascript para realizar esta pesquisa.

Se você estiver usando o jQuery, poderá fazer a jQuery.inArray(element,array).

PedroSena
fonte
Que iria trabalhar, também, embora eu vou com Sublinhado provavelmente :)
Jakub P.
3
Isso não satisfaz a saída do que o solicitante exige (precisa do elemento em algum índice, não um booleano).
Graham Robertson
O @GrahamRobertson $.inArraynão retorna um booleano; ele (surpreendentemente!) Retorna o índice do primeiro elemento correspondente. Ainda não faz o que o OP pediu, no entanto.
Mark Amery
-2

Uma maneira menos elegante que exibirá throwtodas as mensagens de erro corretas (com base em Array.prototype.filter), mas interromperá a iteração no primeiro resultado é

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;
    }
}

Então exemplos são

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

Funciona terminando filterusando throw.

Paul S.
fonte