Much suffering, knowledge will gradually increase. – Homer
This article is an old one I wrote a year ago, and I republish it because I find many mistakes or unclear points in the previous article when I look back. After a year, many things I didn’t understand before suddenly became clear. Maybe this is the growth in practice.
Xiao gang teacher
- Basic types and object types
- Type judgment
- Implicit type conversion
- Scope and execution context
- This points to the
- closure
- Prototype and prototype chain
- Js inheritance
- event loop
Basic types and object types
Js data types are divided into basic types and object types (basic types are also called primitive types, value types, and object types are also called reference types). There are the following basic types:
number
string
boolean
null
undefined
symbol
(es6)bigInt
(es6+)
Object types include object, array array, function, and so on:
object
function
array
set
,weakSet
(es6)map
,weakmap
(es6)
Basic types of
The string type is a string. In addition to single and double quotes, es6 introduced new backquotes to contain strings. The extension of backquotes is that you can use ${… } inserts variables and expressions into strings. The usage method is as follows:
let n = 3
let m = () = > 4
let str = `m + n = ${m() + n}` // "m + n = 7"
Copy the code
Values of type number include integers, floating point numbers, NaN, Infinity, and so on. NaN is the only type in JS that is not equal to itself. NaN is returned when undefined mathematical operations occur, such as 1+’asdf’, Number(‘asdf’). Floating-point operations may occur such as 0.1 + 0.2! == 0.3 problem, which is due to the accuracy of floating point operation, generally using toFixed(10) can solve this problem.
Boolean, string, number, symbol, bigInt as primitive types, there should be no functions to call, because primitive types have no stereotype chain to provide methods. However, these three types can call methods on object prototypes such as toString:
true.toString() // 'true'
`asdf`.toString() // 'asdf'
NaN.toString() // 'NaN'
Symbol(1).toString() // 'Symbol(1)'
bigInt(1).toString() / / '1'
Copy the code
So why can’t the number 1 call toString, you might say? In fact, it is not impossible to call:
1 .toString()
1..toString()
(1).toString()
Copy the code
All three of the above calls are fine, and the first dot after a number is interpreted as a decimal point, not a dot call. It’s just not recommended, and it doesn’t make sense.
Why can primitive types call methods of object types directly? When parsing the above statement, the js engine will parse the three basic types into wrapped objects (new String() below). Wrapped objects are Object types that can call methods on Object.prototype. The general process is as follows:
'asdf'.toString() -> new String('asdf').toString() -> 'asdf'
Copy the code
Null A special value whose meaning is “none”, “empty”, or “value unknown”.
Undefined means “not assigned”. Undefined unless the variable is declared and not assigned, or if the object’s attributes do not exist. Avoid using var a = undefined; Var a = {b: undefined} var a = null; Var o = {b: null}, to distinguish from “not assigned” default undefined.
The Symbol value represents a unique identifier. This can be created using the Symbol() function:
var a = Symbol('asdf')
var b = Symbol('asdf')
a === b // false
Copy the code
BigInt is used to represent arbitrarily large integers written with a lowercase n following the number. The original Javascript representation of the maximum Number is 2 to the power of 53, beyond which precision is lost. Because converting between Number and BigInt loses accuracy, it is recommended to use BigInt only when the value may be greater than 253, and not to convert between the two types.
Object type
An object is a collection of attributes consisting of key-value pairs, where the Key can be a string or symbol, and the value can be of any type.
Arrays and functions are special objects that have the length attribute (functions also have name, prototype, and so on). Array is mainly a variety of methods are numerous, refer to this article. Function is js first-class citizen, involving more content, can refer to this article. I will not repeat it in this article.
The difference between primitive types and object types
Variables are stored in stack memory, except that primitive types hold values on the stack, whereas object types hold Pointers to the real memory address of the object. The real memory address to which this pointer points is actually in heap memory.
Object types operate in two cases:
- In case 1, object attributes are added/modified/deleted in heap memory, affecting all objects that reference the heap memory address
const obj = {
a: 'a'.b: 'b'
}
const newObj = obj
newObj.c = 'cccc'
newObj.a = 'aaaa'
delete newObj.b
// newObj === obj === { a: 'aaaa', c: 'cccc' }
Copy the code
- In case 2, the object is reassigned directly, in which case the pointer stored on the stack is modified, and the object loses contact with the original heap memory address
const obj = {
a: 'a'.b: 'b',}const newObj = obj
newObj = {}
// newObj ! == obj
Copy the code
When a function passes a pointer to an object, it passes a pointer to an object:
function fn (item) {
item.c = 'c'
}
const obj = {
a: 'a'.b: 'b'
}
fn(obj)
// obj.c === 'c'
// When fn(obj) is executed, a new variable item is declared inside the function and assigned to obj
Copy the code
Type judgment
The typeof an object type is different from that of a primitive type.
typeof 1 // 'number'
typeof 'asdf' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol(a)// 'symbol'
typeof null // 'object'
typeof BigInt(1) // 'bigint'
Copy the code
Typeof (NULL) === ‘object’ is a bug with a long history. In the first version of JS, null memory stores information starting with 000 and is judged as object. Although the internal type determination code has now been changed, the bug has to be retained with the release, because modifying this bug will cause bugs to appear on a large number of sites.
Typeof returns object for all object types except function. However, we are definitely developing arrays that return array types, so Typeof is not very suitable for object types. To determine the type of an object, use instanceof:
var obj = {}
var arr = []
var fun = () = > {}
typeof obj // 'object'
typeof arr // 'object'
typeof fun // 'function'
obj instanceof Object // true
arr instanceof Array // true
fun instanceof Function // true
Copy the code
You can see that the instanceof operator correctly determines the type of the object type. Instanceof essentially checks whether the prototype object of the constructor on the right exists on the prototype chain on the left, and returns true if so. So whether arrays, objects, or functions… Instanceof Object all return true.
Finally to a type of universal judgment method: Object. The prototype. ToString. Call (…). , you can try it yourself.
Implicit type conversion
Strong and weak types
Strongly typed languages require that all variables must be defined before they are used, and once a variable is assigned a data type, it is always that data type unless cast. Strongly typed languages do not have implicit type conversions. In weakly typed languages, by contrast, variables are declared without specific types, and implicit type conversions tend to occur at run time as needed.
Static and dynamic types
Static languages do type checking at compile time, while dynamic languages do type checking at run time.
Python is a dynamic language and a strongly typed (type-safe) language; JAVA is a static language and a strongly typed (type-safe) language;
JS is a dynamically weakly typed language, with implicit type conversions between different types occurring in certain situations, such as when comparing equality.
Implicit type conversions in equality comparisons
Basic type equality compares whether the values are the same, object equality compares whether the memory reference address is the same. Here’s an interesting comparison:
[] [] = =// ?[] = =! []// ?
Copy the code
For reference types such as [] {} function (){} that are not assigned to variables, they are valid only in the current statement and are not equivalent to any other object. There’s no way to find a pointer to their memory address. So [] == [] is false.
For [] ==! [], which is much more complicated because implicit type conversions are involved.
The equality rules of operands of different types are as follows:
- Check whether null and undefined are being compared, and return true if so. Null and undefined are not equal to any other value.
null= =undefined // true
null= =0 // false
undefined= =0 // false
Copy the code
- Check whether the two types are string and number. If so, the string is converted to number.
NaN= =NaN // false NaN does not equal any value
Copy the code
- Check whether one of the two parties is Boolean. If yes, the Boolean will be converted to number.
- Check whether one of the objects is object and the other is string, number, or symbol. If so, the object is converted to its original type.
Now uncover [] ==! [] Returns the truth of true:
[] = =! []// true
/* * First, the Boolean operator! Second, the operand has a Boolean value false, which is converted to a number: [] == false * [] == 0 * again, the operand [] is an object and is converted to the primitive type (valueOf() is the same as [] and toString() is the empty string) : "== 0 * Finally, the string is compared to a number and converted to a number: 0 == 0 */
Copy the code
There are only three types of conversion in JS: toNumber, toString, and toBoolean. The normal conversion rules are as follows:
Raw value/type | Destination type: number | The results of |
---|---|---|
null | number | 0 |
symbol | number | Throw the wrong |
string | number | '1' = > 1 '1a'=>NaN , if non-numbers are included, isNaN |
An array of | number | [] = > 0 '1' = > 1 ['1', '2']=>NaN |
object/function/undefined | number | NaN |
Raw value/type | Target type: string | The results of |
---|---|---|
number | string | 1 = > '1' |
array | string | [1, 2] = > '1, 2,' |
Boolean values/functions /symbol | string | The original value is quoted, as in:'true' |
object | string | {}=>'[object Object]' |
Raw value/type | Target type: Boolean | The results of |
---|---|---|
number | boolean | In addition to0 ,NaN forfalse Everything elsetrue |
string | boolean | Except for the empty stringfalse , all the others aretrue |
null/undefined | boolean | false |
Object type | boolean | true |
For more details on implicit type conversions, see this article.
Scope and execution context
scope
The scope in JS is lexical, that is, determined by the position at which the function is declared. (Unlike lexical scope, dynamic scope is confirmed at the time the function is executed. Js has no dynamic scope, but JS’s this looks like dynamic scope.) Lexical scope is a set of rules for accessing identifiers within functions that are created at compile time. At the end of the day, a scope is just an “empty disk” with no real variables, but rules that define how variables are accessed.
A scope chain is essentially a list of Pointers to variable objects that refer only to and contain no actual variable objects. A scope chain defines a set of rules for continuing queries along the scope chain when variables are not accessible in the current context.
Execution context
The execution context is the variable object generated in the execution stack when a function is called. This variable object is not directly accessible, but its variables, this object, and so on can be accessed. Such as:
let fn, bar; Function (x) {let b = 5; fn(x + b); }}}; fn = function(y) { let c = 5; console.log(y + c); //4, fn out of stack, bar out of stack}; bar(10); // enter the bar function contextCopy the code
Each time a function is called, a new execution context is created at the top of the execution stack, and the JavaScript engine handles them as a stack, which we call the Call stack. The bottom of the stack is always the global context, while the top is the currently active execution context, also known as the running Execution Context (blue block), as distinguished from the suspended variable object (execution context) below.
Difference: A scope is a set of access rules for identifiers within a function that are determined at the time the function is declared, while the execution context is the context in which a set of variables is created at the time the function is executed. One is generated at definition time and one is generated at execution time.
Understand how functions are executed
The execution process of the function is divided into two parts. One part is used to generate the execution context, determine the direction of this, declare variables, and generate the scope chain. The other part is to execute the code line by line in order.
- Establish the execution context phase (occurs when the function is called and before the code inside the function is executed)
- Generate variable object, order: create the arguments object – > create function function declarations — > create var variable declarations
- Generate scope chains
- Make sure this points to
- Function execution stage
- Execute code line by line, variable assignment in case of assignment operation, function reference call in case of function call, condition judgment and expression evaluation in case of condition judgment and expression evaluation, etc
This points to the
let fn = function(){
alert(this.name)
}
let obj = {
name: ' ',
fn
}
fn() 1 / / method
obj.fn() 2 / / method
fn.call(obj) 3 / / method
let instance = new fn() 4 / / method
Copy the code
- Method 1 calls the function directly
fn()
, which looks like a light rod commander,this
Point to thewindow
(In strict mode, yesundefined
). - Method 2 is a dot call
obj.fn()
At this time,this
Point to theobj
Object. Points in the callthis
Refers to the object before the dot. - In method 3
call
functionfn
In thethis
It points to the first parameter, and here it isobj
. It is usingcall
,apply
,bind
The delta function can take the delta functionthis
The variable points to the first parameter. - Method 4 using
new
Instantiate an objectinstance
, at this momentfn
In thethis
I’m pointing to the instanceinstance
.
What if more than one rule happens at the same time? In fact, the above four rules have increasing precedence:
fn() < obj.fn() < fn.call(obj) < new fn()
First, the new call has the highest priority, as long as the new keyword is present, this refers to the instance itself; Then, if there is no new keyword and you have call, apply, or bind, this points to the first parameter; Then if there is no new, call, apply, bind, and only obj.foo(), this points to the object in front of the point; Finally, there is the light-rod commander foo(), where this points to window (strictly undefined).
Arrow functions have been added to es6. Arrow functions don’t have their own this, arguments, super, new.target. Arrow functions don’t have a prototype object that can’t be used as a constructor. Since there is no this of its own, this in the arrow function actually refers to this in the containing function. Neither a point call nor a call can change the this in the arrow function.
Es6 also adds modules. In es6 modules, the top-level this refers to undefined, not the global object.
closure
For a long time I had a superficial understanding of closures as “functions defined inside a function”. In fact, this is just one of the necessary conditions for closure formation. It wasn’t until I read Kyle’s javascript you Don’t Know the definition of closures that I realized:
Closures occur when a function can remember and access its lexical scope.
let single = (function(){
let count = 0
return {
plus(){
count++
return count
},
minus(){
count--
return count
}
}
})()
single.plus() / / 1
single.minus() / / 0
Copy the code
This is a singleton pattern that returns an object and assigns the value to the variable single, which contains two functions plus and minus, both of which use the variable count in their lexical scope. Normally count and its execution context are destroyed at the end of the function execution, but because count is still in use by the external environment, it is not destroyed at the end of the function execution, resulting in closures. Each time single.plus() or single.minus() is called, the count variable in the closure is modified, and both functions retain references to their lexical scope.
A closure is a special function that accesses variables inside a function and keeps the values of those variables in memory and not cleared by garbage collection after a function is called.
Watch a classic Amway:
1 / / method
for (var i = 1; i <= 5; i++) {
setTimeout(function() {
console.log(i)
}, 1000)}2 / / method
for (let i = 1; i <= 5; i++) {
setTimeout(function() {
console.log(i)
}, 1000)}Copy the code
In method 1, five timers are set in a loop. After one second, the timer callback function is executed to print the value of variable I. Needless to say, after a second I had increased to 5, so the timer printed 5 five times. (The timer did not find variable I in the current scope, so I was found in the global scope along the scope chain)
In method 2, as the LET of ES6 will create local scopes, five scopes are set in a cycle, and the distribution of variable I in the five scopes is 1-5. A timer is set in each scope to print the value of variable I after one second. One second later, the timers find variables I 1-5 from their respective parent scopes. This is a new way of using closures to solve the problem of variable exceptions in loops.
Prototype and prototype chain
Objects in JS are created by constructors (object literals are syntactic sugar and are essentially created by constructors). All functions except the arrow function have an attribute called prototype. Prototype’s slice, Splice, Join, split, filter, reduce, etc.
Almost all objects in JS have a special [[Prototype]] built-in property that specifies the object’s Prototype object. This property is essentially a reference to other objects. A private __proto__ attribute is exposed in the browser, which is the browser implementation of [[Prototype]]. The object itself has a built-in [[Prototype]] that points to a Prototype object, and that Prototype object has its own [[Prototype]] that points to another Prototype object.
const arr = [1.2.3]
arr.__proto__ === Array.prototype // true
Array.prototype.__proto__ === Object.prototype // true
Object.prototype.__proto__ === null // true
Copy the code
As can be seen from the above example, there is a prototype chain from ARR to NULL as follows:
arr----__proto__---->Array.prototype----__proto__---->Object.prototype----__proto__---->null
Copy the code
The variable arr has access to methods on aarray. prototype and Object.prototype.
Prototype chains are also the essence of JS implementation inheritance, which will be covered in the next section.
Almost all objects in JS have a special [[Prototype]] built-in property. Because js can create objects with no built-in attribute [[Prototype]] :
var o = Object.create(null)
o.__proto__ // undefined
Copy the code
Object. Create is an ES5 method that is supported by all browsers. This method creates and returns a new object and assigns the new object’s prototype object as the first argument. In the above example, object.create (null) creates a new Object with no built-in attributes [[Prototype]].
Js inheritance
Js inheritance is achieved through the prototype chain, specific can refer to my article, here I only talk about we may be relatively strange “behavior delegate”. Behavior delegation is a method recommended by Kyle, author of JavaScript you Don’t Know series. This mode mainly uses setPrototypeOf method to associate the built-in prototype [[Protytype]] of one object with another object, so as to achieve the purpose of inheritance.
let SuperType = {
initSuper(name) {
this.name = name
this.color = [1.2.3]},sayName() {
alert(this.name)
}
}
let SubType = {
initSub(age) {
this.age = age
},
sayAge() {
alert(this.age)
}
}
Object.setPrototypeOf(SubType,SuperType)
Subtype.__proto__ === SuperType
SubType.initSub('17')
SubType.initSuper('gim')
SubType.sayAge() // 'gim'
SubType.sayName() / / '17'
Copy the code
The above example associates the parent object SuperType with the built-in stereotype of the child object SubType so that methods on the child object can be called directly on the parent object. The prototype chain generated by behavior delegation is simpler and clearer than the relationship generated by class inheritance.
event loop
Js is single threaded, all tasks need to queue, the previous task is finished, the next task will be executed. If the first task takes a long time, the second task has to wait forever. But IO devices (input and output devices) are slow (such as Ajax operations reading data from the network), and JS cannot wait for the IO device to complete before moving on to the next task, thus losing the meaning of the language. So JS tasks are divided into synchronous tasks and asynchronous tasks.
- All synchronization tasks are executed on the main thread, forming an “execution context stack”.
- All asynchronous tasks are suspended until the callback function is queued in a task queue.
- When all synchronization tasks in the execution stack are completed, the callback function of the first task queue is read and pushed to the execution stack to start execution.
- The main thread repeats the third step, which is how the event loop works.
In the figure above, when the main thread is running, the heap and stack are created. The heap is used to hold reference types such as array objects. The code in the stack calls various external apis, which add various events (click, load, done) to the “task queue”. As soon as the stack completes, the main thread reads the “task queue” and executes the corresponding callback function for those events.
There are two kinds of asynchronous tasks in the task queue, one is macro task, including Script setTimeout setInterval, and the other is micro task, including Promise Process. nextTick MutationObserver. Whenever a JS script is executed, the entire code in the script is executed first. When the synchronization task in the execution stack is completed, the first task in the microtask will be executed and pushed to the execution stack. When the execution stack is empty, the microtask will be read again and the cycle repeats until the list of microtasks is empty. When the list of microtasks is empty, the first task in the macro task will be read and pushed to the execution stack. When the execution stack is empty, the micro task will be read and executed again. The macro task will be read and executed again when the micro task is empty.