Unterschiede zwischen ECMA-262 3rd und 5th Edition bei der Semantik von Funktionsdefinitionen

04.06.2011

Ein altes Problem in JavaScript ist das Erkennen von Arrays. typeof [] liefert nämlich 'object' zurück, so dass darüber kein Unterschied zwischen einem gewöhnlichen Objekt und einem Array festgestellt werden kann. Im Laufe der Zeit wurde dann eine Methode gefunden, die Object.prototype.toString benutzt. Mit der fünften Edition von ECMA-262 wurde dann endlich Array.isArray offiziell, das diese Funktion erfüllt.

Da es allerdings eine Weile dauern wird, bis Array.isArray überall verfügbar ist, sind alternative Implementierungen weiterhin sinnvoll. Beim Spielen mit solchen bin ich dann auf einen subtilen Unterschied zwischen der dritten (der zuvor verbreiteten) und fünften Edition gestoßen.

Die selbstgebaute isArray basiert darauf, dass Object.prototype.toString, wenn es im Kontext eines Objektes ausgeführt wird, dessen interne [[Class]]-Eigenschaft verrät. Eine typische Funktion (ohne Optimierungen) sieht so aus:

function isArray(obj){
  return Object.prototype.toString.call(obj) == '[object Array]';}

Mein Ansinnen war es nun, eine Funktion schreiben, die so kurz wie möglich ist. Es ging also nicht um eine optimale Performance bei der Ausführung.

Als Erstes entfernte ich alle unnötigen Leerzeichen, das optionale Semikolon und verkürzte den Parameternamen:

function isArray(o){return Object.prototype.toString.call(o)=='[object Array]'}

79 Zeichen ist die Funktion dann lang.

Als Nächstes ersetzte ich den Vergleich mit der kompletten Zeichenkette durch einen regulären Ausdruck:

function isArray(o){return /Array/.test(Object.prototype.toString.call(o))}

Das verkürzte die Funktion immerhin auf 75 Zeichen.

Eine kurze Recherche ergab, dass es lediglich wenige mögliche Werte für [[Class]] gibt (Host-Eigenschaften einer Implementierung ausgenommen) gibt, nämlich 'Function', 'Object', 'Array', 'String', 'RegExp', 'Boolean', 'Number', 'Math', 'Date' und 'Error'. Wie leicht zu sehen ist, kommt der Buchstabe y einzig und allein in „Array“ vor, es reicht also, auf diesen zu prüfen:

function isArray(o){return /y/.test(Object.prototype.toString.call(o))}

Weitere vier Zeichen gespart und so die Gesamtzahl auf 71 gedrückt.

Der gröbste Brocken ist offenbar der Zugriff auf Object.prototype.toString. Kann der verkleinert werden? Natürlich! Das vielgeschmähte with-Statement erlaubt nämlich einen verkürzten Zugriff auf Eigenschaften eines Objektes, sowohl direkte als auch solche, die in der Prototypen-Kette zu finden sind. Damit kam ich auf diese Version:

function isArray(o){with({})return /y/.test(toString.call(o))}

Nur noch 62 Zeichen!

Könnte es vielleicht noch kürzer gehen? Eine weiterer Trick, auf Eigenschaften von Object.prototype zuzugreifen, sind benannte Funktionsausdrücke. Das führte mich zu diesem Code:

var isArray=function f(o){return /y/.test(toString.call(o))}

Immerhin mit 60 Zeichen noch ein kleines bisschen kürzer.

Allerdings hat die letzte Variante einen schwerwiegenden Nachteil: Sie schien nicht zu funktionieren. Zum Testen benutzte ich die Firebug-Konsole meines Mozilla Firefox (3.6) und mein Möchtegern-Meisterwerk lieferte darin für alle Eingaben (Zahlen, Booleans, Zeichenketten, etc.) true zurück statt nur für Arrays:
isArray(*) == true

Das fand ich verwirrend, aber testete auch noch die JavaScript-Konsole von Google Chrome (Version 11.0.696.71, zum Zeitpunkt dieses Artikels aktuell). In diesem lieferte die Funktion die gewünschten Werte:
isArray([]) == true, sonst false
Irgendetwas musste also im Firefox anders sein, aber was? Da derzeit ein Umschwung von ECMA-262 Edition 3 auf Edition 5 stattfindet, vermutete ich den Unterschied dort und wurde auch tatsächlich fündig.

Der entsprechende Abschnitt in der dritten Edition ist „13 Functions“/„Semantics“ (Seite 71), dort heißt es:

The production FunctionExpression : function Identifier ( FormalParameterListopt ){ FunctionBody
}
is evaluated as follows:

  1. Create a new object as if by the expression new Object().
  2. Add Result(1) to the front of the scope chain.
  3. Create a new Function object as specified in 13.2 with parameters specified by FormalParameterListopt
    and body specified by FunctionBody. Pass in the scope chain of the running execution context as the
    Scope.
  4. Create a property in the object Result(1). The property's name is Identifier, value is Result(3), and
    attributes are { DontDelete, ReadOnly }.
  5. Remove Result(1) from the front of the scope chain.
  6. Return Result(3).

Die beiden relevanten Punkte sind 1. und 2. – ein Objekt wird erzeugt und an die Spitze der Scope-Chain gestellt. toString wird damit als Allererstes in diesem Objekt gesucht und, da Object.prototype in dessen Prototypen-Kette ist, in Form von Object.prototype.toString gefunden. Das ist das Verhalten, das Chrome zeigt.

In der fünften Edition sieht die Sache etwas anders aus, dort ist im analogen Abschnitt auf Seite 98 zu lesen:

The production FunctionExpression : function Identifier ( FormalParameterListopt ) { FunctionBody }
is evaluated as follows:

  1. Let funcEnv be the result of calling NewDeclarativeEnvironment passing the running execution context’s Lexical Environment as the argument
  2. Let envRec be funcEnv’s environment record.
  3. Call the CreateImmutableBinding(N) concrete method of envRec passing the String value of Identifier as the argument.
  4. Let closure be the result of creating a new Function object as specified in 13.2 with parameters specified by FormalParameterListopt and body specified by FunctionBody. Pass in funcEnv as the Scope. Pass in true as the Strict flag if the FunctionExpression is contained in strict code or if its FunctionBody is strict code.
  5. Call the InitializeImmutableBinding(N,V) concrete method of envRec passing the String value of Identifier and closure as the arguments.
  6. Return closure.

Statt eines „üblichen“ JavaScript-Objektes wird ein „Lexical Environment“ benutzt. Dieses hat jedoch nicht Object.prototype in seiner Prototypen-Kette und somit kann auf dessen Eigenschaften nicht zugegriffen werden.

Damit ist meine isArray-Variante mit dem Funktionsausdruck nicht mehr möglich, schade.

Interessant ist jedoch noch, dass im Firefox die Funktion keinen Fehler erzeugte, sondern immerhin noch einen Rückgabewert, wenn auch nicht den gewünschten, zurücklieferte. Es wurde also sehr wohl eine toString-Funktion irgendwo gefunden, aber welche?

Ehrlicherweise kann ich nur zugeben, dass ich es nicht weiß. (function f(){ return toString; })() == toString im globalen Kontext liefert true, d.h. es gibt eine globale Variable namens toString, die eine Funktion enthält. toString == window.toString liefert aber false, obwohl ein Vergleich des globalen Objekts mit window true ergibt. toString() allein liefert [object Window], was darauf hindeutet, dass es sich doch um eine Eigenschaft von window handelt. Hochinteressantes liefert toString.call([]) zutage – es wird [xpconnect wrapped native prototype] zurückgegeben. Möglicherweise ist der Zugriff auf gewisse Eigenschaften durch Firefox Sicherheitsmodell eingeschränkt bzw. geschieht über Wrapper. Zuletzt genanntes Resultat ist übrigens auch verantwortlich für den Rückgabewert der Funktionsausdrucksvariante von isArray (der war immer true). Es wird nur auf die Existenz von „y“ geprüft und ein solches ist in [xpconnect wrapped native prototype] vorhanden.

Insgesamt finde ich es nett, wie einige einfache Spielereien letztlich zu einem besseren Verständnis führen.