Trong thế giới lập trình, biến toàn cục (global variables) luôn là một chủ đề gây tranh cãi. Một số lập trình viên coi chúng là công cụ tiện lợi giúp truy cập dữ liệu nhanh chóng, trong khi những người khác lại xem chúng như một ví dụ điển hình của việc viết mã cẩu thả, tiềm ẩn nhiều rủi ro. Vậy biến toàn cục thực sự là gì, chúng mang lại lợi ích gì và tại sao chúng ta cần cực kỳ thận trọng khi sử dụng? Bài viết này của tincongngheso.com sẽ đi sâu phân tích khái niệm này, giúp bạn hiểu rõ bản chất, những vấn đề tiềm tàng và cách tiếp cận hiệu quả nhất để tận dụng chúng một cách an toàn, hoặc tốt hơn là hạn chế sử dụng khi có thể.
Biến Toàn Cục Là Gì?
Về cơ bản, một chương trình máy tính là một danh sách các chỉ thị mà máy tính sẽ thực hiện khi bạn chạy nó. Tuy nhiên, hầu hết các chương trình đủ phức tạp để vượt ra ngoài một danh sách tuần tự đơn giản. Các cấu trúc như khối mã, hàm (functions) và định nghĩa lớp (class definitions) giúp bạn tổ chức mã nguồn để dễ quản lý hơn.
Hàm là công cụ cơ bản nhất mà bạn có thể sử dụng để nhóm mã, cô lập hành vi của nó khỏi phần còn lại của chương trình. Bạn có thể gọi một hàm từ các phần khác của mã và tái sử dụng cùng một logic mà không cần phải lặp lại nhiều lần.
Dưới đây là một ví dụ về một hàm JavaScript sử dụng hai loại biến: một đối số hàm (function argument) và một biến cục bộ (local variable):
function factorial(x) {
let i;
for (i = x - 1; i > 0; i--)
x *= i;
return x;
}
factorial(4); // 24
Đối số hàm, x
, tự động được gán giá trị truyền vào hàm khi nó được gọi. Trong ví dụ trên, nó nhận giá trị 4.
Biến i
được khai báo bằng từ khóa let
. Điều này đảm bảo nó là một biến cục bộ. Ban đầu, giá trị của nó không xác định (undefined), trước khi vòng lặp for
gán và thay đổi giá trị của nó.
Mỗi biến này đều có phạm vi (scope) giới hạn trong hàm factorial
; không có mã nào khác có thể đọc hoặc ghi giá trị của x
hay i
. Bạn có thể chạy JavaScript này trong console của trình duyệt và xác nhận rằng các biến x
và i
không hiển thị bên ngoài hàm:
Kết quả khi cố gắng truy cập biến cục bộ 'x' và 'i' bên ngoài hàm factorial trong console JavaScript
JavaScript cũng hỗ trợ biến toàn cục, có phạm vi truy cập trên toàn bộ chương trình. Dưới đây là một phiên bản thay thế của hàm factorial
yêu cầu bạn đặt một biến toàn cục trước khi gọi nó, thay vì truyền đối số:
function factorial() {
let i;
for (i = x - 1; i > 0; i--)
x *= i;
return x;
}
var x = 4;
factorial(); // 24
Đây là một loại hàm rất bất thường, không thực tế và chỉ được sử dụng ở đây để minh họa một cách sử dụng tiềm năng của biến toàn cục. Nó hoạt động, nhưng cồng kềnh và khó sử dụng hơn. Trên thực tế, cách tiếp cận này giống với mã assembly cũ, vốn không có khái niệm về hàm.
Hầu hết các ngôn ngữ lập trình đều hỗ trợ biến toàn cục, nhưng chúng có xu hướng làm điều đó theo những cách khác nhau. Ví dụ, PHP sử dụng từ khóa global
để truy cập một biến được khai báo bên ngoài bất kỳ hàm nào:
<?php
$a = 0;
$b = 0;
function inc() {
global $a;
$a = 2;
$b = 2;
}
inc();
echo "a is $a and b is $bn";
?>
Trong đoạn mã này, hàm inc
sử dụng từ khóa global
để truy cập biến $a
được khai báo ở cấp cao nhất. Khi nó đặt $a
, nó thay đổi biến cấp cao nhất đó, vì vậy đầu ra xác nhận $a
có giá trị là 2. Tuy nhiên, $b
không được khai báo global
bên trong inc
, vì vậy hàm chỉ thay đổi một biến cục bộ – trùng tên. Biến cấp cao nhất ban đầu vẫn giữ giá trị 0.
Những Vấn Đề Khi Lạm Dụng Biến Toàn Cục
Vấn đề chính với biến toàn cục là chúng phá v vỡ tính đóng gói (encapsulation), tạo ra tiềm năng rộng rãi cho các lỗi khó lường. Hãy xem xét ví dụ này:
var days = [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ];
function day_name(number) {
return days[number];
}
Hàm day_name()
truy cập một mảng toàn cục days
để trả về tên của một ngày, dựa trên số thứ tự. Trong một chương trình nhỏ, điều này có thể không phải là vấn đề, nhưng khi độ dài và độ phức tạp của chương trình tăng lên, các lỗi có thể phát sinh.
Để bắt đầu, vì biến days
là toàn cục, bất kỳ phần nào khác của codebase đều có thể thay đổi nó. Đoạn mã sau đây có thể xuất hiện ở bất cứ đâu và thay đổi cơ bản hành vi của day_name
:
days[5] = "I don't know when";
Trong các chương trình lớn hơn, việc xác định các phần của mã sử dụng hoặc thay đổi một biến toàn cục có thể khó khăn và tốn thời gian.
Vấn đề này còn lớn hơn nếu mã của bạn là đa luồng (multi-threaded). Với nhiều bản sao của mã đang chạy, việc đảm bảo tất cả chúng truy cập và sửa đổi các biến toàn cục mà không “giẫm chân” lên nhau trở nên khó khăn hơn rất nhiều.
Mã này cũng có tính khớp nối chặt chẽ (tightly coupled), khiến việc kiểm thử trở nên khó khăn hơn. Việc giới thiệu một sự phụ thuộc giữa một hàm và một biến toàn cục có nghĩa là hai yếu tố này phải luôn cùng tồn tại, và việc kiểm thử một hàm một cách độc lập trở nên phức tạp hơn.
Biểu tượng liên quan đến kiểm thử phần mềm và unit testing
Càng có nhiều biến toàn cục, khả năng xảy ra xung đột tên (name clashes) càng cao. Đối với JavaScript, đây là một vấn đề đặc biệt vì sự tồn tại của đối tượng global (Window
object trong trình duyệt). Điều này có nghĩa là rất dễ dàng khai báo một biến toàn cục mà vô tình ghi đè lên một thuộc tính của đối tượng Window
mà mã của bạn – hoặc mã của người khác – giả định là có sẵn.
var alert = "no you don't";
// ...
window.alert("Press OK to continue");
Cuộc gọi đến window.alert()
sẽ thất bại với một lỗi vì nó bây giờ là một chuỗi, không phải một hàm. Mặc dù bạn sẽ không viết mã này một cách cố ý, nhưng rất dễ mắc lỗi này một cách vô tình. Càng sử dụng nhiều biến toàn cục, bạn càng dễ “giẫm chân” lên mã khác. Bạn có thể cố gắng tránh điều này bằng cách sử dụng các tên bất thường hơn cho biến toàn cục của mình, nhưng đây chỉ là một giải pháp tạm thời; không có gì đảm bảo mã của bạn sẽ an toàn.
Tất cả những vấn đề này có thể trở nên phổ biến hơn nếu bạn sử dụng các thư viện bên ngoài, tùy thuộc vào cách ngôn ngữ lập trình bảo vệ bạn. JavaScript cung cấp ít sự bảo vệ ở đây, vì vậy, nếu bạn không cẩn thận, biến toàn cục có thể làm hỏng mã thư viện bạn sử dụng – hoặc ngược lại.
Khi Nào Biến Toàn Cục Thực Sự Hữu Ích?
Tuy nhiên, biến toàn cục không phải lúc nào cũng xấu – trên thực tế, đôi khi chúng là cần thiết, và việc tránh chúng bằng mọi giá có thể gây tốn kém! Dưới đây là một ví dụ khá hợp lý:
var debug = true;
function do_something() {
let res = do_a_thing();
if (debug) {
console.debug("res was", res);
}
return res;
}
Cách tiếp cận gỡ lỗi này có thể hữu ích trong quá trình phát triển và kiểm thử, và hoàn toàn ổn đối với các chương trình nhỏ hơn. Tuy nhiên, nó vẫn dễ bị tổn thương bởi các vấn đề, đặc biệt là việc bất kỳ phần nào của mã cũng có thể thay đổi giá trị của debug
; nó có thể thay đổi (mutable). Để tránh điều đó, tốt nhất là khai báo một biến là hằng số (constant) nếu bạn muốn ngăn giá trị của nó bị thay đổi:
const SECONDS_IN_MINUTE = 60;
Điều quan trọng cần lưu ý là một hằng số trong JavaScript, nói đúng ra, không phải là một biến toàn cục theo cách truyền thống. Chẳng hạn, nó sẽ không được thêm làm thuộc tính của đối tượng window
. Nhưng một hằng số bạn khai báo ở cấp cao nhất của một script sẽ hiển thị cho tất cả mã theo sau nó.
Minh họa logo JavaScript tượng trưng cho ngôn ngữ lập trình
Nếu bạn thực sự phải sử dụng biến toàn cục, bạn cũng có thể giảm khả năng xảy ra vấn đề bằng cách sử dụng một đối tượng toàn cục duy nhất. Ví dụ, thay vì:
var color = [0, 0, 255];
var debug = false;
var days = 7;
// ...
Bạn có thể lưu trữ cùng các giá trị đó trong một map toàn cục:
var all_my_globals = {
color: [0, 0, 255],
debug: false,
days: 7,
// ...
};
Với cách tiếp cận này, bạn chỉ thêm một tên duy nhất vào không gian tên (namespace) toàn cục, vì vậy ít khả năng nó sẽ xung đột với các tên khác, và dễ dàng hơn để tìm kiếm mã của bạn để tìm các cách sử dụng biến này.
Cuối cùng, tài liệu hóa (documentation) là người bạn của bạn. Nếu bạn phải sử dụng biến toàn cục, hãy đảm bảo chúng được giải thích trong một bình luận, đặc biệt nếu bạn đã xem xét một cách tiếp cận thay thế nhưng cuối cùng lại bỏ qua nó. Tên biến cũng hoạt động như tài liệu hóa, vì vậy hãy đảm bảo bạn sử dụng các tên rõ ràng, súc tích, cụ thể để giải thích mục đích của từng biến.
Biến toàn cục có thể là một con dao hai lưỡi trong lập trình. Mặc dù chúng có vẻ tiện lợi trong một số trường hợp nhất định, những rủi ro về khả năng gây lỗi, khó quản lý và giảm tính bảo trì của mã nguồn là rất lớn. Các chuyên gia của tincongngheso.com khuyên bạn nên hạn chế tối đa việc sử dụng biến toàn cục, thay vào đó ưu tiên biến cục bộ, đối số hàm và các kỹ thuật đóng gói khác. Khi bắt buộc phải dùng, hãy cân nhắc sử dụng hằng số hoặc một đối tượng toàn cục duy nhất để giảm thiểu tác động tiêu cực, và luôn luôn đi kèm với tài liệu hóa rõ ràng. Bằng cách áp dụng các nguyên tắc này, bạn sẽ xây dựng được những chương trình mạnh mẽ, dễ bảo trì và bền vững hơn.
Bạn có kinh nghiệm gì về việc sử dụng biến toàn cục? Hãy chia sẻ những câu chuyện và lời khuyên của bạn trong phần bình luận bên dưới!