Stick to 36 JS handwritten questions (after understanding, the improvement is really great)


Why write such articles

As a programmer, code ability is undoubtedly very, very important. For example, why do big companies basically ask what API to implement in interviews? We can see its importance. What I want to say is that handwriting is so important that we must master it. Therefore, it is not too much to use the title of the article, and I hope it will not be regarded as the title party.

As an ordinary front-end, I really can't write Promise A + specification, but it doesn't matter. We can stand on the shoulders of giants and believe that the road we are going to take has been passed by our predecessors, so we can find those excellent articles that already exist in the community, such as those written by industrial leaders 100 lines of code to realize Promises/A + specification , after finding these articles, I'm not a favorite to eat ash. I have to find a time to study steadily and grind them line by line until I understand them. That's what I do now.

What can be gained

This article is generally divided into two types of handwritten questions. The first half can be summarized as common requirements, and the second half is the implementation of the existing technology;

  • Handwritten implementation of common requirements, such as data type judgment function and deep copy, can be directly used in future projects to improve project development efficiency;
  • For the implementation of existing keywords and APIs, you may need to use other knowledge or APIs. For example, when writing forEach, you use the operation of moving the unsigned bit to the right. You usually don't have much access to this thing. Now you can master it easily. Therefore, these implementations can imperceptibly expand and consolidate their JS foundation;
  • By writing various test cases, you will know the boundaries of various API s, such as promise All, you have to consider various situations of incoming parameters, so as to deepen your understanding and use of them;

What do you need to do when reading

When reading, you need to understand every line of code, know what it is doing and why it is written like this. Can you write better? For example, when writing an image for lazy loading, we usually judge whether to load the image according to the position of the current element and the viewport. Ordinary programmers write that this is almost complete. The big programmers will consider more details, such as how the performance is better? How can the code be more streamlined? For example, the lazy loading of pictures written by yeyan 1996 considers two more points: for example, when all pictures are loaded, the event listener must be removed; For example, when loading an image, you have to remove the current img from the imgList to optimize memory.

In addition to reading the code, you can also open Chrome's Script snippet to write test case running code for better understanding and use.

After reading several articles and writing a lot of test cases, try to write your own implementation to see how much you have mastered. All roads lead to Rome. What else can you do? Or can you write better than others?

Well, what are you doing? Start working.

Data type judgment

Typeof can correctly identify: Undefined, Boolean, Number, String, Symbol, Function and other types of data, but other types will be regarded as objects, such as Null, Date, etc., so it will be inaccurate to judge the data type through typeof. However, you can use object prototype. ToString implementation.

function typeOf(obj) {
-   let res = Object.prototype.toString.call(obj).split(' ')[1]
-   res = res.substring(0, res.length - 1).toLowerCase()
-   return res
// The better writing mentioned in the comment area
+   return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase()
}
typeOf([])        // 'array'
typeOf({})        // 'object'
typeOf(new Date)  // 'date'

inherit

Prototype chain inheritance

function Animal() {
    this.colors = ['black', 'white']
}
Animal.prototype.getColor = function() {
    return this.colors
}
function Dog() {}
Dog.prototype =  new Animal()

let dog1 = new Dog()
dog1.colors.push('brown')
let dog2 = new Dog()
console.log(dog2.colors)  // ['black', 'white', 'brown']
Copy code

Problems in prototype chain inheritance:

  • Question 1: the reference type attribute contained in the prototype will be shared by all instances;
  • Problem 2: subclasses cannot pass parameters to the parent constructor when instantiating;

Borrowing constructor to implement inheritance

function Animal(name) {
    this.name = name
    this.getName = function() {
        return this.name
    }
}
function Dog(name) {
    Animal.call(this, name)
}
Dog.prototype =  new Animal()
Copy code

Using constructor to implement inheritance solves two problems of prototype chain inheritance: reference type sharing and parameter passing. However, because the method must be defined in the constructor, the method will be created every time a subclass instance is created.

Combinatorial inheritance

Combinatorial inheritance combines the prototype chain and the embezzlement of constructors, which combines the advantages of the two. The basic idea is to use the prototype chain to inherit the properties and methods on the prototype, and inherit the instance properties by stealing the constructor. In this way, the method can be defined on the prototype to realize reuse, and each instance can have its own properties.

function Animal(name) {
    this.name = name
    this.colors = ['black', 'white']
}
Animal.prototype.getName = function() {
    return this.name
}
function Dog(name, age) {
    Animal.call(this, name)
    this.age = age
}
Dog.prototype =  new Animal()
Dog.prototype.constructor = Dog

let dog1 = new Dog('Milkshake', 2)
dog1.colors.push('brown')
let dog2 = new Dog('Hachi', 1)
console.log(dog2) 
// {name: "Hachi", colors: [black "," white], age: 1}

Parasitic combinatorial inheritance

Composite inheritance has been relatively perfect, but there are still problems. Its problem is that it calls the parent constructor twice, the first time in new Animal(), and the second time in animal Call () here.

Therefore, the solution is not to directly call the parent class constructor to assign a value to the child class prototype, but to obtain a copy of the parent class prototype by creating an empty function F.

Parasitic combinatorial inheritance is basically similar to combinatorial inheritance in writing. The differences are as follows:

- Dog.prototype =  new Animal()
- Dog.prototype.constructor = Dog

+ function F() {}
+ F.prototype = Animal.prototype
+ let f = new F()
+ f.constructor = Dog
+ Dog.prototype = f

After slightly encapsulating the code added above:

function object(o) {
    function F() {}
    F.prototype = o
    return new F()
}
function inheritPrototype(child, parent) {
    let prototype = object(parent.prototype)
    prototype.constructor = child
    child.prototype = prototype
}
inheritPrototype(Dog, Animal)

If you dislike the above code, you can also change the code based on composite inheritance to the simplest parasitic composite inheritance:

- Dog.prototype =  new Animal()
- Dog.prototype.constructor = Dog

+ Dog.prototype =  Object.create(Animal.prototype)
+ Dog.prototype.constructor = Dog

class implementation inheritance

class Animal {
    constructor(name) {
        this.name = name
    } 
    getName() {
        return this.name
    }
}
class Dog extends Animal {
    constructor(name, age) {
        super(name)
        this.age = age
    }
}

Array de duplication

ES5 implementation:

function unique(arr) {
    var res = arr.filter(function(item, index, array) {
        return array.indexOf(item) === index
    })
    return res
}

ES6 implementation:

var unique = arr => [...new Set(arr)]

Flatten arrays

Array flattening is to flatten [1, [2, [3]]] this multi-layer array into one layer [1, 2, 3]. Using array prototype. Flat can directly flatten multi-layer arrays into one layer:

[1, [2, [3]]].flat(2)  // [1, 2, 3]

Now is to achieve the effect of flat.

ES5 implementation: recursion.

function flatten(arr) {
    var result = [];
    for (var i = 0, len = arr.length; i < len; i++) {
        if (Array.isArray(arr[i])) {
            result = result.concat(flatten(arr[i]))
        } else {
            result.push(arr[i])
        }
    }
    return result;
}

ES6 implementation:

function flatten(arr) {
    while (arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }
    return arr;
}

Deep and shallow copy

Shallow copy: only object types are considered.

function shallowCopy(obj) {
    if (typeof obj !== 'object') return
    
    let newObj = obj instanceof Array ? [] : {}
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = obj[key]
        }
    }
    return newObj
}

Simple version deep copy: only ordinary object attributes are considered, and built-in objects and functions are not considered.

function deepClone(obj) {
    if (typeof obj !== 'object') return;
    var newObj = obj instanceof Array ? [] : {};
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
        }
    }
    return newObj;
}

Deep cloning of complex version: Based on the simple version, it also considers built-in objects and functions such as Date and RegExp, and solves the problem of circular reference.

const isObject = (target) => (typeof target === "object" || typeof target === "function") && target !== null;

function deepClone(target, map = new WeakMap()) {
    if (map.get(target)) {
        return target;
    }
    // Constructor to get the current value: get its type
    let constructor = target.constructor;
    // Check whether the current object target matches the regular and date format objects
    if (/^(RegExp|Date)$/i.test(constructor.name)) {
        // Create a new instance of a special object (regular class / date class)
        return new constructor(target);  
    }
    if (isObject(target)) {
        map.set(target, true);  // Mark objects referenced circularly
        const cloneTarget = Array.isArray(target) ? [] : {};
        for (let prop in target) {
            if (target.hasOwnProperty(prop)) {
                cloneTarget[prop] = deepClone(target[prop], map);
            }
        }
        return cloneTarget;
    } else {
        return target;
    }
}

Event bus (publish subscribe mode)

class EventEmitter {    constructor() {        this.cache = {}    }    on(name, fn) {        if (this.cache[name]) {            this.cache[name].push(fn)        } else {            this.cache[name] = [fn]        }    }    off(name, fn) {        let tasks = this.cache[name]        if (tasks) {            const index = tasks.findIndex(f => f === fn || f.callback === fn)            if (index >= 0) {                tasks.splice(index, 1)            }        }    }    emit(name, once = false, ...args) {        if (this.cache[name]) {            // Create a copy. If the same event continues to be registered in the callback function, it will cause an endless loop let tasks = this cache[name]. Slice() for (let FN of tasks) {FN (... Args)} if (once) {delete this. Cache [name]}}}} / / test let eventBus = new EventEmitter()let fn1 = function(name, age){ 	 console.log(`${name} ${age}`)}let fn2 = function(name, age) { 	 console.log(`hello, ${name} ${age}`)}eventBus.on('aaa', fn1)eventBus.on('aaa', fn2)eventBus.emit('aaa', false,' bran ', 12) / /' bran 12 '/ /' Hello, bran 12 'copy code

Resolve URL parameters as objects

function parseParam(url) {
    const paramsStr = /.+\?(.+)$/.exec(url)[1]; // Will? Take out the following string
    const paramsArr = paramsStr.split('&'); // Split the string with & and save it in the array
    let paramsObj = {};
    // Save params to object
    paramsArr.forEach(param => {
        if (/=/.test(param)) { // Handle parameters with value
            let [key, val] = param.split('='); // Split key and value
            val = decodeURIComponent(val); // decode
            val = /^\d+$/.test(val) ? parseFloat(val) : val; // Determine whether to convert to number
    
            if (paramsObj.hasOwnProperty(key)) { // If the object has a key, add a value
                paramsObj[key] = [].concat(paramsObj[key], val);
            } else { // If the object does not have this key, create the key and set the value
                paramsObj[key] = val;
            }
        } else { // Processing parameters without value
            paramsObj[param] = true;
        }
    })
    
    return paramsObj;
}

String template

function render(template, data) {
    const reg = /\{\{(\w+)\}\}/; // Template string regular
    if (reg.test(template)) { // Determine whether there is a template string in the template
        const name = reg.exec(template)[1]; // Find the field of the first template string in the current template
        template = template.replace(reg, data[name]); // Render the first template string
        return render(template, data); // Render recursively and return the rendered structure
    }
    return template; // If the template has no template string, it is returned directly
}

Test:

let template = 'I am{{name}},Age{{age}},Gender{{sex}}';let person = {    name: 'Bran',    age: 12}render(template, person); // I'm bran, age 12, gender undefined copy code

Lazy loading of pictures

Different from ordinary image lazy loading, the following two more elaborate processes are made:

  • Remove event listening after all pictures are loaded;
  • The loaded image is removed from imgList;
let imgList = [...document.querySelectorAll('img')]
let length = imgList.length

// Fix the error and add self execution
- const imgLazyLoad = function() {
+ const imgLazyLoad = (function() {
    let count = 0
    
   return function() {
        let deleteIndexList = []
        imgList.forEach((img, index) => {
            let rect = img.getBoundingClientRect()
            if (rect.top < window.innerHeight) {
                img.src = img.dataset.src
                deleteIndexList.push(index)
                count++
                if (count === length) {
                    document.removeEventListener('scroll', imgLazyLoad)
                }
            }
        })
        imgList = imgList.filter((img, index) => !deleteIndexList.includes(index))
   }
- }
+ })()

// It's best to add anti shake treatment here
document.addEventListener('scroll', imgLazyLoad)

reference resources: Lazy loading of pictures

Function anti shake

The high-frequency event will only be executed once after N seconds. If the event is triggered again within N seconds, it will be timed again.

Simple version: this and event objects are supported inside the function;

function debounce(func, wait) {
    var timeout;
    return function () {
        var context = this;
        var args = arguments;
        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context, args)
        }, wait);
    }
}

use:

var node = document.getElementById('layout')
function getUserAction(e) {
    console.log(this, e)  // Print the node and MouseEvent respectively
    node.innerHTML = count++;
};
node.onmousemove = debounce(getUserAction, 1000)

Final version: in addition to supporting this and event, it also supports the following functions:

  • Support immediate implementation;
  • Function may have a return value;
  • Support cancellation function;
function debounce(func, wait, immediate) {
    var timeout, result;
    
    var debounced = function () {
        var context = this;
        var args = arguments;
        
        if (timeout) clearTimeout(timeout);
        if (immediate) {
            // If it has been executed, it will not be executed again
            var callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) result = func.apply(context, args)
        } else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
        return result;
    };

    debounced.cancel = function() {
        clearTimeout(timeout);
        timeout = null;
    };

    return debounced;
}

use:

var setUseAction = debounce(getUserAction, 10000, true);
// Use anti shake
node.onmousemove = setUseAction

// Cancel anti shake
setUseAction.cancel()

reference resources: JavaScript topic: learning anti shake with underscore

Function throttling

High frequency events are triggered and executed only once in N seconds.

Simple version: it is implemented using timestamp, which is executed immediately, and then every N seconds.

function throttle(func, wait) {
    var context, args;
    var previous = 0;

    return function() {
        var now = +new Date();
        context = this;
        args = arguments;
        if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
        }
    }
}

Final version: supports canceling throttling; In addition, pass in the third parameter, options Leading to indicate whether it can be executed immediately, opitons Trailing indicates whether to execute again when the call ends. It is true by default. Note that you cannot set leading or trailing to false at the same time.

function throttle(func, wait, options) {
    var timeout, context, args, result;
    var previous = 0;
    if (!options) options = {};

    var later = function() {
        previous = options.leading === false ? 0 : new Date().getTime();
        timeout = null;
        func.apply(context, args);
        if (!timeout) context = args = null;
    };

    var throttled = function() {
        var now = new Date().getTime();
        if (!previous && options.leading === false) previous = now;
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            func.apply(context, args);
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(later, remaining);
        }
    };
    
    throttled.cancel = function() {
        clearTimeout(timeout);
        previous = 0;
        timeout = null;
    }
    return throttled;
}
Copy code

The use of throttling does not take the code as an example, just refer to the anti shake.

reference resources: JavaScript topic: learning from underscore

Function coritization

What is function coriolism? In fact, it is a technology to convert a function using multiple parameters into a series of functions using one parameter. Don't you understand? Let's take an example.

function add(a, b, c) {
    return a + b + c
}
add(1, 2, 3)
let addCurry = curry(add)
addCurry(1)(2)(3)

Now we need to implement curry function, which changes the function from passing in multiple parameters in one call to passing one parameter in multiple calls.

function curry(fn) {
    let judge = (...args) => {
        if (args.length == fn.length) return fn(...args)
        return (...arg) => judge(...args, ...arg)
    }
    return judge
}

Partial function

What is a partial function? Partial function is to convert a function with n parameters into a function with fixed x parameters, and the remaining parameters (n - x) will be passed in the next call. For example:

function add(a, b, c) {
    return a + b + c
}
let partialAdd = partial(add, 1)
partialAdd(2, 3)

No, actually, the partial function is a little similar to the function coritization, so according to the implementation of the function coritization, the implementation of the partial function can be written quickly:

function partial(fn, ...args) {
    return (...arg) => {
        return fn(...args, ...arg)
    }
}

The above function is relatively simple. Now we hope that the partial function can realize the space occupying function like Coriolis, for example:

function clg(a, b, c) {
    console.log(a, b, c)
}
let partialClg = partial(clg, '_', 2)
partialClg(1, 3)  // Print in sequence: 1, 2, 3

_ The occupied position is actually the position of 1. Equivalent to: partial(clg, 1, 2), and then partial CLG (3). After understanding the principle, let's write the implementation:

function partial(fn, ...args) {
    return (...arg) => {
        args[index] = 
        return fn(...args, ...arg)
    }
}

JSONP

The core principle of JSONP: the script tag is not constrained by the homology policy, so it can be used for cross domain requests. The advantage is good compatibility, but it can only be used for GET requests;

const jsonp = ({ url, params, callbackName }) => {
    const generateUrl = () => {
        let dataSrc = ''
        for (let key in params) {
            if (params.hasOwnProperty(key)) {
                dataSrc += `${key}=${params[key]}&`
            }
        }
        dataSrc += `callback=${callbackName}`
        return `${url}?${dataSrc}`
    }
    return new Promise((resolve, reject) => {
        const scriptEle = document.createElement('script')
        scriptEle.src = generateUrl()
        document.body.appendChild(scriptEle)
        window[callbackName] = data => {
            resolve(data)
            document.removeChild(scriptEle)
        }
    })
}

AJAX

const getJSON = function(url) {
    return new Promise((resolve, reject) => {
        const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp');
        xhr.open('GET', url, false);
        xhr.setRequestHeader('Accept', 'application/json');
        xhr.onreadystatechange = function() {
            if (xhr.readyState !== 4) return;
            if (xhr.status === 200 || xhr.status === 304) {
                resolve(xhr.responseText);
            } else {
                reject(new Error(xhr.responseText));
            }
        }
        xhr.send();
    })
}

Method for implementing array prototype

forEach

Array.prototype.forEach2 = function(callback, thisArg) {
    if (this == null) {
        throw new TypeError('this is null or not defined')
    }
    if (typeof callback !== "function") {
        throw new TypeError(callback + ' is not a function')
    }
    const O = Object(this)  // this is the current array
    const len = O.length >>> 0  // There is an explanation later
    let k = 0
    while (k < len) {
        if (k in O) {
            callback.call(thisArg, O[k], k, O);
        }
        k++;
    }
}

reference resources: forEach#polyfill

O. What is length > > > 0? It's an unsigned right shift of 0 bits. What's the point? This is to ensure that the converted value is a positive integer. In fact, the bottom layer has two layers of conversion. The first is to convert non number into number type, and the second is to convert number into Uint32 type. Interested can read What does something > > > 0 mean?.

map

The implementation based on forEach can easily write out the implementation of map:

- Array.prototype.forEach2 = function(callback, thisArg) {
+ Array.prototype.map2 = function(callback, thisArg) {
    if (this == null) {
        throw new TypeError('this is null or not defined')
    }
    if (typeof callback !== "function") {
        throw new TypeError(callback + ' is not a function')
    }
    const O = Object(this)
    const len = O.length >>> 0
-   let k = 0
+   let k = 0, res = []
    while (k < len) {
        if (k in O) {
-           callback.call(thisArg, O[k], k, O);
+           res[k] = callback.call(thisArg, O[k], k, O);
        }
        k++;
    }
+   return res
}

filter

Similarly, the implementation based on forEach can easily write the implementation of filter:

- Array.prototype.forEach2 = function(callback, thisArg) {
+ Array.prototype.filter2 = function(callback, thisArg) {
    if (this == null) {
        throw new TypeError('this is null or not defined')
    }
    if (typeof callback !== "function") {
        throw new TypeError(callback + ' is not a function')
    }
    const O = Object(this)
    const len = O.length >>> 0
-   let k = 0
+   let k = 0, res = []
    while (k < len) {
        if (k in O) {
-           callback.call(thisArg, O[k], k, O);
+           if (callback.call(thisArg, O[k], k, O)) {
+               res.push(O[k])                
+           }
        }
        k++;
    }
+   return res
}
Copy code

some

Similarly, the implementation based on forEach can easily write the implementation of some:

- Array.prototype.forEach2 = function(callback, thisArg) {
+ Array.prototype.some2 = function(callback, thisArg) {
    if (this == null) {
        throw new TypeError('this is null or not defined')
    }
    if (typeof callback !== "function") {
        throw new TypeError(callback + ' is not a function')
    }
    const O = Object(this)
    const len = O.length >>> 0
    let k = 0
    while (k < len) {
        if (k in O) {
-           callback.call(thisArg, O[k], k, O);
+           if (callback.call(thisArg, O[k], k, O)) {
+               return true
+           }
        }
        k++;
    }
+   return false
}
Copy code

reduce

Array.prototype.reduce2 = function(callback, initialValue) {
    if (this == null) {
        throw new TypeError('this is null or not defined')
    }
    if (typeof callback !== "function") {
        throw new TypeError(callback + ' is not a function')
    }
    const O = Object(this)
    const len = O.length >>> 0
    let k = 0, acc
    
    if (arguments.length > 1) {
        acc = initialValue
    } else {
        // When no initial value is passed in, take the first non empty value in the array as the initial value
        while (k < len && !(k in O)) {
            k++
        }
        if (k > len) {
            throw new TypeError( 'Reduce of empty array with no initial value' );
        }
        acc = O[k++]
    }
    while (k < len) {
        if (k in O) {
            acc = callback(acc, O[k], k, O)
        }
        k++
    }
    return acc
}
Copy code

Method of realizing function prototype

call

Calls a function with a specified value of this and one or more parameters.

Key points:

  • this may pass in null;
  • Pass in a variable number of parameters;
  • Function may have a return value;
Function.prototype.call2 = function (context) {
    var context = context || window;
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }

    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}

apply

Apply is the same as call. The only difference is that call passes in a variable number of parameters, while apply passes in an array.

Key points:

  • this may pass in null;
  • Pass in an array;
  • Function may have a return value;
Function.prototype.apply2 = function (context, arr) {
    var context = context || window;
    context.fn = this;

    var result;
    if (!arr) {
        result = context.fn();
    } else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')')
    }

    delete context.fn
    return result;
}

bind

The bind method will create a new function. When bind() is called, this of the new function is specified as the first parameter of bind(), and the other parameters will be used as the parameters of the new function for calling.

Key points:

  • bind() can pass in multiple parameters besides this;
  • The new function created by bing may pass in multiple parameters;
  • The new function may be called as a constructor;
  • Function may have a return value;
Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

Implement the new keyword

The new operator is used to create instances of user-defined object types or built-in objects with constructors.

Key points:

  • New will generate a new object;
  • The new object needs to be able to access the properties of the constructor, so its prototype needs to be re specified;
  • The constructor may display the return;
function objectFactory() {
    var obj = new Object()
    Constructor = [].shift.call(arguments);
    obj.__proto__ = Constructor.prototype;
    var ret = Constructor.apply(obj, arguments);
    
    // RET | obj is written in this way, considering that the constructor returns null
    return typeof ret === 'object' ? ret || obj : obj;
};

use:

function person(name, age) {
    this.name = name
    this.age = age
}
let p = objectFactory(person, 'Bran', 12)
console.log(p)  // {name: 'bran', age: 12}

Implement the instanceof keyword

instanceof is to judge whether the prototype attribute of the constructor appears on the prototype chain of the instance.

function instanceOf(left, right) {
    let proto = left.__proto__
    while (true) {
        if (proto === null) return false
        if (proto === right.prototype) {
            return true
        }
        proto = proto.__proto__
    }
}

Left above Proto can be changed to object getPrototypeOf(left).

Implement object create

Object. The create () method creates a new object and uses the existing object to provide the name of the newly created object__ proto__.

Object.create2 = function(proto, propertyObject = undefined) {
    if (typeof proto !== 'object' && typeof proto !== 'function') {
        throw new TypeError('Object prototype may only be an Object or null.')
    if (propertyObject == null) {
        new TypeError('Cannot convert undefined or null to object')
    }
    function F() {}
    F.prototype = proto
    const obj = new F()
    if (propertyObject != undefined) {
        Object.defineProperties(obj, propertyObject)
    }
    if (proto === null) {
        // Create an object without a prototype object, object create(null)
        obj.__proto__ = null
    }
    return obj
}

Implement object assign

Object.assign2 = function(target, ...source) {
    if (target == null) {
        throw new TypeError('Cannot convert undefined or null to object')
    }
    let ret = Object(target) 
    source.forEach(function(obj) {
        if (obj != null) {
            for (let key in obj) {
                if (obj.hasOwnProperty(key)) {
                    ret[key] = obj[key]
                }
            }
        }
    })
    return ret
}
Copy code

Implement JSON stringify

JSON. The stringify ([, replace [, space]) method converts a JavaScript value (object or array) into a JSON string. The simulation implementation here does not consider the optional second parameter replace and the third parameter space. If you do not understand the functions of these two parameters, you are recommended to read them MDN file.

  1. Basic data type:
    • Undefined is still undefined after conversion (type is also undefined)
    • boolean value conversion is followed by the string "false" / "true"
    • The number type (except NaN and Infinity) is converted to a string type value
    • symbol conversion is followed by undefined
    • Null conversion is followed by the string "null"
    • String is still string after string conversion
    • The NaN and Infinity transforms are followed by the string "null"
  2. Function type: undefined after conversion
  3. If object type (non function)
    • If it is an array: if undefined, any function and symbol appear in the attribute value, it is converted to the string "null";
    • If it is a RegExp object: return {} (the type is string);
    • If it is a Date object, return the toJSON string value of Date;
    • If it is an ordinary object;
      • If there is a toJSON() method, serialize the return value of toJSON().
      • If undefined, any function and symbol value appear in the attribute value, ignore it.
      • All attributes with symbol as the attribute key will be completely ignored.
  4. Executing this method on objects that contain circular references (objects refer to each other to form an infinite loop) will throw an error.
function jsonStringify(data) {
    let dataType = typeof data;
    
    if (dataType !== 'object') {
        let result = data;
        //data may be string/number/null/undefined/boolean
        if (Number.isNaN(data) || data === Infinity) {
            //NaN and Infinity serialization returned "null"
            result = "null";
        } else if (dataType === 'function' || dataType === 'undefined' || dataType === 'symbol') {
            //function, undefined, symbol serialization returns undefined
            return undefined;
        } else if (dataType === 'string') {
            result = '"' + data + '"';
        }
        //boolean returns String()
        return String(result);
    } else if (dataType === 'object') {
        if (data === null) {
            return "null"
        } else if (data.toJSON && typeof data.toJSON === 'function') {
            return jsonStringify(data.toJSON());
        } else if (data instanceof Array) {
            let result = [];
            //If it's an array
            //The toJSON method can exist in the prototype chain
            data.forEach((item, index) => {
                if (typeof item === 'undefined' || typeof item === 'function' || typeof item === 'symbol') {
                    result[index] = "null";
                } else {
                    result[index] = jsonStringify(item);
                }
            });
            result = "[" + result + "]";
            return result.replace(/'/g, '"');
            
        } else {
            //Common object
            /**
             * Circular reference throwing error (not detected yet, stack overflow during circular reference)
             * symbol key ignore
             * undefined,Function and symbol are attribute values and are ignored
             */
            let result = [];
            Object.keys(data).forEach((item, index) => {
                if (typeof item !== 'symbol') {
                    //If key is a symbol object, ignore it
                    if (data[item] !== undefined && typeof data[item] !== 'function'
                        && typeof data[item] !== 'symbol') {
                        //If the key value is undefined, the function and symbol are attribute values, it is ignored
                        result.push('"' + item + '"' + ":" + jsonStringify(data[item]));
                    }
                }
            });
            return ("{" + result + "}").replace(/'/g, '"');
        }
    }
}
Copy code

reference resources: Implement JSON stringify

Implement JSON parse

Two methods are introduced:

  • eval implementation;
  • new Function implementation;

eval implementation

The first method is the simplest and most intuitive, which is to call eval directly. The code is as follows:

var json = '{"a":"1", "b":2}';
var obj = eval("(" + json + ")");  // obj is the object obtained after json deserialization

However, directly calling Eval will have security problems. If the data may not be json data, but executable JavaScript code, it is likely to cause XSS attack. Therefore, the data needs to be verified before calling eval.

var rx_one = /^[\],:{}\s]*$/;
var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
var rx_four = /(?:^|:|,)(?:\s*\[)+/g;

if (
    rx_one.test(
        json.replace(rx_two, "@")
            .replace(rx_three, "]")
            .replace(rx_four, "")
    )
) {
    var obj = eval("(" +json + ")");
}

reference resources: JSON. Three implementations of parse

new Function implementation

Function has the same string parameter properties as eval.

var json = '{"name":"cute girl", "age":20}';
var obj = (new Function('return ' + json))();

Implement Promise

To implement Promise, you need to fully understand it Promise A + specification However, in terms of overall implementation, the following points need to be considered:

  • then needs to support chained calls, so it has to return a new Promise;
  • To deal with asynchronous problems, you have to use onResolvedCallbacks and onRejectedCallbacks to save successful and failed callbacks respectively;
  • In order for the chain call to proceed normally, you need to judge the types of onFulfilled and onRejected;
  • onFulfilled and onRejected need to be called asynchronously. Here, setTimeout is used to simulate asynchrony;
  • resolve the Promise;
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class Promise {
    constructor(executor) {
        this.status = PENDING;
        this.value = undefined;
        this.reason = undefined;
        this.onResolvedCallbacks = [];
        this.onRejectedCallbacks = [];
        
        let resolve = (value) = > {
            if (this.status === PENDING) {
                this.status = FULFILLED;
                this.value = value;
                this.onResolvedCallbacks.forEach((fn) = > fn());
            }
        };
        
        let reject = (reason) = > {
            if (this.status === PENDING) {
                this.status = REJECTED;
                this.reason = reason;
                this.onRejectedCallbacks.forEach((fn) = > fn());
            }
        };
        
        try {
            executor(resolve, reject);
        } catch (error) {
            reject(error);
        }
    }
    
    then(onFulfilled, onRejected) {
        // Solve the problem that onfuelled and onRejected do not transfer values
        onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v) = > v;
        // Because the wrong value needs to be accessed later, an error should also be thrown here, otherwise it will be caught in the subsequent then resolve
        onRejected = typeof onRejected === "function" ? onRejected : (err) = > {
            throw err;
        };
        // Each call to then returns a new promise
        let promise2 = new Promise((resolve, reject) = > {
            if (this.status === FULFILLED) {
                //Promise/A+ 2.2.4 --- setTimeout
                setTimeout(() = > {
                    try {
                        let x = onFulfilled(this.value);
                        // x may be a proimise
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                }, 0);
            }
        
            if (this.status === REJECTED) {
                //Promise/A+ 2.2.3
                setTimeout(() = > {
                    try {
                        let x = onRejected(this.reason);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                }, 0);
            }
            
            if (this.status === PENDING) {
                this.onResolvedCallbacks.push(() = > {
                    setTimeout(() = > {
                        try {
                            let x = onFulfilled(this.value);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    }, 0);
                });
            
                this.onRejectedCallbacks.push(() = > {
                    setTimeout(() = > {
                        try {
                            let x = onRejected(this.reason);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    }, 0);
                });
            }
        });
        
        return promise2;
    }
}
const resolvePromise = (promise2, x, resolve, reject) = > {
    // It is a wrong implementation to wait for your own completion. End promise promise / A + 2.3 with a type error one
    if (promise2 === x) {
        return reject(
            new TypeError("Chaining cycle detected for promise #<Promise>"));
    }
    // Promise/A+ 2.3.3.3.3 can only be called once
    let called;
    // The subsequent conditions should be strictly judged to ensure that the code can be used with other libraries
    if ((typeof x === "object" && x != null) || typeof x === "function") {
        try {
            // In order to judge the resolved, there is no need to reject (for example, when reject and resolve are called at the same time) Promise/A+ 2.3.3.1
            let then = x.then;
            if (typeof then === "function") {
            // Don't write x.then, just then Just call, because x.then will get the value again, object defineProperty  Promise/A+ 2.3. three point three
                then.call(
                    x, (y) = > {
                        // Success or failure is determined according to the promise status
                        if (called) return;
                        called = true;
                        // Recursive parsing (because there may be promise in promise) Promise/A+ 2.3.3.3.1
                        resolvePromise(promise2, y, resolve, reject);
                    }, (r) = > {
                        // Fail as long as fail promise / A + 2.3 3.3. two
                        if (called) return;
                        called = true;
                        reject(r);
                    });
            } else {
                // If x.then is a normal value, it will directly return resolve as the result promise / A + 2.3 three point four
                resolve(x);
            }
        } catch (e) {
            // Promise/A+ 2.3.3.2
            if (called) return;
            called = true;
            reject(e);
        }
    } else {
        // If x is a normal value, it will directly return resolve as the result promise / A + 2.3 four
        resolve(x);
    }
};

After Promise is written, we can test the code we write through the promises aplus tests package to see whether it conforms to the A + specification. But you have to add A piece of code before the test:

// promise.js
// Here is all the Promise code written above
Promise.defer = Promise.deferred = function () {
    let dfd = {}
    dfd.promise = new Promise((resolve,reject)=>{
        dfd.resolve = resolve;
        dfd.reject = reject;
    });
    return dfd;
}
module.exports = Promise;

Global installation:

npm i promises-aplus-tests -g

Execute the verification command under the terminal:

promises-aplus-tests promise.js

The code written above can successfully pass all 872 test cases.

reference resources:

Promise.resolve

Promsie.resolve(value) can convert any value to value. The status is Promise with full Promise, but if the passed in value itself is Promise, it will be returned as it is.

Promise.resolve = function(value) {
    // If Promsie, output it directly
    if(value instanceof Promise){
        return value
    }
    return new Promise(resolve => resolve(value))
}

reference resources: Deep understanding of Promise

Promise.reject

And Promise Resolve() is similar to Promise Reject () instantiates a Promise in the rejected state. But with Promise The difference between resolve () and Promise Reject() passes a Promise object, which will become the value of the new Promise.

Promise.reject = function(reason) {
    return new Promise((resolve, reject) => reject(reason))
}

Promise.all

Promise. The rule of all is as follows:

  • If all incoming promsies are full, a new Promise in the status of full is returned, which is composed of their values;
  • As long as one Promise is rejected, a new Promsie in the rejected state is returned, and its value is the value of the first rejected Promise;
  • As long as one Promise is pending, a new Promise in pending status is returned;
Promise.all = function(promiseArr) {
    let index = 0, result = []
    return new Promise((resolve, reject) => {
        promiseArr.forEach((p, i) => {
            Promise.resolve(p).then(val => {
                index++
                result[i] = val
                if (index === promiseArr.length) {
                    resolve(result)
                }
            }, err => {
                reject(err)
            })
        })
    })
}

Promise.race

Promise.race returns a new instance wrapped by the first fully or rejected instance of all iteratable instances.

Promise.race = function(promiseArr) {
    return new Promise((resolve, reject) => {
        promiseArr.forEach(p => {
            Promise.resolve(p).then(val => {
                resolve(val)
            }, err => {
                rejecte(err)
            })
        })
    })
}

Promise.allSettled

Promise. The rule of allsettled is as follows:

  • If the states of all promises have changed, a new Promise whose state is fully returned, and its value is an array. Each item of the array is an object composed of the values and states of all promises;
  • If there is a Promise in pending status, a new instance in pending status will be returned;
Promise.allSettled = function(promiseArr) {
    let result = []
        
    return new Promise((resolve, reject) => {
        promiseArr.forEach((p, i) => {
            Promise.resolve(p).then(val => {
                result.push({
                    status: 'fulfilled',
                    value: val
                })
                if (result.length === promiseArr.length) {
                    resolve(result) 
                }
            }, err => {
                result.push({
                    status: 'rejected',
                    reason: err
                })
                if (result.length === promiseArr.length) {
                    resolve(result) 
                }
            })
        })  
    })   
}

Promise.any

Promise.any's rules are as follows:

  • If an empty array or all promises are rejected, an error with a new Promsie status of rejected and a value of AggregateError is returned;
  • As long as one is in the status of full, the first new instance is returned;
  • In other cases, a new instance of pending will be returned;
Promise.any = function(promiseArr) {
    let index = 0
    return new Promise((resolve, reject) => {
        if (promiseArr.length === 0) return 
        promiseArr.forEach((p, i) => {
            Promise.resolve(p).then(val => {
                resolve(val)
                
            }, err => {
                index++
                if (index === promiseArr.length) {
                  reject(new AggregateError('All promises were rejected'))
                }
            })
        })
    })
}

Later words

You can see that the code here is true love. After all, the code looks really boring, but if you understand it, you will be as happy as winning a game, and the game will be addictive. When you pass more levels, your ability will be raised to a higher level. In the words of the title: after understanding, the promotion is really great. Come on 💪, Cook

Reprinted from

Keywords: Javascript Front-end

Added by kporter.porter on Fri, 31 Dec 2021 12:46:25 +0200