12/08/2018, 17:18

[JavaScript] Some mysteries may make you confused!

Trong quá trình làm việc với JS, tôi đã từng nhiều lần gặp những đoạn code khá hay ho thú vị mà có lẽ chỉ trong JS mới có. Có thể là 1 work-through độc đáo, cũng có thể chỉ là 1 đoạn code kỳ lạ. Bài viết này tôi xin tổng hợp lại những trường hợp đã gặp và cho là nó sẽ hữu ích đối với mọi người! ...

Trong quá trình làm việc với JS, tôi đã từng nhiều lần gặp những đoạn code khá hay ho thú vị mà có lẽ chỉ trong JS mới có. Có thể là 1 work-through độc đáo, cũng có thể chỉ là 1 đoạn code kỳ lạ.

Bài viết này tôi xin tổng hợp lại những trường hợp đã gặp và cho là nó sẽ hữu ích đối với mọi người!

Comma Operator

Trong 1 lần dò dẫm trong file bundle được build ra từ webpack, tôi bắt gặp đoạn code gần gần như thế này:

(0, function (arg) { ... })(this)

Thắc mắc đặt ra ở đây là tại sao phải cần dùng cái 0, ở đằng trước, thử nghiệm khi chạy đoạn code này thì cho 2 kết quả giống nhau, nhưng liệu có phải nó thừa không nhỉ?

(0, function () { return 1 })(); // return 1;
function () { return 1 }(); // return 1;

Explain

Dấu , trong JavaScript được gọi là Comma Operator, cách hoạt động của nó khá giống với && và ||, đó là trả về giá trị cuối cùng của 1 expression.

//(VT: Vế trái, VP: Vế phải)

VT && VP

  1. Luôn luôn đánh giá VT
  2. Nếu VTtrue, đánh giá VP

VT || VP

  1. Luôn luôn đánh giá VT
  2. Nếu VTfalse, đánh giá VP

VT , VP

  1. Luôn luôn đánh giá VT
  2. Luôn luôn đánh giá VP

Example:

(0 && 1); // a = 0
(0 || 1); // a = 1
(0, 1); // a = 1

Với đại đa số các trường hợp, Comma Operator có vẻ như thừa thãi. Nhưng nó đặc biệt hữu dụng trong 1 số trường hợp sau:

  • Loại bỏ ngữ cảnh thực thi của method, đảm bảo method luôn được thực thi trong global.
var obj = {
  method: function() { return this; }
};
console.log(obj.method() === obj);     // true
console.log((0,obj.method)() === obj); // false
  • Bởi vì (0, expression) trả về 1 giá trị, không phải 1 reference, bởi thế nó giúp thực thi câu lệnh eval() 1 cách gián tiếp Indirect eval call. Theory
var x = 'outer';
(function() {
  var x = 'inner';
  eval('console.log("direct call: " + x)'); 
  (1,eval)('console.log("indirect call: " + x)'); 
})();

Việc gọi eval() gián tiếp giúp cho câu lệnh mà nó thực thi được đặt trong ngữ cảnh global. Ứng dụng thường gặp nhất là định nghĩa global object ở bất cứ đâu trong source code.

var global = (function () {  
    return this || (1, eval)('this');  
}());  

debugger; vs console.log()

Có 1 lần tôi debug, ứng dụng của mình và gặp 1 trường hợp là console.log() thì log ra được biến, còn chạy đặt breakpoint thì không tài nào log ra được biến. Thật sự kỳ lạ ?!?

Sau khi mò mẫm 1 hồi thì tìm được 1 issue trên group của v8 engine https://bugs.chromium.org/p/v8/issues/detail?id=3491

Explain

Tóm tắt lại thì v8 có thể lưu các biến vào stack hoặc heap (giải thích kỹ hơn ở bài này). Vấn đề ở đây là nó chỉ lưu các biến trên stack khi mà các function con (inner functions) không tham chiếu tới biến này. Theo như giải thích của v8 dev team thì đây là Optimization Feature nhằm tối ưu bộ nhớ. Nếu như có bất cứ inner function nào đề cập tới outer variables, thì biến này sẽ được đưa vào heap. Ngoại trừ trường hợp ở inner function có gọi đến hàm eval(), lúc này toàn bộ các biến mà inner function có khả năng truy cập đến (inner và outer variables) sẽ được đưa ngay vào heap. Việc đưa các biến outer vào heap giúp bạn có thể tạo ra closure với môi trường riêng ngay cả khi function tạo ra nó đã được thực thi xong và bị xóa khỏi stack.

Không may là debugger không thể inpsect được các biến được lưu trên stack, và để pass qua bug/feature này bạn cần phải đề cập đến biến muốn log ở trong inner function, hoặc sử dụng eval('debugger')

Parentheses position matters

Hãy nhìn vào ví dụ này và thử trả lời phép so sánh xem sao ?

function foo() {
   return
   {
      foo: 'bar'
   }
}

function bar() {
   return {
      foo: 'bar'
   }
}

typeof foo() === typeof bar(); // true hay false nhỉ?

Kết quả của phép return trong function foo() là undefined, chứ không phải là 1 object như chúng ta tưởng, đó là bởi vì câu lệnh return đã không thể nhìn thấy cái gì để trả về do chúng ta đã viết dấu { ở dòng dưới.

Không như nhiều ngôn ngữ, JavaScript cho phép developer không viết dấu ; để kết thúc câu lệnh cũng như 1 câu lệnh có thể viết thành nhiều dòng, tự do quá lại là vấn đề gây confused phải không?

Đây là 1 trong những lý do mà good coding style trong JavaScript là đặt dấu mở ngoặc cùng dòng với câu lệnh.

Immediately-Invoked Function Expression

1 Cú pháp khá lạ khi lần đầu tiên nhìn vào, mới nhìn lại tưởng viết nhầm mất

;(functions () {
    //Some stuff
}());

Tuy nhiên, đó chính xác là 1 thói quen tốt khi bạn viết plugins, hay bổ sung code của 1 người khác, Nó nói lên bạn đang chính thức bắt đầu viết 1 câu lệnh mới. Thử hình dung bạn có 1 đoạn black-box script có sẵn như sau:

function foo() { alert(arguments[0]); } foo

Và bạn bắt đầu viết code của mình nối tiếp vào

(function () { return 'foo'; }());

Kết quả nhận được không như mong đợi, do bạn đã vô tình gọi hàm foo() trong blackbox-script, để giải quyết đơn giản chỉ cần thêm dấu ; trước những đoạn code của mình             </div>
            
            <div class=

0