Laura: Schneller Check, ob array und/oder HTMLCollection/NodeList/Args

Hallo,

bin auf der Suche nach einer schnellen cross-browser-Methode, die checkt,
ob ein Objekt ein Array und/oder ein Array-ähnliches Etwas ist, d.h., der
Rückgabewert soll für Arrays, HTMLCollections, NodeLists und Arguments true
sein, für bspw. Strings und Objektliterale hingegen false.
( NUR auf Arrays oder NUR auf Arguments testen, ist einfach )

Fallen:
 - Object.prototype.toString.call( obj ) ist für z.b. HTMLCollection nicht
   browswer-übergreifend zwingend [object HTMLCollection]
 - obj.length funktioniert auch bei Strings
 - obj[0] funktioniert ebenfalls bei Strings ( und auch hier: {0:0} )

Geht wirklich nichts drum herum, lauter Fallunterscheidungen zu machen?

Lieben Gruß,
Laura

  1. Hi,

    Geht wirklich nichts drum herum, lauter Fallunterscheidungen zu machen?

    Die gehen per ternärem Operator auch recht kompakt, und liefern einen hübschen boole'schen Wert.

    • obj.length funktioniert auch bei Strings

    Wenn das der einzige Stolperfall ist, der dich am simplen testen auf length hindert, dann verbinde halt die Abfrage noch mit einer typeof-Abfrage auf String.

    (Ob das ausreicht, hängt halt davon ab, wie "genau" du es haben willst, bzw. für welche Arten von Objekten noch.)

    MfG ChrisB

    --
    RGB is totally confusing - I mean, at least #C0FFEE should be brown, right?
    1. (Ob das ausreicht, hängt halt davon ab, wie "genau" du es haben willst, bzw. für welche Arten von Objekten noch.)

      also, es sollte absolut zuverlässig alles andere als HTMLCollections, Arrays, NodeLists und Arguments ausgeschlossen werden.
      _____
      Ach, und falls zufällig jemand eine zuverlässige Methode weiß, mit der man schnell
      auf HTMLCollection/NodeList - und nur auf dies - schließen kann, ist das auch
      sehr willkommen ;}

      Lieben Gruß

      1. Gruss Laura,

        Ach, und falls zufällig jemand eine zuverlässige Methode weiß, mit der man schnell
        auf HTMLCollection/NodeList - und nur auf dies - schließen kann, ist das auch
        sehr willkommen ;}

        Sowohl [HTMLCollection]s als auch [NodeList]s implementieren eine [item]-Methode.
        Soweit mir bekannt ist, gibt der »typeof« operator für alle Browser außer für MSIE's
        kleiner 9 (habe aber gerade keinen 9er zur Hand, um das zu prüfen) "function" zurück.

        "msie < 9" melden "object" - dieses Objekt ist aber "callable". Eine strenge Prüfung
        müsste da also mit "try catch" arbeiten oder eine selbstgestrickte Methode [isCallable]
        hinzuziehen.

        Hinreichend genau sollte aber schon folgende beispielhaft gegebene Implementierung sein:

        var isHTMLCollectionOrNodeList = function (obj) {  
          
          var item = obj.item;  
          return !!((typeof obj.length == "number") && item && ((typeof item == "function") || (typeof item == "object")));  
        };
        

        so long - peterS. - pseliger@gmx.net

        --
        »Because objects in JavaScript are so flexible, you will want to think differently about class hierarchies.
        Deep hierarchies are inappropriate. Shallow hierarchies are efficient and expressive.« - Douglas Crockford

        ie:( fl:) br:> va:( ls:& fo:) rl:) n3;} n4:} ss:} de:µ js:} mo:? zu:]

        1. Hallo,
          sorry, musste gestern abend spontan nochmal weg und hatte auch nicht damit gerechnet, dass
          noch soviel hilfreicher Input kommt - vielen Dank erstmal :)
          @Peter:
          Danke für isHTMLCollectionOrNodeList.
          Bzgl. des Array-checks hat molily ja schon geschrieben, wie einfach das geht.
          jQuery macht da glaub ich auch nichts anderes.
          @molily:
          Also, der Grund, weshalb ich so einen Check möchte, ist grob zusammengefasst folgender:
          Habe in einem Framework eine init-Methode, die so ähnlich wie die von jQuery funktioniert.
          Man kann alles als Parameter übergeben: Arrays, HTMLCollections, Strings, Object-Literale etc. Wenn Arrays oder HMTLCollection übergeben werden, sollen diese gleich behandelt werden. (normalerweise wird ein String übergeben als Selektor für HTML-Elemente, darüber hinaus soll die init-Methode aber auch dazu dienen, Array- und HTMLCollection-Elemente abzuspeichern aus chaining-Gründen. Muss also insbesondere diese beiden von z.B. Strings diskriminieren können). Das klingt jetzt wahrscheinlich etwas vage und schwammig, aber in diesem
          einen Fall hat eine solche Unterscheidung in meinem Script Sinn.
          Liebe Grüße, Laura

          1. Hallo,

            Habe in einem Framework eine init-Methode, die so ähnlich wie die von jQuery funktioniert.
            Man kann alles als Parameter übergeben: Arrays, HTMLCollections, Strings, Object-Literale etc. Wenn Arrays oder HMTLCollection übergeben werden, sollen diese gleich behandelt werden. (normalerweise wird ein String übergeben als Selektor für HTML-Elemente, darüber hinaus soll die init-Methode aber auch dazu dienen, Array- und HTMLCollection-Elemente abzuspeichern aus chaining-Gründen. Muss also insbesondere diese beiden von z.B. Strings diskriminieren können).

            Wahrscheinlich reicht so etwas aus:

            if (!arg) return; // Sämtliche falsy values sind wahrscheinlich unbenutzbar  
            if (typeof arg == 'string') {  
              // Behandle als Selektor  
            } else if (typeof arg.length == 'number' && arg[0]) {  
              for (var i, l = arg.length; i < l; i++) {  
                // Als Liste behandeln  
              }  
            } else {  
              // Behandle als Object (Hash)  
            }
            

            Generell sollte man zwei Sachen trennen: Die Funktionsfähigkeit eines Codes bei korrekter Benutzung und die Qualität der Fehlerausgabe bei inkorrekten oder unbrauchbaren Eingabewerten. Natürlich kann man der Funktion z.B. einen Array übergeben, in der keine Elemente drin sind. Es ist deine Entscheidung, in wie weit du noch prüfst, ob das wirklich Elemente sind, und Exceptions wirfst, wenn die Eingabewerte nicht brauchbar sind. Das führt dich aber nur auf auf das nicht-triviale Problem, wie man browserübergreifend Elementobjekte erkennt.

            Auch hier würde ich wieder auf Duck Typing setzen, anstatt mit instanceof, constructor, der internen [[Class]]-Eigenschaft oder ähnlich zu arbeiten. Wenn man deinen Erkennungscode betrügen will, dann kann man das vermutlich, daher würde ich mich darauf beschränken, bei offensichtlichen Fehlbenutzung Exceptions zu werfen (throw new Error('…')).

            Siehe auch meinen Artikel Objektabfragen und Fallunterscheidungen in JavaScript.

            Mathias

            1. Vielen Dank!

              Generell sollte man zwei Sachen trennen: Die Funktionsfähigkeit eines Codes bei korrekter Benutzung und die Qualität der Fehlerausgabe bei inkorrekten oder unbrauchbaren Eingabewerten. Natürlich kann man der Funktion z.B. einen Array übergeben, in der keine Elemente drin sind. Es ist deine Entscheidung, in wie weit du noch prüfst, ob das wirklich Elemente sind, und Exceptions wirfst, wenn die Eingabewerte nicht brauchbar sind.

              Man kann init auch beliebige Arrays übergeben (nicht nur welche, die nur HTML-Elemente enthalten). Es wird eine weitere Fallunterscheidung gemacht:
              Der zweite Parameter dient eigentlich der Spezifizierung des Kontextes, wird
              jedoch true übergeben, so wird der Array als Array gespeichert (in this[0]), andernfalls werden die Elemente des Arrays ganz normal auf this[0] bis this[arrayLength-1] "gelegt".
              Übergibt man einen Array mit true als 2. Parameter, so kann man diesen per chaining weiter verarbeiten (dies geht auch, wenn man z.B. ein Objekt-Literal übergibt). Dann kann man bspw. Folgendes schreiben:

                
              _(arrName, true).shuffle().erase(0).get()  
              
              

              Zur Manipulation von HTML-Elementen kann man bspw. einfach einen Array oder eine Collection übergeben oder einen String, der als Selector fungiert:

                
              // findet und speichert alle Elemente mit className small und die Elemente mit den  
              // ids unic1 und unic2, wobei die Suche eingeschränkt wird auf die Elemente,  
              // die im Baum "unter" dem Element mit der id "boss" liegen:  
              _('.small,#unic1,#unic2', '#boss')  
              
              

              Lieben Gruß,
              Laura

            2. Vielleicht unvorbildliche Forums-Nutzung, in kleinen Happen viele Antworten
              zum eigenen Thread zu posten, aber wollte hier doch noch kurz schreiben, was ich
              bislang habe, weil ja vermutlich Leute mit ähnlichen Fragen hier landen.

              Wahrscheinlich reicht so etwas aus:

              if (!arg) return; // Sämtliche falsy values sind wahrscheinlich unbenutzbar

              if (typeof arg == 'string') {
                // Behandle als Selektor
              } else if (typeof arg.length == 'number' && arg[0]) {
                for (var i, l = arg.length; i < l; i++) {
                  // Als Liste behandeln
                }
              } else {
                // Behandle als Object (Hash)
              }

              Habe nochmal jQuery etwas durchforstet und bin momentan bei folgendem Code  
              (wird noch weiter verbessert werden (z.b. werde ich noch NodeLists gegenchecken etc.):  
              ~~~javascript
                
              typeOf : function(obj) {  
              	// ''  
              	if (obj == null) { return String(obj); }  
              	// HTML nodes  
              	if (obj.nodeName) {  
              		if (this.isElem(obj)) { return 'element'; }  
              		if (this.isTxt(obj)) { return (/\S/).test(obj.nodeValue) ? 'textnode' : 'whitespace'; }  
              	}  
              	// array-like lists and strings  
              	else if (obj.length === +obj.length) {  
              		// handle strings before collections -> in-operator throws error on strings!  
              		if (typeof obj === 'string') { return 'string'; }  
              		if (obj.callee) { return 'arguments'; }  
              		if ('item' in obj) { return 'collection'; }  
              	}  
              	// work with shortcuts instead of bulky '[object Object]', ...  
              	return types[ toString.call(obj) ] || 'object';  
              },  
              isArr : function(obj) {  
              // Alias for isArray  
              	if (!obj) { return false; }  
              	// use native meth if browser supports it  
              	if (obj.isArray) { return Array.isArray(obj);  
              	} else { return this.typeOf(obj) === 'array'; }  
              },  
              isArraylike : function(obj) {  
              	if (!obj) { return false; }  
              	var type = this.typeOf(obj);  
              	return type === 'array' || type === 'collection' || type === 'arguments';  
              }  
              
              

              An einigen Stellen werden Framework-interne Methoden und shortcuts verwendet (isElem, isTxt, toString etc.),
              aber denke, dass deren Namen ihren Zweck hinreichend vermitteln ;}
              types ist ein Objekt-Literal, das so aussieht:

                
              var types = {  
                 '[object String]' : 'string',  
                 '[object Array]' : 'array',  
                 usw.  
              }  
              
              

              Lieben Gruß

              1. Hallo,

                // array-like lists and strings
                else if (obj.length === +obj.length) {
                // handle strings before collections -> in-operator throws error on strings!
                if (typeof obj === 'string') { return 'string'; }
                if (obj.callee) { return 'arguments'; }
                if ('item' in obj) { return 'collection'; }
                }

                Das Problem bei so einer Vorgehensweise im Vergleich zu Duck Typing ist, dass nicht beteiligte und nicht benutzte Eigenschaften abgefragt werden. arguments.callee beispielsweise ist deprecated und der Zugriff darauf im ECMAScript 5 Strict Mode wirft eine Exception.

                Ich würde jedem raten, schon jetzt nur noch im Strict Mode zu entwickeln, daher würde ich keine Bibliothek auf einem deprecated Feature aufbauen. Wenn du nirgendwo wirklich die Unterscheidung zwischen »listenartig« und Array, Arguments und NodeList/HTMLCollection brauchst, so würde ich gar nicht erst versuchen, sie zu unterscheiden.

                Wenn ich dich richtig verstanden habe, braucht deine Funktion hauptsächlich die Unterscheidung zwischen Boolean, String, »listenartig« und Listen mit Elementen. Das ist müsste robust umsetzbar sein.

                Mathias

                1. Das Problem bei so einer Vorgehensweise im Vergleich zu Duck Typing ist, dass nicht beteiligte und nicht benutzte Eigenschaften abgefragt werden.

                  Habe "duck typing" gegooglet (Wikipedia und Artikel bzgl. typeof etc.) und bin mir immer
                  noch nicht sicher, was genau damit gemeint ist. In Bezug auf Arrays vielleicht Folgendes(?):
                  Wenn es die Eigenschaften erfüllt, die ich von einem Array und nur von einem Array
                  erwarte, dann verwende ich es als Array bzw. nenne es "Array". Wenn also ein Etwas
                  eine length besitzt, man auf es mit der Bracket-Methode zugreifen kann etc., dann ist's ein Array für mich.
                  Also eine Art pragmatische Deskriptions-Definition?

                  arguments.callee beispielsweise ist deprecated und der Zugriff darauf im ECMAScript 5 Strict Mode wirft eine Exception.

                  Oh, gut zu wissen, das war mir bisher entgangen. Wäre callee aber nicht depricated,
                  so erfüllte doch mein Check auf das Vorhandensein dieser property die Idee von
                  duck typing, oder? Es wird genau die Eigenschaft abgefragt, die ich von arguments
                  und nur von arguments erwarte. >>Verhält sich wie arguments => ist arguments<<

                  Ich würde jedem raten, schon jetzt nur noch im Strict Mode zu entwickeln, daher würde ich keine Bibliothek auf einem deprecated Feature aufbauen. Wenn du nirgendwo wirklich die Unterscheidung zwischen »listenartig« und Array, Arguments und NodeList/HTMLCollection brauchst, so würde ich gar nicht erst versuchen, sie zu unterscheiden.

                  »»
                  Da hast Du wohl voll und ganz Recht. Werde callee rauspfeffern und mein Script auch
                  dahingehend umschreiben, dass ich keine "Ist-so-ähnlich-wie'n-Array-Eigenschaft" definiere.

                  Wenn ich dich richtig verstanden habe, braucht deine Funktion hauptsächlich die Unterscheidung zwischen Boolean, String, »listenartig« und Listen mit Elementen. Das ist müsste robust umsetzbar sein.

                  Wird in Angriff genommen :}
                  Vielen Dank Dir,
                  Laura

                  1. Hallo,

                    Wenn es die Eigenschaften erfüllt, die ich von einem Array und nur von einem Array
                    erwarte, dann verwende ich es als Array bzw. nenne es "Array". Wenn also ein Etwas
                    eine length besitzt, man auf es mit der Bracket-Methode zugreifen kann etc., dann ist's ein Array für mich.

                    Ja, genau.

                    Wäre callee aber nicht depricated,
                    so erfüllte doch mein Check auf das Vorhandensein dieser property die Idee von
                    duck typing, oder? Es wird genau die Eigenschaft abgefragt, die ich von arguments
                    und nur von arguments erwarte. >>Verhält sich wie arguments => ist arguments<<

                    Wenn du callee tatsächlich verwendest und zu diesem Zweck dessen Vorhandensein abfragst, ja. Es ist immer der Sinn von Objektabfragen, in Erfahrung zu bringen, ob das Objekt einem das bietet, was man gerade braucht. Wenn man hingegen den Typ oder andere unbeteiligte Eigenschaften/Methoden abzufragen versucht, ist nicht garantiert, dass man mit dem Objekt das tun kann, was man vorhat.

                    Üblicherweise wird arguments so verwendet, dass man daraus Werte mit [x] holt oder auf .length zugreift. arguments.callee wird eher selten verwendet, z.B. für Rekursion, und ist durch benannte Funktionen ersetzbar.

                    Mathias

        2. ups,
          nochmal sorry, ganz so einfach ist's dann doch nicht seh ich gerade. Ist schon
          ne Weile her, dass ich den Array-Check implementiert hatte.
          Und jetzt habe ich noch etwas in jQuery gestöbert und die typeOf-Methode liefert schon
          so im Wesentlichen das, was ich brauche :)
          Gruß

  2. hallo again Laura,

    ( NUR auf Arrays oder NUR auf Arguments testen, ist einfach )

    das wage ich zu bezweifeln. In einem soliden browserübergreifenden
    Test auf das [arguments] array steckt schon etwas Aufwand.

    Aber wofür benötigst Du diesen speziellen Test eigentlich?...
    Selbst im anspruchsvolleren Frontend-Alltag muss man kaum auf
    solche Helfer zurückgreifen.

    so long - peterS. - pseliger@gmx.net

    --
    »Because objects in JavaScript are so flexible, you will want to think differently about class hierarchies.
    Deep hierarchies are inappropriate. Shallow hierarchies are efficient and expressive.« - Douglas Crockford

    ie:( fl:) br:> va:( ls:& fo:) rl:) n3;} n4:} ss:} de:µ js:} mo:? zu:]

    1. Hallo,

      ( NUR auf Arrays oder NUR auf Arguments testen, ist einfach )

      das wage ich zu bezweifeln. In einem soliden browserübergreifenden
      Test auf das [arguments] array steckt schon etwas Aufwand.

      Wieso?
      Object.prototype.toString.call(arguments) === '[object Arguments]'
      http://es5.github.com/#x10.6

      Mathias

      1. gruss Mathias,

        ... In einem soliden browserübergreifenden Test auf das [arguments] array
        steckt schon etwas Aufwand.

        Wieso?
        Object.prototype.toString.call(arguments) === '[object Arguments]'
        http://es5.github.com/#x10.6

        schon, aber Laura wollte doch eine ( Zitat: ) "cross-browser-Methode".

        Internet Explorer bis einschließlich Version 8 liefern für ...

        (function () {return Object.prototype.toString.call(arguments);})();

        ... - wie für fast alle Objekte - einfach nur "[object Object]" zurück.

        so long - peterS. - pseliger@gmx.net

        --
        »Because objects in JavaScript are so flexible, you will want to think differently about class hierarchies.
        Deep hierarchies are inappropriate. Shallow hierarchies are efficient and expressive.« - Douglas Crockford
        ie:( fl:) br:> va:( ls:& fo:) rl:) n3;} n4:} ss:} de:µ js:} mo:? zu:]
        1. Internet Explorer bis einschließlich Version 8 liefern für ...

          (function () {return Object.prototype.toString.call(arguments);})();

          ... - wie für fast alle Objekte - einfach nur "[object Object]" zurück.

          Okay, das ist gut zu wissen, war mir nicht bekannt. Ich hatte in der Underscore-Bibliothek geschaut und angenommen, dass deren Erkennung zuverlässig und getestet sei. Das bestätigt leider nur, dass Typabfragen dieser Art unpraktikabel sind.

          Mathias

  3. Hallo,

    was genau hast du vor?

    JavaScript ist keine Sprache, in der Typen wichtig sind. Duck Typing ist die Regel. Man fragt i.d.R. nicht ab, ob ein Wert einen gewissen Typ hat, sondern fragt, ob er das bietet, was man will. Wenn er nicht brauchbar ist, dann beendet sich die Verarbeitung einfach oder wirft Exceptions.

    Üblicherweise bedient man sich dem genannten Object.prototype.toString (interne [[Class]]-Eigenschaft) oder instanceof bzw. constructor, um den Typ herauszufinden. Klar, damit kannst du experimentieren. Ich kann dazu nur sagen, dass ich das noch nie gebraucht habe und davon auch abrate. Duck Typing ist das genaue Gegenteil und funktioniert in der Praxis genauso gut.

    Damit ein Wert als Liste iterierbar ist, braucht es eine length-Eigenschaft vom Typ Number und ggf. Eigenschaften [0], [1] usw. Das dürfte zumeist ausreichen. Wenn man deiner Funktion, die eine Liste erwartet, einen String übergibt, so hat der Aufrufende halt Pech. So oder so wird man eine Typabfrage irreführen können, zumindest in einigen Browsern. Daher sehe ich wenig Sinn darin, ohne konkreten Anwendungsfall damit zu kämpfen.

    Mathias