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: Vì sao phải tái sử dụng code bằng hàm?
Sau khi đã biết biến, điều kiện, vòng lặp, mảng và object, bước tiếp theo để viết code tốt hơn là tránh lặp lại cùng một đoạn code ở nhiều nơi. Đây chính là lúc hàm (function) trở nên cực kỳ quan trọng.
Nếu mỗi test case đều phải tự viết lại toàn bộ các bước như đăng nhập, tìm sản phẩm, thêm giỏ hàng hay logout, code sẽ rất nhanh trở nên dài, rối và khó bảo trì. Chỉ cần quy trình thay đổi một chút, bạn sẽ phải sửa ở rất nhiều chỗ.
🔹 1. Vấn đề của code lặp lại
Khi gặp cùng một hành động lặp đi lặp lại, cách làm tốt hơn là gom nó thành một hàm để tái sử dụng. Đây là tư duy nền tảng cho việc viết test chuyên nghiệp và cũng là tiền đề để đi tới Page Object Model sau này.
💻 Ví dụ code bị lặp:
// Test case 1
console.log("Điền username: admin");
console.log("Điền password: 123456");
console.log("Click nút Login");
// Test case 2
console.log("Điền username: tester");
console.log("Điền password: 123456");
console.log("Click nút Login");
Đoạn code trên có thể chạy được, nhưng rõ ràng là bị lặp. Nếu luồng đăng nhập thay đổi, bạn phải sửa lại nhiều lần.
🔹 2. Hàm là gì?
Hàm là một khối lệnh được đặt tên để bạn có thể gọi lại khi cần. Bạn truyền dữ liệu vào, hàm xử lý, rồi có thể trả kết quả về.
Một hàm thường đi kèm 3 khái niệm rất quan trọng:
parameter: tham số được khai báo ở phần định nghĩa hàm.argument: giá trị thực tế được truyền vào khi gọi hàm.return: kết quả mà hàm trả về sau khi xử lý xong.
Nếu không có return, JavaScript sẽ ngầm trả về undefined.
💻 Cú pháp cơ bản của hàm:
function tenHam(thamSo1, thamSo2) { // Khai báo hàm với 2 tham số đầu vào
const ketQua = thamSo1 + thamSo2; // Xử lý dữ liệu bên trong hàm
return ketQua; // Trả kết quả ra ngoài
}
💻 Ví dụ về parameter, argument và return:
function tinhTong(soA, soB) { // soA, soB là parameter
const tong = soA + soB; // Tính tổng của hai số
return tong; // Trả kết quả để bên ngoài có thể dùng tiếp
}
const ketQua1 = tinhTong(5, 10); // 5 và 10 là argument
const ketQua2 = tinhTong(100, 200); // 100 và 200 là argument
console.log(ketQua1); // 15
console.log(ketQua2); // 300
🔹 3. Ba cách tạo hàm trong JavaScript
JavaScript có 3 kiểu tạo hàm mà bạn sẽ gặp thường xuyên nhất. Mỗi kiểu có cú pháp và hành vi hơi khác nhau, nên cần nắm rõ ngay từ đầu.
A. Function Declaration
Đây là cách khai báo hàm “cổ điển” và rất dễ đọc. Kiểu này còn có đặc điểm quan trọng là được hoisting đầy đủ, nên bạn có thể gọi hàm trước cả khi nó xuất hiện trong code.
function tinhTong(soA, soB) { // Khai báo hàm theo kiểu declaration
return soA + soB; // Trả về tổng của hai số
}
console.log(tinhTong(2, 3)); // 5
B. Function Expression
Kiểu này tạo ra một hàm rồi gán nó vào một biến. Nó rất phổ biến trong JavaScript hiện đại, đặc biệt khi bạn muốn kiểm soát rõ việc định nghĩa và sử dụng hàm.
const tinhHieu = function(soA, soB) { // Gán một hàm vào biến tinhHieu
return soA - soB; // Trả về hiệu hai số
};
const ketQua = tinhHieu(20, 8);
console.log(ketQua); // 12
C. Arrow Function
Đây là cú pháp hiện đại, ngắn gọn và được dùng rất nhiều trong code JavaScript hiện nay. Ngoài việc gọn hơn, arrow function còn có hành vi khác với hàm thường ở chỗ this.
const tinhTich = (soA, soB) => { // Arrow function dạng đầy đủ
return soA * soB; // Trả về tích hai số
};
const tinhTichNhanh = (soA, soB) => soA * soB; // Arrow function rút gọn khi chỉ có một biểu thức return
console.log(tinhTich(4, 5)); // 20
console.log(tinhTichNhanh(5, 6)); // 30
💡 Lưu ý: Arrow function rất hợp cho callback và các hàm ngắn. Nhưng để hiểu phần sau về
this, bạn cần nhớ rằng arrow function không cóthisriêng.
🎭 Phần 2: Hiểu đúng this trong hàm
this là một trong những khái niệm gây nhầm lẫn nhiều nhất trong JavaScript. Lý do là giá trị của nó không cố định, mà phụ thuộc vào cách hàm được gọi.
Nói ngắn gọn, this không phải là “hàm đang đứng ở đâu”, mà là “hàm đang được gọi trong ngữ cảnh nào”.
🔹 1. this trong hàm thông thường
Với hàm thường dùng từ khóa function, giá trị của this thay đổi tùy theo cách gọi hàm.
- Nếu gọi như một hàm độc lập,
thisphụ thuộc môi trường chạy. - Nếu gọi như method của object,
thistrỏ tới chính object đó.
💻 Gọi hàm độc lập:
function showContext() {
console.log(this); // Trong script trình duyệt không strict, thường là window
}
showContext();
⚠️ Lưu ý: Trong
strict modehoặc module hiện đại,thiskhi gọi hàm độc lập có thể làundefined. Vì vậy, bạn không nên dựa vào kiểu gọi này trong code thực tế.
💻 Gọi như method của object:
const user = {
name: "Tester",
greet: function() {
console.log(`Xin chào, tôi là ${this.name}`); // this trỏ tới object user
}
};
user.greet(); // "Xin chào, tôi là Tester"
🔹 2. this trong arrow function
Arrow function không có this riêng. Thay vào đó, nó lấy this từ phạm vi bên ngoài nơi nó được tạo ra. Đây là lý do arrow function đặc biệt hữu ích trong callback.
💻 Ví dụ mất this khi dùng callback bằng hàm thường:
const testSuite = {
suiteName: "Login Tests",
testCases: ["TC01_ValidLogin", "TC02_InvalidLogin"],
runBuggy: function() {
console.log(`Bắt đầu chạy bộ test: ${this.suiteName}`);
this.testCases.forEach(function(test) {
// Lỗi: callback này có this riêng, nên không còn trỏ tới testSuite
console.log(`Đang chạy ${test} của bộ test ${this.suiteName}`);
});
}
};
💻 Cách sửa bằng arrow function:
const testSuite = {
suiteName: "Login Tests",
testCases: ["TC01_ValidLogin", "TC02_InvalidLogin"],
runCorrect: function() {
console.log(`Bắt đầu chạy bộ test: ${this.suiteName}`);
this.testCases.forEach((test) => {
// Arrow function mượn this từ hàm runCorrect
console.log(`Đang chạy ${test} của bộ test ${this.suiteName}`);
});
}
};
💡 Mẹo nhớ nhanh: Nếu callback cần dùng
thistừ object bên ngoài, arrow function thường là lựa chọn an toàn và dễ đoán hơn.
🎈 Phần 3: Hoisting và cách dùng an toàn
Hoisting là hành vi mặc định của JavaScript, trong đó khai báo biến và hàm được “nhấc lên” đầu phạm vi trước khi code thực sự chạy. Tuy nhiên, không phải loại hàm nào cũng được hoisting giống nhau.
🔹 1. Hoisting với Function Declaration
Function Declaration được hoisting đầy đủ, nghĩa là bạn có thể gọi hàm trước cả khi phần khai báo nằm phía dưới.
// Có thể gọi trước vì Function Declaration được hoisting đầy đủ
runLoginTest();
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.");
}
🔹 2. Hoisting với Function Expression và Arrow Function
Với Function Expression và Arrow Function gán vào const, chỉ phần khai báo biến được hoisting, còn giá trị thật của hàm thì chưa có cho đến lúc dòng gán được thực thi.
Vì vậy, nếu bạn gọi hàm trước dòng định nghĩa, chương trình sẽ báo lỗi.
try {
logout(); // Gọi trước khi định nghĩa
} catch (error) {
console.log("LỖI:", error.message); // Báo lỗi vì logout chưa có giá trị hàm
}
const logout = () => {
console.log("Người dùng đã đăng xuất.");
};
logout(); // Gọi sau khi định nghĩa thì chạy bình thường
💡 Lời khuyên: Dù JavaScript có hoisting, cách viết an toàn nhất vẫn là định nghĩa hàm trước rồi mới sử dụng. Điều này giúp code dễ đọc và ít gây bất ngờ hơn.
📞 Phần 4: Callback và các bẫy rất hay gặp
Callback là một hàm được truyền vào một hàm khác như một đối số, rồi sẽ được gọi lại vào thời điểm phù hợp. Đây là khái niệm xuất hiện khắp nơi trong JavaScript, từ setTimeout đến event listener, thao tác mảng và xử lý bất đồng bộ.
🔹 1. Callback là gì?
function thongBaoPizzaDaToi() {
console.log("Pizza đã đến! Ra nhận nào!");
}
function datPizza(loaiPizza, callback) {
console.log(`Đang làm pizza ${loaiPizza}...`);
// Giả lập chờ 2 giây rồi gọi lại callback
setTimeout(callback, 2000);
}
datPizza("Hải sản", thongBaoPizzaDaToi);
Trong ví dụ trên, thongBaoPizzaDaToi chính là callback vì nó được truyền vào hàm khác và sẽ được gọi sau.
🔹 2. Bẫy số 1: callback làm mất this
Khi bạn truyền một method của object vào callback dưới dạng hàm thường, this rất dễ bị mất ngữ cảnh. Đây là một trong những lỗi hay gặp nhất khi viết code automation hoặc UI logic.
const testSuite = {
suiteName: "Login Tests",
run: function() {
setTimeout(function() {
// Lỗi: this ở đây không còn là testSuite
console.log(`Đang chạy bộ test: ${this.suiteName}`);
}, 1000);
}
};
💻 Cách sửa đúng bằng arrow function:
const testSuite = {
suiteName: "Login Tests",
run: function() {
setTimeout(() => {
// Arrow function mượn this từ hàm run, nên vẫn trỏ tới testSuite
console.log(`Đang chạy bộ test: ${this.suiteName}`);
}, 1000);
}
};
🔹 3. Bẫy số 2: dùng var trong vòng lặp với setTimeout
Đây là lỗi kinh điển của JavaScript. Khi dùng var trong vòng lặp for với callback bất đồng bộ, tất cả callback sẽ cùng nhìn thấy một biến dùng chung, nên đến lúc chạy thì giá trị đã là giá trị cuối cùng.
for (var i = 1; i <= 3; i++) {
setTimeout(function() {
// Đến lúc callback chạy, vòng lặp đã xong và i đã là 4
console.log(`Test case số: ${i}`);
}, 1000);
}
// Kết quả:
// Test case số: 4
// Test case số: 4
// Test case số: 4
💻 Cách sửa bằng let:
for (let i = 1; i <= 3; i++) {
setTimeout(function() {
// Mỗi lần lặp có một biến i riêng
console.log(`Test case số: ${i}`);
}, 1000);
}
// Kết quả đúng:
// Test case số: 1
// Test case số: 2
// Test case số: 3
⚠️ Lưu ý sống còn: Với callback, bạn phải đặc biệt cẩn thận với hai thứ:
thisvà phạm vi biến trong vòng lặp. Hai lỗi này rất dễ làm test chạy sai nhưng khó phát hiện bằng mắt thường.
🔭 Phần 5: Scope và Closure
Đây là phần nền tảng giúp bạn hiểu biến sống ở đâu, truy cập được từ đâu và vì sao có những hàm “nhớ” được dữ liệu cũ của nó. Scope và Closure là hai khái niệm cực kỳ quan trọng nếu bạn muốn hiểu JavaScript chứ không chỉ học thuộc cú pháp.
🔹 1. Scope là gì?
Scope là phạm vi mà trong đó biến và hàm có thể được truy cập. JavaScript có 3 loại scope bạn cần nhớ:
- Global Scope: phạm vi toàn cục.
- Function Scope: phạm vi bên trong hàm.
- Block Scope: phạm vi bên trong cặp dấu
{}khi dùnglethoặcconst.
🔹 2. Global Scope
Biến khai báo ngoài tất cả hàm và block sẽ thuộc phạm vi toàn cục. Về kỹ thuật thì tiện, nhưng nếu lạm dụng sẽ rất dễ gây lỗi vì mọi nơi đều có thể truy cập và sửa được.
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 từ trong hàm
}
logVersion();
console.log(appVersion); // Vẫn truy cập được ở bên ngoài
🔹 3. Function Scope
Biến tạo bên trong một hàm chỉ có thể dùng bên trong hàm đó. Đây là cách giúp giới hạn phạm vi biến và tránh làm bẩn môi trường bên ngoài.
function runTest() {
const testId = "TC-01"; // Biến cục bộ bên trong hàm
console.log(`Bắt đầu chạy test: ${testId}`);
}
runTest();
// console.log(testId); // Lỗi: Không thể truy cập từ bên ngoài hàm
🔹 4. Block Scope
Block scope được tạo bởi cặp dấu ngoặc nhọn {}. Các biến khai báo bằng let và const bên trong block sẽ chỉ tồn tại trong block đó.
const isLoggedIn = true;
if (isLoggedIn) {
const userRole = "admin"; // Chỉ tồn tại trong block if
console.log(`Vai trò người dùng là: ${userRole}`);
}
// console.log(userRole); // Lỗi: userRole không tồn tại ở ngoài block
🔹 5. Closure là gì?
Closure là khả năng của một hàm con có thể truy cập và ghi nhớ biến của hàm cha, ngay cả sau khi hàm cha đã chạy xong. Đây là một khái niệm nâng cao, nhưng rất mạnh và xuất hiện nhiều hơn bạn nghĩ.
💻 Ví dụ kinh điển với bộ đếm:
function createCounter() {
let count = 0; // Biến riêng của mỗi lần gọi createCounter
const increment = function() {
count++; // Hàm con vẫn nhớ được biến count của hàm cha
console.log(count);
};
return increment; // Trả về hàm con
}
const counter1 = createCounter(); // Tạo một closure riêng
counter1(); // 1
counter1(); // 2
counter1(); // 3
const counter2 = createCounter(); // Tạo một closure mới, độc lập
counter2(); // 1
Điểm quan trọng nhất của ví dụ này là: mỗi lần gọi createCounter(), JavaScript tạo ra một “môi trường nhớ” riêng. Vì vậy counter1 và counter2 không dùng chung biến count.
🧪 Phần 6: Ứng dụng hàm trong automation và cách ghi nhớ nhanh
Tất cả những phần ở trên không chỉ là lý thuyết JavaScript thuần túy. Trong automation, hàm chính là công cụ để bạn viết test rõ ràng hơn, dùng lại được nhiều lần hơn và dễ bảo trì hơn.
- Tái sử dụng: viết một lần, gọi nhiều nơi.
- Dễ đọc: test case nhìn giống chuỗi các bước nghiệp vụ.
- Dễ bảo trì: thay đổi ở một nơi, cập nhật cho nhiều test.
💻 Ví dụ tái sử dụng hàm trong test:
const login = (username, password) => {
console.log("--- Bắt đầu hành động Login ---");
console.log(`Điền username: ${username}`); // Trong Playwright thật có thể là await page.fill(...)
console.log("Điền password"); // Trong Playwright thật có thể là await page.fill(...)
console.log("Click nút Submit"); // Trong Playwright thật có thể là await page.click(...)
console.log("--- Kết thúc hành động Login ---");
};
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}'`);
console.log("--- Kết thúc hành động Thêm sản phẩm ---");
};
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");
🔹 Ghi nhớ nhanh cho Tester
- Dùng hàm để tránh lặp code và gom các bước nghiệp vụ thành khối tái sử dụng.
parameterlà biến đầu vào lúc định nghĩa hàm, cònargumentlà giá trị thực tế lúc gọi hàm.returndùng để trả kết quả ra ngoài; không córeturnthì hàm trả vềundefined.Function Declaration,Function ExpressionvàArrow Functionkhông giống nhau hoàn toàn, nhất là về hoisting vàthis.- Arrow function rất hợp cho callback vì không có
thisriêng. - Với callback bất đồng bộ, tránh dùng
vartrong vòng lặp; hãy ưu tiênlet. - Hiểu đúng
scopegiúp bạn tránh lỗi biến “không tồn tại” hoặc bị ghi đè ngoài ý muốn. - Hiểu
closuregiúp bạn hiểu vì sao có những hàm vẫn “nhớ” được dữ liệu cũ sau khi hàm cha đã kết thúc.
✅ Kết luận: Bài 5 là bước chuyển từ viết code “chạy được” sang viết code có tổ chức, có thể tái sử dụng và ít lỗi hơn. Nếu nắm chắc hàm, this, hoisting, callback, scope và closure, bạn sẽ có nền rất vững để bước tiếp sang code automation chuyên nghiệp và Page Object Model.
