מחלקת `Object` ב-JavaScript

מחלקת `Object` ב-JavaScript

מאמר זה מסביר את מחלקת Object ב-JavaScript.

מאמר זה מסביר את מחלקת Object ב-JavaScript, כולל דוגמאות מעשיות.

YouTube Video

מחלקת Object ב-JavaScript

Object הוא אובייקט מובנה שמשמש בסיס לכל האובייקטים ב-JavaScript. רבות מתכונות הליבה של השפה, כגון ניהול מאפיינים, ירושה (שרשרת אב-טיפוס), מִנּוּי, שכפול והקפאה, מסופקות באמצעות ההתנהגות של Object.

Object.create

ישנם מספר דרכים ליצור אובייקטים, ויש לבחור ביניהן בהתאם לצורך שלך.

תוֹאַר אובייקט (הצורה הנפוצה ביותר)

הקוד הבא מציג את הדרך הפשוטה והברורה ביותר ליצור אובייקט.

 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"
  • בדוגמה זו, מאפיינים ומתודות מוגדרים באמצעות ליטרלים. זה פשוט ולרוב מספק ביצועים טובים יותר.

הקונסטרקטור new Object()

הקונסטרקטור Object לא בשימוש תכוף, אך חשוב להבין את ההתנהגות שלו.

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() מחזיר אובייקט ריק, אך התוֹאַר {} קצר ונפוץ יותר.

הגדרת האב-טיפוס באמצעות Object.create

Object.create משמש ליצירת אובייקט עם אב-טיפוס מוגדר.

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 אידיאלי לתכנון אובייקטים מבוססי ירושה, ומאפשר שליטה מדויקת בשרשרת האב-טיפוס.

מאפייני תכונה ודֵסְקְרִיפְּטוֹרִים

לכל מאפיין יש תכונות כמו 'value', 'writable', 'enumerable', ו-'configurable', שניתן לשלוט בהן בפירוט בעזרת Object.defineProperty.

דוגמה בסיסית לשימוש ב-defineProperty

להלן דוגמה להגדרת מאפיינים שאינם בני מניה ובלתי ניתנים לכתיבה באמצעות ‎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
  • שימוש ב-‎defineProperty‎ מאפשר שליטה מדויקת בהתנהגות המאפיין, כמו מנייה, כתיבה מחדש ומחיקה.

מאפייני גישה (getter / setter)

באמצעות מאפייני גישה, ניתן להכניס לוגיקה לקריאות וכתיבות של מאפיין.

 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
  • עם getter ו-setter אפשר לטפל בגישה למאפיין כמו API חיצוני ולהוסיף בדיקות או תופעות לוואי.

אב-טיפוס וירושה (prototype / __proto__ / Object.getPrototypeOf)

הירושה ב-JavaScript מבוססת על שרשרת האב-טיפוס, ולא על מחלקות. אובייקטים יכולים להתייחס לאובייקטים אחרים בתור האב-טיפוס שלהם.

Object.getPrototypeOf ו-Object.setPrototypeOf

הדוגמה הבאה מציגה כיצד לבדוק ולהגדיר אבות טיפוס.

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 מחזיר את אב הטיפוס של אובייקט.
  • Object.setPrototypeOf משנה את האב-טיפוס של אובייקט קיים, אבל רצוי להיזהר בגלל שזה יכול להשפיע על הביצועים.

שיטות מובנות חשובות

נסביר בבירור את השיטות הנפוצות והחשובות ביותר שנבחרו מתוך שיטות המופע המסופקות על ידי Object.prototype, וכן את השיטות הסטטיות של Object.

hasOwnProperty, isPrototypeOf, toString, valueOf

hasOwnProperty,‎ isPrototypeOf,‎ toString,‎ ו-valueOf מגדירים את ההתנהגות הבסיסית של אובייקטים.

 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 היא שיטה חיונית לבדוק אם מאפיין נמצא ישירות באובייקט.
  • isPrototypeOf בודק אם לאובייקט המטרה יש אותו כאב טיפוס.

Object.keys, Object.values, Object.entries

Object.keys, Object.values ו-Object.entries מחזירות רשימות של מאפייני האובייקט הניתנים למִנּוּי. הן שימושיות לצורך איטרציה והמרה.

 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));
  • אלו בשימוש תכוף לאיטרציה ולהמרה של אובייקטים.

Object.assign

Object.assign משמש לשכפול רדוד ולאיחוד. שימו לב שאב-טיפוס ומאפייני גישה אינם משוכפלים.

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)
  • עבור אובייקטים מקוננים, רק ההפניות משוכפלות ולכן נדרשת מימוש אחר בשביל שכפול עמוק.

Object.freeze, Object.seal, Object.preventExtensions

Object.freeze, Object.seal, ו-Object.preventExtensions שולטים בשינוי של אובייקטים.

 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 הוא המחמיר ביותר—הוא מונע כל שינוי במאפייני האובייקט.
  • seal מונע הוספה או מחיקה של מאפיינים, אך מאפשר שינוי ערכים של מאפיינים קיימים.
  • preventExtensions מונע רק הוספת מאפיינים חדשים; מאפיינים קיימים עדיין ניתן לשנות או למחוק.

מִנּוּי אובייקטים, סדר ו-for...in / for...of

for...in מונה שמות מאפיינים הניתנים למִנּוּי, אבל כולל גם מאפיינים בשרשרת האב-טיפוס, לכן לעיתים קרובות משתמשים בו יחד עם hasOwnProperty. השילוב של Object.keys() עם for...of בטוח יותר ומבהיר את כוונותיך.

 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}
  • כללי מִנּוּי המאפיינים מוגדרים בתקן ECMAScript, ויש מקרים שבהם הסדר מובטח ובאחרים לא. ככלל, מפתחות הנחשבים כמספרים ממוינים בסדר עולה, בעוד שמפתחות אחרים נשמרים לפי סדר ההוספה.

שכפול והעתקה עמוקה

ישנם שני סוגים של העתקת אובייקטים: העתקה רדודה באמצעות Object.assign או סינטקס פיזור, והעתקה עמוקה באמצעות שיטות רקורסיביות. חשוב להשתמש בהן בהתאם למצב.

העתקה רדודה (תחביר 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
  • בהעתקה רדודה, אובייקטים מקוננים שותפים לאותה הפניה, ולכן שינויים באובייקט המקורי ישפיעו על ההעתק.

העתקה עמוקה פשוטה (עם הסתייגויות)

שימוש בטריק של JSON הוא דרך מהירה להעתקה עמוקה, אך קיימות חסרונות כמו איבוד פונקציות, אובייקטי Date, הפניות מעגליות וערכי undefined. להעתקה עמוקה אמיתית, יש להשתמש בספריה ייעודית.

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
  • שיטות המבוססות על JSON נוחות לטיפול מהיר בנתונים פשוטים, אך במקרים נפוצים התנהגותן עלולה להישבר.

Mixin-ים והרכבת אובייקטים

במקום ירושה מרובה, נהוג להשתמש בהרכבת התנהגויות בעזרת mixin-ים.

 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-ים גמישים, אבל יש להיזהר מהתנגשויות שמות מאפיינים ומבעיות בדיקות.

כשלים נפוצים ודרכי עבודה מומלצות

להלן כמה כשלים נפוצים וטיפים לעבודה נכונה.

  • ניתנות לשינוי אובייקטים הם ניתנים לשינוי כברירת מחדל. באפליקציות עם ניהול מצבים, שקול להשתמש במבני נתונים בלתי ניתנים לשינוי עם ‎Object.freeze‎ או ספרייה בלתי ניתנת לשינוי.

  • זיהום אב-טיפוס איחוד נתונים חיצוניים לאובייקט בעזרת Object.assign או לולאות עלול לגרום לתופעות לוואי בלתי צפויות עם מאפיינים מיוחדים כמו __proto__ או constructor—ובכך ליצור סיכוני אבטחה. סנן קלט מהמשתמש לפני איחוד ישיר לאובייקט.

  • כשלים בשימוש for...in for...in סופר גם מאפיינים בשרשרת האב-טיפוס, לכן בדוק בעזרת hasOwnProperty. שימוש ב-Object.keys ברור יותר.

  • שימוש שגוי בהעתקה רדודה שקול אם אתה צריך העתקה עמוקה, כדי למנוע שינוי באובייקטים פנימיים שישפיע על המקור.

דוגמה מעשית: דפוס עדכון אובייקט בלתי-ניתן לשינוי

דפוסים שמחזירים אובייקט חדש בלי לשנות את המצב ישירות נפוצים ב-React ובספריות דומות.

 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
  • קוד זה הוא דוגמה ליצירת אובייקט מצב חדש מבלי לשנות ישירות את אובייקט ה-state המקורי. הפונקציה toggleTodo מעתיקה את מערך ה-todos ומחזירה אובייקט חדש שבו רק האלמנט הרצוי שונה, כך שאובייקט ה-state המקורי נשאר ללא שינוי.
  • עדכונים בלתי ניתנים לשינוי מפחיתים תופעות לוואי ומפשטים ניהול מצב.

דוגמה מעשית: איחוד בטוח (להיזהר מזיהום אב-טיפוס)

באיחוד JSON חיצוני, התעלם מ-__proto__ כדי למנוע זיהום אב-טיפוס.

 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
  • הגנה כזו חשובה גם בספריות ומסגרות עבודה.

שיקולי ביצועים

בנושא ביצועים, התחשב בנקודות הבאות:.

  • הימנע משינויי אב-טיפוס תכופים (Object.setPrototypeOf) או מהוספה/הסרה דינמית של מאפיינים, שכן זה מקשה על אופטימיזציה של המנוע.
  • כאשר יוצרים הרבה אובייקטים קטנים, מיטוב הופך ליעיל יותר אם משתמשים באובייקטים עם מבנה אחיד (אותו סט של מאפיינים).
  • העתקה עמוקה יקרה מאוד בביצועים. צמצם את השימוש בהן או שקול עדכונים מבוססי diff.

סיכום

Object הוא מרכזי ב-JavaScript וכולל תכונות רבות כמו יצירת אובייקט, שליטה במאפיינים, ירושה, שכפול וניהול ניתנות לשינוי. חשוב להבין ממשקי API כמו Object.defineProperty, Object.assign, ו-Object.freeze, ולתכנן בזהירות כדי להימנע ממלכודות כמו זיהום אב-טיפוס והעתקה רדודה.

תוכלו לעקוב אחר המאמר שלמעלה באמצעות Visual Studio Code בערוץ היוטיוב שלנו. נא לבדוק גם את ערוץ היוטיוב.

YouTube Video