Mở đầu
Phương pháp tiếp cận hiệu quả và mang tính bền vững nhất để thấu hiểu tường tận về ngôn ngữ lập trình JavaScript chính là bắt tay trực tiếp vào quá trình kiến tạo mã nguồn. Tuy nhiên, trước khi có thể tự tin triển khai các hệ thống phần mềm phức tạp, điều kiện tiên quyết mang tính bắt buộc là người học phải nắm vững cơ chế vận hành nội tại ở tầng sâu nhất của hệ sinh thái này. Ngay cả khi bạn đã tích lũy được nhiều năm kinh nghiệm phong phú thông qua việc làm việc với vô số các nền tảng ngôn ngữ lập trình khác nhau, bạn vẫn cần phải dành ra một khoảng thời gian thỏa đáng để thích nghi, thực hành và thấu hiểu cặn kẽ từng khía cạnh kiến trúc cốt lõi của ngôn ngữ này. Chương tài liệu này hoàn toàn không được thiết kế với mục đích trở thành một cẩm nang tham chiếu cạn kiệt mọi chi tiết nhỏ nhặt về mặt cú pháp, và nó cũng tuyệt đối không hướng tới việc đóng vai trò như một bài học nhập môn cơ bản, hời hợt. Thay vào đó, mục tiêu học thuật tối thượng của chúng ta là tiến hành một cuộc khảo sát toàn diện, phân tích đa chiều về các phân vùng chủ đề quan trọng bậc nhất, từ đó xây dựng một cảm quan kỹ thuật sắc bén, làm cơ sở vững chắc cho quá trình tự kiến tạo nên những hệ thống chương trình quy mô lớn với một mức độ tự tin và sự tinh tế ở chuẩn mực cao nhất.
Cấu trúc tệp tin và bản chất của chương trình
Kiến trúc nền tảng của một hệ thống phần mềm được xây dựng bằng ngôn ngữ JavaScript không tồn tại dưới dạng một khối nguyên khối khổng lồ, mà được phân mảnh một cách có chủ đích thành vô số các tệp tin mã nguồn độc lập. Tư duy thiết kế này đòi hỏi các kiến trúc sư hệ thống phải rũ bỏ quan niệm truyền thống về một chương trình điện toán duy nhất, và thay vào đó, họ phải đối mặt với một thực tại phức tạp hơn: mỗi tệp tin vật lý về bản chất là một chương trình thu nhỏ, sở hữu vòng đời và không gian thực thi hoàn toàn riêng biệt. Việc thấu hiểu triết lý phân mảnh này là điều kiện tiên quyết để nắm bắt cách thức công cụ thông dịch xử lý các rủi ro hỏng hóc, cũng như cách mà các mảnh ghép rời rạc này có thể giao tiếp, chia sẻ trạng thái và hợp nhất lại thông qua các cơ chế không gian toàn cục hoặc hệ thống khối hiện đại để tạo nên một trải nghiệm liền mạch trên nền tảng mạng lưới toàn cầu. Sự chuyển dịch từ góc nhìn vĩ mô xuống vi mô trong cấu trúc tệp tin chính là chìa khóa để kiến tạo nên những hệ thống ứng dụng có khả năng mở rộng, dễ dàng bảo trì và sở hữu độ tin cậy tuyệt đối trong môi trường vận hành thực tiễn.
Tính độc lập của các tệp tin mã nguồn
Hầu như toàn bộ các trang thông tin trực tuyến hay các hệ thống ứng dụng mạng lưới toàn cầu mà người dùng cuối tương tác hàng ngày đều không phải là một khối kiến trúc nguyên khối duy nhất, mà thực chất chúng được cấu thành từ một tập hợp vô số các tệp tin mã nguồn có phần mở rộng đặc trưng là định dạng tập tin kịch bản. Dưới góc nhìn của một người sử dụng thông thường hoặc thậm chí là của một kỹ sư mới bước chân vào nghề, sức cám dỗ của việc đánh giá toàn bộ tổ hợp phức tạp đó như một chương trình điện toán duy nhất, thống nhất là vô cùng lớn. Thế nhưng, khi phân tích sâu dưới lăng kính kiến trúc cốt lõi của bộ thông dịch ngôn ngữ JavaScript, hệ thống lại vận hành theo một góc nhìn hoàn toàn trái ngược và mang tính phân mảnh cao độ. Hệ sinh thái này được thiết kế với một triết lý đặc thù: mỗi một tệp tin mã nguồn hoạt động hoàn toàn độc lập và được đối xử như một chương trình điện toán biệt lập, trọn vẹn mang trong mình vòng đời thực thi riêng biệt. Nguyên lý thiết kế phân mảnh này đóng một vai trò tối quan trọng, đặc biệt là khi chúng ta thảo luận về các cơ chế xử lý lỗi và tính bền vững (resilience) của toàn bộ hệ thống phần mềm. Việc bóc tách ứng dụng thành vô số các chương trình nhỏ nhắn đảm bảo rằng sự gián đoạn của một bộ phận không nhất thiết kéo theo sự sụp đổ của toàn bộ cấu trúc tổng thể.
Bởi vì nền tảng ngôn ngữ mặc định đối xử với từng tệp tin vật lý như những chương trình điện toán độc lập, một sự cố nghiêm trọng dẫn đến việc hỏng hóc có thể xảy ra ở một tệp tin bất kỳ – dù là trong giai đoạn công cụ phân tích cú pháp tĩnh thực thi nhiệm vụ hay trong suốt quá trình chương trình chạy – cũng sẽ không nhất thiết trở thành một rào cản ngăn chặn công cụ thông dịch tiếp tục xử lý các tệp tin mã nguồn tiếp theo trong hàng đợi. Hiển nhiên, xét trên khía cạnh logic nghiệp vụ, nếu một hệ thống ứng dụng được thiết kế dựa trên sự phụ thuộc lẫn nhau của năm tệp tin riêng biệt, và một trong số đó gặp phải lỗi nghiêm trọng dẫn đến ngừng hoạt động, thì khả năng cao nhất là toàn bộ hệ thống ứng dụng tổng thể đó sẽ chỉ có thể vận hành ở một trạng thái khiếm khuyết, đáp ứng được một phần vô cùng nhỏ nhoi các tính năng được kỳ vọng ban đầu. Chính vì bản chất kỹ thuật này, một trong những trách nhiệm nặng nề nhất đặt lên vai của các kiến trúc sư phần mềm là phải áp dụng mọi biện pháp kỹ thuật tinh vi nhất để đảm bảo rằng từng chương trình thu nhỏ (tức là từng tệp tin) phải được cấu trúc để vận hành một cách hoàn hảo, đồng thời sở hữu khả năng phòng ngự chủ động, xử lý các lỗi phát sinh từ các tệp tin lân cận một cách duyên dáng và an toàn nhất có thể. Sự phụ thuộc lỏng lẻo này là con dao hai lưỡi: nó mang lại sự cô lập lỗi tuyệt vời nhưng đồng thời cũng đòi hỏi nỗ lực bảo trì trạng thái đồng bộ cực kỳ khắt khe.
Có thể bạn sẽ cảm thấy vô cùng kinh ngạc và khó chấp nhận ngay lập tức trước quan điểm học thuật coi các tệp tin riêng lẻ là những chương trình điện toán hoàn toàn biệt lập. Từ góc độ trải nghiệm của người dùng cuối khi tương tác với nền tảng ứng dụng, mọi thứ diễn ra quá đỗi mượt mà, tạo ra một ảo giác hoàn hảo về sự hiện diện của một chương trình khổng lồ, nguyên khối đang xử lý mọi tác vụ ở tầng nền. Ảo giác kiến trúc này có được là nhờ vào cơ chế thực thi đặc biệt của trình duyệt web, nơi cung cấp một môi trường thời gian chạy (runtime environment) phức tạp, cho phép những chương trình cá thể này tương tác, hợp tác chéo và liên kết với nhau một cách chặt chẽ để cùng chung tay mô phỏng lại hình thái của một hệ thống thống nhất duy nhất. Tuy nhiên, cần phải làm rõ một bối cảnh thực tiễn: trong các dự án quy mô doanh nghiệp, các kỹ sư thường xuyên ứng dụng các công cụ tự động hóa quy trình xây dựng (build process tools) nhằm mục đích nhào nặn, tối ưu và gộp chung tất cả các tệp tin rời rạc này thành một tệp tin khổng lồ duy nhất trước khi phân phối nó thông qua mạng lưới đến trình duyệt của khách hàng. Khi quá trình đóng gói kiến trúc này hoàn tất, bộ thông dịch JavaScript lúc bấy giờ mới thực sự đối xử với tệp tin khổng lồ đã được tổng hợp đó như là một và chỉ một chương trình điện toán duy nhất thống trị toàn bộ vòng đời của trang thông tin.
Cơ chế chia sẻ trạng thái thông qua phạm vi toàn cục
Trong mô hình kiến trúc truyền thống nơi hệ thống bao gồm nhiều tệp tin mã nguồn hoạt động hoàn toàn độc lập, phương thức kỹ thuật duy nhất để các mảnh ghép rời rạc này có thể liên kết và hành xử như một khối phần mềm hợp nhất chính là thông qua cơ chế chia sẻ trạng thái dữ liệu (state) và cấp quyền truy cập vào các hàm chức năng công khai thông qua một khái niệm không gian mạng được định nghĩa là phạm vi toàn cục. Khi các tệp tin được tải vào môi trường thực thi của trình duyệt, toàn bộ các biến số, đối tượng và hàm được khai báo ở cấp độ cao nhất sẽ hòa trộn, hợp lưu lại với nhau bên trong một không gian tên (namespace) toàn cục khổng lồ. Quá trình trộn lẫn vô hình này diễn ra trong suốt quá trình phân tích và biên dịch, để rồi khi bước vào giai đoạn thực thi thời gian chạy, sự kết hợp dữ liệu này cho phép các đoạn mã nằm ở tệp tin này có thể gọi đến và thao tác trực tiếp trên các cấu trúc dữ liệu được khởi tạo ở một tệp tin khác, từ đó dệt nên một bức tranh ứng dụng đa chiều và vận hành như một thể thống nhất. Mặc dù phương pháp này đã chứng minh được tính hiệu quả trong những ngày đầu của kỷ nguyên điện toán đám mây, nhưng nó lại che giấu vô số rủi ro hệ thống đáng quan ngại đối với sự an toàn của hệ thống.
Việc lạm dụng cơ chế giao tiếp thông qua phạm vi toàn cục đã sinh ra một trong những vấn đề hóc búa nhất trong khoa học máy tính: hiện tượng ô nhiễm không gian tên (namespace pollution). Khi quy mô của các hệ thống ứng dụng phình to với hàng trăm, hàng ngàn tệp tin mã nguồn, khả năng xảy ra sự cố va chạm định danh – tức là việc hai tệp tin khác nhau vô tình sử dụng chung một tên biến hoặc tên hàm toàn cục – tăng lên theo cấp số nhân. Một khi sự va chạm này xảy ra, tệp tin được tải sau sẽ tàn nhẫn ghi đè lên toàn bộ dữ liệu hoặc logic đã được thiết lập bởi tệp tin tải trước đó, dẫn đến những lỗi logic tiềm ẩn cực kỳ tinh vi, khó tái hiện và vô cùng tốn kém để gỡ lỗi. Hơn thế nữa, việc phơi bày toàn bộ cấu trúc trạng thái nghiệp vụ ra phạm vi toàn cục đồng nghĩa với việc phá vỡ mọi nguyên tắc nền tảng về tính đóng gói (encapsulation) trong kỹ thuật phần mềm. Bất kỳ một đoạn mã kịch bản độc hại nào được nhúng trái phép vào trang thông tin cũng có thể dễ dàng truy cập, thao tác, hoặc đánh cắp các luồng dữ liệu nhạy cảm đang trôi nổi tự do trong không gian bộ nhớ toàn cục này.
Đứng trước sự mong manh và tính rủi ro cao độ của cấu trúc phạm vi toàn cục, cộng đồng kiến trúc sư phần mềm đã phải tốn hàng thập kỷ để sáng tạo ra những mẫu thiết kế (design patterns) nhằm hạn chế tối đa sự phụ thuộc tồi tệ này. Các khuôn mẫu kiến trúc như định nghĩa đối tượng không gian tên duy nhất (single namespace object) hay biểu thức hàm thực thi ngay lập tức (Immediately Invoked Function Expression) đã trở thành chuẩn mực vàng trong một thời gian rất dài. Bằng cách gói gọn toàn bộ logic của một tệp tin vào bên trong một hàm kín và chỉ cố ý phơi bày ra phạm vi toàn cục một đối tượng duy nhất đóng vai trò là giao diện giao tiếp, các nhà phát triển đã thành công trong việc giả lập một ranh giới bảo vệ, cô lập trạng thái dữ liệu nội bộ khỏi sự xâm phạm của các đoạn mã ngoại lai. Mặc dù các phương pháp này đòi hỏi sự tuân thủ kỷ luật viết mã cực kỳ khắt khe và có phần nặng nề về mặt cú pháp, nhưng chúng đã trở thành cầu nối thiết yếu, bảo vệ sự toàn vẹn của các ứng dụng mạng lưới toàn cầu trong giai đoạn chuyển giao lịch sử, trước khi ngôn ngữ lập trình này chính thức đưa ra một giải pháp hệ thống triệt để và an toàn hơn thông qua các bản cập nhật đặc tả hiện đại.
Sự tiến hóa sang mô hình hệ thống khối
Đánh dấu một cột mốc tiến hóa mang tính bước ngoặt, kể từ khi phiên bản đặc tả thứ sáu được công bố rộng rãi, ngôn ngữ JavaScript đã chính thức hỗ trợ thêm định dạng kiến trúc hệ thống khối (module), tồn tại song song và vượt trội hơn hẳn so với định dạng chương trình mã nguồn độc lập truyền thống. Giống như người tiền nhiệm của mình, kiến trúc cốt lõi của các khối này cũng được xây dựng dựa trên nền tảng của các tệp tin vật lý. Tuy nhiên, sự khác biệt mang tính cách mạng nằm ở cơ chế phân phối và quản lý vòng đời: nếu một tệp tin được tải vào môi trường thực thi thông qua các cơ chế nạp khối chuyên biệt, điển hình như việc sử dụng từ khóa nhập liệu import trong mã nguồn hoặc khai báo rõ ràng thông qua thẻ kịch bản với thuộc tính type=module trên cấu trúc trang HyperText Markup Language, thì toàn bộ khối lượng mã lệnh bên trong tệp tin đó sẽ ngay lập tức được công cụ thông dịch đóng gói và quản lý dưới tư cách là một hệ thống khối thống nhất, khép kín và an toàn tuyệt đối. Cơ chế nạp liệu mới mẻ này không chỉ giải quyết triệt để bài toán ô nhiễm không gian tên mà còn cung cấp một phương thức tải tài nguyên bất đồng bộ tối ưu hơn rất nhiều.
Mặc dù trong tư duy kiến trúc thông thường, chúng ta hiếm khi hình dung một hệ thống khối – về bản chất là một tập hợp các trạng thái dữ liệu nội bộ được bảo vệ nghiêm ngặt cùng với các phương thức công khai được cố tình phơi bày để thao tác trên luồng dữ liệu đó – lại vận hành như một chương trình điện toán hoàn toàn độc lập, nhưng trên thực tế nền tảng hệ thống, bộ thông dịch ngôn ngữ vẫn giữ nguyên quan điểm đối xử một cách tách biệt đối với từng khối một. Tương tự như nguyên lý hoạt động của phạm vi toàn cục cho phép các tệp tin rời rạc truyền thống có thể giao thoa và trộn lẫn logic với nhau tại thời điểm chạy, cơ chế nhập liệu một khối này vào bên trong không gian của một khối khác chính là chiếc chìa khóa vạn năng cho phép thiết lập nên các kênh giao tiếp và tương tác vận hành liên tục giữa chúng. Quá trình tương tác này được kiểm soát một cách tinh vi ở cấp độ ngôn ngữ, chỉ cho phép luồng dữ liệu được luân chuyển qua các giao diện đã được định nghĩa và cho phép xuất ra một cách minh bạch, từ đó thiết lập nên một kiến trúc phần mềm tuân thủ nghiêm ngặt nguyên lý đặc quyền tối thiểu (principle of least privilege).
Tựu trung lại, cho dù dự án phần mềm của bạn đưa ra quyết định lựa chọn sử dụng bất kỳ mẫu kiến trúc tổ chức mã nguồn nào (từ tệp tin độc lập truyền thống cho đến hệ thống khối hiện đại), cùng với cơ chế tải tài nguyên tương ứng đi kèm, triết lý tư duy đúng đắn nhất và mang tính học thuật cao nhất vẫn là việc bạn nên đánh giá và nhìn nhận mỗi tệp tin vật lý như một chương trình điện toán thu nhỏ mang tính bản thể. Từng chương trình thu nhỏ bé này, thông qua các cơ chế giao tiếp an toàn, sau đó sẽ tiến hành hợp tác chặt chẽ, tạo thành một mạng lưới liên kết với vô số các chương trình thu nhỏ khác nhằm mục đích phối hợp thực thi các luồng logic nghiệp vụ phức tạp, từ đó hoàn thiện bức tranh tổng thể về các tính năng vận hành của toàn bộ hệ thống ứng dụng khổng lồ mà bạn đang kiến tạo. Việc thấm nhuần triết lý thiết kế hướng vi mô này chính là nền tảng tư duy vững chắc nhất để xây dựng nên những khối mã nguồn có độ gắn kết cao (high cohesion) bên trong nội bộ tệp tin, đồng thời duy trì độ ghép nối thấp (low coupling) giữa các tệp tin với nhau, đảm bảo sự phát triển bền vững cho hệ thống trong dài hạn.
Hệ thống giá trị và cơ chế quản lý biến số
Bên dưới bề mặt của mọi thuật toán và luồng thao tác logic, các giá trị dữ liệu chính là hạt nhân cấu thành nên trạng thái và bản sắc của toàn bộ chương trình điện toán. Ngôn ngữ này thiết lập một hệ thống phân loại dữ liệu vô cùng nghiêm ngặt, vạch ra ranh giới rõ ràng giữa các giá trị nguyên thủy tinh gọn và các cấu trúc đối tượng phức hợp, đòi hỏi người kỹ sư phải thấu hiểu sâu sắc bản chất cấp phát và tham chiếu bộ nhớ của từng chủng loại. Song hành với hệ thống giá trị là một cơ chế quản lý biến số đa tầng, nơi các từ khóa khai báo không chỉ đơn thuần là công cụ đặt tên định danh, mà chúng còn đóng vai trò như những người gác đền, thiết lập phạm vi quyền lực và vòng đời tồn tại của dữ liệu bên trong các khối không gian thuật toán. Hơn thế nữa, việc bộ đặc tả ngôn ngữ nâng tầm các hàm toán học lên một vị thế ngang hàng với các giá trị dữ liệu thông thường đã mở ra một kỷ nguyên kiến trúc mới, cho phép các khối hành vi được vận chuyển, gán ghép và tái sử dụng một cách linh hoạt vô song, làm nền tảng cho những mô hình thiết kế phần mềm ở đẳng cấp cao nhất.
Bản chất của các giá trị nguyên thủy và đối tượng
Trong bất kỳ một chương trình điện toán nào, đơn vị lưu trữ thông tin cơ bản và mang tính nền tảng nhất chính là khái niệm về các giá trị dữ liệu. Về mặt bản chất cốt lõi, giá trị chính là hiện thân của luồng dữ liệu thô, đóng vai trò sống còn trong việc cung cấp phương thức để một hệ thống phần mềm có thể lưu trữ, theo dõi và duy trì trạng thái (state) xuyên suốt vòng đời hoạt động vô cùng phức tạp của nó. Theo tài liệu đặc tả của ngôn ngữ JavaScript, các giá trị này được phân loại một cách nghiêm ngặt thành hai hình thái kiến trúc hoàn toàn trái ngược nhau: hệ thống giá trị nguyên thủy (primitive) vô cùng tinh gọn và hệ thống giá trị đối tượng (object) mang tính phức hợp cao độ. Để nhúng trực tiếp các giá trị này vào bên trong cấu trúc mã nguồn, các lập trình viên thường xuyên sử dụng khái niệm giá trị nguyên bản (literal) – một phương pháp thể hiện dữ liệu trực quan ngay trên văn bản lập trình. Đối với dữ liệu dạng chuỗi ký tự, ngôn ngữ cung cấp sự tự do trong việc sử dụng dấu ngoặc kép __ hoặc dấu ngoặc đơn ’ nhằm mục đích phân định (delimit), bao bọc và định nghĩa rõ ràng ranh giới của một chuỗi thông tin. Sự lựa chọn giữa hai loại dấu câu này mang đậm tính phong cách cá nhân; tuy nhiên, để duy trì sự trong sáng, khả năng đọc hiểu và tính thống nhất trong công tác bảo trì hệ thống quy mô lớn, nguyên tắc thiết kế tối thượng là bạn phải chọn ra một tiêu chuẩn duy nhất và áp dụng nó một cách đồng nhất, triệt để trên toàn bộ cơ sở mã nguồn.
Tuy nhiên, bức tranh về xử lý chuỗi ký tự trở nên phức tạp và mang tính kỹ thuật sâu sắc hơn rất nhiều khi ngôn ngữ giới thiệu thêm một phương pháp phân định thứ ba: sử dụng ký tự dấu phẩy ngược (back-tick) [`]. Trái ngược với việc lựa chọn dấu ngoặc đơn hay ngoặc kép thuần túy mang tính thẩm mỹ, việc quyết định sử dụng dấu phẩy ngược lại đi kèm với một hệ quả thay đổi hoàn toàn về mặt hành vi thực thi của bộ biên dịch. Khi một chuỗi ký tự được bao bọc bởi hệ thống dấu phẩy ngược này, nó cho phép khai mở tính năng siêu việt mang tên nội suy chuỗi (interpolation) – một cơ chế thông minh cho phép hệ thống tự động nhận diện các biểu thức biến số được đánh dấu bởi cấu trúc $ … và tự động giải quyết (resolve) biểu thức đó để lấy ra giá trị dữ liệu hiện hành, sau đó tích hợp mượt mà giá trị ấy vào bên trong chuỗi kết quả cuối cùng ngay tại thời điểm thực thi. Dù bạn hoàn toàn có thể sử dụng chuỗi dấu phẩy ngược để chứa các đoạn văn bản thuần túy không chứa bất kỳ biểu thức nội suy nào, nhưng hành động đó bị giới hàn lâm đánh giá là sự lãng phí tài nguyên và làm suy giảm hoàn toàn mục đích triết học thiết kế ban đầu của loại cú pháp thay thế này. Lời khuyên mang tính chuyên môn sâu sắc nhất là hãy ưu tiên sử dụng dấu ngoặc kép hoặc ngoặc đơn cho mọi chuỗi văn bản thông thường, và chỉ dự trữ quyền sử dụng ký tự dấu phẩy ngược một cách cẩn trọng đối với những chuỗi phức tạp có nhu cầu bắt buộc phải chèn và xử lý các biểu thức nội suy.
Thế giới của các giá trị nguyên thủy không chỉ dừng lại ở các chuỗi ký tự, mà nó còn bao trùm một tập hợp phong phú các kiểu dữ liệu khác như giá trị toán học đếm số (number) và Giá trị luận lý định hướng đúng sai (boolean). Các giá trị đếm số thường xuyên được tận dụng tối đa trong các thuật toán liên quan đến việc đếm số bước lặp (loop iterations) hoặc xác định chính xác vị trí truy xuất thông tin bên trong các cấu trúc mảng. Ngôn ngữ cũng trang bị thêm một kiểu dữ liệu nguyên thủy hoàn toàn mới mang tên số nguyên khổng lồ (bigint) nhằm đáp ứng nhu cầu tính toán và lưu trữ các con số có kích thước lớn vô hạn, vượt xa khỏi những giới hạn kỹ thuật chật hẹp của định dạng số học dấu phẩy động truyền thống. Ở một khía cạnh vô cùng đặc biệt, để biểu diễn trạng thái trống rỗng (emptiness) hay sự vắng mặt hoàn toàn của một giá trị, ngôn ngữ này cung cấp đồng thời hai giá trị nguyên thủy riêng biệt là null và undefined. Bất chấp một số khác biệt tinh tế mang tính chất lịch sử phát triển và triết lý sử dụng đương đại, sự chồng chéo chức năng của hai giá trị này thường xuyên gây ra sự bối rối không nhỏ cho cộng đồng kỹ sư. Đứng trước bối cảnh đó, phương pháp tiếp cận an toàn, mang tính phòng thủ cao và đạt chuẩn mực học thuật tốt nhất chính là việc thiết lập quy tắc chỉ sử dụng duy nhất giá trị undefined như một tiêu chuẩn đơn nhất để đại diện cho trạng thái dữ liệu trống, đồng thời từ bỏ hoàn toàn thói quen sử dụng giá trị null dù cho nó có vẻ hấp dẫn vì ưu điểm ngắn gọn hơn khi gõ phím. Cuối cùng, một kiểu dữ liệu nguyên thủy vô cùng bí ẩn cần phải được nhắc đến là Ký hiệu (symbol), hoạt động như một giá trị ẩn danh, hoàn toàn không thể bị đoán trước, và gần như được ứng dụng độc quyền để đóng vai trò làm các khóa định danh bảo mật cực cao trên các cấu trúc đối tượng.
Tầm quan trọng của việc định danh và phạm vi khối
Để có thể minh bạch hóa một cơ chế hoạt động vốn dĩ không thực sự rõ ràng đối với những người mới tiếp cận: bên trong hệ sinh thái của một chương trình JavaScript, các giá trị dữ liệu có thể tồn tại một cách trần trụi dưới dạng các giá trị nguyên bản (literal) hiện diện trực tiếp trong mã nguồn, hoặc phổ biến hơn, chúng sẽ được hệ thống bao bọc và bảo quản cẩn thận bên trong các vật thể chứa đựng được gọi là biến số (variable). Bạn hoàn toàn có thể tư duy trừu tượng và hình dung các biến số này đơn thuần chỉ là những chiếc hộp rỗng được dán nhãn định danh, chuyên dùng để cất giữ thông tin. Tuy nhiên, một quy tắc bất di bất dịch là mọi biến số đều bắt buộc phải trải qua quá trình khai báo (declare) – hay nói cách khác là quá trình khởi tạo sự tồn tại – trước khi chúng được phép đưa vào sử dụng trong các luồng tính toán. Hệ thống ngôn ngữ cung cấp một hệ sinh thái vô cùng đa dạng bao gồm nhiều hình thái cú pháp khác nhau phục vụ cho mục đích khai báo các định danh này, và điều cực kỳ quan trọng là mỗi một hình thái từ khóa lại kéo theo một hệ thống các quy tắc hành vi ngầm định, quản lý chặt chẽ cách thức và phạm vi mà dữ liệu đó được cấp quyền truy cập.
Phân tích ví dụ kinh điển nhất, từ khóa var mang trong mình sứ mệnh khai báo một biến số để sẵn sàng đưa vào sử dụng bên trong một phân vùng cụ thể của chương trình, đồng thời cung cấp khả năng tự chọn để tiến hành bước gán (assignment) một giá trị khởi tạo ngay tại thời điểm biến số đó chào đời. Một từ khóa mang chức năng tương tự nhưng sở hữu triết lý thiết kế hiện đại hơn là let. Điểm khác biệt mang tính cốt lõi và làm nên giá trị của let so với người đàn anh var nằm ở chỗ: let áp đặt một cơ chế kiểm soát quyền truy cập vô cùng hạn hẹp và nghiêm ngặt đối với sự tồn tại của biến số. Cơ chế phòng ngự tinh vi này trong giới hàn lâm được gọi tên là phạm vi khối (block scoping), tạo ra một sự tương phản hoàn toàn và sâu sắc khi đặt lên bàn cân so sánh với cấu trúc phạm vi hàm (function scoping) hay phạm vi thông thường vốn quá đỗi rộng rãi và lỏng lẻo của từ khóa var. Việc ứng dụng triệt để kiến trúc phạm vi khối là một nghệ thuật tuyệt vời giúp các kỹ sư thiết lập ranh giới, hạn chế tối đa mức độ lan truyền và sự phơi bày không cần thiết của các biến số bên trong một hệ thống chương trình đồ sộ, qua đó xây dựng nên một lá chắn vững chắc ngăn chặn triệt để thảm họa va chạm, chồng chéo tên gọi định danh một cách hoàn toàn vô ý.
Bất chấp những ưu điểm vượt trội của kiến trúc phạm vi khối, việc cộng đồng mạng liên tục truyền tai nhau những lời khuyên cực đoan, xúi giục việc tẩy chay và lảng tránh hoàn toàn từ khóa var để chuyển sang tôn sùng tuyệt đối let (hoặc const) là một sự lạm dụng tồi tệ. Những lời khuyên mang tính áp đặt này thường xuyên bắt nguồn từ một nỗi sợ hãi mơ hồ và sự thiếu hiểu biết cặn kẽ về cách thức mà hành vi phạm vi của var đã miệt mài cống hiến cho sự ổn định của hệ thống kể từ những ngày đầu tiên ngôn ngữ này được thai nghén. Dưới góc độ nghiên cứu chuyên sâu, những lời khuyên mang tính chất hạn chế như vậy thực sự phản tác dụng, cản trở sự phát triển tư duy của lập trình viên, vì nó ngầm định một giả định đầy tính coi thường rằng bạn không có đủ trí lực để học hỏi và phối hợp vận hành một tính năng đúng đắn cùng với vô số các tính năng kiến trúc khác. Thực tế chứng minh, từ khóa var vẫn giữ nguyên giá trị cốt lõi vô giá của nó trong việc thực thi một thông điệp giao tiếp rõ ràng giữa mã nguồn và người đọc: hãy chú ý, biến số này mang một tầm ảnh hưởng lớn và sẽ được truy xuất, nhìn thấy bởi một không gian phạm vi rộng lớn bao trùm toàn bộ hàm điện toán. Bên cạnh đó, từ khóa const lại đóng vai trò như một người gác đền nghiêm ngặt: nó yêu cầu biến số phải được nạp giá trị ngay tại giây phút nó được sinh ra, và vĩnh viễn tước đoạt mọi quyền lợi cho phép bất kỳ ai gán lại cho nó một giá trị mới trong suốt phần đời còn lại của ứng dụng. Tuy nhiên, một lầm tưởng chết người vô cùng phổ biến là việc đánh đồng const với sự bất biến (unchangeable); trên thực tế, nó chỉ ngăn cấm hành động gán lại (re-assignment), còn bản thân nội dung chứa bên trong các giá trị phức tạp như cấu trúc mảng hay đối tượng vẫn hoàn toàn có thể bị đột biến (mutation) thay đổi một cách tự do. Do đó, chiến lược ứng dụng ngôn ngữ đỉnh cao và an toàn nhất là chỉ nên giới hạn việc sử dụng const thuần túy đối với các giá trị nguyên thủy đơn giản, nhằm mục đích đặt cho chúng những cái tên mang ý nghĩa mô tả rõ ràng, giúp toàn bộ chương trình trở nên minh bạch và dễ dàng thẩm thấu hơn trong quá trình phân tích mã nguồn.
Chức năng và vai trò của các hàm toán học
Danh xưng hàm (function) mang trong mình một dải phổ vô cùng rộng lớn về mặt ngữ nghĩa, biến thiên đa dạng tùy thuộc vào từng phân nhánh cụ thể của khoa học máy tính. Nếu chúng ta mạo hiểm bước vào lãnh địa hàn lâm của trường phái lập trình chức năng (Functional Programming), khái niệm hàm sẽ bị thu hẹp và ràng buộc bởi một định nghĩa toán học thuần túy cực kỳ khắt khe, đi kèm với một bộ quy tắc ứng xử vô cùng nghiêm ngặt yêu cầu sự bất biến dữ liệu và không gây ra tác dụng phụ. Tuy nhiên, khi hệ quy chiếu được chuyển sang môi trường hệ sinh thái ngôn ngữ JavaScript, tư duy học thuật đúng đắn nhất đòi hỏi chúng ta phải mở rộng góc nhìn và tiếp nhận khái niệm hàm dưới một lăng kính rộng lượng hơn, tương đồng với một thuật ngữ kỹ thuật mang tính thực tiễn cao: thủ tục (procedure). Một thủ tục, về bản chất cốt lõi, là một tập hợp được cấu trúc hóa của vô số các câu lệnh thao tác, sở hữu khả năng được đánh thức và gọi chạy (invoked) lặp đi lặp lại không giới hạn số lần, có năng lực tiếp nhận các dòng dữ liệu đầu vào (inputs) và sau một quá trình xử lý logic, có thể quyết định cung cấp ngược trở lại một hoặc nhiều kết quả đầu ra (outputs) mang tính ứng dụng cao.
Ngay từ buổi bình minh của lịch sử thiết kế ngôn ngữ này, hình thái phổ biến nhất để khởi tạo nên một cấu trúc hàm được biết đến dưới cái tên là khai báo hàm (function declaration). Điểm đặc trưng nhất của hình thái này là việc nó đứng hiên ngang như một câu lệnh (statement) hoàn toàn độc lập, tách biệt hoàn toàn và không chịu khuất phục dưới tư cách là một biểu thức (expression) thứ cấp nằm ép mình bên trong bất kỳ một câu lệnh nào khác. Một đặc tính kiến trúc vô cùng kỳ diệu diễn ra đằng sau hậu trường là: sự liên kết ràng buộc (association) giữa cái tên định danh của hàm và toàn bộ khối lượng giá trị logic của hàm đó đã được hệ thống ngầm định hoàn tất từ rất sớm, ngay trong giai đoạn trình biên dịch đang quét qua mã nguồn tĩnh, tức là sự kết nối này đã được thắt chặt từ rất lâu trước khi đoạn mã đó có cơ hội thực sự được nạp vào bộ vi xử lý để chạy. Đứng ở một thái cực đối lập, chúng ta có cấu trúc biểu thức hàm (function expression); đây là một hình thái mà tại đó, bản thân toàn bộ nội dung của hàm chỉ được xem như một biểu thức tính toán thông thường, và biểu thức này sau đó sẽ được gán giá trị trực tiếp vào một biến số được định danh. Khác biệt hoàn toàn so với mô hình khai báo, biểu thức hàm bị tước đoạt quyền lợi được liên kết sớm với định danh của nó, và nó buộc phải kiên nhẫn chờ đợi cho đến khi dòng thời gian thực thi (runtime) thực sự chạm đến câu lệnh gán đó thì nó mới chính thức được hệ thống thừa nhận sự tồn tại.
Một chân lý kiến trúc mang tầm vóc tối quan trọng mà mọi kỹ sư bắt buộc phải khắc cốt ghi tâm: bên trong hệ sinh thái này, các cấu trúc hàm không phải là những thực thể chết, mà chúng được đối xử vô cùng trân trọng như những giá trị dữ liệu sống (values) thực thụ. Chúng hoàn toàn có thể được gán vào các biến số, hoặc truyền tay nhau bay qua bay lại giữa các module như những gói hàng thông tin. Xét theo cây phả hệ kiểu dữ liệu, các hàm thực chất lại chính là một phân loài mang tính chất đặc thù (special sub-type) thuộc về hệ tư tưởng của kiểu dữ liệu đối tượng (object value type). Chính đặc ân cho phép đối xử với các hàm như những giá trị dữ liệu độc lập này đã tạo ra một nền móng vững chắc, là điều kiện tiên quyết mang tính bắt buộc để một nền tảng ngôn ngữ có thể đủ sức mạnh chống đỡ và triển khai thành công các khuôn mẫu thiết kế siêu việt thuộc trường phái lập trình chức năng, một năng lực mà ngôn ngữ JavaScript tự hào sở hữu và liên tục hoàn thiện. Trong quá trình hoạt động, các hàm có năng lực tiếp nhận các dữ liệu truyền vào thông qua hệ thống tham số (parameter) – những biến số hoạt động bí mật và chỉ tồn tại le lói bên trong không gian giới hạn của nội bộ hàm đó. Khi hoàn thành sứ mệnh tính toán, hàm có thể gửi trả lại kết quả thông qua từ khóa return. Mặc dù giới hạn vật lý chỉ cho phép sử dụng từ khóa return để trả về một thực thể giá trị duy nhất tại một thời điểm, nhưng trí tuệ của lập trình viên hoàn toàn có thể vượt qua rào cản này bằng cách đóng gói khéo léo vô số các dữ liệu cần thiết vào chung một cấu trúc đối tượng hoặc một mảng dữ liệu duy nhất trước khi ném nó ra ngoài. Thêm vào đó, bởi tính chất là một giá trị, hàm hoàn toàn có thể được gắn kết như một thuộc tính trực tiếp nằm trên một đối tượng, mở ra một phong cách thiết kế định nghĩa hàm vô cùng trực quan và rành mạch.
So sánh giá trị và các mô hình tổ chức mã nguồn
Năng lực định hướng luồng thực thi của một hệ thống máy tính phụ thuộc hoàn toàn vào các quyết định rẽ nhánh logic, vốn được xây dựng vững chắc dựa trên nền tảng của các phép toán đối chiếu và so sánh giá trị dữ liệu. Tuy nhiên, cơ chế so sánh trong hệ sinh thái ngôn ngữ này ẩn chứa những triết lý thiết kế vô cùng dị biệt, vượt xa khỏi các quy chuẩn đối chiếu danh tính thông thường để bao hàm cả những thuật toán ép kiểu tự động đầy phức tạp và thường xuyên gây ra sự tranh luận nảy lửa trong giới hàn lâm. Vượt lên trên cấp độ của các phép toán cơ bản, bài toán tổ chức và quản trị mức độ phức tạp của các cơ sở mã nguồn quy mô lớn đòi hỏi sự can thiệp chiến lược của các mô hình kiến trúc vĩ mô. Sự hiện diện và cạnh tranh song phẳng giữa mô hình hướng đối tượng truyền thống và kiến trúc hệ thống khối tiên tiến đã cung cấp cho các chuyên gia phần mềm những công cụ tổ chức dữ liệu và hành vi cực kỳ sắc bén, qua đó kiến tạo nên những hệ sinh thái ứng dụng sở hữu tính đóng gói cao độ, khả năng tái sử dụng tối đa và sự bền vững đáng kinh ngạc trước mọi thử thách của thời gian.
Cơ chế so sánh tương đồng và ép kiểu dữ liệu
Tiến trình ra quyết định rẽ nhánh logic bên trong bất kỳ một chương trình máy tính nào cũng đòi hỏi phải thực hiện liên tục các hoạt động đối chiếu, so sánh các khối giá trị dữ liệu với nhau nhằm mục đích xác định rõ danh tính (identity) cũng như định vị mối tương quan hệ thống giữa chúng. Câu hỏi so sánh mang tính chất kinh điển và xuất hiện với tần suất dày đặc nhất luôn luôn là một phép thử: Liệu giá trị định danh X này có thực sự ‘giống hệt’ với giá trị định danh Y kia hay không? Thế nhưng, để bóc tách một cách học thuật xem khái niệm giống hệt đó thực sự mang ý nghĩa sâu xa gì dưới góc độ phân tích của công cụ thông dịch, chúng ta sẽ phải đối mặt với một vấn đề kiến trúc phức tạp hơn rất nhiều. Phụ thuộc vào những ảnh hưởng mang tính chất lịch sử phát triển cũng như những nỗ lực cải tiến trải nghiệm làm việc của nhà phát triển (ergonomics), ý nghĩa cốt lõi của phép so sánh này mang nhiều tầng lớp sắc thái, vượt xa khỏi những suy luận ngây thơ về một phép đối chiếu danh tính chính xác tuyệt đối. Có những ngữ cảnh thuật toán mà ở đó, lập trình viên thực sự khát khao một sự trùng khớp hoàn hảo không tì vết, nhưng cũng có vô số những kịch bản xử lý logic khác lại yêu cầu một phép so sánh mang tính bao dung, rộng lượng hơn, cho phép sự tương đồng ở mức độ sát sao hoặc chấp nhận sự trao đổi thay thế chéo (interchangeable matching). Chính vì những nhu cầu thực tiễn đa chiều này, bộ đặc tả ngôn ngữ buộc chúng ta phải có một sự nhạy bén kỹ thuật cực cao để có thể nhận thức và phân định rạch ròi những ranh giới sai biệt vô cùng tinh tế giữa khái niệm so sánh tương đương (equality) thuần túy và khái niệm so sánh đẳng trị (equivalence) linh hoạt.
Khi lướt qua bất kỳ tài liệu học thuật hay dạo quanh các diễn đàn tranh luận về JavaScript, chắc chắn bạn sẽ chạm mặt một khái niệm biểu tượng được gọi là toán tử ba dấu bằng (===), hay còn được giới chuyên môn vinh danh với mỹ từ là toán tử so sánh nghiêm ngặt (strict equality). Ở cái nhìn lướt qua, khái niệm này dường như toát lên một sự minh bạch, thẳng thắn và không có gì phức tạp phải bàn cãi. Chắc hẳn, cụm từ nghiêm ngặt phải mang ý nghĩa là một sự kiểm soát gắt gao, thu hẹp biên độ dung sai và đòi hỏi một sự trùng khớp chính xác một cách máy móc. Thế nhưng, sự thật phũ phàng lại chứng minh điều hoàn toàn ngược lại: sự chính xác tuyệt đối đó chỉ là một ảo giác. Đúng là phần lớn các tổ hợp giá trị dữ liệu khi bị ném vào đấu trường của phép so sánh === đều sẽ hành xử và trả về kết quả tuân thủ đúng theo những suy luận trực giác về sự trùng khớp hoàn hảo về mặt danh tính. Thế nhưng, toán tử === đã được thiết kế một cách có chủ ý để thực hiện hành vi nói dối (lie) một cách trơ tráo trong hai trường hợp kịch bản xử lý liên quan đến các giá trị cực kỳ đặc biệt: giá trị đại diện cho khái niệm không phải là số (NaN) và giá trị số -0 (một giá trị âm không tồn tại trong toán học truyền thống nhưng lại là một thực thể độc lập hữu dụng trong khoa học máy tính). Trong trường hợp đối mặt với NaN, toán tử này sẽ nhắm mắt báo cáo sai sự thật rằng sự hiện diện của một giá trị NaN này hoàn toàn không bằng với sự hiện diện của một giá trị NaN khác. Tương tự như vậy, đối với kịch bản của -0, nó lại tiếp tục thực hiện hành vi gian lận kết quả bằng cách tuyên bố chắc nịch rằng -0 bằng hoàn toàn với giá trị số 0 dương bình thường. Chính vì những khiếm khuyết trong sự thật này, các chuyên gia cảnh báo không bao giờ được phép sử dụng toán tử này để kiểm tra hai giá trị đặc biệt kể trên, mà phải viện đến các tiện ích toán học siêu việt và không bao giờ nói dối như Number.isNaN() hay Object.is().
Câu chuyện về cơ chế so sánh hệ thống lại càng trở nên điên rồ và phức tạp gấp bội phần khi chúng ta đặt bước chân khám phá vào lãnh địa của việc so sánh các giá trị thuộc nhóm cấu trúc đối tượng (không phải là giá trị nguyên thủy). Theo lẽ thường tình, trực giác con người sẽ cho rằng một thuật toán kiểm tra sự tương đương chắc chắn sẽ phải quét qua và đối chiếu toàn bộ bản chất cấu trúc hay đi sâu vào nội dung thành phần cấu thành nên giá trị đó. Trong lĩnh vực phân tích cấu trúc đối tượng, một phép đối chiếu có khả năng nhận thức sâu sắc nội dung như vậy được giới hàn lâm gọi bằng thuật ngữ so sánh cấu trúc tương đương (structural equality). Thế nhưng, bộ đặc tả cốt lõi của JavaScript kiên quyết không cấp phép cho toán tử === thực thi nhiệm vụ so sánh cấu trúc tương đương đối với các giá trị đối tượng. Trái lại, toán tử này bị giáng cấp xuống chỉ còn đóng vai trò thực hiện thuật toán so sánh danh tính tương đương (identity equality). Mọi thực thể giá trị đối tượng bên trong hệ thống bộ nhớ đều bị giam giữ và quản lý thông qua các con trỏ tham chiếu (reference); chúng bị gán ép, bị truyền đi như những bản sao tham chiếu, và quan trọng nhất đối với cuộc thảo luận hiện tại: chúng bị đem ra so sánh với nhau hoàn toàn dựa trên sự trùng khớp của địa chỉ danh tính tham chiếu vật lý. Hệ sinh thái này cố tình không cung cấp sẵn bất kỳ một công cụ, cơ chế nguyên bản nào để hỗ trợ lập trình viên thực hiện phép so sánh cấu trúc tương đương phức tạp, đẩy toàn bộ gánh nặng triển khai các thuật toán đệ quy kiểm tra cấu trúc khó nhằn này lên đôi vai của các kiến trúc sư phần mềm, bởi lẽ việc phải thiết kế một cơ chế nguyên bản có khả năng bao quát và xử lý triệt để toàn bộ vô số các góc khuất kỹ thuật (corner cases) điên rồ là một nhiệm vụ gần như bất khả thi. Ở một mặt trận khác, toán tử so sánh lỏng lẻo với hai dấu bằng == liên tục phải hứng chịu cơn mưa gạch đá và sự phẫn nộ từ cộng đồng mạng vì bị dán nhãn là sự thiết kế tồi tệ, đầy rẫy lỗi nguy hiểm. Mọi sự phẫn nộ này thực chất xuất phát từ một hiểu lầm tai hại: mọi người tin rằng nó so sánh một cách bừa bãi không thèm đếm xỉa gì đến kiểu dữ liệu. Sự thật minh oan cho nó là: nếu hai phía của phép so sánh có kiểu dữ liệu chênh lệch nhau, toán tử == sẽ thể hiện bản lĩnh của một thuật toán so sánh ép kiểu (coercive equality) thực thụ, tự động biến đổi định dạng của một hoặc cả hai giá trị về chung một hệ quy chiếu trước khi áp dụng quy tắc so sánh tương tự như ===. Thay vì trốn chạy khỏi cơ chế ép kiểu này do nỗi sợ hãi từ những trường hợp ngoại lệ hiếm gặp, các kỹ sư phần mềm đẳng cấp cao chọn cách ôm trọn, đào sâu nghiên cứu và làm chủ hoàn toàn quyền năng ép kiểu bí ẩn của nó.
Mô hình hướng đối tượng và cơ chế kế thừa lớp
Trong hệ sinh thái kiến trúc của ứng dụng, có hai mẫu hình (pattern) mang tính thống trị tuyệt đối, được vận dụng rộng rãi ở quy mô toàn cầu nhằm mục đích tổ chức, quản lý luồng dữ liệu thô và các khối hành vi tương tác logic: mô hình các Lớp đối tượng (classes) và mô hình Hệ thống khối (modules). Hai triết lý kiến trúc khổng lồ này hoàn toàn không tồn tại trong tư thế triệt tiêu hay loại trừ lẫn nhau; trên thực tế, vô số các hệ thống chương trình phần mềm thương mại hiện đại đã thể hiện sự tinh tế vượt bậc khi kết hợp và vắt kiệt sức mạnh của cả hai mô hình này trong cùng một không gian dự án. Các thuật ngữ mang tính kinh viện như hướng đối tượng, hướng lớp hay bản thân khái niệm lớp đều là những khái niệm mang theo mình một sức nặng lịch sử to lớn, chứa đựng hàng tá những chi tiết kỹ thuật phức tạp và những sắc thái học thuật tinh vi; chúng hoàn toàn không sở hữu một định nghĩa mang tính chất phổ quát duy nhất có thể làm hài lòng mọi trường phái tư tưởng. Tuy nhiên, để tạo ra một điểm tựa giao tiếp thống nhất, chúng ta sẽ áp dụng một khái niệm mang hơi hướng truyền thống và có độ phổ biến cao nhất, đặc biệt quen thuộc đối với những kỹ sư phần mềm đã từng có quá khứ rèn luyện và xây dựng nền tảng tư duy từ những hệ thống ngôn ngữ hướng đối tượng thuần túy, hạng nặng như C++ hay Java.
Theo lăng kính kỹ thuật này, một kiến trúc lớp tồn tại bên trong một chương trình phần mềm đóng vai trò như một bản thiết kế tóm lược (definition) cho một loại cấu trúc dữ liệu tùy chỉnh, mang tính chất đặc thù do lập trình viên tự sáng tạo ra, trong đó nó gom gọn một cách có tổ chức cả các dữ liệu thô mang tính trạng thái lẫn các thuật toán hành vi (behaviors) có khả năng tương tác, nhào nặn và biến đổi luồng dữ liệu nội bộ đó. Bản thân các lớp chỉ thực hiện sứ mệnh cung cấp một bộ khung quy tắc mô tả cách thức mà khối cấu trúc dữ liệu đó sẽ phản ứng và vận hành trong tương lai, chứ tự thân các bản thiết kế lớp này hoàn toàn không phải là những giá trị dữ liệu vật lý có thể cầm nắm hay trực tiếp thao tác (concrete values). Để có thể thực sự triệu hồi ra một thực thể giá trị vật lý tồn tại chiếm chỗ trong bộ nhớ để chương trình có thể bóc tách và sử dụng, bản thiết kế lớp đó bắt buộc phải trải qua một nghi thức khởi tạo phiên bản (instantiated) thông qua việc sử dụng một từ khóa phép thuật mang tên new, lặp đi lặp lại một hoặc hàng triệu lần tùy theo nhu cầu của thuật toán. Cơ chế kiến trúc lớp cung cấp một ranh giới bảo vệ tuyệt vời, cho phép đóng gói toàn bộ luồng dữ liệu trạng thái được tổ chức một cách logic, gắn liền mật thiết với các hành vi thuật toán xử lý chúng. Không thể phủ nhận rằng một kỹ sư thiên tài hoàn toàn có thể xây dựng nên chính xác hệ thống chương trình đó mà không thèm đụng đến bất kỳ một dòng khai báo lớp nào, nhưng một hành động liều lĩnh như vậy gần như chắc chắn sẽ đẻ ra một đống mã nguồn rối rắm, thiếu tính tổ chức trầm trọng, cản trở nặng nề quá trình đọc hiểu tư duy của đồng nghiệp, và biến hệ thống thành một quả bom nổ chậm đầy rẫy lỗi rò rỉ bộ nhớ, biến công tác bảo trì hệ thống thành một cơn ác mộng tồi tệ ở mức độ hạ cấp (subpar maintenance).
Một mảnh ghép kiến trúc mang tính di sản, bám rễ sâu sắc vào mô hình thiết kế hướng lớp truyền thống – mặc dù mức độ ứng dụng thực tế của nó trong môi trường JavaScript hiện đại có phần thuyên giảm hơn so với các thế hệ ngôn ngữ trước – chính là khái niệm về sự kế thừa (inheritance) kết hợp chặt chẽ với tính đa hình (polymorphism). Thông qua việc tận dụng mệnh đề extends, các kỹ sư có khả năng khai thác quyền lực để mở rộng biên độ định nghĩa của một lớp tổng quát, từ đó cấy ghép thêm những luồng hành vi mới mẻ, đặc thù hơn vào bên trong một lớp con. Lớp con bé nhỏ này, trong quá trình khởi tạo sự sống, sẽ phải viện đến một lời gọi hàm vô cùng đặc biệt là super(…) đặt ngay bên trong hàm tạo của nó; hành động này mang ý nghĩa chuyển giao quyền kiểm soát và ủy thác (delegates) toàn bộ khối lượng công việc thiết lập trạng thái ban đầu cho hàm tạo của lớp cha thực thi, trước khi tự tay nó tiến hành đắp thêm những tính năng tinh xảo và chuyên biệt hơn phù hợp với danh tính phân loại của nó. Một sự thật thú vị là cả lớp con thừa kế lẫn lớp cha nguyên thủy đều có thể thản nhiên định nghĩa và chia sẻ chung một tên gọi cho cùng một phương thức hành vi, và chúng hoàn toàn có khả năng chung sống hòa bình trong cùng một hệ thống. Hiện tượng kiến trúc cho phép sự chung sống của các phương thức trùng tên này chính là biểu hiện rực rỡ nhất của khái niệm tính đa hình. Cơ chế kế thừa thực sự là một vũ khí hạng nặng dùng để kiến thiết, tổ chức và phân luồng dữ liệu cùng hành vi vào những đơn vị logic cách ly hoàn hảo, đồng thời vẫn chừa ra một khe cửa hẹp để các lớp con có thể bắt tay hợp tác, tận dụng lại trí tuệ và tài sản dữ liệu của các lớp cha.
Các hình thái của hệ thống khối trong thực tiễn
Đồng hành cùng với triết lý của mô hình lớp đối tượng, mẫu thiết kế hệ thống khối (module pattern) cũng mang trong lòng một hoài bão kiến trúc tương tự: hướng tới việc quy tụ và kết dính chặt chẽ các luồng dữ liệu cùng với các thuật toán hành vi vào chung những thực thể logic có tính độc lập cao độ. Cùng chia sẻ một tầm nhìn tương đồng với các lớp đối tượng, các khối module cũng sở hữu khả năng bao hàm (include) hoặc cấp quyền truy cập vào các kho dữ liệu nội bộ cũng như các giao thức hành vi của các khối module láng giềng khác, nhằm mục đích tối ưu hóa năng lực hợp tác giải quyết vấn đề. Tuy nhiên, nếu chúng ta dùng kính lúp để soi xét, hệ thống khối bộc lộ những điểm khác biệt mang tính cốt lõi và cực kỳ quan trọng so với kiến trúc lớp. Sự rạn nứt dễ dàng nhận diện nhất nằm ở cấu trúc bề mặt: hình thái cú pháp dùng để kiến tạo nên chúng hoàn toàn khác biệt nhau một trời một vực. Trong kỷ nguyên vàng son của những ngày đầu tiên hình thành ngôn ngữ JavaScript, mô hình hệ thống khối đã sớm khẳng định được vị thế như một mẫu thiết kế vô cùng trọng yếu, xuất hiện với tần suất dày đặc, chi phối và cung cấp năng lượng cho vô vàn các hệ thống ứng dụng khổng lồ, ngay cả khi vào thời điểm đó, ngôn ngữ này chưa hề trang bị bất kỳ một cấu trúc cú pháp nguyên bản (native syntax) nào dành riêng cho nó.
Dấu ấn nhận diện kinh điển nhất của một khối module cổ điển (classic module) chính là sự hiện diện của một cấu trúc hàm bao bọc khổng lồ bên ngoài (được đảm bảo điều kiện sẽ được kích hoạt thực thi ít nhất một lần trong suốt vòng đời). Hàm bao bọc này mang trọng trách tối thượng là phải sinh ra và gửi trả lại một phiên bản (instance) vật lý của chính hệ thống khối đó, đồng thời chủ động phơi bày ra thế giới bên ngoài một hoặc nhiều hàm chức năng được cấp phép, có đủ thẩm quyền để chọc ngoáy và điều khiển các luồng dữ liệu bí mật (hidden data) đang được giấu kín bên trong nội tạng của khối module. Bởi vì một cấu trúc module mang hình thái kiến trúc kiểu này bản chất của nó chỉ là một hàm điện toán không hơn không kém, và hành động gọi thực thi cái hàm đó sẽ đẻ ra một phiên bản vật lý của hệ thống khối, giới học thuật đã ưu ái dành tặng cho các cấu trúc hàm đặc biệt này một danh xưng kiêu kỳ khác là nhà máy sản xuất module (module factories). So sánh đối chiếu những hình thái khối module này với các kiến trúc lớp, một sự thật hiện ra rõ ràng là chúng mang trong mình nhiều sự tương đồng về triết lý hơn là những điểm đối lập. Tuy nhiên, trong khi định dạng lớp bắt buộc phải cất giữ các phương thức và dữ liệu bám dính trên bề mặt của một đối tượng vật lý và phải cầu viện đến tiền tố this để truy cập, thì trong lãnh địa của hệ thống khối, các phương thức và dữ liệu lại được giải phóng, được truy xuất một cách trực tiếp dưới dạng các biến định danh tự do trôi nổi bên trong phạm vi, hoàn toàn không bị trói buộc bởi bất kỳ tiền tố this phiền toái nào. Trong lớp đối tượng, bộ giao diện tương tác (API) mang tính ẩn danh và toàn bộ dữ liệu bị phơi trần; ngược lại, với nhà máy module, bạn nắm quyền sinh sát trong việc tạo ra và xuất khẩu một đối tượng chứa toàn bộ các giao diện công khai, trong khi mọi bí mật nghiệp vụ khác vĩnh viễn bị chôn vùi trong bóng tối riêng tư của hàm bao bọc.
Bước sang kỷ nguyên hiện đại, Hệ thống khối theo chuẩn ECMAScript (được tích hợp chính thức vào bộ ngôn ngữ trong phiên bản đặc tả thứ sáu) ra đời với sứ mệnh lịch sử là kế thừa và phát huy trọn vẹn tinh thần triết học cốt lõi cũng như những hoài bão ứng dụng mà các khối module cổ điển đã dày công xây dựng, đồng thời nó còn tham vọng dung hòa mọi biến thể và đáp ứng mọi kịch bản sử dụng cực đoan nhất đến từ các trường phái Asynchronous Module Definition hay CommonJS. Bất chấp tham vọng đó, chiến lược triển khai kỹ thuật của mô hình mới này lại đánh dấu một sự rẽ nhánh vô cùng sâu sắc. Thứ nhất, sự tồn tại của cấu trúc hàm bao bọc dùng để định nghĩa ranh giới khối module đã chính thức bị xóa sổ. Môi trường bao bọc hiện tại được ấn định tuyệt đối chính là ranh giới vật lý của tệp tin; theo nguyên lý một tệp tin, một hệ thống khối duy nhất. Thứ hai, các kỹ sư không còn phải tương tác một cách thô bạo với hệ thống giao diện API của một khối một cách tường minh, thay vào đó, một từ khóa quyền lực mới là xuất (export) được giới thiệu, cho phép gắn kết một biến số hay một phương thức hành vi vào định nghĩa giao diện công khai một cách vô cùng thanh lịch. Bất kỳ một thực thể nào được khai sinh bên trong khối module nhưng không được đóng dấu xuất sẽ vĩnh viễn bị giam cầm trong bóng tối (tương tự như nguyên lý hoạt động của khối module cổ điển). Thứ ba, và cũng có lẽ là điểm dị biệt gây chấn động nhất khi so sánh với mọi khuôn mẫu kiến trúc đã từng tồn tại: bạn hoàn toàn bị tước đoạt khả năng tự ý khởi tạo phiên bản đối với một khối module chuẩn ECMAScript. Hành động duy nhất bạn được phép làm là nhập (import) nó vào để tận dụng duy nhất một phiên bản duy nhất đã được hệ thống dựng sẵn. Bản chất sâu xa của mô hình khối hiện đại này là hoạt động như một hệ thống độc bản (singletons); quá trình khởi tạo vòng đời chỉ diễn ra một lần duy nhất vào khoảnh khắc lệnh nhập đầu tiên được thực thi trong toàn bộ chương trình, và mọi lệnh nhập diễn ra sau đó ở bất kỳ đâu cũng chỉ nhận được một sợi dây liên kết tham chiếu trỏ ngược về đúng cái phiên bản duy nhất đã ra đời trước đó.
Kết luận
Xuyên suốt toàn bộ chương khảo sát vô cùng đồ sộ vừa qua, chúng ta đã lướt một cách đầy chiến lược trên một bề mặt không gian vô tận, bao trùm hầu hết mọi khía cạnh cốt lõi định hình nên sức mạnh và bản sắc cấu trúc của hệ sinh thái ngôn ngữ lập trình JavaScript. Hoàn toàn là một phản ứng sinh lý cực kỳ bình thường nếu tại giây phút này, não bộ của bạn đang trong trạng thái quá tải thông tin trước một trận bão kiến thức với mật độ dày đặc đến nghẹt thở như vậy. Dù chỉ mang danh nghĩa khiêm tốn là một cuộc khảo sát lướt qua, nhưng chúng ta đã mạnh dạn khai phá, phân tích chiều sâu và hé lộ một khối lượng khổng lồ những tiểu tiết kỹ thuật vi mô – những viên gạch nền móng mà mọi nhà kiến trúc phần mềm tài ba nhất định phải đặt lên bàn cân suy xét cẩn trọng và đảm bảo bản thân đã thấm nhuần một cách thấu đáo. Lời khuyên chân thành và mang tính học thuật cao nhất lúc này là bạn hãy đọc đi đọc lại chương tài liệu này vô số lần để thật sự tiêu hóa nó. Ở những chặng đường nghiên cứu tiếp theo, chúng ta sẽ bắt đầu quá trình đào sâu mũi khoan khai phá vào tận cùng những tầng sâu thăm thẳm của các bộ máy vận hành cốt lõi, khám phá những chân lý kiến trúc ẩn giấu bên dưới bề mặt của ngôn ngữ, nhưng trước khi tự đẩy mình vào hố đen học thuật đó, hãy chắc chắn rằng bạn đã dành ra một quỹ thời gian hoàn toàn tĩnh lặng và đủ dài để tiêu hóa toàn bộ nguồn tri thức uyên bác vừa được phơi bày trong khuôn khổ phần tài liệu này.