Mở đầu
Trong nội dung của Chương 3, thuật ngữ không gian phạm vi toàn cục đã được chúng ta nhắc đi nhắc lại với tần suất không hề nhỏ, thế nhưng, có một khả năng rất cao là bạn vẫn đang trăn trở với một câu hỏi hóc búa: tại sao cái không gian phạm vi nằm ở tầng ngoài cùng, bao bọc toàn bộ một chương trình lại vẫn còn giữ được tầm vóc quan trọng đến vậy trong kỷ nguyên định hình bởi các tiêu chuẩn kiến trúc hiện đại? Thực tế chứng minh, đại đa số các khối lượng công việc tính toán, xử lý logic phức tạp trong các dự án thực tế hiện nay đều đã được đóng gói cẩn thận và thực thi ở sâu bên trong nội tạng của các hàm điện toán và các hệ thống khối (modules), thay vì bị vứt chỏng chơ ngoài không gian toàn cục. Vậy thì, liệu chúng ta có thể đơn giản là đưa ra một tuyên ngôn cửa miệng ngắn gọn, sắc bén: Hãy tẩy chay không gian phạm vi toàn cục bằng mọi giá, rồi coi như vấn đề đã được giải quyết một cách triệt để hay không? Không gian phạm vi toàn cục của một chương trình không đơn giản như vậy, nó là một kho tàng chủ đề phong phú, ẩn chứa nhiều giá trị tiện ích không thể thay thế cũng như những sắc thái kỹ thuật vi tế vượt xa khỏi những gì mà bạn có thể ngây thơ phỏng đoán ban đầu. Nội dung của tài liệu này sẽ thực hiện một cuộc khai phá sâu rộng, trước hết là để phơi bày lý do tại sao không gian phạm vi toàn cục vẫn đang kiên cường chứng minh được tính hữu dụng và mối quan hệ sống còn của nó đối với công việc kiến tạo mã nguồn ngày nay, và sau đó, chúng ta sẽ đặt lên bàn cân phân tích những sự sai lệch, khác biệt tinh vi về vị trí cũng như phương thức truy xuất vào không gian phạm vi toàn cục này khi mã nguồn bị ném vào những môi trường thực thi hoàn toàn khác biệt nhau.
Sự tồn tại thiết yếu của phạm vi toàn cục
Việc phần lớn các hệ thống phần mềm đương đại đều được lắp ghép từ vô số (thậm chí là một số lượng khổng lồ!) các tệp tin chứa mã nguồn riêng biệt có lẽ không còn là một thông tin gây sốc đối với độc giả. Vậy câu hỏi hóc búa được đặt ra ở đây là: bằng một cơ chế ma thuật nào mà cỗ máy thông dịch có thể khâu nối, chắp vá tất cả những mảnh ghép rời rạc đó lại thành một bối cảnh thực thi hợp nhất, duy nhất? Khi thu hẹp lăng kính vào các ứng dụng vận hành trong môi trường trình duyệt web, chúng ta có thể nhận diện ba phương thức liên kết chủ đạo.
Thứ nhất, nếu dự án của bạn đang khai thác sức mạnh nguyên bản của Hệ thống khối chuẩn ECMAScript (mà không hề có sự nhúng tay của các công cụ đóng gói để biến đổi chúng thành một định dạng khác), môi trường thực thi sẽ tự động đảm nhận việc nạp từng tệp tin riêng lẻ này vào bộ nhớ. Từ đó, mỗi khối module sẽ tự giác sử dụng cú pháp import để kéo về những sợi dây tham chiếu trỏ đến các module láng giềng mà nó cần tương tác. Những tệp tin module biệt lập này sẽ hợp tác với nhau một cách độc quyền thông qua những sợi dây liên kết đã được nhập về này, hoàn toàn loại bỏ đi nhu cầu phải có một không gian phạm vi chung bao bọc bên ngoài để làm cầu nối. Thứ hai, nếu quy trình xây dựng dự án của bạn có sự góp mặt của một công cụ đóng gói (bundler), thì toàn bộ tệp tin mã nguồn thường sẽ bị nghiền nát và nối liền lại thành một tệp tin khổng lồ duy nhất trước khi được vận chuyển qua mạng lưới đến trình duyệt và cỗ máy thông dịch, lúc này cỗ máy chỉ phải nuốt trọn một tệp tin duy nhất đó. Tuy nhiên, ngay cả khi toàn bộ các bộ phận nội tạng của ứng dụng đã bị nhồi nhét chung vào một không gian tệp tin, vẫn bắt buộc phải tồn tại một cơ chế nào đó để mỗi một bộ phận có thể tự đăng ký một tên gọi định danh cho chính nó, qua đó cho phép các bộ phận khác có thể réo gọi và tương tác, đồng thời cũng cần phải có một hạ tầng để quá trình truy cập chéo này có thể diễn ra trơn tru.
Trong một số kiến trúc đóng gói tinh vi, toàn bộ nội tạng của tệp tin gộp này sẽ được bọc lại trong một lớp màng bảo vệ là một không gian phạm vi duy nhất, ví dụ điển hình nhất là một hàm bao bọc khổng lồ hoặc một định dạng module vạn năng (Universal Module Definition). Bằng cách này, mỗi một mảnh ghép có thể tự tin phơi bày bản thân cho các mảnh ghép khác truy cập thông qua việc khởi tạo các biến số cục bộ nằm bên trong không gian phạm vi dùng chung đó. Lấy một ví dụ, các biến số đại diện cho module số một và module số hai nằm bên trong hàm bao bọc ngoài cùng được khai báo một cách có chủ ý để hai module này có thể tìm thấy và tương tác với nhau. Mặc dù không gian phạm vi của cái hàm bao bọc khổng lồ này về bản chất vẫn chỉ là một phạm vi hàm chứ không phải là không gian phạm vi toàn cục thực thụ của môi trường thực thi, nhưng nó lại thành công rực rỡ trong việc đóng giả làm một không gian phạm vi cấp ứng dụng — một chiếc xô dung chứa toàn bộ các định danh cấp cao nhất của hệ thống, dù không phải là không gian toàn cục xịn. Ở khía cạnh đó, nó sắm vai hoàn hảo như một kẻ đóng thế thế mạng cho không gian phạm vi toàn cục.
Và cuối cùng là phương thức thứ ba: bất chấp việc dự án có sử dụng công cụ đóng gói hay không, hoặc việc các tệp tin (không tuân theo chuẩn Hệ thống khối ECMAScript) được nạp thủ công từng tệp một vào trình duyệt (thông qua các thẻ script nhúng trực tiếp hoặc các kỹ thuật nạp tài nguyên động), nếu như không hề có sự hiện diện của một lớp màng không gian phạm vi chung bao bọc lấy toàn bộ các mảnh ghép này, thì không gian phạm vi toàn cục chính là con đường sống duy nhất, là sân chơi chung độc nhất để chúng có thể nhận diện và hợp tác với nhau. Trong kịch bản không có hàm bao bọc, các khai báo module đơn giản là bị ném thẳng tay vào không gian phạm vi toàn cục, kết quả tạo ra cũng y hệt như việc các tệp tin đó không bị gộp lại mà được nạp riêng lẻ: mỗi một lệnh khai báo biến số ở cấp độ cao nhất sẽ nghiễm nhiên hóa thân thành một biến số toàn cục, bởi vì đối với cỗ máy thông dịch, mỗi tệp tin đó là một chương trình độc lập và không gian phạm vi toàn cục là tài nguyên chia sẻ duy nhất giữa chúng. Không chỉ đóng vai trò là sân chơi chung cho các mảnh ghép mã nguồn nội bộ tương tác, không gian phạm vi toàn cục còn là nơi hệ thống ngôn ngữ và môi trường máy chủ trưng bày các tiện ích tích hợp sẵn của chúng. Từ các giá trị nguyên thủy như undefined, null, các hàm toán học toàn cục như parseInt(), các không gian tên như Math, JSON do ngôn ngữ cung cấp, cho đến các giao diện lập trình web khổng lồ như console, DOM (window, document), hay các hàm hẹn giờ như setTimeout() do trình duyệt cung cấp; tất cả đều cư ngụ tại đây. Mặc dù việc biến không gian này thành bãi rác chứa mọi biến số là một thảm họa lỗi tiềm ẩn, nhưng không ai có thể phủ nhận vai trò của nó như một loại chất keo kết dính thiết yếu cho hầu hết mọi ứng dụng hoạt động trên nền tảng này.
Sự phân mảnh của phạm vi toàn cục qua các môi trường
Có một lầm tưởng tưởng chừng như hiển nhiên rằng không gian phạm vi toàn cục luôn chễm chệ tọa lạc ở tầng ngoài cùng của một tệp tin; tức là phần không gian không bị nhốt bên trong bất kỳ một cái hàm hay một khối mã lệnh nào. Nhưng thực tại kiến trúc lại không hề đơn giản và ngây thơ như vậy. Mỗi một môi trường thực thi lại áp dụng một triết lý cai trị hoàn toàn khác biệt đối với các không gian phạm vi của chương trình, và đặc biệt là cách chúng đối xử với không gian phạm vi toàn cục. Việc các kỹ sư lập trình vô tình ôm giữ những nhận thức sai lệch, méo mó về vấn đề này mà không hề hay biết là một thực trạng cực kỳ phổ biến.
Môi trường trình duyệt và đối tượng window
Khi đánh giá về cách thức một môi trường đối xử với không gian phạm vi toàn cục, môi trường mang tính chất thuần khiết nhất mà ngôn ngữ này có thể được triển khai chính là khi nó tồn tại dưới dạng một tệp tin độc lập được nạp thẳng vào không gian của một trang web bên trong trình duyệt. Từ thuần khiết ở đây hoàn toàn không mang ý nghĩa là hệ thống không tự động nhồi nhét thêm bất cứ thứ gì vào – thực tế là nó nhét vào cả đống thứ! – mà từ này dùng để ngợi ca mức độ can thiệp cực kỳ tối thiểu của môi trường vào mã nguồn, cũng như sự tôn trọng tuyệt đối đối với những hành vi được kỳ vọng từ không gian phạm vi toàn cục. Bất kể một tệp tin mã nguồn được nhúng vào trang thông qua thẻ <script> nội tuyến, hay liên kết ngoại tuyến <script src=...>, hoặc thậm chí được tạo ra động bằng các thao tác DOM, những định danh được khai báo ở tầng cao nhất đều nghiễm nhiên trở thành công dân của không gian phạm vi toàn cục. Hệ quả tất yếu là nếu bạn tiến hành thọc tay vào đối tượng toàn cục (thường được biết đến với bí danh window trong trình duyệt), bạn sẽ tìm thấy những thuộc tính mang tên gọi y hệt như những định danh đó nằm chễm chệ ở đó. Cách hành xử này tuân thủ một cách hoàn hảo những gì mà một người đọc tài liệu đặc tả kỳ vọng: không gian ngoài cùng chính là không gian phạm vi toàn cục, và các biến số đó đã được khai sinh một cách danh chính ngôn thuận dưới tư cách là những biến số toàn cục thực thụ. Đó là những gì tôi muốn truyền tải thông qua từ thuần khiết. Nhưng một sự thật phũ phàng là, cái chuẩn mực thuần khiết đó sẽ không luôn luôn được duy trì ở mọi môi trường thực thi mà bạn sẽ phải đối mặt, và sự lệch pha này thường xuyên giáng những đòn bất ngờ, gây ra sự hoang mang tột độ cho giới lập trình.
Biến số toàn cục che khuất thuộc tính toàn cục
Hãy cùng làm mới lại ký ức về khái niệm tạo bóng (và thủ thuật xuyên thủng ranh giới tạo bóng toàn cục) đã được đem ra mổ xẻ ở Chương 3, nơi mà một câu lệnh khai báo biến số sở hữu quyền năng đè bẹp và cắt đứt hoàn toàn mọi con đường truy cập đến một khai báo mang cùng tên gọi nằm ở không gian phạm vi bên ngoài. Một hệ lụy kiến trúc vô cùng dị biệt, sinh ra từ sự khác biệt bản chất giữa một biến số toàn cục và một thuộc tính toàn cục mang cùng tên gọi, đó là: ngay tại chính lãnh địa của không gian phạm vi toàn cục, một thuộc tính nằm trên đối tượng toàn cục lại có thể bị che khuất một cách phũ phàng bởi một biến số toàn cục. Khi chúng ta sử dụng từ khóa let để khai báo, hành động này thực chất chỉ bơm thêm một biến số toàn cục vào hệ thống, chứ hoàn toàn không hề sinh ra một thuộc tính phản chiếu tương ứng trên đối tượng toàn cục (như những gì Chương 3 đã phân tích). Hệ quả dở khóc dở cười sinh ra từ đây là cái định danh từ vựng của biến số đó sẽ vươn lên che khuất hoàn toàn cái thuộc tính cùng tên nằm trên đối tượng toàn cục. Việc cố tình kiến tạo ra một sự phân ly, bất đồng bộ giữa đối tượng toàn cục và không gian phạm vi toàn cục gần như chắc chắn là một quyết định kiến trúc tồi tệ nhất mà bạn có thể đưa ra. Bất kỳ ai phải bảo trì đoạn mã đó đều sẽ nắm chắc phần trăm bị sập bẫy và vấp ngã. Một chiến lược phòng ngự chủ động và cực kỳ đơn giản để né tránh cái cạm bẫy liên quan đến các khai báo toàn cục này là: hãy thiết lập một kỷ luật sắt đá, luôn luôn và chỉ sử dụng từ khóa var cho các biến số toàn cục. Hãy bảo lưu từ khóa let và const như những vũ khí chuyên biệt chỉ dành riêng cho việc thiết lập các không gian phạm vi khối.
Biến số toàn cục do thao tác DOM
Mặc dù tôi đã dõng dạc tuyên bố rằng môi trường thực thi trên trình duyệt mang lại một hành vi không gian phạm vi toàn cục thuần khiết nhất mà chúng ta có cơ hội được trải nghiệm, nhưng thực tế nó vẫn chưa đạt đến độ hoàn hảo tuyệt đối của sự thuần khiết. Tồn tại một hành vi vô cùng quái gở, dễ gây sốc và lẩn khuất trong không gian phạm vi toàn cục mà bạn rất có thể sẽ đụng độ khi phát triển các ứng dụng trên trình duyệt: bất kỳ một phần tử DOM nào được gắn một thuộc tính định danh id đều sẽ tự động, bằng một thế lực vô hình nào đó, sinh ra một biến số toàn cục có chức năng tham chiếu thẳng đến cái phần tử DOM đó. Nếu giá trị của thuộc tính id là một chuỗi ký tự hợp lệ để làm tên biến (ví dụ như first), thì cái biến số từ vựng đó sẽ ngay lập tức được hệ thống khai sinh. Nếu nó chứa những ký tự không hợp lệ (ví dụ như dấu gạch nối), con đường duy nhất để bạn có thể chạm tay vào cái biến toàn cục đó là phải sử dụng cú pháp truy cập thuộc tính mảng trên đối tượng toàn cục (ví dụ như window[my-todo-list]). Cơ chế tự động đăng ký nhân khẩu cho toàn bộ các phần tử DOM có chứa thuộc tính id để biến chúng thành các biến số toàn cục thực chất là một hành vi tàn dư, một di sản lỗi thời từ những thế hệ trình duyệt thời kỳ đồ đá, thế nhưng nó vẫn ngoan cố tồn tại cho đến tận ngày nay chỉ vì có quá nhiều trang web cổ lỗ sĩ vẫn còn đang thoi thóp sống dựa vào cái hành vi quái đản này. Lời khuyên chân thành và mang tính mệnh lệnh nhất của tôi là tuyệt đối, vĩnh viễn không bao giờ được phép sử dụng những biến số toàn cục này, bất chấp việc hệ thống vẫn sẽ âm thầm đẻ ra chúng đằng sau lưng bạn.
Nghịch lý của định danh window.name
Một sự kỳ dị khác làm vẩn đục không gian phạm vi toàn cục trong môi trường trình duyệt liên quan đến định danh name. Thuộc tính window.name là một công dân toàn cục đã được định nghĩa sẵn bởi trình duyệt; bản chất nó là một thuộc tính bám trên đối tượng toàn cục, nên nó tạo ra một ảo giác hoàn hảo rằng nó là một biến số toàn cục bình thường (thế nhưng sự thật nó là một thứ quái thai khác xa với từ bình thường). Khi chúng ta tung ra câu lệnh khai báo bằng từ khóa var cho biến name, câu lệnh này tuyệt đối không hề thực hiện hành vi che khuất cái thuộc tính toàn cục name đã tồn tại từ trước đó. Điều này đồng nghĩa với việc, trên thực tế, cái câu lệnh khai báo var đó đã bị hệ thống nhắm mắt làm ngơ, bởi vì cái ghế đó đã có người ngồi – một thuộc tính đối tượng toàn cục mang cùng tên gọi đã chiếm chỗ. Tuy nhiên, hành vi gây chấn động thực sự ở đây là: mặc dù chúng ta đã thực hiện thao tác gán một giá trị số 42 vào cái biến name (và theo logic bắc cầu là gán vào window.name), nhưng khi chúng ta nỗ lực truy xuất để lấy lại cái giá trị đó, thứ mà chúng ta nhận về lại là một chuỗi ký tự 42 thay vì một con số! Trong trường hợp điên rồ này, sự quái dị bắt nguồn từ việc name thực chất là một cặp phương thức lấy/gán dữ liệu (getter/setter) đã được hệ thống cài cắm sẵn vào đối tượng window, và cái cặp phương thức bảo thủ này ép buộc mọi giá trị đi qua nó đều phải bị chuyển đổi thành định dạng chuỗi ký tự. Ngoại trừ những góc khuất kỳ dị hiếm hoi liên quan đến định danh phần tử DOM và thuộc tính window.name vừa nêu, ngôn ngữ JavaScript khi được thả vào chạy tự do như một tệp tin độc lập trên trang web vẫn tự hào mang trong mình một hành vi không gian phạm vi toàn cục thuần khiết bậc nhất mà chúng ta có thể chạm tới.
Luồng thực thi phụ Web Workers
Công nghệ Web Workers là một nỗ lực mở rộng nền tảng web mạnh mẽ, được xây dựng đè lên trên các hành vi cốt lõi của môi trường JavaScript trên trình duyệt, công nghệ này trao quyền năng cho phép một tệp tin mã nguồn được chạy trên một luồng hệ điều hành hoàn toàn biệt lập, tách rời hoàn toàn khỏi cái luồng chính đang gánh vác việc chạy chương trình ứng dụng. Do các chương trình Web Worker này ngự trị trên một luồng xử lý riêng biệt, nên đặc quyền giao tiếp của chúng với cái luồng ứng dụng chính bị hệ thống kiểm soát và thắt chặt nghiêm ngặt, một biện pháp phòng vệ cần thiết nhằm triệt tiêu và giới hạn tối đa thảm họa xung đột dữ liệu (race conditions) cũng như những biến chứng hệ thống khác. Ví dụ điển hình, mã nguồn chạy bên trong Web Worker bị cấm tiệt quyền truy cập và thao tác với cây DOM. Mặc dù vậy, một số lượng hạn chế các giao diện lập trình web vẫn được hệ thống cấp phép cho worker sử dụng, chẳng hạn như đối tượng navigator.
Bởi vì một Web Worker được hệ thống đối xử với tư cách là một chương trình điện toán hoàn toàn biệt lập, nó tuyệt đối không chia sẻ chung bầu không gian phạm vi toàn cục với cái chương trình ứng dụng chính. Tuy nhiên, do đoạn mã đó vẫn đang được nhai nuốt bởi cùng một cỗ máy thông dịch JavaScript của trình duyệt, nên chúng ta hoàn toàn có cơ sở để kỳ vọng về một mức độ thuần khiết tương đương đối với hành vi không gian phạm vi toàn cục của nó. Vì quyền truy cập DOM đã bị tước đoạt, nên cái bí danh window dùng để gọi đối tượng toàn cục cũng vĩnh viễn bốc hơi. Bên trong môi trường của một Web Worker, nếu muốn thực hiện hành vi tham chiếu đến cái đối tượng toàn cục, các kỹ sư thường phải viện đến một từ khóa thay thế là self. Hoàn toàn tương đồng với các chương trình JavaScript ở luồng chính, các câu lệnh khai báo sử dụng từ khóa var và function vẫn sẽ ngoan ngoãn sản sinh ra những thuộc tính phản chiếu nằm trên đối tượng toàn cục (lúc này mang tên là self), trong khi các dạng khai báo khác (như let, v.v.) thì không. Tóm lại một lần nữa, hành vi của không gian phạm vi toàn cục mà chúng ta đang chứng kiến tại đây thực sự đã đạt đến cảnh giới thuần khiết tối đa có thể có đối với việc vận hành các chương trình điện toán; thậm chí có thể lập luận rằng nó còn mang tính thuần khiết cao hơn bởi vì sự vắng mặt của cây DOM đã loại bỏ hoàn toàn một tác nhân gây nhiễu loạn khổng lồ!
Môi trường giả lập của công cụ phát triển
Hãy nhớ lại những kiến thức nền tảng từ Chương 1 trong cuốn sách Khởi Đầu, nơi chúng ta đã chỉ ra một sự thật rằng các Công cụ Dành cho Nhà phát triển (Developer Tools) không hề tạo ra một môi trường thực thi tuân thủ nghiêm ngặt 100% các quy định của ngôn ngữ. Đúng là chúng có khả năng xử lý mã nguồn, nhưng triết lý thiết kế của chúng lại thiên lệch và nhượng bộ rất nhiều để đổi lấy một trải nghiệm tương tác thân thiện, mượt mà nhất cho các kỹ sư (hay còn gọi là tối ưu hóa trải nghiệm nhà phát triển - DX). Trong một số kịch bản, việc đặt nặng yếu tố DX khi người dùng gõ vào những đoạn mã ngắn ngủi, thay vì tuân thủ một cách máy móc các bước xử lý khắt khe vốn được kỳ vọng đối với một chương trình điện toán hoàn chỉnh, đã vô tình sản sinh ra những độ lệch pha rõ rệt về mặt hành vi mã nguồn giữa các chương trình thực tế và các công cụ giả lập này. Ví dụ, một số tình trạng lỗi nghiêm trọng lẽ ra phải được ném ra trong một ngữ cảnh chương trình thực thụ lại có thể bị hệ thống nhắm mắt làm ngơ, nới lỏng và không thèm hiển thị khi đoạn mã đó được ném vào trong một công cụ dành cho nhà phát triển.
Khi chiếu rọi vấn đề này vào những cuộc thảo luận chuyên sâu về không gian phạm vi của chúng ta, những sai lệch hành vi có thể quan sát được đó có thể bao gồm: sự biến tướng trong hành vi của không gian phạm vi toàn cục; hiện tượng kéo lên của biến số; và cách thức hệ thống xử lý các từ khóa khai báo phạm vi khối (let / const) khi chúng được sử dụng lơ lửng ở không gian phạm vi ngoài cùng nhất. Mặc dù khi bạn đang gõ phím cặm cụi trong bảng điều khiển (console) hoặc môi trường REPL, mọi thứ có vẻ tạo ra một ảo giác hoàn hảo rằng những câu lệnh được ném vào cái không gian ngoài cùng đó đang thực sự được xử lý bằng chính cái không gian phạm vi toàn cục hàng thật giá thật, nhưng sự thật lại không hề như vậy. Những môi trường công cụ này thực chất chỉ đang cố gắng bắt chước, giả lập lại vị trí của không gian phạm vi toàn cục ở một mức độ nào đó; nó thuần túy là sự giả lập, chứ không phải là sự tuân thủ nghiêm ngặt theo đặc tả. Việc ưu tiên sự tiện lợi tối đa cho các nhà phát triển đồng nghĩa với việc đôi khi (đặc biệt là đối với những chủ đề gai góc liên quan đến không gian phạm vi mà chúng ta đang mổ xẻ), các hành vi mà bạn quan sát được có thể sẽ đi chệch khỏi con đường mà tài liệu đặc tả của ngôn ngữ đã vạch ra. Bài học xương máu rút ra ở đây là: các Công cụ Dành cho Nhà phát triển, mặc dù đã được tối ưu hóa đến mức hoàn hảo để mang lại sự tiện lợi và hữu ích vô song cho vô vàn các hoạt động phát triển phần mềm, nhưng chúng tuyệt đối không phải là một môi trường đủ tiêu chuẩn và độ tin cậy để bạn đem ra làm căn cứ xác định hay kiểm chứng những hành vi kiến trúc vi tế, mang tính đặc thù sâu sắc của một bối cảnh chương trình thực thụ.
Khái niệm phạm vi cấp khối ECMAScript (ESM)
Phiên bản đặc tả thứ sáu đã tạo ra một cơn địa chấn khi chính thức nâng tầm và hỗ trợ mô hình hệ thống khối lên hàng công dân hạng nhất (first-class support). Một trong những hệ lụy nhãn tiền và đập thẳng vào mắt nhất sinh ra từ việc ứng dụng Hệ thống khối chuẩn ECMAScript (ESM) chính là việc nó lật đổ hoàn toàn cách thức hành xử truyền thống của cái không gian phạm vi có thể quan sát được nằm ở tầng cao nhất của một tệp tin. Hãy cùng lôi lại đoạn mã mẫu từ phần trước ra mổ xẻ (và chúng ta sẽ tiến hành xào xáo lại nó cho phù hợp với định dạng ESM bằng cách cấy ghép thêm từ khóa export). Nếu cái đoạn mã đó nằm lọt thỏm trong một tệp tin được hệ thống nạp vào dưới danh nghĩa là một hệ thống khối chuẩn ES, nó vẫn sẽ vận hành trơn tru và tạo ra kết quả y hệt như cũ. Thế nhưng, những hệ lụy có thể quan sát được về mặt kiến trúc, khi nhìn từ bức tranh tổng thể của toàn bộ ứng dụng, lại rẽ sang một hướng hoàn toàn khác biệt. Bất chấp một sự thật rành rành là chúng đã được khai báo ở ngay cái tầng cao nhất của cái tệp tin (tức là cái hệ thống khối đó), ngay tại cái vị trí hiển nhiên nhất của không gian phạm vi ngoài cùng, thì biến tên sinh viên và hàm xin chào vẫn bị tước đoạt tư cách là những biến số toàn cục. Thay vào đó, thân phận của chúng bị giáng xuống chỉ còn là những biến số có tầm ảnh hưởng bao trùm toàn bộ hệ thống khối đó, hay nếu bạn thích một thuật ngữ mỹ miều hơn, thì là biến số toàn cục cấp module.
Điều đáng sợ hơn là, bên trong một hệ thống khối, vĩnh viễn không hề tồn tại bất kỳ một đối tượng phạm vi cấp module ngầm định nào để các khai báo cấp cao nhất này có thể bấu víu vào và hóa thân thành các thuộc tính, khác biệt hoàn toàn với cái cách mà hệ thống đối xử khi các khai báo xuất hiện ở tầng cao nhất của các tệp tin JS không phải định dạng module. Tuyên bố này hoàn toàn không có ý định phủ nhận sự tồn tại hay khả năng bị truy cập của các biến số toàn cục bên trong những chương trình kiểu này. Vấn đề cốt lõi chỉ là các biến số toàn cục sẽ không tự dưng được khai sinh ra chỉ bằng hành động vứt các câu lệnh khai báo biến vào cái không gian phạm vi cấp cao nhất của một hệ thống khối. Không gian phạm vi cấp cao nhất của một hệ thống khối mang trong mình dòng máu được thừa kế trực tiếp từ không gian phạm vi toàn cục, hoạt động gần giống như thể toàn bộ nội tạng của cái hệ thống khối đó đã bị bọc gọn gàng trong một cái hàm khổng lồ. Do đó, tất cả những biến số đang lởn vởn tồn tại trong không gian phạm vi toàn cục (bất chấp việc chúng có đang bám trên đối tượng toàn cục hay không!) đều sẵn sàng phơi mình ra dưới dạng các định danh từ vựng để có thể bị truy cập một cách dễ dàng từ bên trong không gian phạm vi của hệ thống khối. Triết lý của ESM là cổ vũ mạnh mẽ cho việc giảm thiểu đến mức tối đa sự phụ thuộc tồi tệ vào không gian phạm vi toàn cục, thay vào đó, bạn sẽ chủ động sử dụng cú pháp import để kéo về bất kỳ hệ thống khối nào mà hệ thống khối hiện tại đang khao khát để có thể vận hành trơn tru. Nhờ đó, bạn sẽ ngày càng hiếm khi phải chứng kiến những hành vi lạm dụng không gian phạm vi toàn cục hay cái đối tượng toàn cục của nó. Tuy nhiên, như đã được cảnh báo từ trước, vẫn còn lẩn khuất ngoài kia vô số những biến số toàn cục của môi trường web và của chính bản thân ngôn ngữ mà bạn vẫn sẽ tiếp tục phải tương tác và truy cập thông qua không gian phạm vi toàn cục, bất kể bạn có nhận thức được điều đó hay không!
Môi trường máy chủ Node.js
Có một sự thật về kiến trúc của môi trường Node thường xuyên tung ra những cú đánh úp bất ngờ, khiến các nhà phát triển JavaScript trở tay không kịp: Node đối xử với mọi tệp tin .js mà nó nạp vào bộ nhớ, bao gồm cả cái tệp tin gốc đóng vai trò khởi động toàn bộ tiến trình, dưới tư cách là một hệ thống khối hoàn chỉnh (có thể là ESM hoặc CommonJS). Hệ quả thực tiễn vô cùng tàn khốc sinh ra từ đây là tầng cao nhất của các chương trình chạy trên Node của bạn vĩnh viễn không bao giờ thực sự là không gian phạm vi toàn cục, khác biệt một trời một vực so với cái cách mà mọi thứ diễn ra khi nạp một tệp tin phi module vào trong môi trường trình duyệt. Tính đến thời điểm hiện tại, Node đã bắt kịp thời đại và chính thức hỗ trợ Hệ thống khối chuẩn ES. Thế nhưng, để nhìn lại lịch sử, ngay từ thuở hồng hoang, Node đã đứng ra bảo kê và hỗ trợ cho một định dạng module mang tên gọi CommonJS, một định dạng có hình hài cú pháp khá đặc trưng với việc sử dụng module.exports.
Trước khi thực sự bắt tay vào xử lý, bộ máy của Node đã âm thầm thực hiện một thủ thuật bọc cái đoạn mã đó lại bằng một hàm ẩn danh khổng lồ, nhằm mục đích giam cầm toàn bộ các câu lệnh khai báo var và function vào bên trong cái không gian phạm vi của chính cái hàm bao bọc đó, qua đó kiên quyết không đối xử với chúng như những biến số toàn cục. Hãy thử hình dung trong đầu về cách thức mà Node nhìn nhận cái đoạn mã ban đầu như một cấu trúc hàm khép kín (đây chỉ là một hình ảnh mang tính minh họa, không phải cấu trúc thực tế). Bước tiếp theo, Node về cơ bản sẽ tự mình kích hoạt lời gọi đến cái hàm ẩn danh khổng lồ vừa được cấy ghép đó để thực sự bấm nút chạy cái module của bạn. Nhìn vào cấu trúc này, bạn có thể dễ dàng giác ngộ ra lý do tại sao các định danh khai báo ở tầng ngoài cùng hoàn toàn không phải là biến toàn cục, mà thực chất chúng đã bị giáng cấp xuống thành các biến số được khai báo nội bộ bên trong không gian phạm vi của module. Như đã phân tích, Node nhồi nhét thêm vào môi trường một tá những thứ mang mác toàn cục như require(), nhưng bản chất thật sự của chúng không hề là những định danh trôi nổi trong không gian phạm vi toàn cục (và cũng không phải là thuộc tính bám trên đối tượng toàn cục). Chúng giống như những đặc ân được tiêm thẳng vào không gian phạm vi của mỗi module, đóng vai trò tương tự như những tham số đầu vào được liệt kê trong cái phần khai báo của cái hàm ẩn danh khổng lồ kia.
Vậy thì, con đường nào để bạn có thể thực sự định nghĩa ra những biến số toàn cục hàng thật giá thật trong môi trường Node? Lối thoát hiểm duy nhất là bạn phải cấy ghép thêm các thuộc tính vào một kẻ mang mác toàn cục khác do chính Node tự động cung cấp, một kẻ mang cái tên đầy mỉa mai là global. global chính là một sợi dây tham chiếu trỏ thẳng đến cái đối tượng không gian phạm vi toàn cục thực thụ, cách thức hoạt động của nó có nét tương đồng với việc bạn sử dụng từ khóa window trong môi trường JS trên trình duyệt. Thông qua việc bổ sung thêm một thuộc tính vào đối tượng global, và sau đó trong câu lệnh in ra màn hình, bạn đã thành công trong việc truy cập vào biến số đó dưới tư cách là một biến số toàn cục bình thường. Hãy luôn khắc cốt ghi tâm rằng, cái định danh global này hoàn toàn không phải là một sản phẩm được định nghĩa bởi bộ đặc tả JavaScript; nó là một thực thể đặc quyền được nhào nặn và định nghĩa riêng biệt bởi chính môi trường Node.
Giải pháp hợp nhất tham chiếu đối tượng toàn cục
Khi xâu chuỗi lại toàn bộ những phân tích về các môi trường thực thi mà chúng ta đã đem ra mổ xẻ từ đầu đến giờ, một chương trình phần mềm có thể, hoặc không thể: thực hiện khai báo một biến số toàn cục ở không gian phạm vi tầng cao nhất thông qua các từ khóa var, khai báo function—hoặc các từ khóa hiện đại như let, const, và class; đồng thời âm thầm bổ sung thêm các biến số toàn cục đó dưới hình thức là các thuộc tính bám trên đối tượng không gian phạm vi toàn cục, nhưng điều này chỉ xảy ra nếu từ khóa var hoặc khai báo function được sử dụng; và cuối cùng là thao tác tham chiếu đến đối tượng không gian phạm vi toàn cục (nhằm mục đích thêm mới hoặc truy xuất các biến số toàn cục dưới dạng thuộc tính) thông qua các bí danh như window, self, hoặc global. Tôi tin rằng một nhận định hoàn toàn công bằng và khách quan lúc này là: cách thức truy cập cũng như các hành vi vận hành của không gian phạm vi toàn cục thực sự chứa đựng mức độ phức tạp khủng khiếp hơn rất nhiều so với những gì mà đại đa số các nhà phát triển ngây thơ vẫn huyễn hoặc, giống hệt như những gì mà các phần phân tích trước đó đã lột trần. Thế nhưng, sự phức tạp điên rồ đó chưa bao giờ phơi bày một cách trần trụi và tàn nhẫn hơn khi chúng ta phải vật lộn với nhiệm vụ cố gắng chốt hạ một phương thức tham chiếu duy nhất, có khả năng áp dụng phổ quát cho mọi môi trường, để trỏ đến đối tượng không gian phạm vi toàn cục.
Và thế là, lại thêm một cái thủ thuật tà đạo nữa được sinh ra nhằm mục đích chiếm đoạt lấy một sợi dây tham chiếu trỏ đến cái đối tượng không gian phạm vi toàn cục, trông nó có hình hài đại loại như việc tạo mới một hàm sử dụng hàm khởi tạo Function. Tóm tắt lại, chúng ta đang sở hữu trong tay một mớ hỗn độn bao gồm window, self, global, và cái thủ thuật tạo mới hàm xấu xí đến mức kinh tởm kia. Đó là một ma trận gồm quá nhiều những con đường khác biệt nhau chỉ để phục vụ cho một mục đích duy nhất là cố gắng chạm tay vào cái đối tượng toàn cục này. Mỗi con đường lại mang trên mình những ưu điểm và khuyết điểm chí mạng riêng. Vậy thì tại sao chúng ta không tiếp tục đẻ thêm một con đường mới nữa cho thêm phần hỗn loạn!?!? Kể từ cột mốc lịch sử của phiên bản ES2020, ngôn ngữ JavaScript rốt cuộc cũng đã chịu nhượng bộ và định nghĩa ra một chuẩn tham chiếu tiêu chuẩn hóa duy nhất dùng để trỏ đến cái đối tượng không gian phạm vi toàn cục, và nó được ban cho một cái tên là globalThis. Kể từ nay, phụ thuộc vào mức độ cập nhật của các cỗ máy thông dịch đang chạy đoạn mã của bạn, bạn hoàn toàn có thể tự tin sử dụng globalThis như một sự thay thế hoàn hảo cho bất kỳ phương pháp tiếp cận chắp vá nào ở trên. Thậm chí, chúng ta còn có thể tham vọng xây dựng nên một đoạn mã đắp vá đa hình (polyfill) có khả năng hoạt động trơn tru xuyên suốt các môi trường khác nhau, nhằm mang lại sự an toàn tối đa khi phải vận hành trên những cỗ máy cũ kỹ ra đời trước thời đại của globalThis. Chà! Cái đoạn mã đắp vá đó chắc chắn không phải là một tác phẩm nghệ thuật hoàn mỹ, nhưng nó vẫn sẽ hoàn thành xuất sắc nhiệm vụ nếu như bạn lâm vào tình cảnh bắt buộc phải cần đến một sợi dây tham chiếu không gian phạm vi toàn cục đáng tin cậy.
Cái tên gọi globalThis được đề xuất này đã từng châm ngòi cho những cuộc tranh luận nảy lửa và đầy rẫy sự chia rẽ trong suốt khoảng thời gian cái tính năng này đang được rục rịch đưa vào ngôn ngữ. Nói một cách thẳng thắn, cá nhân tôi và vô số những chuyên gia khác đều có chung một cảm giác rằng việc nhét cái tham chiếu this vào trong tên gọi của nó là một quyết định mang tính chất đánh lừa và định hướng sai lệch, bởi vì lý do cốt lõi khiến bạn phải viện đến cái đối tượng này là để có thể xâm nhập vào không gian phạm vi toàn cục, chứ tuyệt đối không bao giờ là để truy cập vào một cái dạng liên kết this mặc định hay toàn cục quái quỷ nào đó. Đã từng có một danh sách dài dằng dặc những cái tên khác được đưa lên bàn cân xem xét, nhưng rồi cuối cùng đều bị gạch bỏ không thương tiếc vì hàng tá những lý do khác nhau. Thật đáng tiếc thay, cái tên gọi chiến thắng cuối cùng lại chỉ là một sự lựa chọn mang tính chất vớt vát, bất đắc dĩ của hội đồng. Nếu bạn nuôi dưỡng ý định sẽ tiến hành tương tác sâu với cái đối tượng không gian phạm vi toàn cục này bên trong các dự án của mình, nhằm mục đích giảm thiểu tối đa sự hoang mang cho người đọc mã, tôi đưa ra một lời khuyên mạnh mẽ rằng bạn nên ưu tiên thiết lập một tên gọi khác mang tính biểu đạt cao hơn, ví dụ như cái tên gọi siêu dài dòng nhưng lại phản ánh chính xác bản chất vấn đề là theGlobalScopeObject.
Kết luận
Không gian phạm vi toàn cục luôn hiện diện và chứng minh được tầm vóc sống còn của mình trong mọi hệ thống chương trình JavaScript, bất chấp một thực tế là các hệ tư tưởng kiến trúc mã nguồn hiện đại thông qua các hệ thống khối đã nỗ lực làm suy giảm tối đa sự phụ thuộc tồi tệ vào việc nhồi nhét các định danh vào cái không gian tên chung chạ này. Mặc dù vậy, khi mã nguồn của chúng ta ngày càng sinh sôi nảy nở và vượt ra khỏi biên giới chật hẹp của trình duyệt, một mệnh lệnh mang tính sống còn là chúng ta bắt buộc phải trang bị cho mình một sự thấu hiểu tường tận về những sự lệch pha, khác biệt tinh vi trong cách thức mà không gian phạm vi toàn cục (và cả cái đối tượng không gian phạm vi toàn cục!) vận hành và phản ứng khi bị ném vào những môi trường thực thi hoàn toàn khác biệt nhau. Với việc bức tranh toàn cảnh vĩ mô về không gian phạm vi toàn cục giờ đây đã được phác họa sắc nét và rõ ràng hơn bao giờ hết, chương tài liệu tiếp theo sẽ một lần nữa kéo chúng ta chìm sâu xuống những tầng kỹ thuật vi mô của phạm vi từ vựng, để tiến hành soi xét bằng kính lúp xem các biến số có thể được triệu hồi và sử dụng như thế nào và vào những thời điểm nào.