Javscript: Top 10 lỗi mà lập trình viên Javascript thường mắc phải (Phần 1)
Ngày nay, JavaScript là cốt lõi của hầu như tất cả các ứng dụng web hiện đại. Những năm gần đây đã chứng kiến sự gia tăng của một loạt các thư viện và các framework mạnh mẽ giúp phát triển các ứng dụng single page (SPA) , đồ hoạ và hình ảnh động, và thậm chí các nền tảng JavaScript phía server. ...
Ngày nay, JavaScript là cốt lõi của hầu như tất cả các ứng dụng web hiện đại. Những năm gần đây đã chứng kiến sự gia tăng của một loạt các thư viện và các framework mạnh mẽ giúp phát triển các ứng dụng single page (SPA) , đồ hoạ và hình ảnh động, và thậm chí các nền tảng JavaScript phía server. JavaScript đã thực sự phổ biến trong thế giới phát triển ứng dụng web.
Lúc bắt đầu, JavaScript có vẻ khá đơn giản. Và thực sự, để xây dựng các chức năng JavaScript cơ bản cho một trang web là một nhiệm vụ khá đơn giản đối với bất kỳ nhà phát triển phần mềm có kinh nghiệm nào. Tuy nhiên, nó là ngôn ngữ có nhiều sắc thái, mạnh mẽ, và phức tạp hơn nhiều so với những bước đầu tiên. Hãy cùng nhau tìm hiểu một số lỗi thường gặp khi làm việc với JavaScript - điều quan trọng là phải nhận thức được và tránh né chúng trong quá trình để trở thành master JavaScript developer.
Sai lầm thứ 1: Incorrect references to this
Tôi từng nghe có diễn viên hài nói rằng: "Tôi không thực sự ở đây, bởi vì những gì ở đây, bên cạnh đó, không có 't'?" (“I’m not really here, because what’s here, besides there, without the ‘t’?”) Lời nó đùa đó bằng nhiều cách đã đặc trưng cho loại nhầm lẫn thường xảy ra cho các developer về từ khóa this của JavaScript. this có thực sự là this, hay hoàn toàn là cái gì khác ? Hoặc là undefined ? Vì các kỹ thuật code JavaScript và các design patterns đã trở nên tinh vi hơn trong những năm qua, đã có sự gia tăng tương ứng trong việc mở rộng các phạm vi tham chiếu tự trong các callbacks và closures, đây là một nguồn gốc phổ biến của "this/that confusion".
Xem xét đoạn code ví dụ này:
Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(function() { this.clearBoard(); // what is "this"? }, 0); };
Thực hiện đoạn code ở trên sẽ dẫn đến lỗi sau: Uncaught TypeError: undefined is not a function Tại sao ? Lý do bạn nhận được lỗi ở trên là bởi vì, khi bạn gọi setTimeout (), thực ra là bạn đang gọi window.setTimeout (). Kết quả là, anonymous function được truyền cho setTimeout () đang được định nghĩa trong ngữ cảnh của window object, không phải method clearBoard (). Một giải pháp truyền thống, theo các trình duyệt cũ đơn giản là chỉ lưu reference của bạn vào this ở trong một biến mà sau đó có thể được thừa hưởng bởi closure; ví dụ.:
Game.prototype.restart = function () { this.clearLocalStorage(); var self = this; // save reference to 'this', while it's still this! this.timer = setTimeout(function(){ self.clearBoard(); // oh OK, I do know who 'self' is! }, 0); };
Ngoài ra, trong các trình duyệt mới hơn, bạn có thể sử dụng phương pháp bind () để chuyển qua reference đúng:
Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(this.reset.bind(this), 0); // bind to 'this' }; Game.prototype.reset = function(){ this.clearBoard(); // ahhh, back in the context of the right 'this'! };
Sai lầm thứ 2: Thinking there is block-level scope
Như đã thảo luận trong JavaScript Hiring Guide, một nguồn gốc phổ biến của sự nhầm lẫn của các JavaScript developer giả định rằng JavaScript tạo ra một scope mới cho mỗi code block. Mặc dù điều này là đúng trong nhiều ngôn ngữ khác, nhưng điều này lại không đúng trong JavaScript. Xem xét, ví dụ, mã sau đây:
for (var i = 0; i < 10; i++) { /* ... */ } console.log(i); //output sẽ là gì ?
Nếu bạn đoán rằng lệnh console.log () sẽ undefined hoặc throw ra một lỗi, thì bạn đoán sai rồi. Tin hay không, nó sẽ ra 10. Tại sao ?
Trong hầu hết các ngôn ngữ khác, code trên sẽ dẫn đến lỗi vì "life" (tức là scope) của biến i sẽ bị giới hạn trong vòng for. Tuy nhiên trong JavaScript, trường này thì biến i vẫn nằm trong scope ngay cả sau khi vòng lặp for đã hoàn thành, giữ lại giá trị cuối cùng của nó sau khi thoát khỏi vòng lặp.
Tuy nhiên, đáng chú ý là sự hỗ trợ cho block-level scopes JavaScript thông qua từ khóa let. Từ khóa let đã có sẵn trong JavaScript 1.7 và được định nghĩa là một từ khóa JavaScript được chính thức hỗ bởi ECMAScript 6.
Bạn mới bắt đầu với JavaScript ? Hãy đọc thêm về scopes, prototypes, and more.
Sai lầm thứ 3: Creating memory leaks
Vấn đề rò rỉ bộ nhớ hầu như không thể tránh khỏi nếu bạn không có ý thức tránh chúng. Có rất nhiều cách để chúng xảy ra, chúng ta sẽ làm nổi bật một vài sự cố phổ biến hơn cả.
Memory Leak Example 1: Dangling references to defunct objects
Xem đoạn code sau:
var theThing = null; var replaceThing = function () { var priorThing = theThing; // hold on to the prior thing var unused = function () { // 'unused' is the only place where 'priorThing' is referenced, // but 'unused' never gets invoked if (priorThing) { console.log("hi"); } }; theThing = { longStr: new Array(1000000).join('*'), // create a 1MB object someMethod: function () { console.log(someMessage); } }; }; setInterval(replaceThing, 1000); // invoke `replaceThing' once every second
Nếu bạn chạy code trên và theo dõi việc sử dụng bộ nhớ, bạn sẽ thấy rằng bạn bị rò rỉ bộ nhớ lớn, rò rỉ một megabyte mỗi giây! Và ngay cả một GC(Garbage Collector) thủ công cũng không giúp ích gì. Vì vậy, có vẻ như chúng ta đang bị rò rỉ longStr mỗi lần replaceThing được gọi. Nhưng tại sao ? Cùng xem chi tiết hơn:
Mỗi đối tượng theThing có chứa đối tượng longStr 1MB. Mỗi giây, khi chúng ta gọi replaceThing, nó giữ cho một tham chiếu đến các đối tượng theThing trong priorThing. Nhưng chúng ta vẫn không nghĩ rằng đây sẽ là một vấn đề, vì trước đó mỗi lần được tham chiếu, priorThing sẽ được dereferenced (khi priorThing được thiết lập lại thông qua priorThing = theThing;). Và hơn thế nữa, chỉ được referenced trong phần chính của replaceThing và trong function unused, nhưng trên thực tế, không bao giờ được sử dụng.
Vì vậy, một lần nữa chúng ta tự hỏi tại sao có một rò rỉ bộ nhớ ở đây !?
Để hiểu điều gì đang xảy ra, chúng ta cần phải hiểu rõ hơn về cách mọi thứ đang hoạt động trong JavaScript. Cách điển hình mà closures được thực hiện là mọi object function có một liên kết đến một đối tượng từ điển thể hiện lexical scope của nó. Nếu cả hai function được xác định bên trong replaceThing thực sự được priorThing sử dụng, điều quan trọng là cả hai sẽ nhận được cùng một object. Ngay cả khi priorThing được gán lặp đi lặp lại, vì vậy cả hai function chia sẻ cùng một environment lexical. Nhưng ngay khi một biến được sử dụng bởi bất kỳ một closure nào, nó kết thúc trong lexical environment được chia sẻ bởi tất cả các closures trong scope đó. Và đó là điều dẫn đến sự rò rỉ bộ nhớ này. (Chi tiết hơn về điều này có tại đây.)
Memory Leak Example 2: Circular references
Theo dõi đoạn code sau:
function addClickHandler(element) { element.click = function onClick(e) { alert("Clicked the " + element.nodeName) } }
Ở đây, onClick có một closure giữ một tham chiếu đến element (thông qua element.nodeName). Cũng bởi chỉ định onClick to element.click, tham chiếu vòng tròn được tạo ra; i.e.: element -> onClick -> element -> onClick -> element… Thật thú vị, ngay cả khi element bị xóa khỏi DOM, tham chiếu vòng trên sẽ ngăn element và onClick được gom lại và do đó dẫn đến bị rò rỉ bộ nhớ.
Avoiding Memory Leaks: What you need to know
Quản lý bộ nhớ JavaScript chủ yếu dựa trên khái niệm về khả năng tiếp cận đối tượng. Các đối tượng sau được giả định là có thể truy cập được và được gọi là "roots":
- Các đối tượng được tham chiếu từ bất cứ nơi nào trong call stack hiện tại (tức là tất cả các biến local và các parameters trong các function hiện đang được gọi và tất cả các biến trong closure scope)
- Tất cả các biến global
Các đối tượng được lưu giữ trong bộ nhớ ít nhất miễn là chúng có thể truy cập được từ bất kỳ nguồn nào thông qua một tham chiếu, hoặc một chuỗi các tham chiếu. Có một Garbage Collector (GC) trong trình duyệt làm sạch bộ nhớ bị chiếm đóng bởi các object không thể tiếp cận; nghĩa là các đối tượng sẽ được xoá khỏi bộ nhớ khi và chỉ khi GC cho rằng chúng không thể tiếp cận. Thật không may, khá dễ dàng để kết thúc với các "zombie" object - các object đã chết mà trên thực tế không còn sử dụng nữa mà GC vẫn nghĩ là "có thể truy cập được".
Sai lầm thứ 4: Confusion about equality
Một trong những tiện ích của JavaScript là nó sẽ tự động ép buộc bất kỳ giá trị được tham chiếu trong một ngữ cảnh boolean (boolean context) với một giá trị boolean. Nhưng có những trường hợp điều này có thể gây nhầm lẫn. Xem một số ví dụ sau đây:
// All of these evaluate to 'true'! console.log(false == '0'); console.log(null == undefined); console.log(" " == 0); console.log(' == 0); // And these do too! if ({}) // ... if ([]) // ...
Đối với hai cái cuối cùng, bất kể là empty, cả {} và [] thực tế đều là các đối tượng và bất kỳ đối tượng nào sẽ bị ràng buộc bởi giá trị true trong Javascript, phù hợp với ECMA-262 specification
Như những ví dụ này chứng minh, các quy tắc của ép kiểu đôi khi có thể được clear. Theo đó, trừ khi việc ép kiểu được mong muốn một cách rõ ràng, tốt nhất nên sử dụng === và !== (thay vì == và !=), để tránh bất kỳ tác động phụ không mong muốn nào của việc ép kiểu. (== và != tự động thực hiện chuyển đổi loại khi so sánh hai điều, trong khi === và !== làm cùng một so sánh mà không có chuyển đổi kiểu.)
Và hoàn toàn như một điểm phụ - nhưng kể từ khi chúng ta đang nói đến ép kiểu và so sánh kiểu - điều đáng nói là so sánh NaN với bất cứ thứ gì (thậm chí là NaN!) sẽ luôn luôn trả về false. Do đó bạn không thể sử dụng các toán tử bình đẳng (==, ===, !=, !==) để xác định xem giá trị có phải là NaN hay không. Thay vào đó, hãy sử dụng hàmisNaN ():
console.log (NaN == NaN); // false console.log (NaN === NaN); // false console.log (isNaN (NaN)); / / true
Sai lầm thứ 5: Inefficient DOM manipulation
JavaScript dễ dàng thao tác với DOM (tức là thêm, sửa đổi và loại bỏ các elements), nhưng không làm gì để thúc đẩy làm việc hiệu quả như vậy. Một ví dụ phổ biến là thêm một loạt các phần tử vào DOM mỗi lần. Thêm một DOM element là một hoạt động tốn kém. Và thêm nhiều DOM elements liên tiếp là không hiệu quả và có khả năng hoạt động không tốt. Một thay đổi hiệu quả khi cần phải thêm nhiều DOM elements là sử dụng document fragments thay vào đó, giúp cải thiện hiệu suất.
var div = document.getElementsByTagName("my_div"); var fragment = document.createDocumentFragment(); for (var e = 0; e < elems.length; e++) { // elems previously set to list of elements fragment.appendChild(elems[e]); } div.appendChild(fragment.cloneNode(true));
Ngoài hiệu quả được cải thiện của cách tiếp cận này, việc tạo các phần tử DOM kèm theo là tốn kém, trong khi đó tạo ra và sửa đổi chúng trong khi tách ra và sau đó gắn chúng mang lại hiệu suất tốt hơn nhiều.
Reference
https://www.toptal.com/javascript/10-most-common-javascript-mistakes https://viblo.asia/p/memory-leaks-trong-javascript-ZnbRlrj9G2Xo https://viblo.asia/p/tim-hieu-sau-hon-ve-scope-javascript-Qbq5QrRwKD8