Mở đầu
Trong những chương học khởi động, chúng ta đã tiến hành xác lập một định nghĩa vững chắc, bằng bê tông cốt thép, về khái niệm phạm vi từ vựng cùng với tất cả các thành tố cấu thành nên nó, đồng thời đưa ra những phép ẩn dụ sinh động nhằm củng cố nền tảng nhận thức cho khái niệm này. Trước khi dấn thân sâu hơn vào chương tài liệu hiện tại, một bài tập mang tính bắt buộc là bạn hãy tìm kiếm một ai đó và cố gắng giải thích lại cho họ nghe (có thể thông qua văn bản hoặc nói thành lời), bằng chính ngôn từ của bạn, xem phạm vi từ vựng thực chất là cái quái gì và tại sao việc thấu hiểu nó lại mang lại những giá trị to lớn đến vậy. Yêu cầu này thoạt nghe có vẻ như một thủ tục rườm rà mà bạn có thể dễ dàng bỏ qua, nhưng kinh nghiệm thực tiễn đã chứng minh rằng, việc chủ động dành thời gian để nhào nặn lại những ý tưởng trừu tượng này và chuyển hóa chúng thành những lời giải thích dễ hiểu cho người khác là một phương pháp cực kỳ hiệu quả. Quá trình ép buộc não bộ phải hệ thống hóa thông tin đó sẽ giúp chúng ta thực sự tiêu hóa và hấp thụ triệt để những gì đang được học! Giờ đây, đã đến lúc chúng ta gác lại những khái niệm bay bổng để thực sự xắn tay áo lên và lao vào mổ xẻ những cơ chế bánh răng phức tạp nhất, vì vậy hãy chuẩn bị tâm lý đón nhận một khối lượng chi tiết kỹ thuật dày đặc và chuyên sâu hơn gấp bội kể từ thời điểm này trở đi. Tuy nhiên, hãy kiên trì bám trụ, bởi vì những cuộc thảo luận khốc liệt sắp tới sẽ đóng vai trò như một nhát búa tạ giáng xuống, đập tan mọi sự tự mãn và chỉ ra một sự thật phũ phàng: tất cả chúng ta, bất kể kinh nghiệm, thực chất vẫn còn thiếu hiểu biết đến nhường nào về khái niệm phạm vi. Hãy nhớ luôn giữ một nhịp độ chậm rãi, từ tốn khi nhai nuốt từng dòng văn bản và từng đoạn mã mẫu được cung cấp. Để làm mới lại bối cảnh của ví dụ xuyên suốt, chúng ta hãy cùng nhau hồi tưởng lại bức tranh minh họa được mã hóa bằng màu sắc về các bong bóng phạm vi lồng ghép vào nhau đã được giới thiệu trong Chương 2. Mạng lưới các điểm kết nối chằng chịt giữa các không gian phạm vi bị lồng ghép sâu bên trong những không gian phạm vi khác được giới khoa học máy tính định danh là chuỗi phạm vi, và chính mạng lưới này sẽ nắm quyền sinh sát trong việc vẽ ra con đường cho phép các biến số được truy cập. Một đặc tính cốt lõi của chuỗi này là nó có tính định hướng duy nhất, điều đó có nghĩa là luồng tra cứu chỉ được phép di chuyển theo một chiều duy nhất: hướng lên trên và lan tỏa ra bên ngoài.
Phân tích bản chất của quá trình tra cứu
Khái niệm tra cứu biến số thường được hình dung như một cuộc lùng sục vật lý trải dài qua nhiều cấp độ không gian. Tuy nhiên, để tối ưu hóa hiệu suất, các công cụ biên dịch hiện đại đã biến quá trình này thành một khái niệm mang nhiều tính biểu tượng hơn là một hành động thực thi tại thời gian chạy. Việc hiểu rõ cơ chế đằng sau sự biểu tượng này giúp chúng ta nhận thức được sự ưu việt của mô hình phạm vi từ vựng.
Vai trò biểu tượng của quá trình tra cứu động
Quay trở lại với bức tranh minh họa các bong bóng màu sắc, hãy chú ý đến màu sắc của tham chiếu biến số danh sách sinh viên nằm bên trong cấu trúc vòng lặp. Dựa vào những cơ sở kỹ thuật nào mà chúng ta có thể kết luận chắc nịch rằng nó là một viên bi ve màu đỏ? Trong chương trước, chúng ta đã phác họa bức tranh về quá trình truy cập một biến số trong thời gian chạy như một hành động tra cứu, nơi mà cỗ máy thực thi bắt buộc phải khởi đầu bằng việc chất vấn người quản lý phạm vi của không gian hiện hành xem có nhận diện được cái định danh đó hay không, và nếu thất bại, nó sẽ phải miệt mài leo ngược lên phía trên, vượt ra bên ngoài thông qua từng mắt xích của chuỗi phạm vi lồng ghép (hướng thẳng về phía phạm vi toàn cục) cho đến khi tìm thấy, hoặc bỏ cuộc hoàn toàn. Nguyên lý là cuộc tra cứu này sẽ lập tức đóng băng và dừng lại ngay tại giây phút nó va phải một khai báo định danh trùng khớp đầu tiên nằm trong bất kỳ chiếc xô phạm vi nào trên đường đi. Áp dụng nguyên lý đó, quá trình tra cứu đã đưa ra phán quyết rằng danh sách sinh viên phải là một viên bi màu đỏ, bởi vì hệ thống đã liên tục trượt dài qua các mắt xích của chuỗi phạm vi mà không hề bắt gặp một biến số nào có tên trùng khớp, cho mãi cho đến khi nó đặt chân đến ranh giới cuối cùng là phạm vi toàn cục màu đỏ. Hoàn toàn tương tự, định danh tham số trong câu lệnh điều kiện cũng được hệ thống phân giải và gán cho thân phận là một viên bi màu xanh dương.
Khái niệm mường tượng về một quá trình tra cứu chạy bằng sức người trong thời gian chạy thực sự phát huy tác dụng tuyệt vời trong việc xây dựng nền tảng tư duy khái niệm, nhưng phũ phàng thay, đó không hẳn là cách thức mà hệ thống máy móc vận hành trong môi trường thực tiễn. Màu sắc của chiếc xô chứa viên bi (hay nói theo ngôn ngữ kỹ thuật là siêu dữ liệu xác định nguồn gốc không gian phạm vi của một biến số) thường được xác định một cách dứt điểm ngay trong quá trình xử lý biên dịch khởi thủy. Bởi vì cấu trúc của phạm vi từ vựng gần như đã được đóng băng và chốt hạ ngay tại thời điểm đó, nên màu sắc của một viên bi ve sẽ duy trì tính bất biến, không bao giờ bị biến đổi hay thao túng bởi bất kỳ một sự kiện nào có khả năng xảy ra muộn hơn trong suốt dòng thời gian thực thi. Chính vì màu sắc của viên bi đã được hệ thống nắm rõ như lòng bàn tay từ lúc biên dịch, và nó sở hữu đặc tính không thể bị thay đổi, khối lượng thông tin quý giá này gần như chắc chắn sẽ được hệ thống cất giữ đi kèm (hoặc chí ít là thiết lập sẵn đường dẫn truy cập) với hồ sơ của từng biến số nằm bên trong cấu trúc cây cú pháp trừu tượng; và khối lượng thông tin đã được tính toán sẵn đó sau đó sẽ được các chỉ thị mã máy cấu thành nên thời gian chạy của chương trình lấy ra sử dụng một cách trực tiếp.
Nói một cách dễ hiểu nhất, cỗ máy thực thi hoàn toàn không cần phải lãng phí tài nguyên để hì hục thực hiện những cuộc tra cứu mù quáng xuyên qua hàng đống không gian phạm vi chỉ để giải mã xem một biến số có xuất xứ từ chiếc xô phạm vi nào. Thông tin đó đã nằm phơi bày sẵn ở đó rồi! Việc loại bỏ hoàn toàn sự phụ thuộc vào các cuộc tra cứu trong thời gian chạy chính là một trong những lợi ích tối ưu hóa hiệu năng mang tính cốt lõi và vĩ đại nhất mà mô hình phạm vi từ vựng mang lại. Môi trường thời gian chạy sẽ hoạt động với một tốc độ khủng khiếp hơn rất nhiều khi không phải đốt cháy chu kỳ CPU vào tất cả những cuộc tra cứu vô bổ này. Tuy nhiên, cần lưu ý là tôi đã thận trọng sử dụng cụm từ thường được xác định ngay trong đoạn trước, khi đề cập đến việc giải mã màu sắc của một viên bi trong lúc biên dịch. Vậy thì, liệu có bất kỳ một lỗ hổng hay kịch bản ngoại lệ nào mà ở đó, hệ thống lại không thể biết trước được màu sắc này trong giai đoạn biên dịch hay không?
Tra cứu trì hoãn đối với các định danh chưa được khai báo
Hãy cùng nhau phân tích một tình huống hóc búa: hệ thống bắt gặp một thao tác tham chiếu đến một biến số, nhưng đen đủi thay, biến số này lại không hề sở hữu bất kỳ một dòng khai báo nào nằm trong toàn bộ các không gian phạm vi có sẵn về mặt từ vựng bên trong cái tệp tin hiện tại – hãy nhớ lại kiến thức từ cuốn sách Khởi đầu, đã khẳng định chắc nịch rằng dưới lăng kính của quá trình biên dịch, mỗi một tệp tin vật lý đều được đối xử như một chương trình điện toán hoàn toàn độc lập và biệt lập. Nếu hệ thống quét qua mà không tìm thấy bất kỳ một khai báo nào, đó không hẳn đã là bản án tử hình báo lỗi ngay lập tức. Có một khả năng hoàn toàn khả thi là một tệp tin (tức là một chương trình) khác đang chực chờ trong môi trường thời gian chạy thực sự đã tiến hành khai báo cái biến số đó, và khai báo đó được đặt oai vệ trong không gian phạm vi toàn cục dùng chung. Vì vậy, phán quyết cuối cùng về việc liệu cái biến số đó có từng được khai báo một cách hợp pháp trong một chiếc xô có thể chạm tới được nào đó hay không, có khả năng cao sẽ buộc phải bị trì hoãn và đẩy quả bóng trách nhiệm sang cho giai đoạn thời gian chạy giải quyết.
Bất kỳ một tham chiếu nào nhắm vào một biến số mà ban đầu được hệ thống kết luận là chưa được khai báo sẽ bị bỏ lại dưới hình hài của một viên bi ve không màu, trắng toát trong suốt quá trình biên dịch của cái tệp tin đó; hệ thống hoàn toàn bất lực trong việc định nghĩa màu sắc cho nó cho đến khi tất cả các tệp tin có liên quan khác đã được biên dịch xong xuôi và môi trường thời gian chạy của toàn bộ ứng dụng chính thức bấm nút khởi động. Cuộc tra cứu bị trì hoãn đó cuối cùng rồi cũng sẽ phải diễn ra và phân giải màu sắc của viên bi, ép nó phải mang màu sắc của bất kỳ không gian phạm vi nào mà biến số đó được tìm thấy (mà khả năng cao nhất thường là không gian phạm vi toàn cục). Thế nhưng, điểm an ủi ở đây là cuộc tra cứu muộn màng này cũng sẽ chỉ được hệ thống kích hoạt nhiều nhất là một lần duy nhất cho mỗi một biến số, bởi vì một khi màu sắc đã được chốt, không còn bất kỳ một thế lực nào trong suốt thời gian chạy có thể thao túng hay thay đổi màu sắc của viên bi đó được nữa. Phần tài liệu về các hệ lụy từ quá trình tra cứu thất bại ở chương trước đã mổ xẻ tường tận về những hậu quả thảm khốc sẽ xảy ra nếu một viên bi vẫn ngoan cố giữ nguyên trạng thái không màu cho đến tận khoảnh khắc mà tham chiếu của nó bị mang ra thi hành trong thời gian chạy.
Cơ chế tạo bóng và những hệ lụy phức tạp
Việc cho phép các biến số trùng tên tồn tại trong cùng một hệ thống là một tính năng mạnh mẽ nhưng cũng đầy rẫy hiểm nguy. Cơ chế tạo bóng giúp giải quyết sự xung đột này bằng cách ưu tiên các phạm vi cục bộ, nhưng đồng thời nó cũng cắt đứt các liên kết tham chiếu đến những biến số bị che khuất, tạo ra những điểm mù kiến trúc mà người lập trình cần phải thao tác cực kỳ cẩn trọng.
Bản chất của việc che khuất định danh
Khái niệm Tạo bóng (Shadowing) thoạt nghe có vẻ nhuốm màu kỳ bí và ẩn chứa một chút mờ ám. Nhưng bạn hoàn toàn có thể trút bỏ sự lo ắng, bởi vì đây là một cơ chế kiến trúc hoàn toàn hợp pháp và minh bạch! Trong ví dụ xuyên suốt mà chúng ta sử dụng cho những chương này, chúng ta đã vô tình sử dụng những tên gọi hoàn toàn khác biệt cho các biến số băng qua các ranh giới phạm vi. Bởi vì mỗi biến số đều kiêu hãnh mang một cái tên độc nhất vô nhị, nên xét theo một khía cạnh nào đó, chương trình vẫn sẽ chạy tốt ngay cả khi tất cả bọn chúng đều bị tống chung vào một chiếc xô duy nhất (ví dụ như chiếc xô màu đỏ). Giá trị thực sự của việc duy trì nhiều chiếc xô phạm vi từ vựng khác biệt chỉ bắt đầu bộc lộ sức mạnh sống còn của nó khi bạn phải đối mặt với tình huống sở hữu hai hoặc nhiều biến số, mỗi biến số nằm rải rác ở những không gian phạm vi khác nhau, nhưng lại chia sẻ chung một cái tên từ vựng y hệt nhau. Một nguyên tắc vật lý của hệ thống là một không gian phạm vi đơn lẻ tuyệt đối không thể chứa chấp hai hoặc nhiều biến số mang cùng một cái tên; mọi nỗ lực tham chiếu trùng lặp như vậy sẽ bị hệ thống tự động gộp chung và đối xử như thể chúng chỉ là một biến số duy nhất. Vì vậy, nếu thuật toán của bạn bắt buộc phải nuôi dưỡng hai hoặc nhiều biến số mang cùng một tên gọi, bạn không còn con đường nào khác ngoài việc phải phân tán chúng ra thành các không gian phạm vi riêng biệt (và thông thường là lồng ghép vào nhau). Và trong kịch bản khốc liệt đó, cách thức mà các chiếc xô phạm vi khác biệt được sắp đặt và bố trí sẽ đóng vai trò cực kỳ quan trọng.
Hãy cùng phân tích một kịch bản với đoạn mã có chứa biến lưu trữ tên sinh viên ở cả phạm vi toàn cục và phạm vi hàm. Biến số nằm ở dòng đầu tiên của chương trình sẽ khai sinh ra một viên bi màu đỏ. Vẫn chính là cái biến số mang tên gọi đó, nhưng lại được hệ thống ghi nhận là một viên bi màu xanh dương ở dòng thứ ba, đóng vai trò là một tham số đầu vào nằm trong phần định nghĩa của hàm in tên sinh viên. Vậy, viên bi đó sẽ mang màu sắc gì khi xuất hiện trong câu lệnh gán biến đổi thành chữ in hoa và câu lệnh in ra màn hình? Câu trả lời là cả ba tham chiếu này đều sẽ khoác lên mình màu xanh dương. Quay trở lại với khái niệm tưởng tượng về cuộc tra cứu, chúng ta đã thiết lập một quy tắc rằng cuộc truy lùng sẽ luôn khởi hành từ không gian phạm vi hiện hành và nhích dần ra phía ngoài hoặc lên trên, và sẽ lập tức đạp phanh dừng lại ngay khi tóm được một biến số có tên gọi trùng khớp. Viên bi màu xanh dương đã bị hệ thống tóm gọn ngay lập tức trong tích tắc. Viên bi màu đỏ tội nghiệp vĩnh viễn không bao giờ có cơ hội được hệ thống ngó ngàng hay xem xét tới. Hành vi này chính là một đặc tính kiến trúc mang tính sống còn của mô hình phạm vi từ vựng, được vinh danh dưới tên gọi tạo bóng.
Trong tình huống này, biến số màu xanh dương (đóng vai trò là tham số) đã vươn lên che bóng hoàn toàn biến số màu đỏ. Nói một cách rành mạch hơn, cái tham số đó đang thực hiện hành vi tạo bóng đè lên trên cái biến số toàn cục (vốn đang bị che bóng). Hãy tự mình lẩm nhẩm lại câu thần chú này vài lần để đảm bảo rằng bạn đã thấm nhuần và nắm vững hệ thống thuật ngữ! Đó chính là nguyên nhân gốc rễ giải thích tại sao thao tác gán lại giá trị chỉ gây ra tác động vật lý đối với cái biến số nội bộ (tham số) màu xanh dương, chứ hoàn toàn vô hại đối với cái biến số toàn cục màu đỏ. Khi bạn đưa ra một quyết định mang tính chiến lược là tạo bóng đè lên một biến số thuộc về một không gian phạm vi bên ngoài, một hệ lụy trực tiếp và nhãn tiền là kể từ cái không gian phạm vi đó trở đi, đâm sâu vào bên trong (xuyên qua bất kỳ không gian phạm vi lồng ghép nào), hệ thống vĩnh viễn không cho phép bất kỳ một viên bi nào có cơ hội được tô màu giống như cái biến số đang bị che bóng kia nữa (trong trường hợp này là màu đỏ). Hay nói một cách khác, bất kỳ một tham chiếu định danh nào mang tên gọi đó đều sẽ bị hệ thống ép buộc phải ánh xạ tới cái biến số tham số kia, chứ không bao giờ có cửa trỏ về cái biến số toàn cục. Xét về mặt từ vựng học, việc cố gắng thiết lập một tham chiếu trỏ về cái biến số toàn cục từ bất kỳ vị trí nào nằm sâu bên trong cái hàm (hoặc từ bất kỳ không gian phạm vi lồng ghép nào của nó) là một nhiệm vụ bất khả thi.
Thủ thuật xuyên thủng ranh giới tạo bóng toàn cục
Xin hãy khắc cốt ghi tâm một lời cảnh báo khẩn cấp: việc lạm dụng cái thủ thuật mà tôi sắp sửa phơi bày ra đây là một hành vi thực hành vô cùng tồi tệ, bởi vì tính ứng dụng của nó cực kỳ hạn hẹp, nó gieo rắc sự hoang mang tột độ cho bất kỳ ai phải đọc đoạn mã của bạn, và nó giống như một lời mời gọi hấp dẫn thu hút vô số lỗi hỏng hóc xâm nhập vào chương trình. Lý do duy nhất khiến tôi phải miễn cưỡng đề cập đến nó là để trang bị cho bạn một lớp áo giáp phòng ngự, phòng trường hợp bạn vô tình va chạm với cái hành vi quái gở này trong những cơ sở mã nguồn kế thừa, và việc thấu hiểu cơ chế hoạt động của nó là chiếc chìa khóa duy nhất để bạn không bị sập bẫy. Sự thật là, về mặt kỹ thuật, việc thò tay truy cập vào một biến số toàn cục từ một không gian phạm vi mà ở đó cái biến số toàn cục kia đã bị che khuất hoàn toàn là có khả năng thực hiện được, nhưng chắc chắn không phải bằng phương thức tham chiếu định danh từ vựng thông thường. Trong lãnh địa của không gian phạm vi toàn cục (màu đỏ), các khai báo sử dụng từ khóa var và các khai báo hàm cũng đồng thời phô bày cơ thể của chúng dưới hình hài của các thuộc tính (mang tên gọi y hệt như cái định danh của chúng) nằm bám trên một thực thể được gọi là đối tượng toàn cục—về bản chất, đây là một hình bóng phản chiếu dưới dạng đối tượng của toàn bộ không gian phạm vi toàn cục.
Nếu bạn đã từng có kinh nghiệm cày cuốc với JavaScript trong môi trường trình duyệt web, khả năng cao là bạn đã từng nghe danh về cái đối tượng toàn cục này dưới cái tên window. Cách gọi đó không hoàn toàn chính xác tuyệt đối về mặt học thuật, nhưng nó đủ tốt để phục vụ cho cuộc thảo luận hiện tại của chúng ta. Hãy cùng phân tích một đoạn chương trình, đặc biệt khi nó được ném vào chạy dưới tư cách là một tệp tin độc lập bên trong môi trường trình duyệt. Bạn có để ý thấy sự xuất hiện của tham chiếu window chấm tên sinh viên không? Biểu thức quái dị này đang thực hiện hành vi truy xuất trực tiếp vào cái biến số toàn cục thông qua con đường cửa sau, bằng cách coi nó như một thuộc tính nằm trên đối tượng window (mà tạm thời chúng ta đang giả định nó đồng nghĩa với đối tượng toàn cục). Đó chính là lối thoát hiểm duy nhất để bạn có thể chạm tay vào một biến số đã bị che khuất từ bên trong một không gian phạm vi nơi mà cái biến số che khuất đang chễm chệ hiện diện. Tham chiếu thuộc tính này thực chất là một tấm gương phản chiếu trực tiếp cái biến số toàn cục, chứ tuyệt đối không phải là một bản sao chép bị chia cắt. Mọi hành động sửa đổi dữ liệu từ một phía đều sẽ ngay lập tức bị phía bên kia nhìn thấy, bất kể theo chiều hướng nào. Bạn có thể hình dung tham chiếu này như một cặp phương thức lấy/gán dữ liệu chuyên dùng để thọc tay trực tiếp vào cái biến số vật lý đó. Thậm chí, điên rồ hơn nữa, bạn còn có thể ngang nhiên bơm thêm một biến số mới toanh vào không gian phạm vi toàn cục bằng cách khởi tạo hoặc gán giá trị cho một thuộc tính mới trực tiếp trên cái đối tượng toàn cục đó.
Nhưng hãy nhớ lấy lời cảnh báo: chỉ vì bạn có khả năng làm một việc, không đồng nghĩa với việc bạn nên làm nó. Đừng bao giờ dại dột tạo bóng che lấp đi một biến số toàn cục mà bạn thừa biết là mình sẽ cần phải truy cập đến, và ở chiều ngược lại, hãy tránh xa cái thủ thuật bẩn thỉu này khi muốn truy cập một biến số toàn cục mà chính bạn đã tự tay che khuất nó. Và tuyệt đối, đừng bao giờ tra tấn tâm trí của những người đọc mã bằng cách khởi tạo các biến số toàn cục thông qua việc gán thuộc tính cho window thay vì sử dụng các câu lệnh khai báo chính thống minh bạch! Cái thủ thuật nhỏ bé này chỉ phát huy tác dụng duy nhất đối với việc truy cập vào một biến số thuộc không gian phạm vi toàn cục (chứ hoàn toàn vô dụng đối với việc truy cập một biến số bị che khuất thuộc về một không gian phạm vi lồng ghép), và thậm chí ngay cả trong trường hợp đó, nó cũng chỉ áp dụng được cho những biến số được khai báo bằng từ khóa var hoặc khai báo hàm. Bất kỳ một hình thái khai báo nào khác nằm trong không gian phạm vi toàn cục đều tuyệt đối không sinh ra các thuộc tính phản chiếu trên đối tượng toàn cục. Đối với mọi biến số (bất chấp việc chúng được khai báo bằng phương thức nào!) tồn tại ở bất kỳ một không gian phạm vi nào khác ngoại trừ phạm vi toàn cục, một khi chúng đã bị che khuất, chúng sẽ bị giam cầm trong một pháo đài bất khả xâm phạm, vĩnh viễn không thể bị truy xuất từ cái không gian phạm vi đang che khuất chúng.
Ranh giới của việc sao chép tham chiếu
Tôi đã từng phải đối mặt với hàng chục lời chất vấn bắt đầu bằng cụm từ Nhưng còn cái này thì sao…? liên quan đến một kịch bản đánh lừa thị giác. Chà! Vậy liệu cái kỹ thuật sử dụng một đối tượng đóng vai trò trung gian này có đủ sức lật đổ lời khẳng định chắc nịch của tôi rằng cái biến số tham số bị che khuất kia là bất khả xâm phạm từ bên trong hàm nội bộ hay không? Câu trả lời là không, lời khẳng định của tôi vẫn sừng sững như một chân lý. Thao tác gán giá trị vào đối tượng trung gian thực chất chỉ là hành vi sao chép nội dung giá trị của cái biến tham số đó và đổ vào một vật chứa hoàn toàn khác biệt (một thuộc tính mang cùng tên gọi). Dĩ nhiên, nếu bạn đã nhét giá trị vào một vật chứa khác, thì quy luật tạo bóng không còn bất kỳ giá trị áp dụng nào nữa (trừ phi bản thân cái đối tượng trung gian đó cũng bị che khuất nốt!). Thế nhưng, điều đó hoàn toàn không đồng nghĩa với việc chúng ta đang thực sự truy cập vật lý vào chính cái tham số đó; nó chỉ đơn giản là chúng ta đang truy xuất vào một bản sao chép của cái giá trị mà tham số đó đang sở hữu tại khoảnh khắc bị sao chép, thông qua một vật chứa trung gian (thuộc tính của đối tượng). Chúng ta vĩnh viễn bất lực trong việc cố gắng gán lại một giá trị mới cho cái tham số màu xanh dương đó từ bên trong không gian của hàm nội bộ.
Một lời vặn vẹo Nhưng…!? khác mà bạn có thể đang rắp tâm đưa ra: chuyện gì sẽ xảy ra nếu tôi sử dụng các cấu trúc dữ liệu đối tượng hoặc mảng phức tạp làm giá trị thay vì sử dụng các con số nguyên thủy vô hồn? Liệu việc nắm trong tay các sợi dây tham chiếu trỏ đến các đối tượng thay vì chỉ ôm những bản sao của giá trị nguyên thủy có sửa chữa được sự bất lực trong việc truy cập này hay không? Câu trả lời vẫn là một tiếng Không phũ phàng. Việc bạn tiến hành các thao tác đột biến, thay đổi nội tạng bên trong của một giá trị đối tượng thông qua một bản sao của sợi dây tham chiếu hoàn toàn không phải là một hành động tương đương với việc bạn thọc tay truy cập trực tiếp vào bản thân cái biến số đó theo cơ chế từ vựng. Lệnh cấm vẫn được duy trì: chúng ta vĩnh viễn không thể nào gán lại được cái tham số màu xanh dương đó.
Cuối cùng, không phải bất kỳ một sự kết hợp đè bóng nào cũng được hệ thống giang tay chào đón. Từ khóa let hoàn toàn có quyền uy để đè bóng lên từ khóa var, nhưng ở chiều ngược lại, var lại bị tước đoạt quyền lực đè bóng lên let. Hãy chú ý vào hàm thứ hai, câu lệnh khai báo var nằm ở tầng sâu nhất đang manh nha ý định khai báo một biến số phủ trùm toàn bộ không gian hàm, và bản thân ý định đó không có gì là sai trái cả (như đã được chứng minh bởi hàm thứ nhất). Dòng mô tả lỗi cú pháp bị ném ra trong tình huống này chỉ điểm rằng biến số đó đã được định nghĩa từ trước, nhưng thông điệp lỗi đó thực sự mang tính chất lấp liếm và gây nhầm lẫn – xin nhắc lại, không hề có bất kỳ một lỗi nào tương tự xảy ra ở hàm thứ nhất, bởi vì hành vi tạo bóng nhìn chung vẫn luôn được hệ thống dung túng và cho phép qua cửa dễ dàng. Nguyên nhân sâu xa thực sự khiến hệ thống nổi điên và ném ra một Lỗi Cú Pháp là do cái từ khóa var đó về cơ bản đang cố gắng vượt biên trái phép, nhảy chồm qua đầu cái câu lệnh khai báo let mang cùng tên gọi, và đó là một hành vi bị nghiêm cấm tuyệt đối. Lệnh cấm vượt biên trái phép này sẽ tự động mất hiệu lực khi vấp phải các ranh giới của hàm điện toán, do đó, các biến thể lồng ghép thông qua hàm gọi lại sẽ không kích nổ bất kỳ một ngoại lệ nào. Tóm gọn lại bằng một chân lý: từ khóa let (nằm ở không gian phạm vi bên trong) luôn luôn nắm đặc quyền đè bóng lên một khai báo var nằm ở không gian phạm vi bên ngoài. Còn từ khóa var (nằm ở không gian phạm vi bên trong) chỉ được cấp phép đè bóng lên một khai báo let nằm ở không gian phạm vi bên ngoài nếu và chỉ nếu có một ranh giới hàm điện toán vững chắc đứng chắn ở giữa chúng.
Phân tích tác động của các hình thái khai báo hàm
Ngôn ngữ JavaScript cung cấp nhiều cú pháp để khởi tạo một hàm điện toán, và mỗi một hình thái lại mang đến những hệ lụy sâu sắc khác nhau đối với kiến trúc phạm vi. Sự khác biệt giữa khai báo, biểu thức và hàm mũi tên không chỉ nằm ở bề mặt cú pháp, mà nó còn chi phối cách thức định danh hàm được liên kết và cách hệ thống xử lý phạm vi nội bộ của chúng.
Biểu thức hàm và định danh vô hình
Như những gì bạn đã được chứng kiến từ đầu đến giờ, một khai báo hàm thuần túy sẽ mang một hình hài tiêu chuẩn bắt đầu bằng từ khóa, tiếp theo là tên hàm. Và như những phân tích chuyên sâu ở các chương trước đã chỉ ra, một khai báo hàm mang hình hài như vậy sẽ tự động sản sinh ra một biến số định danh nằm chễm chệ trong không gian phạm vi bao bọc bên ngoài nó (trong trường hợp này là không gian phạm vi toàn cục), mang tên gọi trùng khớp với tên hàm. Vậy thì, hệ thống sẽ đối xử ra sao với một chương trình sử dụng cú pháp gán hàm ẩn danh vào một biến số? Chân lý vẫn không hề thay đổi đối với việc cái biến số định danh đó được hệ thống khai sinh. Thế nhưng, bởi vì cấu trúc này thực chất là một biểu thức hàm – tức là một định nghĩa hàm bị giáng cấp xuống chỉ còn đóng vai trò là một giá trị dữ liệu thô thay vì là một câu lệnh khai báo đứng hiên ngang độc lập – nên bản thân cái hàm đó sẽ bị tước đoạt đặc quyền kéo lên.
Một trong những hố sâu ngăn cách khổng lồ nhất giữa trường phái khai báo hàm và trường phái biểu thức hàm nằm ở số phận của cái định danh tên gọi của chính cái hàm đó. Hãy cùng đưa vào tầm ngắm một biểu thức hàm có mang tên gọi đi kèm. Chúng ta đều nắm rõ như lòng bàn tay rằng cái biến số được gán sẽ yên vị ở không gian phạm vi bên ngoài. Nhưng còn cái định danh tên gọi dính liền với hàm thì sao? Đối với các khai báo hàm chính thống, cái định danh tên gọi đó sẽ bị đẩy ra và trôi nổi ở không gian phạm vi bao bọc bên ngoài, vì vậy sẽ là một suy luận hoàn toàn hợp logic nếu ta cho rằng điều tương tự cũng sẽ xảy ra ở đây. Thế nhưng, sự thật lại tát một gáo nước lạnh vào mặt logic: cái định danh tên gọi đó lại bị hệ thống tuyên bố là một định danh tồn tại độc quyền ở không gian bên trong nội tạng của chính cái hàm đó. Không những cái định danh đó bị nhốt chặt ở bên trong thay vì được tự do bên ngoài, mà nó còn bị hệ thống đóng dấu là một tài sản chỉ được phép đọc (read-only). Nếu bạn đang viết mã dưới sự giám sát của chế độ nghiêm ngặt, mọi nỗ lực gán lại giá trị cho nó sẽ bị hệ thống báo cáo thẳng thừng là một Lỗi Kiểu Dữ Liệu; còn nếu bạn đang lẩn khuất ở chế độ thông thường, hành vi gán lén lút đó sẽ thất bại một cách câm lặng mà không hề làm kích nổ bất kỳ một ngoại lệ cảnh báo nào. Vậy hệ thống sẽ hành xử ra sao khi nó phải đối mặt với một biểu thức hàm hoàn toàn trống trơn, không hề có bất kỳ một định danh tên gọi nào? Một biểu thức hàm sở hữu định danh tên gọi sẽ được giới hàn lâm gọi tên là biểu thức hàm có định danh, trong khi đó kẻ không có tên gọi sẽ bị dán nhãn là biểu thức hàm ẩn danh. Và lẽ dĩ nhiên, các biểu thức hàm ẩn danh hoàn toàn không sở hữu bất kỳ một định danh tên gọi nào có đủ khả năng gây ra tác động đến cả hai không gian phạm vi.
Giải mã sự thật về hàm mũi tên
Phiên bản đặc tả thứ sáu đã mang đến một luồng gió mới khi bổ sung thêm một hình thái biểu thức hàm hoàn toàn mới mẻ vào trong cốt lõi ngôn ngữ, mang tên gọi vô cùng tượng hình là hàm mũi tên. Hàm mũi tên sử dụng ký hiệu => không đòi hỏi sự hiện diện của từ khóa function để định nghĩa sự tồn tại của nó. Thêm vào đó, bộ dấu ngoặc đơn bao bọc xung quanh danh sách tham số cũng có thể được lược bỏ trong một vài trường hợp cú pháp đơn giản. Tương tự như vậy, bộ dấu ngoặc nhọn ôm lấy thân hàm cũng trở thành tùy chọn trong một vài ngữ cảnh nhất định. Và khi bộ dấu ngoặc nhọn đó bị tước bỏ, một giá trị dữ liệu sẽ tự động được phóng ra ngoài mà không cần phải viện đến sự trợ giúp của từ khóa return.
Xét về bản chất từ vựng học, các hàm mũi tên là những thực thể ẩn danh tuyệt đối, điều đó đồng nghĩa với việc chúng hoàn toàn không có bất kỳ một định danh nào liên kết trực tiếp trỏ về phía chúng. Thao tác gán hàm vào một biến số có thể giúp hệ thống tự động suy luận ra một cái tên ngầm định, nhưng cái tên ngầm định đó hoàn toàn không mang ý nghĩa tương đương với việc thực thể đó thoát khỏi kiếp ẩn danh. Hàm mũi tên đánh đổi để đạt được sự súc tích về mặt cú pháp bề mặt bằng một cái giá vô cùng đắt đỏ: não bộ của bạn phải liên tục làm xiếc, tung hứng và ghi nhớ một đống các biến thể cú pháp rối rắm phục vụ cho vô số các hình thái và điều kiện khác biệt nhau. Động lực sâu xa duy nhất khiến tôi phải đưa chủ đề hàm mũi tên lên bàn mổ xẻ là để đập tan một lời đồn đại cực kỳ phổ biến nhưng lại sai lệch hoàn toàn: nhiều người lầm tưởng rằng hàm mũi tên bằng một ma thuật nào đó sẽ hành xử khác biệt so với các hàm function tiêu chuẩn khi bị đặt dưới lăng kính của các quy tắc phạm vi từ vựng.
Đó là một sự dối trá về mặt kiến trúc. Bỏ qua sự thật rằng chúng là những thực thể ẩn danh (và không có khả năng khoác lên mình hình hài của một câu lệnh khai báo), các hàm mũi tên vẫn tuân thủ một cách ngoan ngoãn các quy tắc phạm vi từ vựng giống hệt như những gì mà các hàm function truyền thống vẫn làm. Một hàm mũi tên, bất chấp việc nó có được bọc trong bộ dấu ngoặc nhọn hay không, vẫn âm thầm kiến tạo ra một chiếc xô phạm vi lồng ghép, độc lập và tách biệt ở bên trong. Các câu lệnh khai báo biến nằm lọt thỏm bên trong chiếc xô phạm vi lồng ghép này vẫn sẽ hành xử với một sự trung thành tuyệt đối, không có gì khác biệt so với việc chúng đang nằm trong không gian phạm vi của một hàm truyền thống.
Kết luận
Bức tranh toàn cảnh về cách thức các không gian phạm vi móc nối với nhau tạo thành một mạng lưới luân chuyển dữ liệu là cực kỳ phức tạp và chứa đựng nhiều điểm mù. Khi một hàm điện toán (dưới hình thức khai báo hay biểu thức) được định nghĩa, một không gian phạm vi hoàn toàn mới mẻ sẽ được hệ thống kiến tạo nên. Sự bố trí các không gian phạm vi lồng ghép lọt thỏm vào nhau sẽ tự động tạo ra một hệ thống phân cấp phạm vi mang tính tự nhiên lan tỏa khắp toàn bộ chương trình, hệ thống mạng lưới này được gọi là chuỗi phạm vi. Chuỗi phạm vi nắm giữ quyền trượng tối cao trong việc kiểm soát các luồng truy cập biến số, với một định hướng không gian luôn luôn hướng lên trên và lan tỏa ra bên ngoài. Mỗi một không gian phạm vi mới ra đời sẽ cung cấp một vùng đất trống trơn, một không gian lưu trữ chuyên biệt để chứa đựng một bộ sưu tập các biến số của riêng nó. Khi một cái tên biến số vô tình bị lặp lại ở những tầng không gian khác nhau của chuỗi phạm vi, hiện tượng tạo bóng sẽ ngay lập tức xuất hiện, đóng sập cánh cửa và ngăn chặn hoàn toàn mọi nỗ lực truy cập vào cái biến số nằm ở vòng ngoài tính từ điểm xảy ra hiện tượng đó đâm thẳng vào trong. Khi chúng ta lùi lại một bước, thoát khỏi những chi tiết kiến trúc vi mô và rối rắm này, chương tài liệu tiếp theo sẽ điều hướng sự chú ý của chúng ta tập trung vào một không gian phạm vi mang tính chất nguyên thủy mà mọi chương trình JavaScript đều mặc định phải chứa đựng: không gian phạm vi toàn cục.