Văn hay trong hiện tại, chữ tốt ở tương lai

Bạn chưa biết rõ về JavaScript đâu (You don't know JS yet) | Chương 1.3

Khám phá bản chất JavaScript từ lịch sử tên gọi, chuẩn ECMAScript và tính đa mô thức. Xóa bỏ lầm tưởng, xây nền tảng vững chắc cho nhà phát triển website.

41 phút đọc.

0 lượt xem.

Mở đầu

Nếu bạn đã hoàn thành việc nghiên cứu các chương trước đó và dành thời gian thỏa đáng để nghiền ngẫm, thẩm thấu các kiến thức nền tảng, bạn hy vọng sẽ bắt đầu thấu hiểu ngôn ngữ JavaScript ở một mức độ sâu sắc hơn. Trong những phần tài liệu trước, chúng ta đã tiến hành một cuộc khảo sát ở tầm vĩ mô về các cấu trúc cú pháp, các mẫu thiết kế và hành vi cốt lõi của ngôn ngữ. Giờ đây, trọng tâm của sự chú ý sẽ được chuyển dịch hướng tới những đặc tính gốc rễ nằm ở tầng thấp hơn rất nhiều, những nền tảng vô hình nhưng lại đóng vai trò chống đỡ cho gần như mọi dòng mã lệnh mà chúng ta kiến tạo nên trong thực tiễn. Mục tiêu tối thượng của tài liệu này là hỗ trợ người học trân trọng và đánh giá đúng đắn bản chất cốt lõi về cách thức ngôn ngữ điện toán này vận hành, những động lực sâu xa nào khiến nó hoạt động theo cách thức đặc thù như vậy. Quá trình này sẽ bắt đầu giải đáp cho hàng loạt những câu hỏi tại sao vốn luôn thường trực nảy sinh trong tâm trí khi bạn dấn thân khám phá hệ sinh thái phức tạp này. Mặc dù vậy, khối lượng kiến thức được trình bày ở đây vẫn chưa phải là một bản phơi bày cạn kiệt mọi ngóc ngách của ngôn ngữ; nhiệm vụ đồ sộ đó thuộc về toàn bộ các tập sách tiếp theo trong bộ tài liệu khổng lồ này. Đừng vội vã lướt qua những thông tin này để rồi bị lạc lối trong một rừng chi tiết kỹ thuật phức tạp; hãy từ từ chậm rãi, cẩn trọng dành thời gian cho từng khái niệm. Dù có đọc kỹ đến đâu, bạn vẫn có khả năng kết thúc phần tài liệu này với vô vàn những câu hỏi chưa có lời giải đáp hoàn chỉnh, nhưng điều đó là hoàn toàn bình thường bởi vì vẫn còn cả một chặng đường học thuật dài phía trước.

Cơ chế vòng lặp và xử lý dữ liệu tuần tự

Việc thiết kế các thuật toán để duyệt qua các tập hợp dữ liệu khổng lồ đòi hỏi sự can thiệp của những mẫu kiến trúc chuẩn mực nhằm đảm bảo tính toàn vẹn của bộ nhớ và tối ưu hóa hiệu năng hệ thống. Trong hệ sinh thái ngôn ngữ JavaScript, cơ chế lặp không chỉ là một công cụ cú pháp đơn thuần mà nó đã được nâng tầm thành một giao thức tiêu chuẩn, chi phối cách thức mọi cấu trúc dữ liệu giao tiếp và luân chuyển thông tin. Việc thấu hiểu cơ chế này là nền tảng để xây dựng những ứng dụng xử lý dữ liệu quy mô lớn một cách an toàn và tinh gọn.

Nền tảng của mẫu thiết kế vòng lặp

Bởi vì các chương trình điện toán về bản chất được xây dựng nên với mục đích tối thượng là xử lý khối lượng dữ liệu khổng lồ và đưa ra các quyết định logic dựa trên nền tảng dữ liệu đó, các mẫu kiến trúc được sử dụng để duyệt qua các tập dữ liệu này có một tác động khổng lồ đến khả năng đọc hiểu mã nguồn của toàn bộ chương trình. Khuôn mẫu thiết kế Iterator pattern (Tạm dịch: Mẫu thiết kế bộ lặp.) đã tồn tại và chứng minh được giá trị không thể thay thế trong suốt nhiều thập kỷ qua trong ngành khoa học máy tính, và nó đề xuất một phương pháp tiếp cận mang tính tiêu chuẩn hóa cao độ đối với việc tiêu thụ luồng dữ liệu từ một nguồn cung cấp theo từng khối nhỏ một một cách tuần tự. Ý tưởng cốt lõi và mang tính triết học đằng sau khuôn mẫu này là việc lặp qua nguồn dữ liệu một cách tịnh tiến – tức là xử lý tập hợp dữ liệu bằng cách thao tác với phần đầu tiên, sau đó tiếp tục di chuyển đến phần tiếp theo, và cứ thế tiếp diễn – thường mang lại hiệu quả vượt trội và hữu ích hơn rất nhiều so với việc cố gắng nuốt trọn và xử lý toàn bộ tập hợp dữ liệu khổng lồ đó trong cùng một thời điểm duy nhất. Việc ép buộc hệ thống phải xử lý toàn bộ luồng dữ liệu cùng lúc có thể dẫn đến hiện tượng cạn kiệt bộ nhớ cục bộ, gây ra sự sụp đổ dây chuyền cho toàn bộ kiến trúc ứng dụng đang vận hành.

Để minh họa một cách trực quan, hãy tưởng tượng về một cấu trúc dữ liệu đại diện cho một truy vấn chọn lọc từ một hệ quản trị cơ sở dữ liệu quan hệ, nơi mà thông thường kết quả trả về sẽ được tổ chức một cách ngăn nắp dưới dạng các hàng dữ liệu. Nếu truy vấn này chỉ trả về vỏn vẹn một hoặc một vài hàng dữ liệu ít ỏi, bạn hoàn toàn có năng lực xử lý toàn bộ tập hợp kết quả đó ngay lập tức, và thản nhiên gán từng hàng dữ liệu vào một biến số cục bộ riêng biệt, sau đó tiến hành thực thi bất kỳ thao tác thuật toán nào trên dữ liệu đó mà bạn cảm thấy phù hợp với nghiệp vụ. Thế nhưng, câu chuyện sẽ rẽ sang một hướng hoàn toàn khác biệt và cực kỳ khắc nghiệt nếu truy vấn đó trả về hàng trăm, hoặc hàng ngàn, hay thậm chí là hàng triệu hàng dữ liệu khổng lồ; trong tình huống sinh tử đó, bạn bắt buộc phải viện đến các cơ chế xử lý tịnh tiến tuần tự để có thể đối phó một cách an toàn với khối lượng dữ liệu này, mà đại diện tiêu biểu nhất chính là việc sử dụng một vòng lặp. Việc áp dụng cơ chế xử lý tuần tự này giúp giải phóng áp lực lên bộ nhớ truy cập ngẫu nhiên, cho phép ứng dụng duy trì một hiệu năng ổn định và mượt mà bất chấp quy mô của tệp dữ liệu đầu vào có phình to đến mức độ nào đi chăng nữa.

Mẫu thiết kế bộ lặp định nghĩa một cấu trúc dữ liệu đặc biệt được gọi là Iterator (Tạm dịch: Bộ lặp.), cấu trúc này nắm giữ một sợi dây tham chiếu trỏ thẳng đến nguồn dữ liệu nền tảng nằm ở tầng dưới (chẳng hạn như các hàng kết quả từ truy vấn cơ sở dữ liệu), và nó phơi bày ra một phương thức giao tiếp tiêu chuẩn thường được biết đến với tên gọi là next(). Hành động triệu hồi phương thức next() này sẽ ngay lập tức trả về mảnh dữ liệu tiếp theo nằm trong chuỗi tập hợp (ví dụ như một bản ghi hoặc một hàng từ truy vấn cơ sở dữ liệu). Bởi vì trong thực tiễn lập trình, bạn không phải lúc nào cũng nắm rõ được chính xác có bao nhiêu mảnh dữ liệu đang chờ đợi để được lặp qua, mẫu thiết kế này thường đưa ra một tín hiệu thông báo về sự hoàn tất của quá trình duyệt dữ liệu thông qua một giá trị đặc biệt hoặc ném ra một ngoại lệ khi bạn đã lặp qua toàn bộ tập hợp và bước vượt qua ranh giới cuối cùng của tập dữ liệu đó. Tầm quan trọng mang tính lịch sử của mẫu thiết kế bộ lặp nằm ở việc nó buộc các kỹ sư phải tuân thủ một phương thức tiêu chuẩn duy nhất trong quá trình xử lý dữ liệu tuần tự, từ đó kiến tạo nên những cơ sở mã nguồn trong sáng hơn, dễ dàng thẩm thấu hơn rất nhiều, trái ngược hoàn toàn với viễn cảnh hỗn loạn nơi mà mỗi một cấu trúc dữ liệu hay nguồn cung cấp lại tự ý định nghĩa ra một phương pháp tiếp cận tùy chỉnh, độc quyền để thao tác với chính dữ liệu của nó.

Tiêu thụ bộ lặp thông qua cú pháp hiện đại

Sau nhiều năm chứng kiến cộng đồng phát triển ngôn ngữ JavaScript nỗ lực đưa ra vô số các giải pháp kỹ thuật khác nhau xoay quanh các kỹ thuật lặp được thống nhất lẫn nhau, phiên bản đặc tả thứ sáu của ngôn ngữ đã chính thức đưa ra một quyết định mang tính bước ngoặt: tiêu chuẩn hóa một giao thức cụ thể dành riêng cho mẫu thiết kế bộ lặp và nhúng nó trực tiếp vào lõi của ngôn ngữ. Giao thức nghiêm ngặt này định nghĩa một phương thức next() mà giá trị trả về của nó bắt buộc phải là một đối tượng được định danh là kết quả của bộ lặp. Đối tượng kết quả này mang trong mình hai thuộc tính cấu thành nền tảng là valuedone, trong đó thuộc tính done hoạt động như một cờ báo hiệu dưới dạng giá trị luận lý, nó sẽ luôn duy trì ở trạng thái sai cho đến khi tiến trình lặp qua toàn bộ nguồn dữ liệu ở tầng dưới được báo cáo là đã hoàn tất một cách trọn vẹn. Giao thức tiêu chuẩn này đã phá vỡ mọi rào cản giao tiếp giữa các cấu trúc dữ liệu khác biệt, tạo ra một ngôn ngữ chung duy nhất giúp các thuật toán có thể tương tác với bất kỳ dạng dữ liệu nào mà không cần quan tâm đến cấu trúc lưu trữ vật lý của chúng ở tầng thấp.

Với sự hiện diện vững chắc của giao thức lặp tiêu chuẩn mới này, về mặt lý thuyết, người lập trình hoàn toàn có khả năng tiêu thụ một nguồn dữ liệu theo từng giá trị riêng lẻ một cách thủ công, bằng cách liên tục kiểm tra trạng thái của thuộc tính done xem đã chuyển sang trạng thái đúng hay chưa sau mỗi lần gọi phương thức next() để quyết định việc dừng vòng lặp. Tuy nhiên, phương pháp tiếp cận thủ công này mang lại cảm giác vô cùng nặng nề, rườm rà và dễ dẫn đến những sai sót logic không đáng có; chính vì lý do đó, bộ đặc tả ngôn ngữ cũng đã khôn ngoan tích hợp thêm một loạt các cơ chế tự động hóa (bao gồm cả cú pháp lẫn giao diện lập trình ứng dụng) nhằm mục đích phục vụ cho việc tiêu thụ các bộ lặp này một cách tiêu chuẩn và thanh lịch nhất. Một trong những cơ chế tự động hóa nổi bật và thường xuyên được ứng dụng nhất chính là vòng lặp for..of. Vòng lặp này ẩn giấu toàn bộ quá trình quản lý trạng thái phức tạp đằng sau hậu trường, tự động trích xuất giá trị từ thuộc tính value và tiến hành gán nó vào biến số cục bộ, đồng thời tự động giám sát thuộc tính done để bảo vệ chương trình khỏi các thảm họa vòng lặp vô hạn.

Bên cạnh vòng lặp, một cơ chế cực kỳ mạnh mẽ khác thường xuyên được trưng dụng để tiêu thụ các bộ lặp chính là toán tử ba chấm …. Toán tử đa năng này trên thực tế sở hữu hai hình thái vận hành đối xứng nhau hoàn hảo: hình thái phân tán (spread) và hình thái thu thập (rest). Hình thái phân tán đóng vai trò trực tiếp như một cỗ máy tiêu thụ bộ lặp thực thụ. Để có thể phân tán một bộ lặp, điều kiện tiên quyết là bạn bắt buộc phải cung cấp một môi trường chứa đựng nào đó để các giá trị được phân tán bay vào. Trong hệ sinh thái ngôn ngữ JavaScript, tồn tại hai bối cảnh tiếp nhận khả thi: một cấu trúc mảng hoặc một danh sách các đối số phục vụ cho một lời gọi hàm. Trong cả hai kịch bản ứng dụng này, hình thái phân tán của toán tử ba chấm đều tuân thủ một cách tuyệt đối giao thức tiêu thụ bộ lặp (hoạt động với cơ chế giống hệt như vòng lặp for..of đã đề cập) nhằm mục đích trích xuất toàn bộ mọi giá trị đang có sẵn từ một bộ lặp, sau đó sắp xếp và đặt chúng vào đúng vị trí bên trong bối cảnh tiếp nhận tương ứng. Việc tận dụng toán tử phân tán này đã thay thế hoàn toàn những phương thức nối mảng hay truyền tham số nặng nề của quá khứ, mang lại một phong cách viết mã vô cùng súc tích và đậm chất hàm.

Bản chất của các cấu trúc dữ liệu có thể lặp

Giao thức tiêu thụ bộ lặp, xét trên bình diện kỹ thuật chuyên sâu, được định nghĩa cụ thể để phục vụ cho nhiệm vụ tiêu thụ các Iterable (Tạm dịch: Đối tượng có thể lặp.); một đối tượng có thể lặp được định nghĩa đơn giản là một giá trị sở hữu khả năng cho phép hệ thống lặp qua chính bản thân nó. Giao thức này sở hữu một cơ chế tự động vô cùng thông minh: nó sẽ tự động khởi tạo ra một phiên bản của bộ lặp từ đối tượng có thể lặp ban đầu, và sau đó nó sẽ tập trung toàn bộ tài nguyên để tiêu thụ duy nhất cái phiên bản bộ lặp vừa được tạo ra đó cho đến khi tiến trình đi đến hồi kết. Quyết định thiết kế kiến trúc này mang một ý nghĩa cực kỳ to lớn: nó ngụ ý rằng một đối tượng có thể lặp duy nhất hoàn toàn có thể được tiêu thụ lặp đi lặp lại nhiều lần trong suốt vòng đời của chương trình; và trong mỗi lần tiêu thụ như vậy, một phiên bản bộ lặp hoàn toàn mới, mang trạng thái hoàn toàn độc lập, sẽ được hệ thống sinh ra và đưa vào sử dụng. Sự tách biệt rõ ràng giữa cấu trúc lưu trữ dữ liệu tĩnh và con trỏ trạng thái lặp động này đảm bảo tính toàn vẹn của dữ liệu trong các kịch bản thuật toán phức tạp, nơi nhiều vòng lặp có thể cùng lúc truy xuất vào chung một nguồn tài nguyên mà không hề dẫm đạp lên trạng thái của nhau.

Vậy câu hỏi đặt ra là chúng ta có thể tìm thấy các đối tượng có thể lặp này ở đâu trong hệ thống? Phiên bản đặc tả thứ sáu đã thiết lập một nền tảng vững chắc bằng cách định nghĩa các loại cấu trúc dữ liệu hoặc tập hợp cơ bản nhất trong ngôn ngữ JavaScript đều mặc định là các đối tượng có thể lặp. Danh sách này bao trùm các cấu trúc quen thuộc như chuỗi ký tự, mảng, bản đồ, tập hợp, và hàng loạt các cấu trúc dữ liệu phức tạp khác. Lấy ví dụ điển hình, bởi vì mảng là các đối tượng có thể lặp, chúng ta hoàn toàn có thể thực hiện thao tác sao chép nông đối với một mảng bằng cách ứng dụng cơ chế tiêu thụ bộ lặp thông qua toán tử phân tán ba chấm; tương tự như vậy, chúng ta cũng có thể lặp qua từng ký tự riêng lẻ bên trong một chuỗi văn bản một cách dễ dàng. Cấu trúc dữ liệu bản đồ lại mang đến một cách tiếp cận lặp khác biệt so với mặc định, ở chỗ tiến trình lặp không chỉ quét qua các giá trị đơn thuần của bản đồ mà thay vào đó là duyệt qua các mục nhập của nó. Một mục nhập trong ngữ cảnh này được định nghĩa là một bộ dữ liệu (tương đương với một mảng gồm hai phần tử) chứa đựng đồng thời cả khóa định danh và giá trị dữ liệu tương ứng của khóa đó. Trong vòng lặp mặc định duyệt qua bản đồ, các kỹ sư thường xuyên ứng dụng cú pháp phân rã cấu trúc mảng để bóc tách từng bộ dữ liệu được tiêu thụ thành các cặp khóa và giá trị riêng biệt, mang lại sự tiện lợi tối đa trong thao tác mã nguồn.

Phần lớn các đối tượng có thể lặp được tích hợp sẵn trong ngôn ngữ đều cung cấp một cơ chế lặp mặc định, thường là cơ chế phù hợp nhất với suy luận trực giác của con người. Tuy nhiên, hệ thống vẫn trao cho bạn quyền lực tuyệt đối để lựa chọn một cơ chế lặp chuyên biệt hơn nếu hoàn cảnh nghiệp vụ đòi hỏi. Điển hình như, đối với hầu hết tất cả các đối tượng có thể lặp tích hợp sẵn, ngôn ngữ đều cung cấp sẵn ba hình thái bộ lặp khác nhau để bạn tùy ý khai thác: hình thái chỉ lặp qua khóa thông qua phương thức keys(), hình thái chỉ lặp qua giá trị thông qua phương thức values(), và hình thái lặp qua toàn bộ mục nhập thông qua phương thức entries(). Vượt ra khỏi ranh giới của những đối tượng có sẵn, bạn hoàn toàn có năng lực kiến tạo nên các cấu trúc dữ liệu của riêng mình và đảm bảo rằng chúng tuân thủ tuyệt đối giao thức lặp; việc thực hiện điều này đồng nghĩa với việc bạn đã tự nguyện tham gia vào hệ sinh thái, mở khóa khả năng cho phép người khác tiêu thụ dữ liệu của bạn bằng vòng lặp for..of và toán tử phân tán một cách mượt mà. Việc tiêu chuẩn hóa toàn bộ hệ thống trên giao thức cốt lõi này mang ý nghĩa to lớn, giúp cho khối lượng mã nguồn tổng thể trở nên dễ dàng nhận diện và nâng cao khả năng đọc hiểu đến mức tối đa. Có một sự thay đổi sắc thái tinh tế đáng chú ý: giao thức tiêu thụ lặp yêu cầu đầu vào là một đối tượng có thể lặp, nhưng lý do tại sao chúng ta vẫn có thể cung cấp trực tiếp một bộ lặp cho nó là bởi vì về mặt bản chất toán học, một bộ lặp cũng chính là một đối tượng có thể lặp của riêng nó, và khi được yêu cầu khởi tạo một phiên bản bộ lặp mới, nó sẽ đơn giản trả về chính bản thân nó.

Khái niệm bao đóng và ngữ cảnh thực thi động

Bên cạnh việc làm chủ các luồng dữ liệu thông qua hệ thống vòng lặp, việc thấu hiểu cách thức ngôn ngữ quản lý không gian bộ nhớ và định hướng ngữ cảnh thực thi của các khối hành vi là thử thách khốc liệt nhất đối với mọi kỹ sư. Cơ chế bao đóng và tính chất động của từ khóa nhận diện tạo nên hai trụ cột triết học cốt lõi, quyết định sự thành bại của những hệ thống ứng dụng bất đồng bộ mang tính phức tạp cao độ trong môi trường sản xuất thực tiễn.

Bản chất của bao đóng và khả năng ghi nhớ không gian

Có lẽ hoàn toàn không hề nhận thức được điều đó, nhưng gần như mọi nhà phát triển ngôn ngữ JavaScript đều đã từng ít nhất một lần tận dụng sức mạnh của cơ chế Closure (Tạm dịch: Bao đóng.). Trên thực tế, bao đóng được đánh giá là một trong những chức năng lập trình mang tính thâm nhập sâu rộng và phổ biến nhất trên hầu hết các ngôn ngữ lập trình hiện đại. Tầm quan trọng của việc thấu hiểu nó thậm chí có thể được xếp ngang hàng với việc nắm vững các khái niệm nền tảng như biến số hay vòng lặp; sự so sánh đó đã minh chứng rõ nét mức độ căn bản và thiết yếu của bao đóng trong khoa học máy tính. Dẫu vậy, nó vẫn thường xuyên mang lại một cảm giác mơ hồ, bị giấu kín đằng sau hậu trường và nhuốm màu sắc của phép thuật bí ẩn. Tồi tệ hơn, cơ chế này thường xuyên bị đem ra mổ xẻ và bàn luận thông qua những thuật ngữ học thuật quá đỗi trừu tượng, hoặc ngược lại là bằng những ngôn từ quá đỗi suồng sã, thiếu tính quy chuẩn, và những cách tiếp cận đó mang lại rất ít giá trị thực tiễn trong việc giúp chúng ta xác định một cách chính xác bản chất cốt lõi của hiện tượng kiến trúc này là gì.

Chúng ta bắt buộc phải sở hữu năng lực nhận diện một cách sắc bén vị trí mà cơ chế bao đóng đang được ứng dụng bên trong các chương trình phần mềm, bởi vì sự hiện diện, hoặc sự vắng mặt đáng tiếc của cơ chế này đôi khi lại chính là nguyên nhân gốc rễ gây ra những lỗi hỏng hóc nghiêm trọng, hoặc thậm chí là thủ phạm đứng sau các vấn đề làm suy giảm hiệu năng hệ thống. Vì vậy, việc thiết lập một định nghĩa mang tính thực dụng và cụ thể nhất về bao đóng là một nhiệm vụ cấp bách: Bao đóng chính là hiện tượng xảy ra khi một hàm điện toán ghi nhớ và tiếp tục duy trì quyền truy cập trực tiếp vào các biến số nằm ở không gian bên ngoài phạm vi cục bộ của nó, bất chấp việc bản thân hàm đó đang được kích hoạt và thực thi bên trong một không gian phạm vi hoàn toàn khác biệt so với nơi nó sinh ra. Chúng ta có thể dễ dàng chắt lọc ra hai đặc điểm định nghĩa mang tính sống còn từ khái niệm này. Thứ nhất, bao đóng là một bản chất kiến trúc đặc hữu gắn liền với hàm; các thực thể đối tượng tuyệt đối không được cấp phát cơ chế bao đóng, đặc ân này chỉ dành riêng cho các hàm. Thứ hai, để có thể quan sát và chứng minh sự tồn tại của một bao đóng bằng mắt thường, bạn bắt buộc phải tiến hành thực thi một hàm tại một không gian phạm vi khác biệt hoàn toàn so với không gian nơi mà hàm đó được định nghĩa lần đầu tiên.

Khi một hàm cha bao bọc bên ngoài hoàn tất toàn bộ quá trình chạy của nó, theo lẽ thường tình trong lý thuyết quản lý bộ nhớ, chúng ta sẽ kỳ vọng rằng toàn bộ các biến số cục bộ của nó sẽ ngay lập tức bị bộ thu gom rác dọn dẹp sạch sẽ và loại bỏ vĩnh viễn khỏi không gian lưu trữ. Chúng ta đinh ninh rằng các biến số đó sẽ biến mất không tì vết, nhưng thực tế kiến trúc lại chứng minh điều ngược lại, chúng không hề bốc hơi. Cứu cánh đứng sau hiện tượng bảo tồn bộ nhớ kỳ diệu này không gì khác chính là cơ chế bao đóng. Bởi vì các phiên bản của hàm con nội bộ vẫn còn đang duy trì sự sống (thông qua việc được tham chiếu và gán vào các biến số nằm ở phạm vi bên ngoài), bao đóng của những hàm con này vẫn kiên cường bảo tồn sự sống cho các biến số của hàm cha. Một chân lý kỹ thuật vô cùng quan trọng: những bao đóng này tuyệt đối không phải là một bức ảnh chụp nhanh dùng để lưu trữ giá trị của biến số tại một thời điểm đóng băng; thay vào đó, chúng đóng vai trò như một sợi dây liên kết vật lý trực tiếp và là một cơ chế bảo tồn sự sống cho chính bản thân biến số đó. Bản chất này mang một ý nghĩa hệ trọng: cơ chế bao đóng thực sự có năng lực quan sát, hoặc thậm chí trực tiếp tạo ra những sự cập nhật, thay đổi đối với các biến số này theo dòng chảy của thời gian, và mọi sự cập nhật đó đều được lưu giữ một cách hoàn hảo.

Sự tồn tại của biến số và tác động đến bộ nhớ

Cơ chế bao đóng thường xuyên phô diễn sức mạnh tột độ và bộc lộ mức độ phổ biến khổng lồ của nó khi các kỹ sư phần mềm phải thiết kế và làm việc với các khối mã lệnh mang tính chất bất đồng bộ, mà đại diện kinh điển nhất chính là việc sử dụng các hàm gọi lại. Khi một hàm khởi tạo yêu cầu truy xuất dữ liệu được kích hoạt, hàm gọi lại nội bộ nằm bên trong nó sẽ ngay lập tức tạo ra một vòng kìm kẹp bao đóng xung quanh các biến số chứa dữ liệu cấu hình, ví dụ như một biến số lưu trữ đường dẫn mạng lưới, và qua đó, nó bảo tồn và ghi nhớ một cách hoàn hảo giá trị đường dẫn này. Ngay cả khi bản thân cái hàm khởi tạo yêu cầu đó đã hoàn tất trọn vẹn sứ mệnh của mình và kết thúc ngay lập tức, cái biến tham số chứa đường dẫn mạng lưới kia vẫn được duy trì ngọn lửa sự sống một cách bền bỉ bên trong không gian an toàn của bao đóng, kiên nhẫn chờ đợi cho đến tận thời điểm phản hồi từ máy chủ quay trở về và hàm gọi lại chính thức được hệ thống triệu hồi để thực thi. Khả năng vượt qua các rào cản về mặt không gian và thời gian này chính là điều kiện tiên quyết giúp cho kiến trúc lập trình bất đồng bộ không bị vỡ vụn trước những độ trễ không thể dự đoán trước của mạng lưới toàn cầu.

Một ranh giới kỹ thuật cần phải được vạch rõ: hoàn toàn không có bất kỳ một quy định bắt buộc nào ép buộc không gian phạm vi bên ngoài bắt buộc phải là một hàm điện toán – mặc dù trong phần lớn các kịch bản thực tiễn thì nó thường là như vậy – điều kiện duy nhất cần và đủ là phải có sự hiện diện của ít nhất một biến số nằm ở không gian phạm vi bên ngoài bị một hàm nội bộ bên trong vươn tay ra truy cập tới. Xét trong một vòng lặp sử dụng từ khóa khai báo thuộc phạm vi khối, mỗi một chu kỳ lặp độc lập sẽ tự động khai sinh ra những biến số cục bộ hoàn toàn mới mẻ, và đồng thời vòng lặp đó cũng tạo lập ra một hàm xử lý sự kiện nội bộ mới cho từng chu kỳ. Mỗi một hàm nội bộ sinh ra trong chu kỳ đó sẽ tạo ra một vòng bao đóng siết chặt lấy biến số chỉ mục của chu kỳ, bảo vệ sự tồn tại của nó cho đến chừng nào bộ xử lý sự kiện vẫn còn được gắn kết với đối tượng giao diện. Nhờ có cơ chế này, khi một đối tượng giao diện được tương tác, bộ xử lý của nó có khả năng in ra chính xác giá trị chỉ mục tương ứng, bởi vì bộ xử lý vẫn vẹn nguyên ký ức về biến số chỉ mục của riêng nó thông qua bao đóng. Hãy luôn khắc sâu chân lý này: bao đóng kiến tạo vòng bảo vệ xoay quanh chính bản thân biến số vật lý, chứ không phải chỉ là bao bọc lấy một giá trị tĩnh như các con số vô hồn.

Bao đóng nghiễm nhiên được vinh danh là một trong những mẫu kiến trúc lập trình mang tầm vóc thống trị và đóng vai trò quan trọng bậc nhất trong bất kỳ một hệ sinh thái ngôn ngữ nào trên thế giới. Thế nhưng, nhận định đó lại càng trở nên đặc biệt đúng đắn và mang ý nghĩa sống còn đối với bản sắc của ngôn ngữ JavaScript; thật sự là vô cùng khó khăn để trí tưởng tượng của chúng ta có thể phác thảo ra một viễn cảnh thực hiện được bất kỳ một tác vụ phần mềm nào mang lại giá trị hữu ích mà không phải khai thác và đòn bẩy sức mạnh của cơ chế bao đóng theo một phương thức này hay phương thức khác. Việc hiểu lầm cơ chế này không chỉ đơn thuần là một lỗ hổng về mặt lý thuyết, mà nó còn là mầm mống trực tiếp dẫn đến những rò rỉ bộ nhớ nghiêm trọng khi các biến số bị giam cầm vĩnh viễn trong các bao đóng không bao giờ được giải phóng. Nếu tâm trí bạn vẫn còn cảm thấy chông chênh, chưa rõ ràng hoặc thiếu sự tự tin khi đối mặt với khái niệm bao đóng, hãy yên tâm vì phần lớn nội dung của tập sách tiếp theo sẽ được dồn toàn lực để tập trung phân tích và giải phẫu chuyên sâu về chủ đề kiến trúc hệ trọng này.

Ngữ cảnh thực thi động thông qua từ khóa nhận diện

Trong số vô vàn các cơ chế kiến trúc mang lại sức mạnh tột độ cho ngôn ngữ JavaScript, tồn tại một công cụ đồng thời cũng phải gánh chịu nhiều sự hiểu lầm tồi tệ và sâu sắc nhất từ cộng đồng phát triển: từ khóa this. Một trong những lầm tưởng mang tính trực giác vô cùng phổ biến là việc các kỹ sư vội vã đinh ninh rằng từ khóa này nằm bên trong một hàm sẽ tham chiếu ngược lại và trỏ thẳng vào chính bản thân cái hàm đó. Do bị chi phối nặng nề bởi thói quen và cách thức từ khóa này vận hành trong các hệ sinh thái ngôn ngữ hướng đối tượng truyền thống khác, một lầm tưởng trầm trọng thứ hai lại tiếp tục suy diễn rằng từ khóa này đóng vai trò như một mũi tên trỏ thẳng đến phiên bản đối tượng vật lý mà cái phương thức đó đang trực thuộc. Cần phải khẳng định mạnh mẽ bằng các luận điểm hàn lâm rằng cả hai luồng suy luận ngây thơ đó đều hoàn toàn sai lệch so với bản chất thực sự của kiến trúc. Để làm chủ hoàn toàn từ khóa nhận diện này, người kỹ sư bắt buộc phải rũ bỏ mọi định kiến và chuẩn bị tâm lý đón nhận một khái niệm mang tính trừu tượng cao độ về sự phân giải ngữ cảnh trong thời gian thực.

Như đã được thảo luận và phân tích từ trước, tại khoảnh khắc mà một hàm điện toán được định nghĩa và biên dịch, nó ngay lập tức bị buộc chặt vào không gian phạm vi bao bọc nó thông qua các sợi dây liên kết của cơ chế bao đóng. Không gian phạm vi này chính là một bộ quy tắc nghiêm ngặt dùng để kiểm soát và định hướng cách thức mà các nỗ lực tham chiếu đến các biến số sẽ được phân giải như thế nào. Tuy nhiên, các hàm điện toán còn sở hữu một đặc tính nhận dạng thứ hai bên cạnh phạm vi của chúng, một đặc tính có sức ảnh hưởng to lớn đến việc chúng được cấp quyền truy cập vào những tài nguyên nào. Đặc tính này được giới hàn lâm mô tả chính xác nhất dưới thuật ngữ ngữ cảnh thực thi, và nó được phơi bày trực tiếp cho bản thân hàm sử dụng thông qua từ khóa nhận diện this. Phạm vi mang bản chất tĩnh tại và chứa đựng một danh sách cố định, bất di bất dịch các biến số có sẵn ngay tại thời điểm và vị trí vật lý mà bạn định nghĩa hàm; thế nhưng, ngữ cảnh thực thi của hàm lại mang trong mình bản chất động cực kỳ uyển chuyển, phụ thuộc hoàn toàn và tuyệt đối vào cách thức mà cái hàm đó được gọi chạy (hoàn toàn bất chấp việc hàm đó được định nghĩa ở đâu hay thậm chí là nó được gọi chạy từ vị trí nào). Từ khóa this không bao giờ là một đặc tính cố định của một hàm dựa trên bản thiết kế định nghĩa của hàm đó, mà thực chất nó là một thuộc tính mang tính động, được tính toán và quyết định lại từ đầu trong mỗi một lần hàm được triệu hồi.

Một phương pháp tư duy hiệu quả để hình dung về ngữ cảnh thực thi là hãy xem nó như một đối tượng vật lý hiện hữu, mà toàn bộ các thuộc tính cấu thành nên nó được cấp phép sử dụng mở rộng bên trong không gian của hàm trong suốt quá trình hàm đó chạy. Nếu một hàm nhận diện ngữ cảnh được gọi chạy theo phong cách của một hàm thông thường, trần trụi mà không có bất kỳ ngữ cảnh nào được hệ thống chỉ định rõ ràng cho nó, thì ngữ cảnh đó sẽ tự động chuyển hướng và mặc định trỏ về phía đối tượng toàn cục của môi trường. Mặt khác, nếu hàm được gọi thông qua lời gọi thuộc tính của một đối tượng, đối tượng đó sẽ ngay lập tức bị ép buộc trở thành ngữ cảnh thực thi của hàm. Cấp độ kiểm soát cao nhất thuộc về phương thức call(), nó được thiết kế để nhận vào một đối tượng tùy ý và ép buộc từ khóa this của lời gọi hàm phải trỏ trực tiếp đến đối tượng đó. Việc một hàm nhận diện ngữ cảnh duy nhất, khi được triệu hồi theo ba phương thức hoàn toàn khác biệt nhau, lại đưa ra ba câu trả lời hoàn toàn khác nhau cho việc từ khóa this sẽ tham chiếu đến đối tượng nào, chính là minh chứng hùng hồn nhất cho tính linh hoạt của kiến trúc này. Lợi ích cốt lõi của các hàm nhận diện ngữ cảnh – cùng với tính chất động của chúng – nằm ở khả năng siêu việt cho phép hệ thống tái sử dụng một cách cực kỳ linh hoạt một logic hàm duy nhất để xử lý dữ liệu đến từ vô số các đối tượng khác biệt nhau.

Chuỗi nguyên mẫu và cơ chế ủy quyền đối tượng

Trong khi từ khóa nhận diện là linh hồn của quá trình thực thi hàm, thì nguyên mẫu lại là xương sống của mọi cấu trúc đối tượng trong ngôn ngữ JavaScript. Hệ thống nguyên mẫu thiết lập nên một mạng lưới liên kết ngầm, điều hướng mọi nỗ lực truy cập thuộc tính và tạo ra một cơ chế chia sẻ hành vi tốn ít bộ nhớ nhất có thể. Khái niệm này là bức tường thành cuối cùng cần phải chinh phục để thực sự hiểu rõ cách ngôn ngữ này kiến tạo nên khái niệm lập trình hướng đối tượng của riêng nó.

Khái niệm nguyên mẫu và mối liên kết ẩn

Nơi mà từ khóa this thể hiện mình như một đặc tính cốt lõi của quá trình thực thi hàm điện toán, thì nguyên mẫu lại đóng vai trò là một đặc tính nhận dạng quan trọng bậc nhất của một đối tượng, và một cách cụ thể hơn, nó là tác nhân chủ chốt tham gia vào tiến trình phân giải mọi nỗ lực truy cập vào các thuộc tính. Hãy tư duy trừu tượng và hình dung nguyên mẫu như một sợi dây liên kết vật lý kết nối hai thực thể đối tượng lại với nhau; sợi dây liên kết này được vận hành một cách vô cùng âm thầm và giấu kín đằng sau hậu trường, mặc dù ngôn ngữ vẫn cung cấp những công cụ chuyên biệt để phơi bày và cho phép kỹ sư quan sát nó một cách trực diện. Sự liên kết nguyên mẫu này được thiết lập ngay tại khoảnh khắc một đối tượng được khai sinh trong hệ thống; nó sẽ tự động móc nối với một đối tượng khác đã tồn tại từ trước đó trong không gian bộ nhớ. Sự hiểu biết về nguồn gốc của mối liên kết này giúp giải phóng tư duy khỏi những giáo điều hướng đối tượng cứng nhắc, mở ra một mô hình thiết kế linh hoạt hơn rất nhiều.

Một tập hợp bao gồm nhiều thực thể đối tượng được kết nối nối tiếp nhau thành một hàng dài thông qua các sợi dây nguyên mẫu này được giới hàn lâm gọi tên là chuỗi nguyên mẫu. Mục đích tối thượng của sự liên kết nguyên mẫu này (ví dụ như một sợi dây trỏ từ một đối tượng hạng B sang một đối tượng hạng A) là để thiết lập một cơ chế phòng ngự và phân giải tự động; khi có bất kỳ sự truy cập nào nhắm vào đối tượng B nhằm tìm kiếm một thuộc tính hay phương thức mà bản thân B hoàn toàn không sở hữu, yêu cầu truy cập đó sẽ không bị từ chối ngay lập tức mà nó sẽ được ủy quyền ngược lên phía trên cho đối tượng A để đối tượng A tiến hành xử lý. Cơ chế ủy quyền quyền lực trong việc truy cập thuộc tính hoặc phương thức này tạo ra một ranh giới kiến trúc tuyệt vời, cho phép hai (hoặc thậm chí là nhiều hơn) thực thể đối tượng có thể hợp tác, chung tay giải quyết một tác vụ tính toán phức tạp mà không cần phải sao chép dữ liệu của nhau.

Thậm chí ngay cả khi bạn khởi tạo một đối tượng thông qua cú pháp nguyên bản thông thường nhất, đối tượng đó mặc định đã sở hữu một sợi dây liên kết nguyên mẫu kết nối thẳng đến đối tượng nguyên mẫu gốc khổng lồ của hệ thống, nơi cung cấp sẵn vô vàn các phương thức nền tảng dùng chung. Chúng ta hoàn toàn có thể quan sát hiện tượng ủy quyền quyền lực của liên kết nguyên mẫu này trong thực tiễn: một nỗ lực gọi phương thức chuyển đổi chuỗi trên một đối tượng vừa mới tạo ra vẫn thành công mỹ mãn mặc dù bản thân đối tượng đó chưa hề được định nghĩa bất kỳ phương thức nào có tên như vậy; cơ chế ủy quyền đã âm thầm chặn đứng lỗi và chuyển hướng để triệu hồi phương thức tương ứng nằm trên đối tượng nguyên mẫu gốc thay thế. Sự tinh tế này đảm bảo rằng mọi thực thể sinh ra trong ngôn ngữ đều tự động kế thừa một tập hợp các công cụ giao tiếp tiêu chuẩn mà không làm phình to bộ nhớ.

Cơ chế ủy quyền và sự phân giải thuộc tính

Để có thể nắm quyền kiểm soát tuyệt đối và thiết lập sợi dây liên kết nguyên mẫu một cách có chủ đích, hệ thống cung cấp cho các kiến trúc sư phần mềm một tiện ích mạnh mẽ mang tên Object.create(). Đối số đầu tiên được truyền vào tiện ích hệ thống này sẽ đóng vai trò là một đối tượng đích để đối tượng vừa mới được tạo ra trỏ sợi dây liên kết nguyên mẫu của nó tới, sau đó tiện ích sẽ hoàn tất vòng lặp bằng cách trả về chính cái đối tượng vừa được khai sinh (và đã được liên kết thành công đó!). Việc truyền một giá trị rỗng tuyệt đối vào tiện ích này sẽ tạo ra một đối tượng mồ côi, hoàn toàn không có bất kỳ liên kết nguyên mẫu nào trỏ đi đâu cả; trong một vài bối cảnh kiến trúc hệ thống đặc biệt, việc sử dụng một đối tượng độc lập và hoàn toàn trần trụi như vậy lại là một sự lựa chọn ưu việt hơn hẳn, giúp tránh khỏi những cú sốc bảo mật do các thuộc tính thừa kế không mong muốn gây ra.

Cần phải vạch rõ một giới hạn kiến trúc vô cùng nghiêm ngặt: sự ủy quyền thông qua chuỗi nguyên mẫu chỉ được áp dụng và kích hoạt duy nhất đối với các tác vụ truy cập nhằm mục đích tra cứu và đọc giá trị nằm bên trong một thuộc tính. Nếu chương trình tiến hành một thao tác gán giá trị mới vào một thuộc tính của một đối tượng, thao tác gán quyền lực đó sẽ tác động vật lý và tạo ra một thuộc tính mới trực tiếp ngay trên chính đối tượng đang bị thao tác, hoàn toàn bất chấp và bỏ qua việc đối tượng đó đang được liên kết nguyên mẫu trỏ tới đâu trong hệ thống. Sự bất đối xứng giữa thao tác đọc và thao tác ghi này là hòn đá tảng bảo vệ sự toàn vẹn của các đối tượng gốc, ngăn chặn hiện tượng dữ liệu bị thay đổi một cách không mong muốn từ một đối tượng con nằm ở tận cùng của chuỗi nguyên mẫu.

Khi sự gán thuộc tính diễn ra thành công, nếu đối tượng cấp trên trong chuỗi cũng sở hữu một thuộc tính hoàn toàn trùng tên, thuộc tính mới được tạo ra trực tiếp ở lớp dưới sẽ tạo ra một hiện tượng kiến trúc quang học được gọi là Shadowing (Tạm dịch: Hiện tượng đổ bóng.). Thuộc tính mới sinh ra ở lớp dưới sẽ che khuất hoàn toàn thuộc tính đồng danh nằm trên đối tượng ở trong chuỗi nguyên mẫu, đảm bảo rằng các nỗ lực truy cập đọc của hệ thống từ thời điểm đó trở đi sẽ chỉ nhìn thấy và trả về giá trị mới được gán từ lớp dưới. Mặc dù trong quá khứ đã từng tồn tại những khuôn mẫu tạo lập đối tượng với liên kết nguyên mẫu phức tạp hơn nhiều thông qua mẫu kiến trúc lớp nguyên mẫu, việc thấu hiểu cơ chế tạo bóng và gán đè này vẫn là nền tảng tối quan trọng trước khi đi sâu vào các cú pháp lớp hiện đại được bổ sung trong phiên bản đặc tả thứ sáu.

Sự kết hợp giữa ngữ cảnh động và chuỗi nguyên mẫu

Chúng ta đã tiến hành giải phẫu một cách chi tiết về từ khóa nhận diện this ở các phần trước, nhưng tầm quan trọng thực sự mang tính cách mạng của nó chỉ tỏa sáng một cách rực rỡ nhất khi chúng ta tiến hành phân tích cách thức mà nó tiếp năng lượng và điều phối các lời gọi hàm được ủy quyền qua chuỗi nguyên mẫu. Trên thực tế, một trong những động cơ triết học lớn nhất giải thích lý do tại sao từ khóa này lại bắt buộc phải hỗ trợ tính năng ngữ cảnh động dựa trên cách thức gọi hàm chính là để đảm bảo rằng các lời gọi phương thức diễn ra trên các đối tượng – những phương thức vốn dĩ phải dựa vào sự ủy quyền thông qua chuỗi nguyên mẫu để hoạt động – vẫn có thể kiên định duy trì được đối tượng ngữ cảnh đúng như sự kỳ vọng của hệ thống. Đây là minh chứng tuyệt vời cho sự thiết kế mạch lạc, nơi các tính năng độc lập giao thoa và hỗ trợ nhau một cách hoàn hảo.

Hãy xem xét một kịch bản phức tạp: hai đối tượng con riêng biệt cùng thiết lập sợi dây liên kết nguyên mẫu trỏ về chung một đối tượng cha duy nhất, và đối tượng cha này nắm giữ một hàm chức năng dùng chung. Mỗi một đối tượng con lại được cấp phát và duy trì một thuộc tính dữ liệu mang tên riêng biệt của chính nó. Khi một đối tượng con bất kỳ khởi xướng lời gọi đến hàm dùng chung đó, yêu cầu hiển nhiên sẽ được ủy quyền trơn tru cho đối tượng cha xử lý, nhưng điều kỳ diệu là từ khóa ngữ cảnh trong suốt quá trình chạy phương thức đó vẫn được phân giải và chốt chặt vào đúng cái đối tượng con đã gọi nó, nhờ vào nguyên lý ngữ cảnh động dựa trên cách gọi hàm, dẫn đến việc dữ liệu được truy xuất chính là dữ liệu của đối tượng con. Cơ chế này mang lại khả năng tái sử dụng mã nguồn cực kỳ khủng khiếp, một hàm chức năng nằm ở lớp cha có thể phục vụ cho hàng triệu thực thể con khác nhau mà không hề gây ra sự nhầm lẫn về mặt dữ liệu trạng thái.

Khối mã lệnh phối hợp nhịp nhàng giữa ủy quyền nguyên mẫu và ngữ cảnh động ở trên sẽ trở nên vô dụng và sụp đổ hoàn toàn nếu từ khóa nhận diện bị hệ thống ép buộc phải trỏ cố định về đối tượng cha. Thế nhưng, trong tư duy của vô số các ngôn ngữ lập trình hướng đối tượng khác, điều này lại dường như là một sự thật hiển nhiên và hoàn toàn hợp lý bởi vì phương thức đó thực sự được định nghĩa vật lý ngay trên lớp cha. Trái ngược hoàn toàn với triết lý của nhiều ngôn ngữ lập trình khác, tính chất động của từ khóa nhận diện trong ngôn ngữ JavaScript là một mảnh ghép kiến trúc mang tính sống còn, là bệ phóng đảm bảo cho cơ chế ủy quyền nguyên mẫu, và sâu xa hơn là cả kiến trúc lớp hiện đại, có thể vận hành một cách hoàn hảo đúng như những gì các kỹ sư kỳ vọng.

Kết luận

Bài học cốt lõi cần phải đúc kết lại sau toàn bộ những phân tích học thuật chuyên sâu trong chương tài liệu này là cấu trúc nền tảng của ngôn ngữ JavaScript ẩn chứa vô vàn những cơ chế tinh vi và phức tạp ở tầng thấp, vượt xa những gì có thể dễ dàng nhận thấy khi chỉ quan sát cú pháp trên bề mặt. Khi bạn bắt đầu dấn thân vào con đường nghiên cứu và thấu hiểu ngôn ngữ này một cách tường tận hơn, một trong những kỹ năng sống còn quan trọng nhất mà bạn cần phải liên tục mài giũa chính là trí tò mò khoa học, cùng với nghệ thuật không ngừng đặt ra câu hỏi tại sao mỗi khi đối mặt với những hiện tượng kỳ lạ bên trong hệ thống. Dù chúng ta đã đào sâu vào một vài khái niệm trọng yếu, vô số những chi tiết kiến trúc khác vẫn chỉ mới được điểm qua một cách lướt vội trên bề mặt. Vẫn còn một đại dương tri thức khổng lồ đang chờ đợi bạn khám phá ở những tập sách tiếp theo, và hành trình chinh phục đó bắt đầu bằng việc bạn biết cách đặt ra những câu hỏi đúng đắn đối với chính đoạn mã nguồn mà bạn tạo ra mỗi ngày. Việc liên tục truy vấn và tìm kiếm chân lý đằng sau mỗi dòng mã lệnh không chỉ là con đường duy nhất để giải mã hệ sinh thái này, mà đó còn là thước đo chuẩn mực nhất để đánh giá sự trưởng thành của một kỹ sư phần mềm thực thụ trên hành trình làm chủ nghệ thuật kiến trúc hệ thống.

Chuyên mục thu-vien

Theo dõi hành trình

Hãy để lại thông tin, khi có gì mới thì Nhà văn sẽ gửi thư đến bạn để cập nhật. Cam kết không gửi email rác.

Họ và tên

Email liên lạc

Đôi dòng chia sẻ