Copy nông object
Object có thể xử lý nhiều tổ hợp key và property như một thứ duy nhất.
Khi xử lý object, so sánh hoặc gán instance giống như các ngôn ngữ khác là so sánh và gán tham chiếu. Nếu tham chiếu đó được giữ ở đâu đó khác, có thể bị viết lại ở đó.
Tác hại khi ghi đè instance một cách bừa bãi
Ví dụ giả sử bạn đang tạo service liên quan đến bệnh lối sống. Service đó nhập bữa ăn trong ngày và tính nhiệt lượng (calo) từ bữa ăn đó, hơn nữa có thể đoán xem tương lai có bị bệnh lối sống (gọi là Metabolic Syndrome tuy hơi khác) hay không.
Ở đây định nghĩa type MealsPerDay nghĩa là object của bữa ăn trong ngày, và định nghĩa function willBeMetabo() đoán xem có bị bệnh lối sống hay không từ nhiệt lượng bữa ăn trong ngày.
tstypeMealsPerDay = {breakfast : string;lunch : string;dinner : string;};functionwillBeMetabo (meals :MealsPerDay ): boolean {// ...}
tstypeMealsPerDay = {breakfast : string;lunch : string;dinner : string;};functionwillBeMetabo (meals :MealsPerDay ): boolean {// ...}
Cách sử dụng như sau.
ts// 439.2 kcalconstmeals :MealsPerDay = {breakfast : "a vegetable salad",lunch : "a cod's meuniere",dinner : "a half bottle of wine (white)",};willBeMetabo (meals );
ts// 439.2 kcalconstmeals :MealsPerDay = {breakfast : "a vegetable salad",lunch : "a cod's meuniere",dinner : "a half bottle of wine (white)",};willBeMetabo (meals );
Tuy nhiên, chỉ như vậy thì khi có input không hợp lệ như đồ không phải thức ăn như ốc vít, service có thể phản ứng không như mong đợi. Vì vậy định nghĩa function isMeals() để validate xem input có thực sự là bữa ăn hay không. Function này throw exception khi nhận được thứ không phải bữa ăn.
Cấu trúc của isMeals() đơn giản. Chỉ cần đoán từng bữa sáng, trưa, tối xem có phải bữa ăn hay không. Nếu có function isMeal() đoán một bữa ăn có phải bữa ăn hay không thì chỉ cần gọi nó bên trong. Triển khai isMeal() không quan trọng lần này nên bỏ qua.
tsfunctionisMeals (meals :MealsPerDay ): void {if (!isMeal (meals .breakfast )) {throw newError ("BREAKFAST IS NOT A MEAL!");}if (!isMeal (meals .lunch )) {throw newError ("LUNCH IS NOT A MEAL!!!");}if (!isMeal (meals .dinner )) {throw newError ("DINNER IS NOT A MEAL!!!");}}
tsfunctionisMeals (meals :MealsPerDay ): void {if (!isMeal (meals .breakfast )) {throw newError ("BREAKFAST IS NOT A MEAL!");}if (!isMeal (meals .lunch )) {throw newError ("LUNCH IS NOT A MEAL!!!");}if (!isMeal (meals .dinner )) {throw newError ("DINNER IS NOT A MEAL!!!");}}
Trong use case lần này, sau khi validate bằng isMeals() thì đoán bữa ăn đó bằng willBeMetabo(). Khi nhận được thứ không ăn được, chỉ cần catch exception và xử lý nên đại khái như sau.
tsfunctionshouldBeCareful (meals :MealsPerDay ): boolean {try {// ...isMeals (meals );returnwillBeMetabo (meals );} catch (err : unknown) {// ...}}
tsfunctionshouldBeCareful (meals :MealsPerDay ): boolean {try {// ...isMeals (meals );returnwillBeMetabo (meals );} catch (err : unknown) {// ...}}
Ở đây giả sử người tạo hoặc bảo trì isMeals() vì lý do gì đó đã viết chương trình ghi đè instance gốc bằng bữa ăn béo ngậy yêu thích của mình. Thay đổi này khiến user đang ăn rất lành mạnh chưa đến 500 kcal bị isMeals() biến thành đang ăn bom calo 19,800 kcal.
tsfunctionisMeals (meals :MealsPerDay ): void {meals .breakfast = "a beef steak";// beef steak will be 1200 kcalmeals .lunch = "a bucket of ice cream";// a bucket of ice cream will be 7200 kcalmeals .dinner = "3 pizzas";// 3 pizzas will be 11400 kcalif (!isMeal (meals .breakfast )) {throw newError ("BREAKFAST IS NOT MEAL!");}if (!isMeal (meals .lunch )) {throw newError ("LUNCH IS NOT MEAL!!!");}if (!isMeal (meals .dinner )) {throw newError ("DINNER IS NOT MEAL!!!");}}console .log (meals );isMeals (meals );console .log (meals );willBeMetabo (meals );
tsfunctionisMeals (meals :MealsPerDay ): void {meals .breakfast = "a beef steak";// beef steak will be 1200 kcalmeals .lunch = "a bucket of ice cream";// a bucket of ice cream will be 7200 kcalmeals .dinner = "3 pizzas";// 3 pizzas will be 11400 kcalif (!isMeal (meals .breakfast )) {throw newError ("BREAKFAST IS NOT MEAL!");}if (!isMeal (meals .lunch )) {throw newError ("LUNCH IS NOT MEAL!!!");}if (!isMeal (meals .dinner )) {throw newError ("DINNER IS NOT MEAL!!!");}}console .log (meals );isMeals (meals );console .log (meals );willBeMetabo (meals );
Khi đã gọi isMeals(), bất kể bữa ăn nào được đưa vào, willBeMetabo() sẽ đoán ai cũng đang trên đường đến bệnh lối sống. Thay đổi biến meals không chỉ dừng lại trong isMeals() mà còn ảnh hưởng ra bên ngoài.
Vấn đề lần này
Lần này ví dụ isMeals() gây hại. Nếu function này do chính mình tạo thì có thể tìm nguyên nhân ngay. Không viết function có vấn đề như vậy tất nhiên quan trọng, nhưng nếu có teammate chưa thành thạo thì có thể viết function như vậy. Thiết kế ngăn không cho con người mắc lỗi quan trọng hơn giả định con người không mắc lỗi.
Nếu isMeals() là package lấy từ bên ngoài thì có vấn đề. Tự mình sửa package này không dễ (không phải không thể). Gửi pull request cho người tạo và dừng phát triển cho đến khi bug được fix cũng không thực tế.
Nên làm thế nào
Nên làm cho instance không thể bị viết lại, hoặc chuẩn bị instance scapegoat để instance gốc không bị phá hủy. Phương pháp trước được đại diện bởi value object. Ở đây giới thiệu phương pháp sau, scapegoat, tức là chuẩn bị copy.
Shallow copy là gì
Như tiêu đề, nông nghĩa là gì? Đó là khi copy object, dù object có cấu trúc sâu đến đâu (nested) thì chỉ copy tầng đầu tiên. Tất nhiên từ đối nghịch là deep copy.
Object đã shallow copy không bằng nhau
Giả sử function shallow copy là shallowCopy(). Triển khai không khó nhưng lần này chỉ muốn nói về hành vi nên để sau. Object đã shallow copy và original khi so sánh bằng === trả về false. Điều này tự nhiên theo định nghĩa của copy, nếu trả về true thì copy đã thất bại.
tsconstobject1 : object = {};constobject2 : object =shallowCopy (object1 );console .log (object1 ===object2 );
tsconstobject1 : object = {};constobject2 : object =shallowCopy (object1 );console .log (object1 ===object2 );
Ví dụ sau ngăn chặn ghi đè instance bằng shallow copy. Instance meals không thay đổi và chỉ scapegoat được truyền làm tham số cho isMeals() bị thay đổi.
tsconstscapegoat :MealsPerDay =shallowCopy (meals );console .log (meals );console .log (scapegoat );isMeals (scapegoat );console .log (meals );console .log (scapegoat );
tsconstscapegoat :MealsPerDay =shallowCopy (meals );console .log (meals );console .log (scapegoat );isMeals (scapegoat );console .log (meals );console .log (scapegoat );
Trường hợp shallow copy không ngăn được
Như đã nói, shallow copy chỉ copy tầng đầu tiên của object. Vì vậy nếu object có cấu trúc sâu, phức tạp, nó không copy tất cả mà tầng thứ hai trở đi chỉ là tham chiếu. Ví dụ sau cho thấy khi property của shallow copy có object, nó là tham chiếu chứ không phải copy.
tstypeNestObject = {nest : object;};constobject1 :NestObject = {nest : {},};constobject2 :NestObject =shallowCopy (object1 );console .log (object1 ===object2 );console .log (object1 .nest ===object2 .nest );
tstypeNestObject = {nest : object;};constobject1 :NestObject = {nest : {},};constobject2 :NestObject =shallowCopy (object1 );console .log (object1 ===object2 );console .log (object1 .nest ===object2 .nest );
Nếu muốn tạo copy hoàn chỉnh, sử dụng deep copy đã đề cập cùng với shallow copy.
Lần này không đi sâu vào deep copy. So với shallow copy, deep copy tốn thời gian copy hơn, và vì copy thực thể chứ không phải tham chiếu, cần cấp phát cùng lượng bộ nhớ. Nếu deep copy tràn lan sẽ nhanh chóng lãng phí tài nguyên thời gian và không gian. Khi shallow copy đủ dùng thì nên sử dụng shallow copy.
Triển khai shallow copy
Triển khai shallow copy trong JS hiện đại rất dễ, chỉ cần code sau là xong.
tsconstshallowCopied : object = { ...sample };
tsconstshallowCopied : object = { ...sample };
Tất nhiên biến sample phải là object. ... này là spread syntax. Về spread syntax hãy xem chương function.
Có thể sử dụng spread syntax để copy object từ ES2018. Ví dụ shallow copy như sau
tsconstsample : object = {year : 1999,month : 7,};constshallowCopied : object = { ...sample };
tsconstsample : object = {year : 1999,month : 7,};constshallowCopied : object = { ...sample };
khi compile với ES2018 sẽ thành như sau.
tsconstsample = {year : 1999,month : 7,};constshallowCopied = { ...sample };
tsconstsample = {year : 1999,month : 7,};constshallowCopied = { ...sample };
Gần như giống nhau nhưng khi compile với ES2017 sẽ thành như sau.
tsconstsample = {year : 1999,month : 7,};constshallowCopied =Object .assign ({},sample );
tsconstsample = {year : 1999,month : 7,};constshallowCopied =Object .assign ({},sample );
Trước khi spread syntax được triển khai, sử dụng Object.assign() này. Hai cái này không hoàn toàn giống nhau nhưng có thể sử dụng Object.assign({}, obj) gần như thay thế cho {...obj}.
Sử dụng API để copy
Trong JavaScript, tùy object có API được cung cấp để viết shallow copy ngắn gọn. Map và Set có thể sử dụng điều đó.
Copy Map<K, V>
Khi copy Map, truyền object Map muốn copy vào constructor Map.
tsconstmap1 = newMap ([[".js", "JS"],[".ts", "TS"],]);constmap2 = newMap (map1 );// Phần tử giống nhau nhưng instance Map khác nhauconsole .log (map2 );console .log (map1 !==map2 );
tsconstmap1 = newMap ([[".js", "JS"],[".ts", "TS"],]);constmap2 = newMap (map1 );// Phần tử giống nhau nhưng instance Map khác nhauconsole .log (map2 );console .log (map1 !==map2 );
📄️ Map<K, V>
Map là một trong những built-in API của JavaScript, là object để xử lý cặp key-value. Trong Map, một key chỉ có thể lưu trữ một giá trị duy nhất.
Copy Set<T>
Khi copy Set, truyền object Set muốn copy vào constructor Set.
tsconstset1 = newSet ([1, 2, 3]);constset2 = newSet (set1 );// Phần tử giống nhau nhưng instance Set khác nhauconsole .log (set2 );console .log (set1 !==set2 );
tsconstset1 = newSet ([1, 2, 3]);constset2 = newSet (set1 );// Phần tử giống nhau nhưng instance Set khác nhauconsole .log (set2 );console .log (set1 !==set2 );
📄️ Set<T>
Set là một trong những built-in API của JavaScript, là object để xử lý collection các giá trị. Set không thể lưu trữ giá trị trùng lặp. Các giá trị được lưu trong Set là duy nhất (unique).
Copy Array<T>
Có nhiều cách copy array, nhưng đơn giản nhất là sử dụng spread syntax của array.
tsconstarray1 = [1, 2, 3];constarray2 = [...array1 ];
tsconstarray1 = [1, 2, 3];constarray2 = [...array1 ];
Khi đó nếu quên viết spread syntax ... sẽ tạo ra array của array T[][] nên hãy cẩn thận.
Thông tin liên quan
📄️ Spread syntax của array "..."
Trong JavaScript, với array có thể sử dụng spread syntax "..." để triển khai các phần tử.