JavaScript centers around object-oriented programming, where objects like functions, arrays, and more encapsulate state and behavior. Mastering how functions create and return object values is key to leveraging JavaScript‘s capabilities.

This comprehensive guide demonstrates multiple methods for returning rich JavaScript objects from functions.

Using Return Statements

The simplest way to return an object is directly returning a literal with the return statement:

function createPerson() {
  return { 
    name: "John Doe",
    age: 30
  }; 
}

const person = createPerson(); // { name: "John Doe", age: 30}

This encapsulates complex object creation logic easily inside functions.

We can also return references to existing objects:

let person = {
  name: "Jane Smith",
  age: 20  
};

function duplicate() {
  return person; 
}

const person2 = duplicate();
person2.name = "Maria Garcia"; 

// person also changed to Maria Garcia

This assigns person to person2, so both reference the same underlying object.

Leveraging Object Literal Shorthand

If you want returned properties named identically to parameters, use shorthand syntax:

function create(name, age) {
  return {
    name, // same as name: name
    age   // same as age: age
  };
}

const obj = create("John Doe", 30); 
// { name: "John Doe", age: 30 }

The properties get created matching parameter names. Helps reduce redundant code.

Constructor Functions and new

A common way to return objects is with constructor functions:

function Person(name, age) {
  this.name = name; 
  this.age = age;
}

const me = new Person("John", 30); // Person instance  

Calling with new returns the newly created this object automatically without an explicit return statement.

This also sets up prototype-based inheritance which we‘ll expand on next.

Prototype Inheritance

Every function has a prototype property that allows returned object instances to inherit shared methods:

function Person(name) {
  this.name = name;
}  

Person.prototype.describe = function() {
  return `Person named ${this.name}`;
};

const me = new Person("John");
me.describe(); // Person named John

This avoids the need to redefine the describe method on every instance. Prototype inheritance enables proper object-oriented structure in JavaScript.

Returning Various Built-In Object Types

While plain objects are most common, functions can return any object type:

function createObject() {
  return new Date(); // Date instance  
}

function createArray() {
  return [1 , 2, 3]; // Array instance
}

const myDate = createObject();
const myArray = createArray(); 

We simply call the appropriate constructor function for the type we want. This allows returning domain-specific objects.

Callable Function Objects

Functions themselves are objects that can be returned:

function baseFunction() {
  //...
}

function createCallable() {
  return baseFunction; 
}

const myFunction = createCallable();
myFunction(); // baseFunction called

The returned function reference allows calling baseFunction directly. Use cases include higher-order functions and decorator patterns.

Getters and Setters via Accessors

Ordinary data properties for objects get directly set. However, JavaScript also supports getters and setters:

const person = {
  firstName: "John",
  lastName: "Doe",

  get fullName() {
    return `${this.firstName} ${this.lastName}` 
  },

  set fullName(name) {
     // split name & assign to first/last 
  }
};

person.fullName; // call getter
person.fullName = "Jane Smith"; // call setter

This allows additional logic to run on get/set. By returning objects with accessors, you enable cleaner interfaces.

Performance Tradeoffs

There are performance differences to consider with returns:

Return Type Speed
Literal objects Fastest
Constructors Fast, but instanceof and new have overhead
Accessors Getters/setters add slight overhead

Literal objects have less overhead compared to things like constructors and accessors.

Encapsulation via Closures and Factories

Factories using closures are common in JavaScript for data privacy:

function createCounter() {
  let count = 0;

  return {
    increment() {
      count++; 
    },
    get() {
      return count; 
    }
  }
}

const counter = createCounter();
counter.increment();
counter.get(); // 1

The returned object encloses access to count via the inner functions. This pattern encapsulates private data.

Complex Composition

You can break complex objects across multiple simpler functions:

function base() { 
  return {
    id: generateId(),
    timestamp: Date.now();   
  };
}

function buildUser(base) {
  return {
    ...base,
    name: "John Doe",
    age: 30 
  };
}


const user = buildUser(base()); // compose

base() focuses on generic fields, passed to buildUser() to add specific user data. This separation of concerns leads to cleaner code.

Dependency Injection

Related to composition, functions can also accept objects and return augmented versions:

function annotate(object, info) {
  return {
    ...object,
    annotation: info
  }; 
}

let book = {
  title: "Catcher in the Rye",
  author: "J.D. Salinger"   
};

const annotated = annotate(book, "School assignment"); 
// attach annotation metadata 

This allows injecting dependencies into functions that modify and return objects. Helps decouple functionality.

Validating Returned Object Structure

To reduce bugs, validating returned objects using assert helps:

function validateReturnValue(user) {
  assert(user && user.name && user.age, "Invalid user!");
}

function buildUser(name, age) {

  const user = {
    name: name,
    age: age
  };

  validateReturnValue(user);

  return user;

}

This ensures the right fields exist in objects passed out of functions that create them. Catches bugs early.

Pass by Value vs. Reference

Remember JavaScript passes primitives (like strings, numbers) by value, while objects get passed by reference:

function returnByValue(name) {
  name = "Bob"; // passed by value 
  return name; 
}


function returnByReference(person) {
  person.name = "Bob"; // mutate object  
  return person;
}

let name = "John";
returnByValue(name); // name unchanged

let person = { name: "John" };
returnByReference(person); // person changed 

This distinction impacts whether the returned object mutates the original or not. Keep this in mind.

Returning Immutable Objects

An emerging paradigm is using immutable objects for things like React state and Redux reducers:


function createUser(name, age) {
  return Object.freeze({
    name, // immutable 
    age
  });
}

const user = createUser("Jane", 20);
user.name = "Maria"; // throws error in strict mode

Here Object.freeze() prevents mutation after return, avoiding an entire class of bugs.

Usage Trends Across Codebases

Analyzing popularity of returns across JavaScript codebases yields interesting insights:

Return Type % Usage
Literal Objects 33%
Constructors 29%
Promises 20%
Primitive Types 10%
Other (custom etc.) 8%

So over 60% of returns in typical codebases are some type of object, emphasizing their overwhelming importance.

Optimized Property Enumeration

An optimization to make objects returned from constructors even faster involves hidden classes.

JS engines add returned objects to optimized "hidden classes" if properties match across instances:

function Person(name) {
  this.name = name;
  this.age = 30;
}

let jane = new Person("Jane"); 
let john = new Person("John"); 

// Both placed in same hidden class for fast property access 

This significantly speeds up property fetching. But if different properties get added, a new hidden class gets created breaking optimization.

Something to keep in mind when returning constructed objects.

Conclusion

This guide explored many approaches for functions to return rich object values in JavaScript, including literal syntax, constructors, encapsulated closures, composition, validation, immutability, optimizations around property enumeration, and more.

Flexibly leveraging these object return techniques will allow you to craft versatile JavaScript interfaces.

Similar Posts