Skip to content
Back to blog

Reimplementing Native Code

Studying the behavior of native JS code by simulating native functionality.

Updated

range()

Test case:

for (let num of range(1, 4)) {
  console.log(num)
}
// 1
// 2
// 3
// 4

Plain Loop

/**
 * @param {number} from
 * @param {number} to
 */
function range(from, to) {
  const list = []

  for (let i = from; i <= to; ++i) {
    list.push(i)
  }

  return list
}

Iterator

/**
 * Manually implementing an iterator
 *
 * @param {number} from
 * @param {number} to
 */
function range(from, to) {
  let i = from
  return {
    [Symbol.iterator]() {
      const isDone = i > to
      return {
        done: isDone,
        value: isDone ? undefined : i,
      }
    }
  }
}

Generator

/**
 * A generator's result can be iterated by for of; it already implements iterator itself
 *
 * @param {number} from
 * @param {number} to
 */
function range(from, to) {
  return (function * () {
    while (from <= to) {
      yield from++
    }
  })()
}

Iterator + Generator

/**
 * One extra layer of wrapping, purely to pad the count of approaches; the upside is that no IIFE is needed
 *
 * @param {number} from
 * @param {number} to
 */
function range(from, to) {
  return {
    [Symbol.iterator]: function * () {
      while (from <= to) {
        yield from++
      }
    },
  }
}

new

function _new(fn, ...args) {
  const target = Object.create(fn.prototype)
  const result = fn.call(target, ...args)
  const isObjectOrFunction = (result !== null && typeof result === 'object') || typeof result === 'function'
  return isObjectOrFunction ? result : target
}

instanceof

function myInstanceof(left, right) {
  // Use typeof first to check for primitive types; if so, return false directly
  if (typeof left !== 'object' || left === null) {
    return false
  }
  // getPrototypeOf is an Object API that gets the prototype object of the argument
  let proto = Object.getPrototypeOf(left)
  while (true) {
    if (proto === null) {
      return false
    }
    if (proto === right.prototype) {
      // Found the same prototype object, return true
      return true
    }
    proto = Object.getPrototypeOf(proto)
  }
}

call

function myCall(thisArg, ...args) {
  if (thisArg == null) {
    return fn(...args)
  }

  // Use a Symbol to prevent property conflicts
  const prop = Symbol('temporary property for fn')
  thisArg[prop] = fn

  const result = thisArg[prop](...args)
  // Remove the temporary property
  Reflect.deleteProperty(thisArg, prop)

  return result
}

function printName() {
  console.log(this.name)
}

console.log(printName.myCall({ name: 'ly' })) // => ly

apply

function myApply(thisArg, args) {
  // Here `this` is actually the func in func.myCall(thisArg, ...args), because myCall is invoked through func
  const func = this

  if (thisArg === undefined || thisArg === null) {
    // If thisArg is undefined or null, this points to the global object; calling directly achieves pointing to the global object
    return func(args)
  }

  const tempFunc = Symbol('Temp property')
  // Temporarily bind func onto thisArg
  thisArg[tempFunc] = func

  // Invoke func through thisArg to change the this binding
  const result = thisArg[tempFunc](args)

  // Delete the temporary property
  delete thisArg[tempFunc]
  return result
}

function printName() {
  console.log(this.name)
}

console.log(printName.myCall({ name: 'ly' })) // => ly

bind

Basic Implementation, No new Support

function myBind(func, thisArg, ...args) {
  // bind returns a new function
  return function (...otherArgs) {
    // When the function executes, this is always the thisArg from the outer function, and the earlier call arguments are also bound as args
    return func.call(thisArg, ...args, ...otherArgs)
  }
}

function printThisAndAndArgs(...args) {
  console.log(`This is ${JSON.stringify(this)}, arguments is ${[...args].join(', ')}`)
}

const boundFunc = myBind(printThisAndAndArgs, { name: 'Lily' }, 1, 2, 3)
boundFunc(4, 5, 6) // => This is {"name":"Lily"}, arguments is 1, 2, 3, 4, 5, 6

A bind Version That Supports new

function myBind(func, thisArg, ...args) {
  // bind returns a new function; if the bound function is invoked with new, the this inside it is the instance object returned by new, i.e. target
  return function (...otherArgs) {
    // When new.target is func and not empty, bind this instead of thisArg
    return func.call(new.target ? this : thisArg, ...args, ...otherArgs)
  }
}

function Student(name, age) {
  this.name = name
  this.age = age
}

const BoundStudent1 = Student.bind({ name: 'Taylor' }, 'ly')
console.log(new BoundStudent1(22)) // => Student { name: 'ly', age: 22 }

const BoundStudent2 = myBind(Student, { name: 'Taylor' }, 'ly')
console.log(new BoundStudent2(22)) // => { name: 'ly', age: 22 }

The Final, Complete Version

/**
 * Simulating Function.prototype.bind
 *
 * @param {Function} fn The function whose this is being bound
 * @param {*} thisArg
 * @param  {...any} boundArgs The bound arguments
 * @returns {Function} The new function with this bound
 */
function myBind(fn, thisArg, ...boundArgs) {
  function boundFn(...otherArgs) {
    // When invoked with new, this should be the designated this
    const ctx = new.target ? this : thisArg
    return fn.call(ctx, ...boundArgs, ...otherArgs)
  }

  // This step ensures that when boundFn is used as a constructor, its instances can properly access properties on fn's prototype chain
  boundFn.prototype = Object.create(fn.prototype)
  boundFn.prototype.constructor = boundFn

  // The default is writable: false, so both name and length are set to non-writable
  Object.defineProperties(boundFn, {
    name: {
      // The name of a this-bound new function is the original function name prefixed with bound
      value: `bound ${fn.name}`,
    },
    length: {
      // boundFn's length is the number of remaining arguments to pass
      value: Math.max(fn.length - boundArgs.length, 0),
    },
  })

  return boundFn
}

function Student(name, age) {
  this.name = name
  this.age = age
}

Student.prototype.type = 'student'
const BoundStudent2 = Student.myBind({ name: 'Taylor' }, 'ly')

console.log(new BoundStudent2(22).type) // => student
console.log(BoundStudent2.name) // => bound Student
console.log(BoundStudent2.length) // => 2