loading...
Cover image for JavaScript: The Fun Parts

JavaScript: The Fun Parts

solkimicreb profile image Miklos Bertalan ・4 min read

JavaScript is evolving rapidly with a bunch of yearly additions. Some of them never reach the spotlight, others get forgotten or deprecated. Let's put these into increasingly hacky editable code sandboxes. The goal is to make you play with occasional horror on your face.

The examples are taken from real libraries or apps that I have written over the years. The last few are controversial though, and I do not encourage you to use them in your code.

Finally

I usually use finally when I have to wrap other devs' functions with some extra logic. finally does this without messing with the original behavior - like return values or thrown Errors.

This example instruments the passed function with performance measurements.

function measure (fn) {
  const start = Date.now()
  try {
    return fn()
  } finally {
    console.log(`${fn.name} took ${Date.now() - start} ms`)
  }
}

Edit l9wj5zx20l

WeakSet and WeakMap

These two provide truly private properties (unlike Symbols). I use them when I have to append some metadata to others' objects. Directly mutating these would be bad manners, plus you get screwed if the object is frozen.

This example stringifies the passed object and caches the result. The memoization speeds up the operation and the WeakMap usage avoids object pollution and memory leaks.

const cache = new WeakMap()

export default function stringify(obj) {
  let result = cache.get(obj)
  if (!result) {
    result = JSON.stringify(obj, null, 2)
    cache.set(obj, result)
  }
  return result
}

Edit wn1ror8jz5

Proxy

Proxies can tweak language behavior. Most of the time I use them to extend the language and occasionally I use them completely change some default behavior.

This reactive example lets you create observables and reactions. A reaction automatically re-runs when an observable - used inside it - is mutated. The snippet is extremely oversimplified but some additional coding can take it pretty far.

let runningFn
const reactions = new WeakMap()

export function observe(fn) {
  runningFn = fn
  try {
    return fn()
  } finally {
    runningFn = undefined
  }
}

export function observable(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      runningFn && reactions.set(target, runningFn)
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      Reflect.set(target, key, value, receiver)
      const reaction = reactions.get(target)
      reaction && reaction()
    }
  })
}

Edit wn7nz5x7x8

New Function

new Function() is similar to eval but it creates functions in the global scope, which is outside of any 'strict mode' declarations. We will abuse this fact soon.

This example gets the global object - regardless of the platform - which is currently trickier than it should be. It is also used in the ES2019 globalThis proposal polyfill.

const globalThis = new Function('return this')()

This won't work if you have CSP set up for your page.

With

with extends the scope chain for a statement. Sadly, it is deprecated and it won't work in strict mode - which includes ES6 modules. A few tricks can save the day though.

This example lets you write Vue or Angular like template files and renders them in the context of the passed state.

export default function compile(template) {
  return new Function("state", `with (wrap(state)) { return \`${template}\` }`)
}

window.wrap = state => new Proxy(state, {
  has: () => true
})

Edit wyv631ry7k

It tweaks with behavior with the following tricks:

  • It uses new Function() - instead of eval - to create functions in the global scope. The global scope is outside strict mode so with works there.

  • It uses Proxies to tweak with behavior a bit. Normally with extends the scope chain and does not replace it completely. When a property is not found on the passed object it will search for it in the next scope - in our case the global one. We do not want the global scope to leak into the template so we have to trick with into thinking that every possible property is present on the passed object. This is pretty simple with the has Proxy trap.

Everything Together

We accidentally created a blazing fast lightweight MVC framework along the way.

Please check the repo here and don't forget to leave a star!


I hope you found some time to play with the sandboxes. Leave a comment if you created something crazy.

Thanks for reading!

Posted on by:

solkimicreb profile

Miklos Bertalan

@solkimicreb

JavaScript dev, OSS maintainer, finance, traveler, and owner of two cats. I like slightly hacky things.

Discussion

markdown guide
 

How about empty loops, they're a neat but slightly unknown feature:

var total = 0;
for (var i = 0; i < myarr.length; total += myarr[i++]);

This isn't a perfect example (could use .reduce) but it gives you an idea of how tidy looking empty for loops can be.

Also, in a for..in loop the expression before in can be any left-hand side of an assignment.

var obj = {'foo': 'bar', 'baz': 'quux'},
    arr = [],
    i = 0;
for (arr[i++] in obj);
// arr is now ["foo", "baz"]
 

Looking for advice, do you feel like using a WeakMap would be better here than using the symbol?

import { BufferWithPointer } from "./buffer"

const byteLength = Symbol("ByteLengthRead")

/** Utility to specifiy that a reader function consumes a constant length of buffer */
export const fixedLength = <
  L extends number,
  F extends ((r: BufferWithPointer) => any) & { [byteLength]?: L }
>(
  len: L,
  fn: F
) => {
  if (!Number.isInteger(len) || len < 1)
    throw new TypeError(
      `byte length must be a positive integer, got ${len} instead`
    )
  fn[byteLength] = len
  return fn as F & { [byteLength]: L }
}

/** Wraps a reader function, to read until end of buffer and return array */
export const readUntilEnd = <R>(
  fn: ((r: BufferWithPointer) => R) & { [byteLength]?: number }
): ((r: BufferWithPointer) => R[]) => {
  const len = fn[byteLength]
  return len
    ? r => {
        const repeats = r.remaining() / len
        if (!Number.isInteger(repeats))
          throw new RangeError(
            `Bad buffer: Remaining length not multiple of repeat unit length`
          )
        const arr: R[] = new Array(repeats)
        for (let i = 0; i < repeats; i++) {
          arr[i] = fn(r)
        }
        return arr
      }
    : r => {
        const arr: R[] = []
        let last = r.remaining()
        while (last) {
          arr.push(fn(r))
          const remaining = r.remaining()
          if (remaining >= last)
            throw new Error(`Infinite loop: repeat unit isn't consuming buffer`)
          last = remaining
        }
        return arr
      }
}

 

Yep, I think WeakMap suits this more but it's just a semantical change in this case, not a critical one. Theoretically, this implementation could cause issues with some Proxy based libraries (which do intercept Symbol get/set).

Generally, I like to use symbols like well-known symbols. When I create an internal feature that I want to expose to experienced devs (by exporting the symbol) but don't want to be accidentally overwritten (like a normal prop).

I hope this helps a bit.

PS: I use both Symbols and WeakMaps in a lib which I perf profile often and I found no real difference in performance (in Chrome and Node at least.)

 

Cool. Maybe I'll switch then.
It was fun to type this, with the "wrapped" functions having a literal number attached to them, but it's not like either the Symbol or the WeakMap will ever be exported.

On the other hand, I won't be able to tell if a function is "wrapped" by type tooltip any more or see its bytelength :/

That's true. I only use TypeScript when I must so I tend to not think about typings.

 

Great article! A lot of these I didn't know. I especially like finally, from the naming it already says a lot about what it does. Your measuring example is nice too, I might use that myself :)