9. Prototype inheritance and Class inheritance

Question: How does a prototype achieve inheritance? How does Class implement inheritance? What is the nature of Class?

There is no such thing as a class in JS. Class is just syntax sugar. It is essentially a function

class Person {}
Person instanceof Function // true
Copy the code

Combination of inheritance

Composite inheritance is the most common type of inheritance

function Parent(value) {
  this.val = value;
}
Parent.prototype.getValue = function () {
  console.log(this.val);
};
function Child(value) {
  Parent.call(this, value);
}
Child.prototype = new Parent();
const child = new Child(1);
child.getValue(); / / 1
child instanceof Parent; // true
Copy the code
  • The core method of inheritance is to inherit the property of the Parent class through the Parent.call(this) constructor of the child class, and then change the prototype of the child class to new Parent() to inherit the function of the Parent class.
  • The advantage of this inheritance method is that the constructor can pass parameters and will not be shared with the reference attribute of the parent class, and the function of the parent class can be reused. However, there is also a disadvantage that the constructor of the parent class is called when inheriting the function of the parent class, resulting in more unnecessary attributes of the parent class on the prototype of the child class, and there is a waste of memory

Parasitic combination inheritance

This inheritance method optimizes combination inheritance. The disadvantage of combination inheritance is that it calls the constructor when inheriting the superclass function. We only need to optimize this

function Parent(value) {
  this.val = value;
}
Parent.prototype.getValue = function () {
  console.log(this.val);
};
function Child(value) {
  Parent.call(this, value);
}
Child.prototype = Object.create(Parent.prototype, {
  constructor: {
    value: Child,
    enumerable: false.writable: true.configurable: true,}});const child = new Child(1);
child.getValue(); / / 1
child instanceof Parent; // true
Copy the code

The core of the above inheritance implementation is to assign the prototype of the parent class to the subclass, and set the constructor to the subclass, which not only solves the problem of useless superclass attributes, but also can correctly find the constructor of the subclass.

The Class inheritance

Both of these methods of inheritance are solved through prototypes. In ES6, we can use classes to implement inheritance, and it is very simple to implement

class Parent {
  constructor(value) {
    this.val = value;
  }
  getValue() {
    console.log(this.val); }}class Child extends Parent {
  constructor(value) {
    super(value);
    this.val = value; }}let child = new Child(1);
child.getValue(); / / 1
child instanceof Parent; // true
Copy the code

The core of class implementation inheritance is that the use of extends indicates which Parent class it inherits from, and that super must be called in the subclass constructor because this code can be thought of as parent-.call (this, value).

10. Modularity

Question: Why use modularity? What are the ways to achieve modularity and what are the characteristics of each?

There must be a reason to use a technology, so using modularity can give us the following benefits

  • Resolving name conflicts
  • Provide reusability
  • Improve code maintainability

Immediate execution function

In the early days, modularizing with immediate-execution functions was a common approach, and function scopes solved the problem of naming conflicts and fouling the global scope

(function(globalVariable){
 globalVariable.test = function() {}
 / /... Declaring variables and functions does not pollute the global scope
})(globalVariable)
Copy the code

AMD and CMD

Since these two implementations are so rare these days, I won’t go into the details of the features, just understand how they are used.

// AMD
define(['./a'.'./b'].function(a, b) {
 // After loading the module, you can use it
 a.do()
 b.do()
})
// CMD
define(function(require.exports.module) {
 // Load the module
 // Require can be written anywhere in the function body to implement lazy loading
 var a = require('./a')
 a.doSomething()
})
Copy the code

CommonJS

CommonJS was first used by Node and is still widely used today, for example in Webpack, but module management in Node is different from CommonJS

// a.js
module.exports = {
  a: 1};// or
exports.a = 1;
// b.js
var module = require("./a.js");
module.a; // -> log 1
var module = require("./a.js");
module.a;
// The function is executed immediately so that the global variable is not polluted.
In this case, module is a variable unique to Node
module.exports = {
  a: 1};// Module basic implementation
var module = {
  id: "xxxx".// Must I know how to find him
  exports: {}, // exports is null
};
Exports are similar to module. Exports
var exports = module.exports;
var load = function (module) {
  // The exported thing
  var a = 1;
  module.exports = a;
  return module.exports;
};
// Then find the unique when I require
// id, and then the thing to use is wrapped in the immediate execute function, over
Copy the code

Exports are similar to module. Exports, but you cannot directly assign a value to exports. Var exports = module.exports var exports = module.exports Exports no longer point to the same memory address. This change does not apply to module.exports

ES Module

ES Module is a modular solution for a native implementation and differs from CommonJS in several ways

  1. CommonJSSupport for dynamic import, i.e. Require (${path}/xx.js), which is currently not supported, but has been proposed
  2. CommonJSIs synchronous import, because for the server, the files are local, synchronous import even if the main thread jammed. The latter is an asynchronous import, because for the browser, you need to download the file, and if you use synchronous import, it will have a big impact on the rendering
  3. CommonJSIf the exported value changes, the imported value will not change, so if you want to update the value, you must import it again. butES ModuleIn real-time binding mode, the import and export values point to the same memory address, so the import value changes with the export value
  4. ES ModuleWill be compiled intorequire/exportsTo perform the
// Import the module API
import XXX from './a.js'
import { XXX } from './a.js'
// Export the module API
export function a() {}
export default function() {}
Copy the code

Implement a concise version of the promise

// Three constants are used to represent the state
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
function MyPromise(fn) {
  const that = this;
  this.state = PENDING;
  // The value variable is used to hold the value passed in resolve or reject
  this.value = null;
  // This is used to save the callback in then, because the state may still be waiting when the Promise is finished
  that.resolvedCallbacks = [];
  that.rejectedCallbacks = [];
  function resolve(value) {
    // First, both functions must determine whether the current state is waiting
    if (that.state === PENDING) {
      that.state = RESOLVED;
      that.value = value;
      // Iterate over the callback array and execute
      that.resolvedCallbacks.map((cb) = >cb(that.value)); }}function reject(value) {
    if (that.state === PENDING) {
      that.state = REJECTED;
      that.value = value;
      that.rejectedCallbacks.map((cb) = >cb(that.value)); }}// After these two functions, we should implement how to execute the function passed in the Promise
  try {
    fn(resolve, reject);
  } catch(e) { reject(e); }}// Finally, let's implement the more complicated then function
MyPromise.prototype.then = function (onFulfilled, onRejected) {
  const that = this;
  // Check whether the two arguments are of type because they are optional
  onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v) = > v;
  onRejected =
    typeof onRejected === "function"
      ? onRejected
      : (e) = > {
          throw e;
        };
  // When the state is not a wait state, the corresponding function is executed. If the state is waiting, push the function back
  if (this.state === PENDING) {
    this.resolvedCallbacks.push(onFulfilled);
    this.rejectedCallbacks.push(onRejected);
  }
  if (this.state === RESOLVED) {
    onFulfilled(that.value);
  }
  if (this.state === REJECTED) { onRejected(that.value); }};Copy the code

12, the Event Loop

12.1 Processes and Threads

The difference between process and thread? Benefits of JS single threading?

  • JS is executed single-threaded, but have you ever wondered what a thread is?
  • Speaking of threads, we must also talk about processes. In essence, both terms are descriptions of CPU time slices.
  • A process describes how long it takes the CPU to run instructions and load and save the context. In the case of an application, a process represents a program. Threads are smaller units in a process that describe the time it takes to execute a single instruction

When you open a Tab page, you create a process. A process can have multiple threads, such as a rendering thread, a JS engine thread, an HTTP request thread, and so on. When you make a request, you create a thread that may be destroyed when the request ends

  • The JS engine thread and the render thread are mutually exclusive. As you know, the JS engine thread and the render thread may block UI rendering while javascript is running. The reason for this is that JS can modify the DOM, and if the UI thread is still working while JS is executing, it may not render the UI safely. This is also a single thread benefits, because JS is run in a single thread, can achieve memory savings, save context switch time, no lock problems benefits

12.2 execution stack

Question: What is the execution stack?

The execution stack can be thought of as a stack structure for storing function calls, following the first in, then out principle

When we start executing our JS code, we first execute a main function and then execute our code. According to the principle of “in, out”, the function that is executed later will pop up the stack first. In the diagram, we can also see that the function foo is executed later, and when it is finished, it will pop up from the stack

During development, you can also find traces of the execution stack in the error report

function foo() {
  throw new Error("error");
}
function bar() {
  foo();
}
bar();
Copy the code

As you can see in the figure above, the error is reported in the function foo, which in turn is called in the function bar

When we use recursion, because the stack can store a limited number of functions, once stored too many functions are not released, there will be a stack burst problem

function bar() {
 bar()
}
bar()
Copy the code

12.3 Event Loop in the Browser

Asynchronous code execution sequence? What is an Event Loop?

It is well known that JS is a non-blocking single-threaded language, since it was originally created to interact with browsers. If JS is a multithreaded language, we might have problems dealing with DOM in multiple threads (adding nodes in one thread and removing nodes in another).

  • JS will generate execution environments during execution, which will be added to the execution stack sequentially. If asynchronous code is encountered, it is suspended and added to the Task queue (there are several tasks). Once the execution stack is empty, the Event Loop will take out the code to be executed from the Task queue and put it into the execution stack for execution, so essentially the asynchronous or synchronous behavior in JS
console.log("script start");
setTimeout(function () {
  console.log("setTimeout");
}, 0);
console.log("script end");
Copy the code

Different Task sources are allocated to different Task queues. Task sources can be divided into microTask and macroTask. In the ES6 specification, microTasks are called Jobs and macroTasks are called Tasks

console.log('script start');
setTimeout(function() {
 console.log('setTimeout');
}, 0);
new Promise((resolve) = > {
 console.log('Promise')
 resolve()
}).then(function() {
 console.log('promise1');
}).then(function() {
 console.log('promise2');
});
console.log('script end');
// script start => Promise => script end => promise1 => promise2 => setTime
Copy the code

In the above code, setTimeout is written before the Promise, but because the Promise is a microtask and setTimeout is a macro task

Micro tasks

  • process.nextTick
  • promise
  • Object.observe
  • MutationObserver

Macro task

  • script
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI rendering

If a macro task includes a script, the browser will execute a macro task first and then execute the microtask if there is asynchronous code

So the correct sequence of an Event loop would look like this

  • Execute the synchronization code, which is a macro task
  • If the execution stack is empty, query whether microtasks need to be executed
  • Perform all microtasks
  • Render the UI if necessary
  • Then start the next roundEvent loopTo execute the asynchronous code in the macro task

According to the above Event loop order, if the asynchronous code in the macro task has a large number of calculations and needs to manipulate the DOM, we can put the manipulation DOM into the microtask for faster interface response

Write the call, apply, and bind functions

First of all, consider how to implement these functions

  • If the first argument is not passed, the context defaults towindow
  • Changed thethisSo that new objects can execute the function and accept arguments

To realize the call

  • First of all,contextIs an optional parameter. If not passed, the default context iswindow
  • Next to thecontextCreate an FN property and set the value to the function you want to call
  • becausecallYou can pass multiple arguments as arguments to the calling function, so you need to strip them out
  • The function is then called and the function is removed from the object
Function.prototype.myCall = function(context) {
 if (typeof this! = ='function') {
 throw new TypeError('Error')
 }
 context = context || window
 context.fn = this
 const args = [...arguments].slice(1)
 constresult = context.fn(... args)delete context.fn
 return result
}
Copy the code

Apply to realize

The implementation of Apply is similar, except for the handling of parameters

Function.prototype.myApply = function (context) {
  if (typeof this! = ="function") {
    throw new TypeError("Error");
  }
  context = context || window;
  context.fn = this;
  let result;
  // Processing parameters is different from call
  if (arguments[1]) { result = context.fn(... arguments[1]);
  } else {
    result = context.fn();
  }
  delete context.fn;
  return result;
};
Copy the code

The realization of the bind

The implementation of Bind is slightly more complicated than the other two functions because bind needs to return a function and has to determine some boundary issues. Here is the implementation of bind

  • bindReturns a function. There are two ways to call a function, either directly or throughnewLet’s start with the direct invocation
  • For direct calls, selectapplyBut for the parameters need to pay attention to the following situation: becausebindYou can implement code like thisf.bind(obj, 1)(2), so we need to concatenate the parameters on both sides, and we have this implementationargs.concat(... arguments)
  • And finally, throughnewIn the previous chapter we learned how to judgethisfornewWill not be changed in any waythisSo in this case we need to ignore the incomingthis
Function.prototype.myBind = function (context) {
  if (typeof this! = ="function") {
    throw new TypeError("Error");
  }
  const _this = this;
  const args = [...arguments].slice(1);
  // Return a function
  return function F() {
    // We return a function, so we can use new F()
    if (this instanceof F) {
      return new_this(... args, ... arguments); }return_this.apply(context, args.concat(... arguments)); }; };Copy the code

14, the new

What is the principle of new? What’s the difference between creating an object as a new object and creating it as a literal?

Four things happen during the call to new

  • Freshmen became an object
  • Linking to prototypes
  • Binding this
  • Return new object

Based on the above process, we can also try to implement a new ourselves

  • Create an empty object
  • Getting the constructor
  • Sets the prototype for the empty object
  • Bind this and execute the constructor
  • Make sure the return value is an object
function create() {
  let obj = {};
  let Con = [].shift.call(arguments);
  obj.__proto__ = Con.prototype;
  let result = Con.apply(obj, arguments);
  return result instanceof Object ? result : obj;
}
Copy the code
  • For objects, it’s all throughnewProduced by eitherfunction Foo()orlet a = {b : 1 }.
  • For creating an object, it is more recommended to create the object in a literal way (both for performance and readability). Because you usenew Object()Objects are created in a way that requires finding each layer of the scope chainObjectBut the way you use literals doesn’t have that problem
function Foo() {}
// Function is a syntactic sugar
// Internal equivalent to new Function()
let a = { b: 1 }
// This literal uses new Object() internally
Copy the code

15, instanceof principle

How does instanceof work?

Instanceof can correctly determine the type of an object, because the internal mechanism is to determine whether the prototype of an object can be found in the prototype chain

Implement instanceof

  • First get the prototype of the type
  • Then get the prototype of the object
  • Then we keep iterating to see if the stereotype of the object equals the stereotype of the type until the stereotype of the object isnull, because the prototype chain ends up asnull
function myInstanceof(left, right) {
  let prototype = right.prototype;
  left = left.__proto__;
  while (true) {
    if (left === null || left === undefined) return false;
    if (prototype === left) return true; left = left.__proto__; }}Copy the code

Why 0.1 + 0.2! = 0.3

Involved area test questions: why 0.1 + 0.2! = 0.3? How to solve this problem?

Because JS uses IEEE 754 double precision version (64-bit), and any language that uses IEEE 754 has this problem

We all know that computers store things in binary, so 0.1 is represented in binary as

// (0011) indicates loop
0.1 = 2^ -4 * 1.10011(0011)
Copy the code

We can see that 0.1 is a number that loops indefinitely in binary, not just 0.1, but many decimal numbers loop indefinitely in binary. That’s fine, but the JS floating-point standard cuts our numbers.

The IEEE 754 double precision version (64-bit) divides 64-bit into three segments

  • The first digit is used to indicate the sign
  • It follows that the11Bits are used to denote exponents
  • The other bits are used to represent the significant bits, which are represented in binary0.1In the10011 (0011).

The number of these loops is clipped, and the accuracy is lost, so that 0.1 is no longer 0.1, but instead becomes 0.100000000000000002

0.100000000000000002= = =0.1 // true
Copy the code

Similarly, 0.2 is infinite in binary and loses its precision after being clipped to 0.200000000000000002

0.200000000000000002= = =0.2 // true
Copy the code

So the two add up not to 0.3 but to 0.300000000000000004

0.1 + 0.2= = =0.30000000000000004 // true
Copy the code

If 0.1 is not 0.1, then why is console.log(0.1) correct?

Since the binary is converted to decimal, which is converted to string, and an approximation occurs during the conversion, the printed value is actually an approximation, which you can verify by using the following code

console.log(0.100000000000000002) / / 0.1
Copy the code