JavaScript Object Fundamentals: Literals, Properties, Methods, and Shorthand Syntax

Link copied
JavaScript Object Fundamentals: Literals, Properties, Methods, and Shorthand Syntax

JavaScript Object Fundamentals: Literals, Properties, Methods, and Shorthand Syntax

Objects are the most-used data structure in JavaScript. Almost every value you'll work with — DOM nodes, API responses, function arguments, framework state, even arrays and functions — is an object underneath. This lesson covers the fundamentals: how to create objects, how properties work, how methods differ from regular functions, and the modern syntax shortcuts that make object code in 2026 read very differently from object code in 2015.

We leave the deeper machinery (prototypes, classes, descriptors) for later lessons. This is the practical, daily-use foundation.

What an object actually is #

At its simplest: an object is a collection of key-value pairs.

const user = {
  name: 'Ada',
  age: 36,
  active: true,
};

Keys are strings (or Symbols — covered in Lesson 3.6). Values are anything: primitives, other objects, functions, arrays, more objects.

Three things make objects different from primitives:

  1. They're composite — they hold multiple values.
  2. They're mutable — you can add, change, and remove properties after creation.
  3. They're passed by reference — assigning one variable to another copies the reference, not the contents.

The third point is the source of more surprise than most other JavaScript features combined. We'll get to it shortly.

Creating objects #

Four common ways. The first two cover ~99% of cases.

1. Object literal #

const point = { x: 10, y: 20 };

This is the default — fast, readable, what every modern codebase uses for ad-hoc objects.

2. Object.create(proto) #

Creates an object with a specific prototype:

const proto = { greet() { return `Hi, ${this.name}`; } };
const u = Object.create(proto);
u.name = 'Ada';
u.greet(); // 'Hi, Ada'

Useful when you want to control the prototype chain explicitly. We cover this in Module 4.

3. Constructor functions (legacy) #

function User(name) {
  this.name = name;
}
const u = new User('Ada');

Largely replaced by class syntax in modern code. Still useful to read.

4. Classes (modern OOP) #

class User {
  constructor(name) { this.name = name; }
}
const u = new User('Ada');

We cover classes properly in Module 4.

Accessing properties #

Two ways. They look similar but behave differently.

// Dot notation
user.name;          // 'Ada'
user.address.city;  // chained access

// Bracket notation
user['name'];       // 'Ada'
user[someVariable]; // dynamic key
user['email-2'];    // keys with special characters

Use dot notation when the key is a static, valid identifier. Use brackets when the key is in a variable, computed at runtime, or has characters that aren't valid in identifiers (-, spaces, starting with a digit).

Reading missing properties #

Reading a property that doesn't exist returns undefined. It does not throw.

const u = { name: 'Ada' };
u.age;       // undefined
u.missing;   // undefined

Reading a property of undefined or null DOES throw:

u.address.city;
// TypeError: Cannot read properties of undefined (reading 'city')
// because u.address is undefined, and you can't read .city of that

This is what ?. (optional chaining, from Lesson 1.4) was designed for:

u.address?.city;  // undefined — short-circuits cleanly

Setting, updating, and removing properties #

user.email = 'ada@example.com';     // add
user.age = 37;                       // update
delete user.active;                  // remove

Note: delete removes the property entirely (not just sets it to undefined). Iteration with Object.keys will skip deleted keys; assigning undefined would not.

Often you don't want mutation at all — you want a new object with the changes. That's what spread (covered in Lesson 3.3) is for:

const updated = { ...user, age: 37, email: 'ada@example.com' };

Immutable updates are the default in modern frameworks and become important again in Module 4 (when we cover composition vs. inheritance).

Modern shorthand syntax #

Three shorthands you'll see in every modern codebase.

1. Property shorthand #

If the key matches a variable name in scope, write it once:

const name = 'Ada';
const age = 36;

// Long form
const user = { name: name, age: age };

// Shorthand
const user = { name, age };

Widely used. Almost every modern object literal has at least one.

2. Computed property keys #

Use a brackets expression as the key:

const field = 'email';
const obj = { [field]: 'ada@example.com' };  // { email: 'ada@example.com' }

// Useful for dynamic field names
function setOne(state, key, value) {
  return { ...state, [key]: value };
}

Common in reducers, form-state updates, and anywhere the key isn't known until runtime.

3. Method shorthand #

For functions on an object, you can drop the colon and the function keyword:

// Long form
const utils = {
  add: function (a, b) { return a + b; },
  hello: function () { return 'Hi'; },
};

// Method shorthand
const utils = {
  add(a, b) { return a + b; },
  hello() { return 'Hi'; },
};

Methods vs. regular functions: this #

A method is a function called via a property of an object (obj.method()). Inside the function, this refers to the object the method was called on.

const user = {
  name: 'Ada',
  greet() {
    return `Hi, ${this.name}`;
  },
};

user.greet();           // 'Hi, Ada' — `this` is `user`

const grab = user.greet;
grab();                  // 'Hi, undefined' — `this` lost

The rule: this is determined by the call site, not the definition site. user.greet() sets this to user. grab() calls the same function with no explicit receiver, so this is undefined (in strict mode).

Arrow functions don't have their own this — they inherit from the surrounding scope. That's why this fails:

const user = {
  name: 'Ada',
  greet: () => `Hi, ${this.name}`,  // BUG
};
user.greet(); // 'Hi, undefined' — `this` is the surrounding scope, not user

For object methods, use regular function syntax (or method shorthand). Save arrows for callbacks and the cases where you specifically want the outer this.

We cover this exhaustively in Lesson 2.6.

Iterating an object #

Four idiomatic patterns:

// Keys only
Object.keys(user);   // ['name', 'age', 'email']

// Values only
Object.values(user); // ['Ada', 36, 'ada@example.com']

// Both
Object.entries(user); // [['name', 'Ada'], ['age', 36], ['email', 'ada@...']]

// for-of with destructuring (covered in 3.3)
for (const [key, value] of Object.entries(user)) {
  console.log(key, value);
}

Avoid for...in for plain objects — it walks the prototype chain (Lesson 1.6 covered this). Object.keys/entries give you only the object's own enumerable string keys, which is almost always what you want.

Checking what's in an object #

Four ways. They subtly differ.

'name' in user;                          // true — checks own + inherited
user.hasOwnProperty('name');             // true — own only (older API)
Object.hasOwn(user, 'name');             // true — own only (modern, since ES2022)
user.name !== undefined;                 // truthy check — fails if value IS undefined

For "does this object have this property", Object.hasOwn(obj, key) is the modern correct answer.

Comparing objects #

Objects compare by reference, not by content.

const a = { x: 1 };
const b = { x: 1 };
a === b;  // false — different objects
a === a;  // true — same object

There is no built-in deep equality in JavaScript. For shallow comparison, you can compare key counts and per-key values yourself. For deep, use JSON.stringify for simple cases, or lodash.isEqual / fast-deep-equal for production.

Reference semantics — the most common bug #

const a = { count: 0 };
const b = a;

b.count = 5;
console.log(a.count); // 5 — !

Assignment copies the reference, not the object. a and b point to the same object.

To get a real copy (shallow):

const copy = { ...a };          // spread (Lesson 3.3)
const copy = Object.assign({}, a); // older API, same effect
const copy = structuredClone(a);  // deep clone (modern, since 2022)

For nested objects, only structuredClone is a true deep copy. ... and Object.assign are shallow — nested objects are still shared.

We cover spread deeply in Lesson 3.3.

A best-practices checklist #

  • Default to object literals for ad-hoc structures. Use class only when you specifically need instances + methods + a stable type.
  • Use property shorthand and method shorthand — they're cleaner and idiomatic.
  • Treat objects as immutable when you can. Spread to update, don't mutate.
  • Use Object.entries + destructuring to iterate. It's the readable modern pattern.
  • Be aware of reference semantics. Assignment doesn't copy.
  • Use Object.hasOwn for membership checks.
  • Use ?. and ?? liberally — they were designed for object access.
  • For deep clones, use structuredClone. Not JSON.parse(JSON.stringify(…)).

What's next #

Lesson 3.2 covers array methodsmap, filter, reduce, find, some, every, and the dozen others. Together with object iteration, you'll have the full data-transformation toolkit that powers modern JavaScript.

Try it yourself #

Reference semantics is the single most surprising behavior of objects. Predict the output:

YouPredict the output:
const a = { n: 1 };
const b = { ...a };
const c = a;
b.n = 2;
c.n = 3;
console.log(a.n, b.n, c.n);
Claude · used js_sandboxOutput: 3 2 3.

b = { ...a } creates a new object — mutating b doesn’t affect a.
c = a just copies the reference — c and a are the same object, so setting c.n = 3 also changes a.n.

That’s the difference between a copy and a reference assignment, in one experiment.

Mastering this distinction is what separates JavaScript developers who can debug "why did my state change?" from those who can't.

Up next in JavaScript

More from this topic

View all JavaScript articles →

Enjoyed this article?

Get new JavaScript tutorials delivered. No spam — just code-first articles when they ship.

Leave a Comment

Your email address will not be published. Required fields are marked *