Determină dacă șirul se află în listă în JavaScript


În SQL putem vedea dacă un șir de caractere se află într-o listă astfel:

Column IN ('a', 'b', 'c')

Care este o modalitate bună de a face asta în JavaScript? Este atât de greoi să faci asta:

if (expression1 || expression2 || str === 'a' || str === 'b' || str === 'c') {
   // do something
}

Și nu sunt sigur în legătură cu performanța sau claritatea acestuia:

if (expression1 || expression2 || {a:1, b:1, c:1}[str]) {
   // do something
}

Sau cineva ar putea folosi funcția switch:

var str = 'a',
   flag = false;

switch (str) {
   case 'a':
   case 'b':
   case 'c':
      flag = true;
   default:
}

if (expression1 || expression2 || flag) {
   // do something
}

Dar asta este un dezastru groaznic. Ai vreo idee?

În acest caz, trebuie să folosesc Internet Explorer 7 deoarece este pentru o pagină intranet corporativă. Deci ['a', 'b', 'c'].indexOf(str) !== -1 nu va funcționa nativ fără puțin ajutor sintactic.

Răspuns #1

ES6 (ES2015) și ulterior

Dacă folosești ECMAScript 6 (cunoscut și ca ES2015) sau o versiune mai recentă, cea mai curată metodă este să construiești un array cu elementele și să folosești Array.includes:

['a', 'b', 'c'].includes('b')

Acest lucru are unele beneficii inerente față de indexOf deoarece poate testa corespunzător prezența NaN în listă și poate potrivi elementele lipsă dintr-un array, cum ar fi cel din mijloc în [1, , 2], cu undefined. De asemenea, tratează +0 și -0 ca fiind egale. includes funcționează și pe array-urile tipizate JavaScript, cum ar fi Uint8Array.

Dacă sunteți îngrijorat de suportul pentru browsere (cum ar fi pentru IE sau Edge), puteți verifica Array.includes pe CanIUse.Com, și dacă doriți să țintiți un browser sau o versiune de browser care nu are includes, va trebui să transpilați la o versiune mai veche de ECMAScript folosind un instrument precum Babel, sau să includeți un script polyfill în browser, cum sunt cele disponibile la polyfill.io.

Performanță mai ridicată

Rețineți că nu există nicio garanție că timpul de execuție al Array.includes() nu va crește odată cu numărul de elemente din array: acesta poate avea performanță O(n). Dacă aveți nevoie de o performanță mai ridicată și nu veți construi setul de elemente de mai multe ori (dar veți verifica repetat dacă elementele conțin un anumit element), ar trebui să folosiți un Set deoarece specificația ES solicită implementările Set (și Map de asemenea) să fie sub-liniare pentru citiri:

Specificația impune ca mulțimile să fie implementate „într-un mod care, în medie, să ofere timpi de acces sublinari în funcție de numărul de elemente din colecție”. Prin urmare, acestea ar putea fi reprezentate intern ca un tabel de dispersie (cu căutare O(1)), un arbore de căutare (cu căutare O(log(N))) sau orice altă structură de date, atât timp cât complexitatea este mai bună decât O(N).

const interestingItems = new Set(['a', 'b', 'c'])
const isItemInSet = interestingItems.has('b')

Rețineți că puteți transmite orice element iterabil constructorului Set (orice lucru care suportă for...of). De asemenea, puteți transforma un Set într-un array folosind Array.from(set) sau prin răspândirea sa [...set].

Fără un tablou

Acest lucru nu este chiar recomandat, dar ai putea adăuga o nouă proprietate isInList la șiruri de caractere astfel:

if (!String.prototype.isInList) {
  Object.defineProperty(String.prototype, 'isInList', {
    get: () => function(...args) {
      let value = this.valueOf();
      for (let i = 0, l = args.length; i < l; i += 1) {
        if (arguments[i] === value) return true;
      }
      return false;
    }
  });
}

Apoi folosește-l astfel:

'fox'.isInList('weasel', 'fox', 'stoat') // true
'fox'.isInList('weasel', 'stoat') // false

Poți face același lucru și pentru Number.prototype.

Rețineți că Object.defineProperty nu poate fi folosit în IE8 și versiunile anterioare, sau în versiunile foarte vechi ale altor browsere. Totuși, este o soluție mult superioară față de String.prototype.isInList=function() { ... } pentru că utilizarea unei atribuiri simple ca aceasta va crea o proprietate enumerabilă pe String.prototype, ceea ce este mai probabil să deterioreze codul.

Array.indexOf

Dacă folosești un browser modern, indexOf funcționează întotdeauna. Totuși, pentru IE8 și versiunile anterioare vei avea nevoie de un polyfill.

Dacă indexOf returnează -1, elementul nu se află în listă. Totuși, trebuie să fii atent, deoarece această metodă nu va verifica corect NaN, și, deși poate să coincidă cu un undefined explicit, nu poate să facă să coincidă un element absent cu undefined așa cum în array-ul [1, , 2].

Polyfill pentru indexOf sau includes în IE, sau în orice alt browser/versiune care nu oferă suport

Dacă nu doriți să folosiți un serviciu precum polyfill.io, așa cum s-a menționat mai sus, puteți întotdeauna să includeți în propriul cod sursă polyfill-uri personalizate conforme cu standardele. De exemplu, biblioteca CoreJs are o implementare a indexOf.

În această situație, în care a trebuit să găsesc o soluție pentru Internet Explorer 7, am „creat propria mea” versiune mai simplă a funcției indexOf() care nu respectă standardele:

if (!Array.prototype.indexOf) {
   Array.prototype.indexOf = function(item) {
      var i = this.length;
      while (i--) {
         if (this[i] === item) return i;
      }
      return -1;
   }
}

Note despre modificarea prototipurilor obiectelor

Totuși, nu cred că modificarea String.prototype sau Array.prototype este o strategie bună pe termen lung. Modificarea prototipurilor obiectelor în JavaScript poate duce la erori serioase. Trebuie să decizi dacă este sigur să faci acest lucru în propriul tău mediu. Este de remarcat, în mod special, că iterarea unui array (când Array.prototype are proprietăți adăugate) cu for ... in va returna numele noii funcții ca unul dintre chei:

Array.prototype.blah = function() { console.log('blah'); };
let arr = [1, 2, 3];
for (let x in arr) { console.log(x); }
// Result:
0
1
2
blah // Extra member iterated over!

Codul tău poate funcționa acum, dar în momentul în care cineva în viitor va adăuga o bibliotecă sau un plugin JavaScript terț care nu protejează cu zel împotriva cheilor moștenite, totul se poate strica.

Modul vechi de a evita acea ruptură este, în timpul enumerării, să verifici fiecare valoare pentru a vedea dacă obiectul o are efectiv ca proprietate nenăscută cu if (arr.hasOwnProperty(x)) și doar atunci să lucrezi cu acea x.

Noile metode ES6 pentru a evita această problemă a tastelor suplimentare sunt:

  1. Folosește of în loc de in, for (let x of arr). Totuși, în funcție de ținta de ieșire și de setările/capabilitățile exacte ale transpilerului tău de nivel inferior, acest lucru poate să nu fie fiabil. În plus, dacă nu poți garanta că tot codul tău și bibliotecile terțe respectă strict această metodă, atunci, pentru scopurile acestei întrebări, probabil că vei dori doar să folosești includes așa cum este menționat mai sus.

  2. Defineți noile proprietăți pe prototip utilizând Object.defineProperty(), deoarece acest lucru va face proprietatea (implicit) nenumerabilă. Aceasta rezolvă cu adevărat problema doar dacă toate bibliotecile sau modulele JavaScript pe care le utilizați fac, de asemenea, acest lucru.

Un Capcană: Domeniul de execuție în browsere și Node.js

Deși polyfill-urile pentru browser au sens și modificarea prototipului obiectului este o strategie utilă, pot exista probleme de sferă în atât browsere, cât și în Node.js, din motivele lor unice.

Într-un browser, fiecare obiect document distinct reprezintă propriul său scop global nou, iar în JS pentru browser este posibil să creezi documente noi (cum ar fi cele utilizate pentru redarea off-screen sau pentru a crea fragmente de document) sau să obții o referință la obiectul document al unei alte pagini (de exemplu prin comunicare între pagini folosind un link cu țintă numită), astfel că este posibil, în anumite circumstanțe (raroase?), ca prototipurile obiectelor să nu aibă metodele pe care te aștepți să le aibă—deși ai putea întotdeauna să rulezi din nou polyfill-urile tale împotriva noilor obiecte globale...

În Node.js, modificarea prototipurilor obiectelor global poate fi sigură, dar modificarea prototipurilor obiectelor non-globale, importate, ar putea duce la probleme dacă ajungi vreodată să ai două versiuni ale aceluiași pachet care sunt cerute/importate, deoarece importurile celor două versiuni nu vor expune aceleași obiecte, astfel încât nu vor avea aceleași prototipuri de obiect. Adică, codul tău ar putea funcționa bine până când o dependență sau o sub-dependență folosește o versiune diferită de cea pe care o aștepți, și fără ca vreo parte din codul tău să se schimbe, un simplu npm install sau yarn install ar putea declanșa această problemă. (Există opțiuni pentru a gestiona acest lucru, cum ar fi proprietatea resolutions a lui yarn în package.json, dar nu este recomandat să te bazezi pe asta dacă ai alte opțiuni.)

Această problemă Node.js depășește diferențele de versiune și poate apărea chiar și cu aceeași versiune folosită de diferite importuri, deoarece atunci când o aplicație este complet transpilată și rulează (sau codul dintr-un pachet este folosit într-o altă aplicație), diferite părți ale aplicației pot ajunge să importe cod commonJs ȘI cod ES-module. Cu excepția cazului în care pachetele sunt concepute foarte, foarte atent astfel încât să existe un singur import de bază scris commonJs, utilizat în toate modulele, atunci pot apărea diviziuni foarte surprinzătoare între acestea două, chiar dacă totul funcționează într-o aplicație de test care folosește pachetul tău! Aceasta se întâmplă deoarece nu poți controla detaliile de transpilare și adaptare la nivel inferior ale aplicațiilor care consumă pachetul, iar modul tău frumos și elegant ES ar putea fi coborât sau transformat între module, chiar și după propriul tău proces de transpilare și impachetare în timpul publicării.

Trebuie luate măsuri speciale pentru a asigura că prototipurile obiectelor modificate au fost modificate la fiecare utilizare sau să se efectueze alte lucrări de inginerie pentru a asigura că transpilarea și împachetarea nu strică lucrurile.

Răspunsul #2

Poți apela indexOf:

if (['a', 'b', 'c'].indexOf(str) >= 0) {
    //do something
}

Răspunsul #3

Majoritatea răspunsurilor sugerează metoda Array.prototype.indexOf, singura problemă este că nu va funcționa pe nicio versiune de IE înainte de IE9.

Ca alternativă, vă las două opțiuni în plus care vor funcționa pe toate browserele:

if (/Foo|Bar|Baz/.test(str)) {
  // ...
}


if (str.match("Foo|Bar|Baz")) {
  // ...
}

Răspunsul #4

Array-urile au o metodă indexOf care poate fi folosită pentru a căuta șiruri de caractere:

js> a = ['foo', 'bar', 'baz']
foo,bar,baz
js> a.indexOf('bar')
1
js> a.indexOf('quux')
-1

Răspunsul #5

Pe lângă indexOf (pe care alți posterii l-au sugerat), utilizarea Enumerable.include() din prototype poate face acest lucru mai ordonat și mai concis:

var list = ['a', 'b', 'c'];
if (list.includes(str)) {
  // do stuff
}

Răspunsul #6

Un truc pe care l-am folosit este

>>> ("something" in {"a string":"", "somthing":"", "another string":""})
false
>>> ("something" in {"a string":"", "something":"", "another string":""})
true

Ai putea să faci ceva de genul

>>> a = ["a string", "something", "another string"];
>>> b = {};
>>> for(var i=0; i<a.length;i++){b[a[i]]="";} /* Transform the array in a dict */
>>> ("something" in b)
true

Răspunsul #7

Folosind indexOf (nu funcționează cu IE8).

if (['apple', 'cherry', 'orange', 'banana'].indexOf(value) >= 0) {
    // found
}

Pentru a suporta IE8, ai putea implementa funcția indexOf a Mozilla.

if (!Array.prototype.indexOf) {
    // indexOf polyfill code here
}

Expresii regulate prin String.prototype.match (documentație).

if (fruit.match(/^(banana|lemon|mango|pineapple)$/)) {

}