Scope trong Javascript

MỞ ĐẦU

Javascript có các khái niệm liên quan tới scope và không khái niệm nào là dễ ăn đối với những newbie mới làm quen với Javascript (hay kể cả những dev lâu năm). Bài viết này hướng tới những bạn mong muốn tìm hiểu sâu hơn về scope sau khi đã "đối mặt" với các từ khóa như scope, closure, this, namespace, function scope, global scope, lexical scope và public/private scope

Scope là gì?

Trong Javascript, scope hay phạm vi truy cập, đề cập đến ngữ cảnh của đoạn code. Scope có thể định nghĩa là toàn cục (globally) hoặc cục bộ (locally). Nắm rõ scope trong Javascrip là chía khóa để viết những đoạn code rõ ràng, sạch sẽ, hiểu được các biến/hàm này có thể truy cập đến không hay giúp cho đoạn code của bạn dễ manitain, dễ debug hơn. Khi xét scope của variable/function, ta thường đặt câu hỏi: nó thuộc scope A hay scope B ???

Global Scope

Trước khi bắt đầu viết một dòng code, chúng ta đang nằm trong cái mà được gọi là phạm vi truy cập toàn cục(global scope). Nếu ta định nghĩa biến, biến đó là toàn cục:

// global scope
var name = 'Duy Buffet';

Global scope là bạn tốt nhất và cũng là cơn ác mộng tồi tệ nhất!!! Nếu không nắm rõ mình đang nằm trong scope nào, chắc chắn ta sẽ gặp vấn đề với global scope (thường là xung đột namespace). Người ta cứ nói rằng việc dùng Global scope là rất dở, nhưng không phải trong mọi trường hợp. Ta cần sử dụng nó để tạo ra các Modules/APIs được truy cập bởi các scope khác. Ví dụ: trong jQuery, ta select một element bằng class name như sau:

jQuery('.myClass');

Ở đây, ta đang truy cập đến namespace jQuery trong global scope. Khái niệm namespace đôi khi có thể dùng thay thế cho scope, nhưng chủ yếu là đề cập đến scope có level cao nhất. Trong trường hợp này, jQuery nằm trong global scope đồng thời cũng là namespace cho thư viện jQuery.

Local scope

Local scope đề cập tới bất kỳ scope nào được xác định qua global scope. Thường có một phạm vi truy cập toàn cục (global scope) duy nhất và mối function lại định nghĩa phạm vi truy cập cục bộ (local scope) của riêng nó. Nếu định nghĩa một function và tạo các biến bên trong nó, các biến này được gọi là biến cục bộ. Ví dụ:

// Scope A: Global scope out here
var myFunction = function () {
  // Scope B: Local scope in here
  var name = 'Duy';
  console.log(name); // Duy
};
// Uncaught ReferenceError: name is not defined
console.log(name);

Biến name có phạm vi truy cập là local scope và nó sẽ không thể được truy cập bởi scope cha, do đó dẫn đến kết quả là undefined

Function scope

Tất cả các scope trong Js không được tạo bởi vòng lặp for hoặc while, hay các lệnh rẽ nhánh if hoặc switch mà bởi function scope. Công thức là: tạo functions = tạo scope mới. Ví dụ:

// Scope A
var myFunction = function () {
  // Scope B
  var myOtherFunction = function () {
    // Scope C
  };
};

Lexical Scope

Khi nhìn thấy một function nằm trong một function khác, function trong có quyền truy cập tới scope của function bên ngoài, đó gọi là Lexical Scope hay Closure - còn được gọi là Static Scope. Ví dụ:

// Scope A
var myFunction = function () {
  // Scope B
  var name = 'Duy'; // định nghĩa trong Scope B
  var myOtherFunction = function () {
    // Scope C: `name`vẫn có thể được truy cập đến từ đây!!
  };
};

Ở đây có thể nhận thấy rằng, myOtherFunction mới chỉ được định nghĩa chứ chưa được gọi. Thứ tự gọi cũng có ảnh hưởng đến các biến:

var myFunction = function () {
  var name = 'Duy';
  var myOtherFunction = function () {
    console.log('My name is ' + name);
  };
  console.log(name);
  myOtherFunction(); // call function
};

// `Duy`
// `My name is Duy`

Làm việc với Lexical scope cũng khá là dễ dàng, bật cứ biến/object/ function được định nghĩa trong parent scope, đều có thể được truy cập bởi các scope con nhỏ hơn. Ví dụ:

var name = 'Duy';
var scope1 = function () {
  // name is available here
  var scope2 = function () {
    // name is available here too
    var scope3 = function () {
      // name is also available here!
    };
  };
};

Chú ý, Lexical scope không hoạt động theo chiều ngược lại, tức là biến/object/function định nghĩa trong scope con thì ko thể truy cập bởi scope cha.

// name = undefined
var scope1 = function () {
  // name = undefined
  var scope2 = function () {
    // name = undefined
    var scope3 = function () {
      var name = 'Duy'; // locally scoped
    };
  };
};

Scope Chain

Mình có đoạn code như sau:

function b() {
  console.log(text);
}
 
function a() {
  var text = "in a";
  b();
}
 
a();
var text = "in gloal";

Theo các bạn đoạn code trên in ra in a hay in global ??? Đáp án là undefined. Vì trong một scope, nếu ta truy cập giá trị một biến, mà không tìm thấy biến đó trong scope hiện tại thì nó sẽ tìm ở scope cha (chính là cái mà scope chain muốn đề cập). Trong function b không có biến text, do vậy nó sẽ ngược lên scope cha để tìm biến text. Tuy dòng khai báo text nằm ở cuối cùng, tuy nhiên do hoisting trong Js, nên mọi khai báo sẽ được chuyển lên đầu scope:

var text;
function b() {
  console.log(text);
}
 
function a() {
  var text = "in a";
  b();
}
 
a();
text = "in gloal";

như vậy function b sẽ cố in ra biến b lúc chưa có giá trị nên kết quả là undefined

Closures là gì?

Closure có mối quan hệ chặt chẽ với Lexical Scope. Ví dụ tiêu biểu về cách thức hoạt động của closure đó là khi 1 function trả về tham chiếu tới 1 function.

var sayHello = function (name) {
  var text = 'Hello, ' + name;
  return function () {
    console.log(text);
  };
};

Khái niệm closure làm cho scope của ta không thể tiếp cận được public scope. Chỉ gọi function sẽ không thực hiện gì bởi nó trả về kết quả là tham chiếu tới function.

sayHello('Duy'); // nothing happens, no errors, just silence...

Để method hoạt động ta cần gán nó vào biến rồi mới thực thi:

var helloMethod = sayHello('Duy');
helloMethod(); // Hello Duy

Không nhất thiết phải trả về function mới được gọi là closure. Đơn giản chỉ cần truy cập tới biến nằm ngoài Lexical scope cũng là closure

Scope và ‘this’

Mỗi scope lại bind giá trị khác nhau cho this tùy thuộc vào vị trí nó được gọi tới. Mặc định thì this bind đến object toàn cục nhất window. Ta cùng xem cách gọi hàm khác nhau cho ra kết quả của this khác nhau:

var myFunction = function () {
  console.log(this); // this = global, [object Window]
};
myFunction();

var myObject = {};
myObject.myMethod = function () {
  console.log(this); // this = Object { myObject }
};

var nav = document.querySelector('.nav'); // <nav class="nav">
var toggleNav = function () {
  console.log(this); // this = <nav> element
};
nav.addEventListener('click', toggleNav, false);

Có trường hợp dù trong cùng một function, scope vẫn có thể thay đổi và giá trị của this cũng bị thay đổi:

var nav = document.querySelector('.nav'); // <nav class="nav">
var toggleNav = function () {
  console.log(this); // <nav> element
  setTimeout(function () {
    console.log(this); // [object Window]
  }, 1000);
};
nav.addEventListener('click', toggleNav, false);

Ở đây ta đã tạo ra một scope mới mà không được gọi tới bởi event handler, nên mặc định giá trị của thiswindow object. Để lấy được giá trị this trong context của event handler mà không phải object window, ta có thể cache this lại và refer đến nó theo lexical binding:

var nav = document.querySelector('.nav'); // <nav class="nav">
var toggleNav = function () {
  var that = this;
  console.log(that); // <nav> element
  setTimeout(function () {
    console.log(that); // <nav> element
  }, 1000);
};
nav.addEventListener('click', toggleNav, false);

Thay đổi scope với .call(), .apply() and .bind()

Đôi khi bạn cần điều chỉnh scope cho phù hợp với mục đích sử dụng:

var links = document.querySelectorAll('nav li');
for (var i = 0; i < links.length; i++) {
  console.log(this); // [object Window]
}

Giá trị của this ở đây không refer tới các element như ta mong muốn. Ta có thể thay đổi scope theo cách sau đây

.call() và .apply()

call()apply() cho phép ta bind đúng giá trị của this bằng cách truyền một scope vào một function. Bây giờ mình sẽ sửa lại đoạn code bên trên để this bind đến đúng các phần từ của mảng:

var links = document.querySelectorAll('nav li');
for (var i = 0; i < links.length; i++) {
  (function () {
    console.log(this);
  }).call(links[i]);
}

Ở đây, mình đang truyền element trong mỗi vòng lặp links[i] vào để thay đổi scope của function ===> this = iterated element.apply() có chức năng tương tự, chỉ khác cách truyển tham số, trong khi .call(scope, arg1, arg2, arg3) nhận các tham số riêng lẻ cách nhau bởi dấu ,, còn .apply(scope, [arg1, arg2]) lại nhận một mảng.

.bind()

Không như ví dụ bên trên, .bind() không gọi tới function, nó bind giá trị trước khi function được gọi tới. Phương thức này được giới thiệu trong ECMAScript 5. Như đã biết, ta không thể truyền tham số vào tham chiếu của function:

// works
nav.addEventListener('click', toggleNav, false);

// will invoke the function immediately
nav.addEventListener('click', toggleNav(arg1, arg2), false);

Có thể fix bằng cách:

nav.addEventListener('click', function () {
  toggleNav(arg1, arg2);
}, false);

Nhưng một lần nữa ở đây đã làm thay đổi scope và đồng thời vô tình tạo ra một function vô dụng có thể làm ảnh hưởng đến performance nếu ta đặt nó trong vòng lặp và binding event listener. Đó là lúc .bind() tỏa sáng và giải quyết vấn đề, vẫn đảm bảo truyền được tham số, nhưng function chưa được gọi ngay lúc đó:

nav.addEventListener('click', toggleNav.bind(scope, arg1, arg2), false);

KẾT LUẬN

Trên đây mình đã giới thiệu sơ qua một số vấn đề về scope trong Javascript. Hi vọng bài viết phần nào hữu ích cho những người đang bắt đầu tìm hiểu Javascript.

Nguồn tham khảo

  1. https://toddmotto.com/everything-you-wanted-to-know-about-javascript-scope/