Viết code javascript kiểu immutable là một thói quen tốt. Có một vài thư viện tốt hỗ trợ immutable như immutable.js và chúng ta có thể sử dụng nó. Nhưng liệu nó có thực sự hoạt tốt với Javascript thế hệ tiếp theo ko ?
Câu trả lời là có. ES6 và ES(EcmaScript) tiếp theo có chứa các tính năng mà có thể giúp chúng ta thực thi các hoạt động immutable mà không gặp rắc rối nào.
Vấn đề
Trước tiên chúng ta phải hiểu, tại sao immutable (không đổi) lại quan trọng đến thế trong Javascript ? Dữ liệu dạng mutbale (thay đổi được) có thể làm cho code của chúng ta khó đọc và dễ bị lỗi. Các kiểu giá trị cơ bản như Number và String tương đối dễ để viết immuable code, bởi vì các giá trị này sẽ không tự thay đổi giá trị của nó. Các biến mà chứa các kiểu cơ bản này thì luôn luôn được trỏ đến một giá trị cụ thể. Nếu bạn đặt giá trị này vào một biến khác thì các biến đấy sẽ có được giá trị được sao chép chuẩn.
Object hoặc mảng thì lại là một câu chuyện khác, chúng được đặt bởi tham chiếu. Nghĩa là nếu bạn đặt một object vào trong biến khác, thì cả hai biến sẽ cùng tham chiếu đến một object, nếu bạn muốn thay đổi object từ bất kỳ biến nào thì thay đổi đấy sẽ ảnh hưởng đến tất cả các biến tham chiếu đến object ví dụ
const person = { name: 'John', age: 28 } const newPerson = person newPerson.age = 30 console.log(newPerson === person) // true console.log(person) // { name: 'John', age: 30 }
Bạn có nhìn thấy vấn đề của đoạn code trên không ? Khi chúng ta thay đổi giá trị của newPerson, thì biến person cũng tự động thay đổi theo, bởi vì chúng đều tham chiếu đến một object . Trong phần lớn các trường hợp thì đây là những hoạt động không mong muốn và thói quen ko tốt. Nào hãy xem làm cách nào chúng ta có thể giải quyết vấn đề này.
Sử dụng immutable
Thay vì chúng ta gán một object mà lại gây ảnh hưởng như trên, tốt hơn hết chúng ta tạo ra một object hoàn toàn mới.
const person = { name: 'John', age: 28 } const newPerson = Object.assign({}, person, { age: 30 }) console.log(newPerson === person) // false console.log(person) // { name: 'John', age: 28 } console.log(newPerson) // { name: 'John', age: 30 }
Object.assign
là một một đặc tính của ES6 mà lấy các object như tham số đầu vào. Nó sẽ merge tất cả các object truyền vào thành một object mới. Bạn sẽ chắc chắn tự hỏi tại sao tham số đầu tiên lại là một empty object {}
. Bởi vì nếu tham số đầu tiên là person thì nó sẽ tạo ra một mutable object. Nếu nó là {age: 30}
thì nó sẽ ghi đè 30 bằng 28 vì nó là tham số được truyền trước, mà mục đích chúng ta muốn sửa tuổi 28 thành 30.
Giải pháp này chạy tốt, vì giữ được giá trị person không thay đổi, chúng ta đã có thể giải quyết vấn đề ở trên bằng immutable.
Tuy nhiên, EcmaScript chắc chắn có cú pháp đặc biệt để cho phép chúng ta làm việc này dễ dàng hơn. Nó được gọi là object spread. Chúng ta có thể sử dụng Babel transpiler như sau
const person = { name: 'John', age: 28 } const newPerson = { ...person, age: 30 } console.log(newPerson === person) // false console.log(newPerson) // { name: 'John', age: 30 }
Kết quả lại giống như trên. Lúc này thậm chí code nhìn còn sách sẽ hơn. Toán tử “spread” (…), sao chép toàn bộ thuộc tính của person thành một object mới. Sau đó định nghĩa một “age” mới và ghi đè lên. Cần chú ý vấn đề về thứ tự, nếu {age: 30}
ở trên ...person
thì nó sẽ bị ghi đè bởi age: 28
Làm thế nào chúng ta có thể xoá một item. Chúng ta sẽ không thể dùng delete vì nó sẽ lại biến thành mutable object. Nó chắc chắn sẽ khó hơn một chút nhưng chúng ta có thể làm như sau:
const person = { name: 'John', password: '123', age: 28 } const newPerson = Object.keys(person).reduce((obj, key) => { if (key !== property) { return { ...obj, [key]: person[key] } } return obj }, {})
Có thể thấy rằng, chúng ta phải code rất nhiều để xử lý cho việc này. Bạn có thể đặt đoạn code này như một hàm dùng lại cho các chỗ khác. Tiếp theo chúng ta sẽ thử xem, làm thế nào để dùng immutable trong mảng
Array
Sau đây là một ví dụ về cách mà chúng ta thường thêm một phần tử vào trong mảng
const characters = [ 'Obi-Wan', 'Vader' ] const newCharacters = characters newCharacters.push('Luke') console.log(characters === newCharacters) // true
Giống như vấn đề với các object. Chúng ta thất bại trong việc tạo một mảng mới, chúng làm thay đổi giá trị của mảng trước. Tin vui là ES6 cũng có toán tử “spread” cho mảng, đây là cách sử dụng:
const characters = [ 'Obi-Wan', 'Vader' ] const newCharacters = [ ...characters, 'Luke' ] console.log(characters === newCharacters) // false console.log(characters) // [ 'Obi-Wan', 'Vader' ] console.log(newCharacters) // [ 'Obi-Wan', 'Vader', 'Luke' ]
Nó tương đối dễ dàng phải không? Chúng ta tạo ra một mảng mới mà chứa characters cộng với ‘luke’ và giữ cho characters nguyên vẹn không đổi.
Nào hãy cũng xem tiếp các hoạt động khác trên mảng, mà không gây thay đổi giá trị cho mảng ban đầu.
const characters = [ 'Obi-Wan', 'Vader', 'Luke' ] // Removing Vader const withoutVader = characters.filter(char => char !== 'Vader') console.log(withoutVader) // [ 'Obi-Wan', 'Luke' ] // Changing Vader to Anakin const backInTime = characters.map(char => char === 'Vader' ? 'Anakin' : char) console.log(backInTime) // [ 'Obi-Wan', 'Anakin', 'Luke' ] // All characters uppercase const shoutOut = characters.map(char => char.toUpperCase()) console.log(shoutOut) // [ 'OBI-WAN', 'VADER', 'LUKE' ] // Merging two character sets const otherCharacters = [ 'Yoda', 'Finn' ] const moreCharacters = [ ...characters, ...otherCharacters ] console.log(moreCharacters) // [ 'Obi-Wan', 'Vader', 'Luke', 'Yoda', 'Finn' ]
Trông rất ngắn gọn và đơn giản đúng ko ? Các hàm trong ES6 có thể viết tắt bằng cách dùng mũi tên (=>), nên trông nó sạch sẽ hơn nhiều. Chúng luôn sẽ trả về một mảng mới trong mỗi lần chạy
Nhưng nó lại có vấn đề khi sử dụng hàm sort như sau
const characters = [ 'Obi-Wan', 'Vader', 'Luke' ] const sortedCharacters = characters.sort() console.log(sortedCharacters === characters) // true console.log(characters) // [ 'Luke', 'Obi-Wan', 'Vader' ]
Hàm sort và hàm push nên trả về một mảng mới nhưng đáng tiếc lại không phải như vậy. Nếu bạn muốn sort bạn phải dùng slice để thực hiện
const characters = [ 'Obi-Wan', 'Vader', 'Luke' ] const sortedCharacters = characters.slice().sort() console.log(sortedCharacters === characters) // false console.log(sortedCharacters) // [ 'Luke', 'Obi-Wan', 'Vader' ] console.log(characters) // [ 'Obi-Wan', 'Vader', 'Luke' ]
Dùng slice()
giống như kiểu ‘hack’ để cho nó chạy như ta mong muốn. Nhưng như bạn nhìn thấy, chúng ta có được immutable mảng một cách đơn giản.
Hiệu suất
Hiệu suất thì thế nào ? Nó sẽ gây ảnh hưởng đển thời gian để tạo một object mới hay tiêu thụ bộ nhớ ko ? Chính xác, nó sẽ ảnh hưởng một chút đến hiệu suất. Nhưng những nhược điểm này là rất nhỏ so với những ưu điểm mà nó đem lại cho chúng ta
Một trong những hoạt động phức tạp hơn trong Javascript là phải theo dõi và kiểm soát object thay đổi. Giống như Object.observe(object, callback)
là khá nặng. Tuy nhiên, để kiểm soát trạng thái object nếu dùng immutable thì chúng ta sẽ chỉ cần dựa vào newObject == oldObject
để kiểm tra thay đổi hay chưa, cách này cũng đòi hỏi ít CPU hơn.
Ưu điểm thứ 2 là chất lượng code. Nó đảm bảo rằng khi tất cả đều immutable sẽ giúp cho bạn cấu trúc source code của ứng dụng tốt hơn. Nó cũng khuyến khích chúng ta lập trình theo cách Functional, giúp cho việc kiểm soát code tốt hơn, giảm rủi ro do code bẩn.