Autoren:
Champions:
Berater
Stufe: 2
Record
und Tuple
-StandardbibliothekIn diesem Vorschlag werden JavaScript zwei neue zutiefst unveränderliche Datenstrukturen eingeleitet:
Record
, eine zutiefst unveränderliche objektähnliche Struktur #{ x: 1, y: 2 }
Tuple
, eine tief unveränderliche Array-ähnliche Struktur #[1, 2, 3, 4]
Datensätze und Tupel können nur Primitive und andere Datensätze und Tupel enthalten. Sie könnten sich Aufzeichnungen und Tupel als "zusammengesetzte Primitive" vorstellen. Durch die gründliche Basis von Primitiven sind keine Objekte, Aufzeichnungen und Tupeln zutiefst unveränderlich.
Aufzeichnungen und Tupel unterstützen komfortable Redewendungen für Konstruktion, Manipulation und Verwendung, ähnlich wie die Arbeit mit Objekten und Arrays. Sie werden eher mit ihrem Inhalt als mit ihrer Identität verglichen.
JavaScript -Engines können bestimmte Optimierungen für die Konstruktion, Manipulation und den Vergleich von Aufzeichnungen und Tupeln durchführen, analog zu der Art und Weise, wie Zeichenfolgen häufig in JS -Motoren implementiert werden. (Es ist zu verstehen, dass diese Optimierungen nicht garantiert sind.)
Datensätze und Tupel zielen darauf ab, mit externen Typensystem -Supersets wie Typscript oder Flow zu nutzbar und verstanden zu werden.
Heutzutage implementieren Userland -Bibliotheken ähnliche Konzepte wie Unerable.js. Auch ein früherer Vorschlag wurde versucht, aufgrund der Komplexität des Vorschlags und des Mangels an ausreichenden Anwendungsfällen aufgegeben.
Dieser neue Vorschlag ist immer noch von diesem vorherigen Vorschlag inspiriert, führt jedoch einige bedeutende Änderungen vor: Rekord und Tupel sind jetzt zutiefst unveränderlich. Diese Eigenschaft basiert grundsätzlich auf der Beobachtung, dass in großen Projekten das Risiko, unveränderliche und veränderliche Datenstrukturen zu mischen . Dies kann schwer zu findende Fehler einführen.
Dieser Vorschlag bietet als integrierte, zutiefst unveränderliche Datenstruktur auch einige Benutzerfreundlichkeitsvorteile im Vergleich zu Userland-Bibliotheken:
Immer ist ein bemerkenswerter Ansatz für unveränderliche Datenstrukturen und schreibt ein Muster für die Manipulation durch Hersteller und Reduzierer vor. Es liefert jedoch keine unveränderlichen Datentypen, da es gefrorene Objekte generiert. Das gleiche Muster kann an die in diesem Vorschlag definierten Strukturen zusätzlich zu gefrorenen Objekten angepasst werden.
Die in Benutzerbibliotheken definierte tiefe Gleichheit kann erheblich variieren, teilweise aufgrund möglicher Verweise auf veränderliche Objekte. Indem dieser Vorschlag eine harte Linie über tiefgreifende Primitive, Aufzeichnungen und Tupel und Tupel durch die gesamte Struktur enthält, definiert er eine einfache, einheitliche Semantik für Vergleiche.
Record
const proposal = # {
id : 1234 ,
title : "Record & Tuple proposal" ,
contents : `...` ,
// tuples are primitive types so you can put them in records:
keywords : # [ "ecma" , "tc39" , "proposal" , "record" , "tuple" ] ,
} ;
// Accessing keys like you would with objects!
console . log ( proposal . title ) ; // Record & Tuple proposal
console . log ( proposal . keywords [ 1 ] ) ; // tc39
// Spread like objects!
const proposal2 = # {
... proposal ,
title : "Stage 2: Record & Tuple" ,
} ;
console . log ( proposal2 . title ) ; // Stage 2: Record & Tuple
console . log ( proposal2 . keywords [ 1 ] ) ; // tc39
// Object functions work on Records:
console . log ( Object . keys ( proposal ) ) ; // ["contents", "id", "keywords", "title"]
Im Spielplatz geöffnet
Funktionen können Aufzeichnungen und Objekte im Allgemeinen auf die gleiche Weise verarbeiten:
const ship1 = # { x : 1 , y : 2 } ;
// ship2 is an ordinary object:
const ship2 = { x : - 1 , y : 3 } ;
function move ( start , deltaX , deltaY ) {
// we always return a record after moving
return # {
x : start . x + deltaX ,
y : start . y + deltaY ,
} ;
}
const ship1Moved = move ( ship1 , 1 , 0 ) ;
// passing an ordinary object to move() still works:
const ship2Moved = move ( ship2 , 3 , - 1 ) ;
console . log ( ship1Moved === ship2Moved ) ; // true
// ship1 and ship2 have the same coordinates after moving
Im Spielplatz geöffnet
Weitere Beispiele finden Sie hier.
Tuple
const measures = # [ 42 , 12 , 67 , "measure error: foo happened" ] ;
// Accessing indices like you would with arrays!
console . log ( measures [ 0 ] ) ; // 42
console . log ( measures [ 3 ] ) ; // measure error: foo happened
// Slice and spread like arrays!
const correctedMeasures = # [
... measures . slice ( 0 , measures . length - 1 ) ,
- 1
] ;
console . log ( correctedMeasures [ 0 ] ) ; // 42
console . log ( correctedMeasures [ 3 ] ) ; // -1
// or use the .with() shorthand for the same result:
const correctedMeasures2 = measures . with ( 3 , - 1 ) ;
console . log ( correctedMeasures2 [ 0 ] ) ; // 42
console . log ( correctedMeasures2 [ 3 ] ) ; // -1
// Tuples support methods similar to Arrays
console . log ( correctedMeasures2 . map ( x => x + 1 ) ) ; // #[43, 13, 68, 0]
Im Spielplatz geöffnet
Ähnlich als bei Aufzeichnungen können wir Tupel als Array-ähnlich behandeln:
const ship1 = # [ 1 , 2 ] ;
// ship2 is an array:
const ship2 = [ - 1 , 3 ] ;
function move ( start , deltaX , deltaY ) {
// we always return a tuple after moving
return # [
start [ 0 ] + deltaX ,
start [ 1 ] + deltaY ,
] ;
}
const ship1Moved = move ( ship1 , 1 , 0 ) ;
// passing an array to move() still works:
const ship2Moved = move ( ship2 , 3 , - 1 ) ;
console . log ( ship1Moved === ship2Moved ) ; // true
// ship1 and ship2 have the same coordinates after moving
Im Spielplatz geöffnet
Weitere Beispiele finden Sie hier.
Wie vor Record & Tuple angegeben, sind zutiefst unveränderlich: Der Versuch, ein Objekt in diese einzufügen, führt zu einem TypenError:
const instance = new MyClass ( ) ;
const constContainer = # {
instance : instance
} ;
// TypeError: Record literals may only contain primitives, Records and Tuples
const tuple = # [ 1 , 2 , 3 ] ;
tuple . map ( x => new MyClass ( x ) ) ;
// TypeError: Callback to Tuple.prototype.map may only return primitives, Records or Tuples
// The following should work:
Array . from ( tuple ) . map ( x => new MyClass ( x ) )
Dies definiert die neuen Syntaxstücke, die der Sprache mit diesem Vorschlag hinzugefügt werden.
Wir definieren einen Datensatz- oder Tuple -Expression, indem wir den #
Modifikator vor ansonsten normalen Objekt- oder Array -Ausdrücken verwenden.
# { }
# { a : 1 , b : 2 }
# { a : 1 , b : # [ 2 , 3 , # { c : 4 } ] }
# [ ]
# [ 1 , 2 ]
# [ 1 , 2 , # { a : 3 } ]
Löcher werden im Gegensatz zu Arrays in der Syntax verhindert, die Löcher ermöglichen. Weitere Diskussionen finden Sie in Ausgabe 84.
const x = # [ , ] ; // SyntaxError, holes are disallowed by syntax
Die Verwendung der __proto__
-Kennung als Eigenschaft wird in der Syntax verhindert. Weitere Diskussionen finden Sie in Ausgabe 46.
const x = # { __proto__ : foo } ; // SyntaxError, __proto__ identifier prevented by syntax
const y = # { [ "__proto__" ] : foo } ; // valid, creates a record with a "__proto__" property.
Präzise Methoden werden in der Datensatzsyntax nicht zugelassen.
# { method ( ) { } } // SyntaxError
Datensätze haben möglicherweise nur Stringschlüssel, keine Symbolschlüssel, aufgrund der in Nr. 15 beschriebenen Probleme. Das Erstellen eines Datensatzes mit einem Symbolschlüssel ist ein TypeError
.
const record = # { [ Symbol ( ) ] : # { } } ;
// TypeError: Record may only have string as keys
Datensätze und Tupel enthalten möglicherweise nur Primitive und andere Datensätze und Tupel. Der Versuch, einen Record
oder Tuple
zu erstellen, das ein Object
enthält ( null
ist kein Objekt), oder eine Function
wirft ein TypeError
aus.
const obj = { } ;
const record = # { prop : obj } ; // TypeError: Record may only contain primitive values
Die Gleichheit von Datensätzen und Tupeln funktioniert wie die anderer JS -Primitive -Typen wie booleale und Stringwerte, verglichen nach Inhalten, nicht Identität:
assert ( # { a : 1 } === # { a : 1 } ) ;
assert ( # [ 1 , 2 ] === # [ 1 , 2 ] ) ;
Dies unterscheidet sich von der Funktionsweise von Gleichheit für JS -Objekte: Der Vergleich von Objekten beobachtet, dass jedes Objekt unterschiedlich ist:
assert ( { a : 1 } !== { a : 1 } ) ;
assert ( Object ( # { a : 1 } ) !== Object ( # { a : 1 } ) ) ;
assert ( Object ( # [ 1 , 2 ] ) !== Object ( # [ 1 , 2 ] ) ) ;
Die Einfügungsreihenfolge der Datensatzschlüssel beeinflusst die Gleichheit von Datensätzen nicht, da es keine Möglichkeit gibt, die ursprüngliche Reihenfolge der Schlüssel zu beobachten, wie sie implizit sortiert sind:
assert ( # { a : 1 , b : 2 } === # { b : 2 , a : 1 } ) ;
Object . keys ( # { a : 1 , b : 2 } ) // ["a", "b"]
Object . keys ( # { b : 2 , a : 1 } ) // ["a", "b"]
==
ihre Struktur und ihr Inhalt zutiefst ===
sind Object.is
Record
und Tuple
nach allen Gleichstellungsoperationen auf. . Sie unterscheiden sich in Bezug auf, wie -0
behandelt wird:
Object.is
behandelt -0
und 0
als ungleich==
, ===
und sameValuezero behandeln -0
mit 0
als gleich Beachten Sie, dass ==
und ===
direkter über andere Arten von Werten sind, die in Aufzeichnungen und Tupeln verschachtelt true
-wenn die Inhalte identisch sind (mit Ausnahme von 0
/ -0
). Diese Direktheit hat Auswirkungen auf NaN
und Vergleiche zwischen Typen. Siehe Beispiele unten.
Siehe weitere Diskussion in #65.
assert ( # { a : 1 } === # { a : 1 } ) ;
assert ( # [ 1 ] === # [ 1 ] ) ;
assert ( # { a : - 0 } === # { a : + 0 } ) ;
assert ( # [ - 0 ] === # [ + 0 ] ) ;
assert ( # { a : NaN } === # { a : NaN } ) ;
assert ( # [ NaN ] === # [ NaN ] ) ;
assert ( # { a : - 0 } == # { a : + 0 } ) ;
assert ( # [ - 0 ] == # [ + 0 ] ) ;
assert ( # { a : NaN } == # { a : NaN } ) ;
assert ( # [ NaN ] == # [ NaN ] ) ;
assert ( # [ 1 ] != # [ "1" ] ) ;
assert ( ! Object . is ( # { a : - 0 } , # { a : + 0 } ) ) ;
assert ( ! Object . is ( # [ - 0 ] , # [ + 0 ] ) ) ;
assert ( Object . is ( # { a : NaN } , # { a : NaN } ) ) ;
assert ( Object . is ( # [ NaN ] , # [ NaN ] ) ) ;
// Map keys are compared with the SameValueZero algorithm
assert ( new Map ( ) . set ( # { a : 1 } , true ) . get ( # { a : 1 } ) ) ;
assert ( new Map ( ) . set ( # [ 1 ] , true ) . get ( # [ 1 ] ) ) ;
assert ( new Map ( ) . set ( # [ - 0 ] , true ) . get ( # [ 0 ] ) ) ;
Record
und Tuple
Im Allgemeinen können Sie Aufzeichnungen wie Objekte behandeln. Zum Beispiel funktioniert der Object
und der in
Operator mit Records
.
const keysArr = Object . keys ( # { a : 1 , b : 2 } ) ; // returns the array ["a", "b"]
assert ( keysArr [ 0 ] === "a" ) ;
assert ( keysArr [ 1 ] === "b" ) ;
assert ( keysArr !== # [ "a" , "b" ] ) ;
assert ( "a" in # { a : 1 , b : 2 } ) ;
Record
und Tuple
-Wrapper -Objekte JS -Entwickler müssen in der Regel nicht über Record
und Tuple
-Wrapper -Objekte nachdenken, aber sie sind ein wesentlicher Bestandteil der Funktionsweise von Datensätzen und Tupeln "unter der Haube" in der JavaScript -Spezifikation.
Zugriff auf einen Datensatz oder Tupel über .
oder []
folgt der typischen GetValue
-Semantik, die implizit in eine Instanz des entsprechenden Wrapper -Typs umwandelt. Sie können die Konvertierung auch explizit über Object()
durchführen:
Object(record)
erstellt ein Rekord -Wrapper -ObjektObject(tuple)
erstellt ein Tupel -Wrapper -Objekt (Man könnte sich vorstellen, dass new Record
oder new Tuple
diese Wrapper erstellen könnte, wie new Number
und new String
, aber Aufzeichnungen und Tupel folgen der neueren Konvention, die von Symbol und Bigint festgelegt ist und diese Fälle werfen, da es nicht der Weg ist, den wir wollen Programmierer ermutigen zu nehmen.)
Aufzeichnungs- und Tupel -Wrapper -Objekte haben alle ihre eigenen Eigenschaften mit den writable: false, enumerable: true, configurable: false
. Das Wrapper -Objekt ist nicht erweiterbar. Alle zusammenstellen, sie verhalten sich als gefrorene Objekte. Dies unterscheidet sich von vorhandenen Wrapper -Objekten in JavaScript, ist jedoch notwendig, um die Arten von Fehlern zu geben, die Sie von gewöhnlichen Manipulationen auf Aufzeichnungen und Tupeln erwarten würden.
Eine Instanz des Record
hat die gleichen Schlüssel und Werte wie der zugrunde liegende record
. Das __proto__
jeder dieser Datensatzverpackungsobjekte ist null
(Diskussion: #71).
Eine Instanz von Tuple
verfügt über Schlüsseln, die Ganzzahlen sind, die jedem Index im zugrunde liegenden tuple
entsprechen. Der Wert für jeden dieser Schlüssel ist der entsprechende Wert im ursprünglichen tuple
. Darüber hinaus gibt es einen nicht-erhöhten length
. Insgesamt stimmen diese Eigenschaften mit denen des String
-Wrapper -Objekts überein. Das heißt, Object.getOwnPropertyDescriptors(Object(#["a", "b"]))
und Object.getOwnPropertyDescriptors(Object("ab"))
geben jeweils ein Objekt zurück, das so aussieht:
{
"0" : {
"value" : " a " ,
"writable" : false ,
"enumerable" : true ,
"configurable" : false
},
"1" : {
"value" : " b " ,
"writable" : false ,
"enumerable" : true ,
"configurable" : false
},
"length" : {
"value" : 2 ,
"writable" : false ,
"enumerable" : false ,
"configurable" : false
}
}
Der __proto__
von Tupel -Wrapper -Objekten ist Tuple.prototype
. Tuple.prototype
.prototype
nicht an den Tupelwert selbst angeschlossen. Tuple.prototype
hat verschiedene Methoden, analog zu Arrays.
Für die Integrität gibt die numerische Indexierung von Tupeln außerhalb der Untergrenze undefined
Numerische Indizierung zurück, anstatt wie bei TypeDarrays durch die Prototypkette weiterzuleiten. Die Suche von nicht numerischen Eigenschaftenschlüssel wird bis zu Tuple.prototype
vorgestellt, was wichtig ist, um ihre Array-ähnlichen Methoden zu finden.
Record
und Tuple
-Standardbibliothek Tuple
haben Funktionen im weitesten Anhang zu Array
. In ähnlicher Weise werden Record
durch verschiedene statische Object
unterstützt.
assert . deepEqual ( Object . keys ( # { a : 1 , b : 2 } ) , [ "a" , "b" ] ) ;
assert ( # [ 1 , 2 , 3 ] . map ( x => x * 2 ) , # [ 2 , 4 , 6 ] ) ;
Weitere Informationen zum Record
und den Tuple
-Namespaces finden Sie im Anhang.
Sie können Strukturen mit Record()
, Tuple()
(mit dem Spread Operator), Record.fromEntries()
oder Tuple.from()
konvertieren:
const record = Record ( { a : 1 , b : 2 , c : 3 } ) ;
const record2 = Record . fromEntries ( [ [ "a" , 1 ] , # [ "b" , 2 ] , { 0 : 'c' , 1 : 3 } ] ) ; // note that any iterable of entries will work
const tuple = Tuple ( ... [ 1 , 2 , 3 ] ) ;
const tuple2 = Tuple . from ( [ 1 , 2 , 3 ] ) ; // note that an iterable will also work
assert ( record === # { a : 1 , b : 2 , c : 3 } ) ;
assert ( tuple === # [ 1 , 2 , 3 ] ) ;
Record ( { a : { } } ) ; // TypeError: Can't convert Object with a non-const value to Record
Tuple . from ( [ { } , { } , { } ] ) ; // TypeError: Can't convert Iterable with a non-const value to Tuple
Beachten Sie, dass Record()
, Tuple()
, Record.fromEntries()
und Tuple.from()
kollektionen erwarten, die aus Datensätzen, Tupeln oder anderen Primitiven (wie Zahlen, Zeichenfolgen usw.) bestehen. Verschachtelte Objektreferenzen würden einen Typeerror verursachen. Es liegt an dem Anrufer, innere Strukturen auf die für die Anwendung geeignete Strukturen zu konvertieren.
HINWEIS : Der aktuelle Entwurfsvorschlag enthält keine rekursiven Umwandlungsroutinen, nur flache. Siehe Diskussion in #122
Wie Arrays sind Tupel iterbar.
const tuple = # [ 1 , 2 ] ;
// output is:
// 1
// 2
for ( const o of tuple ) { console . log ( o ) ; }
Ähnlich wie bei Objekten sind Datensätze nur in Verbindung mit APIs wie Object.entries
iterbar.
const record = # { a : 1 , b : 2 } ;
// TypeError: record is not iterable
for ( const o of record ) { console . log ( o ) ; }
// Object.entries can be used to iterate over Records, just like for Objects
// output is:
// a
// b
for ( const [ key , value ] of Object . entries ( record ) ) { console . log ( key ) }
JSON.stringify(record)
entspricht dem Aufruf JSON.stringify
für das Objekt, das sich aus der rekursiven Konvertierung des Datensatzes in ein Objekt, das keine Datensätze oder Tupel enthält, resultiert.JSON.stringify(tuple)
entspricht dem Aufruf JSON.stringify
auf dem Array, das sich aus der rekursiven Konvertierung des Tupels in ein Array ergibt, das keine Datensätze oder Tupel enthält. JSON . stringify ( # { a : # [ 1 , 2 , 3 ] } ) ; // '{"a":[1,2,3]}'
JSON . stringify ( # [ true , # { a : # [ 1 , 2 , 3 ] } ] ) ; // '[true,{"a":[1,2,3]}]'
Bitte beachten Sie https://github.com/tc39/proposal-json-parseimmable
Tuple.prototype
Tuple
unterstützt Instanzmethoden ähnlich wie Array mit wenigen Änderungen:
Tuple.prototype.push
.Tuple.prototype.withAt
. Der Anhang enthält eine vollständige Beschreibung des Prototyps von Tuple
.
typeof
typeof
identifiziert Datensätze und Tupel als unterschiedliche Typen:
assert ( typeof # { a : 1 } === "record" ) ;
assert ( typeof # [ 1 , 2 ] === "tuple" ) ;
Map
| Set
| WeakMap
| WeakSet
} Es ist möglich, einen Record
oder Tuple
als Schlüssel in einer Map
und als Wert in einem Set
zu verwenden. Wenn Sie hier einen Record
oder Tuple
verwenden, werden sie nach Wert verglichen.
Es ist nicht möglich, einen Record
oder Tuple
als Schlüssel in einer WeakMap
oder als Wert in einem WeakSet
zu verwenden, da Records
und Tuple
keine Objects
sind und ihre Lebensdauer nicht beobachtet werden kann.
const record1 = # { a : 1 , b : 2 } ;
const record2 = # { a : 1 , b : 2 } ;
const map = new Map ( ) ;
map . set ( record1 , true ) ;
assert ( map . get ( record2 ) ) ;
const record1 = # { a : 1 , b : 2 } ;
const record2 = # { a : 1 , b : 2 } ;
const set = new Set ( ) ;
set . add ( record1 ) ;
set . add ( record2 ) ;
assert ( set . size === 1 ) ;
const record = # { a : 1 , b : 2 } ;
const weakMap = new WeakMap ( ) ;
// TypeError: Can't use a Record as the key in a WeakMap
weakMap . set ( record , true ) ;
const record = # { a : 1 , b : 2 } ;
const weakSet = new WeakSet ( ) ;
// TypeError: Can't add a Record to a WeakSet
weakSet . add ( record ) ;
Ein zentraler Vorteil der Aufzeichnungen und des Tupels ist, dass sie durch ihren Inhalt verglichen werden, nicht nach ihrer Identität. Gleichzeitig hat ===
in JavaScript auf Objekten eine sehr klare, konsistente Semantik: die Objekte nach Identität zu vergleichen. Das Erstellen von Aufzeichnungen und Tupeln Primitive ermöglicht den Vergleich auf der Grundlage ihrer Werte.
Auf einer hohen Ebene bildet die Objekt/primitive Unterscheidung eine harte Grenze zwischen der zutiefst unveränderlichen, kontextfreien, identitätsfreien Welt und der Welt der veränderlichen Objekte darüber. Diese Kategorie -Trennung macht das Design und das mentale Modell klarer.
Eine Alternative zur Implementierung von Datensatz und Tupel als Primitive wäre die Verwendung der Operatorüberlastung, um ein ähnliches Ergebnis zu erzielen, indem ein überlasteter abstrakter Gleichheit ( ==
) implementiert wird, der Objekte tief vergleicht. Dies ist zwar möglich, erfüllt jedoch nicht den vollständigen Anwendungsfall, da die Überladung des Bedieners keine Überschreibung für den Operator ===
bietet. Wir möchten, dass der Operator der strengen Gleichheit ( ===
) eine zuverlässige Überprüfung der "Identität" für Objekte und "beobachtbarer Wert" (Modulo -0/+0/nan) für primitive Typen ist.
Eine andere Möglichkeit besteht darin, sogenannte Einlagen auszuführen: Wir verfolgen weltweit Aufzeichnung oder Tuple -Objekte. Wenn wir versuchen, eine neue zu erstellen, die mit einem vorhandenen Datensatzobjekt identisch ist, verweisen wir nun auf diesen vorhandenen Datensatz, anstatt ein neues zu erstellen. Dies ist im Wesentlichen das, was die Polyfill tut. Wir gleichwertig Wert und Identität. Dieser Ansatz schafft Probleme, sobald wir dieses Verhalten in mehreren JavaScript-Kontexten erweitern und von Natur aus keine tiefe Unveränderlichkeit verleihen würden, und es ist besonders langsam, was die Verwendung von Datensatz und Tupel zu einer leistungsnegativen Wahl macht.
Record & Tuple ist so erstellt, dass sie gut mit Objekten und Arrays zusammenarbeiten: Sie können sie genauso lesen wie mit Objekten und Arrays. Die Hauptänderung liegt in der tiefen Unveränderlichkeit und dem Vergleich nach Wert anstelle von Identität.
Entwickler manipulierten Objekte auf unveränderliche Weise (z. B. das Transformieren von Teilen von Redux -Zustand), können weiterhin die gleichen Manipulationen durchführen, die sie früher für Objekte und Arrays durchgeführt haben, diesmal mit mehr Garantien.
Wir werden durch Interviews und Umfragen empirische Forschung durchführen, um herauszufinden, ob dies funktioniert, wie wir es glauben.
.get()
/ .set()
-Methoden wie Iminual.js?Wenn wir den Zugriff auf Aufzeichnungen und Tupel wie im vorherigen Abschnitt beschriebenen Objekten und Arrays beibehalten möchten, können wir uns nicht auf Methoden verlassen, um diesen Zugriff auszuführen. Wenn Sie dies tun, müssen wir den Code für den Verzweigungscode erfordern, wenn wir versuchen, eine "generische" Funktion zu erstellen, die Objekte/Arrays/Datensätze/Tupel aufnehmen kann.
Hier ist eine Beispielfunktion, die für unveränderliche .JS -Datensätze und gewöhnliche Objekte unterstützt wird:
const ProfileRecord = Immutable . Record ( {
name : "Anonymous" ,
githubHandle : null ,
} ) ;
const profileObject = {
name : "Rick Button" ,
githubHandle : "rickbutton" ,
} ;
const profileRecord = ProfileRecord ( {
name : "Robin Ricard" ,
githubHandle : "rricard" ,
} ) ;
function getGithubUrl ( profile ) {
if ( Immutable . Record . isRecord ( profile ) ) {
return `https://github.com/ ${
profile . get ( "githubHandle" )
} ` ;
}
return `https://github.com/ ${
profile . githubHandle
} ` ;
}
console . log ( getGithubUrl ( profileObject ) ) // https://github.com/rickbutton
console . log ( getGithubUrl ( profileRecord ) ) // https://github.com/rricard
Dies ist fehleranfällig, da beide Zweige im Laufe der Zeit leicht aus der Synchronisierung herauskommen können ...
So würden wir diese Funktion schreiben, die Aufzeichnungen aus diesem Vorschlag und gewöhnlichen Objekten entnommen:
const profileObject = {
name : "Rick Button" ,
githubHandle : "rickbutton" ,
} ;
const profileRecord = # {
name : "Robin Ricard" ,
githubHandle : "rricard" ,
} ;
function getGithubUrl ( profile ) {
return `https://github.com/ ${
profile . githubHandle
} ` ;
}
console . log ( getGithubUrl ( profileObject ) ) // https://github.com/rickbutton
console . log ( getGithubUrl ( profileRecord ) ) // https://github.com/rricard
Diese Funktion unterstützt sowohl Objekte als auch Datensätze in einem einzelnen Code-Pfad und zwingt den Verbraucher nicht, auszuwählen, welche Datenstrukturen verwendet werden sollen.
Warum müssen wir beide gleichzeitig gleichzeitig unterstützen? Dies dient in erster Linie, um einen Ökosystem -Split zu vermeiden. Nehmen wir an, wir verwenden unveränderliche Js, um unser Staatsmanagement durchzuführen, aber wir müssen unseren Staat einigen externen Bibliotheken, die sie nicht unterstützen, übertragen:
state . jobResult = Immutable . fromJS (
ExternalLib . processJob (
state . jobDescription . toJS ( )
)
) ;
Sowohl toJS()
als auch fromJS()
können je nach Größe der Unterstrukturen sehr teure Vorgänge sein. Ein Ökosystem -Split bedeutet Konvertierungen, die wiederum mögliche Leistungsprobleme bedeuten.
Die vorgeschlagene Syntax verbessert die Ergonomie der Verwendung Record
und Tuple
im Code signifikant. Zum Beispiel:
// with the proposed syntax
const record = # {
a : # {
foo : "string" ,
} ,
b : # {
bar : 123 ,
} ,
c : # {
baz : # {
hello : # [
1 ,
2 ,
3 ,
] ,
} ,
} ,
} ;
// with only the Record/Tuple globals
const record = Record ( {
a : Record ( {
foo : "string" ,
} ) ,
b : Record ( {
bar : 123 ,
} ) ,
c : Record ( {
baz : Record ( {
hello : Tuple (
1 ,
2 ,
3 ,
) ,
} ) ,
} ) ,
} ) ;
Die vorgeschlagene Syntax soll einfacher und einfacher zu verstehen sind, da sie absichtlich der Syntax für Objekt- und Array -Literale ähnelt. Dies nutzt die vorhandene Vertrautheit des Benutzers mit Objekten und Arrays. Darüber hinaus führt das zweite Beispiel zusätzliche temporäre Objektliterale ein, was zur Komplexität des Ausdrucks beiträgt.
Die Verwendung eines Schlüsselworts als Präfix für die Standard -Objekt-/Array -Literal -Syntax zeigt Probleme um die Rückwärtskompatibilität. Darüber hinaus kann die Wiederverwendung vorhandener Schlüsselwörter Mehrdeutigkeiten einführen.
ECMascript definiert eine Reihe von reservierten Schlüsselwörtern , die für zukünftige Erweiterungen der Sprache verwendet werden können. Das Definieren eines neuen Keywords, das noch nicht reserviert ist, ist theoretisch möglich, erfordert jedoch erhebliche Anstrengungen, um zu bestätigen, dass das neue Keyword wahrscheinlich nicht die Kompatibilität für die Rückwärtsspurung durchbricht.
Die Verwendung eines reservierten Schlüsselworts erleichtert diesen Vorgang, aber es ist keine perfekte Lösung, da keine reservierten Schlüsselwörter mit der "Absicht" der Funktion als const
übereinstimmen. Das const
-Schlüsselwort ist ebenfalls schwierig, da es ein ähnliches Konzept beschreibt (Unveränderlichkeit der variablen Referenz), während dieser Vorschlag neue unveränderliche Datenstrukturen hinzufügen soll. Während Unveränderlichkeit der gemeinsame Faden zwischen diesen beiden Merkmalen ist, gab es ein erhebliches Feedback der Gemeinschaft, das darauf hinweist, dass die Verwendung const
in beiden Kontexten unerwünscht ist.
Anstatt ein Schlüsselwort zu verwenden, {| |}
und [||]
wurden als mögliche Alternativen vorgeschlagen. Derzeit neigt die Champion -Gruppe in Richtung #[]
/ #{}
, aber die Diskussion wird in #10 fortgesetzt.
Die Definition von Record & Tuple als zusammengesetzte Primitive erzwingt alles in Record & Tuple, keine Objekte zu sein. Dies kommt mit einigen Nachteilen (die Verweise von Objekten wird schwieriger, ist aber immer noch möglich), aber auch mehr Garantien, um häufige Programmierfehler zu vermeiden.
const object = {
a : {
foo : "bar" ,
} ,
} ;
Object . freeze ( object ) ;
func ( object ) ;
// func is able to mutate object’s keys even if object is frozen
Im obigen Beispiel versuchen wir, eine Garantie für Unveränderlichkeit mit Object.freeze
zu erstellen. Da wir das Objekt nicht tief gefroren haben, sagt uns nichts, dass object.a
unberührt wurde. Mit Record & Tuple ist diese Einschränkung von Natur aus und es besteht kein Zweifel, dass die Struktur unberührt ist:
const record = # {
a : # {
foo : "bar" ,
} ,
} ;
func ( record ) ;
// runtime guarantees that record is entirely unchanged
assert ( record . a . foo === "bar" ) ;
Schließlich unterdrückt eine tiefe Unveränderlichkeit die Notwendigkeit eines gemeinsamen Musters, das aus tiefklonenden Objekten besteht, um Garantien zu erhalten:
const clonedObject = JSON . parse ( JSON . stringify ( object ) ) ;
func ( clonedObject ) ;
// now func can have side effects on clonedObject, object is untouched
// but at what cost?
assert ( object . a . foo === "bar" ) ;
Im Allgemeinen funktioniert der Spread -Operator gut dafür:
// Add a Record field
let rec = # { a : 1 , x : 5 }
# { ... rec , b : 2 } // #{ a: 1, b: 2, x: 5 }
// Change a Record field
# { ... rec , x : 6 } // #{ a: 1, x: 6 }
// Append to a Tuple
let tup = # [ 1 , 2 , 3 ] ;
# [ ... tup , 4 ] // #[1, 2, 3, 4]
// Prepend to a Tuple
# [ 0 , ... tup ] // #[0, 1, 2, 3]
// Prepend and append to a Tuple
# [ 0 , ... tup , 4 ] // #[0, 1, 2, 3, 4]
Und wenn Sie etwas in einem Tupel ändern, funktioniert die Methode Tuple.prototype.with
:
// Change a Tuple index
let tup = # [ 1 , 2 , 3 ] ;
tup . with ( 1 , 500 ) // #[1, 500, 3]
Einige Manipulationen von "tiefen Wegen" können etwas umständlich sein. Zu diesem Zweck fügt die Deep Path -Eigenschaften für Datensätze den Vorschlag zusätzliche Kurzsyntax hinzu, um Literale aufzuzeichnen.
Wir entwickeln den Vorschlag für Deep Path Properties als separate Vorschlag, da wir ihn nicht als Kern für die Verwendung von Datensätzen betrachten, die unabhängig voneinander funktionieren. Es ist die Art von syntaktischer Ergänzung, die gut für den Prototypen im Laufe der Zeit bei Transpilern passt, und wo wir viele Entscheidungspunkte haben, die nicht mit Aufzeichnungen und Tupeln zu tun haben (z. B. wie es mit Objekten funktioniert).
Wir haben mit den Readonly Collections Champions gesprochen, und beide Gruppen sind sich einig, dass es sich um Ergänzungen handelt:
Keiner einer ist eine Untergruppe der anderen in Bezug auf die Funktionalität. Bestenfalls sind sie parallel, genau wie jeder Vorschlag parallel zu anderen Sammlungstypen in der Sprache.
Die beiden Champion -Gruppen haben sich also entschlossen, sicherzustellen, dass die Vorschläge parallel zueinander sind. Dieser Vorschlag fügt beispielsweise eine neue Methode Tuple.prototype.withReversed
hinzu. Die Idee wäre, während des Entwurfsprozesses zu überprüfen, ob diese Unterschrift auch für Arrays nur schreibgeschützt ist (falls diese existieren): Wir haben diese neuen Methoden durch Kopiervorschlag an das Änderungsarray extrahiert, damit wir eine API diskutieren können Das baut ein konsistentes, gemeinsames mentales Modell auf.
In den aktuellen Vorschlägen gibt es keine überlappenden Typen für die gleichen Daten, aber beide Vorschläge könnten in dieser Zukunft in diese Richtungen wachsen, und wir versuchen, diese Dinge im Voraus zu überlegen. Wer weiß, ein Tag könnte TC39 eines Tages entscheiden, primitive RecordMap- und Datensatztypen hinzuzufügen, wie die zutiefst unveränderlichen Versionen von Set und MAP! Und diese würden parallel zu Readonly -Sammlungstypen sein.
TC39 diskutiert seit langem über "Werttypen", die seit mehreren Jahren eine Art Klassenerklärung für einen primitiven Typ sein würden. Eine frühere Version dieses Vorschlags machte sogar einen Versuch. Dieser Vorschlag versucht, einfach und minimal zu beginnen und nur die Kernstrukturen bereitzustellen. Die Hoffnung ist, dass es das Datenmodell für einen zukünftigen Vorschlag für Klassen bereitstellen könnte.
Dieser Vorschlag hängt locker mit einem breiteren Satz von Vorschlägen zusammen, einschließlich Operator-Überladung und erweiterten numerischen Literalen: Diese alle verschwören sich, um benutzerdefinierte Typen eine Möglichkeit zu bieten, dasselbe wie Bigint zu tun. Die Idee ist jedoch, diese Funktionen hinzuzufügen, wenn wir feststellen, dass sie unabhängig motiviert sind.
Wenn wir benutzerdefinierte primitive/Werttypen hätten, könnte es sinnvoll sein, sie in integrierten Funktionen wie CSS-Typed OM oder den zeitlichen Vorschlag zu verwenden. Dies ist jedoch weit in der Zukunft, wenn es jemals passiert; Im Moment funktioniert es gut, Objekte für diese Art von Funktionen zu verwenden.
Obwohl beide Arten von Datensätzen sich auf Objekte beziehen und beide Arten von Tupeln sich auf Arrays beziehen, ist dies ungefähr dort, wo die Ähnlichkeit endet.
Datensätze in TypeScript sind ein generischer Dienstprogrammtyp, um ein Objekt darzustellen, das einen Tastetyp mit einem Werttyp übereinstimmt. Sie repräsentieren immer noch Objekte.
Ebenso sind Tupel in TypeScript eine Notation, um Typen in einem Array einer begrenzten Größe auszudrücken (beginnend mit TypeScript 4.0, sie haben eine variadische Form). Tupel in TypeScript sind eine Möglichkeit, Arrays mit heterogenen Typen auszudrücken. ECMascript -Tupel können TS -Arrays oder TS -Tupeln leicht entsprechen, da sie entweder eine unbestimmte Anzahl von Werten desselben Typs enthalten oder eine begrenzte Anzahl von Werten mit unterschiedlichen Typen enthalten können.
TS -Aufzeichnungen oder Tupel sind orthogonale Merkmale für ECMascript -Datensätze und Tupel und beide können gleichzeitig ausgedrückt werden:
const record : Readonly < Record < string , number > > = # {
foo : 1 ,
bar : 2 ,
} ;
const tuple : readonly [ number , string ] = # [ 1 , "foo" ] ;
Dieser Vorschlag bietet keine Leistungsgarantien und erfordert keine spezifischen Optimierungen bei Implementierungen. Basierend auf dem Feedback von Implementierern wird erwartet, dass sie gemeinsame Operationen über "lineare Zeit" -Algorithmen implementieren. Dieser Vorschlag verhindert jedoch einige klassische Optimierungen für rein funktionale Datenstrukturen, einschließlich, aber nicht beschränkt auf:
Diese Optimierungen sind analog zu der Art und Weise, wie moderne JavaScript -Motoren mit verschiedenen internen Arten von Saiten mit String -Verkettung umgehen. Die Gültigkeit dieser Optimierungen beruht auf der Nichtbeobachtbarkeit der Identität von Datensätzen und Tupeln. Es ist nicht zu erwarten, dass alle Motoren in Bezug auf diese Optimierungen identisch handeln, sondern dass sie jeweils Entscheidungen darüber treffen, welche bestimmte Heuristiken verwendet werden sollen. Vor Stufe 4 dieses Vorschlags planen wir, einen Leitfaden für Best Practices für die optimierbare Verwendung von Aufzeichnungen und Tupeln zu veröffentlichen, basierend auf der Implementierungserfahrung, die wir zu diesem Zeitpunkt haben werden.
Eine neue, zutiefst unveränderliche, zusammengesetzte primitive Typdatenstruktur, die in diesem Dokument vorgeschlagen wurde, die zu Objekt analog ist. #{ a: 1, b: 2 }
Eine neue, zutiefst unveränderliche, zusammengesetzte primitive Typdatenstruktur, die in diesem Dokument vorgeschlagen wurde und der Array analog ist. #[1, 2, 3, 4]
Werte, die wie andere JavaScript -Primitiven wirken, aber aus anderen Bestimmungswerten bestehen. In diesem Dokument werden die ersten beiden zusammengesetzten primitiven Typen vorgeschlagen: Record
und Tuple
.
String
, Number
, Boolean
, undefined
, null
, Symbol
und BigInt
Dinge, die entweder zusammengesetzte oder einfache primitive Typen sind. Alle Primitiven in JavaScript teilen bestimmte Eigenschaften:
Eine Datenstruktur, die Operationen, die sie intern ändern, nicht akzeptiert, sondern stattdessen Vorgänge aufweist, die einen neuen Wert zurückgeben, der das Ergebnis der Anwendung dieses Vorgangs darauf zurückzuführen ist.
In diesem Vorschlag sind Record
und Tuple
zutiefst unveränderliche Datenstrukturen.
Der Operator ===
ist mit dem strengen Gleichheitsvergleichsalgorithmus definiert. Strikte Gleichheit bezieht sich auf diesen besonderen Begriff der Gleichheit.
Die Strukturfreigabe ist eine Technik, mit der der Speicher Fußabdruck unveränderlicher Datenstrukturen eingeschränkt wird. Kurz gesagt, bei der Anwendung einer Operation, um eine neue Version einer unveränderlichen Struktur abzuleiten, wird der strukturelle Teilen versuchen, den größten Teil der internen Struktur sowohl von den alten als auch von abgeleiteten Versionen dieser Struktur intakt zu halten. Dies schränkt die Höhe der Kopie stark ein, um die neue Struktur abzuleiten.