Comment trouver le premier élément d'un tableau correspondant à une condition booléenne en JavaScript?

220

Je me demande s'il existe une méthode connue, intégrée / élégante pour trouver le premier élément d'un tableau JS correspondant à une condition donnée. L'équivalent AC # serait List.Find .

Jusqu'à présent, j'utilise un combo à deux fonctions comme celui-ci:

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

Et puis je peux utiliser:

var result = someArray.findFirst(isNotNullNorUndefined);

Mais comme il y a tellement de méthodes de tableau de style fonctionnel dans ECMAScript , peut-être qu'il y a déjà quelque chose comme ça? J'imagine que beaucoup de gens doivent implémenter des trucs comme ça tout le temps ...

Jakub P.
la source
6
Il n'y a pas de méthode intégrée, mais il existe des bibliothèques d'utilitaires qui se rapprochent de cette fonctionnalité telles que documentcloud.github.com/underscore
kinakuta
Underscore.js est vraiment très joli! Et il a find (). Merci!
Jakub P.
1
Juste pour que vous le sachiez, vous pouvez réduire cela: return (typeof (o) !== 'undefined' && o !== null);jusqu'à cela return o != null;. Ils sont exactement équivalents.
falaises de folie
1
Bon à savoir. Mais vous savez, je me méfie des opérateurs coercitifs comme! = Ou ==. Je ne serais même pas en mesure de le tester facilement, car je devrais vérifier d'une manière ou d'une autre qu'il n'y a pas d'autre valeur forcée à null de cette façon ... :) Alors, quelle chance j'ai d'avoir une bibliothèque qui a permis moi de supprimer complètement cette fonction ... :)
Jakub P.
Je dois honnêtement dire que c'est une solution assez élégante. La chose la plus proche que je peux trouver est Array.prototype.some qui essaie de trouver si un élément satisfait une condition donnée que vous lui passez sous la forme d'une fonction. Malheureusement, cela renvoie un booléen au lieu de l'index ou de l'élément. Je recommanderais votre solution plutôt que d'utiliser une bibliothèque car les bibliothèques ont tendance à être beaucoup plus grandes et contiennent des choses que vous n'utiliserez pas et je préfère garder les choses claires (car vous ne pouvez utiliser que la seule fonction en dehors de la suite qu'elle fournit).
Graham Robertson

Réponses:

220

Depuis ES6, il existe une findméthode native pour les tableaux; cela arrête l'énumération du tableau une fois qu'il trouve la première correspondance et renvoie la valeur.

const result = someArray.find(isNotNullNorUndefined);

Ancienne réponse:

Je dois poster une réponse pour arrêter ces filtersuggestions :-)

puisqu'il y a tellement de méthodes de tableau de style fonctionnel dans ECMAScript, peut-être qu'il y a déjà quelque chose comme ça?

Vous pouvez utiliser la someméthode Array pour itérer le tableau jusqu'à ce qu'une condition soit remplie (puis s'arrêter). Malheureusement, il ne retournera que si la condition a été remplie une fois, et non par quel élément (ou à quel indice) elle a été remplie. Nous devons donc l'amender un peu:

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
la source
28
Je ne peux pas dire que je comprends parfaitement toute l'aversion dirigée contre filter (). Cela peut être plus lent, mais en réalité; dans la plupart des cas où cela est probablement utilisé, c'est une petite liste pour commencer, et la majorité des applications JavaScript ne sont pas assez compliquées pour vraiment se soucier de l'efficacité à ce niveau. [] .filter (test) .pop () ou [] .filter (test) [0] sont simples, natifs et lisibles. Bien sûr, je parle d'applications professionnelles ou de sites Web qui ne sont pas des applications intensives telles que des jeux.
Josh Mc
11
Les solutions de filtrage traversent-elles tous les tableaux / collections? Dans l'affirmative, le filtrage est très inefficace, car il s'exécute sur tout le tableau même si la valeur trouvée est la première de la collection. some()d'autre part, retourne immédiatement, ce qui est beaucoup plus rapide dans presque tous les cas que les solutions de filtrage.
AlikElzin-kilaka le
@ AlikElzin-kilaka: Oui, exactement.
Bergi
15
@JoshMc bien sûr, mais il est logique de ne pas être gratuitement inefficace lors de la publication d'une solution à un problème simple quelque part comme Stack Overflow. Beaucoup de gens vont copier et coller le code d'ici dans une fonction utilitaire, et certains finiront par utiliser cette fonction utilitaire dans un contexte où les performances comptent sans penser à l'implémentation. Si vous leur avez donné quelque chose qui a une implémentation efficace pour commencer, vous avez soit résolu un problème de performance qu'ils n'auraient pas autrement, soit vous leur avez épargné beaucoup de temps de développement pour le diagnostiquer.
Mark Amery du
1
@SuperUberDuper: Non. Voir la réponse de Mark Amery ci-dessous.
Bergi
104

Depuis ECMAScript 6, vous pouvez l'utiliser Array.prototype.findpour cela. Ceci est implémenté et fonctionne dans Firefox (25.0), Chrome (45.0), Edge (12) et Safari (7.1), mais pas dans Internet Explorer ou un tas d'autres plates-formes anciennes ou peu communes .

Par exemple, l'expression ci-dessous est évaluée à 106.

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

Si vous souhaitez l'utiliser maintenant mais avez besoin de la prise en charge d'IE ou d'autres navigateurs non compatibles, vous pouvez utiliser un shim. Je recommande l' es6-shim . MDN propose également une cale si, pour une raison quelconque, vous ne voulez pas mettre l'intégralité de la cale es6 dans votre projet. Pour une compatibilité maximale, vous voulez le shim es6, car contrairement à la version MDN, il détecte les implémentations natives boguées de findet les remplace (voir le commentaire qui commence "Contournement des bogues dans Array # find et Array # findIndex" et les lignes qui le suivent immédiatement) .

Mark Amery
la source
findest mieux que filterpuisque finds'arrête immédiatement lorsqu'il trouve un élément correspondant à la condition, tandis que filterboucle tous les éléments pour donner tous les éléments correspondants.
Anh Tran
59

Qu'en est-il de l'utilisation du filtre et de l'obtention du premier index du tableau résultant?

var result = someArray.filter(isNotNullNorUndefined)[0];
Phil Mander
la source
6
Continuez à utiliser les méthodes es5. var result = someArray.filter (isNotNullNorUndefined) .shift ();
someyoungideas
Bien que j'ai moi-même voté pour trouver une réponse au-dessus de @Bergi, je pense qu'avec la déstructuration d'ES6, nous pouvons nous améliorer un peu ci-dessus: var [result] = someArray.filter (isNotNullNorUndefined);
Nakul Manchanda
@someyoungideas pourriez-vous expliquer les avantages d'utiliser .shiftici?
jakubiszon
3
@jakubiszon L'avantage d'utiliser shiftest qu'il "semble intelligent" mais est en fait plus déroutant. Qui penserait qu'appeler shift()sans argument reviendrait à prendre le premier élément? Ce n'est pas clair OMI. L'accès à la baie
Josh M.
En outre, la notation entre crochets est disponible pour les objets et les tableaux dans ES5 AFAIK, jamais vu une préférence de .shift()plus [0]explicitement indiquée comme ceci. Malgré cela, c'est une alternative que vous pouvez choisir d'utiliser ou non, je resterais [0]cependant.
SidOfc
15

Il devrait être clair maintenant que JavaScript n'offre pas une telle solution de manière native; voici les deux dérivés les plus proches, les plus utiles en premier:

  1. Array.prototype.some(fn)offre le comportement souhaité d'arrêt lorsqu'une condition est remplie, mais renvoie uniquement si un élément est présent; il n'est pas difficile d'appliquer une ruse, comme la solution offerte par la réponse de Bergi .

  2. Array.prototype.filter(fn)[0]en fait un super doublure mais est le moins efficace, car vous jetez des N - 1éléments juste pour obtenir ce dont vous avez besoin.

Les méthodes de recherche traditionnelles en JavaScript se caractérisent par le retour de l'index de l'élément trouvé au lieu de l'élément lui-même ou -1. Cela évite d'avoir à choisir une valeur de retour dans le domaine de tous les types possibles; un index ne peut être qu'un nombre et les valeurs négatives ne sont pas valides.

Les deux solutions ci-dessus ne prennent pas en charge la recherche de décalage non plus, j'ai donc décidé d'écrire ceci:

(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
Jack
la source
Ressemble à la réponse la plus complète. Pouvez-vous ajouter une troisième approche dans votre réponse?
Mrusful
13

Résumé:

  • Pour trouver le premier élément d'un tableau qui correspond à une condition booléenne, nous pouvons utiliser le ES6 find()
  • find()est situé sur Array.prototypeafin qu'il puisse être utilisé sur chaque baie.
  • find()prend un rappel où une booleancondition est testée. La fonction renvoie la valeur (pas l'index!)

Exemple:

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

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

console.log(found);   //  56

Willem van der Veen
la source
8

Si vous utilisez, underscore.jsvous pouvez utiliser ses fonctions findet indexOfpour obtenir exactement ce que vous voulez:

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

Documentation:

Matt Woelk
la source
Utilisez l'option underscorejs juste si c'est nécessaire car chargez une bibliothèque juste pour cela ne vaut pas la peine
DannyFeliz
4

Depuis ES 2015, Array.prototype.find()prévoit cette fonctionnalité exacte.

Pour les navigateurs qui ne prennent pas en charge cette fonctionnalité, le Mozilla Developer Network a fourni un polyfill (collé ci-dessous):

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
la source
2
foundElement = myArray[myArray.findIndex(element => //condition here)];
dotista2008
la source
3
Un exemple concret avec une clause de condition et quelques mots explicatifs rendrait votre réponse plus précieuse et plus compréhensible.
SaschaM78
0

Je me suis inspiré de plusieurs sources sur Internet pour trouver la solution ci-dessous. Voulait prendre en compte à la fois une valeur par défaut et fournir un moyen de comparer chaque entrée pour une approche générique que cela résout.

Utilisation: (donnant la valeur "Second")

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

La mise en oeuvre:

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
la source
-2

Il n'y a pas de fonction intégrée en Javascript pour effectuer cette recherche.

Si vous utilisez jQuery, vous pouvez le faire jQuery.inArray(element,array).

PedroSena
la source
Cela fonctionnerait aussi, bien que j'irai probablement avec Underscore :)
Jakub P.
3
Cela ne satisfait pas la sortie de ce que le demandeur a besoin (a besoin de l'élément à un index, pas un booléen).
Graham Robertson
@GrahamRobertson $.inArrayne renvoie pas de booléen, il (étonnamment!) Renvoie l'index du premier élément correspondant. Cependant, il ne fait toujours pas ce que le PO a demandé.
Mark Amery
-2

Une manière moins élégante qui contiendra throwtous les bons messages d'erreur (basés sur Array.prototype.filter) mais cessera d'itérer sur le premier résultat est

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

Ensuite, des exemples sont

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

Cela fonctionne en terminant filteren utilisant throw.

Paul S.
la source