Khai báo biến: var, const, let

Trong JavaScript, có 3 keyword có thể dùng để khai báo biến và mỗi keyword lại mang ý nghĩa khác nhau. Đó là var, letconst.

Giải thích ngắn gọn

Các biến được khai báo bằng keyword const không thể được gán lại giá trị, trong khi các biến được khai báo bằng letvar thì có thể. Tôi gợi ý là mặc định thì chúng ta nên khai báo biến bằng const còn khai báo biến bằng let nếu chúng ta cần thay đổi giá trị của biến đó (mutate hoặc reassign) về sau.

Scope Reassignable Mutable Temporal Dead Zone
const Block No Yes Yes
let Block Yes Yes Yes
var Function Yes Yes No

Ví dụ:

const person = "Nick";
person = "John" // Sẽ báo lỗi, biến person không thể được gán lại giá trị
let person = "Nick";
person = "John";
console.log(person) // "John", biến được khai báo bởi let có thể được gán lại giá trị

Giải thích chi tiết

Scope của 1 biến được định nghĩa là phạm vi mà biến đó có thể được sử dụng ở trong code.

var

Các biến được khai báo bằng varfunction scoped, nghĩa là khi biến đó được khai báo trong hàm thì tất cả mọi thứ trong hàm đều có thể truy cập được giá trị của nó. Ngược lại, biến đó không thể được truy cập từ bên ngoài hàm. Ví dụ:

function myFunction() {
  var myVar = "Nick";
  console.log(myVar); // "Nick" - myVar có thể được truy cập ở bên trong hàm
}
console.log(myVar); // Báo lỗi ReferenceError, myVar không thể được truy cập từ bên ngoài hàm

Dưới đây là 1 ví dụ khác:

function myFunction() {
  var myVar = "Nick";
  if (true) {
    var myVar = "John";
    console.log(myVar); // "John"
    // myVar có phạm vi sử dụng trong hàm, chúng ta vừa mới xoá giá trị cũ của nó ("Nick") và thay bằng giá trị mới ("John")
  }
  console.log(myVar); // "John" - đoạn xử lý trong block if đã thay đổi giá trị của biến
}
console.log(myVar); // Báo lỗi ReferenceError, myVar không thể được truy cập từ bên ngoài hàm

Ngoài ra, các biến được khai báo bằng var sẽ được chuyển lên trên cùng của scope khi chạy code. Điều này được gọi là var hoisting. Đoạn code này

console.log(myVar) // undefined -- không báo lỗi
var myVar = 2;

sẽ được hiểu là

var myVar;
console.log(myVar) // undefined -- không báo lỗi
myVar = 2;

let

varlet khá giống nhau nhưng các biến được khai báo bằng let thì

  • block scoped
  • không thể được truy cập trước khi được gán trá trị
  • không thể được khai báo lại trong cùng 1 scope.

Ví dụ:

function myFunction() {
  let myVar = "Nick";
  if (true) {
    let myVar = "John";
    console.log(myVar); // "John"
    // myVar có phạm vi sử dụng trong block, chúng ta vừa mới tạo ra 1 biến myVar mới
    // biến myVar này không thể được truy cập từ bên ngoài block này và hoàn toàn độc lập với biến myVar mà chúng ta tạo lúc đầu
  }
  console.log(myVar); // "Nick - đoạn xử lý trong block if đã KHÔNG thay đổi giá trị của biến
}
console.log(myVar); // Báo lỗi ReferenceError, myVar không thể được truy cập từ bên ngoài hàm

Các biến được khai báo bằng let (hoặc const) không thể được truy cập trước khi được gán trá trị:

console.log(myVar) // Báo lỗi ReferenceError !
let myVar = 2;

Trái ngược với các biến khai báo bằng var, sẽ xảy ra lỗi nếu bạn cố đọc hay ghi các biến khai bảo bởi let (hoặc const) trước khi chúng được gán giá trị. Hiện tượng này được gọi là Temporal dead zone hoặc TDZ.

Một chú ý nữa là bạn không thể khai báo lại 1 biến đã được khai báo bằng let.

let myVar = 2;
let myVar = 3; // Báo lỗi SyntaxError

const

Các biến được khai báo bằng const thì cũng giống các biến được khai báo bằng let, ngoài ra thì chúng không thể được gán lại giá trị. Điều đó có nghĩa là chúng có các đặc điểm sau:

  • block scoped
  • không thể được truy cập trước khi được gán trá trị
  • không thể được khai báo lại trong cùng 1 scope
  • không thể được gán lại giá trị.
const myVar = "Nick";
myVar = "John" // Báo lỗi, không thể gán lại giá trị cho biến
const myVar = "Nick";
const myVar = "John" // Báo lỗi, không thể khai báo lại biến

Có 1 điểm cần chú ý ở đây: các biến được khai báo bằng const không phải là immutable. Cụ thể hơn thì object hoặc array khai báo bằng const có thể bị thay đổi giá trị.

Ví dụ với object:

const person = {
  name: 'Nick'
};
person.name = 'John' // Không báo lỗi! Biến person không bị gán lại giá trị hoàn toàn mà chỉ bị thay đổi giá trị
console.log(person.name) // "John"
person = "Sandra" // Báo lỗi vì không thể gán lại giá trị cho biến khai báo bằng const

Ví dụ với array:

const person = [];
person.push('John'); // Không báo lỗi! Biến person không bị gán lại giá trị hoàn toàn mà chỉ bị thay đổi giá trị
console.log(person[0]) // "John"
person = ["Nick"] // Báo lỗi vì không thể gán lại giá trị cho biến khai báo bằng const

Tham khảo

Arrow function

Bản update ES6 JavaScript đã giới thiệu arrow function - 1 cách khác để khai báo và sử dụng hàm trong JavaScript. Arrow function mang đến những lợi ích như

  • Ngắn gọn, súc tích hơn
  • this có thể được lấy từ ngữ cảnh bao quanh
  • return ngầm (implicit return)

Ví dụ

  • Ngắn gọn và return ngầm
function double(x) { return x * 2; } // Cách truyền thống
console.log(double(2)) // 4
const double = x => x * 2; // Cùng 1 hàm nhưng viết dưới dạng arrow function với implicit return
console.log(double(2)) // 4
  • Tham chiếu this

Ở trong arrow function, this bằng với this của ngữ cảnh bao quanh. Với arrow function, bạn không cần phải dùng tới trò that = this trước khi gọi 1 hàm trong 1 hàm khác nữa.

function myFunc() {
  this.myVar = 0;
  setTimeout(() => {
    this.myVar++;
    console.log(this.myVar) // 1
  }, 0);
}

Giải thích chi tiết

Ngắn gọn, súc tích

Arrow function ngắn gọn hơn function truyền thống theo nhiều cách khác nhau. Hãy cùng xem qua các trường hợp dưới đây.

Implicit và Explicit return

Explicit return là khi chúng ta sử dụng keyword return trong một hàm.

function double(x) {
  return x * 2; // Hàm này trả về x * 2 một cách rõ ràng thông qua việc sử dụng keyword return 
}

Trong cách viết hàm truyền thống, return luôn là explicit nhưng đối với arrow function chúng ta có thể implicit return - trả về giá trị mà không cần sử dụng keyword return.

const double = (x) => {
  return x * 2; // Explicit return
}

Vì hàm này chỉ return giá trị (không xử lý gì trước keyword return) nên chúng ta có thể viết theo kiểu implicit return

const double = (x) => x * 2; // Trả về x*2

Chúng ta chỉ cần bỏ cặp dấu {} và keyword return. Đó cũng là lí do vì sao chúng ta gọi đó là return ngầm, tuy không có keyword return nhưng hàm này vẫn trả về x*2.

Chú ý là nếu bạn muốn return ngầm 1 object thì phải có dấu ngoặc () xung quanh nó.

const getPerson = () => ({ name: "Nick", age: 24 })
console.log(getPerson()) // { name: "Nick", age: 24 } -- object được return ngầm bởi arrow function

Khi hàm có 1 tham số

Khi hàm chỉ có 1 tham số chúng ta có thể bỏ cặp dấu () xung quanh tham số. Ví dụ với hàm double ở trên

const double = (x) => x * 2;

chúng ta có thể viết thành

const double = x => x * 2;

Khi hàm không có tham số

Khi hàm không có tham số thì chúng ta bắt buộc phải dùng cặp dấu () xung quanh tham số nếu không sẽ bị lỗi cú pháp.

() => { // Có dấu ngoặc, cú pháp hợp lệ
  const x = 2;
  return x;
}
=> { // Không có dấu ngoặc, cú pháp không hợp lệ
  const x = 2;
  return x;
}

Tham chiếu this

Ở trong 1 arrow function, this bằng với this của ngữ cảnh bao quanh. Điều đó có nghĩa là 1 arrow function không tạo ra this mới mà nó tự lấy từ xung quanh nó.

Nếu không có arrow function, khi muốn truy cập đến biến của this trong 1 hàm nằm trong hàm khác, chúng ta phải dùng đến trò that = this hoặc self = this.

Ví dụ sử dụng hàm setTimeout trong hàm myFunc:

function myFunc() {
  this.myVar = 0;
  var that = this; // that = this trick
  setTimeout(
    function() { // 1 this mới được tạo ra trong phạm vi hàm này
      that.myVar++;
      console.log(that.myVar) // 1

      console.log(this.myVar) // undefined - xem lại khai báo hàm ở phía trên
    },
    0
  );
}

Với arrow function, this sẽ được lấy từ ngữ cảnh bao quanh:

function myFunc() {
  this.myVar = 0;
  setTimeout(
    () => { // this lấy từ ngữ cảnh bao quanh (myFunc)
      this.myVar++;
      console.log(this.myVar) // 1
    },
    0
  );
}

Tham khảo

Giá trị mặc định của tham số hàm

Kể từ JavaScript ES2015, chúng ta có thể set giá trị mặc định cho tham số của hàm bằng cú pháp dưới đây

function myFunc(x = 10) {
  return x;
}
console.log(myFunc()) // 10 -- không có giá trị được truyền vào nên x được gán giá trị mặc định 10
console.log(myFunc(5)) // 5 -- có giá trị được truyền vào nên x được gán giá trị 5

console.log(myFunc(undefined)) // 10 -- giá trị undefined được truyền vào nên x được gán giá trị mặc định 10
console.log(myFunc(null)) // null -- giá trị null được truyền vào, xem chi tiết ở dưới

Giá trị mặc định của tham số được sử dụng trong 2 trường hợp:

  • Không có giá trị được truyền vào hàm
  • Giá trị undefined được truyền vào hàm

Nếu bạn truyền vào giá trị null thì giá trị mặc định của tham số cũng không được sử dụng đến.

Tham khảo

Destructuring object và array

Destructuring là 1 cách thuận tiện để tạo ra các biến mới bằng cách tách giá trị được lưu trữ trong object hoặc array.

Object

Hãy cùng xem xét object sau

const person = {
  firstName: "Nick",
  lastName: "Anderson",
  age: 35,
  sex: "M"
}

Khi không dùng destructuring:

const first = person.firstName;
const age = person.age;
const city = person.city || "Paris";

Khi dùng destructuring:

const { firstName: first, age, city = "Paris" } = person;

console.log(age) // 35 -- Một biến age mới đã được tạo ra và có giá trị bằng với person.age
console.log(first) // "Nick" -- Một biến first mới đã được tạo ra và có giá trị bằng với person.firstName
console.log(firstName) // ReferenceError -- person.firstName tồn tại nhưng biến mới được tạo tên là first
console.log(city) // "Paris" -- Một biến city mới đã được tạo ra. Vì person.city là undefined, city nhận giá trị mặc định "Paris"

Chú ý: Trong const { age } = person; thì cặp dấu {} không dùng để khai báo 1 object hay 1 block mà nó là cú pháp destructuring.

Tham số hàm

Destructuring thường được dùng để tách object truyền vào hàm thành các biến.

Khi không dùng destructuring:

function joinFirstLastName(person) {
  const firstName = person.firstName;
  const lastName = person.lastName;
  return firstName + '-' + lastName;
}


joinFirstLastName(person); // "Nick-Anderson"

Khi dùng destructuring chúng ta có 1 hàm ngắn gọn hơn:

function joinFirstLastName({ firstName, lastName }) { // Chúng ta tạo 2 biến mới firstName và lastName bằng cách tách tham số person
  return firstName + '-' + lastName;
}

joinFirstLastName(person); // "Nick-Anderson"

Destructuring cũng có thể được sử dụng cùng với arrow function:

const joinFirstLastName = ({ firstName, lastName }) => firstName + '-' + lastName;

joinFirstLastName(person); // "Nick-Anderson"

Array

Ví dụ với array dưới

const myArray = ["a", "b", "c"];

Khi không dùng destructuring:

const x = myArray[0];
const y = myArray[1];

Khi dùng destructuring:

const [x, y] = myArray;

console.log(x) // "a"
console.log(y) // "b"

Tham khảo

Array methods: map, filter, reduce

map, filter, reduce là những array method bắt nguồn từ 1 mẫu hình lập trình có tên gọi functional programming.

Giới thiệu khái quát thì

  • Array.prototype.map() nhận vào 1 mảng, tiến hành xử lí gì đó với các phần tử của mảng và trả về 1 mảng với các phần tử đã được xử lí.
  • Array.prototype.filter() nhận vào 1 mảng, kiểm tra và quyết định xem có giữ lại từng phần tử của mảng hay không, kết quả trả về là 1 mảng chỉ bao gồm những phần tử được giữ lại.
  • Array.prototype.reduce() nhận vào 1 mảng và gộp các phần tử của mảng thành 1 giá trị duy nhất rồi trả về giá trị đó.

Với 3 method này chúng ta có thể tránh việc sử dụng vòng lặp for hoặc forEach trong hầu hết các trường hợp.

Ví dụ

const numbers = [0, 1, 2, 3, 4, 5, 6];
const doubledNumbers = numbers.map(n => n * 2); // [0, 2, 4, 6, 8, 10, 12]
const evenNumbers = numbers.filter(n => n % 2 === 0); // [0, 2, 4, 6]
const sum = numbers.reduce((prev, next) => prev + next, 0); // 21

Ví dụ khác: tính tổng điểm của những học sinh có điểm trên 10 bằng cách kết hợp map, filterreduce

const students = [
  { name: "Nick", grade: 10 },
  { name: "John", grade: 15 },
  { name: "Julia", grade: 19 },
  { name: "Nathalie", grade: 9 },
];

const aboveTenSum = students
  .map(student => student.grade) // map mảng students thành 1 mảng chứa điểm của những học sinh đó
  .filter(grade => grade >= 10) // filter mảng chứa điểm và giữ lại những điểm trên 10
  .reduce((prev, next) => prev + next, 0); // tính tổng của những điểm trên 10

console.log(aboveTenSum) // 44 -- 10 (Nick) + 15 (John) + 19 (Julia), Nathalie có điểm dưới 10 nên không tính

Giải thích

Chúng ta sẽ sử dụng mảng sau trong các các ví dụ dưới đây

const numbers = [0, 1, 2, 3, 4, 5, 6];

Array.prototype.map()

const doubledNumbers = numbers.map(function(n) {
  return n * 2;
});
console.log(doubledNumbers); // [0, 2, 4, 6, 8, 10, 12]

Ở đây chúng ta sử dụng map đối với mảng numbers, nó duyệt qua từng phần tử của mảng và truyền phần tử đó vào hàm. Mục đích của hàm là nhân phần tử được truyền vào với 2 và trả về giá trị mới.

Chúng ta có thể viết lại ví dụ này cho rõ ràng hơn như sau:

const doubleN = function(n) { return n * 2; };
const doubledNumbers = numbers.map(doubleN);
console.log(doubledNumbers); // [0, 2, 4, 6, 8, 10, 12]

numbers.map(doubleN) trả về [doubleN(0), doubleN(1), doubleN(2), doubleN(3), doubleN(4), doubleN(5), doubleN(6)] tức là bằng với [0, 2, 4, 6, 8, 10, 12].

Ngoài ra, chúng ta có thể sẽ thường thấy method này dùng chung với arrow function

const doubledNumbers = numbers.map(n => n * 2);
console.log(doubledNumbers); // [0, 2, 4, 6, 8, 10, 12]

Array.prototype.filter()

const evenNumbers = numbers.filter(function(n) {
  return n % 2 === 0;
});
console.log(evenNumbers); // [0, 2, 4, 6]

Ở đây chúng ta sử dụng filter đối với mảng numbers, nó duyệt qua từng phần tử của mảng và truyền phần tử đó vào hàm. Mục đích của hàm là quyết định xem phần tử được truyền vào có được giữ lại trong mảng hay không (nếu là số chẵn thì giữ lại). Sau đó filter trả về 1 mảng mới chỉ gồm các phần tử đã được giữ lại.

Tương tự như map, filter cũng thường được dùng chung với arrow function

const evenNumbers = numbers.filter(n => n % 2 === 0);
console.log(evenNumbers); // [0, 2, 4, 6]

Array.prototype.reduce()

Mục đích của method reduce là gộp các phần tử của mảng mà nó duyệt qua thành 1 giá trị duy nhất. Gộp như thế nào thì phụ thuộc vào cách chúng ta quy định.

const sum = numbers.reduce(
  function(acc, n) {
    return acc + n;
  },
  0 // giá trị của biến tích lũy acc ở lần lặp đầu tiên
);

console.log(sum) //21

Giống như method mapfilter, reduce được sử dụng với 1 mảng và nhận vào tham số thứ nhất là 1 hàm. Tuy nhiên có 1 điểm khác biệt đó là reduce nhận vào 2 tham số

  • Tham số đầu tiên là 1 hàm sẽ được gọi ở mỗi lần lặp,
  • Tham số thứ 2 là giá trị của biến tích lũy (acc trong ví dụ trên) ở lần lặp đầu tiên.

Hàm được truyền vào reduce cũng nhận vào 2 tham số. Tham số đầu tiên (acc) là biến tích lũy, tham số thứ 2 là phần tử hiện tại (n). Giá trị của biến tích lũy bằng với giá trị trả về của hàm ở lần lặp trước đó. Ở lần lặp đầu tiên acc bằng với giá trị của tham số thứ 2 được truyền vào method reduce.

Tham khảo

Spread operator, rest operator

Spread operator và rest operator có cùng cú pháp ... và được giới thiệu từ ES2015. Spread operator được sử dụng để mở rộng 1 iterable (ví dụ như 1 mảng) thành nhiều phần tử còn rest operator thì ngược lại, nó được sử dụng để gom nhiều phần tử thành 1 iterable.

Ví dụ

const arr1 = ["a", "b", "c"];
const arr2 = [...arr1, "d", "e", "f"]; // ["a", "b", "c", "d", "e", "f"]
function myFunc(x, y, ...params) {
  console.log(x);
  console.log(y);
  console.log(params)
}

myFunc("a", "b", "c", "d", "e", "f")
// "a"
// "b"
// ["c", "d", "e", "f"]
const { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
console.log(x); // 1
console.log(y); // 2
console.log(z); // { a: 3, b: 4 }

const n = { x, y, ...z };
console.log(n); // { x: 1, y: 2, a: 3, b: 4 }

Giải thích

Iterable

Nếu chúng ta có 2 mảng như dưới

const arr1 = ["a", "b", "c"];
const arr2 = [arr1, "d", "e", "f"]; // [["a", "b", "c"], "d", "e", "f"]

Phần tử đầu tiên của arr2 là 1 mảng bởi arr1 được đưa nguyên vào arr2. Nếu chúng ta muốn arr2 là mảng các kí tự thì phải làm thế nào? Với spread operator chúng ta có thể làm như sau

const arr1 = ["a", "b", "c"];
const arr2 = [...arr1, "d", "e", "f"]; // ["a", "b", "c", "d", "e", "f"]

Function rest parameter

Ở tham số hàm, chúng ta có thể dùng rest operator để gom nhiều phần tử thành 1 mảng. Hãy cùng xem ví dụ sau:

function myFunc() {
  for (var i = 0; i < arguments.length; i++) {
    console.log(arguments[i]);
  }
}

myFunc("Nick", "Anderson", 10, 12, 6);
// "Nick"
// "Anderson"
// 10
// 12
// 6

Javascript cung cấp sẵn 1 object arguments cho mỗi hàm và nó chứa tất cả các tham số được truyền vào hàm.

Giả sử bây giờ chúng ta muốn hàm này trả về 1 học sinh với thông tin về nhiều điểm số và điểm trung bình. Khi đó sẽ tiện hơn nếu chúng ta tách 2 tham số đầu tiên thành 2 biến riêng biệt và tách các tham số còn lại thành 1 mảng mà chúng ta có thể duyệt qua. Đó chính xác là những gì rest operator cho phép chúng ta làm.

function createStudent(firstName, lastName, ...grades) {
  // firstName = "Nick"
  // lastName = "Anderson"
  // [10, 12, 6] -- "..." nhận tất cả các tham số còn lại và tạo ra 1 mảng "grades" chứa chúng

  const avgGrade = grades.reduce((acc, curr) => acc + curr, 0) / grades.length; // computes average grade from grades

  return {
    firstName: firstName,
    lastName: lastName,
    grades: grades,
    avgGrade: avgGrade
  }
}

const student = createStudent("Nick", "Anderson", 10, 12, 6);
console.log(student);
// {
//   firstName: "Nick",
//   lastName: "Anderson",
//   grades: [10, 12, 6],
//   avgGrade: 9,33
// }

Object properties spreading

const myObj = { x: 1, y: 2, a: 3, b: 4 };
const { x, y, ...z } = myObj; // object destructuring
console.log(x); // 1
console.log(y); // 2
console.log(z); // { a: 3, b: 4 }

// z là phần còn lại của object đã được destructure

const n = { x, y, ...z };
console.log(n); // { x: 1, y: 2, a: 3, b: 4 }

// Ở đây các thuộc tính của z được mở rộng vào n

Tham khảo

Object property shorthand

Khi gán 1 biến vào 1 thuộc tính của 1 object, nếu tên biến trùng với tên của thuộc tính thì chúng ta có thể viết ngắn gọn như sau

const x = 10;
const myObj = { x };
console.log(myObj.x) // 10

Giải thích

Trước ES2015 thì khi chúng ta muốn khai báo 1 object và gán các biến vào các thuộc tính chúng ta thường phải viết

const x = 10;
const y = 20;

const myObj = {
  x: x, // gán giá trị của biến x cho myObj.x
  y: y // gán giá trị của biến y cho myObj.y
};

console.log(myObj.x) // 10
console.log(myObj.y) // 20

Cách viết này hơi trùng lặp vì ở đây tên thuộc tính trùng với tên biến. Với ES2015 chúng ta có thể viết ngắn gọn lại:

const x = 10;
const y = 20;

const myObj = {
  x,
  y
};

console.log(myObj.x) // 10
console.log(myObj.y) // 20

Tham khảo

Template literals

Template literal là 1 cú pháp mới cho phép nội suy biểu thức JavaScript trong string.

Ví dụ

const name = "Nick";
`Hello ${name}, the following expression is equal to four : ${2+2}`;

// Hello Nick, the following expression is equal to four: 4

Tham khảo

Tagged template literals

Template tag là 1 function có thể được gắn trước 1 template literal. Khi function đó được gọi thì tham số đầu tiên là 1 mảng các string đứng xen kẽ các biến nội suy trong templage còn các tham số tiếp theo là các giá trị của các biến nội suy.

Ví dụ

function highlight(strings, ...values) {
  const interpolation = strings.reduce((prev, current) => {
    return prev + current + (values.length ? "<mark>" + values.shift() + "</mark>" : "");
  }, "");

  return interpolation;
}

const condiment = "jam";
const meal = "toast";

highlight`I like ${condiment} on ${meal}.`;
// "I like <mark>jam</mark> on <mark>toast</mark>."

// giá trị của tham số strings trong hàm highlight là ["I like ", " on ", "."]
// giá trị của tham số values trong hàm highlight là ["jam", "toast"]

1 ví dụ khác hay ho hơn:

function comma(strings, ...values) {
  return strings.reduce((prev, next) => {
    let value = values.shift() || [];
    value = value.join(", ");
    return prev + next + value;
  }, "");
}

const snacks = ['apples', 'bananas', 'cherries'];
comma`I like ${snacks} to snack on.`;
// "I like apples, bananas, cherries to snack on."

Tham khảo

Nguồn bài viết