Mở đầu
Đến cột mốc này của cuộc hành trình, toàn bộ năng lượng của chúng ta đã được vắt kiệt để mổ xẻ từng đường tơ kẽ tóc của cơ chế phạm vi từ vựng, từ những định nghĩa khô khan cho đến những ứng dụng thực tiễn trong việc tổ chức và thao túng các biến số bên trong một hệ thống phần mềm. Giờ là lúc chúng ta phải dịch chuyển sự tập trung sang một tầm nhìn trừu tượng và bao quát hơn, hướng thẳng đến một chủ đề mang đậm tính lịch sử và thường xuyên gieo rắc sự khiếp đảm cho giới lập trình: hiện tượng bao đóng (closure). Nhưng đừng vội chùn bước! Bạn hoàn toàn không cần phải trang bị cho mình một tấm bằng tiến sĩ khoa học máy tính danh giá để có thể bóc tách và thấu hiểu nó. Sứ mệnh tối thượng của chúng ta trong toàn bộ bộ tài liệu này không chỉ đơn thuần dừng lại ở việc hiểu một cách máy móc về khái niệm phạm vi, mà là làm thế nào để có thể biến nó thành một công cụ quyền lực giúp cấu trúc lại các chương trình của chúng ta một cách ưu việt nhất; và hiện tượng bao đóng chính là trái tim, là động cơ cốt lõi để hiện thực hóa tham vọng đó.
Hãy kích hoạt lại trí nhớ về bài học đắt giá nhất của Chương 6: nguyên lý phơi bày tối thiểu (POLE) chính là kim chỉ nam hối thúc chúng ta phải lạm dụng sức mạnh của không gian phạm vi khối (và cả không gian phạm vi hàm) nhằm mục đích siết chặt và bóp nghẹt sự phơi bày vô tội vạ của các biến số. Triết lý này không chỉ là tấm khiên bảo vệ sự trong sáng và khả năng bảo trì của mã nguồn, mà nó còn là một biện pháp phòng dịch hoàn hảo giúp hệ thống né tránh được vô số những cạm bẫy chết người liên quan đến không gian phạm vi (ví dụ điển hình như thảm họa xung đột định danh, v.v.). Hiện tượng bao đóng thực chất là một sự thăng hoa, một bước tiến hóa tất yếu được xây dựng ngay trên nền tảng của triết lý tiếp cận này: đối với những biến số mang sứ mệnh phải được lưu giữ và sử dụng trải dài theo dòng chảy của thời gian, thay vì cứ vứt phịch chúng ra những không gian phạm vi rộng lớn bao bọc bên ngoài một cách hớ hênh, chúng ta hoàn toàn có thể chọn giải pháp giam cầm (thu hẹp tối đa không gian phạm vi phơi bày của) chúng lại, nhưng song song với đó, nhờ có phép màu của bao đóng, chúng ta vẫn bảo toàn được nguyên vẹn khả năng truy cập vào chúng từ bên trong nội tạng của các hàm, từ đó mở ra vô vàn những cơ hội sử dụng rộng rãi hơn trong tương lai.
Chúng ta thực tế đã từng được chiêm ngưỡng một màn trình diễn xuất sắc của cơ chế bao đóng này ngay ở chương trước (thông qua hàm tính giai thừa ở Chương 6), và tôi dám cá rằng bạn gần như chắc chắn cũng đã từng vô thức sử dụng nó vô số lần trong chính những dự án mã nguồn của mình. Đã bao giờ bạn tự tay nhào nặn ra một hàm gọi lại (callback) có khả năng thọc tay vào và lấy ra những biến số nằm chễm chệ bên ngoài ranh giới không gian phạm vi của chính nó chưa… Bạn đoán xem đó là gì!? Đó đích thị là hiện tượng bao đóng. Hiện tượng bao đóng được xưng tụng là một trong những đặc tính kiến trúc ngôn ngữ vĩ đại và quan trọng bậc nhất từng được loài người phát minh ra trong lịch sử ngành công nghiệp lập trình—nó là nền móng, là trụ cột chống đỡ cho hàng loạt các hệ tư tưởng lập trình khổng lồ, bao gồm Lập trình Chức năng (FP), thiết kế dựa trên hệ thống khối (modules), và thậm chí là nó còn nhúng tay vào một phần của các thiết kế hướng đối tượng (class-oriented design). Việc đạt đến cảnh giới thoải mái làm chủ hiện tượng bao đóng không chỉ là một yêu cầu bắt buộc để bạn có thể vỗ ngực xưng tên là một bậc thầy JS, mà nó còn là chìa khóa vạn năng giúp bạn có thể khai thác triệt để sức mạnh của vô số các khuôn mẫu thiết kế cực kỳ quan trọng trải dài xuyên suốt trong các dự án mã nguồn của mình. Việc giải phẫu toàn diện mọi ngóc ngách và sắc thái của hiện tượng bao đóng đòi hỏi chúng ta phải đương đầu với một khối lượng thảo luận và phân tích mã nguồn đồ sộ đến mức đáng sợ xuyên suốt toàn bộ chương này. Lời khuyên chân thành là bạn hãy thả lỏng, giữ một nhịp độ chậm rãi, và phải chắc chắn một trăm phần trăm rằng mình đã thực sự tiêu hóa trọn vẹn từng mẩu thông tin trước khi đánh liều bước sang phân đoạn tiếp theo.
Bản chất quan sát được của bao đóng
Bao đóng là một khái niệm trừu tượng bắt nguồn từ toán học, nhưng trong lập trình, nó hiện diện thông qua những hành vi cụ thể của các hàm. Việc nhận diện bao đóng thông qua khả năng bảo tồn và truy cập biến số là bước đầu tiên để làm chủ cơ chế này.
Nhận diện bao đóng thông qua vòng đời hàm
Hiện tượng bao đóng, xét về nguồn gốc sâu xa, là một khái niệm toán học thuần túy được thai nghén từ nền tảng của phép tính lambda (lambda calculus). Nhưng bạn có thể thở phào nhẹ nhõm, vì tôi sẽ không tra tấn bạn bằng việc liệt kê ra một mớ các công thức toán học khô khan hay ném vào mặt bạn một đống thuật ngữ chuyên ngành và ký hiệu rườm rà chỉ để định nghĩa nó. Thay vào đó, toàn bộ sự tập trung của tôi sẽ được dồn vào một góc nhìn mang đậm tính thực tiễn và ứng dụng cao. Chúng ta sẽ khởi động bằng việc định nghĩa hiện tượng bao đóng dựa trên những gì mà đôi mắt trần tục của chúng ta có thể trực tiếp quan sát được thông qua những hành vi biến thiên của các chương trình mà chúng ta viết ra, và thử đặt lên bàn cân so sánh với viễn cảnh nếu như hiện tượng bao đóng hoàn toàn bị xóa sổ khỏi JS thì chương trình sẽ ra sao. Tuy nhiên, để mang đến một cái nhìn đa chiều, ở phần sau của chương này, chúng ta sẽ thực hiện một cú lật ngược vấn đề để soi xét hiện tượng bao đóng dưới lăng kính của một hệ quy chiếu hoàn toàn khác biệt.
Hiện tượng bao đóng, xin được khẳng định lại một cách đanh thép, là một đặc quyền hành vi chỉ thuộc về các hàm điện toán và chỉ duy nhất các hàm điện toán mà thôi. Nếu cái thực thể mà bạn đang thao tác không phải là một cái hàm, thì khái niệm bao đóng hoàn toàn vô giá trị và không có đất dụng võ. Một đối tượng dữ liệu hoàn toàn không có cửa để sở hữu bao đóng, và tương tự như vậy, một lớp (class) cũng không hề có khái niệm bao đóng (mặc dù các hàm/phương thức cấu thành nên nó thì có thể). Độc quyền sở hữu bao đóng chỉ thuộc về các hàm. Để hiện tượng bao đóng có cơ hội hiển linh và bị quan sát, một điều kiện tiên quyết là một cái hàm bắt buộc phải được gọi ra để chạy thực thi, và đi sâu hơn nữa vào tiểu tiết, nó bắt buộc phải được gọi ra chạy ở một cái nhánh hoàn toàn xa lạ của chuỗi phạm vi, khác biệt hoàn toàn với cái nơi mà nó được sinh ra và định nghĩa ban đầu. Một cái hàm nếu cứ ngoan ngoãn nằm chạy tịt ở đúng cái không gian phạm vi nơi nó được sinh ra thì sẽ tuyệt đối không bao giờ phô diễn ra bất kỳ một sự khác biệt hành vi nào có thể quan sát được, bất kể việc cái cơ chế bao đóng đó có tồn tại hay không; dựa trên cái định nghĩa thuần túy về mặt quan sát này, cái hành vi ngoan ngoãn đó không được gọi là hiện tượng bao đóng.
Hãy cùng nhau đặt lên bàn mổ một đoạn mã ví dụ, nơi mà các bong bóng không gian phạm vi đã được chúng ta cẩn thận mã hóa bằng những màu sắc đặc trưng (như đã phân tích ở Chương 2). Điểm đầu tiên đập thẳng vào mắt chúng ta khi quan sát cái đoạn mã này là cái hàm bao bọc bên ngoài tên là tra cứu sinh viên đã tiến hành kiến tạo và ngay lập tức nôn ra một cái hàm con nằm bên trong mang tên là chào hỏi sinh viên. Cái hàm tra cứu sinh viên đó đã được hệ thống gọi chạy đến tận hai lần, hệ quả là nó đã đẻ ra hai cái bản sao (instance) hoàn toàn tách biệt của cái hàm con chào hỏi sinh viên nằm bên trong nó, và cả hai cái bản sao vô giá này đều đã được chúng ta cẩn thận cất giữ vào trong cái mảng danh sách sinh viên được chọn. Chúng ta hoàn toàn có thể tự mình kiểm chứng lại chân lý đó bằng cách thọc tay vào và kiểm tra cái thuộc tính tên của cái hàm vừa được trả về đang nằm chễm chệ ở vị trí đầu tiên của cái mảng danh sách sinh viên được chọn, và kết quả rành rành ra đó, nó đích thị là một bản sao của cái hàm nội bộ chào hỏi sinh viên.
Theo logic thông thường, ngay sau khi mỗi một lời gọi đến cái hàm tra cứu sinh viên hoàn thành sứ mệnh của mình, chúng ta đều có xu hướng tin rằng toàn bộ mớ biến số nội bộ của nó sẽ bị hệ thống thẳng tay vứt bỏ và ném vào xe rác để dọn dẹp bộ nhớ (garbage collected). Cái hàm con có vẻ như là thứ tài sản duy nhất may mắn sống sót, được trả về và bảo tồn. Nhưng đây chính là cái bước ngoặt định mệnh nơi mà hành vi của hệ thống bắt đầu rẽ sang một hướng hoàn toàn khác biệt, phô bày ra những hiện tượng mà chúng ta hoàn toàn có thể quan sát bằng mắt thường. Trong khi cái hàm chào hỏi sinh viên thực sự có mở cửa để tiếp nhận một đối số duy nhất thông qua cái tham số mang tên là lời chào, thì nó đồng thời cũng âm thầm vươn cái vòi bạch tuộc của mình ra để tạo ra những sợi dây tham chiếu bám chặt lấy cả cái biến danh sách sinh viên lẫn cái biến mã sinh viên, và cả hai cái định danh này đều có nguồn gốc xuất thân từ cái không gian phạm vi bao bọc bên ngoài của cái hàm tra cứu sinh viên. Mỗi một cái sợi dây tham chiếu vươn ra từ cái hàm nội bộ để bám vào một biến số nằm ở một không gian phạm vi bao bọc bên ngoài như vậy được gọi là một bao đóng. Nếu dùng ngôn từ mang đậm chất hàn lâm học thuật, thì mỗi một cái bản sao của cái hàm chào hỏi sinh viên đã thực hiện một hành vi bao đóng đè lên các biến số nằm ở vòng ngoài là danh sách sinh viên và mã sinh viên.
Vậy thì, những cái bao đóng đó thực chất đang thực thi cái phép thuật gì ở đây, xét trên một khía cạnh hoàn toàn vật lý và có thể quan sát được? Hiện tượng bao đóng đã ban phát cho cái hàm chào hỏi sinh viên một đặc quyền vô song là nó có thể tiếp tục tự do truy cập và thao túng những cái biến số nằm ở vòng ngoài đó ngay cả khi cái không gian phạm vi bao bọc bên ngoài đã chính thức bị khai tử (tức là khi mỗi lời gọi hàm tra cứu sinh viên đã chạy xong). Thay vì bị hệ thống coi như rác rưởi và đem đi tiêu hủy, những cái bản sao của các biến danh sách sinh viên và mã sinh viên đó lại được ban cho sự bất tử, ngoan cường bám trụ lại trong bộ nhớ của hệ thống. Ở một thời điểm nào đó trong tương lai, khi bất kỳ một cái bản sao nào của hàm chào hỏi sinh viên bị gọi ra để chạy, những cái biến số đó vẫn sẽ nằm ngoan ngoãn chễm chệ ở đó, bảo toàn nguyên vẹn những giá trị dữ liệu hiện tại của chúng. Nếu như các hàm trong hệ sinh thái JS không được trang bị thứ vũ khí mang tên bao đóng, thì sự kết thúc của mỗi một lời gọi hàm tra cứu sinh viên sẽ ngay lập tức đồng nghĩa với một bản án tử hình, kéo sập toàn bộ cái không gian phạm vi của nó và dọn dẹp sạch sẽ những cái biến danh sách sinh viên và mã sinh viên vào thùng rác. Và nếu kịch bản đó xảy ra, chuyện gì sẽ đón chờ chúng ta khi chúng ta gọi một trong những cái hàm chào hỏi sinh viên ra chạy sau đó?
Nếu cái hàm chào hỏi sinh viên đó ngây thơ cố gắng thọc tay vào để lấy một thứ mà nó đinh ninh là một viên bi màu xanh dương, nhưng khốn nỗi cái viên bi đó trên thực tế lại hoàn toàn không hề tồn tại (nữa), thì một suy luận hoàn toàn hợp logic là chúng ta chắc chắn sẽ bị hệ thống ném thẳng vào mặt một cái lỗi Lỗi Tham Chiếu, đúng không nào? Thế nhưng, phép màu đã xảy ra: chúng ta hoàn toàn không phải hứng chịu bất kỳ một cái lỗi nào cả. Cái thực tế không thể chối cãi là việc thực thi cái lời gọi hàm nằm ở vị trí đầu tiên của mảng danh sách sinh viên được chọn với tham số Xin chào đã diễn ra cực kỳ trơn tru và trả về cho chúng ta cái thông điệp hoàn hảo Xin chào, Sarah!, điều này là minh chứng đanh thép nhất cho thấy nó vẫn hoàn toàn đủ sức mạnh để truy cập vào các biến danh sách sinh viên và mã sinh viên. Đây chính là một sự quan sát trực tiếp và trần trụi nhất về sự hiện diện của bao đóng!
Sự tinh tế của bao đóng trong hàm mũi tên
Thành thật mà nói, chúng ta đã lướt qua một chi tiết kiến trúc vô cùng nhỏ nhặt nhưng lại cực kỳ vi tế trong cái cuộc thảo luận vừa rồi, và tôi dám cá rằng đại đa số các độc giả đều đã để vuột mất nó! Bởi vì hình hài cú pháp của các hàm mũi tên => được thiết kế quá mức súc tích và gọn gàng, nên chúng ta rất dễ mắc phải một sai lầm chết người là quên béng đi sự thật rằng chúng vẫn có khả năng tự mình đẻ ra một cái không gian phạm vi hoàn toàn mới (như những gì đã được chúng ta dõng dạc tuyên bố trong phần Giải mã sự thật về hàm mũi tên ở Chương 3). Cái cấu trúc hàm mũi tên có nhiệm vụ kiểm tra sinh viên đó thực chất đang âm thầm kiến tạo ra thêm một cái bong bóng không gian phạm vi mới toanh nằm lọt thỏm ngay bên trong lòng của cái không gian phạm vi thuộc về hàm chào hỏi sinh viên.
Tiếp tục bám víu vào cái phép ẩn dụ về những chiếc xô và bong bóng màu sắc từ Chương 2, nếu chúng ta đang phải vẽ ra một cái biểu đồ màu sắc chi tiết cho cái đoạn chương trình này, thì chắc chắn sẽ phải có sự xuất hiện của một cái không gian phạm vi thứ tư nằm tít sâu ở cái tầng lồng ghép trong cùng này, vì vậy chúng ta sẽ buộc phải pha thêm một màu sắc thứ tư; có lẽ màu CAM sẽ là một sự lựa chọn không tồi cho cái không gian phạm vi đó. Sợi dây tham chiếu mã sinh viên màu xanh dương thực tế lại đang nằm chễm chệ bên trong cái không gian phạm vi màu CAM chứ không phải là không gian phạm vi màu xanh lá cây của hàm chào hỏi sinh viên; đi xa hơn nữa, cái tham số sinh viên của cái hàm mũi tên đó cũng bị nhuộm màu CAM, qua đó nó đã thực hiện hành vi che khuất hoàn toàn cái biến sinh viên màu xanh lá cây. Hệ lụy kiến trúc sinh ra từ sự sắp đặt này là cái hàm mũi tên (đóng vai trò như một hàm gọi lại được ném vào cho phương thức tìm kiếm của mảng) bắt buộc phải là kẻ đích thân ôm giữ cái bao đóng đè lên cái biến mã sinh viên, chứ không phải là cái hàm chào hỏi sinh viên ôm giữ nó. Thực ra thì đây cũng chẳng phải là một vấn đề gì quá đỗi to tát hay nghiêm trọng, bởi vì mọi thứ rốt cuộc vẫn vận hành trơn tru như đúng kỳ vọng. Vấn đề mấu chốt ở đây chỉ là một lời nhắc nhở để bạn không bao giờ được phép làm ngơ trước một sự thật rằng: ngay cả những cái hàm mũi tên bé xíu xiu cũng hoàn toàn có quyền được tham gia vào cái bữa tiệc bao đóng hoành tráng này.
Bản chất đa bản sao của bao đóng
Hãy cùng nhau mổ xẻ một ví dụ mang tính kinh điển và thường xuyên được mang ra để làm bằng chứng thép cho khái niệm bao đóng. Mỗi một cái bản sao của cái hàm nội bộ cộng vào đều đang tự mình bao đóng đè lên cái biến số một của riêng nó (mang những giá trị lần lượt là 10 và 42), vì vậy những cái biến số một đó hoàn toàn không hề tan biến vào hư vô chỉ vì cái hàm bao bọc cộng đã chạy xong nhiệm vụ của nó. Tại một thời điểm nào đó trong tương lai, khi chúng ta gọi một trong những cái bản sao của hàm nội bộ cộng vào ra chạy, lấy ví dụ như cái lời gọi cộng 10 vào 15, cái biến số một bị nó bao đóng đè lên vẫn sừng sững tồn tại ở đó và vẫn kiên cường bảo vệ cái giá trị số 10 nguyên thủy của nó. Nhờ đó, cái phép toán này hoàn toàn có đủ dữ kiện để thực hiện thao tác 10 cộng 15 và trả về một đáp án hoàn hảo là 25.
Có một chi tiết kiến trúc vô cùng quan trọng mà có lẽ đã quá dễ dàng bị bạn lướt qua một cách vô thức trong cái đoạn văn vừa rồi, vì vậy chúng ta hãy cùng nhau gõ búa nhấn mạnh lại nó một lần nữa: hiện tượng bao đóng được buộc chặt và gắn liền với một cái bản sao (instance) của một cái hàm, chứ tuyệt đối không phải là nó gắn liền với cái dòng định nghĩa từ vựng duy nhất của cái hàm đó trong mã nguồn. Trong cái đoạn mã ví dụ vừa rồi, về mặt vật lý chỉ có đúng một cái định nghĩa duy nhất cho cái hàm nội bộ cộng vào nằm bên trong cái hàm cộng, vì vậy điều này rất dễ tạo ra một ảo giác đánh lừa tâm trí rằng sẽ chỉ có duy nhất một cái bao đóng được sinh ra. Thế nhưng, sự thật kiến trúc lại phũ phàng hơn thế: mỗi một lần cái hàm bao bọc cộng được hệ thống gọi ra chạy, một cái bản sao mới toanh của cái hàm nội bộ cộng vào sẽ được cỗ máy tự động kiến tạo ra, và đi kèm với mỗi cái bản sao mới toanh đó, một cái bao đóng hoàn toàn mới cũng được khai sinh. Vì vậy, mỗi một cái bản sao của hàm nội bộ (được chúng ta dán nhãn là cộng 10 vào và cộng 42 vào trong chương trình) đều tự hào ôm giữ một cái bao đóng của riêng nó đè lên cái bản sao không gian phạm vi tương ứng được sinh ra từ cái lần chạy đó của hàm cộng. Mặc dù hiện tượng bao đóng được xây dựng và cắm rễ trên nền tảng của phạm vi từ vựng, vốn là một thứ được hệ thống nhào nặn và chốt hạ ngay tại thời gian biên dịch tĩnh, thế nhưng bao đóng lại hiển linh và bị chúng ta quan sát dưới tư cách là một đặc tính hành vi trong thời gian chạy của các bản sao hàm điện toán.
Bao đóng: Liên kết động chứ không phải ảnh chụp tĩnh
Trong cả hai cái ví dụ vừa được lôi ra mổ xẻ ở các phần trước, chúng ta chỉ đơn thuần thực hiện một thao tác là đọc dữ liệu từ một biến số đang bị giam giữ bên trong một bao đóng. Cái hành động thụ động đó vô tình đã gieo rắc vào đầu chúng ta một cảm giác sai lệch rằng bao đóng có thể chỉ là một cơ chế chụp lại (snapshot) giá trị của một biến số tại một khoảnh khắc nào đó trong quá khứ. Và quả thực, đó là một trong những ngộ nhận kinh điển và phổ biến nhất trong giới lập trình viên. Hiện tượng bao đóng thực chất là một sợi dây liên kết động (live link), nó bảo tồn nguyên vẹn quyền truy cập vào toàn bộ bản thân cái biến số vật lý đó. Quyền hạn của chúng ta hoàn toàn không bị gò bó trong cái khuôn khổ tù túng của việc chỉ được phép đọc giá trị; cái biến số đang bị bao đóng đè lên kia hoàn toàn có thể bị mang ra cập nhật (gán lại một giá trị mới) một cách tự do! Bằng cách thực thi hành vi bao đóng đè lên một biến số bên trong một cái hàm, chúng ta có thể tiếp tục tự do thao túng cái biến số đó (cả đọc lẫn ghi) miễn là cái sợi dây tham chiếu trỏ đến cái hàm đó vẫn còn tồn tại bất kỳ nơi nào trong chương trình, và chúng ta có thể thực hiện thao tác đó từ bất kỳ một xó xỉnh nào mà chúng ta muốn gọi cái hàm đó ra chạy. Đây chính là cái gốc rễ giải thích tại sao bao đóng lại là một thứ vũ khí kiến trúc mang sức mạnh hủy diệt và được ứng dụng rộng rãi đến vậy trên khắp các mặt trận của thế giới lập trình!
Biểu đồ đã lột tả một cách chân thực nhất các bản sao của hàm và các sợi dây liên kết không gian phạm vi của chúng. Theo như những gì biểu đồ đã phơi bày, mỗi một lời gọi đến cái hàm cộng đều sẽ tự động đẻ ra một cái không gian phạm vi màu xanh dương mới toanh chứa đựng một cái biến số một bên trong, đồng thời kiến tạo luôn một bản sao mới của cái hàm cộng vào dưới hình hài của một cái không gian phạm vi màu xanh lá cây. Hãy dồn sự chú ý vào việc các bản sao của hàm đó đều đang hiện diện chễm chệ bên trong và được gọi chạy từ cái không gian phạm vi màu đỏ. Bây giờ, chúng ta hãy cùng nhau chuyển hướng lăng kính sang một ví dụ nơi mà cái biến số đang bị bao đóng đè lên thực sự bị mang ra cập nhật và thay đổi giá trị. Cái biến đếm đã bị cái hàm nội bộ lấy hiện tại bao đóng đè lên, nhờ vậy mà nó được bao bọc và giữ lại trong bộ nhớ thay vì bị hệ thống lôi ra xử tử bằng cơ chế dọn dẹp bộ nhớ. Những lời gọi đến hàm đếm vừa thọc tay vào đọc giá trị vừa ngang nhiên cập nhật lại cái biến số này, mỗi lần chạy đều đẻ ra một con số đếm ngày càng phình to hơn. Mặc dù cái không gian phạm vi bao bọc bên ngoài của một cái bao đóng thường có xu hướng là một cái hàm, nhưng đó hoàn toàn không phải là một đạo luật bắt buộc; điều kiện cần và đủ duy nhất chỉ là sự hiện diện của một cái hàm nội bộ nằm lọt thỏm bên trong một cái không gian phạm vi bao bọc nào đó ở bên ngoài.
Tôi đã vô cùng có chủ ý khi lựa chọn cách định nghĩa cái hàm lấy hiện tại dưới dạng một biểu thức hàm thay vì là một khai báo hàm chính thống. Quyết định này hoàn toàn không dính líu gì đến khái niệm bao đóng cả, mà nó thuần túy là một biện pháp phòng vệ để né tránh những cái bẫy nguy hiểm khôn lường của tính năng FiB (đã được cảnh báo ở Chương 6). Chính bởi vì cái thói quen cực kỳ phổ biến của cộng đồng là hay nhìn nhận bao đóng dưới góc độ của những giá trị chết (value-oriented) thay vì dưới góc độ của những biến số sống động (variable-oriented), các nhà phát triển phần mềm thỉnh thoảng lại tự đưa mình vào tròng khi cố gắng lạm dụng bao đóng để chụp lại một bức ảnh lưu giữ giá trị của một biến số tại một khoảnh khắc nào đó trong quá khứ. Hãy cùng mổ xẻ một ví dụ. Bằng cách khai báo cái hàm chào hỏi khi cái biến tên sinh viên đang ôm trong mình cái giá trị Frank (trước khi cái thao tác gán lại giá trị thành Suzy kịp diễn ra), một cái ảo tưởng sai lầm thường xuyên xuất hiện là cái bao đóng đó sẽ chộp lấy và bảo vệ cái chuỗi Frank đó vĩnh viễn. Thế nhưng, cái hàm chào hỏi đó lại đang thực hiện hành vi bao đóng đè lên chính cái biến vật lý tên sinh viên, chứ tuyệt đối không phải là bao đóng lên cái giá trị đang nằm bên trong nó. Bất cứ khi nào cái hàm chào hỏi bị gọi ra chạy, cái giá trị mới nhất của cái biến số đó (trong trường hợp này là Suzy) sẽ lập tức được hệ thống móc ra sử dụng.
Kịch bản kinh điển nhất để bóc trần cái sự ngộ nhận này là việc định nghĩa các hàm nằm lọt thỏm ngay bên trong bụng của một vòng lặp. Bạn có thể đã đinh ninh hy vọng rằng cái lời gọi hàm đầu tiên nằm ở mảng giữ lại sẽ nôn ra số 0, bởi vì cái hàm đó rõ ràng đã được hệ thống đẻ ra ngay trong cái vòng lặp đầu tiên khi mà cái biến i vẫn còn đang mang giá trị là 0. Nhưng xin nhắc lại một lần nữa, cái ảo tưởng đó chỉ là một hệ lụy sinh ra từ việc bạn đang nhìn nhận bao đóng qua lăng kính của những giá trị tĩnh thay vì là những biến số sống động. Một cái ảo giác nào đó tỏa ra từ cấu trúc của vòng lặp for có khả năng đánh lừa bộ não của chúng ta, khiến chúng ta tin rằng mỗi một chu kỳ lặp đều sẽ được hệ thống ban phát cho một cái biến i mới toanh của riêng nó; nhưng sự thật kiến trúc phũ phàng là, cái chương trình này chỉ sở hữu duy nhất một cái biến i mà thôi bởi vì nó đã được khai báo bằng từ khóa kiểu cũ var. Mỗi một cái hàm được cất giữ đó đều thi nhau trả về con số 3, lý do đơn giản là bởi vì khi cái vòng lặp kết thúc vòng đời của nó, cái biến i duy nhất trong chương trình đã bị hệ thống nhét cho con số 3. Mỗi một cái hàm trong số ba cái hàm nằm trong mảng giữ lại đều tự hào sở hữu những cái bao đóng tách biệt của riêng chúng, thế nhưng bi kịch thay, tất cả bọn chúng đều đang cố gắng bao đóng đè lên cùng một cái biến i dùng chung duy nhất đó.
Và dĩ nhiên, một quy luật vật lý không thể chối cãi là một biến số đơn lẻ chỉ có thể chứa chấp duy nhất một giá trị tại bất kỳ một khoảnh khắc thời gian nào. Vì vậy, nếu tham vọng của bạn là muốn bảo tồn nguyên vẹn nhiều giá trị khác biệt nhau, bạn không có con đường nào khác ngoài việc phải chuẩn bị một cái biến số riêng biệt cho từng cái giá trị đó. Vậy thì làm thế quái nào mà chúng ta có thể hiện thực hóa được điều đó trong cái đoạn mã vòng lặp chết tiệt này? Hãy cùng nhau kiến tạo ra một biến số mới toanh cho mỗi một vòng lặp. Mỗi cái hàm giờ đây đã được phép bao đóng đè lên một biến số tách biệt (hoàn toàn mới) sinh ra từ mỗi vòng lặp, bất chấp một sự thật là toàn bộ bọn chúng đều phải dùng chung một cái tên là j. Và mỗi một cái biến j đó đều sẽ được hệ thống bơm cho một cái bản sao của giá trị mà biến i đang sở hữu ngay tại cái thời điểm của vòng lặp đó; và cái biến j đó vĩnh viễn không bao giờ phải chịu chung số phận bị gán lại giá trị. Nhờ đó, cả ba cái hàm giờ đây đều ngoan ngoãn trả về đúng cái giá trị mà chúng ta kỳ vọng: 0, 1, và 2! Hãy khắc sâu vào tâm trí một lần nữa, ngay cả khi chúng ta có nhúng yếu tố bất đồng bộ vào trong cái chương trình này, ví dụ như ném từng cái hàm nội bộ vào trong một hàm độ trễ hay một cái bộ đăng ký sự kiện nào đó, thì cái hành vi bao đóng y hệt như vậy vẫn sẽ diễn ra sờ sờ trước mắt chúng ta.
Hãy kích hoạt lại trí nhớ về phần Vòng lặp ở Chương 5, phần đã lột tả một cách trần trụi về việc làm thế nào mà một câu lệnh khai báo let nằm chễm chệ trong một vòng lặp for thực chất không chỉ sinh ra đúng một cái biến số cho cái vòng lặp đó, mà trên thực tế nó lại đẻ ra hẳn một cái biến số hoàn toàn mới cho mỗi một vòng lặp. Cái thủ thuật/hành vi dị biệt đó chính xác là thứ vũ khí lợi hại nhất mà chúng ta đang khao khát để giải cứu những cái bao đóng vòng lặp của mình. Bởi vì chúng ta đang viện đến sức mạnh của từ khóa let, nên ba cái biến i hoàn toàn tách biệt sẽ được hệ thống khai sinh, mỗi cái biến phụ trách một vòng lặp, thế nên cả ba cái bao đóng kia nghiễm nhiên hoạt động hoàn hảo đúng như những gì chúng ta kỳ vọng.
Những ứng dụng kinh điển: Ajax và Quản lý sự kiện
Hiện tượng bao đóng phô diễn sức mạnh và sự hiện diện của mình một cách dày đặc nhất khi nó bắt tay với các hàm gọi lại (callbacks). Cái hàm gọi lại xử lý bản ghi sẽ được hệ thống triệu hồi để chạy vào một thời điểm xa xăm nào đó trong tương lai, sau khi cái phản hồi từ lời gọi Ajax đã lội ngược dòng trở về. Cái lời gọi này sẽ được kích nổ từ sâu thẳm bên trong bộ máy của cái công cụ Ajax, bất kể cái công cụ đó có nguồn gốc từ đâu. Hơn thế nữa, khi cái khoảnh khắc đó thực sự xảy ra, cái lời gọi hàm tra cứu bản ghi sinh viên bao bọc bên ngoài đã mồ yên mả đẹp từ đời thuở nào rồi. Vậy thì tại sao cái biến mã sinh viên vẫn ngoan cường sống sót và sẵn sàng phơi mình ra cho cái hàm gọi lại truy cập vào? Đáp án chỉ có một: Bao đóng.
Các hàm xử lý sự kiện cũng là một sân khấu quen thuộc nơi bao đóng thường xuyên phô diễn tài năng của mình. Cái tham số nhãn đã bị cái hàm gọi lại xử lý sự kiện click đè đầu cưỡi cổ bằng một cái bao đóng. Khi cái nút bị người dùng nhấn vào, cái biến nhãn vẫn sừng sững ở đó chờ đợi để được gọi tên. Đây chính là quyền năng của bao đóng.
Ranh giới giữa thực tại và khái niệm hàn lâm
Khái niệm bao đóng chỉ mang ý nghĩa thực tiễn khi những hành vi của nó tạo ra tác động có thể quan sát được trong chương trình. Việc phân định rõ ràng giữa sự tồn tại lý thuyết và biểu hiện thực tế giúp chúng ta tập trung vào những khía cạnh có giá trị.
Nếu một cái cây ngã xuống trong một khu rừng vắng lặng và không có bất kỳ một sinh vật nào ở đó để lắng nghe, liệu nó có thực sự tạo ra một tiếng động hay không?
Đó chỉ là một trò chơi chữ triết lý ngớ ngẩn và nhảm nhí. Dĩ nhiên là xét theo một góc độ khoa học vật lý thuần túy, các luồng sóng âm bắt buộc phải được tạo ra. Nhưng câu hỏi sắc bén thực sự ở đây là: liệu có ai thèm quan tâm đến việc cái âm thanh đó có thực sự được phát ra hay không? Hãy luôn khắc cốt ghi tâm rằng, điểm nhấn mang tính sống còn nhất trong cái định nghĩa về bao đóng của chúng ta chính là tính chất có thể quan sát được. Nếu như một cái bao đóng thực sự tồn tại (xét trên một bình diện kỹ thuật, cách thức triển khai, hay một góc độ hàn lâm thuần túy) thế nhưng nó lại vĩnh viễn không bao giờ phơi bày ra bất kỳ một hành vi nào có thể quan sát được trong các chương trình của chúng ta, vậy thì liệu chúng ta có cần phải bận tâm đến sự tồn tại của nó hay không? Chắc chắn là Không. Để củng cố và đập tan mọi nghi ngờ về luận điểm này, chúng ta hãy cùng nhau đặt lên bàn mổ một vài ví dụ nơi mà hành vi của chúng không hề có bất kỳ một sự liên quan nào có thể quan sát được đến hiện tượng bao đóng. Lấy ví dụ, việc gọi ra chạy một cái hàm đang lợi dụng triệt để cơ chế tra cứu của phạm vi từ vựng.
Cái hàm nội bộ đầu ra đã thọc tay vào và truy cập thành công vào các biến số lời chào và tên tôi nằm trong cái không gian phạm vi bao bọc bên ngoài nó. Thế nhưng, sự thật là cái lời gọi để thi hành cái hàm đầu ra đó lại diễn ra ngay bên trong lòng của chính cái không gian phạm vi đó, nơi mà lẽ dĩ nhiên các biến lời chào và tên tôi vẫn còn đang tồn tại và sống nhăn răng; đó thuần túy chỉ là sức mạnh của cơ chế phạm vi từ vựng, chứ tuyệt đối không phải là hiện tượng bao đóng. Bất kỳ một ngôn ngữ lập trình nào tuân thủ cơ chế phạm vi từ vựng mà các hàm của nó hoàn toàn mù tịt và không hỗ trợ hiện tượng bao đóng thì vẫn sẽ phô diễn ra những hành vi y hệt như vậy mà thôi. Thực tế phũ phàng là, các biến số ngự trị ở không gian phạm vi toàn cục về cơ bản là không thể bị bao đóng đè lên (một cách có thể quan sát được), bởi vì đặc quyền của chúng là luôn luôn sẵn sàng phơi mình ra để bị truy cập từ bất kỳ mọi xó xỉnh nào trong chương trình. Vĩnh viễn không bao giờ có chuyện một cái hàm lại có thể bị gọi ra chạy ở một cái phân vùng nào đó của chuỗi phạm vi mà cái phân vùng đó lại không phải là hậu duệ thừa kế của cái không gian phạm vi toàn cục.
Cái hàm nội bộ sinh viên đầu tiên thực sự có vươn một sợi dây tham chiếu trỏ đến cái biến danh sách sinh viên, vốn là một biến số nằm bên ngoài cái không gian phạm vi của riêng nó. Thế nhưng, trớ trêu thay cái biến danh sách sinh viên lại tình cờ có xuất thân từ không gian phạm vi toàn cục, vì vậy bất kể cái hàm đó bị vứt ra chạy ở cái ngóc ngách nào của chương trình đi chăng nữa, thì cái quyền năng truy cập vào danh sách sinh viên của nó cũng chẳng có gì đặc sắc hay ma thuật hơn cái cơ chế phạm vi từ vựng thông thường cả. Toàn bộ mọi lời gọi hàm đều được ban cho cái đặc quyền thọc tay vào các biến toàn cục, bất chấp việc cái ngôn ngữ đó có thèm hỗ trợ cơ chế bao đóng hay là không. Các biến toàn cục sinh ra không phải để bị bao đóng đè lên. Các biến số chỉ đơn thuần hiện diện ở đó nhưng vĩnh viễn không bao giờ bị bất kỳ ai ngó ngàng hay truy cập tới thì tuyệt đối không bao giờ tạo ra được bao đóng.
Cái hàm nội bộ không có ai hoàn toàn không thèm thực hiện hành vi bao đóng đè lên bất kỳ một cái biến vòng ngoài nào cả—nó chỉ khư khư ôm lấy và sử dụng duy nhất cái biến tin nhắn của riêng nó. Mặc dù cái biến mã sinh viên vẫn đang sờ sờ tồn tại ở cái không gian phạm vi bao bọc bên ngoài, nhưng cái biến mã sinh viên đó lại hoàn toàn bị cái hàm không có ai ngó lơ và không hề tạo tham chiếu tới. Cỗ máy JS hoàn toàn không có bất kỳ một lý do hay động lực nào để phải ôm giữ cái biến mã sinh viên lại sau khi cái hàm tra cứu sinh viên đã chạy xong nhiệm vụ của nó, thế nên hệ thống dọn dẹp bộ nhớ (GC) đang rất nóng lòng muốn được dọn dẹp sạch sẽ cái vùng nhớ đó! Bất luận việc các hàm JS có hỗ trợ cơ chế bao đóng hay không, thì cái chương trình này vẫn sẽ vận hành theo đúng một kiểu y hệt như vậy mà thôi. Tóm lại, chẳng có bất kỳ một cái bao đóng nào có thể quan sát được ở đây cả. Nếu như không hề có bất kỳ một lời gọi hàm nào được kích nổ, thì bao đóng cũng vĩnh viễn không bao giờ có thể bị quan sát thấy. Ca này có vẻ hơi căng, bởi vì cái hàm bao bọc bên ngoài rõ ràng là đã bị gọi ra để chạy rồi. Thế nhưng cái hàm nội bộ, kẻ duy nhất có tư cách và khả năng sở hữu bao đóng, lại vĩnh viễn không bao giờ được hệ thống gọi ra chạy; cái hàm được trả về ở đây rốt cuộc chỉ bị đem vứt thẳng vào thùng rác. Vì vậy, cho dù xét về mặt kỹ thuật máy móc cỗ máy JS có thể đã nặn ra một cái bao đóng trong một khoảnh khắc ngắn ngủi ngủi nào đó, thì nó cũng hoàn toàn không thể bị chúng ta quan sát thấy theo bất kỳ một ý nghĩa thực tiễn nào trong cái chương trình này.
Một cái cây có thể đã ngã xuống… nhưng chúng ta chẳng nghe thấy tiếng động nào cả, thế nên chúng ta cũng cóc cần quan tâm.
Định nghĩa dựa trên quan sát thực tiễn
Giờ đây, chúng ta đã được trang bị đầy đủ vũ khí để có thể dõng dạc định nghĩa thế nào là hiện tượng bao đóng:
Hiện tượng bao đóng được quan sát thấy khi một hàm điện toán thọc tay vào và sử dụng (các) biến số có nguồn gốc từ (các) không gian phạm vi bao bọc bên ngoài, ngay cả trong khi cái hàm đó đang chạy thục mạng ở một không gian phạm vi xa lạ nơi mà (các) biến số đó lẽ ra vĩnh viễn không thể nào chạm tới được.
Những mảnh ghép mang tính chất sống còn cấu thành nên cái định nghĩa này là:
– Bắt buộc phải có sự hiện diện và dính líu của một cái hàm.
– Bắt buộc phải vươn dây tham chiếu trỏ đến ít nhất một biến số nằm ở một không gian phạm vi bao bọc bên ngoài.
– Bắt buộc phải bị gọi ra chạy ở một cái nhánh hoàn toàn biệt lập của chuỗi phạm vi so với cái nơi chứa (các) biến số đó.
Cái định nghĩa được xây dựng dựa trên nền tảng của sự quan sát thực tiễn này truyền tải một thông điệp đanh thép rằng chúng ta không bao giờ được phép xem nhẹ hay gạt bỏ hiện tượng bao đóng như thể nó chỉ là một mớ lý thuyết hàn lâm gián tiếp rác rưởi. Trái ngược hoàn toàn, chúng ta bắt buộc phải luôn luôn tìm kiếm và lập kế hoạch đối phó với những hệ lụy vật lý, trực tiếp mà bao đóng có thể giáng xuống đầu những hành vi của chương trình mà chúng ta viết ra.
Vòng đời của bao đóng và Bộ dọn dẹp bộ nhớ (GC)
Bởi vì bản chất của bao đóng là một thực thể bị trói buộc và gắn liền một cách hữu cơ với một bản sao hàm, nên cái bao đóng đè lên một biến số của nó sẽ chỉ kéo dài được sinh mệnh chừng nào mà hệ thống vẫn còn duy trì một sợi dây tham chiếu trỏ đến cái bản sao hàm đó. Nếu như có đến mười cái hàm cùng xúm lại bao đóng đè lên một cái biến số duy nhất, và theo dòng chảy của thời gian, chín cái sợi dây tham chiếu hàm trong số đó đã bị hệ thống thẳng tay vứt bỏ, thì cái sợi dây tham chiếu hàm đơn độc cuối cùng còn sót lại đó vẫn sẽ tiếp tục oằn mình ra để bảo vệ sự sống cho cái biến số đó. Chỉ đến khi cái sợi dây tham chiếu hàm cuối cùng đó cũng bị vứt vào sọt rác, thì cái bao đóng cuối cùng bám víu vào cái biến số đó mới chính thức bốc hơi, và bản thân cái biến số đó mới được bàn giao cho cơ chế GC để đem đi tiêu hủy.
Quy luật này giáng một đòn chí mạng vào việc thiết kế và xây dựng những chương trình phần mềm có khả năng vận hành trơn tru và đạt hiệu năng tối ưu. Hiện tượng bao đóng có thể âm thầm và bất ngờ đứng ra ngăn cản cơ chế GC dọn dẹp một biến số mà đáng lẽ ra bạn đã hoàn thành việc sử dụng nó từ lâu, hệ lụy tàn khốc là nó sẽ dẫn đến một thảm họa rò rỉ và ngốn bộ nhớ mất kiểm soát theo thời gian. Đó chính là lý do sống còn giải thích tại sao bạn bắt buộc phải có ý thức vứt bỏ các sợi dây tham chiếu hàm (và kéo theo đó là bóp chết những cái bao đóng của chúng) khi chúng đã hoàn thành sứ mệnh và không còn cần thiết nữa.
Trong cái chương trình này, cái hàm nội bộ khi nhấn đang ôm khư khư một cái bao đóng đè lên cái biến gọi lại (cái hàm gọi lại sự kiện được truyền vào từ bên ngoài). Điềm báo tồi tệ từ việc đó là các sợi dây tham chiếu của các biểu thức hàm thanh toán và theo dõi hành động sẽ bị hệ thống giam lỏng thông qua cơ chế bao đóng (và vĩnh viễn không thể bị dọn dẹp bởi GC) chừng nào mà những cái bộ đăng ký sự kiện này còn chưa bị gỡ bỏ. Khi chúng ta tung ra một lời gọi không truyền vào bất kỳ một đầu vào nào ở cái dòng lệnh chốt sổ cuối cùng, toàn bộ các bộ đăng ký sự kiện sẽ bị hệ thống tháo gỡ không thương tiếc, và cái mảng chứa các hàm xử lý click cũng sẽ bị dọn sạch bách. Ngay khi toàn bộ các sợi dây tham chiếu của các hàm xử lý click bị ném vào thùng rác, thì những cái bao đóng của các tham chiếu biến gọi lại trỏ đến thanh toán và theo dõi hành động cũng chính thức bị tiêu diệt theo. Khi chúng ta đặt lên bàn cân để đánh giá về tình trạng sức khỏe tổng thể và hiệu năng vận hành của một chương trình, thì việc có ý thức chủ động gỡ bỏ một cái bộ đăng ký sự kiện khi nó đã hết giá trị lợi dụng thậm chí còn mang tầm quan trọng sống còn hơn cả cái hành động đăng ký nó vào lúc ban đầu!
Phân giải theo từng biến số hay theo toàn bộ không gian phạm vi?
Có một câu hỏi hóc búa khác mà chúng ta bắt buộc phải đối mặt và giải quyết: liệu chúng ta nên thiết lập một mô hình tư duy coi bao đóng như một thứ chỉ được áp dụng và dính chặt vào (các) biến số nằm ở vòng ngoài bị tham chiếu tới, hay bao đóng thực chất là một lớp màng bảo vệ ôm trọn và bảo tồn toàn bộ cái chuỗi không gian phạm vi bao gồm tất cả các biến số bên trong nó? Nói một cách cụ thể hơn, trong cái đoạn mã đăng ký sự kiện vừa rồi, liệu cái hàm nội bộ khi nhấn có phải chỉ bao đóng đè lên duy nhất cái biến gọi lại hay không, hay là nó đang tham lam bao đóng đè lên cả hàm xử lý click, mảng các hàm xử lý click, và cả cái biến nút nữa?
Về mặt khái niệm thuần túy, bao đóng là một cơ chế vận hành dựa trên từng cá thể biến số chứ không phải là dựa trên toàn bộ không gian phạm vi. Các hàm gọi lại Ajax, các bộ xử lý sự kiện, và toàn bộ các hình thái đa dạng khác của bao đóng hàm thường được giới học thuật mặc định là chỉ bao đóng đè lên những thứ mà chúng có tạo ra tham chiếu tới một cách tường minh. Thế nhưng, thực tại kiến trúc lại phức tạp và tàn nhẫn hơn thế rất nhiều.
Cái hàm bao bọc bên ngoài quản lý điểm sinh viên mở cửa để tiếp nhận một mảng danh sách các bản ghi sinh viên, và nôn ra một sợi dây tham chiếu trỏ đến hàm thêm điểm, mà chúng ta đã cẩn thận gắn cho nó một cái nhãn ở bên ngoài là thêm điểm tiếp theo. Mỗi một lần chúng ta gọi cái hàm thêm điểm tiếp theo với một con số điểm mới toanh, chúng ta sẽ nhận về một cái danh sách nóng hổi chứa top 10 số điểm cao nhất, đã được hệ thống phân loại theo thứ tự giảm dần (hãy soi kỹ cái hàm sắp xếp và cắt xén danh sách điểm). Kể từ khi lời gọi hàm quản lý điểm sinh viên ban đầu kết thúc sứ mệnh, và đan xen giữa vô số các lần gọi hàm thêm điểm tiếp theo, cái biến danh sách điểm vẫn luôn được bảo tồn nguyên vẹn bên trong nội tạng của cái hàm thêm điểm thông qua sức mạnh của bao đóng; đó chính là cái cơ chế ma thuật giúp duy trì được cái danh sách top điểm luôn được cập nhật liên tục. Hãy luôn khắc cốt ghi tâm rằng, đó là một cái bao đóng đè lên chính cái biến vật lý danh sách điểm, chứ tuyệt đối không phải là đè lên cái mảng dữ liệu mà nó đang chứa đựng. Tuy nhiên, đó hoàn toàn không phải là cái bao đóng duy nhất đang nhúng tay vào cuộc chơi này. Liệu con mắt tinh đời của bạn có phát hiện ra những cái biến số nào khác đang bị bao đóng đè lên hay không?
Bạn có nhìn thấy sự thật là cái hàm thêm điểm đang vươn một sợi dây tham chiếu trỏ đến cái hàm sắp xếp và cắt xén danh sách điểm hay không? Điều đó mang một ý nghĩa sống còn là nó cũng đang thực hiện hành vi bao đóng đè lên cái định danh đó, mà cái định danh đó lại tình cờ ôm giữ một sợi dây tham chiếu trỏ thẳng đến cái hàm sắp xếp và cắt xén danh sách điểm. Cái hàm nội bộ thứ hai đó bắt buộc phải ngoan cường sống sót để cái hàm thêm điểm có thể tiếp tục tự do gọi đến nó, và điều đó cũng đồng nghĩa với việc bất kỳ một biến số nào mà bản thân nó bao đóng đè lên cũng sẽ được hưởng ké sự bất tử—mặc dù trong cái kịch bản cụ thể này thì chẳng có thêm một cái thứ gì khác bị nó bao đóng đè lên cả. Vậy thì còn cái gì khác đang bị bao đóng đè lên nữa?
Hãy dồn sự chú ý vào cái biến lấy điểm (và cái hàm gắn liền với nó); liệu nó có đang bị bao đóng đè lên hay không? Nó chắc chắn đã bị tham chiếu tới ở không gian phạm vi bao bọc bên ngoài của cái hàm quản lý điểm sinh viên trong cái lời gọi phương thức bản đồ (map). Thế nhưng nó lại hoàn toàn vô hình và không hề bị tham chiếu tới bên trong cái hàm thêm điểm hay cái hàm sắp xếp và cắt xén danh sách điểm. Vậy còn cái mảng chứa danh sách các bản ghi sinh viên (có tiềm năng vô cùng khổng lồ) mà chúng ta đã nhét vào dưới dạng tham số bản ghi sinh viên thì sao? Liệu cái biến đó có đang bị bao đóng đè lên hay không? Nếu sự thật là nó bị đè lên, thì cái mảng khổng lồ chứa các bản ghi sinh viên đó sẽ vĩnh viễn không bao giờ được GC dọn dẹp, và hậu quả là cái chương trình này sẽ biến thành một con quái vật nuốt chửng một lượng bộ nhớ lớn hơn rất nhiều so với những gì chúng ta có thể ngây thơ phỏng đoán. Nhưng nếu chúng ta chịu khó vạch lá tìm sâu một lần nữa, thì thực tế là không hề có bất kỳ một cái hàm nội bộ nào thèm vươn dây tham chiếu trỏ đến cái mảng bản ghi sinh viên đó cả.
Dựa trên cái định nghĩa thuần túy về bao đóng trên từng cá thể biến số, bởi vì cái hàm lấy điểm và cái mảng bản ghi sinh viên không hề bị các hàm nội bộ ngó ngàng hay tham chiếu tới, nên chúng đương nhiên không bị bao đóng đè lên. Chúng đáng lẽ ra phải được cấp thẻ tự do để có thể bị GC nuốt chửng ngay tắp lự sau khi lời gọi hàm quản lý điểm sinh viên hoàn thành xong nhiệm vụ của nó. Và quả thực, nếu bạn thử đem đoạn mã này đi gỡ lỗi trong một cỗ máy JS hiện đại, ví dụ như con quái vật v8 trong Chrome, và cắm một cái chốt chặn (breakpoint) vào ngay giữa nội tạng của cái hàm thêm điểm. Bạn có thể sẽ tinh ý nhận ra một điều kỳ lạ là cái bộ thanh tra (inspector) tuyệt đối không hề liệt kê cái biến bản ghi sinh viên ra màn hình. Đó chính là một bằng chứng đanh thép, chí ít là dưới góc độ gỡ lỗi, cho thấy cỗ máy JS hoàn toàn không hề bao bọc hay nuôi dưỡng cái biến bản ghi sinh viên thông qua cơ chế bao đóng. Thật hú hồn! Nhưng liệu cái bằng chứng mắt thấy tai nghe này có thực sự mang lại sự đáng tin cậy tuyệt đối hay không? Hãy cùng mổ xẻ một cái chương trình (khá là khiên cưỡng!) sau đây.
Hãy dồn sự chú ý vào một sự thật là cái hàm nội bộ lấy thông tin hoàn toàn không hề cố tình thực hiện hành vi bao đóng đè lên bất kỳ một biến số nào trong nhóm mã sinh viên, tên, hay điểm một cách tường minh. Vậy mà bằng một phép màu quỷ dị nào đó, những lời gọi đến cái hàm lấy thông tin có vẻ như vẫn hoàn toàn đủ sức mạnh để thọc tay vào và truy cập thành công vào các biến số đó, mặc dù phải thừa nhận rằng nó đã ăn gian bằng cách lạm dụng cái thủ thuật xâm phạm không gian phạm vi từ vựng của hàm eval() (xem Chương 1). Như vậy, rõ ràng là toàn bộ mớ biến số đó đã chắc chắn được hệ thống bảo tồn một cách nguyên vẹn thông qua cơ chế bao đóng, bất chấp một sự thật rành rành là chúng không hề bị cái hàm nội bộ tham chiếu tới một cách tường minh. Vậy thì liệu cái sự thật phũ phàng này có đủ sức lật đổ cái chân lý bao đóng trên từng cá thể biến số để suy tôn cái thuyết bao đóng trên toàn bộ không gian phạm vi hay không? Câu trả lời là Còn tùy.
Đại đa số các cỗ máy JS hiện đại đều được trang bị một cơ chế tối ưu hóa siêu việt nhằm mục đích tự động quét và loại bỏ sạch sẽ mọi biến số không hề bị tham chiếu tới một cách tường minh ra khỏi một không gian phạm vi bao đóng. Tuy nhiên, như những gì chúng ta vừa được chứng kiến với cái hàm ma quỷ eval(), vẫn luôn lẩn khuất những kịch bản chết người mà ở đó cái cơ chế tối ưu hóa kia hoàn toàn bị vô hiệu hóa và bất lực trong việc can thiệp, hệ lụy là cái không gian phạm vi bao đóng vẫn tiếp tục ôm khư khư toàn bộ các biến số nguyên thủy của nó. Nói một cách dễ hiểu hơn, cơ chế bao đóng về mặt bản chất thi hành bên dưới cỗ máy bắt buộc phải là bao đóng trên toàn bộ không gian phạm vi, và sau đó hệ thống sẽ cố gắng áp dụng một cơ chế tối ưu hóa mang tính chất tùy chọn để xén bớt cái không gian phạm vi đó xuống sao cho nó chỉ còn chứa chấp những thứ thực sự đã bị bao đóng đè lên (kết quả cuối cùng thu được có vẻ khá tương đồng với mô hình bao đóng trên từng cá thể biến số). Ngay cả khi lật lại lịch sử chỉ mới vài năm trước đây thôi, vẫn có vô khối các cỗ máy JS không thèm đếm xỉa đến việc tích hợp cái cơ chế tối ưu hóa này; hoàn toàn có một xác suất không nhỏ là các trang web của bạn vẫn đang phải cắn răng chạy trên những cái trình duyệt cổ lỗ sĩ đó, đặc biệt là trên các thiết bị già cỗi hoặc yếu sinh lý. Điều đó mang một ý nghĩa rợn người là hoàn toàn có khả năng những cái bao đóng có tuổi thọ cao ngất ngưởng như các bộ xử lý sự kiện đang âm thầm ngốn và giam giữ một lượng bộ nhớ lớn hơn và dai dẳng hơn rất nhiều so với những gì mà chúng ta có thể ngây thơ phỏng đoán.
Và cái thực tế phũ phàng rằng bản thân nó chỉ là một cơ chế tối ưu hóa mang tính chất tùy chọn ngay từ thuở ban sơ, chứ hoàn toàn không phải là một đạo luật bắt buộc được ghi trong bộ đặc tả, như một hồi chuông cảnh tỉnh chúng ta không bao giờ được phép quá tự tin hay ảo tưởng về khả năng ứng dụng phổ quát của nó. Trong những kịch bản khốc liệt mà một biến số đang phải gồng gánh một khối lượng dữ liệu khổng lồ (ví dụ như một đối tượng hay một mảng béo phì) và cái biến số đó lại vô tình lọt vào một không gian phạm vi bao đóng, nếu như bạn đã vắt kiệt giá trị sử dụng của cái khối dữ liệu đó và không hề có ý định muốn hệ thống tiếp tục giam giữ cái phần bộ nhớ đó nữa, thì một biện pháp phòng vệ an toàn nhất (xét trên khía cạnh quản lý bộ nhớ) là hãy tự tay mình thủ tiêu cái giá trị đó đi thay vì cứ ngoan cố phó mặc số phận cho cái cơ chế tối ưu hóa bao đóng/GC hên xui của hệ thống.
Hãy cùng nhau tiêm một liều thuốc giải vào cái đoạn mã ví dụ quản lý điểm sinh viên lúc nãy để chắc chắn 100% rằng cái mảng dữ liệu có khả năng khổng lồ đang bị kẹt trong cái biến bản ghi sinh viên sẽ không bị cuốn vào một cái không gian phạm vi bao đóng một cách vô lý. Chúng ta hoàn toàn bất lực trong việc cố gắng trục xuất cái biến bản ghi sinh viên ra khỏi cái không gian phạm vi bao đóng; đó là một địa hạt vượt quá tầm kiểm soát của chúng ta. Nhưng thứ chúng ta đang làm là đảm bảo chắc chắn rằng ngay cả khi cái biến bản ghi sinh viên đó có ngoan cố bám trụ lại trong cái không gian phạm vi bao đóng, thì cái biến số đó cũng đã bị cắt đứt hoàn toàn sợi dây tham chiếu trỏ đến cái mảng dữ liệu có khả năng khổng lồ kia; nhờ đó, cái mảng dữ liệu có thể an tâm nhắm mắt xuôi tay và bị GC dọn dẹp sạch sẽ. Xin nhắc lại một lần nữa, trong đại đa số các trường hợp thì cỗ máy JS hoàn toàn đủ thông minh để tự động tối ưu hóa chương trình và đạt được một cái kết cục y hệt như vậy. Thế nhưng, việc tự rèn luyện cho bản thân một thói quen cẩn trọng và luôn chủ động kiểm soát để đảm bảo chắc chắn rằng chúng ta không vô tình trói buộc và giam giữ bất kỳ một khối lượng bộ nhớ đáng kể nào của thiết bị lâu hơn mức cần thiết vẫn luôn là một phẩm chất đáng quý.
Đi sâu hơn một chút vào bản chất sự việc, về mặt kỹ thuật thì chúng ta cũng hoàn toàn không còn bất kỳ lý do gì để phải cần đến cái hàm lấy điểm nữa sau khi cái lời gọi phương thức bản đồ (map) đã hoàn thành xong nhiệm vụ của nó. Nếu như quá trình soi cấu hình (profiling) ứng dụng của chúng ta chỉ điểm rằng đây chính là một cái rốn hút cạn kiệt bộ nhớ mang tính chất chí mạng, thì chúng ta hoàn toàn có khả năng vắt kiệt thêm một chút xíu bộ nhớ nữa bằng cách giải phóng luôn cả cái sợi dây tham chiếu đó để cho cái giá trị của nó không còn bị trói buộc nữa. Mặc dù hành động đó có vẻ như hơi thừa thãi và thái quá trong cái ví dụ mang tính chất đồ chơi này, nhưng đây là một tuyệt kỹ nền tảng mà bạn phải luôn ghim vào đầu nếu như bạn đang phải oằn mình ra tối ưu hóa dung lượng bộ nhớ (memory footprint) cho ứng dụng của mình. Bài học xương máu rút ra ở đây là: điều mang tính chất sống còn là bạn phải luôn luôn nắm rõ như lòng bàn tay xem những cái bao đóng đang lẩn khuất ở những cái xó xỉnh nào trong chương trình của mình, và chính xác là những cái biến số nào đang bị cuốn vào vòng xoáy của chúng. Chúng ta bắt buộc phải quản lý những cái bao đóng này với một sự cẩn trọng tột độ để đảm bảo rằng chúng ta chỉ đang ôm giữ khư khư những thứ thực sự tối thiểu cần thiết nhất và tuyệt đối không bao giờ được phép phung phí tài nguyên bộ nhớ một cách vô tội vạ.
Một hệ quy chiếu hoàn toàn khác biệt
Khái niệm bao đóng không chỉ giới hạn ở việc một hàm di chuyển mang theo môi trường của nó. Việc nhìn nhận bao đóng từ góc độ các tham chiếu giữ cho hàm và môi trường tồn tại tại chỗ sẽ cung cấp một lăng kính thực tế hơn về cơ chế hoạt động bên trong của trình thông dịch.
Nhìn lại cái định nghĩa đã được chúng ta dày công nhào nặn từ đầu đến giờ về bao đóng, có một lời khẳng định đanh thép rằng các hàm điện toán là những giá trị hạng nhất, mang trong mình cái đặc quyền vĩ đại là có thể tự do được vận chuyển và ném qua ném lại khắp mọi ngóc ngách của chương trình, không khác gì một cái giá trị dữ liệu bình thường. Bao đóng chính là cái sợi xích vô hình, cái cầu nối vững chắc dùng để trói chặt cái hàm đó vào với cái không gian phạm vi/những cái biến số nằm ở vòng ngoài của nó, bất chấp việc cái hàm đó có bị lưu đày đến bất kỳ một cái xó xỉnh nào đi chăng nữa. Hãy cùng nhau triệu hồi lại cái đoạn mã ví dụ đã xuất hiện ở phần đầu của chương này, dĩ nhiên là với những cái bong bóng không gian phạm vi màu sắc đã được chú thích cẩn thận. Cái hệ quy chiếu hiện tại mà chúng ta đang sử dụng luôn cố gắng vẽ ra một ảo tưởng rằng dù cho một cái hàm có bị đẩy đi và gọi ra chạy ở bất kỳ một cái tọa độ nào, thì bao đóng vẫn luôn là thứ đứng ra bảo tồn một sợi dây liên kết bí mật trỏ ngược trở về cái không gian phạm vi nguyên thủy ban đầu nhằm mục đích mở đường cho việc truy cập vào các biến số đã bị bao đóng đè lên. Biểu đồ thứ tư, được mang trở lại đây để tiện cho việc quan sát, chính là bức tranh chân thực nhất mô tả lại cái ý niệm đó.
Thế nhưng, vẫn còn tồn tại một con đường tư duy hoàn toàn khác biệt để có thể mổ xẻ hiện tượng bao đóng, và quan trọng hơn cả là đi sâu vào bản chất của cái gọi là các hàm đang bị ném qua ném lại, con đường này có tiềm năng rất lớn trong việc giúp bạn khoét sâu thêm những mô hình tư duy của mình. Cái mô hình mang tính chất lật đổ này cố tình dập tắt đi ánh hào quang của cái triết lý các hàm là những giá trị hạng nhất, và thay vào đó nó dang tay ôm trọn lấy một sự thật kiến trúc rằng các hàm (giống y hệt như toàn bộ những cái giá trị không phải là nguyên thủy khác) thực chất bị cỗ máy JS nắm giữ và thao túng thông qua các sợi dây tham chiếu, và việc gán/truyền chúng đi thực chất chỉ là việc sao chép các sợi dây tham chiếu đó mà thôi—bạn có thể tìm đọc Phụ lục A của cuốn sách Khởi Đầu để bổ sung thêm dưỡng chất cho phần kiến thức này.
Thay vì cứ mãi chìm đắm trong cái ảo tưởng rằng cái bản sao hàm nội bộ cộng vào đang thực sự di chuyển vật lý và xách vali chuyển hộ khẩu ra cái không gian phạm vi màu đỏ vòng ngoài thông qua cái lệnh return và thao tác gán, chúng ta hoàn toàn có thể tự hình dung ra một kịch bản thực tế hơn rất nhiều là các bản sao hàm đó trên thực tế chỉ là đang chôn chân tại chỗ ngay trong chính cái môi trường không gian phạm vi của riêng chúng, và lẽ dĩ nhiên là với cái chuỗi không gian phạm vi của chúng vẫn còn nguyên vẹn không sứt mẻ một ly nào. Cái thứ thực sự bị vận chuyển ra cái không gian phạm vi màu đỏ chỉ đơn thuần là một cái sợi dây tham chiếu trỏ thẳng đến cái bản sao hàm đang nằm bất động tại chỗ kia, chứ tuyệt đối không phải là bản thân cái bản sao hàm đó. Biểu đồ thứ năm sẽ phác họa lại một bức tranh hoàn toàn mới nơi mà các bản sao hàm nội bộ vẫn đang ngoan ngoãn chôn chân tại chỗ, và chúng đang bị chỉ điểm bởi các sợi dây tham chiếu cộng 10 vào và cộng 42 vào màu đỏ tương ứng.
Theo như những gì mà Biểu đồ 5 đã bóc trần, mỗi một lời gọi đến cái hàm cộng vẫn sẽ trung thành với kịch bản đẻ ra một cái không gian phạm vi màu xanh dương mới toanh chứa đựng một cái biến số một bên trong, đồng thời kiến tạo luôn một bản sao không gian phạm vi màu xanh lá cây của cái hàm cộng vào. Nhưng cái điểm tạo nên sự khác biệt kinh thiên động địa so với Biểu đồ 4 là, giờ đây những cái bản sao màu xanh lá cây này vẫn đang kiên quyết bám trụ tại chỗ, nằm nép mình một cách vô cùng tự nhiên bên trong lòng của những cái bản sao không gian phạm vi màu xanh dương của chúng. Những cái sợi dây tham chiếu cộng 10 vào và cộng 42 vào mới chính là những kẻ bị cưỡng chế di lý ra cái không gian phạm vi màu đỏ vòng ngoài, chứ tuyệt đối không phải là bản thân các bản sao hàm. Khi lời gọi cộng 10 vào 15 được phát động, cái bản sao hàm cộng vào (vẫn đang chôn chân tại chỗ trong cái môi trường không gian phạm vi màu xanh dương nguyên thủy của nó) sẽ bị hệ thống gọi ra chạy. Bởi vì bản thân cái bản sao hàm đó chưa từng dịch chuyển đi dù chỉ nửa bước, nên hiển nhiên là nó vẫn nắm trong tay cái đặc quyền tự nhiên để có thể truy cập vào chuỗi không gian phạm vi của mình. Kịch bản y hệt cũng xảy ra đối với cái lời gọi cộng 42 vào 9—chẳng có bất kỳ một cái ma thuật nào chen ngang ở đây cả ngoài cơ chế phạm vi từ vựng thuần túy.
Vậy thì rốt cuộc cái quái gì mới là bao đóng, nếu như nó không phải là cái thứ phép màu ban phát cho một cái hàm cái quyền năng duy trì một sợi dây liên kết trỏ ngược về cái chuỗi không gian phạm vi nguyên thủy của nó ngay cả khi cái hàm đó đang nhảy múa tung tăng ở những cái không gian phạm vi hoàn toàn xa lạ? Trong cái mô hình mang tính chất lật đổ này, các hàm vẫn mãi mãi chôn chân tại chỗ và vẫn tiếp tục bòn rút truy cập vào cái chuỗi không gian phạm vi nguyên thủy của chúng giống y hệt như cái cách mà chúng vẫn luôn có thể làm từ trước đến nay. Bao đóng thay vào đó sẽ chuyển mình thành một thuật ngữ dùng để mô tả cái phép màu duy trì sự sống cho một cái bản sao hàm, song hành cùng với toàn bộ cái môi trường và chuỗi không gian phạm vi của nó, chừng nào mà vẫn còn tồn tại ít nhất là một cái sợi dây tham chiếu trỏ đến cái bản sao hàm đó đang trôi nổi lơ lửng ở bất kỳ một cái xó xỉnh nào khác trong chương trình.
Cái định nghĩa đó về bao đóng có vẻ như thiếu đi tính chất quan sát thực tiễn và mang một âm hưởng hơi lạ tai khi bị đem ra mổ xẻ so sánh với cái hệ quy chiếu hàn lâm truyền thống. Nhưng nó vẫn ẩn chứa những giá trị sử dụng không thể chối cãi, bởi vì lợi ích khổng lồ mà nó mang lại là giúp chúng ta có thể tối giản hóa những lời giải thích rườm rà về bao đóng thành một sự kết hợp vô cùng mộc mạc và dễ hiểu giữa các sợi dây tham chiếu và những cái bản sao hàm đang nằm chôn chân tại chỗ. Cái mô hình trước đó (Biểu đồ 4) tuyệt đối không hề sai lệch khi nỗ lực miêu tả về bao đóng trong hệ sinh thái JS. Nó chỉ đơn thuần là mang nặng tính chất cảm hứng khái niệm hơn, một góc nhìn mang đậm màu sắc hàn lâm về hiện tượng bao đóng. Đặt lên bàn cân so sánh, cái mô hình lật đổ này (Biểu đồ 5) có thể được định danh là mang đậm hơi hướng tập trung vào cơ chế triển khai thực tế hơn, sát với cái cách thức mà cỗ máy JS thực sự đang cày cuốc bên dưới. Cả hai hệ quy chiếu/mô hình này đều phô diễn được những giá trị hữu ích riêng biệt trong việc giúp bạn thấu hiểu bao đóng, nhưng mỗi một độc giả sẽ tự tìm thấy cho mình một cái mô hình mang lại cảm giác dễ tiêu hóa hơn cái còn lại. Dù cho bạn có quyết định đặt cược vào cái mô hình nào đi chăng nữa, thì những cái kết quả có thể quan sát được phơi bày ra trong chương trình của chúng ta vẫn vĩnh viễn là một.
Bao đóng trong thực tiễn
Bao đóng không chỉ là một khái niệm học thuật mà còn là một công cụ mạnh mẽ để tối ưu hóa hiệu suất và tổ chức mã. Nó cho phép các hàm ghi nhớ trạng thái và tạo ra các API chuyên biệt, sạch sẽ hơn thông qua các kỹ thuật như áp dụng một phần.
Bây giờ chúng ta đã được trang bị một cảm quan vô cùng toàn diện và sâu sắc về bản chất của bao đóng là gì và cách thức máy móc vận hành của nó, hãy cùng nhau thực hiện một chuyến du hành để khám phá một vài chiến thuật sử dụng nó nhằm mục đích thăng hạng cho cấu trúc và cách tổ chức mã nguồn của một chương trình ví dụ. Hãy thử tưởng tượng một viễn cảnh bạn đang sở hữu một cái nút bấm chễm chệ trên trang web, và khi người dùng click vào nó, nó sẽ phải thực hiện nhiệm vụ lấy dữ liệu và gửi đi thông qua một yêu cầu Ajax. Nếu bạn kiên quyết từ chối việc sử dụng sức mạnh của bao đóng: Cái công cụ tạo yêu cầu này mở cửa chỉ để đón nhận duy nhất một đối tượng sự kiện từ một sự kiện click. Từ xuất phát điểm đó, nó bắt buộc phải tự mình hì hục đi bới móc cái thuộc tính loại dữ liệu ra khỏi cái phần tử nút bấm là mục tiêu của sự kiện, và sau đó dùng chính cái giá trị đó để đi tra cứu vừa cái đường dẫn URL cho điểm cuối API, vừa cái đống dữ liệu cần phải được đóng gói kèm theo cái yêu cầu Ajax đó.
Cái cách tiếp cận này về cơ bản là vẫn CHẠY ĐƯỢC, thế nhưng nó lại mang đến một cảm giác vô cùng thảm hại (vừa kém hiệu quả, vừa gây lú lẫn) khi mà cái bộ xử lý sự kiện cứ phải nai lưng ra đi đọc lại một cái thuộc tính DOM mỗi khi nó bị hệ thống kích nổ. Tại sao một cái bộ xử lý sự kiện lại không thể được ban cho khả năng ghi nhớ cái giá trị đó luôn cho nhẹ nợ? Hãy cùng nhau thử nghiệm việc bơm bao đóng vào để cứu vãn cái đoạn mã này. Với cái chiến thuật sử dụng hàm cài đặt xử lý nút, cái thuộc tính loại dữ liệu sẽ chỉ bị bắt ép phải lấy ra đúng một lần duy nhất và ngay lập tức được gán luôn vào cho biến loại bản ghi ngay tại cái thời điểm cài đặt ban đầu. Sau đó, cái biến loại bản ghi này sẽ bị cái hàm nội bộ xử lý click tạo yêu cầu nằm bên trong bao đóng đè lên, và cái giá trị của nó sẽ nghiễm nhiên được tái sử dụng trong mỗi lần sự kiện bị kích nổ nhằm mục đích đi tra cứu cái đường dẫn URL và dữ liệu cần phải được gửi đi.
Bằng cách giam cầm cái biến loại bản ghi vào bên trong nội tạng của cái hàm cài đặt xử lý nút, chúng ta đã thành công trong việc bóp nghẹt sự phơi bày không gian phạm vi của cái biến số đó xuống một phạm vi tập con hẹp hơn và phù hợp hơn rất nhiều đối với toàn bộ chương trình; nếu như chúng ta dại dột vứt nó nằm chỏng chơ ở không gian toàn cục thì đó sẽ là một thảm họa tồi tệ đối với việc tổ chức mã nguồn và khả năng đọc hiểu. Bao đóng chính là vị cứu tinh đã ban phát cho cái bản sao hàm tạo yêu cầu nội bộ khả năng ghi nhớ cái biến số này và cho phép nó tự do truy cập bất cứ khi nào nó cần đến. Dựa trên cái khuôn mẫu thiết kế này, đáng lẽ ra chúng ta hoàn toàn có thể tối ưu hóa thêm một bước nữa bằng cách tiến hành tra cứu luôn cả cái đường dẫn URL lẫn dữ liệu chỉ trong một lần duy nhất, ngay tại cái khâu cài đặt. Giờ đây, cái hàm tạo yêu cầu đã tiến hành bao đóng đè lên cả đường dẫn yêu cầu lẫn dữ liệu yêu cầu, một cách tiếp cận mang lại sự trong sáng hơn rất nhiều cho việc đọc hiểu, và đồng thời cũng mang lại một cú hích nho nhỏ nhưng đáng giá về mặt hiệu năng.
Hai cái tuyệt kỹ kiến trúc có nét tương đồng nhau được vay mượn từ hệ tư tưởng Lập trình Chức năng (FP) và sống bám vào sức mạnh của bao đóng chính là áp dụng một phần (partial application) và currying. Nói một cách ngắn gọn, với những tuyệt kỹ này, chúng ta sẽ thực hiện một cuộc phẫu thuật làm biến dạng cái hình hài của các hàm vốn dĩ đang đòi hỏi phải được mớm cho nhiều đầu vào, sao cho một số đầu vào sẽ được chúng ta nhét sẵn vào từ trước, và những cái đầu vào còn lại sẽ được bổ sung sau; những cái đầu vào ban đầu đó sẽ được hệ thống cẩn thận ghi nhớ lại thông qua phép màu của bao đóng. Một khi toàn bộ các đầu vào đã được hội tụ đầy đủ, thì cái hành động cốt lõi bên dưới mới thực sự được bóp cò thi hành. Bằng cách tự tay kiến tạo ra một cái bản sao hàm mang trong mình khả năng nuốt chửng và che giấu một lượng thông tin nhất định ở bên trong bụng của nó (thông qua bao đóng), thì cái hàm-đã-được-bơm-sẵn-thông-tin đó sau này có thể được triệu hồi ra và mang vào sử dụng một cách trực tiếp mà hoàn toàn không cần phải lặp lại cái thao tác mớm lại cái đầu vào đó thêm một lần nào nữa. Điều này không những giúp thanh lọc và làm sạch sẽ cái phân vùng đó của đoạn mã, mà nó còn mở ra một cơ hội vàng để chúng ta có thể dán cho những cái hàm được áp dụng một phần này những cái tên ngữ nghĩa tuyệt vời hơn rất nhiều. Nhờ vào việc chuyển thể kỹ thuật áp dụng một phần, chúng ta hoàn toàn có thể tiếp tục nâng cấp cái đoạn mã lúc nãy lên một tầm cao mới.
Những cái đầu vào đường dẫn yêu cầu và dữ liệu yêu cầu đã được chúng ta mớm sẵn từ trước, thành quả thu được là một cái hàm áp dụng một phần mang tên tạo yêu cầu, mà chúng ta đã dán cho nó một cái nhãn cục bộ là trình xử lý. Khi sự kiện rốt cuộc cũng bị kích nổ, cái đầu vào chốt sổ cuối cùng (chính là đối tượng sự kiện, bất chấp việc nó có bị ngó lơ đi chăng nữa) sẽ được ném vào cho cái trình xử lý, qua đó hoàn thiện bộ sưu tập các đầu vào của nó và chính thức bóp cò kích nổ cái yêu cầu Ajax nằm ẩn bên dưới. Xét trên khía cạnh hành vi, cái chương trình này phô diễn sự tương đồng đến kinh ngạc so với cái chương trình trước đó, với cùng một cái khuôn mẫu bao đóng y hệt. Thế nhưng, bằng cách cô lập cái công đoạn nhào nặn ra hàm tạo yêu cầu vào hẳn một cái công cụ tiện ích riêng biệt (hàm định nghĩa trình xử lý), chúng ta đã phù phép để biến cái định nghĩa đó trở thành một thứ tài sản có khả năng tái sử dụng cao hơn rất nhiều xuyên suốt toàn bộ chiều dài chương trình. Thêm vào đó, chúng ta cũng đã cực kỳ chủ động và minh bạch trong việc bóp nghẹt cái không gian phạm vi bao đóng sao cho nó chỉ chứa chấp duy nhất đúng hai cái biến số thực sự cần thiết mà thôi.
Kết luận
Khi chúng ta chuẩn bị kéo sập cánh cửa để khép lại một chương tài liệu chứa đựng mật độ kiến thức dày đặc đến ngạt thở, lời khuyên là hãy hít thở thật sâu và để cho toàn bộ những tinh hoa kiến trúc này từ từ thẩm thấu vào tâm trí. Nghiêm túc mà nói, đó thực sự là một khối lượng thông tin khổng lồ và quá tải để bất kỳ ai có thể dễ dàng nhai nuốt! Chúng ta đã thực hiện một chuyến lặn sâu để khám phá hai cái mô hình tư duy khác biệt nhằm vật lộn và thuần phục khái niệm bao đóng:
– Mô hình quan sát thực tiễn: bao đóng được nhìn nhận là một cái bản sao hàm có khả năng ghi nhớ như in những cái biến số vòng ngoài của nó ngay cả khi cái hàm đó đang bị ném qua ném lại và được gọi ra chạy ở những cái không gian phạm vi hoàn toàn xa lạ.
– Mô hình cơ chế triển khai: bao đóng thực chất là một bản sao hàm và toàn bộ cái môi trường không gian phạm vi của nó đang được hệ thống bảo tồn nguyên vẹn và chôn chân tại chỗ trong khi mọi cái sợi dây tham chiếu trỏ đến nó lại bị ném qua ném lại và được gọi ra chạy từ những cái không gian phạm vi hoàn toàn xa lạ.
Tổng hợp lại những giá trị cốt lõi mà bao đóng mang lại cho các chương trình của chúng ta:
– Bao đóng là một vũ khí hạng nặng giúp kích thích hiệu năng bằng cách ban phát cho một bản sao hàm cái khả năng ghi nhớ lại những thông tin đã được hệ thống dày công tính toán từ trước thay vì bắt nó phải nai lưng ra hì hục tính toán lại từ đầu trong mỗi lần gọi.
– Bao đóng là một giải pháp hoàn hảo để thanh lọc và nâng cấp khả năng đọc hiểu của mã nguồn, bằng cách siết chặt và bóp nghẹt ranh giới phơi bày không gian phạm vi thông qua việc giam cầm (các) biến số vào bên trong nội tạng của các bản sao hàm, nhưng cùng lúc đó vẫn đảm bảo chắc chắn 100% rằng những thông tin vô giá nằm bên trong những biến số đó vẫn luôn sẵn sàng phơi mình ra để được truy cập và phục vụ cho những mục đích sử dụng trong tương lai. Những bản sao hàm thu được với đặc tính chuyên biệt hóa cao và không gian hoạt động bị thu hẹp này sẽ mang lại một cảm giác tương tác sạch sẽ và trong sáng hơn rất nhiều, bởi vì những thông tin đã được bảo tồn cẩn thận kia không còn phải bị bắt ép nhồi nhét vào dưới dạng tham số trong mỗi lần hàm bị gọi ra chạy nữa.
Trước khi bạn quyết định nhổ neo và tiến bước xa hơn, hãy dành ra một khoảng thời gian tĩnh lặng để tự mình diễn đạt lại toàn bộ những cái kết luận tổng kết này bằng chính ngôn từ của bạn, cố gắng giải thích một cách thật gãy gọn xem rốt cuộc bao đóng là cái quái gì và tại sao nó lại là một vị cứu tinh vĩ đại đến vậy cho những cái chương trình do bạn viết ra. Toàn bộ khối lượng văn bản chính của bộ tài liệu này sẽ được khép lại bằng một chương cuối cùng, nơi mà chúng ta sẽ tiếp tục xây dựng và thăng hoa trên chính cái nền tảng vững chắc của bao đóng thông qua khuôn mẫu thiết kế hệ thống khối (module pattern).