JavaScript Patterns: Vỡ lòng về global

JavaScript sử dụng function để tạo ra phạm vi hoạt động của biến. Một biến được khai báo trong function sẽ có phạm vi hoạt động giới hạn trong function đó. Mặt khác, biến global là những biến được khai báo nằm ngoài bất kỳ function nào, và có thể được sử dụng tại bất kỳ đâu.

Mỗi môi trường thực thi JavaScript đều có một đối tượng được gọi là global object. Đối tượng này có thể được truy xuất bằng cách sử dụng tham chiếu this bên ngoài mọi function. Bất kỳ biến global nào được khai báo đều sẽ trở thành một thuộc tính của đối tượng global. Trong môi trường thực thi của trình duyệt, để thuận tiện, các trình duyệt tạo thêm một thuộc tính cho global có tên là window, thuộc tính này tham chiếu tới chính bản thân global. Đoạn code sau đây thể hiện những tính chất trên:

myglobal = "hello"; // antipattern
console.log(myglobal); // "hello"
console.log(window.myglobal); // "hello"
console.log(window["myglobal"]); // "hello"
console.log(this.myglobal); // "hello"

Bài viết này đưa bạn lặn sâu hơn vào những vấn đề xoay quanh global.

Vấn đề với các biến global

Vấn đề với các biến global được thể hiện ngay tại tên của chúng — chúng được chia sẻ cho toàn bộ mã nguồn trong toàn bộ chương trình hay website. Chúng hiện diện ở toàn cục, và luôn có khả năng gây ra xung đột tên — khi mà có hay bộ phận khác nhau của chương trình cùng khai báo và sử dụng chung một tên biến global, cho những mục đích khác nhau.

Tệ hơn, một chương trình hay một website luôn có khả năng sử dụng đến những mã không được đích thân nhà phát triển chương trình viết ra, chẳng hạn như:

  • Mã thư viện bên thứ ba
  • Mã từ nhà phát hành quảng cáo
  • Mã từ nhà theo dõi định danh hay phân tích hành vi người dùng
  • Các thể loại widget, badge, button…

Hãy nghĩ tới trường hợp những mã này sử dụng biến global, và bạn hoàn toàn có khả năng tạo ra xung đột tên với chúng. Kết quả thường sẽ là một chương trình nào đó ngừng hoạt động.

Vậy nên, vấn đề làm sao để tăng tính tương thích với các mã nguồn lân cận với chương trình, hay làm sao để sử dụng càng ít càng tốt những biến global sẽ trở nên vô cùng quan trọng.

Sử dụng từ khóa var

Một trong những phương pháp hạn chế sử dụng biến global quan trọng nhất phải kể đến đó là sử dụng từ khóa var (hay let, const nếu bạn đang lập trình ES6 trở đi). Điều này đến từ hai nguyên do. Thứ nhất, JavaScript cho phép bạn sử dụng biến mà không cần khai báo. Thứ hai, JavaScript tồn tại cơ chế ngầm hiểu global đặt mọi biến mà bạn không khai báo thành thuộc tính của đối tượng global — tương đương với việc chúng trở thành biến global.

function sum(x, y) {
  // antipattern: implied global
  result = x + y;
  return result;
}
console.log(sum(1, 2)); // 3
console.log(rusult); // 3

Vậy nên, luôn sử dụng từ khóa var để khai báo biến, như dưới đây:

function sum(x, y) {
  // antipattern: implied global
  var result = x + y;
  return result;
}
console.log(sum(1, 2)); // 3
console.log(rusult); // undefined

Một trường hợp khai báo biến mà không sử dụng var một cách vô tình, đó là khi bạn khai báo một chuỗi biến như dưới đây:

// antipattern, do not use
function foo() {
  var a = b = 0;
  // ...
}

Lưu ý rằng bản thân JavaScript không hề cung cấp cú pháp để tạo và gán một cách xâu chuỗi nhiều biến một lúc. Mã ở trên chỉ là một cách lợi dụng tính chất cố hữu của phép gán: chúng đặt giá trị cho biến được gán, nhưng đồng thời phép gán cũng là một phép tính, chúng trả về giá trị vừa được gán. Khối mã ở trên có thể được viết lại như sau để dễ phân tích hơn:

// antipattern, do not use
function foo() {
  var a = (b = 0);
  // ...
}

Giá trị của biến a được đặt bằng kết quả của phép tính (b = 0), kết quả này bằng 0. Và cả ab đều nhận giá trị 0. Nhưng a được khai báo với từ khóa var, còn b thì không. Sau khi hàm foo được thực thi, biến b sẽ trở thành biến global và khả năng cao là đây không phải ý muốn của bạn. Tốt hơn, hãy đảm bảo mọi biến bạn khai báo trong hàm đều đi với một từ khóa var:

function foo() {
  var a, b;
  a = b = 0; // both local
}

function bar() {
  var a = 0, b = 0; // both local
}

Hệ quả của việc sót từ khóa var

Có một sự khác nhau nho nhỏ giữa biến global được tạo ra một cách tường minh hay ngầm định. Sự khác nhau đó liên quan đến toán tử delete:

  • Biến global được tạo ra một cách tường minh, bởi từ khóa var (ở bên ngoài tất cả mọi function), không thể bị xóa.
  • Biến global được tạo ra một cách ngầm định có thể bị xóa bởi toán tử delete.
var global_var = 1;
delete global_var; // false
typeof global_var; // "number"

global_novar = 2; // antipattern
delete global_novar; // true
typeof global_novar; // "undefined"

(function () {
  global_fromfunc = 3; // antipattern
})();
delete global_fromfunc; // true
typeof global_fromfunc; // "undefined"

Chính bởi tính chất dễ gây lú này mà strict mode của JavaScript sẽ tung lỗi nếu bạn sơ sót hay cố ý tạo biến mà không có từ khóa khai báo.

Truy cập đối tượng global

Tại các trình duyệt, đối tượng global có thể được truy cập thông qua thuộc tính tham chiếu có tên window (trừ khi bạn đã tạo lại một biến local có cùng tên). Tại các môi trường khác, tên thuộc tính tham chiếu tới global có thể sẽ khác đi. Nếu bạn không muốn bị phụ thuộc, bạn có thể sử dụng mẫu sau, tại bất kỳ scope nào, để có tham chiếu tới global:

var global = (function () {
  return this;
})();

Hàm ở trên được tạo ra bởi cú pháp function declaration, nó hoạt động tại không gian global, vậy nên tham chiếu this sẽ tham chiếu tới đối tượng global. Lưu ý rằng điều này không còn đúng tại strict mode, vậy nên nếu đang ở trong strict mode thì bạn cần mẫu code khác, chẳng hạn:

(function(global) {
  let foo = function() {
    // sử dụng tham chiếu `global` tại đây khi cần thiết
  }
})(this);

Hoisting

Nguồn ảnh: Wiktionary

Một cơ chế gây lú khác. Sự thật thì phức tạp hơn một chút, nhưng chúng ta có thể hiểu như sau, JavaScript cho phép bạn đặt nhiều câu lệnh var tại bất kỳ đâu trong function, nhưng các biến tương ứng sẽ hoạt động y như thể chúng được khai báo tại đầu hàm. Hãy quan sát khối mã sau đây:

// antipattern
myname = "global"; // global variable
function func() {
  alert(myname); // "undefined", không hề là "global"
  var myname = "local";
  alert(myname); // "local"
}
func();

Dưới mắt của trình thực thi JavaScript, khối mã trên tương đương với như sau:

myname = "global"; // global variable
function func() {
  var myname; // tương đương var myname = undefined;
  alert(myname); // "undefined"
  myname = "local";
  alert(myname); // "local"
}
func();

Mẫu code câu lệnh var đơn

Nếu bạn viết mã trước ES6, mẫu code này đáng để cân nhắc, theo đó, bạn sử dụng một câu lệnh var duy nhất, để khai báo tất cả các biến mà hàm sử dụng tới, ngay tại đầu hàm:

function func() {
  var a = 1,
  b = 2,
  sum = a + b,
  myobject = {},
  i,
  j;
// function body... }

Một số lợi ích:

  • Bao quát được tất cả các biến mà hàm cần tới, và gợi ý cho bạn nhu cầu tách hàm từ sớm
  • Hạn chế lỗi logic xảy ra khi bạn không kiểm soát được sự hoạt động của cơ chế hoist
  • Hạn chế sơ sót xảy ra khiến bạn vô tình tạo biến global
  • Tốn ít mã hơn

Trong mẫu code này, thứ tự khai báo các biến cũng có ý nghĩa tương đối thực dụng, hãy để ý biểu thức khởi tạo giá trị cho biến sum ở trên, hay biến style trong khối mã dưới đây:

function updateElement() {
  var el = document.getElementById("result"),
    style = el.style;
  // do something with el and style...
}

Tổng kết

Vậy là bạn đã dạo qua được cách mà JavaScript phân bố các thứ vào hai khu vực:

  • Không gian global
  • Không gian trong hàm (local)

Loading

Leave a Reply

Your email address will not be published. Required fields are marked *