Skip to content

traverse walk()

Eugene Lazutkin edited this page May 8, 2026 · 6 revisions

Walks an object tree non-recursively, visiting all values with customizable processors.

Introduction

This is the foundation traversal module used by clone, assemble, deref, and preprocess. It uses a stack-based approach to avoid recursion limits and handles circular references.

import walk from 'deep6/traverse/walk.js';

const result = [];
walk(
  {a: 1, b: {c: 2}},
  {
    processOther: (v, ctx) => result.push(v),
    circular: true
  }
);
// result contains all primitive values: [2, 1]
// (reverse order because the stack processes depth-first)

API

walk(o [, options])

Arguments:

  • o — a required value to traverse. It can be anything.
  • options — an optional object. The following optional properties are recognized:
    • processObject — a function (object, context) => void called for each plain object (not matched by the registry or filters). Default: pushes all property values onto the stack.
    • processOther — a function (value, context) => void called for non-object values (primitives, null, and _). Default: no-op.
    • processCircular — a function (value, context) => void called when an already-seen object is encountered. Default: no-op.
    • registry — a flat array of Constructor/handler pairs: [Type1, handler1, Type2, handler2, ...]. When an object is instanceof a registered type, the corresponding handler is called instead of processObject. Default: the built-in registry export.
    • filters — an array of predicate functions (value, context) => boolean. Called for objects that don't match the registry. Return true to indicate the value was fully handled. Default: empty.
    • circular — a boolean flag to enable circular reference detection via a seen Map.
    • symbols — a boolean flag. When true, symbol properties are included.
    • allProps — a boolean flag. When true, non-enumerable properties are included.
    • context — a custom context object. Its properties are preserved and available to all processors.

The walk function returns nothing. Results are collected through the context object (typically context.stackOut).

Context object

The following properties are set by walk on the context object and available to all processors:

  • stack — the processing stack. Push values here to schedule them for traversal.
  • seen — a Map tracking visited objects (present when circular: true, otherwise null).
  • symbols — reflects the symbols option.
  • allProps — reflects the allProps option.

Processors and registry handlers may set additional properties (e.g., stackOut, env).

Registry and filters

The registry is a flat array iterated in steps of 2: [Type1, handler1, Type2, handler2, ...]. For each object encountered, if o instanceof Type, the corresponding handler (object, context) => void is called. The first match wins.

Extend with:

import {registry} from 'deep6/traverse/walk.js';
registry.push(MyClass, (val, context) => {
  // handle MyClass instances
  context.stackOut.push(val); // or push children to context.stack
});

Filters are a plain array of predicate functions. They are checked for objects that don't match any registry entry. Each filter receives (object, context) and returns true if the value was handled.

Processor factories

Two factory functions create standard processors that handle post-processing:

  • processObject(postProcess, postProcessSeen) — returns a processor for plain objects and arrays. It pushes a Command onto the stack for post-processing, then pushes all property values. postProcessSeen is used instead of postProcess when circular tracking is active.
  • processMap(postProcess, postProcessSeen) — same pattern for Maps. Pushes all Map values onto the stack.

The postProcess callbacks are called as methods on a Command, so this.s is the source object being processed.

Command class

Command is a deferred action pushed onto the walk stack. When popped, processCommand calls this.f(context). The this.s property holds the source object. This is how post-processing works: the command is pushed before the children, so it fires after all children have been processed.

import {Command, processCommand} from 'deep6/traverse/walk.js';

// Custom post-processor
function myPostProcess(context) {
  const source = this.s;
  // context.stackOut has processed children
}

Circular class

Circular is a marker wrapping an already-seen object. It is pushed to context.stackOut by the default processCircular. Post-processors check for Circular instances to resolve back-references.

Exports

All exports are named. The default export is walk.

  • walk — the main traversal function (also walk.Command for convenience).
  • Command — deferred processing command class.
  • Circular — marker class for circular references.
  • registry — default type handler registry (flat array).
  • filters — default filter array (initially empty).
  • processOther — pushes value to context.stackOut.
  • processCircular — pushes new Circular(value) to context.stackOut.
  • processObject(fn, fnSeen) — factory for object/array processors.
  • processMap(fn, fnSeen) — factory for Map processors.
  • processCommand — executes a Command's function.
  • processVariable — resolves a Variable via context.env or passes it through.
  • getObjectData(object, context) — returns {descriptors, keys} respecting symbols/allProps flags.
  • buildNewObject(source, descriptors, keys, stackOut, wrap) — reconstructs an object from processed values on stackOut.
  • buildNewMap(keys, stackOut, wrap) — reconstructs a Map from processed values on stackOut.
  • postObjectCircular(source, descriptors, keys, context) — reconstructs an object with circular reference resolution.
  • postMapCircular(source, context) — reconstructs a Map with circular reference resolution.
  • setObject(seen, source, value) — resolves deferred circular reference actions.
  • replaceObject(upTo, object, stackOut) — replaces stackOut tail with a single object (used when source is unchanged).

Notes

Built-in registry handles: Command, Array, Date, RegExp, Map, Set, URL, Promise, typed arrays, DataView, ArrayBuffer. Of these, only Array and Map actually recurse into children (via processObject/processMap); Date, RegExp, Set, Promise, URL, typed arrays, DataView, and ArrayBuffer are mapped to nop (no traversal). The default processObject handles plain objects (those not matched by the registry) and is what makes naked {...} and [...] recurse.

Cycle detection only fires for the recursing types — tracking atomic-leaf values would spuriously rewrite reused leaves as Circular placeholders on second visit. If a custom registry handler recurses into children (pushes onto context.stack), the type it handles is implicitly considered recursable.

Clone this wiki locally