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.
- 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"
- Function type: undefined after conversion
- 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.
- 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:
- BAT front-end classic interview questions: the most detailed handwritten Promise tutorial in history
- 100 lines of code to realize Promises/A + specification
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