Is there an intuitive way to understand how this Javascript callback function is working?

Viewed 28

Code line in question:

callbackFn ? callbackFn(currentNode) : levelOrderList.push(currentNode.value);

I am having trouble of a way to think of this in psuedo-code terms since 'callbackFn' is used like a function but not defined like a function.

I know this code works and have ran it myself. I have also solved this without using the callbackFn, but I would really like to understand why this works.

My guess for psuedo cod would be: if callbackFn exists (not null or undefined), then return callbackFn(currentNode). else push currentNode.value to the levelOrderList.

Full code for context:

  function levelOrder(callbackFn) {
    const queue = [this.root];
    const levelOrderList = [];
    while (queue.length > 0) {
      const currentNode = queue.shift();
      callbackFn ? callbackFn(currentNode) : levelOrderList.push(currentNode.value);

      const enqueueList = [
        currentNode?.leftChild,
        currentNode?.rightChild
      ].filter((value) => value);
      queue.push(...enqueueList);
    }
    if (levelOrderList.length > 0) return levelOrderList;
  }
1 Answers

Your guess for pseudo code is correct.

The author of that could should better have used an if...else structure like your pseudo code does. The conditional operator (? :) is used here as an unnecessary short-cut. Normally you would use the conditional operator to use the value that it evaluates to, like x = condition ? a : b;. But here that value is ignored. There is really no good reason to avoid if...else here.

The author added support for a callback mechanism as an alternative to returning an array. This doesn't look like best practice to me either. For two reasons:

  • This "polymorphism" can be confusing for the user of such an API. It is easier to understand when the two functionalities are offered by two different functions, one that returns the result in an array, another that calls the callback. The caller will choose the function based on how they want to deal with the traversed nodes.

  • A callback mechanism is rather "old style". It makes more sense to turn this function into a generator function. The caller can then easily decide what to do with the nodes that the returned iterator yields: collect those nodes in an array or just process them one by one.

This is how that generator function would look like:

class Node {
    constructor(value) {
        this.value = value;
        this.leftChild = this.rightChild = null;
    }
}

class Tree {
    constructor(...values) {
        this.root = null;
        for (let value of values) this.add(value);
    }
    add(value) {
        function addTo(node) {
            if (!node) return new Node(value);
            if (value < node.value) {
                node.leftChild = addTo(node.leftChild);
            } else {
                node.rightChild = addTo(node.rightChild);
            }
            return node;
        }
        this.root = addTo(this.root);
    }
    *levelOrder() {
        if (!this.root) return;
        const queue = [this.root];
        while (queue.length > 0) {
            const currentNode = queue.shift();
            yield currentNode.value;
            if (currentNode.leftChild) queue.push(currentNode.leftChild);
            if (currentNode.rightChild) queue.push(currentNode.rightChild);
        }
    }
}

// Demo
const tree = new Tree(4, 6, 7, 2, 1, 5, 3);
// Several ways to use the levelOrder generator function:
console.log(...tree.levelOrder());
console.log(Array.from(tree.levelOrder()));
for (let value of tree.levelOrder()) console.log(value);

Related