Nội dung được giới thiệu trong bài viết này

Đây là bài viết thứ 2 trong loạt bài giới thiệu về TypeScript. Nối tiếp thành công vài bài viết thứ nhất giới thiệu về BasicType, lần này mình sẽ tổng hợp lại về khai báo biến(Variable Declarations) trong TypeScript.

var declarations

Thông thường khi khai báo biến trong javascript chúng ta có thể sử dụng từ khóa var.

var a = 10;

Trong method cũng có thể định nghĩa biến giống như vậy

function f() {
    var message = "Hello, world!";

    return message;
}

Và trong method khác cũng có thể truy cập đến biến đó

function f() {
    var a = 10;
    return function g() {
        var b = a + 1;
        return b;
    }
}

var g = f();
g(); // 11

Trong cùng môt lần, nếu sử lý của hàm f() kết thúc. Dù gọi hàm g() thì cũng có thể đọc và thay đổi biến a

function f() {
    var a = 1;

    a = 2;
    var b = g();
    a = 3;

    return b;

    function g() {
        return a;
    }
}

f(); // 2
  • Mình không biết là ví dụ đã đầy đủ chưa nhưng trong ví dụ này có thể hiểu thêm là khi thay thế b sau đó thay đổi a cũng không ảnh hưởng đến kết quả

Scoping rules

Biến khi khai báo bằng từ khóa var có phạm vi không giống với các ngôn ngữ. Biến, method, module, định danh không tên,... đã được khai báo thì có thể truy cập ở bất cứ đâu.

function f(shouldInitialize: boolean) {
    if (shouldInitialize) {
        var x = 10;
    }

    return x;
}

f(true);  // 10
f(false); // undefined (`var x = ...` mặc dù chưa được khai báo nhưng vẫn có thế truy cập được)

Ngoài ra, nhiều lần khai báo biến cũng không phát sinh lỗi

function sumMatrix(matrix: number[][]) {
    var sum = 0;
    for (var i = 0; i < matrix.length; i++) {
        var currentRow = matrix[i];
        for (var i = 0; i < currentRow.length; i++) { // Ở đây nó lại khai báo lần nữa
            sum += currentRow[i];
        }
    }

    return sum;
}

Variable capturing quirks (những kiểu bắt biến không minh bạch)

Liệu sau khi thực hiện đoặn code gì sẽ xẩy ra?

for (var i = 0; i < 10; i++) {
    setTimeout(function() { console.log(i); }, 100 * i);
}

Kết quả thu được sẽ như thế này:

10
10
10
10
10
10
10
10
10
10

Đây là một lỗi thường gặp mà giá trí biến giống nhau không như mong muốn. Để tránh những lỗi như này xẩy ra nên dùng IIFE. Đây là pattern khá phổ biến để 1 function expression và thực thi function đó ngay lập tức. Pattern này đặc biệt hữu dụng khi bạn không muốn làm lộn xộn global namespace, bởi mọi biến hay function trong function đó đều không thể được truy cập từ bên ngoài.

for (var i = 0; i < 10; i++) {
    // Biên `i` này sẽ được chuyền giá trị vào function
    // Ở đây sẽ bắt biến đã chuyền vào
    (function(i) {
        setTimeout(function() { console.log(i); }, 100 * i);
    })(i);
}

let declarations

Để giải quyết vấn đề của biến var, TypeScript sử dụng từ khóa let

TypeScript

let hello = "Hello!";

JavaScript

var hello = "Hello!";

Block-scoping

Khác với var, dùng let để khai báo biên có phạm vi cụ thể.

function f(input: boolean) {
    let a = 100;

    if (input) {
        // Có thể dùng biên 'a' ở đấy
        let b = a + 1;
        return b;
    }

    // Lỗi, Không thể sử dụng biến 'b ở đây'
    return b;
}

Trong khối catch cũng áp dụng quy tắc tương tự

try {
    throw "oh no!";
}
catch (e) {
    console.log("Oh well.");
}

// Lỗi, Không thể sử dụng biến 'e' ở đây
console.log(e);

Một điểm đặc trưng khác nữa là không thể sử dụng biến nếu chưa được khai báo

a++; // Lỗi, biến 'a' chưa được định nghĩa
let a;

Trước khi khai báo biên vẫn có thể capture(không phải access luôn như trên) được nhưng khi gọi hàm để thực hiện nó thì vẫn bắt buộc phải khai báo

function foo() {
    // Chưa khai báo 'a' nhưng capture vẫn OK
    return a;
}

// gọi hàm foo() trước khi khai báo 'a'
// Khi thực hiện sẽ bi lỗi
foo();

let a;

Re-declarations and Shadowing

Dùng var có thể khai báo nhiều lần cũng không lỗi.

function f(x) {
    var x;
    var x;

    if (true) {
        var x;
    }
}

Khai báo bằng let chỉ có thể khai báo 1 lần

let x = 10;
let x = 20; // Lỗi, Trong cùng một block chỉ được khai báo biến 1 lần
function f(x) {
    let x = 100; // Lỗi, bị lập lại với tham số truyền vào hàm
}

function g() {
    let x = 100;
    var x = 100; // Lỗi. Đã khai báo x bằng let rồi
}

Tuy nhiên có thể khai báo biến trong block khác nhau

function f(condition, x) {
    if (condition) {
        let x = 100;
        return x;
    }

    return x;
}

f(false, 0); // 0 (Trả về tham số 'x')
f(true, 0);  // 100 (Trả về 'x' trong câu lệnh if)

Việc mà khai báo biến trong các block lồng nhau gọi la Shadowing. Thông thường nên tránh làm vậy nhưng trong một số trường hợp nó lại có ích.

TypeScript

function sumMatrix(matrix: number[][]) {
    let sum = 0;
    for (let i = 0; i < matrix.length; i++) {
        var currentRow = matrix[i];
        for (let i = 0; i < currentRow.length; i++) { // Lại khai báo 'i'
            sum += currentRow[i];
        }
    }
;
    return sum;
}

JavaScript

function sumMatrix(matrix) {
    var sum = 0;
    for (var i = 0; i < matrix.length; i++) {
        var currentRow = matrix[i];
        for (var i_1 = 0; i_1 < currentRow.length; i_1++) {
            sum += currentRow[i_1];
        }
    }
    return sum;
}
  • Tên biến tự động đổi tên bằng cách thêm số vào

Block-scoped variable capturing

Cũng giống như phần var declarations mình đã giới thiệu rồi. Biến mà được khai báo 1 lần rồi sau khi gọi nó ở block con khác thì vẫn có thể truy cập được.

function theCityThatAlwaysSleeps() {
    let getCity;

    if (true) {
        let city = "Seattle";
        getCity = function() {
            return city;
        }
    }

    return getCity();
}

Nếu sử dụng từ khóa let trong vòng lặp, mỗi lần lặp nó đều tạo 1 phạm vi mới cho biến

TypeScript

for (let i = 0; i < 10 ; i++) {
    setTimeout(function() { console.log(i); }, 100 * i);
}

JavaScript

var _loop_1 = function (i) {
    setTimeout(function () { console.log(i); }, 100 * i);
};
for (var i = 0; i < 10; i++) {
    _loop_1(i);
}
0
1
2
3
4
5
6
7
8
9

const declarations

const cũng giống với let nhưng giống như tên của nó dùng để khai báo hằng số

TypeScript

const numLivesForCat = 9;

JavaScript

var numLivesForCat = 9;

Dùng từ khóa này sẽ giúp tránh những nhầm lẫn không đáng có

const numLivesForCat = 9;
const kitty = {
    name: "Aurora",
    numLives: numLivesForCat,
}

// Lỗi, không thể thay đổi 'kitty'
kitty = {
    name: "Danielle",
    numLives: numLivesForCat
};

// Tất cả đều OK, 'kitty' là hằng số nhưng thuộc tính của nó không phải hằng số.
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;

Thuộc tính không phải là hằng số nhưng để sử dụng trong thực tế có thể thêm keyword readonly vào thuộc tính của object.

Destructuring

TypeScript thực hiện việc phân chia, tách biến mà ECMAScript2015 không làm được

Array destructuring

Phân tách biến được thực hiện đơn giản giống như ví dụ dưới đây.

TypeScript

let input = [1, 2];
let [first, second] = input;
console.log(first); // 1
console.log(second); // 2

JavaScript

var input = [1, 2];
var first = input[0], second = input[1];
console.log(first); // 1
console.log(second); // 2

Có thể cập nhật lại các biến đã khai báo rồi.

TypeScript

// Đổi giá trị biến
[first, second] = [second, first];

JavaScript

// Đổi giá trị biến
_a = [second, first], first = _a[0], second = _a[1];
var _a;
  • Việc phải khai báo biến tạm thời hơi khó chịu

Thàm só truyền vào hàm cũng có thể làm vậy.

TypeScript

function f([first, second]: [number, number]) {
    console.log(first);
    console.log(second);
}
f(input);

JavaScript

// Đổi giá trị biến
function f(_a) {
    var first = _a[0], second = _a[1];
    console.log(first);
    console.log(second);
}
f(input);

Sử dụng ... để lấy các phần còn lại.

TypeScript

let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // 1
console.log(rest); // [ 2, 3, 4 ]

JavaScript

var _a = [1, 2, 3, 4], first = _a[0], rest = _a.slice(1);
console.log(first); // 1
console.log(rest); // [ 2, 3, 4 ]

Bỏ bớt đi phần tử phía sau

TypeScript

let [first] = [1, 2, 3, 4];
console.log(first); // 1

JavaScript

var first = [1, 2, 3, 4][0];
console.log(first); // 1

Các phần tử khác cũng có thể bỏ đi được

TypeScript

let [, second, , fourth] = [1, 2, 3, 4];

JavaScript

var _a = [1, 2, 3, 4], second = _a[1], fourth = _a[3];

Object destructuring

Không chỉ mảng mà object cũng có thể phân tách các phần tử được.

  • Có chút khác biệt là các tên biến để nhận giá trị bắt buộc phải giống với tên thuộc tính của Object

TypeScript

let o = {
    a: "foo",
    b: 12,
    c: "bar"
}
let { a, b } = o;

JavaScript

var o = {
    a: "foo",
    b: 12,
    c: "bar"
};
var a = o.a, b = o.b;

Khi soạn lại các biến từ thuộc tính cũng giống như mảng, không cần khai báo cũng được.

TypeScript

({ a, b } = { a: "baz", b: 101 });

JavaScript

(_a = { a: "baz", b: 101 }, a = _a.a, b = _a.b);
var _a;
  • Chú ý nếu loại bỏ dấu ngoặc () thì compile sẽ không được ngon lắm

Giống như mảng dùng ... để lấy những phần tử còn lại

TypeScript

let { a, ...passthrough } = o;
let total = passthrough.b + passthrough.c.length;

JavaScript

var __rest = (this && this.__rest) || function (s, e) {
    var t = {};
    for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
        t[p] = s[p];
    if (s != null && typeof Object.getOwnPropertySymbols === "function")
        for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) if (e.indexOf(p[i]) < 0)
            t[p[i]] = s[p[i]];
    return t;
};
var a = o.a, passthrough = __rest(o, ["a"]);
var total = passthrough.b + passthrough.c.length;
  • Code sau khi được compile quả là ...
Property renaming

Có thể thay đổi tên thuộc tính lấy được

TypeScript

let { a: newName1, b: newName2 } = o;

JavaScript

var newName1 = o.a, newName2 = o.b;

Chú ý dấu : không được dùng để định nghĩa kiểu. Nếu muốn chỉ định kiểu cho nó sẽ viết như sau

TypeScript

let { a, b }: { a: string, b: number } = o;

JavaScript

var a = o.a, b = o.b;

Nếu đồng thời vừa đổi tên vừa chỉ định kiểu

TypeScript

let { a: newName1, b: newName2 }: { a: string, b: number } = o;
Default values

Trường hợp thuộc tính của Object chưa được gán giá trị, có thể để giá trị mặc định như sau

TypeScript

function keepWholeObject(wholeObject: { a: string, b?: number }) {
    // Trường hợp mà 'wholeObject' không tồn tại 'b' sẽ được gán mắc định là 1001
    let { a, b = 1001 } = wholeObject;
}

JavaScript

function keepWholeObject(wholeObject) {
    // Trường hợp mà 'wholeObject' không tồn tại 'b' sẽ được gán mắc định là 1001
    var a = wholeObject.a, _a = wholeObject.b, b = _a === void 0 ? 1001 : _a;
}

Function declarations

Có thể sử dụng chức năng phân tách biến để truyền tham số vào hàm

TypeScript

type C = { a: string, b?: number }
function f({ a, b }: C): void {
    // ...
}

JavaScript

function f(_a) {
    var a = _a.a, b = _a.b;
    // ...
}

Chú ý sử dụng giá trị mặc định một cách khôn ngoan

TypeScript

function f({ a, b } = { a: "", b: 0 }): void {
    // ...
}
f(); // OK. Có thể sử dụng giá trị mặc định là { a: "", b: 0 }

JavaScript

function f(_a) {
    var _b = _a === void 0 ? { a: "", b: 0 } : _a, a = _b.a, b = _b.b;
    // ...
}
f(); // OK. Có thể sử dụng giá trị mặc định là { a: "", b: 0 }

Có thể kết hợp với giá trị mặc định đối với Object

TypeScript

function f({ a, b = 0 } = { a: "" }): void {
    // ...
}
;
f({ a: "yes" }) // OK. Sử dụng phân tách mạc định b = 0
f() // OK. Sử dụng giá trị mặc định của tham số truyền vào và giá trị mặc định của Object
f({}) // Lỗi tham số truyền vào cần phải có thuộc tính 'a'

JavaScript

function f(_a) {
    var _b = _a === void 0 ? { a: "" } : _a, a = _b.a, _c = _b.b, b = _c === void 0 ? 0 : _c;
    // ...
}
;
f({ a: "yes" }) // OK. Sử dụng phân tách mạc định b = 0
f() // OK. Sử dụng giá trị mặc định của tham số truyền vào và giá trị mặc định của Object
f({}) // Lỗi tham số truyền vào cần phải có thuộc tính 'a'

Đến đây là cấp độ khó tin rồi

Spread

Toán tử spread không chỉ được dùng trong trường hợp phân tách biến từ mảng, ngược lại nó còn dùng để mở rộng từ mảng đã có vào trong mảng khác.

TypeScript

let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5]; // [0, 1, 2, 3, 4, 5]

JavaScript

var first = [1, 2];
var second = [3, 4];
var bothPlus = [0].concat(first, second, [5]); // [0, 1, 2, 3, 4, 5]

Còn có thể thêm vào Object

TypeScript

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" }; // { food: "rich", price: "$$", ambiance: "noisy" };

JavaScript

var __assign = (this && this.__assign) || Object.assign || function(t) {
    for (var s, i = 1, n = arguments.length; i < n; i++) {
        s = arguments[i];
        for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
            t[p] = s[p];
    }
    return t;
};
var defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
var search = __assign({}, defaults, { food: "rich" }); // { food: "rich", price: "$$", ambiance: "noisy" };

Những thuộc tính giống nhau, thì những thuộc tính đặt phía sau sẽ được ghi đè lên các thuộc tình phía trước theo thứ tự

TypeScript

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { food: "rich", ...defaults }; // { food: "spicy", price: "$$", ambiance: "noisy" };

JavaScript

var __assign = (this && this.__assign) || Object.assign || function(t) {
    for (var s, i = 1, n = arguments.length; i < n; i++) {
        s = arguments[i];
        for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
            t[p] = s[p];
    }
    return t;
};
var defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
var search = __assign({ food: "rich" }, defaults); // { food: "spicy", price: "$$", ambiance: "noisy" };

Sự hạn chế của mở rộng Object có 2 điểm. Một là việc lấy các thuộc tính không bao gồm các method

class C {
  p = 12;
  m() {
  }
}
let c = new C();
let clone = { ...c };
clone.p; // OK
clone.m(); // Lỗi

Hai là TypeScript không cho phép dùng với các tham số type từ các hàm generic. Có thể sẽ được khắc phục trong tương lai.