المؤلفون:
الأبطال:
المستشارون
المرحلة: 2
Record
و Tuple
يقدم هذا الاقتراح هيكلين جديدين للبيانات غير القابلة للتغيير إلى JavaScript:
Record
، بنية شبيهة بالكائنات غير القابلة للتغيير #{ x: 1, y: 2 }
Tuple
، هيكل شبيه بعمق يشبه المصفوفة #[1, 2, 3, 4]
يمكن أن تحتوي السجلات والطول فقط على بدائل وسجلات أخرى و tuples. يمكن أن تفكر في السجلات و tuples على أنها "بدائية مركبة". من خلال كونها تعتمد تمامًا على البدائيات ، لا تكون الأشياء والسجلات والألواح غير قابلة للتغيير بعمق.
تدعم السجلات والألوان التعاصيلات المريحة للبناء والتلاعب والاستخدام ، على غرار العمل مع الكائنات والصفوف. تتم مقارنتها بعمق من خلال محتوياتهم ، وليس عن طريق هويتهم.
قد تؤدي محركات JavaScript تحسينات معينة على البناء والتلاعب ومقارنة السجلات والطائبة ، مماثلة للطريقة التي يتم بها تنفيذ السلاسل غالبًا في محركات JS. (يجب أن نفهم أن هذه التحسينات غير مضمونة.)
تهدف السجلات والطول إلى أن تكون قابلة للاستخدام وفهمها مع مجموعات SuperStists من النوع الخارجي مثل TypeScript أو التدفق.
اليوم ، تقوم مكتبات Userland بتنفيذ مفاهيم مماثلة ، مثل Immutable.js. كما تمت محاولة اقتراح سابق ولكن تم التخلي عنه بسبب تعقيد الاقتراح وعدم وجود حالات استخدام كافية.
لا يزال هذا الاقتراح الجديد مستوحى من هذا الاقتراح السابق ولكنه يقدم بعض التغييرات المهمة: السجلات و tuples أصبحت الآن غير قابلة للتغيير بعمق. تعتمد هذه الخاصية بشكل أساسي على الملاحظة التي تفيد بأن مخاطر خلط هياكل البيانات غير القابلة للتغيير والقابلة للتغيير تنمو مع وجود كمية البيانات التي يتم تخزينها وتمريرها حول النمو ، لذا فمن المحتمل أن تتعامل مع هياكل السجلات والطواف الكبيرة على الأرجح . هذا يمكن أن يقدم الأخطاء التي يصعب العثور عليها.
بصفته بنية بيانات مدمجة ومكلفة للغاية ، يوفر هذا الاقتراح أيضًا بعض مزايا قابلية الاستخدام مقارنة بمكتبات Userland:
Immer هو نهج ملحوظ لهياكل البيانات غير القابلة للتغيير ، ويصف نمطًا للتلاعب من خلال المنتجين والمخفضات. إنه لا يوفر أنواع بيانات غير قابلة للتغيير ، لأنه يولد كائنات مجمدة. يمكن تكييف هذا النمط نفسه مع الهياكل المحددة في هذا الاقتراح بالإضافة إلى الكائنات المجمدة.
يمكن أن تختلف المساواة العميقة كما هو محدد في مكتبات المستخدمين بشكل كبير ، ويرجع ذلك جزئيًا إلى الإشارات المحتملة إلى الكائنات القابلة للتغيير. من خلال رسم خط صلب حول بدائل السجلات والسجلات والطول ، والتراجع من خلال الهيكل بأكمله ، يحدد هذا الاقتراح دلالات بسيطة وموحدة للمقارنات.
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"]
مفتوح في الملعب
يمكن للوظائف التعامل مع السجلات والكائنات بنفس الطريقة بشكل عام:
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
مفتوح في الملعب
رؤية المزيد من الأمثلة هنا.
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]
مفتوح في الملعب
وبالمثل من السجلات ، يمكننا التعامل مع tuples على أنها تشبه المصفوفة:
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
مفتوح في الملعب
رؤية المزيد من الأمثلة هنا.
كما هو مذكور قبل السجل والتوبل ، فإن عدم وجود عميق: محاولة إدراج كائن فيها ستؤدي إلى نوع من النوع:
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 ) )
هذا يحدد الأجزاء الجديدة من بناء الجملة التي يتم إضافتها إلى اللغة مع هذا الاقتراح.
نقوم بتحديد سجل أو تعبير tuple باستخدام المعدل #
أمام تعبيرات الكائن أو الصفيف العادي.
# { }
# { a : 1 , b : 2 }
# { a : 1 , b : # [ 2 , 3 , # { c : 4 } ] }
# [ ]
# [ 1 , 2 ]
# [ 1 , 2 , # { a : 3 } ]
يتم منع الثقوب في بناء الجملة ، على عكس المصفوفات ، والتي تسمح بالثقوب. انظر العدد رقم 84 لمزيد من المناقشة.
const x = # [ , ] ; // SyntaxError, holes are disallowed by syntax
باستخدام معرف __proto__
كخاصية يتم منعها في بناء الجملة. انظر العدد رقم 46 لمزيد من المناقشة.
const x = # { __proto__ : foo } ; // SyntaxError, __proto__ identifier prevented by syntax
const y = # { [ "__proto__" ] : foo } ; // valid, creates a record with a "__proto__" property.
لا يتم السماح بالأساليب الموجزة في بناء الجملة السجل.
# { method ( ) { } } // SyntaxError
قد تحتوي السجلات على مفاتيح سلسلة فقط ، وليس مفاتيح الرموز ، بسبب المشكلات الموضحة في #15. إنشاء سجل مع مفتاح رمز هو TypeError
.
const record = # { [ Symbol ( ) ] : # { } } ;
// TypeError: Record may only have string as keys
قد تحتوي السجلات والطول فقط على بدائل وسجلات أخرى و tuples. محاولة إنشاء Record
أو Tuple
الذي يحتوي على Object
( null
ليس كائنًا) أو Function
ترمي TypeError
.
const obj = { } ;
const record = # { prop : obj } ; // TypeError: Record may only contain primitive values
تعمل المساواة بين السجلات والطول مثل تلك الخاصة بالأنواع البدائية الأخرى مثل قيم Boolean و String ، ومقارنتها بالمحتويات ، وليس الهوية:
assert ( # { a : 1 } === # { a : 1 } ) ;
assert ( # [ 1 , 2 ] === # [ 1 , 2 ] ) ;
هذا يختلف عن كيفية عمل المساواة مع كائنات 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 . keys ( # { a : 1 , b : 2 } ) // ["a", "b"]
Object . keys ( # { b : 2 , a : 1 } ) // ["a", "b"]
إذا كانت هيكلها ومحتوياتهم متطابقة بعمق ، فستقوم Record
وقيم Tuple
متساوية وفقًا لجميع عمليات المساواة: Object.is
، ==
، ===
، وخوارزمية نفس Valuezero الداخلية (المستخدمة لمقارنة مفاتيح الخرائط والمجموعات) . إنها تختلف من حيث كيفية معاملة -0
:
Object.is
يعامل -0
و 0
غير متكافئ==
، ===
و Samevaluezero علاج -0
مع 0
على قدم المساواة لاحظ أن ==
و ===
أكثر مباشرة حول الأنواع الأخرى من القيم المتداخلة في السجلات والطائبة-تتجاوز true
إذا كانت المحتويات متطابقة (باستثناء 0
/ -0
). هذا المباشر له آثار على NaN
وكذلك المقارنات عبر الأنواع. انظر الأمثلة أدناه.
انظر المزيد من المناقشة في #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
Tuple
بشكل عام ، يمكنك علاج السجلات مثل الكائنات. على سبيل المثال ، تعمل مساحة اسم Object
و in
مع 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
و Tuple
لن يضطر مطورو JS عادةً إلى التفكير في كائنات Record
وملفات Tuple
، لكنهم جزء أساسي من كيفية عمل السجلات والطائلين "تحت الغطاء" في مواصفات JavaScript.
الوصول إلى سجل أو انتقام عبر .
أو []
يتبع دلالات GetValue
النموذجية ، والتي تتحول ضمنيًا إلى مثيل من نوع الغلاف المقابل. يمكنك أيضًا القيام بالتحويل بشكل صريح من خلال Object()
:
Object(record)
ينشئ كائن غلاف السجلObject(tuple)
ينشئ كائن غلاف tuple (يمكن للمرء أن يتخيل أن new Record
أو new Tuple
يمكن أن ينشئ هذه الأغلفة ، مثل new Number
new String
، لكن السجلات والطول تتبع الاتفاقية الأحدث التي وضعتها Symbor و Bigint ، مما يجعل هذه الحالات ترمي ، لأنها ليست المسار الذي نريد أن نريده شجع المبرمجين على أخذ.)
تحتوي كائنات Wrapper Record و Tuple على جميع خصائصها الخاصة مع السمات writable: false, enumerable: true, configurable: false
. كائن الغلاف غير قابل للتمديد. كلهم وضعوا معًا ، يتصرفون كأشياء مجمدة. هذا يختلف عن الكائنات المجهزة الموجودة في JavaScript ، ولكن من الضروري إعطاء أنواع الأخطاء التي تتوقعها من التلاعب العادي في السجلات والطائبة.
مثيل Record
له نفس المفاتيح والقيم مثل قيمة record
الأساسية. __proto__
لكل من كائنات غلاف السجل هذه null
(مناقشة: #71).
يحتوي مثيل Tuple
على مفاتيح هي أعداد صحيحة تقابل كل فهرس في قيمة tuple
الأساسية. القيمة لكل من هذه المفاتيح هي القيمة المقابلة في tuple
الأصلية. بالإضافة إلى ذلك ، هناك مفتاح length
غير الممكن. بشكل عام ، تتطابق هذه الخصائص مع تلك الموجودة في كائن String
Cropper. وهذا هو ، Object.getOwnPropertyDescriptors(Object(#["a", "b"]))
و Object.getOwnPropertyDescriptors(Object("ab"))
كل كائن يشبه هذا:
{
"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
}
}
__proto__
من كائنات غلاف tuple هو Tuple.prototype
. لاحظ أنه ، إذا كنت تعمل عبر كائنات مختلفة من JavaScript Global ("العوالم") ، يتم تحديد Tuple.prototype
استنادًا إلى العالم الحالي عندما يتم إجراء تحويل الكائن ، على نحو مشابه لكيفية تصرف .prototype
. غير مرتبط بقيمة tuple نفسها. Tuple.prototype
لديه طرق مختلفة على ذلك ، مماثلة للمصفوفات.
من أجل النزاهة ، تُرجع الفهرسة العددية خارج الحدود على tuples undefined
، بدلاً من إعادة توجيهها من خلال سلسلة النموذج الأولي ، كما هو الحال مع المطبوعات. يبحث البحث عن مفاتيح الملكية غير الوريدية إلى Tuple.prototype
، وهو أمر مهم للعثور على أساليب تشبه الصفيف.
Record
و Tuple
قيم Tuple
لها وظائف مماثلة على نطاق واسع Array
. وبالمثل ، يتم دعم قيم Record
بواسطة طرق ثابتة Object
مختلفة.
assert . deepEqual ( Object . keys ( # { a : 1 , b : 2 } ) , [ "a" , "b" ] ) ;
assert ( # [ 1 , 2 , 3 ] . map ( x => x * 2 ) , # [ 2 , 4 , 6 ] ) ;
راجع التذييل لمعرفة المزيد حول مساحات الأسماء Record
و Tuple
.
يمكنك تحويل الهياكل باستخدام Record()
أو Tuple()
(مع مشغل الانتشار) أو Record.fromEntries()
أو Tuple.from()
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
لاحظ أن Record()
، Tuple()
، Record.fromEntries()
و Tuple.from()
تتوقع مجموعات تتكون من السجلات أو tuples أو غيرها من البدائل (مثل الأرقام والسلاسل ، إلخ). مراجع الكائن المتداخلة قد تسبب typeerror. الأمر متروك للمتصل لتحويل الهياكل الداخلية بأي طريقة مناسبة للتطبيق.
ملاحظة : لا يحتوي مسودة الاقتراح الحالي على إجراءات تحويل متكررة ، فقط تلك الضحلة. انظر المناقشة في #122
مثل المصفوفات ، tuples هي ITERBILE.
const tuple = # [ 1 , 2 ] ;
// output is:
// 1
// 2
for ( const o of tuple ) { console . log ( o ) ; }
على غرار الكائنات ، فإن السجلات غير قابلة للاقتران مع واجهات برمجة التطبيقات مثل Object.entries
.
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)
يعادل استدعاء JSON.stringify
على الكائن الناتج عن تحويل السجل بشكل متكرر إلى كائن لا يحتوي على سجلات أو tuples.JSON.stringify(tuple)
يعادل استدعاء JSON.stringify
على الصفيف الناتج عن تحويل tuple بشكل متكرر إلى صفيف لا يحتوي على سجلات أو tuples. JSON . stringify ( # { a : # [ 1 , 2 , 3 ] } ) ; // '{"a":[1,2,3]}'
JSON . stringify ( # [ true , # { a : # [ 1 , 2 , 3 ] } ] ) ; // '[true,{"a":[1,2,3]}]'
يرجى الاطلاع على https://github.com/tc39/proposal-json-parseimmutable
Tuple.prototype
يدعم Tuple
أساليب مثيل مشابهة للمصفوفة مع بعض التغييرات:
Tuple.prototype.push
.Tuple.prototype.withAt
. يحتوي الملحق على وصف كامل للنموذج الأولي لـ Tuple
.
typeof
يحدد typeof
السجلات والطائبة على أنها أنواع متميزة:
assert ( typeof # { a : 1 } === "record" ) ;
assert ( typeof # [ 1 , 2 ] === "tuple" ) ;
Map
| Set
| WeakMap
| WeakSet
} من الممكن استخدام Record
أو Tuple
كمفتاح في Map
، وكقيمة في Set
. عند استخدام Record
أو Tuple
هنا ، تتم مقارنتها بالقيمة.
لا يمكن استخدام Record
أو Tuple
كمفتاح في WeakMap
أو كقيمة في WeakSet
، لأن Records
و Tuple
s ليست Objects
، ولا يمكن ملاحظتها.
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 ) ;
تتمثل إحدى الفوائد الأساسية في السجلات والاقتراح في أن تتم مقارنتها بمحتوياتها ، وليس هويتها. في الوقت نفسه ، ===
في JavaScript على الكائنات لها دلالات واضحة للغاية ومتسقة: لمقارنة الكائنات حسب الهوية. يتيح صنع السجلات و primitives للمقارنة استنادًا إلى قيمها.
على مستوى عالٍ ، يساعد الكائن/التمييز البدائي في تكوين خط صلب بين العالم غير القابل للتغيير والخالي من السياق وخالي من الهوية وعالم الأشياء القابلة للتغيير فوقه. هذا الانقسام الفئة يجعل التصميم والنموذج العقلي أكثر وضوحا.
بديل لتنفيذ السجلات والتوابل كحوالي هو استخدام التحميل الزائد للمشغل لتحقيق نتيجة مماثلة ، من خلال تنفيذ مشغل مساواة مجردة ( ==
) يقارن بعمق الكائنات. على الرغم من أن هذا ممكن ، إلا أنه لا يفي بحالة الاستخدام الكاملة ، لأن التحميل الزائد للمشغل لا يوفر تجاوزًا للمشغل ===
. نريد أن يكون مشغل المساواة الصارم ( ===
) فحصًا موثوقًا لـ "الهوية" للكائنات و "القيمة القابلة للملاحظة" (Modulo -0/+0/NAN) للأنواع البدائية.
هناك خيار آخر هو تنفيذ ما يسمى Interning : نحن نتتبع كائنات تسجيل عالمي أو tuple ، وإذا حاولنا إنشاء واحد جديد يحدث ليكون متطابقًا مع كائن سجل موجود ، فإننا نشير الآن إلى هذا السجل الموجود بدلاً من إنشاء سجل جديد. هذا هو في الأساس ما يفعله polyfill. نحن الآن يساوي القيمة والهوية. يخلق هذا النهج مشاكل بمجرد توسيع نطاق هذا السلوك عبر سياقات JavaScript المتعددة ولن يعطي ثباتًا عميقًا بطبيعته وهو بطيء بشكل خاص مما يجعل استخدام Record & Tuple خيارًا سلبيًا للأداء.
تم تصميم Record & Tuple للتبديل مع الكائنات والصفائف جيدًا: يمكنك قراءتها بنفس الطريقة التي ستفعل بها بالضبط مع الكائنات والصفائف. يكمن التغيير الرئيسي في التثبيت العميق والمقارنة بالقيمة بدلاً من الهوية.
سيكون المطورون المستخدمون في معالجة الكائنات بطريقة غير قابلة للتغيير (مثل تحويل أجزاء من حالة Redux) قادرين على الاستمرار في القيام بنفس التلاعب الذي استخدموه للقيام به على الأشياء والصفائف ، هذه المرة ، مع مزيد من الضمانات.
سنقوم بإجراء البحوث التجريبية من خلال المقابلات والدراسات الاستقصائية لمعرفة ما إذا كان هذا يعمل كما نعتقد.
.get()
/ .set()
مثل Immutable.js؟إذا كنا نريد الاستمرار في الوصول إلى التسجيلات والطوق على غرار الكائنات والصفائف كما هو موضح في القسم السابق ، فلا يمكننا الاعتماد على طرق لإجراء هذا الوصول. سيتطلب القيام بذلك منا أن نفرع رمز عند محاولة إنشاء وظيفة "عامة" قادرة على أخذ الكائنات/المصفوفات/السجلات/tuples.
فيما يلي وظيفة مثال لديها دعم لسجلات undelfable.js والأشياء العادية:
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
هذا معرض للخطأ لأن كلا الفرعين يمكن أن يخرج بسهولة من المزامنة مع مرور الوقت ...
إليكم كيف سنكتب هذه الوظيفة أخذ السجلات من هذا الاقتراح والأشياء العادية:
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
تدعم هذه الوظيفة كل من الكائنات والسجلات في مسار رمز واحد بالإضافة إلى عدم إجبار المستهلك على اختيار هياكل البيانات التي يجب استخدامها.
لماذا نحتاج إلى دعم كليهما في نفس الوقت على أي حال؟ هذا هو في المقام الأول لتجنب انقسام النظام البيئي. دعنا نقول أننا نستخدم Immutable.js للقيام بإدارة الدولة لدينا ، لكننا نحتاج إلى إطعام ولايتنا لبعض المكتبات الخارجية التي لا تدعمها:
state . jobResult = Immutable . fromJS (
ExternalLib . processJob (
state . jobDescription . toJS ( )
)
) ;
يمكن أن ينتهي كل من toJS()
و fromJS()
إلى أن تكون عمليات باهظة الثمن اعتمادًا على حجم الهياكل الأساسية. يعني تقسيم النظام البيئي التحويلات التي تعني بدورها مشكلات الأداء المحتملة.
يحسن بناء الجملة المقترح بشكل كبير من بيئة العمل لاستخدام Record
Tuple
في الكود. على سبيل المثال:
// 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 ,
) ,
} ) ,
} ) ,
} ) ;
يهدف بناء الجملة المقترح إلى أن يكون أبسط وأسهل في الفهم ، لأنه يشبه عن قصد بناء الجملة للكائنات والصفيف الحرفي. هذا يستفيد من معرفة المستخدم الحالية مع الكائنات والصفائف. بالإضافة إلى ذلك ، يقدم المثال الثاني حرفيًا مؤقتًا كائنًا مؤقتًا ، والذي يضيف إلى تعقيد التعبير.
إن استخدام كلمة رئيسية كبادئة للكائن القياسي/بناء الجملة الحرفي القياسي يمثل مشكلات حول التوافق مع الإصدارات السابقة. بالإضافة إلى ذلك ، يمكن إعادة استخدام الكلمات الرئيسية الحالية تقديم الغموض.
يحدد ECMASCRIPT مجموعة من الكلمات الرئيسية المحجوزة التي يمكن استخدامها للامتدادات المستقبلية للغة. إن تحديد كلمة رئيسية جديدة غير محفوظة بالفعل أمر ممكن من الناحية النظرية ، ولكنه يتطلب جهد كبير للتحقق من أن الكلمة الرئيسية الجديدة لن تكسر التوافق الخلفي.
إن استخدام كلمة رئيسية محجوزة يجعل هذه العملية أسهل ، لكنها ليست حلاً مثاليًا لأنه لا توجد كلمات رئيسية محجوزة تتطابق مع "نية" الميزة ، بخلاف const
. تعتبر الكلمة الرئيسية const
صعبة أيضًا ، لأنها تصف مفهومًا مشابهًا (قابلية التأثير المرجعية المتغيرة) بينما يعتزم هذا الاقتراح إضافة هياكل بيانات جديدة ثابتة. في حين أن التثبيت هو الخيط المشترك بين هاتين الميزتين ، فقد كانت هناك ردود فعل كبيرة من المجتمع تشير إلى أن استخدام const
في كلا السياقين غير مرغوب فيه.
بدلا من استخدام كلمة رئيسية ، {| |}
و [||]
تم اقتراحها كبدائل ممكنة. حاليًا ، تميل مجموعة Champion نحو #[]
/ #{}
، لكن المناقشة مستمرة في #10.
إن تعريف السجلات والتوابل كـ Primitives المركبة يفرض كل شيء في التسجيلات والتطوير على عدم أن يكون كائنات. يأتي ذلك مع بعض العيوب (تصبح الكائنات المرجعية أكثر صعوبة ولكن لا تزال ممكنة) ولكن أيضًا المزيد من الضمانات لتجنب أخطاء البرمجة الشائعة.
const object = {
a : {
foo : "bar" ,
} ,
} ;
Object . freeze ( object ) ;
func ( object ) ;
// func is able to mutate object’s keys even if object is frozen
في المثال أعلاه ، نحاول إنشاء ضمان للتأثير مع Object.freeze
. لسوء الحظ ، نظرًا لأننا لم نجمد الكائن بعمق ، فلا شيء يخبرنا بهذا object.a
. لم يمس. مع التسجيل والتطوير ، يكون القيد بطبيعته وليس هناك شك في أن الهيكل لم يمس:
const record = # {
a : # {
foo : "bar" ,
} ,
} ;
func ( record ) ;
// runtime guarantees that record is entirely unchanged
assert ( record . a . foo === "bar" ) ;
أخيرًا ، فإن الثبات العميق يمنع الحاجة إلى نمط مشترك يتكون من الأشياء العميقة للحفاظ على الضمانات:
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" ) ;
بشكل عام ، يعمل مشغل الانتشار بشكل جيد لهذا:
// 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]
وإذا كنت تقوم بتغيير شيء ما في tuple ، فإن طريقة Tuple.prototype.with
تعمل:
// Change a Tuple index
let tup = # [ 1 , 2 , 3 ] ;
tup . with ( 1 , 500 ) // #[1, 500, 3]
يمكن أن تكون بعض التلاعب بـ "المسارات العميقة" محرجة بعض الشيء. لذلك ، تضيف خصائص المسار العميق لاقتراح السجلات بناء جملة مختصرة إضافية لتسجيل الحرفيين.
نحن نقوم بتطوير اقتراح خصائص المسار العميق كاقتراح متابع منفصل لأننا لا نرى أنه أساسي لاستخدام السجلات ، والتي تعمل بشكل مستقل. إنه نوع من الإضافة النحوية التي ستعمل بشكل جيد على النموذج الأولي مع مرور الوقت في Transpilers ، وحيث لدينا العديد من نقاط القرار التي لا علاقة لها بالسجلات والطول (على سبيل المثال ، كيف تعمل مع الكائنات).
لقد تحدثنا مع أبطال المجموعات القراءة ، وتوافق كلتا المجموعتين على أن هذه مكملات:
لا أحد هو مجموعة فرعية من الآخر من حيث الوظيفة. في أحسن الأحوال ، فهي متوازية ، تمامًا مثل كل اقتراح موازٍ لأنواع التجميع الأخرى باللغة.
لذلك ، فقد قررت مجموعتان بطل التأكد من أن المقترحات متوازية فيما يتعلق ببعضها البعض . على سبيل المثال ، يضيف هذا الاقتراح طريقة جديدة Tuple.prototype.withReversed
. تتمثل الفكرة في التحقق ، أثناء عملية التصميم ، إذا كان هذا التوقيع سيكون منطقيًا أيضًا بالنسبة للمصفوفات للقراءة فقط (إذا كانت موجودة): لقد استخرجنا هذه الأساليب الجديدة إلى صفيف التغيير عن طريق اقتراح النسخ ، حتى نتمكن من مناقشة واجهة برمجة التطبيقات الذي يبني نموذجًا عقليًا ثابتًا ومشتركًا.
في مسودات الاقتراح الحالية ، لا توجد أنواع متداخلة لنفس النوع من البيانات ، ولكن قد ينمو كلا المقترحين في هذه الاتجاهات في المستقبل ، ونحن نحاول التفكير في هذه الأشياء في وقت مبكر. من يدري ، يمكن أن يقرر TC39 في يوم من الأيام إضافة أنواع RECORDER MAPERMATION و RECORDST ، كإصدارات غير قابلة للتغيير العميقة من SET والخريطة! وستكون هذه بالتوازي مع أنواع المجموعات القراءة.
كان TC39 منذ فترة طويلة مناقشة "أنواع القيمة" ، والتي ستكون نوعًا من الإعلان الطبقي لنوع بدائي ، لعدة سنوات ، وإيقافها. نسخة سابقة من هذا الاقتراح قام بمحاولة. يحاول هذا الاقتراح البدء بسيطًا وأقلًا من الحد الأدنى ، مما يوفر فقط الهياكل الأساسية. الأمل هو أنه يمكن أن يوفر نموذج البيانات لاقتراح مستقبلي للفصول.
يرتبط هذا الاقتراح بشكل فضفاض بمجموعة أوسع من المقترحات ، بما في ذلك التحميل الزائد للمشغل والحرفيات الرقمية الممتدة: كل هذه المتآمرات لتوفير وسيلة لأنواع محددة من قبل المستخدم للقيام بنفس الشيء مثل Bigint. ومع ذلك ، فإن الفكرة هي إضافة هذه الميزات إذا قررنا أنها متحمسة بشكل مستقل.
إذا كان لدينا أنواع بدائية/قيمة محددة من قبل المستخدم ، فقد يكون من المنطقي استخدامها في ميزات مدمجة ، مثل CSS المكتوبة OM أو الاقتراح الزمني. ومع ذلك ، هذا بعيد في المستقبل ، إذا حدث ذلك ؛ في الوقت الحالي ، يعمل بشكل جيد لاستخدام كائنات لهذه الأنواع من الميزات.
على الرغم من أن كلا النوعين من السجلات يرتبطان بالأشياء ، ويرتبط كلا النوعين من tuples بالصفائف ، فإن هذا هو المكان الذي ينتهي فيه التشابه.
السجلات في TypeScript هي نوع الأداة المساعدة العامة لتمثيل كائن يأخذ نوع المفتاح مطابقة مع نوع القيمة. أنها لا تزال تمثل الأشياء.
وبالمثل ، فإن tuples في typeScript هي تدوين للتعبير عن أنواع في مجموعة من الحجم المحدود (بدءًا من TypeScript 4.0 ، لديهم شكل متغير). Tuples in TypeScript هي وسيلة للتعبير عن المصفوفات مع أنواع غير متجانسة. يمكن أن تتوافق TUPLES ECMASCRIPT مع صفائف TS أو TS TUPLES بسهولة حيث يمكن أن تحتوي على عدد غير محدد من القيم من نفس النوع أو يحتوي على عدد محدود من القيم مع أنواع مختلفة.
سجلات TS أو tuples هي ميزات متعامدة لسجلات ecmascript و tuples ويمكن التعبير عنها في نفس الوقت:
const record : Readonly < Record < string , number > > = # {
foo : 1 ,
bar : 2 ,
} ;
const tuple : readonly [ number , string ] = # [ 1 , "foo" ] ;
لا يجعل هذا الاقتراح أي ضمانات أداء ولا يتطلب تحسينات محددة في التطبيقات. بناءً على ردود الفعل من المنفذين ، من المتوقع أن يقوموا بتنفيذ عمليات مشتركة عبر خوارزميات "الوقت الخطي". ومع ذلك ، فإن هذا الاقتراح لا يمنع بعض التحسينات الكلاسيكية لهياكل البيانات الوظيفية البحتة ، بما في ذلك على سبيل المثال لا الحصر:
هذه التحسينات مماثلة للطريقة التي تتعامل بها محركات JavaScript الحديثة في سلسلة متسلسل ، مع أنواع مختلفة من الأوتار الداخلية المختلفة. تعتمد صحة هذه التحسينات على عدم رصد هوية السجلات والطول. من غير المتوقع أن تتصرف جميع المحركات بشكل متماثل فيما يتعلق بهذه التحسينات ، بل سيتخذ كل منها قرارات بشأن الاستدلال المعين لاستخدامها. قبل المرحلة الرابعة من هذا الاقتراح ، نخطط لنشر دليل لأفضل الممارسات للاستخدام القابل للتحسين للمحركين ، بناءً على تجربة التنفيذ التي سنحصل عليها في هذه المرحلة.
بنية بيانات النوع البدائي الجديد ، غير القابل للتغيير العميق ، المركب ، المقترح في هذا المستند ، مماثل للكائن. #{ a: 1, b: 2 }
بنية بيانات بدائية جديدة ، غير قابلة للتغيير بعمق ، مركبة ، مقترحة في هذا المستند ، تشبه الصفيف. #[1, 2, 3, 4]
القيم التي تعمل مثل أولي جافا سكريبت ، ولكنها تتألف من قيم مكونة أخرى. يقترح هذا المستند النوعين البدائيين المركبين الأولين: Record
Tuple
.
String
، Number
، Boolean
، undefined
، null
، Symbol
و BigInt
الأشياء التي هي إما مركبة أو أنواع بدائية بسيطة. تشترك جميع البدائل في JavaScript في بعض الخصائص:
بنية البيانات التي لا تقبل العمليات التي تغيرها داخليًا ، ولكن بدلاً من ذلك لديها عمليات تُرجع قيمة جديدة نتيجة لتطبيق هذه العملية عليها.
في Record
الاقتراح هذا و Tuple
هي هياكل بيانات غير قابلة للتغيير.
يتم تعريف المشغل ===
مع خوارزمية مقارنة المساواة الصارمة. تشير المساواة الصارمة إلى هذه الفكرة الخاصة بالمساواة.
المشاركة الهيكلية هي تقنية تستخدم للحد من بصمة الذاكرة لهياكل البيانات غير القابلة للتغيير. باختصار ، عند تطبيق عملية لاستخلاص نسخة جديدة من بنية غير قابلة للتغيير ، ستحاول المشاركة الهيكلية الحفاظ على معظم الهيكل الداخلي سليماً واستخدامها من قبل كل من الإصدارات القديمة والمشتقة من هذا الهيكل. هذا يحد بشكل كبير من المبلغ للنسخ لاستخلاص الهيكل الجديد.