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