Hiểu nhanh về Scope trong Javascript trong vòng 5 phút

Scope là gì ? Scope trong Javascript có tác dụng như thế nào ? … trong bài viết ngay hôm nay mình sẽ làm sáng tỏ những câu hỏi trên trong vòng 5 phút , cùng vào bài viết nào .

Scope là gì ?

Trong Javascript ta biết đến Scope hay phạm vi truy cập ( Ngữ cảnh của đoạn code). Scope cũng có thể định nghĩa là toàn cục (globally) hoặc cục bộ (locally) . Scope trong Javascript là một cái rất quan trọng ,khi nắm vững được nó bạn có thể làm cho code của bản than “sạch sẽ “ hơn , dễ dàng debug , 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ễ maintain ,…

Global Scope

Trước khi bạn viết một đoạn code ,bạn đang nằm trong cái mà được gọi là phạm vi truy cập toàn cục(global scope) nói một cách dễ hiểu đó là toàn bộ , bao quát code của các bạn ,hay một tập hợp chứa code của các bạn . Nếu ta định nghĩa biến, biến đó là toàn cục :

// global scope
var name = 'NodeJS';

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:

$('.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

Hiểu một cách đơn giản Local Scope là các tập con của Global Scope .Thường thì chỉ có một Global Scope 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ộ . Chúng ta có ví dụ sau :

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

Ta có thể thấy 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 được tạo nên từ function scope , chúng ta có thể hiểu là khi tạo 1 function thì chúng ta cung đang tạo 1 scope mới .
Ví dụ :

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

Lexical scope

Khi chúng ta tạo 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 , chúng ta có ví dụ sau :

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

Trong ví dụ trên 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 = 'nodejs.vn';
  var myOtherFunction = function () {
    console.log('website số 1 VN' + name);
  };
  console.log(name);
  myOtherFunction(); // call function
};

// `nodejs.vn`
// `website số một VN nodejs.vn`

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 = 'nodejs.vn';
var scope1 = function () {
  // name is available here
  var scope2 = function () {
    // name is available here too
    var scope3 = function () {
      // name is also available here!
    };
  };
};

Một lưu ý nhỏ, 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 = 'nodejs.vn'; // 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";

Ta có thể thấ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('nodejs'); // 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('nodejs');
helloMethod(); // Hello nodejs

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 this là window 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() và 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

Hi vọng qua các bạn có thể học được những điều thú vị thông qua bài viết này , mọi ý kiến thắc mắc các bạn có thể bình luận dưới bài viết , mình sẽ giải đáp cho các bạn .