---
# System prepended metadata

title: Tiny JavaScript Developer interview tasks

---

# Tiny JavaScript Developer interview tasks

## Browser differences

### Assumptions
I have made the following assumptions to write these functions.

1. The node may not be a valid node.
2. All browsers support node name in uppercase while other supports both lowercase and uppercase. In this case, it would be a good idea to return the node name in uppercase only to avoid inconsistent behaviour for the browsers which do not support lowercase.

**1. Write a function getGrandparentName(node) that, given a node, returns the node name of the grandparent of that node. Must work on all browsers**
```
function isValidNode(node) {
  return typeof node === "object" && node instanceof Node;
}

function getGrandparent(node) {
  if (isValidNode(node)) {
    const _parentNode = node.parentNode;
    if (!_parentNode) {
      return;
    }
    return _parentNode.parentNode;
  } else {
    return null;
  }
}

function getGrandparentName(node) {
  const _grantParentName = getGrandparent(node);
  if (isValidNode(_grantParentName)) {
    const _nodeName = _grantParentName.nodeName.toString();
    return _nodeName.toUpperCase();
  } else {
    return;
  }
}
```
**2. Write a function getGrandparent(node) that, given a node, returns the grandparent of that node. Must work on all browsers**
```
function isValidNode(node) {
  return typeof node === "object" && node instanceof Node;
}

function getGrandparent(node) {
  if (isValidNode(node)) {
    const _parentNode = node.parentNode;
    if (!_parentNode) {
      return;
    }
    return _parentNode.parentNode;
  } else {
    return null;
  }
}

```

**3. Give a new definition of getGrandparentName that uses your definition of getGrandparent from (2)**
```
function isValidNode(node) {
  return typeof node === "object" && node instanceof Node;
}

function getGrandparent(node) {
  if (isValidNode(node)) {
    const _parentNode = node.parentNode;
    if (!_parentNode) {
      return;
    }
    return _parentNode.parentNode;
  } else {
    return null;
  }
}

function getGrandparentName(node) {
  const _grantParentName = getGrandparent(node);
  if (isValidNode(_grantParentName)) {
    const _nodeName = _grantParentName.nodeName.toString();
    return _nodeName.toUpperCase();
  } else {
    return;
  }
}
```

**4. Write a function getGreatGrandparent(node) that given a node, returns the node that is the great grandparent of node (e.g. getGreatGrandparent(eclair) === apple). Must work on all browsers**

```
function getGreatGrantParentName(node) {
  if (isValidNode(node)) {
    const _grantparentNode = getGrandparent(node);
    if (_grantparentNode) {
      return _grantparentNode.parentNode;
    } else {
      return null;
    }
  } else {
    return null;
  }
}
```
**5. What are your thoughts about your implementations above? If you knew you had to write all of these functions at the start, would you have changed your approach? If so, why? How well does your approach scale to going even further up the tree?**

I would not have written dedicated functions to return grant parent or great grandparent. Instead, I would have written a generic function which would allow getting any node from higher up within the tree. This function would expect a node which wants to get access to its ancestors nodes and a number to get a specific higher up ancestor node.

For example:
getNode(node, 3) // 3 would indicate you want to get great grand Parent.

getNode(node, 0) // 0 would return null as it would be the node itself.

Please check out the below implemetation of getNode function that I have written through a recrusive function:
```
// get any node from higher up.
function getNode(node, level) {
  if (level) {
    if (!isValidNode(node)) {
      return;
    }

    const parentNode = node.parentNode;
    if (!parentNode) {
      return;
    }

    if (level === 1) {
      return parentNode;
    } else {
      return getNode(parentNode, level - 1);
    }
  }
  return;
}
```

## Options

**1. Using these methods for Option, rewrite getGrandparentName(node) and getGrandparent(node) to return Options.**

```

const some = (value) => {
  const isSome = () => !!value;
  const getOrDie = () => value;
  
  return {
    isSome,
    getOrDie
  };
};

const none = (value = "none") => {
  const isSome = () => value !== "none";

  const getOrDie = () => {
    const error = new Error("error");
    return error;
  };

  return {
    isSome,
    getOrDie
  };
};

function isValidNode(node) {
  return typeof node === "object" && node instanceof Node;
}

function getGrandparent(node) {
  if (isValidNode(node)) {
    const _parentNode = node.parentNode;
    if (!_parentNode) {
      return none();
    }
    return some(_parentNode.parentNode);
  } else {
    return none();
  }
}

function getGrandparentName(node) {
  const _grantParentName = getGrandparent(node);
  if (isValidNode(_grantParentName)) {
    const _nodeName = _grantParentName.nodeName.toString();
    return some(_nodeName.toUpperCase());
  } else {
    return none();
  }
}
```

**2. Using these two functions, remove any usage of isSome and getOrDie, and rewrite getGrandparentName and getGrandparent to use map and/or bind.**

```
const some = (value) => {
  const map = (f) => some(f(value));
  const bind = (f) => f(value);

  return {
    map,
    bind
  };
};

const none = (value = "none") => {

  const map = (f) => none();
  const bind = (f) => none();

  return {
    map,
    bind
  };
};

function isValidNodeWithOption(node) {
  return typeof node === "object" && node instanceof Node ? some(node) : none();
}

function getGrandparentWithMapAndBind(node) {
  return isValidNodeWithOption(node)
    .bind((node) => (node?.parentNode ? some(node.parentNode) : none()))
    .bind((_parentNode) =>
      _parentNode?.parentNode ? some(_parentNode.parentNode) : none()
    );
}

function getGrandparentNameWithMapAndBind(node) {
  const _grantParentName = getGrandparentWithMapAndBind(node);
  return _grantParentName
    .bind((node) => {
      return isValidNode(node) ? some(node.nodeName) : none();
    })
    .map((val) => {
      return val.toUpperCase();
    });
}
```

**3. Now, also rewrite getGreatGrandparent and using map and/or bind**

```
function getGreatGrantParentName(node) {
  if (isValidNode(node)) {
    return getGrandparent(node).bind((val) => {
      return val?.parentNode ? some(val.parentNode.id) : none();
    });
  } else {
    return none();
  }
}
```

**4. What do you think of the developer’s Option approach. What are its strengths? What are its weaknesses?**

#### Strengths:

1. If the initial value was null, you have added extra operations, you don't have to worry about null check because null Option would not execute any operations and would simply return another none option.
2. With map and bind operations, we are not modifying the underlying option. Instead, we are just returning a new option or returning its value through a bind function.

#### Weakness:

1. It might feel too much for the simple scenarios. For example, in order to retrieve a value from an option, you have to perform additional an operation through bind. If you use a map, it would return another some and will require a bind to get the value.


**5. Write a function getParentOrSelf(node) which uses fold and takes a node and returns a parent of that node if one exists, otherwise the node itself.**

```

const some = (value) => {
  const map = (f) => some(f(value));
  const bind = (f) => f(value);
  const fold = (ifNone, f2) => f2(value);

  return {
    map,
    bind,
    fold
  };
};

const none = (value = "none") => {
  const map = (f) => none();
  const bind = (f) => none();
  const fold = (ifNone, f2) => ifNone();

  return {
    map,
    bind,
    fold
  };
};

function isValidNode(node) {
  return typeof node === "object" && node instanceof Node;
}

function getParentOrSelf(node) {
  if (isValidNode(node)) {
    const _parentNode = node.parentNode;
    if (!_parentNode) {
      return some(_parentNode).fold(
        () => "none",
        (val) => `some of ${val}`
      );
    } else {
      return none().fold(
        () => node,
        (val) => `some of ${val}`
      );
    }
  }
  return some(node).fold(
    () => "none",
    (val) => `some of ${val}`
  );
}
```

**6. You can see that fold can fulfil a similar objective to using isSome and if. Think about why someone might want to use one over the other. It may help to write out the two side-by-side and compare for the getParentOrSelf example.**

With fold, you are aware that the value may or may not exist. Hence it expects to provide 2 functions one for some and another for none.

you can keep performing additional operations without worrying about the value being null. If the value were null, it would have been typed of None and the fold will perform the function with the empty parameter and wold return default empty value. If it had a value, it would just return that value.

As compare to iSSome, you will be able to check whether the value is there. You will have to call getOrDie method to retrieve the value.

I think fold makes code more readable in the complex scenarios.


## Mutability

**1. One way of testing a stack’s operations is to identify relationships that should hold between the operations. Given a completely random stack, list all of the relationships that you can think of between (push, pop, peek, size). We’ll get you started**

1. If peek is none(), then stack size should be 0
2. if peek is some(anyValue), then stack size should be greater than 0
3. after peek, the stack size should not change
4. after push(x), pop should return some(x)
5. after push(x), the stack size should be increase by 1
6. after push(x), peek should be last added some(lastValue)
7. after pop, the stack size should be decreased by 1,
8. after pop, option some should be of the last stack item
9. after size, the stack size should not change
10. if stack size is 0, peek and pop should be none()

#### Introducing immutability

2. Rewrite the stack pseudo code using these new methods. We’ll get you started...


```
const size = (stack) => stack.length;

const push = (stack, item) => [...stack, item];

const pop = (stack) => {
  if (stack.length > 0) {
    const newStack = stack.filter((item) => item !== item[stack.size - 1]);
    return [...newStack, stack[stack.size - 1]];
  } else {
    return [...stack, null];
  }
};

const peek = (stack) => {
  if (stack.length > 0) {
    return [...stack, stack[stack.size - 1]];
  } else {
    return [...stack, null];
  }
};
```

**3.  What is the difference between this implementation and the previous Stack object implementation?**

The previous stack object implementation performs all the operations on the same underlying object. Whereas the array implementations, we are performing each new operation (except size) on a new copy of array so the original array does not get modified.

**4. What are the advantages and disadvantages?**

#### Advantages

1. Since we are interacting with a new copy of the array with each operation, we are not mutating the original array. Hence, we can be sure that the immutable arrays would not have been modified by other function or thread that will lead to less chance of having nasty bugs
2. Easier and faster to compare two immutable arrays by `newArray === oldArray`

#### Disadvantages

1. Expensive to create a new array in terms of time and memory.


**5. Which one do you think would be easier to test? Why?**

Immutable arrays are easier test as a new array is created with every operation and never gets modified afterwards. so we can predict the future state.

## Events

Note, in the following code we are using let instead of var
**1. Why is that? What would happen if we used var?**

If we used a var in the loops, this var variable could be accessed after the loops have executed. Even though these variables are scoped to these loops only. Whereas if we used `let`, they would not be accessible after the loop has executed.

Since var variables are hoisted, you can access them before they are declared if you are not using strict mode. That could lead to unexpected behaviour.

Hence it is a good idea to always use `let` variables to prevent them accessible outside their local scope.

**2. How would you rewrite this to use event delegation? You can add any attributes to the cells that you wish to achieve the task.**

```
const table = document.getElementsByTagName("table")[0];
table.onclick = (event) => {
  if (event.target.nodeName === "TD") {
    const cellIndex = event.target.cellIndex;
    const rowIndex = event.target.closest("TR").rowIndex;
    console.log(`clicked on element located at Row #${rowIndex} and Column #${cellIndex}`);
  }
};
```

**3. What are the advantages of event delegation? What are the disadvantages?**

#### Advantages:

1. Event delegation allows managing click events from one place for any child element within this table
2. We will not need to add dedicated click event handlers
3. If we add more rows/column dynamically, we will not need to add additional custom click handlers. The same function can handle events for the newly added columns and rows.
4. Since the event is managed from one place, it would make it much easier to maintain the code.

#### Disadvantages

1. If the child elements prevent event propagation using the stopPropagation, the events will not be bubble up to the parent element. Hence, the parent will not be able listen to any events.
2. Event delegation does not work for all types of events. Other events like blur, focus cab be tough to work with event delegations.


## Demo

You can see the output of all the functions I have written on the code sandbox. Please check out https://codesandbox.io/s/funny-newton-pc537?file=/index.html

## Thank you!

I have really enjoyed working on this test. Especially the option monad. I did not know about them. I got to learn them and implement them in this test.

If you have any feedback, you can share them in here. This tool allows you to make comments. I look forward to hearing your feedback. 