12/08/2018, 15:25

Effective JavaScript - Chapter 1 - Accustoming Yourself to JavaScript (Part I)

JavaScript được thiết kế để mang lại cảm giác quen thuộc. Với cú pháp (syntax) gợi nhớ về Java và hàm dựng vốn dĩ đã phổ biến ở rất nhiều ngôn ngữ scripting (function, array, dictionary và regular expression), JavaScript dường như là một cái gì đó dễ học với bất cứ ai đã có một chút kinh nghiệm về ...

JavaScript được thiết kế để mang lại cảm giác quen thuộc. Với cú pháp (syntax) gợi nhớ về Java và hàm dựng vốn dĩ đã phổ biến ở rất nhiều ngôn ngữ scripting (function, array, dictionary và regular expression), JavaScript dường như là một cái gì đó dễ học với bất cứ ai đã có một chút kinh nghiệm về programming. Và với các programmer ít kinh nghiệm, họ có thể bắt đầu viết các chương trình mà không cần training quá nhiều tại vì lượng khái niệm core trong JavaScript là không quá nhiều.

Việc tiếp cận tuy dễ dàng, nếu muốn thuần thục (master) nó thì sẽ mất khá nhiều thời gian và đòi hỏi sự hiểu biết sâu hơn về ngữ nghĩa, các đặc tính và các idiom hữu hiệu nhất của nó. Mỗi chapter của cuốn sách này sẽ đề cập đến một phạm vi chủ đề khác nhau của effective JavaScript. Chương đầu tiên này bắt đầu với một vài topic cơ bản nhất.

Giống như hầu hết các công nghệ phồn thịnh khác (successful technology), JavaScript đã phát triển theo thời gian. Dù ban đầu được tạo ra như một sự bổ sung của Java dành cho việc viết các trang web tương tác (interactive web page), JavaScript cuối cùng lại "hất cẳng" Java để trở thành kẻ thống trị trong thế giới ngôn ngữ lập trình web. Sự phổ biến của JavaScript dẫn đến việc chính thức hóa trở thành một chuẩn toàn cầu, ECMAScript, vào năm 1997. Ngày nay có rất nhiều các bản implement JavaScript đáp ứng được sự tương thích với các phiên bản khác nhau của chuẩn ECMAScript.

Bản thứ ba của chuẩn ECMAScript (ES3), hoàn thành năm 1999, tiếp tục là bản được dụng rộng rãi nhất (Chú ý là cuốn sách này được viết vào năm 2014 nên chưa chắc lúc tôi viết bài dịch này - tháng 06/2017 - điều này hãy còn đúng). Bản nâng cấp chính tiếp theo là Edition 5 (ES5), được release vào năm 2009 (thực tế là đã có thêm hai phiên bản khác là ECMAScript 6 (2015) và ECMAScript 7 (2016) đã được release). ES5 giới thiệu một số feature mới cũng như là đã chuẩn hóa một số feature đã được sử dụng rộng rãi nhưng chưa được đặc tả trước đó. Bởi vì việc hỗ trợ ES5 chưa động rộng khắp, tôi sẽ ghi chú ở những mục (item) dành cho bản ES5.

Ngoài các feature đã được đặc tả bởi ECMAScript, rất nhiều feature phi chuẩn đặc hỗ trợ trong một vài bản implement nhưng lại không được hỗ trợ trong các bản implement khác. Ví dụ, rất nhiều JavaScript engine hỗ trợ keyword const để khai báo biến (variable) nhưng chuẩn ECMAScript không hề cung cấp bất cứ định nghĩa về cú pháp hay hành vi (behaviour) của const. Hơn nữa, hành vi của const lại khác nhau ở các bản implement. Trong một số trường hợp, biến const không được phép update:

const PI = 3.141592653589793;
PI = "modified!";
PI; // 3.141592653589793

Các bản implement khác lại coi const như là một sự thay thế (synonym) của var:

const  PI = 3.141592653589793;
PI = "modified!";
PI; // "modified!"

Với bề dày lịch sử và sự đa dạng ở các bản implement của JavaScript, rất khó để theo dõi feature nào là có sẵn ở nền tảng (platform) nào. Vấn đề này khiến hệ sinh thái chính của JavaScript - web browser - không cho phép các programmer control được phiên bản JavaScript nào có thể chạy được code của họ. Bởi vì người dùng cuối có thể sử dụng các phiên bản khác nhau của các web browser khác nhau, chương trình web phải được viết cẩn thận để làm việc thống nhất ở các browser.

Mặt khác, JavaScript không phải chỉ được sử dụng ở lập trình web client-side (client-side web programming). Các mục đích sử dụng khác bao gồm lập trình server-side, browser extension và scripting cho mobile và app desktop. Ở một vài trong số những trường hợp này, bạn có thể có một phiên bản JavaScript đặc thù hơn rất nhiều. Với những trường hợp này, sẽ rất tuyệt nếu bạn có thể tận dụng các feature bổ sung.

Cuốn sách này chủ yếu quan tâm tới các feature chuẩn của JavaScript. Nhưng việc bàn luận về các feature được hỗ trợ rộng rãi nhưng phi chuẩn cũng rất quan trọng. Khi đề cập đến những chuẩn mới hoặc các feature phi chuẩn, cũng rất cần thiết để hiểu rằng ứng dụng của bạn sẽ chạy ở trong môi trường có hỗ trợ những feature đó hay không. Nếu không thì, bạn có thể sẽ rơi vào trường hợp mà app chạy trên máy của bạn hoặc môi trường testing nhưng lại thất bại khi người dùng chạy nó trên các môi trường khác nhau. Ví dụ, const có thể hoạt động tốt khi test trên một engine có hỗ trợ feature phi chuẩn này nhưng lại bị mắc lỗi cú pháp (syntax error) khi được sử dụng (deploy) trong một web browser không thừa nhận keyword này.

ES5 giới thiệu strict mode. Feature này cho phép bạn lựa chọn phiên bản JavaScript hạn chế (stricted version) - phiên bản không cho phép sử dụng một số feature mơ hồ và có thể gây ra lỗi (problematic and error-prone feature). Cú pháp được thiết kế để có thể tương thích ngược nên các môi trường không implement việc kiểm tra strict mode vẫn có thể chạy strict code. Strict mode được enable trong chương trình bằng cách thêm một string đặc biệt ở đoạn đầu chương trình:

"use strict";

Một cách tương tự, bạn có thể enable strict mode trong một hàm bằng cách đặt một directive (chỉ dẫn) ở ngày đầu thân hàm:

function f(x) {
    "use strict";
    // ...
}

Việc sử dụng một chuỗi ký tự (string literal) cho directive nghe có vẻ lạ lẫm nhưng nó có lợi ích về mặt tương thích ngược: Việc evaluate một chuỗi ký tự không gây ra các hậu quả không mong muốn (side effect), vậy nên engine ES3 sẽ coi directive như là một câu lệnh vô thưởng vô phạt (innocuous statement) - nó evaluate chuỗi đó và sau đó loại bỏ giá trị của nó ngay lập tức. Điều này giúp cho việc viết code với strict mode có thể chạy trên các engine JavaScript phiên bản cũ hơn nhưng với một sự hạn chế nhất định: Các engine cũ sẽ không thực hiện việc check strict mode. Nếu bạn không test trong môi trường ES5, thật dễ để viết ra đoạn code bị reject khi chạy trong môi trường ES5:

function f(x) {
    "use strict";
    var  arguments = []; // error: redefinition of arguments
    // ...
}

Việc khai báo lại biến "arguments" sẽ không được cho phép trong strict mode. Nhưng trong môi trường không implement việc kiểm tra strict mode, việc khai báo lại này được chấp thuận. Việc sử dụng code này trong production có thể gây ra lỗi ở các môi trường implement ES5. Vì lý do này, bạn nên test strict code trong các môi trường ES5.

Có một cạm bẫy khi sử dụng strict mode chính là việc directive "use strict" chỉ được nhận biết ở trên đầu của một script hoặc một hàm - điều này làm cho nó nhạy cảm với script concatenation, thao tác kết hợp các script riêng biệt lại với nhau để tạo thành một file đơn để deploy. Xét một file mà chúng ta mong muốn được sử dụng với strict mode:

// file1.js
"use strict";
function f() {
    // ...
}
// ...

Và một file khác không được sử dụng với strict mode:

// file2.js
// no strict-mode directive
function g() {
    var  arguments = [];
    // ...
}
// ...

Làm thế nào để chúng ta nối hai file đó lại một cách đúng đắn? Nếu chúng ta bắt đầu với file1.js thì cả file được nối sẽ hoạt động với strict mode:

// file1.js
"use strict";
function f() {
    // ...
}
// ...
// file2.js
// no strict-mode directive
function f() {
    var  arguments = []; // error: redefinition of arguments
    // ...
}
// ...

Và nếu chúng ta bắt đầu với file2.js, file được nối sẽ không hoạt động với strict mode:

// file2.js
// no strict-mode directive
function g() {
    var  arguments = [];
    // ...
}
// ...
// file1.js
"use strict";
function f() { // no longer strict
    // ...
}
// ...

Trong project của bạn, bạn có thể chọn "strict-mode only" hoặc "non-strict-mode only". Nhưng nếu muốn viết code khỏe (robust code - mình cũng chẳng biết dịch thế nào cho hay hơn) mà có thể được kết hợp với những đoạn code khác, bạn có một vài lựa chọn.

Never concatenate strict files and nonstrict files (Đừng bao giờ nối hai loại file - strict và nonstrict với nhau). Đây có lẽ là giải pháp dễ dàng nhất nhưng tất nhiên khả năng control cấu trúc file (file structure) của ứng dụng sẽ bị hạn chế. Tốt nhất, bạn nên lưu hai loại file này ở hai folder riêng biệt.

Concatenate files by wrapping their bodies in immediately invoked function expressions. Item 13 sẽ giải thích kỹ hơn về khái niệm immediately invoked function expressions (IIFEs). Nhưng một cách ngắn gọn, bằng việc wrap content của mỗi file trong một function, chúng có thể được biên dịch độc lập trong các mode khác nhau.

// no strict-mode directive
( function () {
    // file1.js
    "use strict";
    function f() {
        // ...
    }
    // ...
})();
( function () {
    // file2.js
    // no strict-mode directive
    function f() {
        var  arguments = [];
        // ...
    }
    // ...
})();

Vì content của mỗi file được đặt ở trong các scope riêng biệt, directive "use strict" chỉ ảnh hưởng đến content của file đó. Tuy nhiên, để cách tiếp cận này hoạt động, content của các file không được biên dịch ở scope global. Ví dụ, var và khai báo function không được coi là các biến global (xem Item 8 để tìm hiểu về scope global). Điều này vô tình lại rơi đúng vào trường hợp của các hệ thống module (module system) phổ biến. Chúng quản lý file và dependency bằng cách tự động đặt content của mỗi module trong các hàm riêng biệt. Vì các file được đặt trong scope local, mỗi file có thể tự quyết định có sử dụng strict mode hay không.

Write your files so that they behave the same in either mode. Để viết một thư viện có thể làm việc trong các context khác nhau, bạn không được giả thuyết rằng nó sẽ được đặt trong content của một hàm bằng một tool nối script (script concatenation tool) hoặc nghĩ rằng codebase của khách hàng là strict hoặc nonstrict. Cách đơn giản nhất để xây dựng code có khả nằng tương thích tốt nhất chính là viết cho strict mode nhưng lại wrap tường minh content trong các hàm sử dụng strict mode một cách cục bộ. Điều này tương tự với giải pháp trước đó - wrap content của mỗi file trong một IIFE - nhưng trong trường hợp này, bạn viết IIFE bằng tay thay vì tin vào tool nối file hoặc hệ thống module làm hộ bạn và lựa chọn strict mode một cách tường minh:

( function () {
    "use strict";
    function f() {
        // ...
    }
    // ...
})();

Chú ý rằng đoạn này được coi là strict mà chẳng cần biết nó được nối trong context strict hay nonstrict. Trái lại, một hàm không lựa chọn strict mode vẫn có thể được coi là strict nếu nó được nối vào sau strict code. Vì vậy lựa chọn phù hợp hơn chính là viết trong strict mode.

Things to Remember:

  • Xác định những phiên bản JavaScript mà app của bạn support
  • Chắc chắn rằng bất cứ feature nào bạn sử dụng phải được support bởi tất cả các môi trường mà app bạn sẽ chạy
  • Luôn luôn kiểm tra strict mode trong các môi trường thực hiện việc kiểm tra strict mode.
  • Cẩn thận với việc nối các script khác nhau về strict mode.

Trên đây là phần dịch cho item đầu tiên - Item 1 - trong 68 item được đề cập đến trong cuốn sách Effective JavaScript của tác giả David Herman.

Homepage: http://effectivejs.com/

0