Lớp `Object` trong JavaScript

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.create rấ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 defineProperty cho 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 gettersetter, 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.getPrototypeOfObject.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.getPrototypeOf lấy prototype của một đối tượng.
  • Object.setPrototypeOf thay đổ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
  • hasOwnProperty là 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.
  • isPrototypeOf kiể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.sealObject.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
  • freeze là nghiêm ngặt nhất; nó ngăn mọi thay đổi cho thuộc tính của đối tượng.
  • seal ngă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.
  • preventExtensions chỉ 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.freeze hoặ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.assign hoặ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ặc constructor, 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...in for...in cũng liệt kê cả thuộc tính prototype, nên hãy kiểm tra với hasOwnProperty. Dùng Object.keys sẽ 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 state gốc. Hàm toggleTodo sao chép mảng todos và trả về một đối tượng mới chỉ thay đổi phần tử mục tiêu, do đó đối tượng state ban đầ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.assignObject.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.

YouTube Video