DEV Community

Cover image for Implementing JavaScript Concepts from Scratch

Implementing JavaScript Concepts from Scratch

Anton Zamay on April 07, 2024

In this article, we explore the foundational building blocks of JavaScript by crafting several key components from the ground up. As we delve into ...
Collapse
 
efpage profile image
Eckehard

Thanks for sharing. I was looking for internals quite some time to get better understanding.

Did you make some performance comparison to the "native" calls?

Collapse
 
antonzo profile image
Anton Zamay • Edited

Example of testing myMap performance:

Array.prototype.myMap = function(fn) {
  const result = new Array(this.length);
  for (let i = 0; i < this.length; i++) {
    result[i] = fn(this[i], i, this);
  }
  return result;
};

function square(n) {
  return n * n;
}

const smallArray = Array.from({length: 1000}, (_, index) => index);
const mediumArray = Array.from({length: 100000}, (_, index) => index);
const largeArray = Array.from({length: 10000000}, (_, index) => index);


function performTest(array, description) {
  console.time(`Built-in map - ${description}`);
  array.map(square);
  console.timeEnd(`Built-in map - ${description}`);

  console.time(`Custom myMap - ${description}`);
  array.myMap(square);
  console.timeEnd(`Custom myMap - ${description}`);
}


performTest(smallArray, 'Small Array');
performTest(mediumArray, 'Medium Array');
performTest(largeArray, 'Large Array');
Enter fullscreen mode Exit fullscreen mode

Results:

Built-in map - Small Array: 0.132ms
Custom myMap - Small Array: 0.131ms
Built-in map - Medium Array: 15.931ms
Custom myMap - Medium Array: 6.256ms
Built-in map - Large Array: 1.028s
Custom myMap - Large Array: 79.629ms
Enter fullscreen mode Exit fullscreen mode

What might be the potential reasons?

  1. My custom myMap function is a simplistic and direct implementation. It doesn't include many of the protective and flexible features of the built-in map, such as checking if the callback is a function or handling thisArg, which allows you to specify the value of this inside the callback. The absence of these features means less work is done under the hood, contributing to faster execution for straightforward tasks.

  2. The behavior of built-in functions is often more complex due to spec compliance, which might include checks and operations that your custom method does not perform. For instance, the built-in map must accommodate a wide range of scenarios and edge cases defined in the ECMAScript specification, like handling sparse arrays or dealing with objects that have modified prototypes. My custom myMap is straightforward and lacks these comprehensive checks, which might lead it to run faster for straightforward cases. For straightforward cases, in that scenario, JavaScript engines like V8 (Chrome, Node.js), SpiderMonkey (Firefox), and JavaScriptCore (Safari) might apply various optimizations at runtime.

Collapse
 
efpage profile image
Eckehard

The performance is awsome. Just the results for the large array are suspicious, as it is so much faster. Maybe you double check the results if Array.from({length: 10000000} is handled properly.

I found that there are a lot of strange effects resulting from the optimizations done by the JS-engine, so it is hard to find the real reason. Is const result = new Array() really faster than result.push()? Is for(;;) faster than this.forEach (I suppose it is...)?

The only way to find out it so try, and sometimes you will find no logical explanation for the results.

But anyway it is good to know your code is comparable, so it will be a good basis for own implementations with similar features.

Thread Thread
 
antonzo profile image
Anton Zamay

If the number of elements is predefined and static, then allocating memory by means of new Array(...) is certainly more efficient than dynamically re-allocating memory (done under the hood for .push), because during the next allocation all elements are copied into a new memory block (like in C++ for vectors, if I remember correctly). For the .filter I use the method with dynamic memory allocation, but to be honest I don't know how justified it is, I just decided to show different methods :)

And yes, as I said, all my implementations are simplified compared to native JS methods, which handle more edge cases. If you don't need to handle them, then yes, simplified implementations will speed up your project!

Thread Thread
 
efpage profile image
Eckehard • Edited

I'll try and report later, but more than once I found strange results on JS performance. Take this post as an example:

Strings are not objects, though, so why do they have properties? Whenever you try to refer to a property of a string s, JavaScript converts the string value to an object as if by calling new String(s). […] Once the property has been resolved, the newly created object is discarded. (Implementations are not required to actually create and discard this transient object: they must behave as if they do, however.)

On some properties (not all), differences are amazing:

  var i = strprimitive.charAt(0);
  var k = strobject["0"];
Enter fullscreen mode Exit fullscreen mode

performance

No chance to do some estimation based on "logic"...

Thread Thread
 
efpage profile image
Eckehard

I tried this

Array.prototype.myMap2 = function (fn) {
      const result = [];
      for (let i = 0; i < this.length; i++) {
        result.push(fn(this[i], i, this));
      }
      return result;
    };
Enter fullscreen mode Exit fullscreen mode

and this are the results (which are greatly varying with each run):

Built-in map - Small Array: 0.18896484375 ms
Custom myMap - Small Array: 0.5419921875 ms
Custom myMap2 - Small Array: 0.6591796875 ms
Built-in map - Medium Array: 14.93798828125 ms
Custom myMap - Medium Array: 9.138916015625 ms
Custom myMap2 - Medium Array: 11.64697265625 ms
Built-in map - Large Array: 2860.112060546875 ms
Custom myMap - Large Array: 235.31494140625 ms
Custom myMap2 - Large Array: 1246.418212890625 ms

So, as expected, differences are larger visible and dominant for very large arrays, but neglectible for usual sizes.

Thread Thread
 
antonzo profile image
Anton Zamay

You actually found a very interesting pitfall about creating a temporary object when accessing string methods. If you collect some of these pitfalls and publish them, I'd love to read them!

It would be even more interesting to test .filter than .map because this method can return a smaller array. It would be interesting to find the threshold of the returned number of elements, at which it is more advantageous to use dynamic array allocation, if such a threshold exists.

Thread Thread
 
efpage profile image
Eckehard

Writing a collection of JS pitfalls would probably fill a whole book. And even if you know all this tips, this does not save you from tapping into one. We will still need to do some performance testing.

If you are interested in general JS performance, maybe this is a comprehensive source.

But as Panagiotis Tsalaportas said:

Also, note that a browser’s overall performance, particularly when dealing with the DOM, is not only based on the page’s JS performance; there is a lot more in a browser than its JS engine.

Collapse
 
jonrandy profile image
Jon Randy πŸŽ–οΈ • Edited

Some of these aren't quite recreating JS functionality - you're missing the optional thisArg on map and filter.

Collapse
 
antonzo profile image
Anton Zamay

That's right, showing all aspects would actually be cumbersome, so I decided to show the most interesting ones. The implementation of thisArg in this case is extremely trivial.

Collapse
 
miketalbot profile image
Mike Talbot ⭐

Things like the filter implementation have bugs in the code, like misnamed parameters etc.

That's definitely not how finally works. There's no exception throwing in a finally

Collapse
 
antonzo profile image
Anton Zamay • Edited

Hey!

First, I would call a "typo", not "bugs". But thank you, I have corrected it!

Second, let's break down the functionality of .finally and how it's being used in MyPromise class.

In traditional synchronous code, a finally block indeed runs after a try/catch block regardless of the outcome (whether the try block executed successfully or an exception was caught in the catch block), and it is used for cleanup activities. It's true that in such a context, you wouldn't typically throw exceptions from a finally block because the purpose of finally is not to handle errors but to ensure some code runs no matter what happened in the try/catch blocks.

However, when it comes to promises, .finally has a somewhat different behavior:

  • It takes a callback that does not receive any argument.
  • After the callback is executed:
    • If the promise was fulfilled, the resulting promise from .finally is also fulfilled with the original value.
    • If the promise was rejected, the resulting promise from .finally is also rejected with the original reason.
  • If the callback function in .finally runs successfully, it does not alter the fulfillment or rejection of the promise chain (unlike what you suggest).
  • However, if an exception happens in the .finally callback itself, or if it returns a rejected promise, this will become the new rejection reason for the chain.
Collapse
 
oculus42 profile image
Samuel Rouse

Great article! I especially like the details of Description and Key Aspects...these are essentially the requirements process on a small scale.