Giải mã bản chất cốt lõi của JavaScript (Phần 4, chương 2) là nội dung chuyển ngữ Việt ngữ từ tác phẩm kinh điển You Don’t Know JS Yet của Kyle Simpson.
Mở đầu
Trong chương trước thuộc hệ thống tài liệu nghiên cứu về các kiểu dữ liệu và ngữ pháp, chúng ta đã tiến hành một cuộc đại phẫu chi tiết vào bảy kiểu giá trị nguyên thủy được tích hợp sẵn trong tầng vi mạch của ngôn ngữ JavaScript, bao gồm: giá trị rỗng, giá trị không xác định, giá trị đúng sai, chuỗi ký tự, con số, số nguyên lớn, và biểu tượng. Việc thấu hiểu tường tận những viên gạch nền móng này đã đòi hỏi một nỗ lực tư duy không hề nhỏ, vượt xa những định kiến thông thường của đại đa số các kỹ sư phần mềm. Tuy nhiên, việc chỉ dừng lại ở mức độ nhận diện các kiểu dữ liệu là hoàn toàn chưa đủ để kiến tạo nên những hệ thống phần mềm với độ tin cậy tuyệt đối. Khi tâm trí đã trở nên tĩnh lặng và sẵn sàng tiếp nhận những tầng kiến thức sâu sắc hơn, chúng ta sẽ bước vào hành trình giải mã những hành vi đặc thù được quy định ngầm bởi từng kiểu giá trị nguyên thủy này. Cuộc thám hiểm học thuật này sẽ bóc trần những cơ chế hoạt động ẩn sâu dưới lớp vỏ bọc cú pháp, từ tính bất biến tuyệt đối cho đến những nghịch lý toán học, qua đó cung cấp một hệ quy chiếu toàn diện để các nhà phát triển có thể kiểm soát và dự đoán chính xác mọi phản ứng của dữ liệu trong quá trình thực thi chương trình.
Bản chất bất biến và cơ chế gán giá trị của kiểu nguyên thủy
Khái niệm về tính bất biến không chỉ là một đặc tính phụ trợ, mà nó là triết lý thiết kế cốt lõi định hình nên toàn bộ cách thức mà cỗ máy ngôn ngữ JavaScript quản lý và thao tác với các giá trị nguyên thủy trong không gian bộ nhớ. Sự thấu hiểu triết lý này là ranh giới phân định giữa một người thợ viết mã thông thường và một kiến trúc sư phần mềm thực thụ.
Tính bất biến tuyệt đối của các giá trị nguyên thủy
Toàn bộ mọi giá trị nguyên thủy tồn tại trong hệ sinh thái của ngôn ngữ JavaScript đều mang trong mình một bản chất bất biến tuyệt đối, một đặc tính kiến trúc mang ý nghĩa rằng không tồn tại bất kỳ một cơ chế hay thủ đoạn nào bên trong chương trình có thể thọc tay vào nội tạng của giá trị đó và thực hiện việc chỉnh sửa hay biến đổi nó. Khi một nhà phát triển khởi tạo một biến số lưu trữ tuổi tác và gán cho nó giá trị là bốn mươi hai, sau đó lại tiếp tục thực hiện một thao tác gán mới với giá trị là bốn mươi ba, một ảo giác quang học thường đánh lừa tư duy của họ rằng cái giá trị ban đầu đã bị thay đổi. Tuy nhiên, sự thật diễn ra dưới tầng vi mạch lại hoàn toàn khác biệt; thao tác gán thứ hai hoàn toàn không hề đụng chạm hay cạo sửa gì đến con số bốn mươi hai nguyên thủy, mà nó chỉ đơn thuần là việc gán lại một giá trị bốn mươi ba hoàn toàn mới vào cái vật chứa biến số đó, qua đó thay thế và quét sạch hoàn toàn dấu vết của giá trị cũ. Nguyên lý bất biến này cũng được áp dụng một cách triệt để và tàn nhẫn đối với mọi thao tác tính toán hay phép toán sinh ra giá trị mới. Lấy ví dụ, khi thực hiện phép cộng giữa con số bốn mươi hai và con số một, hay thao tác nối chuỗi giữa chữ Xin chào và dấu chấm than, kết quả nôn ra là con số bốn mươi ba và chuỗi Xin chào! đều là những giá trị hoàn toàn mới toanh, tồn tại độc lập và tách biệt hoàn toàn so với những giá trị nền tảng ban đầu. Sự tách bạch này mang lại một rào chắn bảo vệ cực kỳ kiên cố cho tính toàn vẹn của dữ liệu, ngăn chặn những hiệu ứng phụ không mong muốn khi nhiều luồng xử lý cùng truy xuất vào một giá trị chung. Không chỉ vậy, cơ chế bất biến còn đóng vai trò tối thượng trong việc tối ưu hóa quá trình thu gom rác thải bộ nhớ của cỗ máy thực thi; khi những giá trị nguyên thủy cũ không còn bất kỳ một sợi dây tham chiếu nào trỏ tới, chúng sẽ bị hệ thống âm thầm tiêu hủy một cách an toàn mà không gây ra bất kỳ sự xáo trộn nào cho các khối dữ liệu đang vận hành khác.
Sự nhầm lẫn tồi tệ nhất thường xảy ra khi các kỹ sư phần mềm đối mặt với kiểu giá trị chuỗi ký tự, một cấu trúc dữ liệu mang hình hài và vẻ bề ngoài trông giống hệt như một mảng chứa đựng các ký tự đơn lẻ – mà theo lẽ thường tình, nội dung bên trong mảng là thứ hoàn toàn có thể bị băm vằm và thay đổi theo ý muốn. Thế nhưng, bất chấp cái vẻ ngoài lừa tình đó, chuỗi ký tự vẫn là một giá trị nguyên thủy và do đó, nó bị khóa chặt trong sự bất biến vĩnh hằng. Nếu bạn cố tình thực hiện một phép gán để thay đổi ký tự tại một tọa độ chỉ mục cụ thể của một chuỗi, chẳng hạn như gán dấu chấm than vào vị trí thứ năm của chữ Xin chào., hệ thống sẽ phản ứng theo hai cách tùy thuộc vào chế độ thực thi. Trong chế độ lỏng lẻo thông thường, thao tác gán vô vọng này sẽ bị cỗ máy lạnh lùng phớt lờ và chết trong im lặng, để lại giá trị chuỗi nguyên vẹn như chưa từng có cuộc chạm trán nào xảy ra. Ngược lại, nếu đoạn mã đang bị cùm kẹp trong chế độ nghiêm ngặt, hành vi xâm phạm tài sản chỉ đọc này sẽ ngay lập tức kích nổ một ngoại lệ lỗi, trừng phạt thẳng tay nỗ lực vi phạm nguyên tắc thiết kế cốt lõi của ngôn ngữ. Cần phải đính chính một cách mạnh mẽ rằng, bản chất bất biến của các giá trị nguyên thủy này hoàn toàn không bị suy suyển hay chịu bất kỳ một sự tác động nào từ cái cách thức mà biến số chứa đựng nó được khai báo. Dù bạn sử dụng từ khóa khai báo hằng số, biến số cục bộ, hay biến số toàn cục, thì cái giá trị chuỗi nằm lọt thỏm bên trong đó vẫn luôn luôn bất biến. Từ khóa khai báo hằng số tuyệt đối không sở hữu năng lực ma thuật để nhào nặn ra các giá trị bất biến, mà nó chỉ đơn thuần là việc khai báo ra những vật chứa không cho phép tái gán giá trị – hay còn gọi là các phép gán bất biến – một khái niệm đã được mổ xẻ tường tận trong các tài liệu nghiên cứu về phạm vi và bao đóng. Tính toàn vẹn này đảm bảo rằng các chuỗi văn bản dùng làm khóa định danh hay dữ liệu cấu hình sẽ không bao giờ bị tiêm nhiễm hay thay đổi bởi các hàm xử lý bên thứ ba, giữ cho luồng thông tin của chương trình luôn nằm trong trạng thái có thể dự đoán và kiểm soát tuyệt đối.
Để đào sâu hơn vào ranh giới giữa khái niệm bất biến của giá trị và giới hạn quyền truy cập của vật chứa, chúng ta cần phải soi chiếu vào trường hợp của các thuộc tính trên đối tượng. Khi một thuộc tính trên một đối tượng được các kiến trúc sư cấu hình và đóng đinh mác là chỉ đọc – thông qua việc thiết lập cờ khả năng ghi đè thành giá trị sai bên trong bộ mô tả thuộc tính, như những gì đã được trình bày chi tiết trong cuốn sách nghiên cứu về Đối tượng và Lớp – điều này hoàn toàn không mang ý nghĩa là bản thân cái giá trị đang được lưu trữ bên trong thuộc tính đó bỗng nhiên hóa thành bất biến. Hành động cấu hình đó chỉ có tác dụng giăng ra một rào cản vật lý ngăn chặn nỗ lực gán một giá trị mới đè lên cái thuộc tính đó, bảo vệ định danh của thuộc tính chứ không bảo vệ bản chất của dữ liệu. Nếu cái giá trị nằm bên trong thuộc tính chỉ đọc đó là một mảng hay một đối tượng khác, bạn vẫn hoàn toàn có đặc quyền thọc tay vào nội tạng của chúng để thêm bớt phần tử hay thay đổi các thuộc tính con mà không vấp phải bất kỳ sự phản kháng nào từ hệ thống. Tuy nhiên, khi cái giá trị đang ngự trị bên trong thuộc tính chỉ đọc đó lại là một giá trị nguyên thủy, thì chúng ta có được một hệ thống phòng ngự kép hoàn hảo: bản thân cái thuộc tính vật chứa không thể bị gán lại bằng một giá trị khác, và chính cái giá trị nguyên thủy nằm bên trong cũng cự tuyệt mọi nỗ lực biến đổi cấu trúc. Sự giao thoa tinh vi giữa cơ chế kiểm soát thuộc tính của giao thức siêu đối tượng và tính bất biến nội tại của giá trị nguyên thủy tạo ra những mô hình kiến trúc dữ liệu cực kỳ kiên cố, là nền tảng sống còn cho việc thiết kế các kho lưu trữ trạng thái một chiều trong các ứng dụng máy tính cấp doanh nghiệp quy mô lớn. Việc thấu tỏ nguyên lý phân tách trách nhiệm này giúp các kỹ sư phần mềm ngăn chặn triệt để những con bọ logic phát sinh từ sự nhầm lẫn tai hại giữa việc thay đổi nội dung của một vật chứa và việc thay đổi bản chất của một giá trị, một bài toán kinh điển luôn làm đau đầu vô số các thế hệ lập trình viên trong quá trình gỡ lỗi hệ thống.
Hành vi thao tác với thuộc tính trên giá trị nguyên thủy
Bên cạnh tính bất biến, một ranh giới tuyệt đối khác cần phải được khắc cốt ghi tâm là hệ thống ban hành một lệnh cấm vĩnh viễn đối với việc đắp thêm bất kỳ một thuộc tính mới nào lên trên bề mặt của các giá trị nguyên thủy. Khi một nhà phát triển dại dột thực thi một câu lệnh nhằm mục đích gán thêm một thuộc tính mang tên đã kết xuất với giá trị đúng vào một biến số đang ôm giữ chữ Xin chào., kết quả nhận được sẽ là một sự thất bại thảm hại. Đoạn mã này trông có vẻ như đang thành công trong việc tiêm nhiễm một thuộc tính mới vào giá trị chuỗi, thế nhưng hành vi gán này sẽ bị hệ thống lạnh lùng phớt lờ và chết trong im lặng, một phản ứng xảy ra ngay cả khi chương trình đang bị cùm kẹp trong chế độ thực thi nghiêm ngặt. Khi truy xuất lại cái thuộc tính vừa mới gán đó, cỗ máy sẽ vô tình nôn ra giá trị không xác định, phơi bày sự ảo tưởng của người lập trình. Quy luật này càng trở nên hà khắc hơn đối với các giá trị nguyên thủy mang tính chất rỗng như giá trị rỗng và giá trị không xác định; bất kỳ một nỗ lực thọc mạch nào nhằm truy cập thuộc tính trên hai kẻ ngoại đạo này đều sẽ ngay lập tức kích nổ hệ thống và ném ra một ngoại lệ sập chương trình. Thế nhưng, một nghịch lý đầy thách thức đối với tư duy logic là: mặc dù cấm thêm mới, nhưng hệ thống lại bật đèn xanh cho phép truy cập vào các thuộc tính đã được tích hợp sẵn trên toàn bộ những giá trị nguyên thủy phi rỗng còn lại – một hành vi nghe có vẻ hoàn toàn đi ngược lại với trực giác. Sự hiện diện của các thuộc tính khả dụng này không phá vỡ quy luật bất biến, mà thực chất nó là một sự thỏa hiệp có chủ đích của ngôn ngữ nhằm cung cấp các giao diện tương tác cần thiết cho các thuật toán xử lý dữ liệu ở tầng cao.
Để minh họa một cách trực quan cho cái nghịch lý truy cập thuộc tính này, hãy soi chiếu vào trường hợp của mọi giá trị chuỗi ký tự, chúng đều được cỗ máy ưu ái trang bị sẵn một thuộc tính chỉ đọc mang tên chiều dài. Khi truy cập vào thuộc tính chiều dài của một chuỗi chứa chữ Xin chào., hệ thống sẽ ngoan ngoãn trả về con số sáu. Thuộc tính chiều dài này là một pháo đài bất khả xâm phạm, tuyệt đối không thể bị ép buộc thay đổi giá trị thông qua phép gán, thế nhưng nó lại luôn luôn sẵn sàng mở cửa cho các thao tác đọc và truy xuất. Vai trò cốt lõi của nó là phơi bày ra ánh sáng tổng số lượng các đơn vị mã đang được giam giữ bên trong cái giá trị chuỗi đó – một khái niệm kỹ thuật chuyên sâu về mã hóa ký tự đã được mổ xẻ tường tận trong chương trước – thứ mà trong đại đa số các trường hợp thông thường thường đồng nghĩa với số lượng ký tự hiển thị bằng mắt thường. Tuy nhiên, cần phải gióng lên một hồi chuông cảnh báo học thuật: đối với hầu hết các ký tự tiêu chuẩn, phương trình một ký tự bằng một điểm mã bằng một đơn vị mã là hoàn toàn chính xác. Thế nhưng, câu chuyện sẽ trở nên cực kỳ phức tạp và rắc rối khi chúng ta phải đối mặt với các ký tự Unicode mở rộng sở hữu điểm mã vượt qua giới hạn cực đại là sáu vạn năm ngàn năm trăm ba mươi lăm. Như những gì đã được giải phẫu trong chương trước, những ký tự siêu việt này bắt buộc phải được hệ thống lưu trữ dưới hình hài của hai đơn vị mã tách biệt, được gọi là các nửa thay thế. Hậu quả tàn khốc là, đối với mỗi một ký tự mang cấu trúc dị hợm như vậy, thuộc tính chiều dài sẽ lạnh lùng cộng thêm hai đơn vị vào tổng số đếm của nó, bất chấp một sự thật nhãn tiền là cái ký tự đó chỉ hiển thị ra màn hình như một biểu tượng đồ họa duy nhất.
Vượt ra ngoài ranh giới của các thuộc tính dữ liệu mộc mạc, các giá trị nguyên thủy phi rỗng còn mang trong mình một sức mạnh ngầm định khi chúng cung cấp một vài phương thức tiện ích được tích hợp sẵn theo tiêu chuẩn hệ thống, luôn trong tư thế sẵn sàng để được triệu hồi. Lấy ví dụ, một giá trị chuỗi ký tự hoàn toàn có thể tự tin đáp trả các lời gọi hàm như phương thức chuyển đổi thành chuỗi hay phương thức lấy giá trị thực, nôn ra chính cái giá trị nguyên bản của nó – mặc dù hành động này thường bị coi là dư thừa và mang tính chất lặp lại không cần thiết. Thêm vào đó, hầu hết các kiểu giá trị nguyên thủy đều tự trang bị cho mình một kho vũ khí riêng biệt bao gồm các phương thức chứa đựng những hành vi xử lý mang tính chất đặc thù, gắn liền với bản chất nội tại của kiểu dữ liệu đó. Những phương thức chuyên biệt này – thứ sẽ được chúng ta mổ xẻ một cách tàn nhẫn ở phần sau của tài liệu – đóng vai trò là những công cụ đắc lực để thao tác, trích xuất, và nhào nặn dữ liệu mà không cần phải viện đến các hàm xử lý bên ngoài. Dưới góc độ hàn lâm, việc làm thế nào mà một giá trị nguyên thủy trơ trọi – thứ vốn dĩ không được phép sở hữu bất kỳ một thuộc tính hay phương thức nào – lại có thể kiêu hãnh đứng ra phản hồi các thao tác truy cập này là một bí ẩn kỹ thuật đáng kinh ngạc. Lời giải đáp cho cái nghịch lý này nằm ở một cơ chế vi mạch cực kỳ tinh vi của cỗ máy ngôn ngữ, một thao tác ép kiểu ngầm định được giới chuyên môn xưng tụng là tự động đóng hộp… Cơ chế này cho phép hệ thống chớp nhoáng tạo ra một lớp vỏ bọc đối tượng ảo bao quanh giá trị nguyên thủy ngay tại khoảnh khắc thao tác truy cập diễn ra, và sau đó vứt bỏ lớp vỏ đó ngay khi nhiệm vụ hoàn tất, một chủ đề mang tính chất sinh tử sẽ được đào sâu ở chương tiếp theo.
Cơ chế sao chép giá trị trong các phép gán
Khi bước vào lãnh địa của các thao tác quản lý bộ nhớ, một nguyên lý tối thượng cai quản toàn bộ các giá trị nguyên thủy là: bất kỳ một phép gán nào thực hiện việc luân chuyển một giá trị nguyên thủy từ một biến số hay vật chứa này sang một biến số hay vật chứa khác đều mang bản chất là một quá trình sao chép giá trị thuần túy. Hãy tưởng tượng một kịch bản kiến trúc khi chúng ta khởi tạo một biến số lưu trữ tuổi của tôi và gán cho nó con số bốn mươi hai, sau đó tiếp tục khởi tạo một biến số lưu trữ tuổi của bạn và gán cho nó bằng chính biến số tuổi của tôi. Trong bối cảnh này, quá trình gán dữ liệu không hề tạo ra một sợi dây liên kết hay một sự phụ thuộc nào giữa hai biến số, mà thay vào đó, hệ thống tiến hành một thao tác sao chép giá trị độc lập. Hậu quả là, cả hai biến số tuổi của tôi và tuổi của bạn đều ngạo nghễ ôm giữ một bản sao hoàn toàn riêng biệt của cái giá trị số bốn mươi hai đó. Sự độc lập về mặt dữ liệu này là một trong những cột mốc phân thủy lĩnh quan trọng nhất giúp phân biệt rạch ròi giữa hành vi của các giá trị nguyên thủy và hành vi của các giá trị đối tượng – nơi mà phép gán chỉ đơn thuần là việc sao chép sợi dây tham chiếu trỏ về cùng một vùng nhớ vật lý. Đối với các giá trị nguyên thủy, biến số đóng vai trò như những chiếc hòm an toàn, nơi mà mỗi chiếc hòm đều tự hào cất giữ một thỏi vàng độc lập của riêng mình, không chung đụng và không chịu bất kỳ một sự chi phối nào từ các chiếc hòm khác trong hệ thống. Việc thấu hiểu triết lý sao chép giá trị này là tiền đề bắt buộc để có thể truy xuất và dự đoán chính xác luồng dịch chuyển của trạng thái dữ liệu trong những thuật toán đa luồng phức tạp.
Tuy nhiên, dưới lăng kính của việc tối ưu hóa hiệu suất vi mạch, sự thật diễn ra đằng sau cánh gà của cỗ máy thực thi JavaScript có thể mang một hình hài tinh vi và phức tạp hơn rất nhiều so với những gì được phơi bày ở tầng cú pháp. Nằm sâu bên trong nội tạng của cỗ máy, hoàn toàn có khả năng xảy ra một kịch bản tối ưu hóa cực đoan nơi mà chỉ tồn tại duy nhất một phiên bản vật lý của cái giá trị bốn mươi hai đó nằm chễm chệ trong không gian bộ nhớ. Nhằm mục đích tiết kiệm tài nguyên hệ thống một cách tối đa, cỗ máy ngôn ngữ có thể mưu mẹo thiết lập các con trỏ nội bộ từ cả hai biến số tuổi của tôi và tuổi của bạn chĩa thẳng vào cái giá trị vật lý dùng chung duy nhất đó. Hành vi chia sẻ tài nguyên ngầm định này không hề vi phạm các quy tắc của ngôn ngữ, bởi vì, như chúng ta đã khắc cốt ghi tâm, toàn bộ mọi giá trị nguyên thủy đều mang trong mình bản chất bất biến tuyệt đối. Chính cái lớp áo giáp bất biến này đã triệt tiêu hoàn toàn mọi nguy cơ rủi ro liên quan đến việc một biến số vô tình hay cố ý làm vấy bẩn và thay đổi giá trị đang được chia sẻ chung, gây ra hiệu ứng sập đổ dây chuyền lên các biến số khác. Đối với tầng kiến trúc sư và các nhà phát triển ứng dụng JavaScript như chúng ta, những thủ thuật tối ưu hóa ngầm định này của cỗ máy thực thi là hoàn toàn vô hình và không đáng bận tâm. Điều cốt lõi và mang tính chất sinh tử đối với tư duy logic trong việc xây dựng chương trình là chúng ta phải luôn luôn ngầm định và đối xử với các biến số như thể chúng thực sự đang ôm giữ những bản sao dữ liệu hoàn toàn độc lập của riêng mình, từ chối mọi ý niệm về sự chia sẻ hay ràng buộc dữ liệu.
Sự độc lập tuyệt đối của các bản sao giá trị nguyên thủy này được phơi bày một cách trần trụi và rõ nét nhất khi chúng ta tiến hành thao tác tái gán giá trị trong các bước tiếp theo của vòng đời chương trình. Lặp lại kịch bản trước đó, nếu ở một thời điểm trong tương lai, chúng ta quyết định tái gán biến số tuổi của tôi thành con số bốn mươi ba – ví dụ như khi tôi vừa tổ chức sinh nhật – hành động này chỉ tác động duy nhất lên cái bản sao dữ liệu đang nằm lọt thỏm bên trong vật chứa của riêng nó. Khi sử dụng toán tử tăng lên một đơn vị, thứ về mặt bản chất cấu trúc có thể được coi là một biến thể rút gọn của phép toán cộng thêm một vào chính nó, hệ thống sẽ tiến hành bòn rút giá trị cũ, tính toán ra con số bốn mươi ba mới toanh, và nhét ngược nó trở lại vào biến số tuổi của tôi. Kết quả nhãn tiền là, trong khi biến số tuổi của tôi đã hãnh diện vươn lên thành con số bốn mươi ba, thì cái giá trị bốn mươi hai tội nghiệp đang được giam giữ bên trong biến số tuổi của bạn vẫn hoàn toàn không hề hấn gì và giữ nguyên trạng thái nguyên thủy của nó. Sự miễn nhiễm này chính là minh chứng hùng hồn nhất cho nguyên lý sao chép giá trị, đập tan mọi lo ngại về những hệ lụy sửa đổi ngoài ý muốn khi truyền tải các tham số nguyên thủy xuyên qua các tầng lớp hàm và mô đun phức tạp. Nhờ có cái ranh giới cách ly dữ liệu kiên cố này, các nhà thiết kế thuật toán có thể yên tâm nhào nặn, băm vằm, và thao túng các giá trị biến số cục bộ mà không phải thấp thỏm lo âu về việc gây ra những cơn địa chấn làm sụp đổ trạng thái dữ liệu của toàn bộ hệ thống phần mềm bao trùm bên ngoài.
Hành vi và đặc tính cấu trúc của chuỗi ký tự
Các giá trị mang kiểu chuỗi ký tự không chỉ đơn thuần là những đoạn văn bản tĩnh vô hồn, mà chúng còn ôm giữ một loạt các hành vi xử lý cực kỳ đặc thù và phức tạp. Việc thấu tỏ những đặc tính nội tại này là mệnh lệnh bắt buộc đối với bất kỳ một kỹ sư JavaScript nào mang tham vọng thao túng luồng dữ liệu văn bản một cách an toàn và tối ưu.
Cơ chế truy xuất và vòng lặp phân giải ký tự
Mặc dù sự thật hiển nhiên là các chuỗi ký tự hoàn toàn không mang trong mình bản chất kiến trúc của một mảng dữ liệu, thế nhưng ngôn ngữ JavaScript lại rủ lòng thương và cung cấp một lớp đường cú pháp cho phép thực thi các thao tác truy xuất ký tự mang phong cách y hệt như mảng, thông qua việc sử dụng cặp dấu ngoặc vuông để chỉ định một tọa độ chỉ mục bắt đầu từ con số không. Khi áp dụng cú pháp ngoặc vuông với tọa độ là bốn lên một biến số đang ôm giữ chuỗi Xin chào!, hệ thống sẽ ngay lập tức định vị và nôn ra ký tự o đang nằm tại đúng cái vị trí đó. Một đặc tính ép kiểu ngầm định cực kỳ thú vị và ẩn chứa nhiều cạm bẫy của cỗ máy ngôn ngữ là: nếu như cái giá trị hay biểu thức nằm lọt thỏm giữa cặp ngoặc vuông đó không thể tự phân giải thành một con số chuẩn mực, hệ thống sẽ tự động thi hành một mệnh lệnh ép kiểu ngầm định nhằm biến đổi cái giá trị lạc loài đó trở về hình hài của một con số nguyên toán học, miễn là điều đó còn nằm trong giới hạn khả thi. Do đó, việc truyền vào một chuỗi 4 thay vì con số bốn trần trụi vẫn sẽ mang lại kết quả truy xuất chính xác là ký tự o. Tuy nhiên, lưỡi gươm công lý của hệ thống sẽ giáng xuống nếu như cái giá trị hoặc biểu thức đó phân giải ra một con số nằm lọt thỏm bên ngoài ranh giới an toàn của dải chỉ mục nguyên từ không cho đến giới hạn chiều dài trừ đi một, hoặc tồi tệ hơn là phân giải ra giá trị số không hợp lệ. Trong những kịch bản thảm họa đó, hoặc nếu cái giá trị truyền vào hoàn toàn không thuộc về kiểu con số, thao tác truy xuất mạo danh mảng này sẽ bị hệ thống lột mặt nạ và đối xử y hệt như một thao tác truy cập thuộc tính thông thường với một cái tên thuộc tính tương đương bằng chuỗi. Và hệ lụy tất yếu là, nếu cái nỗ lực truy cập thuộc tính đó chuốc lấy thất bại vì thuộc tính không hề tồn tại, kết quả cuối cùng được hệ thống nôn ra sẽ là giá trị không xác định.
Mặc dù không phải là mảng, nhưng các chuỗi ký tự lại mang trong mình một khả năng giả dạng mảng cực kỳ xuất sắc ở vô số các khía cạnh kiến trúc khác nhau. Một trong những hành vi giả dạng mang tính chiến lược nhất là việc các chuỗi ký tự, cũng giống y hệt như mảng, đều là những cấu trúc dữ liệu có thể lặp lại được. Đặc tính siêu việt này mang một ý nghĩa học thuật to lớn: các ký tự cấu thành nên chuỗi – hay nói một cách chính xác hơn về mặt vi mạch là các đơn vị mã – hoàn toàn có thể bị lôi cổ ra và duyệt qua từng phần tử một cách tuần tự. Khi sử dụng vòng lặp for…of để quét qua một biến số chứa chữ Kyle, cỗ máy thực thi sẽ cần mẫn bốc tách và in ra từng ký tự đơn lẻ K, y, l, e trong mỗi một chu kỳ lặp. Không dừng lại ở đó, khả năng lặp lại này còn mở toang cánh cửa cho việc ứng dụng cú pháp trải rộng mang ký hiệu ba dấu chấm. Bằng cách nhốt cú pháp trải rộng cùng với biến số chuỗi vào bên trong cặp ngoặc vuông của mảng, hệ thống sẽ ngay lập tức xé toạc cái chuỗi nguyên khối đó ra và nhào nặn thành một mảng dữ liệu hoàn chỉnh, nơi mà mỗi một phần tử của mảng chính là một ký tự đơn lẻ được bóc tách từ chuỗi gốc. Tính linh hoạt này cung cấp cho các kiến trúc sư phần mềm những công cụ cực kỳ sắc bén để chuyển đổi qua lại giữa các hình thái lưu trữ văn bản, phục vụ đắc lực cho các thuật toán tìm kiếm, thay thế, hay mã hóa dữ liệu ở tầng thấp mà không cần phải viện đến những hàm tiện ích cồng kềnh.
Cơ chế lặp lại tinh vi này hoàn toàn không phải là một phép thuật ngẫu nhiên, mà nó tuân thủ nghiêm ngặt theo một hệ thống giao thức nội bộ của ngôn ngữ. Các giá trị vật lý, điển hình như chuỗi ký tự hay mảng dữ liệu, chỉ được hệ thống chính thức dán nhãn phong thánh là những cấu trúc có thể lặp lại – qua đó cho phép sử dụng cú pháp trải rộng ba dấu chấm, vòng lặp for…of, hay hàm tạo mảng từ nguồn – nếu như và chỉ nếu chúng có khả năng phơi bày ra một phương thức chuyên biệt dùng để sản xuất bộ lặp. Cái phương thức thần thánh này bắt buộc phải được định vị tại một tọa độ thuộc tính siêu việt mang tên là biểu tượng lặp, một cấu trúc biểu tượng danh tiếng đã được chúng ta mổ xẻ tường tận trong chương trước. Khi thọc tay vào gọi cái phương thức sản xuất bộ lặp này trên một chuỗi ký tự, hệ thống sẽ nôn ra một đối tượng bộ lặp. Mỗi lần triệu hồi phương thức next trên cái đối tượng bộ lặp đó, hệ thống sẽ nhả ra một đối tượng kết quả chứa đựng hai mảnh thông tin sinh tử: một thuộc tính giá trị mang theo cái ký tự hiện tại đang được duyệt tới, và một thuộc tính trạng thái hoàn thành đóng vai trò như một cờ hiệu báo cáo xem quá trình lặp đã chạm đến đáy hay chưa. Ngay cả ở lần lặp vớt vát cuối cùng để lấy ra ký tự e, cờ trạng thái hoàn thành vẫn ngoan cố báo cáo là sai, một đặc điểm thiết kế cực kỳ tinh vi của giao thức bộ lặp nhằm đảm bảo rằng hệ thống chỉ báo cáo hoàn thành khi và chỉ khi nó đã vượt ra khỏi giới hạn cuối cùng của dữ liệu. Những tiểu tiết kiến trúc mang tính chất học thuật cao độ này của giao thức lặp lại sẽ tiếp tục được bóc tách và phân tích sâu hơn trong các tài liệu chuyên ngành về lập trình đồng bộ và bất đồng bộ.
Những thách thức phức tạp trong việc tính toán chiều dài chuỗi
Như những luận điểm đã được thiết lập vững chắc trong chương trước, các giá trị chuỗi ký tự luôn tự hào phơi bày một thuộc tính chiều dài dùng để tự động báo cáo về kích thước của chính nó; một đặc tính bất khả xâm phạm chỉ cho phép truy xuất và thẳng tay phớt lờ mọi nỗ lực gán giá trị làm thay đổi nó. Trong tư duy trực giác của đại đa số lập trình viên, cái giá trị chiều dài được nôn ra này thường được kỳ vọng là có sự tương đồng tuyệt đối với số lượng các ký tự đang hiện diện bên trong chuỗi – hay nói một cách kỹ thuật hơn là các đơn vị mã – thế nhưng, bức tranh toàn cảnh lại trở nên tăm tối và phức tạp hơn gấp bội phần khi có sự nhúng tay của các ký tự mã hóa đa ngôn ngữ Unicode. Đối với nhãn quan của một con người bình thường, chúng ta thường phân tách và nhận diện các biểu tượng hiển thị một cách rời rạc; cái ý niệm về một biểu tượng đồ họa độc lập đứng trơ trọi một mình này được giới ngôn ngữ học và khoa học máy tính định danh bằng một thuật ngữ chuyên ngành là một hình vị, hay một cụm hình vị… Chính vì cái hệ quy chiếu trực quan đó, khi bàn luận về việc đếm chiều dài của một chuỗi văn bản, logic thông thường luôn ngầm định rằng chúng đang tiến hành kiểm đếm số lượng các hình vị đang tồn tại. Đáng buồn thay, đó hoàn toàn không phải là cái cách thức mà cỗ máy tính toán cấu trúc và đếm ký tự ở tầng vi mạch. Bên trong não bộ của JavaScript, mỗi một ký tự thực chất là một đơn vị mã có kích thước mười sáu bit, mang trong mình một giá trị điểm mã tối đa là sáu vạn năm ngàn năm trăm ba mươi lăm. Thuộc tính chiều dài của một chuỗi luôn luôn mù quáng đếm tổng số lượng các đơn vị mã vật lý đang lọt thỏm trong giá trị chuỗi, chứ tuyệt đối không thèm đếm số lượng các điểm mã logic. Một đơn vị mã đó có thể kiêu hãnh đứng một mình đại diện cho một ký tự trọn vẹn, hoặc nó có thể chịu kiếp làm một mảnh ghép của một cặp thay thế, hoặc bị cưỡng ép dung hợp với một biểu tượng kết hợp nằm kề cạnh, hay thậm chí là một thành phần cấu tạo nên một cụm hình vị đồ sộ. Hậu quả nhãn tiền là, cái con số chiều dài được trả về hiếm khi nào khớp nối hoàn hảo với cái khái niệm đếm ký tự hay đếm hình vị trực quan của con người.
Nhằm mục đích kéo sát cái con số toán học vô hồn đó lại gần hơn với cái chiều dài hình vị trực quan và hợp lý nhất cho một chuỗi văn bản, một bước sơ chế dữ liệu bắt buộc là cái giá trị chuỗi nguyên thủy đó cần phải được trải qua thuật toán chuẩn hóa bằng việc gọi phương thức chuẩn hóa kết hợp nguyên dạng. Như đã được mổ xẻ trong tài liệu về Chuẩn hóa Unicode ở chương trước, thao tác này gánh vác sứ mệnh nặn ra bất kỳ một đơn vị mã tổ hợp nào nếu điều kiện cho phép, nhằm dập tắt nguy cơ hệ thống bóc tách và tính toán sai lệch trong trường hợp các ký tự ban đầu bị lưu trữ dưới hình hài phân rã thành nhiều đơn vị mã rời rạc. Lấy một ví dụ sắc bén, một chữ điện thoại có mang dấu gạch trên đầu nếu không được chuẩn hóa có thể bị báo cáo chiều dài là chín – một kết quả gây sốc – nhưng sau khi được gột rửa qua thuật toán chuẩn hóa, chiều dài sẽ ngoan ngoãn tụt xuống còn tám – một con số hợp lý mang lại tiếng thở phào nhẹ nhõm. Đáng tiếc thay, bóng ma của các ký tự sở hữu điểm mã lớn hơn sáu vạn năm ngàn năm trăm ba mươi lăm vẫn luôn chực chờ ám ảnh chúng ta, ép buộc hệ thống phải viện đến một cặp thay thế gồm hai đơn vị mã để có thể biểu diễn chúng. Một hệ lụy kiến trúc tàn khốc là những ký tự siêu việt này sẽ trơ trẽn chiếm đoạt gấp đôi không gian và đếm thành hai vị trí trong thuộc tính chiều dài. Một biểu tượng điện thoại di động thông minh sẽ mang chiều dài là hai, phá nát mọi logic đếm thông thường. Một chiến lược giải cứu khả dĩ cho thảm họa này là lợi dụng vòng lặp phân giải ký tự thông qua cú pháp trải rộng, bởi vì bộ lặp nội tại của chuỗi sở hữu trí thông minh để tự động nhận diện và trả về toàn bộ một ký tự hợp nhất từ một cặp thay thế, giúp con số đếm tụt xuống còn một một cách an toàn. Thế nhưng, sự xuất hiện của các cụm hình vị lại tiếp tục ném một hòn đá tảng vào bộ máy tính toán chiều dài, phá vỡ mọi hy vọng về một giải pháp hoàn hảo.
Hãy soi chiếu vào trường hợp cực đoan của một biểu tượng cảm xúc hình ngón tay cái chúc xuống được gắn thêm mã bổ trợ làm tối màu da; mặc dù chỉ hiển thị như một biểu tượng duy nhất, nhưng chiều dài chuỗi bị báo cáo là bốn, và thậm chí khi dùng thủ thuật cú pháp trải rộng thì kết quả vẫn là hai, hoàn toàn bế tắc. Đây là minh chứng hùng hồn cho việc hai điểm mã hoàn toàn độc lập, do sự sắp xếp kề vai sát cánh nhau, đã ép buộc hệ thống kết xuất Unicode vẽ ra một biểu tượng duy nhất, bỏ mặc thuật toán đếm chiều dài trong sự ngơ ngác. Việc xây dựng một cỗ máy tính toán đủ sức tái hiện lại toàn bộ logic kết xuất Unicode phức tạp của một hệ điều hành nhằm nhận diện chính xác các cụm điểm mã đó thành một ký tự duy nhất để đếm chiều dài là một thách thức kỹ thuật khổng lồ, đòi hỏi những thư viện đồ sộ đánh đổi bằng tài nguyên bộ nhớ khắt khe. Chính vì sự phức tạp điên rồ này, ngay cả những gã khổng lồ công nghệ như mạng xã hội Twitter cũng đã phải nhượng bộ. Trước đây, người dùng kỳ vọng có thể nhồi nhét hai trăm tám mươi biểu tượng cảm xúc vào một dòng trạng thái, nhưng Twitter lại thiết lập quy tắc tính sổ hai ký tự cho mọi biểu tượng Unicode. Bất kể đó là ngón tay cái mặc định với chiều dài chuỗi là hai, ngón tay cái tối màu với chiều dài chuỗi là bốn, hay thậm chí là biểu tượng gia đình với chiều dài chuỗi lên đến bảy, tất cả đều bị cào bằng và tính là hai ký tự, khiến sức chứa thực tế giảm đi một nửa. Thay đổi này diễn ra vào năm 2018 nhằm san bằng sự bất công trong việc sử dụng biểu tượng đa văn hóa, nhưng đồng thời cũng phơi bày một sự thật tàn nhẫn: việc đếm chiều dài của một chuỗi để khớp với hệ quy chiếu trực quan của con người thực sự là một nghệ thuật đầy rẫy sự đánh đổi, chứ không còn là một môn khoa học chính xác.
Cơ chế so sánh và thao tác nối chuỗi trong môi trường đa ngôn ngữ
Để đáp ứng nhu cầu khát khao mở rộng quy mô của các chương trình JavaScript trong môi trường quốc tế hóa và đa văn hóa, hội đồng ECMAScript đã dày công ban hành bộ Giao diện lập trình ứng dụng Quốc tế hóa ECMAScript. Theo nguyên lý hoạt động mặc định, một chương trình JavaScript sẽ ngoan ngoãn ngả theo các thiết lập ngôn ngữ và vùng miền do môi trường máy chủ hoặc trình duyệt áp đặt. Cái bối cảnh vùng miền đang có hiệu lực này sở hữu quyền lực tối thượng trong việc chi phối toàn bộ các thuật toán liên quan đến việc sắp xếp, so sánh giá trị, định dạng hiển thị, và hàng loạt các hành vi ngầm định khác. Những sự can thiệp kiến trúc này bộc lộ một cách trần trụi nhất khi thao tác với các chuỗi văn bản, nhưng chúng cũng âm thầm tác động lên cả cách thức hệ thống xử lý các con số và dữ liệu ngày tháng. Bản thân các ký tự chuỗi cũng có thể cất giấu những thông tin vùng miền nội tại, thứ sẽ đè bẹp các thiết lập mặc định của môi trường. Đặc biệt, cấu trúc nội dung của chuỗi có quyền tự quyết định xem nó sẽ được kết xuất hiển thị từ trái sang phải hay từ phải sang trái; chính vì lẽ đó, các thuật toán thao tác chuỗi luôn ưu tiên sử dụng các định danh mang tính logic như bắt đầu hay kết thúc thay cho các khái niệm định hướng vật lý. Trong các ngôn ngữ hiển thị từ phải sang trái như tiếng Do Thái cổ hay tiếng Ả Rập, ký tự đầu tiên trong mã nguồn thực chất lại là ký tự nằm ngoài cùng bên phải khi được vẽ lên màn hình. Bạn không cần phải viết mã ngược từ phải sang trái, mà cứ tuần tự khai báo theo tọa độ logic; lớp vỏ kết xuất hình ảnh sẽ gánh vác trách nhiệm đảo ngược trật tự hiển thị đó một cách hoàn hảo. Do đó, việc truy xuất chỉ mục số không sẽ luôn móc ra cái ký tự logic đứng đầu mã nguồn, chứ tuyệt đối không bị đánh lừa bởi vị trí hiển thị ngược đời của nó trên màn hình.
Sự tác động của vùng miền lên hành vi của chuỗi càng trở nên dữ dội khi chúng ta bàn về các cơ chế so sánh quan hệ. Các giá trị chuỗi hoàn toàn có thể được ném vào các bàn cân so sánh quan hệ như nhỏ hơn, lớn hơn, hoặc bằng nhau để phân định trật tự sắp xếp. Các toán tử so sánh lớn hơn và nhỏ hơn sẽ mổ xẻ hai giá trị chuỗi theo trật tự từ vựng học – một thuật toán sắp xếp dựa trên bảng chữ cái tương tự như cách bạn tra cứu một cuốn từ điển dày cộp. Cần phải đặc biệt lưu ý rằng các toán tử này hoàn toàn dựa dẫm vào thiết lập vùng miền đang có hiệu lực tại thời điểm chạy để đưa ra phán quyết. Tuy nhiên, trong những kịch bản kiến trúc đòi hỏi sự cứng rắn, các kỹ sư phần mềm hoàn toàn có thể cưỡng ép cỗ máy phải tuân thủ theo một hệ quy chiếu vùng miền cụ thể bằng cách triệu hồi phương thức so sánh theo vùng miền. Phương thức này là một vũ khí sắc bén cho phép so sánh hai chuỗi theo các quy tắc ngôn ngữ khắt khe, nôn ra một con số âm nếu chuỗi gốc đứng trước chuỗi đối chiếu trong từ điển, hoặc một con số dương nếu nó đứng sau. Sức mạnh này được củng cố thêm thông qua cấu trúc đối tượng bộ đối chiếu chuẩn quốc tế, cho phép khởi tạo ra các bộ máy sắp xếp tùy chỉnh có khả năng nhận diện sự ưu tiên chữ hoa chữ thường hay phân loại các ký tự đặc thù của từng quốc gia. Khi phải thao tác sắp xếp một mảng dữ liệu chứa hàng vạn tên người dùng đến từ nhiều quốc gia khác nhau, việc ứng dụng trực tiếp đối tượng bộ đối chiếu chuẩn quốc tế này không chỉ mang lại độ chính xác tuyệt đối mà còn tối ưu hóa hiệu suất thực thi vượt trội so với việc liên tục gọi phương thức so sánh chuỗi cục bộ.
Đối với bài toán kiểm tra tính ngang bằng, ngôn ngữ cung cấp hai trường phái đối lập: so sánh ngang bằng tuyệt đối với ba dấu bằng, và so sánh ngang bằng ép kiểu với hai dấu bằng. Toán tử ba dấu bằng luôn quét qua kiểu dữ liệu trước, nếu lệch pha là thẳng tay bác bỏ; còn nếu trùng kiểu chuỗi, nó sẽ soi chiếu từng đơn vị mã một cách tàn nhẫn từ đầu đến cuối. Trong khi đó, toán tử hai dấu bằng lại mang trong mình một bản chất ép kiểu cực đoan; nếu hai bên không cùng kiểu, nó sẽ ưu tiên bẻ cong mọi thứ về dạng con số toán học để phân định thắng thua. Do đó, một phép thử ngang bằng chỉ thực sự là so sánh chuỗi khi và chỉ khi cả hai ứng viên đều đã khoác lên mình tấm áo chuỗi ký tự ngay từ đầu. Ranh giới giữa hai toán tử này là chủ đề của những cuộc tranh luận đẫm máu trong giới học thuật, thế nhưng việc tẩy chay toán tử hai dấu bằng đôi khi lại tước đoạt đi tính linh hoạt nghệ thuật của ngôn ngữ. Vượt lên trên các phép thử so sánh, khi chúng ta có nhu cầu dung hợp hai hay nhiều đoạn văn bản lại với nhau, toán tử cộng sẽ hóa thân thành một cỗ máy nối chuỗi khổng lồ, miễn là một trong hai vế có sự hiện diện của một giá trị chuỗi. Nếu một vế là chuỗi còn vế kia mang kiểu dữ liệu ngoại đạo, cỗ máy sẽ tàn nhẫn lôi cổ kẻ ngoại đạo đó ra và ép nó phải hóa thành chuỗi trước khi dán dính chúng lại với nhau. Tuy nhiên, trong kỷ nguyên của lập trình hiện đại, kỹ thuật nối chuỗi cổ lỗ sĩ này thường mang lại mã nguồn rối rắm và dễ sinh lỗi; thay vào đó, các kiến trúc sư phần mềm được khuyến nghị sử dụng sức mạnh nội suy của chuỗi mẫu với dấu tích ngược, một cấu trúc thanh lịch cho phép nhúng trực tiếp mọi giá trị hay biểu thức phức tạp vào ngay giữa lòng đoạn văn bản mà không cần phải gọi toán tử cộng một cách chắp vá.
Bản chất toán học và giới hạn xử lý của con số
Bỏ qua những tranh cãi về văn bản, sức mạnh cốt lõi của mọi cỗ máy tính toán luôn nằm ở khả năng nhai nuốt và thao túng các con số. Tuy nhiên, cách thức mà ngôn ngữ JavaScript giam cầm và xử lý các giá trị số học lại ẩn chứa những giới hạn vật lý và nghịch lý bất ngờ đòi hỏi sự cảnh giác cao độ từ phía người lập trình.
Sai số dấu phẩy động và sự ảo tưởng về hằng số dung sai
Để có thể thuần phục được hệ thống số học, chúng ta buộc phải đào xới lại những khái niệm nền tảng về Tiêu chuẩn IEEE-754 đã được thiết lập từ chương trước. Một trong những cái bẫy kinh điển và gây sốc nhất đối với bất kỳ một tân binh nào khi bước chân vào lãnh địa của kiến trúc số học dấu phẩy động – và cần phải hét lên thật to rằng đây tuyệt đối không phải là một lỗi lầm độc quyền của riêng JavaScript – chính là sự thật tàn nhẫn rằng không phải mọi thao tác toán học hay giá trị nào cũng có thể được nhét vừa vặn vào những khuôn mẫu biểu diễn nhị phân giới hạn. Minh chứng đẫm máu nhất cho thảm họa này là phép toán cộng mộc mạc giữa con số không phẩy một và con số không phẩy hai; thay vì nôn ra một kết quả viên mãn là không phẩy ba như bao cuốn sách giáo khoa toán học vẫn rao giảng, hệ thống lại trơ trẽn trả về một dãy số thập phân dị hợm kết thúc bằng con số bốn. Hậu quả là, một phép thử so sánh ngang bằng tuyệt đối giữa kết quả của phép cộng đó và con số không phẩy ba sẽ bị cỗ máy lạnh lùng bác bỏ và ném trả lại giá trị sai. Sự sai lệch kinh hoàng này bắt nguồn từ hiện tượng trôi dạt sai số dấu phẩy động; khi mổ xẻ cấu trúc nhị phân của hai con số đó, chúng sẽ kinh ngạc phát hiện ra rằng chúng chỉ lệch nhau vỏn vẹn ở hai bit cuối cùng. Thế nhưng, chỉ một sự khác biệt mỏng manh đó thôi cũng đã quá đủ để phán quyết rằng hai giá trị này là hoàn toàn bất bình đẳng. Sự cám dỗ buông lời chế giễu ngôn ngữ JavaScript vì cái nghịch lý này luôn chực chờ bùng nổ, thế nhưng những lời nhạo báng đó là hoàn toàn vô tri; bất kỳ một ngôn ngữ lập trình nào trên thế giới dại dột chọn Tiêu chuẩn IEEE-754 làm xương sống số học đều sẽ phải hứng chịu chung một số phận hẩm hiu tương tự.
Nhằm mục đích cứu rỗi các thuật toán khỏi cái địa ngục sai số dấu phẩy động này, một phương án thường xuyên được giới hàn lâm rỉ tai nhau là viện đến một hằng số dung sai cực kỳ bé nhỏ được hệ thống tích hợp sẵn mang tên hằng số Epsilon. Hằng số siêu việt này đại diện cho khoảng cách sai biệt nhỏ bé nhất mà cỗ máy ngôn ngữ có khả năng phân định và biểu diễn giữa con số một và cái giá trị liền kề ngay sát sạt phía trên nó. Bất chấp việc cái giá trị tuyệt đối này có thể bị bóp méo đôi chút tùy thuộc vào nền tảng phần cứng, nó thường loanh quanh ở ngưỡng hai phẩy hai nhân mười lũy thừa âm mười sáu. Đối với những bộ óc thiếu vắng sự đào sâu nghiên cứu – trong đó tôi cũng tự thú nhận bản thân mình trong quá khứ – một sự ngầm định chết người thường được thiết lập: bất kỳ một độ lệch pha nào nảy sinh từ một phép toán dấu phẩy động đơn lẻ sẽ vĩnh viễn không bao giờ có thể phình to vượt quá cái giới hạn Epsilon đó. Theo đuổi cái hệ quy chiếu ngây thơ này, vô số các kỹ sư đã thi nhau nhào nặn ra các hàm tiện ích kiểm tra sự ngang bằng an toàn, trong đó họ tính toán giá trị tuyệt đối của hiệu số giữa hai con số và hân hoan kết luận chúng là ngang bằng nếu cái hiệu số đó lọt thỏm dưới ngưỡng Epsilon. Quả thực, khi đem cái hàm tiện ích đó ra thử lửa với phép toán không phẩy một cộng không phẩy hai và đem so sánh với không phẩy ba, hệ thống sẽ gật đầu xác nhận là đúng, mang lại một ảo giác chiến thắng ngọt ngào.
Thế nhưng, cái ảo giác chiến thắng đó sẽ nhanh chóng bị vỡ vụn và biến thành một thảm họa logic tồi tệ. Khúc ca bi tráng thực sự cất lên khi chúng ta nâng quy mô của các con số lên một chút; thử ném phép toán mười phẩy một cộng không phẩy hai vào hàm kiểm tra dung sai đó để đọ sức với mười phẩy ba, cỗ máy sẽ tàn nhẫn vả vào mặt chúng ta một giá trị sai. Sự thật phũ phàng được phơi bày: cái ngưỡng dung sai Epsilon đó chỉ thực sự phát huy tác dụng bảo vệ khi hệ thống thao tác với những con số cực kỳ ti ti; một khi giá trị phình to ra, hằng số này trở nên quá đỗi chật hẹp và liên tục nôn ra những kết quả âm tính giả một cách vô tội vạ. Đứng trước bờ vực thẳm này, việc cố gắng nhân bản cái hằng số Epsilon đó lên một tỷ lệ nào đó để tạo ra một cái lồng dung sai lớn hơn là một canh bạc kiến trúc vô cùng rủi ro, hoàn toàn dựa dẫm vào cảm tính và việc phỏng đoán quy mô dữ liệu sẽ chảy qua chương trình. Không tồn tại bất kỳ một thuật toán thần thánh nào có thể tự động bóp nghẹt hay phình to cái ngưỡng dung sai đó một cách hoàn hảo cho mọi trường hợp. Lời khuyên xương máu và mang tính chất định hướng kiến trúc tối thượng là: hãy thẳng tay vứt bỏ cái mộng tưởng sử dụng hằng số Epsilon làm lá chắn. Nếu hệ thống đòi hỏi độ chính xác tuyệt đối trong toán học, hãy né tránh hoàn toàn các số thập phân bằng cách nhân bản mọi thứ lên thành số nguyên khổng lồ để tính toán, chỉ chia nhỏ chúng ra ở khâu hiển thị cuối cùng; hoặc hãy viện đến sức mạnh của các thư viện giả lập số thập phân chuyên biệt, hoàn toàn chối bỏ cái kiểu giá trị con số đầy khiếm khuyết của hệ thống.
Cơ chế so sánh và các nghịch lý toán học nền tảng
Giống hệt như cách mà hệ thống cai quản các chuỗi văn bản, các giá trị con số cũng bị ném vào đấu trường của các toán tử so sánh để phân định tính ngang bằng và thứ tự quan hệ. Phép thử ngang bằng đối với các con số sử dụng chung một kho vũ khí bao gồm toán tử hai dấu bằng ép kiểu, toán tử ba dấu bằng nghiêm ngặt, hoặc phương thức kiểm tra tĩnh của đối tượng. Trong trường hợp hai ứng viên tham gia đọ sức đã cùng mang trong mình dòng máu của kiểu con số, toán tử ép kiểu sẽ trút bỏ lớp vỏ bọc và hành xử khắt khe y hệt như người anh em nghiêm ngặt của nó. Thế nhưng, khi kiểu dữ liệu của hai bên bị lệch pha, toán tử hai dấu bằng sẽ lập tức bộc lộ bản chất thiên vị toán học cực đoan của mình; nếu một trong hai kẻ không phải là chuỗi, nó sẽ tàn nhẫn cưỡng ép cả hai phải lột xác và hóa thân thành những con số toán học trước khi đưa lên bàn cân. Điều này có nghĩa là một chuỗi văn bản chứa chữ số sẽ bị nghiền nát và đúc lại thành số nguyên để so đọ, chứ không bao giờ có chuyện chiều ngược lại xảy ra. Một khi cả hai đã cùng đứng trên cùng một hệ quy chiếu toán học, chúng sẽ bị soi xét đến từng bit nhị phân. Cần phải khắc cốt ghi tâm rằng, cỗ máy JavaScript hoàn toàn bị mù trước các hình thái cú pháp như số nguyên trần trụi hay số thập phân mang theo hàng tá số không vô nghĩa ở đuôi; dưới tầng vi mạch, chúng bị nung chảy thành một giá trị đồng nhất duy nhất, và hiển nhiên mọi phép thử ngang bằng giữa chúng đều trả về kết quả mỹ mãn. Sự ép kiểu ngầm định này đòi hỏi một tư duy cảnh giác cao độ để tránh những pha xử lý luồng dữ liệu mù quáng dựa trên vẻ bề ngoài của biến số.
Hệ quy chiếu trực giác của con người luôn thầm nhủ rằng, nếu hai con số có diện mạo giống hệt nhau, thì chúng phải là một. Tư duy đó hoàn toàn trùng khớp với cách cỗ máy diễn dịch. Thế nhưng, tại sao kết quả của phép toán không phẩy một cộng không phẩy hai lại cự tuyệt sự ngang bằng với con số không phẩy ba? Sự thật tàn khốc là, cái giá trị được sinh ra từ phép toán đó chỉ nằm mấp mé và tiệm cận vô cùng sát sao với không phẩy ba, nhưng chúng hoàn toàn không phải là những kẻ song sinh đồng nhất ở tầng bit nhị phân. Điểm mấu chốt gây sốc ở đây là, khoảng cách sai biệt giữa hai cái giá trị đó lại nhỏ bé đến mức nó chìm nghỉm và lọt thỏm hoàn toàn dưới ngưỡng giới hạn của hằng số Epsilon. Điều đó đồng nghĩa với việc bộ máy toán học của JavaScript hoàn toàn bất lực và bị tê liệt trong việc biểu diễn cái khoảng cách mong manh đó một cách chính xác. Một bộ óc thiếu cảnh giác có thể lập luận rằng, nếu cái sự khác biệt đó quá đỗi ti ti để hệ thống có thể nắm bắt, thì theo một khía cạnh phi chính thức nào đó, chúng nên được hệ thống du di và phán quyết là ngang bằng. Thế nhưng, hãy nhìn cho kỹ: hệ thống vẫn thừa sức nhận diện được sự tồn tại của cái khoảng cách đó, minh chứng là cái đuôi chứa con số bốn thòi lòi ra trong kết quả tính toán. Bạn thậm chí hoàn toàn có thể gõ ra một hằng số thập phân đại diện cho cái sự khác biệt đó, nhưng khi ném nó vào bộ máy nhị phân dấu phẩy động IEEE-754, cỗ máy sẽ giương cờ trắng đầu hàng vì không thể biểu diễn một con số nhỏ đến thế với độ chính xác cần thiết để phục vụ cho bất kỳ một phép toán nào khác.
Bức tranh về tính ngang bằng số học càng trở nên hỗn loạn hơn với sự xuất hiện của hai ngoại lệ tàn khốc, mang tính chất phá vỡ mọi quy luật logic thông thường, ngay cả khi bạn đang núp bóng sự bảo vệ của toán tử ba dấu bằng nghiêm ngặt. Ngoại lệ thứ nhất là sự hiện diện của giá trị số không hợp lệ, một thực thể mang mầm mống hỗn mang, vĩnh viễn không bao giờ chịu cúi đầu thừa nhận sự ngang bằng với chính bản thân nó. Ngoại lệ thứ hai là nghịch lý của số không âm, thứ luôn luôn ăn nằm và được phán quyết là ngang bằng tuyệt đối với số không dương. Sự phản bội của toán tử nghiêm ngặt trước hai ngoại lệ này là nguồn cơn gây ra những cơn đau đầu không lối thoát cho các lập trình viên. Giải pháp cứu rỗi duy nhất cho thảm họa này là thẳng tay vứt bỏ các toán tử so sánh thông thường và chạy trốn vào vòng tay bảo vệ của phương thức kiểm tra tĩnh trên đối tượng nguyên thủy, thứ đã được cỗ máy tước bỏ hoàn toàn những ngoại lệ tai hại đó. Mở rộng ra các phép thử quan hệ như lớn hơn hay nhỏ hơn, chúng ta lại tiếp tục đối mặt với bản tính ép kiểu ngầm định tàn nhẫn tương tự như toán tử hai dấu bằng. Không hề tồn tại bất kỳ một toán tử so sánh quan hệ nào mang tính chất nghiêm ngặt để trốn tránh việc ép kiểu. Nếu bạn không chủ động thi hành thiết quân luật để đảm bảo rằng cả hai ứng viên đều là những con số nguyên chất trước khi tống chúng vào lò so sánh, các toán tử này sẽ tự động giở thói ép kiểu quan hệ, nghiền nát mọi kiểu dữ liệu ngoại đạo thành số học, sinh ra những kết quả sắp xếp điên rồ và phá nát toàn bộ trật tự dữ liệu của hệ thống.
Toán tử bitwise và ranh giới tương thích với số nguyên lớn
Mặc dù JavaScript là một ngôn ngữ bậc cao, nó vẫn cung cấp một kho vũ khí các toán tử thao tác cấp độ bit, cho phép các kiến trúc sư thọc sâu vào thao túng trực tiếp các dải nhị phân của một giá trị số. Tuy nhiên, một cú lừa ngoạn mục về mặt kiến trúc là: những phép toán bit này hoàn toàn cự tuyệt việc thao tác trực tiếp lên trên cái khuôn mẫu nhị phân dấu phẩy động sáu mươi tư bit nguyên thủy đang chứa đựng con số đó. Thay vào đó, một chuỗi các thao tác vi mạch phức tạp sẽ được kích hoạt: cỗ máy sẽ cưỡng ép nung chảy và nhào nặn cái con số ban đầu đó lọt thỏm vào trong một khuôn khổ số nguyên có dấu ba mươi hai bit chật hẹp, thi hành phép toán bit trên cái khuôn khổ mới đó, rồi sau đó lại một lần nữa nấu chảy kết quả và đúc ngược trở lại vào hình hài của một con số dấu phẩy động tiêu chuẩn. Quá trình chuyển đổi qua lại này biến các thao tác bit trở thành một công cụ cực kỳ hữu hiệu để băm vằm và gọt giũa dữ liệu, đặc biệt là trong thủ thuật ép kiểu số thập phân bằng toán tử hoặc bitwise với con số không. Vì toán tử bit chỉ chấp nhận làm việc với số nguyên, phép toán này sẽ thẳng tay chặt đứt và vứt bỏ hoàn toàn phần thập phân của con số, để lại một phần số nguyên trần trụi. Một ảo tưởng chết người thường lan truyền trong cộng đồng là coi thủ thuật này như một bản sao hoàn hảo của hàm làm tròn xuống. Sự thật là, chúng chỉ đồng thuận khi đối mặt với số dương; khi đụng chạm đến lãnh địa của số âm, hàm làm tròn xuống sẽ tuân thủ nguyên lý toán học và kéo giá trị chìm sâu hơn về phía âm vô cực, trong khi toán tử bit lại chỉ hành xử như một lưỡi dao cắt gọt thô bạo, vứt bỏ phần đuôi và sinh ra những kết quả hoàn toàn trái ngược.
Sự tồn tại của kiểu dữ liệu con số chủ yếu là để phục vụ như những viên gạch xây dựng nên các lâu đài thuật toán toán học phức tạp, và để hỗ trợ cho tham vọng đó, hệ sinh thái ngôn ngữ đã cẩn thận gom nhóm hàng tá các hằng số và hàm tiện ích toán học kinh điển vào chung một không gian tên gọi tĩnh mang tên Toán học. Khác biệt hoàn toàn với đối tượng Số học vốn dĩ có thể được triệu hồi như một hàm để thi hành việc ép kiểu, không gian Toán học chỉ đơn thuần là một cái túi chứa đồ vô tri giác; mọi nỗ lực gọi nó như một hàm điện toán đều sẽ bị hệ thống trừng phạt bằng ngoại lệ. Bên trong cái túi đó chứa đựng những công cụ mạnh mẽ từ tính giá trị tuyệt đối, làm tròn, cho đến truy tìm giá trị cực đại hay cực tiểu. Thế nhưng, một thành viên mang trong mình mầm mống của sự bất ổn bảo mật nằm chễm chệ trong không gian này chính là hàm sinh số ngẫu nhiên, một công cụ được thiết kế để nôn ra một giá trị thập phân lọt thỏm giữa số không và số một. Việc xếp chung thao tác sinh số ngẫu nhiên – một hành vi mang nặng tính chất tạo ra hiệu ứng phụ và thao túng trạng thái hệ thống – vào cùng một mâm với các hàm toán học thuần túy là một quyết định kiến trúc gây nhiều tranh cãi. Đáng sợ hơn, thuật toán sinh số giả ngẫu nhiên chống lưng cho cái hàm này hoàn toàn là một trò hề dưới lăng kính của giới bảo mật mật mã, vô cùng dễ bị dự đoán và thao túng. Chính vì sự yếu kém này, nền tảng web đã buộc phải tung ra một hệ thống giao diện thay thế cung cấp các giá trị ngẫu nhiên an toàn tuyệt đối ở tầng mật mã, và việc tiếp tục sử dụng hàm ngẫu nhiên cổ lỗ sĩ kia giờ đây bị cộng đồng chuyên gia tẩy chay mạnh mẽ trong mọi hệ thống yêu cầu độ bảo mật cao.
Khi bức tranh số học của chương trình bị xé lẻ và chia sẻ giữa kiểu con số thông thường và kiểu số nguyên lớn, một hàng rào cách ly dữ liệu tàn khốc sẽ được hệ thống giương lên: hai kiểu giá trị này bị cấm tiệt tuyệt đối không được phép giao cấu hay hòa trộn với nhau trong bất kỳ một biểu thức toán học nào. Sự cấm đoán này hà khắc đến mức, ngay cả một thao tác lặp vòng lặp ngây thơ sử dụng toán tử tăng thêm một đơn vị trên một biến số nguyên lớn cũng sẽ lập tức kích nổ một ngoại lệ sập chương trình, buộc người lập trình phải ý thức rõ ràng về kiểu dữ liệu mà họ đang thao tác. Hậu quả của sự phân ly này là các kiến trúc sư phần mềm phải liên tục tự tay nhào nặn và ép kiểu thủ công giữa hai thái cực mỗi khi có nhu cầu luân chuyển dữ liệu xuyên qua các hàm xử lý khác nhau. Việc gọi hàm Số nguyên lớn sẽ cưỡng ép một con số thông thường hóa thân thành khổng lồ, và ngược lại, hàm Số học sẽ cố gắng bóp nghẹt một gã khổng lồ thu nhỏ lại thành một con số dấu phẩy động. Tuy nhiên, những thao tác ép kiểu chéo này không phải là một con đường trải đầy hoa hồng mà nó chằng chịt những bãi mìn rủi ro. Nếu bạn cố gắng ép một con số thập phân, một giá trị số không hợp lệ, hay một giá trị vô cực vào khuôn mẫu của số nguyên lớn, hệ thống sẽ nổi điên và ném thẳng vào mặt bạn một ngoại lệ vi phạm giới hạn. Ngược lại, nếu bạn cố tình nhồi nhét một số nguyên lớn với kích thước vượt quá sức chứa của giới hạn dấu phẩy động sáu mươi tư bit vào hàm Số học, dữ liệu sẽ bị vỡ vụn và hệ thống chỉ đành bất lực nôn ra một giá trị Vô cực vô nghĩa. Việc kiểm soát chặt chẽ ranh giới chuyển đổi này là bài kiểm tra sống còn đối với độ ổn định của hệ thống.
Kết luận
Trải qua hành trình nghiên cứu sâu rộng trong suốt hai chương học thuật vừa qua, chúng ta đã tiến hành giải phẫu một cách không thương tiếc toàn bộ những hành vi, đặc tính, và cả những góc khuất đầy cạm bẫy của các giá trị nguyên thủy trong ngôn ngữ JavaScript. Những khái niệm tưởng chừng như khô khan và đáng bị bỏ qua này, từ tính bất biến kiên cố, sự phức tạp của bảng mã đa ngôn ngữ, cho đến những nghịch lý toán học điên rồ của hệ thống dấu phẩy động, thực chất lại chính là những viên gạch nền móng sinh tử quyết định sự thành bại của mọi thuật toán và kiến trúc phần mềm. Việc thấu tỏ cặn kẽ những nguyên lý cơ bản này không chỉ giúp các kiến trúc sư tránh được những hố đen lỗi logic tiềm ẩn, mà còn rèn giũa một tư duy lập trình sắc bén, tuân thủ nghiêm ngặt các triết lý thiết kế của cỗ máy thực thi. Tuy nhiên, bức tranh toàn cảnh về hệ thống dữ liệu của JavaScript vẫn chưa dừng lại ở lãnh địa của những kẻ nguyên thủy mộc mạc này. Trong chương học thuật tiếp theo, mũi dùi nghiên cứu của chúng ta sẽ chuyển hướng tấn công trực diện vào một thế giới dữ liệu mang cấu trúc khổng lồ và phức tạp hơn gấp bội phần: các kiểu giá trị đối tượng, nơi mà những quy luật về tham chiếu và siêu lập trình sẽ mở ra một chân trời thách thức hoàn toàn mới.
Đọc Giải mã bản chất cốt lõi của JavaScript chương 1.1 tại đây.
Đọc Giải mã bản chất cốt lõi của JavaScript chương 1.2 tại đây.
Đọc Giải mã bản chất cốt lõi của JavaScript chương 1.3 tại đây.
Đọc Giải mã bản chất cốt lõi của JavaScript chương 1.4 tại đây.
Đọc Giải mã bản chất cốt lõi của JavaScript chương 2.1 tại đây.
Đọc Giải mã bản chất cốt lõi của JavaScript chương 2.2 tại đây.
Đọc Giải mã bản chất cốt lõi của JavaScript chương 2.3 tại đây.
Đọc Giải mã bản chất cốt lõi của JavaScript chương 2.4 tại đây.
Đọc Giải mã bản chất cốt lõi của JavaScript chương 2.5 tại đây.
Đọc Giải mã bản chất cốt lõi của JavaScript chương 2.6 tại đây.
Đọc Giải mã bản chất cốt lõi của JavaScript chương 2.7 tại đây.
Đọc Giải mã bản chất cốt lõi của JavaScript chương 2.8 tại đây.
Đọc Giải mã bản chất cốt lõi của JavaScript chương 3.1 tại đây.
Đọc Giải mã bản chất cốt lõi của JavaScript chương 3.2 tại đây.
Đọc Giải mã bản chất cốt lõi của JavaScript chương 3.3 tại đây.
Đọc Giải mã bản chất cốt lõi của JavaScript chương 3.4 tại đây.
Đọc Giải mã bản chất cốt lõi của JavaScript chương 3.5 tại đây.
Đọc Giải mã bản chất cốt lõi của JavaScript chương 4.1 tại đây.
Đọc Giải mã bản chất cốt lõi của JavaScript chương 4.2 tại đây.
Đọc Giải mã bản chất cốt lõi của JavaScript chương 4.3 tại đây.
Đọc Giải mã bản chất cốt lõi của JavaScript chương 4.4 tại đây.

- thu-vien (1039)
- viet-lach (284)
- javascript (21)
- lap-trinh (21)
- lap-trinh-web (21)
- web-development (21)
- ydkjs (21)
- get-started (21)
- you-dont-know-js-yet (21)
- chua-biet-javascript (21)
- chua-biet-ro-javascript (21)
- kyle-simpson (21)
- coercion (21)
- type-awareness (21)
- triet-ly-lap-trinh (21)
- giai-ma-javascript (21)
- giai-ma-ban-chat-coi-loi-javascript (21)