Bài 4.2: Kiểu dữ liệu và phạm vi trong JavaScript

Bài 4.2: Kiểu dữ liệu và phạm vi trong JavaScript
Chào mừng các bạn quay trở lại với hành trình chinh phục Lập trình Web Front-end cùng FullhouseDev! Sau khi làm quen với những cú pháp cơ bản của JavaScript, hôm nay chúng ta sẽ đào sâu vào hai khái niệm _cốt lõi_ và _quan trọng bậc nhất_ mà mọi lập trình viên JS đều phải nắm vững: Kiểu dữ liệu (Data Types) và Phạm vi (Scope).
Hiểu rõ kiểu dữ liệu giúp bạn biết _loại thông tin_ mà biến đang lưu trữ và _những thao tác_ nào có thể thực hiện trên nó. Nắm vững phạm vi giúp bạn quản lý _quyền truy cập_ của biến và hàm trong các phần khác nhau của chương trình, từ đó tránh được những lỗi không đáng có và viết code gọn gàng, dễ bảo trì hơn. Hãy cùng bắt đầu!
1. Kiểu dữ liệu trong JavaScript: Nền tảng của mọi thông tin
Mọi giá trị trong JavaScript đều có một kiểu dữ liệu (data type) nhất định. Kiểu dữ liệu này cho biết bản chất của giá trị đó là gì – có phải là số, là chữ, là một tập hợp, hay là một thứ gì đó khác. JavaScript là ngôn ngữ có kiểu dữ liệu động (dynamically typed), nghĩa là bạn không cần khai báo kiểu dữ liệu khi tạo biến (let
, const
, var
). Kiểu dữ liệu sẽ được xác định tự động dựa trên giá trị mà bạn gán cho biến đó.
JavaScript có hai loại kiểu dữ liệu chính:
- Kiểu dữ liệu nguyên thủy (Primitive Types): Các giá trị đơn giản, không phải là đối tượng và không có phương thức.
- Kiểu dữ liệu tham chiếu (Reference Types): Các đối tượng phức tạp hơn.
1.1. Kiểu dữ liệu nguyên thủy (Primitive Types)
Có 7 kiểu dữ liệu nguyên thủy trong JavaScript (tính đến ES11):
a. String (string
)
Kiểu string
dùng để biểu diễn các chuỗi ký tự, tức là văn bản. Chuỗi ký tự được đặt trong dấu ngoặc đơn (''
), ngoặc kép (""
), hoặc dấu backtick (``
). Dấu backtick cho phép bạn sử dụng template literals, rất tiện lợi để nhúng biến hoặc biểu thức vào chuỗi.
Ví dụ:
let greeting = "Xin chào các bạn!"; // Dùng ngoặc kép
let name = 'FullhouseDev'; // Dùng ngoặc đơn
let message = `Tôi là ${name}. Chào mừng đến với blog!`; // Dùng backtick (template literal)
console.log(greeting);
console.log(message);
console.log(typeof greeting); // Output: string
- Giải thích: Biến
greeting
,name
, vàmessage
đều lưu trữ các giá trị là chuỗi văn bản.typeof
là toán tử giúp kiểm tra kiểu dữ liệu của một biến hoặc giá trị. Template literal với dấu backtick``
cho phép bạn dễ dàng kết hợp văn bản cố định với giá trị biến bằng cú pháp${tenBien}
.
b. Number (number
)
Kiểu number
dùng để biểu diễn các giá trị số, bao gồm cả số nguyên (integers) và số thập phân (floating-point numbers). JavaScript không phân biệt giữa hai loại này như nhiều ngôn ngữ khác.
Ví dụ:
let age = 30; // Số nguyên
let price = 19.99; // Số thập phân
let result = age + price; // Thực hiện phép toán
console.log(age);
console.log(price);
console.log(result);
console.log(typeof age); // Output: number
console.log(typeof price); // Output: number
- Giải thích: Các biến
age
,price
, vàresult
đều lưu trữ các giá trị số. Bạn có thể thực hiện các phép toán cơ bản (cộng, trừ, nhân, chia, v.v.) trực tiếp trên các giá trị kiểunumber
.
c. Boolean (boolean
)
Kiểu boolean
chỉ có hai giá trị khả dĩ: true
(đúng) hoặc false
(sai). Kiểu này thường được sử dụng trong các cấu trúc điều kiện (if
, else
, while
) để kiểm soát luồng thực thi của chương trình.
Ví dụ:
let isLearning = true; // Giá trị đúng
let isFinished = false; // Giá trị sai
if (isLearning) {
console.log("Tuyệt vời, bạn đang học JavaScript!");
} else {
console.log("Hãy bắt đầu học nào!");
}
console.log(typeof isLearning); // Output: boolean
- Giải thích: Biến
isLearning
vàisFinished
lưu trữ giá trị logic đúng/sai. Câu lệnhif
kiểm tra giá trị củaisLearning
để quyết định khối code nào sẽ được thực thi.
d. Undefined (undefined
)
Kiểu undefined
biểu thị rằng một biến đã được khai báo nhưng _chưa được gán giá trị_ nào. Nó là giá trị mặc định của các biến vừa được khai báo mà không kèm theo giá trị khởi tạo.
Ví dụ:
let myVariable; // Khai báo biến nhưng chưa gán giá trị
console.log(myVariable); // Output: undefined
console.log(typeof myVariable); // Output: undefined
// Nếu bạn khai báo và gán undefined một cách tường minh (không nên làm thường xuyên)
let anotherVariable = undefined;
console.log(anotherVariable); // Output: undefined
- Giải thích: Khi
myVariable
được khai báo nhưng không có dấu=...
theo sau, giá trị mặc định của nó làundefined
.typeof
cũng trả vềundefined
cho kiểu dữ liệu này.
e. Null (object
)
Kiểu null
biểu thị sự _vắng mặt một cách có chủ ý_ của _bất kỳ giá trị đối tượng_ nào. Nó thường được gán cho một biến để báo hiệu rằng biến đó hiện không trỏ đến đối tượng nào cả. Lưu ý rằng typeof null
trả về object
, đây là một _lỗi lịch sử_ của JavaScript và không nên hiểu null
là một object thực sự.
Ví dụ:
let car = null; // Biến car hiện không trỏ đến đối tượng xe nào
console.log(car); // Output: null
console.log(typeof car); // Output: object (lỗi lịch sử!)
// Sau này, bạn có thể gán một đối tượng cho nó
// car = { brand: "Toyota", model: "Camry" };
- Giải thích: Biến
car
được gán giá trịnull
để thể hiện rõ ràng rằng nó đang trống rỗng, không lưu trữ thông tin về một chiếc xe nào. Mặc dùtypeof null
làobject
, hãy luôn nhớnull
là một kiểu nguyên thủy đặc biệt đại diện cho việc không có giá trị đối tượng.
f. Symbol (symbol
)
Kiểu symbol
được giới thiệu trong ES6 (ECMAScript 2015) để tạo ra các giá trị _duy nhất_ và _bất biến_. Symbols thường được dùng làm khóa thuộc tính cho các đối tượng để tránh xung đột tên.
Ví dụ:
const id1 = Symbol('id');
const id2 = Symbol('id');
console.log(id1 === id2); // Output: false (mặc dù mô tả giống nhau, chúng là hai Symbol khác nhau)
console.log(typeof id1); // Output: symbol
- Giải thích:
Symbol('id')
tạo ra một giá trịSymbol
mới. Dù cả haiSymbol
đều có mô tả là'id'
, chúng hoàn toàn khác nhau và không bằng nhau. Điều này đảm bảo tính duy nhất khi sử dụng chúng làm khóa trong đối tượng.
g. BigInt (bigint
)
Kiểu bigint
được giới thiệu trong ES11 (ECMAScript 2020) để làm việc với các số nguyên có độ lớn _vượt quá giới hạn_ của kiểu number
thông thường (là 2^53 - 1). Bạn tạo một BigInt
bằng cách thêm hậu tố n
vào cuối một số nguyên hoặc gọi hàm BigInt()
.
Ví dụ:
const largeNumber = 123456789012345678901234567890n; // Thêm 'n' ở cuối
const anotherLargeNumber = BigInt(123);
console.log(largeNumber);
console.log(anotherLargeNumber);
console.log(typeof largeNumber); // Output: bigint
- Giải thích:
largeNumber
là mộtBigInt
vì nó vượt quá giới hạn an toàn của kiểunumber
.anotherLargeNumber
được tạo bằng hàm tạoBigInt()
. Kiểubigint
cho phép tính toán chính xác với các số nguyên cực lớn.
1.2. Kiểu dữ liệu tham chiếu (Reference Types)
Các kiểu dữ liệu tham chiếu phức tạp hơn các kiểu nguyên thủy. Khi bạn làm việc với kiểu tham chiếu, biến không lưu trữ trực tiếp giá trị, mà lưu trữ _một tham chiếu (địa chỉ bộ nhớ)_ đến nơi chứa giá trị đó.
Loại tham chiếu phổ biến nhất là Object
. Các kiểu dữ liệu khác như Array
, Function
, Date
, RegExp
, v.v., thực chất đều là _biến thể_ hoặc _instance_ của Object
.
a. Object (object
)
Object
là kiểu dữ liệu cơ bản nhất trong các kiểu tham chiếu. Nó là một tập hợp các cặp thuộc tính (property), mỗi thuộc tính bao gồm một khóa (key) (thường là chuỗi hoặc Symbol) và một giá trị (value) (có thể là bất kỳ kiểu dữ liệu nào khác).
Ví dụ:
let person = {
firstName: "Nguyễn Văn",
lastName: "A",
age: 25,
isStudent: false
};
console.log(person.firstName); // Truy cập thuộc tính bằng dấu chấm
console.log(person['age']); // Truy cập thuộc tính bằng ngoặc vuông
console.log(typeof person); // Output: object
- Giải thích: Biến
person
lưu trữ một đối tượng với các thuộc tính nhưfirstName
,lastName
,age
,isStudent
. Các thuộc tính này có thể truy cập bằng dấu chấm (.
) hoặc ngoặc vuông ([]
).
b. Array (object
)
Array
là một kiểu đối tượng đặc biệt, được thiết kế để lưu trữ một tập hợp các giá trị _có thứ tự_ theo chỉ mục (index). Chỉ mục của mảng trong JavaScript bắt đầu từ 0.
Ví dụ:
let colors = ["Đỏ", "Xanh lá", "Xanh dương"]; // Mảng các chuỗi
let numbers = [1, 5, 10, 20]; // Mảng các số
console.log(colors[0]); // Output: Đỏ (truy cập phần tử đầu tiên)
console.log(numbers.length); // Output: 4 (số phần tử trong mảng)
console.log(typeof colors); // Output: object (Array là một loại Object)
- Giải thích:
colors
vànumbers
là các mảng. Bạn truy cập các phần tử trong mảng bằng cách sử dụng chỉ mục đặt trong ngoặc vuông sau tên biến mảng (colors[0]
). Mảng có nhiều phương thức hữu ích để thao tác (ví dụ:push
,pop
,map
,filter
, v.v.).
c. Function (function
)
Trong JavaScript, hàm cũng là một loại đối tượng đặc biệt (first-class objects). Hàm là một khối code được định nghĩa để thực hiện một nhiệm vụ cụ thể và có thể gọi (thực thi) khi cần. Mặc dù typeof
một hàm trả về function
, nó vẫn được xem là một loại đối tượng.
Ví dụ:
function greet(name) {
console.log(`Chào bạn, ${name}!`);
}
greet("Độc giả"); // Gọi (thực thi) hàm
console.log(typeof greet); // Output: function
- Giải thích: Hàm
greet
nhận một tham sốname
và in ra lời chào. Bạn gọi hàm bằng cách sử dụng tên hàm theo sau là dấu ngoặc đơn()
, truyền vào các đối số nếu cần.
Sự khác biệt quan trọng giữa Primitive và Reference Types:
- Primitive Types: Giá trị được lưu trữ _trực tiếp_ trong biến. Khi gán một biến nguyên thủy cho biến khác, giá trị được sao chép.
- Reference Types: Biến lưu trữ _địa chỉ_ đến giá trị trong bộ nhớ. Khi gán một biến tham chiếu cho biến khác, chỉ địa chỉ được sao chép, cả hai biến cùng trỏ đến _một_ giá trị trong bộ nhớ. Thay đổi giá trị thông qua một biến sẽ ảnh hưởng đến biến còn lại.
Ví dụ về sự khác biệt:
// Primitive
let a = 10;
let b = a; // Sao chép giá trị 10
b = 20;
console.log(a); // Output: 10 (a không bị ảnh hưởng)
// Reference
let obj1 = { value: 10 };
let obj2 = obj1; // Sao chép địa chỉ (cả hai trỏ đến cùng một đối tượng)
obj2.value = 20;
console.log(obj1.value); // Output: 20 (obj1 bị ảnh hưởng vì trỏ đến cùng đối tượng đã bị thay đổi)
- Giải thích: Với kiểu nguyên thủy (
number
), thay đổib
không ảnh hưởng đếna
. Với kiểu tham chiếu (object
), thay đổi thuộc tính củaobj2
làm thay đổi đối tượng mà cảobj1
vàobj2
cùng trỏ tới, do đóobj1.value
cũng thay đổi.
2. Phạm vi (Scope) trong JavaScript: Ai thấy gì và ở đâu?
Phạm vi (Scope) trong JavaScript xác định _khu vực_ mà các biến và hàm có thể được truy cập và sử dụng. Nó giống như ranh giới giữa các phần khác nhau của chương trình, ngăn chặn việc các biến ở một khu vực can thiệp hoặc xung đột với biến ở khu vực khác.
Hiểu rõ phạm vi giúp bạn:
- Tránh xung đột tên biến.
- Quản lý bộ nhớ hiệu quả hơn (biến chỉ tồn tại trong phạm vi của nó).
- Viết code an toàn và dễ dự đoán hơn.
JavaScript có ba loại phạm vi chính:
a. Phạm vi Toàn cầu (Global Scope)
Các biến được khai báo _ngoài bất kỳ hàm hoặc khối mã ({}
) nào_ nằm trong phạm vi toàn cầu. Biến trong phạm vi toàn cầu có thể được truy cập _từ bất kỳ đâu_ trong chương trình JavaScript của bạn.
Ví dụ:
let globalVariable = "Tôi có mặt ở khắp mọi nơi!"; // Khai báo ngoài hàm
function accessGlobal() {
console.log(globalVariable); // Có thể truy cập biến toàn cầu từ bên trong hàm
}
accessGlobal();
console.log(globalVariable); // Có thể truy cập biến toàn cầu từ bên ngoài hàm
- Giải thích:
globalVariable
được khai báo ở cấp cao nhất, không nằm trong hàm hay khối lệnh nào, nên nó là biến toàn cục. Bạn có thể truy cập nó cả bên trong hàmaccessGlobal()
và ngoài hàm.
b. Phạm vi Hàm (Function Scope)
Các biến được khai báo _bên trong một hàm_ với từ khóa var
nằm trong phạm vi hàm. Biến trong phạm vi hàm chỉ có thể được truy cập _từ bên trong hàm đó_ và các hàm lồng bên trong nó. Chúng không thể truy cập từ bên ngoài hàm.
Ví dụ:
function myFunction() {
var functionScopedVariable = "Tôi chỉ sống trong hàm này";
console.log(functionScopedVariable); // Có thể truy cập biến trong phạm vi hàm
}
myFunction();
// console.log(functionScopedVariable); // Lỗi! Không thể truy cập biến này từ bên ngoài hàm
- Giải thích:
functionScopedVariable
được khai báo bên trongmyFunction
vớivar
. Nó chỉ tồn tại và có thể truy cập được khi hàmmyFunction
đang chạy. Cố gắng truy cập nó từ bên ngoài hàm sẽ gây ra lỗiReferenceError
.
c. Phạm vi Khối (Block Scope)
Phạm vi khối được giới thiệu với ES6 thông qua các từ khóa let
và const
. Các biến được khai báo bằng let
hoặc const
_bên trong bất kỳ khối mã nào_ (được định nghĩa bởi cặp dấu ngoặc nhọn {}
) chỉ có thể được truy cập _từ bên trong khối đó_.
Ví dụ:
function exampleBlockScope() {
if (true) {
let blockScopedVariable = "Tôi chỉ trong khối if này";
const anotherBlockVariable = "Tôi cũng vậy!";
console.log(blockScopedVariable); // Có thể truy cập biến trong khối
console.log(anotherBlockVariable);
}
// console.log(blockScopedVariable); // Lỗi! Không thể truy cập biến này từ bên ngoài khối if
// console.log(anotherBlockVariable); // Lỗi!
}
exampleBlockScope();
- Giải thích:
blockScopedVariable
vàanotherBlockVariable
được khai báo bên trong khốiif
bằnglet
vàconst
. Chúng chỉ có thể truy cập được trong phạm vi của khối{}
đó. Sau khi khối kết thúc, chúng không còn tồn tại và không thể truy cập từ bên ngoài.
Sự khác biệt giữa var
, let
, và const
liên quan đến phạm vi:
var
: Có phạm vi hàm (Function Scope) hoặc phạm vi toàn cầu (Global Scope). Nó _không_ có phạm vi khối. Điều này có thể dẫn đến những hành vi không mong muốn, đặc biệt là trong các vòng lặp hoặc khốiif
.let
vàconst
: Có phạm vi khối (Block Scope). Đây là lý do chính khiếnlet
vàconst
được ưa dùng hơnvar
trong code JavaScript hiện đại.
Ví dụ minh họa sự khác biệt var
và let
trong vòng lặp:
// Sử dụng var
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log('var:', i); // Output: 3, 3, 3 (vì i có phạm vi hàm/toàn cục và giá trị cuối cùng là 3)
}, 100);
}
// Sử dụng let
for (let j = 0; j < 3; j++) {
setTimeout(function() {
console.log('let:', j); // Output: 0, 1, 2 (vì j có phạm vi khối, mỗi lần lặp tạo ra một j mới)
}, 100);
}
- Giải thích: Ví dụ kinh điển này cho thấy sự nguy hiểm của
var
trong vòng lặp khi kết hợp vớisetTimeout
. Vìvar i
có phạm vi ngoài vòng lặp (phạm vi hàm hoặc toàn cục), khi hàmsetTimeout
thực thi sau một chút, nó nhìn thấy giá trị _cuối cùng_ củai
là 3. Ngược lại,let j
tạo ra một biếnj
mới cho _mỗi lần lặp_, mỗi biến này có phạm vi riêng trong khối lặp đó, do đó khisetTimeout
thực thi, nó nhìn thấy giá trịj
tương ứng với lần lặp cụ thể đó.
d. Chuỗi phạm vi (Scope Chain)
Khi JavaScript cần tìm kiếm giá trị của một biến, nó tuân theo một quy tắc được gọi là Chuỗi phạm vi (Scope Chain). Quá trình tìm kiếm bắt đầu từ _phạm vi hiện tại_ (nơi biến đang được truy cập). Nếu không tìm thấy biến ở đó, nó sẽ đi lên _phạm vi chứa nó (phạm vi cha)_, rồi phạm vi của phạm vi cha, và cứ thế tiếp tục cho đến _phạm vi toàn cầu_. Nếu biến không được tìm thấy ở bất kỳ đâu trong chuỗi phạm vi, JavaScript sẽ báo lỗi ReferenceError
.
Ví dụ về Chuỗi phạm vi:
let globalVar = "Tôi là Global";
function outerFunction() {
let outerVar = "Tôi là Outer";
function innerFunction() {
let innerVar = "Tôi là Inner";
console.log(innerVar); // Tìm thấy trong phạm vi hiện tại (innerFunction)
console.log(outerVar); // Tìm thấy trong phạm vi cha (outerFunction)
console.log(globalVar); // Tìm thấy trong phạm vi ông (Global)
// console.log(nonExistentVar); // Lỗi ReferenceError
}
innerFunction();
// console.log(innerVar); // Lỗi! innerVar chỉ trong phạm vi innerFunction
}
outerFunction();
- Giải thích: Bên trong
innerFunction
, JavaScript tìminnerVar
trong phạm vi của chính nó. Khi tìmouterVar
, nó không có tronginnerFunction
, nên nó đi lên phạm vi cha (outerFunction
) và tìm thấy. Khi tìmglobalVar
, nó không có trong cảinnerFunction
lẫnouterFunction
, nên nó đi lên phạm vi toàn cầu và tìm thấy.
Hiểu rõ cách Chuỗi phạm vi hoạt động là cực kỳ quan trọng để debug (gỡ lỗi) các vấn đề liên quan đến biến không tìm thấy hoặc biến có giá trị không mong muốn.
Comments