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
CommonJS
Support for dynamic import, i.e. Require (${path}/xx.js), which is currently not supported, but has been proposedCommonJS
Is 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 renderingCommonJS
If the exported value changes, the imported value will not change, so if you want to update the value, you must import it again. butES Module
In real-time binding mode, the import and export values point to the same memory address, so the import value changes with the export valueES Module
Will be compiled intorequire/exports
To 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 round
Event loop
To 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 to
window
- Changed the
this
So that new objects can execute the function and accept arguments
To realize the call
- First of all,
context
Is an optional parameter. If not passed, the default context iswindow
- Next to the
context
Create an FN property and set the value to the function you want to call - because
call
You 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
bind
Returns a function. There are two ways to call a function, either directly or throughnew
Let’s start with the direct invocation- For direct calls, select
apply
But for the parameters need to pay attention to the following situation: becausebind
You 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, through
new
In the previous chapter we learned how to judgethis
fornew
Will not be changed in any waythis
So 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 through
new
Produced 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 use
new Object()
Objects are created in a way that requires finding each layer of the scope chainObject
But 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 is
null
, 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 the
11
Bits are used to denote exponents - The other bits are used to represent the significant bits, which are represented in binary
0.1
In 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