Lớp `Object` trong JavaScript
Bài viết này giải thích về lớp Object trong JavaScript.
Bài viết này giải thích về lớp Object trong JavaScript, bao gồm cả ví dụ thực tế.
YouTube Video
Lớp Object trong JavaScript
Object là một đối tượng dựng sẵn đóng vai trò là cơ sở cho tất cả các đối tượng trong JavaScript. Nhiều tính năng cốt lõi của ngôn ngữ, chẳng hạn như quản lý thuộc tính, kế thừa (chuỗi nguyên mẫu), liệt kê, sao chép và đóng băng, được cung cấp thông qua hành vi của Object.
Object.create
Có nhiều cách để tạo đối tượng và bạn nên sử dụng phù hợp tùy theo mục đích của mình.
Object literal (cách phổ biến nhất)
Mã dưới đây cho thấy cách đơn giản và dễ đọc nhất để tạo một đối tượng.
1// Create an object using object literal
2const user = {
3 name: "Alice",
4 age: 30,
5 greet() {
6 return `Hello, I'm ${this.name}`;
7 }
8};
9
10console.log(user.greet()); // "Hello, I'm Alice"
- Trong ví dụ này, các thuộc tính và phương thức được định nghĩa bằng cách sử dụng literal. Nó đơn giản và thường mang lại hiệu suất vượt trội.
Hàm tạo new Object()
Hàm tạo Object ít được sử dụng, nhưng việc hiểu cách nó hoạt động là rất hữu ích.
1// Create an object using the Object constructor
2const objFromCtor = new Object();
3objFromCtor.x = 10;
4objFromCtor.y = 20;
5
6console.log(objFromCtor); // { x: 10, y: 20 }
new Object()trả về một đối tượng rỗng, nhưng literal{}ngắn hơn và được sử dụng phổ biến hơn.
Chỉ định prototype với Object.create
Object.create được dùng để tạo một đối tượng với prototype được chỉ định.
1// Create an object with a specified prototype
2const proto = { hello() { return "hi"; } };
3const obj = Object.create(proto);
4obj.name = "Bob";
5
6console.log(obj.hello()); // "hi"
7console.log(Object.getPrototypeOf(obj) === proto); // true
Object.createrất lý tưởng cho thiết kế đối tượng dựa trên kế thừa, cho phép bạn kiểm soát chính xác chuỗi prototype.
Thuộc tính và bộ mô tả thuộc tính
Thuộc tính có các đặc điểm như 'value', 'writable', 'enumerable' và 'configurable', có thể kiểm soát chi tiết qua Object.defineProperty.
Ví dụ cơ bản về sử dụng defineProperty
Tiếp theo là ví dụ về việc định nghĩa các thuộc tính không liệt kê được và chỉ đọc bằng cách sử dụng defineProperty.
1// Define a non-enumerable read-only property
2const person = { name: "Carol" };
3
4Object.defineProperty(person, "id", {
5 value: 12345,
6 writable: false,
7 enumerable: false,
8 configurable: false
9});
10
11console.log(person.id); // 12345
12console.log(Object.keys(person)); // ["name"] — "id" is non-enumerable
13person.id = 999; // silently fails or throws in strict mode
14console.log(person.id); // still 12345
- Sử dụng
definePropertycho phép bạn kiểm soát chính xác hành vi của thuộc tính như liệt kê, ghi đè và xóa bỏ.
Thuộc tính truy cập (getter / setter)
Với thuộc tính truy cập, bạn có thể thêm logic vào quá trình đọc và ghi thuộc tính.
1// Use getter and setter to manage internal state
2const data = {
3 _value: 1,
4 get value() {
5 return this._value;
6 },
7 set value(v) {
8 if (typeof v === "number" && v > 0) {
9 this._value = v;
10 } else {
11 throw new Error("value must be a positive number");
12 }
13 }
14};
15
16console.log(data.value); // 1
17data.value = 5;
18console.log(data.value); // 5
19// data.value = -1; // would throw
- Với
gettervàsetter, bạn có thể xử lý việc truy cập thuộc tính giống như API bên ngoài và thêm xác thực hoặc hiệu ứng phụ.
Prototype và kế thừa (prototype / __proto__ / Object.getPrototypeOf)
Kế thừa trong JavaScript dựa trên chuỗi prototype, không dựa trên class. Các đối tượng có thể tham chiếu đến đối tượng khác làm prototype của chúng.
Object.getPrototypeOf và Object.setPrototypeOf
Ví dụ dưới đây cho thấy cách kiểm tra và thiết lập prototype.
1// Inspect and change prototype
2const base = { speak() { return "base"; } };
3const derived = Object.create(base);
4console.log(Object.getPrototypeOf(derived) === base); // true
5
6const other = { speak() { return "other"; } };
7Object.setPrototypeOf(derived, other);
8console.log(derived.speak()); // "other"
Object.getPrototypeOflấy prototype của một đối tượng.Object.setPrototypeOfthay đổi prototype của một đối tượng hiện có, nhưng bạn nên dùng cẩn thận vì nó có thể ảnh hưởng đến hiệu suất.
Các phương thức tích hợp quan trọng
Chúng tôi sẽ giải thích rõ ràng những phương thức quan trọng và thường được sử dụng nhất, được chọn ra từ các phương thức thể hiện do Object.prototype cung cấp, cũng như các phương thức tĩnh thuộc về Object.
hasOwnProperty, isPrototypeOf, toString, valueOf
hasOwnProperty, isPrototypeOf, toString, và valueOf định nghĩa hành vi cơ bản của đối tượng.
1// Demonstrate prototype methods
2const base = { greet() { return "hello"; } };
3const child = Object.create(base);
4const a = { x: 1 };
5
6console.log(a.hasOwnProperty("x")); // true
7console.log(a.hasOwnProperty("toString")); // false — toString is inherited
8
9console.log(a.toString()); // "[object Object]" by default
10
11console.log(base.isPrototypeOf(child)); // true
12console.log(Object.prototype.isPrototypeOf(child)); // true
hasOwnPropertylà phương thức thiết yếu để kiểm tra xem một thuộc tính có tồn tại trực tiếp trên đối tượng hay không.isPrototypeOfkiểm tra xem đối tượng đích có prototype là đối tượng hiện tại hay không.
Object.keys, Object.values, Object.entries
Object.keys, Object.values, và Object.entries trả về danh sách các thuộc tính enumerable của đối tượng. Chúng rất hữu ích cho việc lặp và chuyển đổi.
1// Keys, values and entries
2const item = { id: 1, name: "Widget", price: 9.99 };
3
4// ["id", "name", "price"]
5console.log(Object.keys(item));
6
7// [1, "Widget", 9.99]
8console.log(Object.values(item));
9
10// [["id",1], ["name","Widget"], ["price",9.99]]
11console.log(Object.entries(item));- Chúng thường được sử dụng để lặp và chuyển đổi đối tượng.
Object.assign
Object.assign được dùng để sao chép nông và gộp đối tượng. Lưu ý rằng prototype và thuộc tính truy cập sẽ không được sao chép.
1// Shallow copy / merge using Object.assign
2const target = { a: 1 };
3const source = { b: 2 };
4const result = Object.assign(target, source);
5
6console.log(result); // { a: 1, b: 2 }
7console.log(target === result); // true (merged into target)
- Đối với đối tượng lồng nhau, chỉ các tham chiếu được sao chép, nên bạn cần triển khai cách khác để sao chép sâu.
Object.freeze, Object.seal, Object.preventExtensions
Object.freeze, Object.seal và Object.preventExtensions kiểm soát tính thay đổi của đối tượng.
1// Freeze vs seal vs preventExtensions
2const obj = { a: 1 };
3Object.freeze(obj);
4obj.a = 2; // fails silently or throws in strict mode
5delete obj.a; // fails
6
7const obj2 = { b: 2 };
8Object.seal(obj2);
9obj2.b = 3; // allowed
10// delete obj2.b; // fails
11
12const obj3 = { c: 3 };
13Object.preventExtensions(obj3);
14obj3.d = 4; // fails
freezelà nghiêm ngặt nhất; nó ngăn mọi thay đổi cho thuộc tính của đối tượng.sealngăn việc thêm hoặc xóa thuộc tính, nhưng vẫn cho phép thay đổi giá trị của các thuộc tính đã tồn tại.preventExtensionschỉ ngăn việc thêm thuộc tính mới; các thuộc tính hiện tại vẫn có thể bị thay đổi hoặc xóa bỏ.
Các vấn đề về liệt kê, thứ tự và for...in / for...of
for...in liệt kê các tên thuộc tính enumerable, nhưng cũng bao gồm thuộc tính trong chuỗi prototype nên thường được dùng kết hợp với hasOwnProperty. Kết hợp Object.keys() với for...of sẽ an toàn hơn và làm rõ ý định của bạn hơn.
1// Safe enumeration
2const obj = Object.create({ inherited: true });
3obj.own = 1;
4
5for (const key in obj) {
6 if (obj.hasOwnProperty(key)) {
7 console.log("own prop:", key);
8 } else {
9 console.log("inherited prop:", key);
10 }
11}
12
13for (const key of Object.keys(obj)) {
14 console.log("key via Object.keys:", key);
15}- Các quy tắc liệt kê thuộc tính được xác định trong đặc tả ECMAScript, và có trường hợp thứ tự được đảm bảo, có trường hợp không. Theo quy tắc chung, các khóa được hiểu là số sẽ được sắp xếp theo thứ tự tăng dần, trong khi các khóa khác sẽ theo thứ tự chèn vào.
Sao chép (nông) và sao chép sâu
Có hai kiểu sao chép đối tượng: sao chép nông bằng cách sử dụng Object.assign hoặc cú pháp spread, và sao chép sâu bằng các phương pháp đệ quy. Việc sử dụng chúng một cách phù hợp tùy theo tình huống là rất quan trọng.
Sao chép nông (cú pháp spread / Object.assign)
1// Shallow copy with spread operator
2const original = { a: 1, nested: { x: 10 } };
3const shallow = { ...original };
4shallow.nested.x = 99;
5console.log(original.nested.x); // 99 — nested object is shared
- Với sao chép nông, đối tượng lồng nhau sẽ dùng chung tham chiếu nên thay đổi ở đối tượng gốc có thể ảnh hưởng đến bản sao.
Sao chép sâu đơn giản (với một số lưu ý)
Dùng mẹo JSON là cách nhanh để sao chép sâu, nhưng có nhược điểm như mất hàm, Date, tham chiếu vòng tròn và giá trị undefined. Để sao chép sâu thực sự, bạn cần dùng thư viện chuyên dụng.
1// Deep clone using JSON methods — limited use-cases only
2const source = { a: 1, d: new Date(), nested: { x: 2 } };
3const cloned = JSON.parse(JSON.stringify(source));
4console.log(cloned); // ok for plain data, but Date becomes string, functions lost
- Các phương thức dựa trên JSON thuận tiện cho việc xử lý nhanh dữ liệu đơn giản, nhưng trong nhiều trường hợp phổ biến, hành vi của chúng có thể bị lỗi.
Mixin và mở rộng đối tượng
Thay vì kế thừa đa cấp, mẫu ghép hành vi qua mixin thường được sử dụng.
1// Simple mixin function
2const canEat = {
3 eat() { return `${this.name} eats`; }
4};
5const canWalk = {
6 walk() { return `${this.name} walks`; }
7};
8
9function createPerson(name) {
10 const person = { name };
11 return Object.assign(person, canEat, canWalk);
12}
13
14const p = createPerson("Dana");
15console.log(p.eat()); // "Dana eats"
16console.log(p.walk()); // "Dana walks"
- Mixin rất linh hoạt, nhưng bạn cần cẩn trọng với sự trùng lặp tên thuộc tính và khả năng kiểm thử.
Những cạm bẫy thường gặp và thông lệ tốt nhất
Dưới đây là một số cạm bẫy phổ biến và thực hành tốt.
-
Tính thay đổi (Mutability) Đối tượng mặc định có thể thay đổi. Trong các ứng dụng quản lý trạng thái, hãy cân nhắc sử dụng cấu trúc dữ liệu bất biến với
Object.freezehoặc một thư viện bất biến. -
Ô nhiễm prototype Gộp dữ liệu bên ngoài trực tiếp vào đối tượng bằng
Object.assignhoặc vòng lặp có thể gây tác dụng phụ ngoài ý muốn với các thuộc tính đặc biệt như__proto__hoặcconstructor, tạo rủi ro bảo mật. Lọc dữ liệu đầu vào của người dùng trước khi gộp trực tiếp. -
Những cạm bẫy của
for...infor...incũng liệt kê cả thuộc tính prototype, nên hãy kiểm tra vớihasOwnProperty. DùngObject.keyssẽ rõ ràng hơn. -
Lạm dụng sao chép nông Xem xét việc cần sao chép sâu để tránh thay đổi ở đối tượng lồng nhau ảnh hưởng đến đối tượng gốc.
Ví dụ thực tế: Mẫu cập nhật đối tượng bất biến
Các mẫu trả về đối tượng mới mà không sửa đổi trực tiếp trạng thái thường được dùng trong React và các thư viện tương tự.
1// Immutable update example
2const state = { todos: [{ id: 1, text: "Buy milk", done: false }] };
3
4// Toggle todo done immutably
5function toggleTodo(state, todoId) {
6 return {
7 ...state,
8 todos: state.todos.map(t => t.id === todoId ? { ...t, done: !t.done } : t)
9 };
10}
11
12const newState = toggleTodo(state, 1);
13console.log(state.todos[0].done); // false
14console.log(newState.todos[0].done); // true
- Đoạn mã này là ví dụ về việc tạo đối tượng trạng thái mới mà không sửa đổi trực tiếp đối tượng
stategốc. HàmtoggleTodosao chép mảngtodosvà trả về một đối tượng mới chỉ thay đổi phần tử mục tiêu, do đó đối tượngstateban đầu không bị thay đổi. - Cập nhật bất biến giúp giảm hiệu ứng phụ và làm cho quản lý trạng thái dễ dàng hơn.
Ví dụ thực tế: Gộp an toàn (Cẩn trọng với ô nhiễm prototype)
Khi gộp JSON bên ngoài, hãy bỏ qua __proto__ để tránh ô nhiễm prototype.
1// Safe merge ignoring __proto__ keys
2function safeMerge(target, source) {
3 for (const key of Object.keys(source)) {
4 if (key === "__proto__" || key === "constructor") continue;
5 target[key] = source[key];
6 }
7 return target;
8}
9
10const target = {};
11const source = JSON.parse('{"a":1,"__proto__":{"polluted":true}}');
12safeMerge(target, source);
13console.log(target.polluted); // undefined — safe
14console.log({}.polluted); // undefined — prototype not polluted
- Bảo vệ kiểu này cũng quan trọng trong các thư viện và framework.
Cân nhắc về hiệu suất
Về mặt hiệu suất, hãy lưu ý các điểm sau:.
- Tránh thay đổi prototype thường xuyên (
Object.setPrototypeOf) hoặc thêm/xóa thuộc tính động vì sẽ cản trở việc tối ưu hóa của engine. - Khi tạo ra nhiều đối tượng nhỏ, việc tối ưu hóa sẽ hiệu quả hơn nếu bạn sử dụng các đối tượng có cấu trúc đồng nhất (cùng một tập thuộc tính).
- Sao chép sâu tốn kém. Giảm thiểu việc sử dụng hoặc cân nhắc dùng cập nhật dựa trên diff.
Tóm tắt
Object là trung tâm của JavaScript, cung cấp nhiều tính năng như tạo đối tượng, kiểm soát thuộc tính, kế thừa, sao chép và quản lý tính thay đổi. Điều quan trọng là phải hiểu các API như Object.defineProperty, Object.assign và Object.freeze, cũng như thiết kế cẩn thận để tránh các rủi ro như ô nhiễm nguyên mẫu (prototype pollution) và sao chép nông (shallow copy).
Bạn có thể làm theo bài viết trên bằng cách sử dụng Visual Studio Code trên kênh YouTube của chúng tôi. Vui lòng ghé thăm kênh YouTube.