מחלקת `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...infor...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 בערוץ היוטיוב שלנו. נא לבדוק גם את ערוץ היוטיוב.