NỘI DUNG BÀI HỌC
✅ HIỂU SÂU VỀ THIS
✅ HOISTING LÀ GÌ
✅ HÀM CALLBACK VÀ LƯU Ý SỐNG CÒN
✅ SCOPE VÀ CLOSURE
Phần 1: Bước ngoặt tiếp theo: Làm chủ Hàm (Function) trong JavaScript!
Sau khi đã làm quen với các cấu trúc lặp 🔄, đã đến lúc chúng ta khám phá một trong những khái niệm nền tảng và mạnh mẽ nhất của lập trình: Hàm (Function).
Làm chủ được hàm là một bước ngoặt ✨ giúp bạn chuyển từ việc viết các dòng lệnh rời rạc sang xây dựng các kịch bản test có cấu trúc 🏗️, dễ bảo trì 🔧 và có thể tái sử dụng ♻️.
🤔 Vấn đề: Sự lặp lại Code (Code Duplication)
Hãy tưởng tượng trong kịch bản test, bạn cần đăng nhập ở 3 nơi khác nhau. Code của bạn có thể trông như thế này:
// --- Test case 1 ---
console.log("Điền username: admin");
console.log("Điền password: pass123");
console.log("Click nút Login");
// ... các bước test khác ...
// --- Test case 2 ---
console.log("Điền username: admin");
console.log("Điền password: pass123");
console.log("Click nút Login");
// ... các bước test khác ...
// --- Test case 3 ---
console.log("Điền username: admin");
console.log("Điền password: pass123");
console.log("Click nút Login");
// ...
Vấn đề là gì? Code bị lặp lại rất nhiều 👯♀️. Nếu sau này nút "Login" thay đổi selector, bạn sẽ phải sửa ở cả 3 nơi! Đây là lúc Hàm xuất hiện để giải cứu. 🦸
⚙️ Hàm là gì?
Hàm là một khối code được đặt tên, được thiết kế để thực hiện một công việc cụ thể. Bạn có thể "gọi" (call) nó nhiều lần ở nhiều nơi khác nhau.
Ví von: Hàm giống như một công thức nấu ăn 📜 hoặc một cái máy xay sinh tố 🍹.
-
Bạn định nghĩa công thức một lần (các nguyên liệu, các bước thực hiện).
-
Sau đó, bạn có thể sử dụng công thức đó bất cứ khi nào bạn muốn, với các "nguyên liệu" (tham số) khác nhau để tạo ra "thành phẩm" (giá trị trả về).
Các cách tạo Hàm 🛣️
Có 3 cách phổ biến để tạo một hàm trong JavaScript hiện đại.
A. Function Declaration (Khai báo hàm)
Đây là cách viết truyền thống và cơ bản nhất. 📜
📝 Cú pháp:
function tenHam(thamSo1, thamSo2) { // Thân hàm - các dòng code để thực thi // ... return ketQua; // Trả về kết quả }
-
thamSo
(Parameter): Là các biến "đầu vào", giống như danh sách nguyên liệu 🥕. -
return
: Từ khóa để "trả về" thành phẩm sau khi hàm thực hiện xong ✅. Nếu không có, hàm sẽ tự động trả vềundefined
.
💡 Ví dụ:
function tinhTong(soA, soB) {
const tong = soA + soB;
return tong;
}
// Gọi hàm với các "đối số" (argument) cụ thể
const ketQua1 = tinhTong(5, 10); // 15
const ketQua2 = tinhTong(100, 200); // 300
console.log(ketQua1);
B. Function Expression (Biểu thức hàm)
Cách này tạo ra một hàm và gán nó vào một biến. ➡️
📝 Cú pháp:
const tenBien = function(thamSo1, thamSo2) { // Thân hàm return ketQua; };
💡 Ví dụ:
const tinhHieu = function(soA, soB) { return soA - soB; }; const ketQua = tinhHieu(20, 8); // 12 console.log(ketQua);
C. Arrow Function (Hàm mũi tên - ES6) ⚡️
Đây là cách viết hiện đại, ngắn gọn và được khuyên dùng nhiều nhất hiện nay.
📝 Cú pháp:
const tenBien = (thamSo1, thamSo2) => { // Thân hàm return ketQua; };
✨ "Phép thuật" của Arrow Function: Nếu hàm chỉ có một dòng lệnh duy nhất và lệnh đó là return
, bạn có thể bỏ qua dấu {}
và từ khóa return
.
// Cách viết đầy đủ
const tinhTich = (soA, soB) => {
return soA * soB;
};
// Cách viết siêu gọn 🚀
const tinhTichNhanh = (soA, soB) => soA * soB;
console.log(tinhTichNhanh(5, 6)); // 30
Phần 2: HIỂU SÂU VỀ this
TRONG HÀM
Từ khóa this
- Ngữ cảnh thực thi 🎭
Từ khóa this
tham chiếu đến "ngữ cảnh" hoặc "đối tượng sở hữu" mà hàm đang được thực thi bên trong.
Ví von: 🗣️ Hãy nghĩ về từ "tôi". Khi tôi nói "tôi đói", "tôi" là chỉ tôi. Khi bạn nói "tôi đói", "tôi" là chỉ bạn. Ý nghĩa của từ "tôi" (this
) thay đổi tùy thuộc vào người đang nói (cách hàm được gọi).
A. Hàm thông thường (function
) - this
linh hoạt như tắc kè hoa 🦎
Giá trị của this
phụ thuộc hoàn toàn vào cách hàm đó được gọi.
-
Gọi trong ngữ cảnh toàn cục: Khi một hàm được gọi độc lập,
this
sẽ tham chiếu đến đối tượng toàn cục (window
trên trình duyệt).function showContext() { console.log(this); } showContext(); // 'this' ở đây là đối tượng window
-
Gọi như một phương thức của object: Khi một hàm được gọi như một phương thức của object,
this
sẽ tham chiếu đến chính object đó.
const user = {
name: "Tester",
greet: function() {
console.log(`Xin chào, tôi là ${this.name}`); // 'this' ở đây là object 'user'
}
};
user.greet(); // "Xin chào, tôi là Tester"
B. Hàm mũi tên (=>
) - this
cố định như hình xăm 📌
Hàm mũi tên không có this
của riêng nó. Thay vào đó, nó "mượn" giá trị this
từ ngữ cảnh bên ngoài nơi nó được định nghĩa. Hành vi này cực kỳ dễ đoán.
🧪 So sánh ứng dụng trong Automation: Vấn đề "mất ngữ cảnh this
" thường xảy ra với các hàm callback. Đây là lúc Arrow Function tỏa sáng.
const testSuite = {
suiteName: "Login Tests",
testCases: ["TC01_ValidLogin", "TC02_InvalidLogin"],
// ❌ DÙNG HÀM THƯỜNG -> GÂY LỖI
run_buggy: function() {
console.log(`Bắt đầu chạy bộ test: ${this.suiteName}`);
this.testCases.forEach(function(test) {
// LỖI: Bên trong callback này, 'this' bị mất ngữ cảnh!
console.log(`- Đang chạy ${test} của bộ test ${this.suiteName}`);
});
},
// ✅ DÙNG HÀM MŨI TÊN -> CHẠY ĐÚNG
run_correct: function() {
console.log(`\nBắt đầu chạy bộ test: ${this.suiteName}`);
this.testCases.forEach(test => {
// OK: Arrow function "mượn" 'this' từ hàm run_correct.
console.log(`- Đang chạy ${test} của bộ test ${this.suiteName}`);
});
}
};
Phần 3: Hoisting - "Sự nhấc lên" của JavaScript 🎈
Hoisting là hành vi mặc định của JavaScript, trong đó các khai báo biến và hàm sẽ được "nhấc lên" trên đầu phạm vi của chúng trước khi code được thực thi.
Ví von: 🧠 JavaScript Engine giống như quét qua phần tóm tắt đầu chương sách để biết các "nhân vật" (biến và hàm) trước khi thực sự "đọc" chi tiết.
A. Hoisting với Function Declaration
Được hoisted toàn bộ, bao gồm cả tên và thân hàm.
✅ Kết quả: Bạn có thể gọi hàm trước cả khi nó được viết trong code.
// Luồng test chính đặt ở đầu cho dễ đọc
runLoginTest();
// Các hàm phụ trợ được định nghĩa ở dưới
function runLoginTest() {
console.log("Bắt đầu test đăng nhập...");
verifyLoginSuccess();
}
function verifyLoginSuccess() {
console.log("Xác thực đăng nhập thành công.");
}
B. Hoisting với Function Expression
và Arrow Function
Chỉ có phần khai báo biến được hoisted, còn phép gán giá trị (thân hàm) thì không.
❌ Kết quả: Bạn không thể gọi các hàm này trước khi chúng được định nghĩa.
try {
logout(); // Cố gắng gọi hàm trước khi định nghĩa
} catch (error) {
console.log("LỖI:", error.message);
}
const logout = () => {
console.log("Người dùng đã đăng xuất.");
};
// Gọi hàm sau khi định nghĩa thì hoạt động bình thường
logout();
Lời khuyên: 💡 Cách làm hiện đại và an toàn là luôn khai báo/định nghĩa hàm trước khi sử dụng. Hãy ưu tiên dùng
const myFunc = () => {}
để tránh các hành vi khó đoán của hoisting.
Phần 4: Hàm Callback và những lưu ý "sống còn" 🚨
📞 Hàm Callback là gì?
Một hàm callback là một hàm được truyền vào một hàm khác như một đối số, và sau đó được "gọi lại" (called back) để thực thi.
Ví von: 🍕 Bạn gọi điện đặt pizza. Bạn đưa cho họ thông tin pizza và số điện thoại của bạn (chính là hàm callback). Khi pizza xong, cửa hàng sẽ dùng số điện thoại đó để "gọi lại" cho bạn.
function thongBaoPizzaDaToi() {
console.log("Pizza đã đến! 🍕 Ra nhận nào!");
}
function datPizza(loaiPizza, soDienThoaiCallback) {
console.log(`Đang làm pizza ${loaiPizza}...`);
// Giả lập 2 giây làm bánh, sau đó gọi lại cho bạn
setTimeout(soDienThoaiCallback, 2000);
}
datPizza("Hải sản", thongBaoPizzaDaToi);
Hai cạm bẫy khi dùng Callback trong Automation 🪤
-
Mất ngữ cảnh
this
(Lỗi phổ biến nhất)-
Vấn đề: Khi truyền một phương thức (dùng
function
thường) vào làm callback, nó sẽ mất ngữ cảnhthis
.const testSuite = { suiteName: "Login Tests", run: function() { // 'this' ở đây là testSuite setTimeout(function() { // LỖI: Bên trong callback này, 'this' là đối tượng 'window' console.log(`Đang chạy bộ test: ${this.suiteName}`); // in ra 'undefined' }, 1000); } };
-
Giải pháp: ✅ Luôn ưu tiên dùng hàm mũi tên (
=>
) cho callback.const testSuite = { suiteName: "Login Tests", run: function() { // 'this' ở đây là testSuite setTimeout(() => { // Dùng arrow function // OK: Arrow function "mượn" this từ hàm run, nên 'this' vẫn là testSuite console.log(`Đang chạy bộ test: ${this.suiteName}`); }, 1000); } };
-
-
Vấn đề về Scope và "Closure" (với vòng lặp)
-
Vấn đề: Dùng
var
trong vòng lặpfor
với các tác vụ bất đồng bộ (setTimeout
) sẽ gây ra lỗi kinh điển. Tất cả các callback sẽ chỉ thấy giá trị cuối cùng của biến lặp.for (var i = 1; i <= 3; i++) { setTimeout(function() { // Vấn đề: Khi hàm callback này chạy, vòng lặp for đã chạy xong từ lâu. // Lúc này, giá trị của biến 'i' đã là 4. console.log(`Test case số: ${i}`); }, 1000); } Kết quả sẽ là: Test case số: 4 Test case số: 4 Test case số: 4 Lý do là vì var có phạm vi toàn cục (hoặc function scope), chỉ có một biến i duy nhất được dùng chung. Cả ba hàm callback đều tham chiếu đến cùng một biến i đó. Giải pháp: Luôn dùng let trong vòng lặp. let tạo ra một biến i mới cho mỗi lần lặp, vì vậy mỗi hàm callback sẽ "nhớ" được giá trị i đúng của lần lặp đó. JavaScript
-
Giải pháp: ✅ Luôn dùng
let
trong vòng lặp.let
tạo ra một biến mới cho mỗi lần lặp, giúp callback "nhớ" đúng giá trị.
-
// Chỉ cần thay 'var' bằng 'let' là giải quyết được vấn đề!
for (let i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(`Test case số: ${i}`);
}, 1000);
}
// Kết quả đúng:
// Test case số: 1
// Test case số: 2
// Test case số: 3
Kết luận: Khi làm việc với callback, việc nắm vững cách
this
hoạt động và sự khác biệt giữalet
/const
vớivar
là cực kỳ quan trọng để tránh lỗi và đảm bảo kịch bản chạy đúng.
Phần 5: 🔭 Scope & Closure: Nền tảng của tương tác Biến
Scope - Phạm vi truy cập của Biến
Scope định nghĩa nơi mà các biến và hàm có thể được truy cập.
Ví von: 🏢 Hãy tưởng tượng code của bạn là một tòa nhà lớn. Scope giống như các căn phòng bên trong.
-
Người ở phòng ngoài không thể nhìn thấy đồ đạc trong phòng riêng của bạn, nhưng bạn có thể nhìn thấy đồ đạc ở sảnh chung.
-
Global Scope (Phạm vi Toàn cục) 🌍: Khai báo bên ngoài tất cả các hàm/khối. Có thể truy cập từ mọi nơi. (Giống như sảnh chính).
-
Định nghĩa: Bất kỳ biến nào được khai báo bên ngoài tất cả các hàm hoặc khối lệnh ({}) đều thuộc về Global Scope.
-
Quy tắc: Biến toàn cục có thể được truy cập từ bất cứ đâu trong chương trình.
-
Ví von: 🧠 Sảnh chính của tòa nhà. Ai cũng có thể thấy và sử dụng.
-
Lời khuyên: Hạn chế tối đa việc sử dụng biến toàn cục. Việc có thể truy cập và thay đổi chúng từ bất cứ đâu rất dễ gây ra lỗi khó kiểm soát.
const appVersion = "1.0.2"; // Biến toàn cục function logVersion() { console.log(`Phiên bản ứng dụng là: ${appVersion}`); // Truy cập được } logVersion(); console.log(appVersion); // Vẫn truy cập được
-
-
Function Scope (Phạm vi Hàm) 🚪: Biến tạo bên trong một hàm. Chỉ có thể truy cập bên trong hàm đó. (Giống như phòng riêng).
-
Định nghĩa: Các biến được khai báo bằng var (cách cũ) hoặc các biến được tạo bên trong một hàm sẽ thuộc về phạm vi của hàm đó.
-
Quy tắc: Chỉ có thể được truy cập bên trong hàm đó.
-
Ví von: 🧠 Một căn phòng riêng. Chỉ những ai ở trong phòng mới thấy đồ đạc bên trong.
function runTest() { const testId = "TC-01"; // Biến cục bộ trong hàm console.log(`Bắt đầu chạy test: ${testId}`); } runTest(); // console.log(testId); // LỖI! testId is not defined. Không thể truy cập từ bên ngoài.
-
-
Block Scope (Phạm vi Khối) 📦: Phạm vi tạo bởi cặp dấu
{}
(if, for,...). Biếnlet
vàconst
tuân theo quy tắc này. (Giống như két sắt trong phòng).-
Định nghĩa: Đây là phạm vi được tạo ra bởi cặp dấu ngoặc nhọn {}, ví dụ như trong if, for, while, hoặc đơn giản là {}. Các biến khai báo bằng let và const sẽ tuân theo quy tắc này.
-
Quy tắc: Biến chỉ tồn tại và có thể được truy cập bên trong khối ({...}) mà nó được khai báo.
-
Ví von: 🧠 Một chiếc két sắt bên trong một căn phòng. Chỉ những ai ở trong phòng và có chìa khóa mới mở được.
const isLoggedIn = true; if (isLoggedIn) { const userRole = "admin"; // Chỉ tồn tại trong khối if console.log(`Vai trò người dùng là: ${userRole}`); // "admin" } // console.log(userRole); // LỖI! userRole is not defined.
-
Closure - "Trí nhớ" của Hàm 🧠
Closure là một khái niệm nâng cao hơn nhưng cực kỳ mạnh mẽ. 💪
Định nghĩa: Closure là khả năng của một hàm "con" có thể truy cập và ghi nhớ các biến của hàm "cha", ngay cả khi hàm cha đã thực thi xong.
Ví von: 👨🍳 Hãy tưởng tượng hàm cha là một đầu bếp chuẩn bị nguyên liệu. Hàm con là một chiếc hộp giữ nhiệt 🥡. Đầu bếp đặt nguyên liệu vào hộp và đưa cho bạn. Ngay cả khi đầu bếp đã đi về (hàm cha đã chạy xong), bạn vẫn có thể mở hộp ra và sử dụng nguyên liệu. Chiếc hộp đó chính là Closure.
💡 Ví dụ kinh điển: Hàm đếm (Counter)
function createCounter() {
let count = 0; // Biến này thuộc về phạm vi của hàm cha
// Hàm con được trả về
const increment = function() {
count++;
console.log(count);
};
return increment;
}
// Khi gọi createCounter(), hàm cha chạy xong và trả về hàm 'increment'.
// Hàm 'increment' tạo thành một closure, "nhớ" được biến 'count' của hàm cha.
const counter1 = createCounter();
counter1(); // 1
counter1(); // 2
counter1(); // 3
// Mỗi lần gọi createCounter() sẽ tạo ra một scope (và biến count) mới, độc lập.
const counter2 = createCounter();
counter2(); // 1
Cách hoạt động:
Khi createCounter() được gọi, nó tạo ra một môi trường (phạm vi) riêng và một biến count = 0.
Nó trả về hàm increment. Hàm increment này "gói" theo môi trường mà nó được tạo ra, bao gồm cả biến count.
Khi bạn gọi counter1() (chính là hàm increment), nó vẫn có thể truy cập và thay đổi biến count mà nó "nhớ".
Biến count này hoàn toàn riêng tư, không thể truy cập được từ bên ngoài, chỉ có thể thông qua hàm counter1.
Ứng dụng Hàm trong Automation
Hàm là nền tảng của việc viết kịch bản test chuyên nghiệp, đặc biệt là theo mô hình Page Object Model (POM).
Lợi ích:
- Tái sử dụng (Reusability): Viết hàm login() một lần, gọi nó trong 50 test case khác nhau.
- Dễ đọc (Readability): Kịch bản test của bạn sẽ trông như một chuỗi các bước bằng tiếng Anh, thay vì một mớ code lộn xộn.
- Dễ bảo trì (Maintainability): Nếu quy trình đăng nhập thay đổi, bạn chỉ cần sửa code bên trong một hàm login() duy nhất.
- Kịch bản: Xây dựng các hàm có thể tái sử dụng để test một trang web.
- Ví dụ Automation:
/ Một hàm để đăng nhập, nhận vào username và password
const login = (username, password) => {
console.log("--- Bắt đầu hành động Login ---");
console.log(`Điền username: ${username}`);
// Code Playwright thật: await page.fill('#username', username);
console.log("Điền password");
// Code Playwright thật: await page.fill('#password', password);
console.log("Click nút Submit");
// Code Playwright thật: await page.click('#submit');
console.log("--- Kết thúc hành động Login ---\n");
};
// Một hàm để thêm sản phẩm vào giỏ hàng
const addProductToCart = (productName) => {
console.log(`--- Bắt đầu hành động Thêm sản phẩm ---`);
console.log(`Tìm và click nút 'Add to cart' của sản phẩm '${productName}'`);
// Code Playwright thật: await page.locator(...).click();
console.log(`--- Kết thúc hành động Thêm sản phẩm ---\n`);
};
// --- KỊCH BẢN TEST CHÍNH ---
// Kịch bản test bây giờ đọc rất rõ ràng!
console.log("BẮT ĐẦU TEST CASE: Mua hàng thành công với tài khoản admin");
login("admin_user", "pass123");
addProductToCart("Laptop Pro");
console.log("KẾT THÚC TEST CASE\n");
console.log("BẮT ĐẦU TEST CASE: Mua hàng thành công với tài khoản thường");
login("standard_user", "secret_sauce");
addProductToCart("T-shirt");
console.log("KẾT THÚC TEST CASE");