JavaScript Patterns: Strict Mode

Những thành phần lõi của ngôn ngữ JavaScript (ngoại trừ DOM, BOM và những biến môi trường phụ thuộc vào host) được phát triển dựa trên tiêu chuẩn ECMAScript, hay còn gọi tắt là ES. Phiên bản ES5 là một bước ngoặt, nó bổ sung một số đối tượng, phương thức và thuộc tính vào ngôn ngữ, nhưng quan trọng nhất là thứ được gọi là strict mode — nôm na là chế độ nghiêm ngặt. Việc mà strict mode thực hiện thực ra là loại bỏ đi một số chức năng khỏi ngôn ngữ, khiến cho chương trình bắt buộc phải trở nên đơn giản và ít lỗi hơn.

Khi một scope (bất kể function, global, hay một string được truyền tới eval()), được bắt đầu với "use strict" có nghĩa là mã nguồn trung scope đó được thực thi trong tập con các chức năng của ngôn ngữ. Các trình dịch cũ hơn sẽ đơn giản coi đó là một String literal không được gán vào biến nào và đơn giản là bỏ qua nó. Điều này giúp strict mode có khả năng tương thích ngược.

Kế hoạch phát triển của hiệp hội ES là trong tương lai, strict sẽ là tập các chức năng duy nhất hiện diện. Điều đó khiến ES5 trở thành một phiên bản tiêu chuẩn cho các nhà phát triển.

Biến các sơ suất thành Error

JavaScript vốn là một ngôn ngữ lỏng lẻo (và dễ chịu, cho các lập trình viên mới toe). Và đôi khi nó cho phép thực thi những xử lý mà thật ra hoàn toàn lầm lỗi. Cơ chế đó, đôi khi xử lý được vấn đề; và đôi khi khiến vấn đề trở nên trầm trọng hơn trong tương lai. Strict mode đơn giản là khiến các sơ suất mà nó phát hiện được trở thành những lỗi được throws và lập trình viên bắt buộc sẽ phải sửa.

Chẳng hạn, đầu tiên, không thể nào vô tình tạo ra các biến global được nữa (biến dưới đây sẽ trở thành biến global, do khai báo thiếu var hay let, trong điều kiện bình thường):

'use strict';
                       // Giả sử không có biến nào tồn tại sẵn,
mistypeVariable = 17;  // dòng này sẽ tung ReferenceError

Thứ hai, strict mode khiến trình dịch tung Error khi gặp những phép gán mà bình thường sẽ không thực hiện được và được cho qua. Chẳng hạn như phép gán cho undefined, cho NaN

'use strict';

// Gán vào biến global chỉ đọc
var undefined = 5; // throws a TypeError
var Infinity = 5; // throws a TypeError

// Gán vào thuộc tính chỉ đọc
var obj1 = {};
Object.defineProperty(obj1, 'x', { value: 42, writable: false });
obj1.x = 9; // throws a TypeError

// Gán vào thuộc tính chỉ có getter
var obj2 = { get x() { return 17; } };
obj2.x = 5; // throws a TypeError

// Gán thuộc tính mới cho đối tượng không thể mở rộng
var fixed = {};
Object.preventExtensions(fixed);
fixed.newProp = 'ohai'; // throws a TypeError

Thứ ba, strict mode tung lỗi gặp chỉ lệnh xóa một thuộc tính không thể xóa, điều mà bình thường sẽ không gây ra hiệu ứng gì và được trình dịch cho qua:

'use strict';
delete Object.prototype; // throws a TypeError

Thứ tư, strict mode yêu cầu tên tham số của hàm phải duy nhất. Trong điều kiện bình thường, tham số trùng tên cuối cùng sẽ giấu đi các đối số đứng trước. Tuy các đối số bị giấu vẫn có thể truy cập thông qua arguments[i] nhưng dù sau đặt tên cho duy nhất vẫn dễ đọc hơn, vậy nên strict mode coi tham số trùng tên là lỗi syntax:

function sum(a, a, c) { // !!! syntax error
  'use strict';
  return a + a + c; // wrong if this code ran
}

Thứ năm, trong chế độ bình thường, một number literal bắt đầu bằng ký tự 0 sẽ được coi là một số bát phân. Chẳng hạn số 0644 sẽ được coi là số bát phân, nó có giá trị thập phân là 420. Strict mode của ES5 coi đó là lỗi syntax. Còn strict mode của ES6 thì cho phép literal cho số bát phân được bắt đầu bằng hai ký tự 0o (số 0 và ký tự o). Tất cả những việc này là để hạn chế hiểu lầm khi lập trình viên làm việc với number:

var a = 0o10; // ES2015: Octal
var sumWithOctal = 0o10 + 8;
console.log(sumWithOctal); // 16

Thứ sáu, strict mode coi việc đặt thuộc tính cho các giá trị nguyên thủy là lỗi, trong khi chế độ bình thường sẽ bỏ qua:

'use strict';

false.true = '';         // TypeError
(14).sailing = 'home';   // TypeError
'with'.you = 'far away'; // TypeError

Thứ bảy, strict mode của ES5 coi việc đặt thuộc tính trùng tên là lỗi. ES6 giới thiệu tính năng là tính toán ra tên cho thuộc tính (computed property names), ràng buộc này được gỡ bỏ:

'use strict';
var o = { p: 1, p: 2 }; // syntax error prior to ECMAScript 2015

Đơn giản hóa việc sử dụng biến

JavaScript có một số cơ chế gây phức tạp cho việc tìm hiểu xem tên được map với biến nào trong scope. Strict mode giúp cơ chế trở nên đơn giản hơn.

Đầu tiên, strict mode bỏ đi từ khóa with. Vấn đề với with là tên được sử dụng trong khối with có thể được map với một biến nằm ngoài khối, hoặc map với một thuộc tính của đối tượng được with. Thực tế giá trị nào được map hoàn toàn là ẩn số trước khi runtime. Thế nên strict đơn giản là bỏ chức năng with đi:

'use strict';
var x = 17;
with (obj) { // !!! syntax error
  // Chả ai biết ở đây x là biến x, hay là thuộc tính obj.x,
  // cả hai đều có thể đúng
  x;
}

Thứ hai, trong điều kiện bình thường, eval("var x;") sẽ tạo ra biến x tại scope mà chỉ lệnh evan nằm. Trong strict mode, biến được tạo bởi eval sẽ chỉ khả dụng trong scope của eval. Khiến eval không còn khả năng gây ảnh hưởng tới biến bên ngoài nữa:

var x = 17;
var evalX = eval("'use strict'; var x = 42; x;");
console.assert(x === 17);
console.assert(evalX === 42);

Thứ ba, strict coi hành động xóa một định danh chưa được gán giá trị là lỗi:

'use strict';

var x;
delete x; // !!! syntax error

eval('var y; delete y;'); // !!! syntax error

Đơn giản hóa evalarguments

Strict mode mang đến một số thay đổi khiến evalarguments trở nên đỡ magic hơn, và trông giống keywords hơn. Đầu tiên, nó không cho phép đặt định danh với những từ đó.

'use strict';
eval = 17;
arguments++;
++eval;
var obj = { set p(arguments) { } };
var eval;
try { } catch (arguments) { }
function x(eval) { }
function arguments() { }
var y = function eval() { };
var f = new Function('arguments', "'use strict'; return 17;");

Thứ hai, strict mode sẽ không liên kết các thuộc tính của đối tượng arguments với các biến tham số. Chẳng hạn, bình thường, khi tham số đầu tiên của một hàm là arg, thay đổi arg sẽ làm thay đổi arguments[i] và ngược lại. Strict mode ngắt bỏ mối liên kết này.

function f(a) {
  'use strict';
  a = 42;
  return [a, arguments[0]];
}
var pair = f(17);
console.assert(pair[0] === 42);
console.assert(pair[1] === 17);

Thứ ba, strict mode loại bỏ chức năng arguments.callee. Điểm yếu của callee là nó có thể được gán bí danh và gây ra các lỗi khó hiểu trong tương lai.

'use strict';
var f = function() { return arguments.callee; };
f(); // throws a TypeError

Khiến JavaScript “bảo mật” hơn

Bảo mật theo nghĩa khó rò rỉ thông tin hơn. Một số website cung cấp cho người dùng khả năng viết mã JavaScript và thực thi thay mặt cho một người dùng khác. Để tránh rò rỉ thông tin cá nhân, rất nhiều bước kiểm tra runtime sẽ cần phải được thực hiện, và các bước kiểm tra này phải đánh đổi với chi phí hiệu năng rất cao. Với strict mode và kết hợp thêm một vài tweak về cơ bản sẽ giảm bớt đi các bước kiểm tra cần phải thực hiện.

Thứ nhất, giá trị được truyền đi khi gửi tham chiếu this tới function khác sẽ không nhất thiết phải là một đối tượng nữa. Trong chế độ bình thường, this luôn là một đối tượng, nó sẽ được boxed nếu nó đại diện cho một giá trị number, boolean, hay string, hoặc sẽ biến thành đối tượng global nếu nó đại diện cho giá trị null hay undefined.

'use strict';
function fun() { return this; }
console.assert(fun() === undefined);
console.assert(fun.call(2) === 2);
console.assert(fun.apply(null) === null);
console.assert(fun.call(undefined) === undefined);
console.assert(fun.bind(true)() === true);

Vậy là các trình duyệt không còn có khả năng tham chiếu tới đối tượng window thông qua tham chiếu this trong strict mode được nữa.

Thứ hai, trong strict mode, mã không còn có khả năng đi ngược stack được nữa. Trong chế độ bình thường, các tham chiếu fun.arguments và fun.caller sẽ cho phép truy cập tới bộ tham số cũng như hàm mà đã gọi hàm fun. Điều này dĩ nhiên là không được bảo mật lắm. Trong strict mode, cả hai tham chiếu đó đều là những thuộc tính không thể xóa bỏ, và sẽ tung lỗi một khi bị truy cập hay sửa đổi.

function restricted() {
  'use strict';
  restricted.caller;    // throws a TypeError
  restricted.arguments; // throws a TypeError
}
function privilegedInvoker() {
  return restricted();
}
privilegedInvoker();

Dọn đường cho các phiên bản ES trong tương lai

Đầu tiên, một danh sách từ khóa được bổ sung (tại thời điểm ES5 ra mắt thì các từ này chưa được triển khai tính năng, nên chúng được gọi là reversed words thay vì keywords). Bao gồm implementsinterfaceletpackageprivateprotectedpublicstatic, and yield.

Thứ hai, bình thường, khai báo function có thể được đặt ở bất cứ đâu. Strict mode trong một số trình duyệt từ chối các khai báo function không nằm ở top-level. Lưu ý là ES6 trở đi cho phép điều này trở lại.

'use strict';
if (true) {
  function f() { } // !!! syntax error
  f();
}

for (var i = 0; i < 5; i++) {
  function f2() { } // !!! syntax error
  f2();
}

function baz() { // kosher
  function eit() { } // also kosher
}

Loading

Leave a Reply

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