JavaScript is an interesting language that we all love because of its nature. The browser is where JavaScript is primarily run, and the two work together in our service. JS has a few concepts that people tend to take lightly and sometimes ignore. Concepts such as prototypes, closures, and event loops are still one of the arcane areas that most JS developers circumvent. As we know, ignorance is a dangerous thing and it can lead to mistakes.
Next, let’s look at some questions that you can try to think about and answer.
Question 1: What will be printed on the browser console?
var a = 10;
function foo() {
console.log(a); // ??
var a = 20;
}
foo();
Copy the code
Question 2: Is the output the same if we use let or const instead of var
var a = 10;
function foo() {
console.log(a); // ??
let a = 20;
}
foo();
Copy the code
Question 3: What elements are in “newArray”?
var array = [];
for(var i = 0; i <3; i++) {
array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray); // ??
Copy the code
Q4: If we run the function ‘foo’ in the browser console, does it cause a stack overflow error?
function foo() { setTimeout(foo, 0); // Is there a stack overflow error? };Copy the code
Problem 5: Is the UI of the page (TAB) still responsive if you run the following function in the console
function foo() {
return Promise.resolve().then(foo);
};
Copy the code
Question 6: Can we somehow use the expansion operation for the following statement without causing a type error
var obj = { x: 1, y: 2, z: 3 };
[...obj]; // TypeError
Copy the code
Question 7: What is printed on the console when the following code snippet is run?
var obj = { a: 1, b: 2 };
Object.setPrototypeOf(obj, {c: 3});
Object.defineProperty(obj, 'd', { value: 4, enumerable: false });
// what properties will be printed when we run the for-in loop?
for(let prop in obj) {
console.log(prop);
}
Copy the code
Question 8: What value does xGetter() print?
var x = 10; var foo = { x: 90, getX: function() { return this.x; }}; foo.getX(); // prints 90 var xGetter = foo.getX; xGetter(); // prints ??Copy the code
The answer
Now, let’s answer each question from beginning to end. I will give you a brief explanation while trying to demystify these behaviors and provide some references.
Question 1:undefined
Resolution:
Variables declared using the var keyword are promoted in JavaScript and assigned the value undefined in memory. But initialization happens exactly where you assign a value to a variable. In addition, var declares variables that are [function-scoped][2], while let and const are block-scoped. So, this is what the process looks like:
var a = 10; Function foo() {// var a's declaration will be promoted to the top of the function. // For example, var a console.log(a); // Print undefined // the actual initialization value 20 only happens here var a = 20; // local scope }Copy the code
Question 2:ReferenceError: a undefined
.
Resolution:
Let and const declarations can limit a variable to the block, statement, or expression it uses in its scope. Unlike VAR, these variables are not promoted and have a so-called temporary dead zone (TDZ). Attempting to access these variables in the TDZ will raise a ReferenceError because they can only be accessed when the arrival declaration is executed.
var a = 10; Function foo() {// TDZ starts // uninitialized 'a' console.log(a) is created; // ReferenceError // TDZ end, 'a' only initialized here, value 20 let a = 20; }Copy the code
The following table summarizes the promotion behavior and usage domains for variables declared with different keywords used in JavaScript:
Question 3:(3, 3, 3)
Resolution:
Declaring a variable with the var keyword in the header of the for loop creates a single binding (storage) for that variable. Read more about [closures][3]. Let’s look at the for loop again.
Var array = []; for (var i = 0; i < 3; I ++) {// Each ' 'I' 'in the three arrow function bodies points to the same binding, // that's why they return the same value '3' at the end of the loop. array.push(() => i); } var newArray = array.map(el => el()); console.log(newArray); / / [3, 3, 3]Copy the code
If let is used to declare a variable with block-level scope, a new binding is created for each iteration of the loop.
Var array = []; for (let i = 0; i < 3; I ++) {// This time, each 'I' refers to a new binding and keeps the current value. Therefore, each arrow function returns a different value. array.push(() => i); } var newArray = array.map(el => el()); console.log(newArray); / / [0, 1, 2]Copy the code
Another way to solve this problem is to use closures [4].
let array = []; for (var i = 0; i < 3; i++) { array[i] = (function(x) { return function() { return x; }; })(i); } const newArray = array.map(el => el()); console.log(newArray); / / [0, 1, 2]Copy the code
Problem 4: No overflow
Resolution:
The JavaScript concurrency model is based on an “event loop.” When we say “the browser is the home of JS” WHAT I really mean is that the browser provides the runtime environment to execute our JS code.
The main components of a browser includeThe call stack.Event loop**, task queueandWeb API * *. likesetTimeout
.setInterval
andPromise
Such global functions are not part of JavaScript, but part of the Web API. A visual representation of the JavaScript environment looks like this:
The JS call stack is last in, first out (LIFO). The engine pulls a function off the stack one at a time and runs the code from top to bottom. Whenever it encounters some asynchronous code, such as setTimeout, it hands it to the Web API(arrow 1). Therefore, every time an event is triggered, the callback is sent to the task queue (arrow 2).
Event loops constantly monitor the Task Queue and process callbacks one at a time in the order in which they are queued. Whenever the Call stack is empty, the Event Loop takes the callback and places it on the stack (arrow 3) for processing. Remember that if the call stack is not empty, the event loop will not push any callbacks onto the stack.
Now, with that knowledge in mind, let’s answer the aforementioned question:
steps
- call
foo()
willfoo
Function in aCall Stack. - While processing internal code, the JS engine encounters
setTimeout
. - then
foo
Callback letter data transmissionWebAPIs(Arrow 1) and returns from the function, the call stack is empty again - The timer is set to 0, so
foo
Will be sent toTask queue(Arrow 2). - Since the call stack is empty, the event loop will select
foo
Calls back and pushes it onto the call stack for processing. - The process repeats again and the stack does not overflow.
Problem 5: No response
Resolution:
Most of the time, developers assume that there is only one task queue in an event cycle graph. But that’s not the case. We can have multiple task queues. The browser selects one of the queues and processes the callback in that queue.
At the bottom, there are macro and micro tasks in JavaScript. The setTimeout callback is a macro task, while the Promise callback is a microtask.
The main difference is their execution. Macro tasks are pushed onto the stack one at a time in a single cycle, but the microtask queue is always cleared after execution before returning to the event loop. So, if you add items to this queue at the same rate as you process items, you’re always dealing with microtasks. The event loop rerenders the page only when the microtask queue is empty,
Now, run the following code snippet from the console
function foo() {
return Promise.resolve().then(foo);
};
Copy the code
Each call to ‘foo’ continues to add another ‘foo’ callback to the microtask queue, so the event loop cannot continue processing other events (scrolling, clicking, etc.) until the queue is completely empty. Therefore, it prevents rendering.
Problem 6: TypeError is caused
Resolution:
The [expansion syntax][6] and [for-of][7] statements iterable define the data to iterate over. Array or Map are built-in iterators with default iterative behavior. Objects are not iterable, but they can be made iterable by using the [iterable][8] and [iterator][9] protocols.
In the Mozilla documentation, an object is iterable if it implements the @@iterator method, which means that the object (or an object in its prototype chain) must have a property with an @@iterator key, which can be obtained through the constant symbol.iterator.
The above statement may seem wordy, but the following example makes more sense:
var obj = { x: 1, y: 2, z: 3 }; Obj [symbol.iterator] = function() {// Iterator is an object with a next method, // it returns at least one object and // two attributes: value&done. Return {next: function() {if (this._countdown === 3) {const lastValue = this._countdown; return { value: this._countDown, done: true }; } this._countDown = this._countDown + 1; return { value: this._countDown, done: false }; }, _countDown: 0 }; }; [...obj]; // Print [1, 2, 3]Copy the code
You can also use the [Generator][10] function to customize the iterative behavior of an object:
var obj = {x:1, y:2, z: 3} obj[Symbol.iterator] = function*() { yield 1; yield 2; yield 3; } [...obj]; // Print [1, 2, 3]Copy the code
Question 7: A, B, C
Resolution:
A for-in loop iterates through the [enumerable properties][11] of the object itself, as well as properties that the object inherits from its prototype. Enumerable properties are properties that can be included and accessed during a for-in loop.
var obj = { a: 1, b: 2 };
var descriptor = Object.getOwnPropertyDescriptor(obj, "a");
console.log(descriptor.enumerable); // true
console.log(descriptor);
// { value: 1, writable: true, enumerable: true, configurable: true }
Copy the code
Now that you have this knowledge, it should be easy to understand why our code prints these particular properties, right
var obj = { a: 1, b: 2 }; //a, b are both enumerables attributes // Set {c: 3} to 'obj' archetype, and we know that // for-in also iterates over obj inherited attributes // from its archetype, 'c' can also be accessed. Object.setPrototypeOf(obj, { c: 3 }); // We define another property 'd' in 'obj', but // set 'enumerable' to false. This means that the 'd' will be ignored. Object.defineProperty(obj, "d", { value: 4, enumerable: false }); for (let prop in obj) { console.log(prop); } // print // a // b // cCopy the code
Question 8:10
Resolution:
When x is initialized globally, it becomes an attribute of the Window object (not strictly mode). Look at the following code:
var x = 10; // global scope var foo = { x: 90, getX: function() { return this.x; }}; foo.getX(); // prints 90 let xGetter = foo.getX; xGetter(); // prints 10Copy the code
We can assert that:
window.x === 10; // true
Copy the code
This always refers to the object on which the method is called. So, in the case of foo.getx(), it points to foo and returns the value of 90. In the case of xGetter(), this refers to the window object and returns the value of x in the window, which is 10.
To get the value of foo.x, create a new Function by binding the value of this to the foo object using function.prototype. bind.
let getFooX = foo.getX.bind(foo); getFooX(); / / 90Copy the code
In this way! If you got all your answers right, well done. We all learn by making mistakes. It’s all about understanding the “why” behind it.
The bugs that may exist after code deployment cannot be known in real time. In order to solve these bugs, I spent a lot of time on log debugging. Incidentally, HERE I recommend a good BUG monitoring tool Fundebug