记录和元组只能包含基元和其他记录和元组。您可以将Records和Tuples视为复合原语。由于完全基于基元而不是对象,因此记录和元组是完全不变的。
记录和元组支持舒适的构造、操作和使用习惯用法,类似于使用对象和数组。他们被深入比较的是他们的内容,而不是他们的身份。
JavaScript引擎可以对记录和元组的构造、操作和比较执行某些优化,类似于字符串通常在JS引擎中实现的方式。(应该理解的是,不保证这些优化。)。
记录和元组旨在通过外部类型系统超集(如TypeScript或Flow)使用和理解。
今天,用户地库实现了类似的概念,例如Immutable.js。此外,之前的提案也曾尝试过,但由于提案的复杂性和缺乏足够的用例而放弃。
这个新的建议仍然受到以前的建议的启发,但是引入了一些重要的更改:记录和元组现在是完全不变的。此属性从根本上基于这样的观察:在大型项目中,随着存储和传递的数据量的增加,混合不可变和可变数据结构的风险也会增加,因此您更有可能处理大型记录和元组结构。这可能会引入难以找到的错误。
作为一种内置的、高度不变的数据结构,与Userland库相比,此建议还提供了一些可用性优势:
记录和元组在调试器中很容易自省,而库提供的不可变类型通常很难检查,因为您必须通过数据结构详细信息进行检查。
因为它们是通过典型的对象和数组习惯用法访问的,所以不需要额外的分支就可以编写同时使用不可变对象和JS对象的泛型库;对于用户库,可能只在不可变的情况下才需要方法调用。
我们避免了开发人员可能昂贵地在常规JS对象和不可变结构之间进行转换的情况,因为这使得总是使用不可变的结构变得更容易。
IMMER是不可变数据结构的一种值得注意的方法,它规定了通过生成器和减法器进行操作的模式。但是,它不提供不可变的数据类型,因为它生成冻结的对象。除了冻结的对象之外,该相同的模式还可以适用于本提案中定义的结构。
用户库中定义的深度相等可能会有很大差异,部分原因是对可变对象的可能引用。通过在只包含基元、记录和元组的深度上划出一条硬线,并递归遍历整个结构,该建议定义了简单、统一的比较语义。
Const document=#{id:1234,title:";Record&;Tuple Proposal";,Const&;Tuple Proposal";,Const document=#{id:1234;Title:";Record&;Tuple";,//tuple";,";,";TC39";,";Proposal";,";,";记录";,";元组";],};//像访问对象一样访问键!控制台。日志(文档。Title);//记录&;元组提案控制台。日志(文档。关键词[1]);//TC39//像物体一样散布!常量文档2=#{...。文档,标题:";阶段2:记录&;Tuple";,};控制台。日志(文档。标题);//阶段2:录制元组控制台(&A;T)。日志(文档。关键词[1]);//TC39//记录上的对象功函数:控制台。日志(对象。密钥(文档));//[";目录";,";id";,";关键字";,";标题";]。
Const shi1=#{x:1,y:2};//Ship2是一个普通对象:const shi2={x:-1,y:3};函数move(start,deltaX,deltaY){//我们总是在移动后返回一条记录,返回#{x:start。X+deltaX,y:开始。Y+deltaY,};}const shi1Moved=Move(Ship1,1,0);//传递普通对象来Move()仍然有效:const shi2Moved=Move(Ship2,3,-1);console。Log(shi1Moved=shi2Moved);//true//Ship1和Ship2移动后坐标相同。
Const measures=#[42,12,67,";MEASURE ERROR:FOO HEREED";];//像访问数组一样访问索引!控制台。Log(Measures[0]);//42控制台。Log(Measures[3]);//测量错误:FOO发生//切片散布成数组!常量已更正的度量值=#[...。措施。切片(0,测量。长度-1),-1];控制台。Log(已更正的测量[0]);//42控制台。Log(CorrectedMeasures[3]);//-1//或使用.with()速记以获得相同的结果:const meditedMeasures2=document。带有(3,-1);控制台。Log(已更正的Measures2[0]);//42控制台。日志(已更正的测量2[3]);//-1//类似于阵列控制台的元组支持方法。日志(已更正的测量2。Map(x=>;x+1);//#[43,13,68,0]。
Const shi1=#[1,2];//shi2为数组:const shi2=[-1,3];函数move(start,deltaX,deltaY){//移动后始终返回元组#[start[0]+deltaX,start[1]+deltaY,];}const shi1Moved=Move(Ship1,1,0);//传递数组Move()仍然有效:const shi2Moved=Move(Ship2。Log(shi1Moved=shi2Moved);//true//Ship1和Ship2移动后坐标相同
如前所述,记录&;元组是完全不变的:尝试在其中插入对象将导致TypeError:
Const instance=new MyClass();const conContainer=#{instance:instance};//TypeError:记录文本只能包含基元、记录和元组const tuple=#[1,2,3];tuple。Map(x=>;new MyClass(X));//TypeError:对Tuple.Prototype.map的回调只能返回基元、记录或元组//应该可以执行以下操作:Array。从(元组)。Map(x=>;new MyClass(X))。
这定义了使用此建议添加到语言中的新语法。
我们通过在正常的对象或数组表达式前面使用#修饰符来定义记录或元组表达式。
#{}#{a:1,b:2}#{a:1,b:#[2,3,#{c:4}]}#[]#[1,2]#[1,2,#{a:3}]。
与允许空洞的数组不同,语法中防止了空洞。更多讨论见第84期。
语法中禁止将__proto__标识符用作属性。更多讨论见第46期。
Const x=#{__proto__:foo};//语法错误,语法阻止的__proto__标识符const y=#{";__proto__";:foo};//有效,使用";__proto__&34;属性创建记录。
由于#15中描述的问题,记录可能只有字符串键,没有符号键。使用符号键创建记录是TypeError。
Const记录=#{[Symbol()]:#{}};//TypeError:记录只能将字符串作为键。
记录和元组只能包含基元和其他记录和元组。试图创建包含除以下类型之外的任何类型的记录或元组:Record、Tuple、String、Number、Symbol、Boolean、Bigint、UnDefined或NULL属于TypeError。
记录和元组相等的工作原理与其他JS基元类型(如布尔值和字符串值)类似,按内容进行比较,而不是按标识进行比较:
这与JS对象的相等工作方式不同:对对象进行比较会发现每个对象都是不同的:
Assert({a:1}!=={a:1});Assert(Object(#{a:1})!==Object(#{a:1}));Assert(Object(#[1,2])!==Object(#[1,2]));
记录键的插入顺序不会影响记录的相等,因为无法观察键的原始顺序,因为它们是隐式排序的:
Assert(#{a:1,b:2}=#{b:2,a:1});Object。Key(#{a:1,b:2})//[";a";,";b";]对象。密钥(#{b:2,a:1})//[";a";,";b";]
如果它们的结构和内容完全相同,则根据所有相等操作(Object.is、==、=)和内部SameValueZero算法(用于比较Map和Set的键),将Record和Tuple值视为相等。它们在如何处理-0方面有所不同:
请注意,==和=更直接地表示嵌套在Records和Tuples中的其他类型的值--当且仅当内容相同(0/-0除外)时才返回TRUE。这种直接性对NaN以及跨类型的比较都有影响。请参见下面的示例。
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(!对象。Is(#{a:-0},#{a:+0}));Assert(!对象。Is(#[-0],#[+0]));Assert(Object。Is(#{a:nan},#{a:nan}));Assert(Object。Is(#[nan],#[nan]));//映射键与SameValueZero算法断言(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]));
通常,您可以将记录视为对象。例如,Object命名空间和In运算符使用Records。
Const keysArr=Object。Key(#{a:1,b:2});//返回数组[";a";,";b";]assert(keysArr[0]=";a";);assert(keysArr[1]=";b";);assert(keysArr!==#[";a";,";b";]。Assert(#{a:1,b:2}中的";a";);
JS开发人员通常不必考虑Record和Tuple包装器对象,但它们是JavaScript规范中隐藏在幕后的Records和Tuples工作方式的关键部分。
通过访问记录或元组。Or[]遵循典型的GetValue语义,该语义隐式转换为相应包装类型的实例。您也可以通过object()显式执行转换:
(可以想象,新的记录或新的元组可以创建这些包装器,就像新的数字和新的字符串一样,但是记录和元组遵循由Symbol和BigInt设置的较新的约定,这使得这些情况抛出,因为这不是我们想要鼓励程序员走的道路。)
Record和Tuple包装器对象都有自己的所有属性,属性分别为Writable:False、Enumerable:True、Configurable:False。包装对象不可扩展。所有这些放在一起,它们的行为就像冻结的对象。这与JavaScript中现有的包装器对象不同,但是对于给出您期望从对Records和Tuples的普通操作中得到的错误种类来说,这是必要的。
记录的实例具有与基础记录值相同的键和值。每个记录包装对象的__proto__为空(讨论:#71)。
对于基础元组值中的每个索引,Tuple实例具有${index}的键。每个键的值都是原始元组中的相应值。此外,还有一个不可枚举的长度密钥。总体而言,这些属性与字符串包装对象的属性相匹配。即,Object.getOwnPropertyDescriptors(Object(#[";a";,";b";]))和Object.getOwnPropertyDescriptors(new String(";ab";))各自返回如下所示的对象:
{";0";:{";值";:";a";,";:false,";可配置";:true,";可配置";:false},";1";:{";value";:";b";,";可写";:假,";可枚举";:真,";可配置";:假},";长度";:{";值";:2,";可写";:假,";可枚举";:假,";可配置";:假}}。
元组包装对象的__proto__是Tuple.Prototype。请注意,如果您正在处理不同的JavaScript全局对象(Realms),则在执行对象转换时会根据当前领域选择Tuple.type--它不会附加到Tuple值本身。Prototype上有各种方法,类似于数组。
为了完整性,元组上的越界数字索引返回未定义的结果,而不是像TypedArray那样通过原型链向上转发。非数字属性键的查找最高可转发到Tuple.type,这对于查找它们的类似数组的方法非常重要。
元组值具有与Array大致相似的功能。同样,记录值由不同的对象静态方法支持。
断言(对象。KEYS(#{a:1,b:2})=#[";a";,";b";]);Assert(#[1,2,3]。Map(x=>;x*2),#[2,4,6]);
常量记录=记录({a:1,b:2,c:3});常量记录2=记录。FromEntries([#[";a";,1],#[";b";,2],#[";c";:3]]);//请注意,迭代器也可以使用const tuple=Tuple。From([1,2,3]);//请注意,迭代器也将工作Assert(Record=#{a:1,b:2,c:3});Assert(tuple=#[1,2,3]);Record。From({a:{}});//TypeError:无法将具有非常数值的对象转换为记录元组。From([{},{},{}]);//TypeError:无法将带非常数值的迭代转换为元组。
请注意,Records()和Tuple.from()需要由Records、Tuples或其他原语(如Numbers、String等)组成的集合。嵌套的对象引用将导致TypeError。以任何适合应用程序的方式转换内部结构取决于调用者。
注:当前的提案草案不包含递归转换例程,只包含浅层例程。参见#122中的讨论。
当调用元组时,只会抛出一个错误--当元组是不可变的时,类似数组的语义没有多大意义/用处。
Const tuple=#[1,2];//输出为://1//2 for(Const O Of Tuple){Console。Log(O);}。
常量记录=#{a:1,b:2};//TypeError:记录对于(常量记录){Console不可迭代。Log(O);}//Object.Entries可用于迭代记录,就像对象一样//输出为://a//b for(const[key,value]of Object。条目(记录)){控制台。日志(密钥)}。
JSON.stringify(Record)的行为等同于在将记录递归转换为不包含任何记录或元组的对象后对对象调用JSON.stringify。
JSON.stringify(Tuple)的行为等同于在将记录递归转换为不包含任何记录或元组的对象时对数组调用JSON.stringify。
杰森。Stringify(#{a:#[1,2,3]});//';{";a";:[1,2,3]}';json。Stringify(#[true,#{a:#[1,2,3]}]);//';[true,{";a";:[1,2,3]}]';
我们建议添加JSON.parseImmutable,这样我们就可以从JSON字符串而不是Object/Array中提取Record/Tuple类型。
JSON.parseImmutable的签名与JSON.parse相同,唯一的变化是返回类型现在是记录或元组。
Tuple和Array方法的机制略有不同;Array方法通常依赖于能够增量地修改Array,并且是为子类化而构建的,这两种方法都不适用于Tuples。
改变数组的操作将被返回新的、修改后的数组的操作替换。因为它有一个不同的签名,所以有一个不同的名称,例如,与Array.Prototype.ush并行推送的Tuple.Prototype.Push。
我们添加了Tuple.Prototype.with(),它返回一个新的元组,该元组的值在给定的索引处发生了更改。
可以将记录或元组用作Map中的键,也可以用作集合中的值。在这里使用记录或元组时,它们是按值进行比较的。
不能将记录或元组用作WeakMap中的键或WeakSet中的值,因为Records和Tuple不是对象,它们的生存期是不可观察的。
常量记录1=#{a:1,b:2};常量记录2=#{a:1,b:2};常量map=new Map();map。Set(record1,true);assert(map.。Get(Record2));
常量记录1=#{a:1,b:2};常量记录2=#{a:1,b:2};常量集=new set();set。Add(Record1);set。Add(Record2);Assert(设置。尺寸=1);
常量记录=#{a:1,b:2};常量弱映射=新的WeakMap();//TypeError:不能将记录用作WeakMap弱映射中的键。Set(记录,true);
Const Record=#{a:1,b:2};Const WeakSet=new WeakSet();//TypeError:无法将记录添加到WeakSet WeakSet。添加(记录);
为什么要引入新的原始类型?为什么不直接使用不可变数据结构库中的对象呢?
“记录和元组”提案的一个核心好处是,它们是按内容进行比较的,而不是按身份进行比较的。同时,JavaScript中的=在对象上具有非常清晰、一致的语义:通过标识来比较对象。创建记录和元组基元可以根据它们的值进行比较。
从高层次上讲,对象/原语的区别有助于在深度不变的、上下文无关的、身份无关的世界和它上面的可变对象世界之间形成一条硬线。这种类别划分使设计和心理模型更加清晰。
将Record和Tuple实现为基元的另一种方法是使用运算符重载来实现类似的结果,方法是实现一个重载的抽象相等(==)运算符来深入比较对象。虽然这是可能的,但它不能满足全部用例,因为操作符重载不会为=操作符提供覆盖。我们希望严格相等(=)运算符是对象的";身份";和原始类型的";可观测值";(模-0/+0/NaN)的可靠检查。
另一种选择是执行所谓的交互:我们全局跟踪记录或元组对象,如果我们尝试创建恰好与现有记录对象相同的新记录,则现在引用该现有记录,而不是创建新记录。这基本上就是多边形填充的功能。我们现在把价值和身份等同起来。一旦我们将该行为扩展到多个JavaScript上下文中,这种方法就会产生问题,而且本质上不会提供深度的不变性,而且速度特别慢,这将使使用Record&;Tuple成为对性能不利的选择。
Record&;Tuple是为与对象和数组很好地互操作而构建的:您可以像读取对象和数组一样读取它们。主要的变化在于深层的不变性和以价值代替同一性的比较。
习惯于以不可变的方式操作对象(例如转换Redux状态的片段)的开发人员将能够继续执行他们过去在对象和数组上所做的相同操作,这一次有了更多的保证。
我们将通过访谈和调查进行实证研究,以确定这是否。
.