您的位置:首页 > Web前端 > JavaScript

快速掌握JavaScript面试基础知识

2018-02-20 00:00 435 查看
根据StackOverflow调查, 自2014年一来,JavaScript是最流行的编程语言。当然,这也在情理之中,毕竟1/3的开发工作都需要一些JavaScript知识。因此,如果你希望在成为一个开发者,你应该学会这门语言。

01、类型和类型转换

在JavaScript中有7个内置类型:
null
undefined
boolean
number
string
object
,和
symbol
(ES6)。除了
object
以外,其它都叫做基本类型。
typeof 0              // number
typeof true           // boolean
typeof 'Hello'        // string
typeof Math           // object
typeof null           // object  !!
typeof Symbol('Hi')   // symbol (New ES6)
➤Null vs. UndefinedUndefined表示未定义。对于没有初始化的变量、函数调用时候未提供的函数参数、缺失的对象属性,它们的默认值就是
undefined
。如果一个函数没有返回语句,那么默认的返回值也是
undefined
NUll表示值为空。一个变量我们可以将其赋值为
null
,表示当前的没有值。
➤隐式转换请看下面的例子:
var name = 'Joey';
if (name) {
 console.log(name + " doesn't share food!")
 // Joey doesn’t share food!
}
if
语句的条件判断中,
name
从字符串转换为布尔型。在
if
的代码块中,在控制台将
name
原原本本打印出来。你知道在什么情况下字符串会转换为真,什么时候为假么?
""
0
, 
null
undefined
NaN
false
 会自动转换为
false
。其它的都会转换为真:
Boolean(null)         // false
Boolean('hello')      // true
Boolean('0')          // true
Boolean(' ')          // true
Boolean([])           // true
Boolean(function(){}) // true
空数组、对象、函数定义都会自动转换为真。➤String & Number之间的转换第一个你要非常小心的是
+
操作符。因为它同时用于数字相加和字符串拼接。
*
,
/
,
-
只用于数字运算,当这些操作符和字符串一起使用,那么字符串会被强制转换为数字。
1 + "2" = "12"
"" + 1 + 0 = "10"
"" - 1 + 0 = -1
"-9\n" + 5 = "-9\n5"
"-9\n" - 5 = -14
"2" * "3" = 6
4 + 5 + "px" = "9px"
"$" + 4 + 5 = "$45"
"4" - 2 = 2
"4px" - 2 = NaN
null + 1 = 1
➤== vs. ===一个广泛被接受的认知就是:
==
判断值是否相等,
===
同时判断值是否相等和类型是否相同。但是,这里有些误解。实际上,
==
在验证相等性的时候,会对类型不同的值做一个类型转换。
===
对要判断的值不做类型转换。
2 == '2'     // True
2 === '2'     // False
undefined == null   // True
undefined === null  // False
类型转换有很多取巧的地方,要注意:
let a = '0';
console.log(Boolean(a)); // True
let b = false;
console.log(Boolean(b)); // False
```
你认为下面的相等判断会输出什么值呢?
```js
console.log(a == b);
实际上会返回true。知道为什么吗?
如果你将一个布尔类型的和非布尔类型的判断,JavaScript会将布尔类型的转换为数字然后再比对。
执行过程如下:
'0' == false   (1)
'0' == 0       (2)
0  == 0       (3)
所以,最终变成了
0==0
,当然返回true啦。如果你想看完整的资料,请查看ES5的官方文档。如果想看cheat sheet, 点击这里。一些比较容易掉坑的比较,我在这里列出来:
false == ""  // true
false == []  // true
false == {}  // false
"" == 0      // true
"" == []     // true
"" == {}     // false
0 == []      // true
0 == {}      // false
0 == null    // false

02、值 vs. 引用

对于基本类型的值,赋值是通过值拷贝的形式;比如:
null
undefined
boolean
number
string
和ES6的
symbol
。对于复杂类型的值,通过引用拷贝的形式赋值。比如:对象、对象包括数组和函数。
var a = 2;
 // 'a' hold a copy of the value 2.
var b = a;  
// 'b' is always a copy of the value in 'a'
b++;
console.log(a);  
// 2
console.log(b);  
// 3
var c = [1,2,3];
var d = c;  
// 'd' is a reference to the shared value
d.push( 4 );  
// Mutates the referenced value (object)
console.log(c);  
// [1,2,3,4]
console.log(d);  
// [1,2,3,4]
/* Compound values are equal by reference */
var e = [1,2,3,4];
console.log(c === d);  
// true
console.log(c === e);
 // false
如果想对复杂类型的值进行值拷贝,你需要自己去对所有子元素进行拷贝。
const copy = c.slice()    
// 'copy' 即使copy和c相同,但是copy指向新的值
console.log(c);          
// [1,2,3,4]
console.log(copy);        
// [1,2,3,4]
console.log(c === copy);  
// false
Fundebug提供实时、专业的错误监控服务,为您的线上代码保驾护航,欢迎大家免费使用!

03、作用域(Scope)

作用域值程序的执行环境,它包含了在当前位置可访问的变量和函数。全局作用域是最外层的作用域,在函数外面定义的变量属于全局作用域,可以被任何其他子作用域访问。在浏览器中,window对象就是全局作用域。局部作用域是在函数内部的作用域。在局部作用域定义的变量只能在该作用域以及其子作用域被访问。
function outer() {
 let a = 1;
 function inner() {
   let b = 2;
   function innermost() {
     let c = 3;
     console.log(a, b, c);  
     // 1 2 3
   }
   innermost();
   console.log(a, b);        
   // 1 2 — 'c' is not defined
 }
 inner();
 console.log(a);            
 // 1 — 'b' and 'c' are not defined
}
outer();
你可以将作用域想象成一系列不断变小的门。如果一个个子不高的人可以穿过最小的门(局部最小作用域),那么必然可以穿过任何比它大的门(外部作用域)。

04、提升(Hoisting)

在编译过程中,将
var
function
的定义移动到他们作用域最前面的行为叫做提升。整个函数定义会被提升。所以,你可以在函数还未定义之前调用它,而不用担心找不到该函数。
console.log(toSquare(3));  
// 9

function toSquare(n){
 return n*n;
}
变量只会被部分提升。而且只有变量的声明会被提升,赋值不会动。
let
const
不会被提升。
{  /* Original code */
 console.log(i);  
 // undefined
 var i = 10
 console.log(i);  
 // 10
}
{  /* Compilation phase */
 var i;
 console.log(i);  
 // undefined
 i = 10
 console.log(i);  
 // 10
}
// ES6 let & const
{
 console.log(i);  
 // ReferenceError: i is not defined
 const i = 10
 console.log(i);  
 // 10
}
{
 console.log(i);  
 // ReferenceError: i is not defined
 let i = 10
 console.log(i);  
 // 10
}

05、函数表达式和函数声明

➤函数表达式一个函数表达式是在函数执行到函数表达式定义的位置才开始创建,并被使用。它不会被提升。
var sum = function(a, b) {
 return a + b;
}
➤函数声明函数声明的函数可以在文件中任意位置调用,因为它会被提升。
function sum(a, b) {
 return a + b;
}

06、变量:var,let和const

在ES6之前,只能使用
var
来声明变量。在一个函数体中声明的变量和函数,周围的作用域内无法访问。在块作用域
if
for
中声明的变量,可以在
if
for
的外部被访问。注意:如果没有使用
var
,
let
或则
const
关键字声明的变量将会绑定到全局作用域上。
function greeting() {
 console.log(s) // undefined
 if(true) {
   var s = 'Hi';
   undeclaredVar = 'I am automatically created in global scope';
 }
 console.log(s)
  // 'Hi'
}
console.log(s);  
// Error — ReferenceError: s is not defined
greeting();
console.log(undeclaredVar)
// 'I am automatically created in global scope'
ES6的
let
const
都是新引入的关键字。它们不会被提升,而且是块作用域。也就是说被大括号包围起来的区域声明的变量外部将不可访问。
let g1 = 'global 1'
let g2 = 'global 2'
{  
/* Creating a new block scope */
 g1 = 'new global 1'
 let g2 = 'local global 2'
 console.log(g1)  
 // 'new global 1'
 console.log(g2)  
  // 'local global 2'
 console.log(g3)  
 // ReferenceError: g3 is not defined
 let g3 = 'I am not hoisted';
}
console.log(g1)    
// 'new global 1'
console.log(g2)    
// 'global 2'
一个常见的误解是:使用
const
声明的变量,其值不可更改。准确地说它不可以被重新赋值,但是可以更改。
const tryMe = 'initial assignment';
tryMe = 'this has been reassigned';  
// TypeError: Assignment to constant variable.
// You cannot reassign but you can change it…
const array = ['Ted', 'is', 'awesome!'];
array[0] = 'Barney';
array[3] = 'Suit up!';
console.log(array);    
// [“Barney”, “is”, “awesome!”, “Suit up!”]
const airplane = {};
airplane.wings = 2;
airplane.passengers = 200;
console.log(airplane);  
// {passengers: 200, wings: 2}

07、闭包

闭包由一个函数以及该函数定义是所在的环境组成。我们通过例子来形象解释它。
function sayHi(name){
 var message = `Hi ${name}!`;
 function greeting() {
   console.log(message)
 }
 return greeting
}
var sayHiToJon = sayHi('Jon');
console.log(sayHiToJon)    
// ƒ() { console.log(message) }
console.log(sayHiToJon())  
// 'Hi Jon!'
请理解
var sayHiToJon = sayHi('Jon');
这行代码的执行过程,
sayHi
函数执行,首先将
message
的值计算出来;然后定义了
greeting
函数,函数中引用了
message
变量;最后,返回
greeting
函数。如果按照C/Java语言的思路,
sayHiToJon
就等价于
greeting
函数,那么会报错:message未定义。但是在JavaScript中不一样,这里的
sayHiToJon
函数等于
greeting
函数以及一个环境,该环境中包含了
message
。因此,当我们调用
sayHiToJon
函数,可以成功地将
message
打印出来。因此,这里的闭包就是
greeting
函数和一个包含
message
变量的环境。(备注: 为了便于理解,此段落未按照原文翻译。)闭包的一个优势在于数据隔离。我们同样用一个例子来说明:
function SpringfieldSchool() {
 let staff = ['Seymour Skinner', 'Edna Krabappel'];
 return {
   getStaff: function() { console.log(staff) },
   addStaff: function(name) { staff.push(name) }
 }
}

let elementary = SpringfieldSchool()
console.log(elementary)        
// { getStaff: ƒ, addStaff: ƒ }
console.log(staff)            
// ReferenceError: staff is not defined
/* Closure allows access to the staff variable */
elementary.getStaff()          
// ["Seymour Skinner", "Edna Krabappel"]
elementary.addStaff('Otto Mann')
elementary.getStaff()          
// ["Seymour Skinner",
"Edna Krabappel", "Otto Mann"]
elementary
被创建的时候,
SpringfieldSchool
已经返回。也就是说
staff
无法被外部访问。唯一可以访问的方式就是里面的闭包函数
getStaff
addStaff
。我们来看一个面试题:下面的代码有什么问题,如何修复?
const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
 setTimeout(function() {
   console.log(`The value ${arr[i]} is at index: ${i}`);
 }, (i+1) * 1000);
}
上面的代码输出的结果全部都一样:”The value undefined is at index: 4”。因为所有在
setTimeout
中定义的匿名函数都引用了同一个外部变量
i
。当匿名函数执行的时候,
i
的值为4。这个问题可以改用
IIFE
(后面会介绍)方法来解决,通过对每一个匿名函数构建独立的外部作用域来实现。
const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
 (function(j) {
   setTimeout(function() {
     console.log
     (`The value ${arr[j]} is at index: ${j}`);
   }, j * 1000);
 })(i)
}
当然,还有一个方法,使用
let
来声明
i
const arr = [10, 12, 15, 21];
for (let i = 0;
i < arr.length; i++) {
 setTimeout(function() {
   console.log(`The value ${arr[i]} is at index: ${i}`);
 }, (i) * 1000);
}

立即调用的函数表达式(Immediate Invoked Function Expression)(IIFE)

一个IIFE是一个函数表达式在定义之后立即被调用。常用在你想对一个新声明的变量创建一个隔离的作用域。
它的格式为: 
(function(){....})()
。前面的大括号用于告诉编译器这里不仅仅是函数定义,后面的大括号用于执行该函数。
var result = [];
for (var i=0; i < 5; i++) {
 result.push( function() { return i } );
}
console.log( result[1]() );
// 5
console.log( result[3]() );
// 5
result = [];
for (var i=0; i < 5; i++) {
 (function () {
   var j = i;
    // copy current value of i
   result.push( function() { return j } );
 })();
}
console.log( result[1]() );
// 1
console.log( result[3]() );
// 3
使用IIFE可以:➤为函数绑定私有数据
➤创建一个新的环境
➤避免污染全局命名空间

08、环境(Context)

我们往往容易将环境(Context)和作用域(Scope)搞混,我来简单解释一下:➤环境(Context): 由函数如何被调用而决定,往往指
this

➤作用域(Scope): 可访问的变量。

09、函数调用:call, apply, bind

这三个方法都是为了将this绑定到函数,区别在于调用的方式。
➤.call()
会立即执行函数,你需要把参数按顺序传入;
➤.apply()
会立即执行函数,你需要把所有的参数组合为一个数组传入;
.call()
.apply()
几乎相同。哪个传入参数方便,你就选择哪个。
const Snow = {surename: 'Snow'}
const char = {
 surename: 'Stark',
 knows: function(arg, name) {
   console.log(
   `You know ${arg}, ${name} ${this.surename}`);
 }
}
char.knows('something', 'Bran');              
// You know something, Bran Stark
char.knows.call(Snow, 'nothing', 'Jon');      
// You know nothing, Jon Snow
char.knows.apply(Snow, ['nothing', 'Jon']);  
// You know nothing, Jon Snow
注意:如果你将数组传入
call
函数,它会认为只有一个参数。ES6允许使用新的操作符将数组变换为一个序列。
char.knows.call(Snow, ...["nothing", "Jon"]);  
// You know nothing, Jon Snow
.bind()
返回一个新的函数,以及相应的环境和参数。如果你想该函数稍后调用,那么推荐使用
bind

.bind()
函数的优点在于它可以记录一个执行环境,对于异步调用和事件驱动的编程很有用。
.bind()
传参数的方式和
call
相同。
const Snow = {surename: 'Snow'}
const char = {
 surename: 'Stark',
 knows: function(arg, name) {
   console.log(`You know ${arg},
${name} ${this.surename}`);}
 }
const whoKnowsNothing = char.knows.bind(Snow, 'nothing');
whoKnowsNothing('Jon');  
// You know nothing, Jon Snow

09、this关键字

要理解JavaScript中
this
关键字,特别是它指向谁,有时候相当地复杂。
this
的值通常由函数的执行环境决定。简单的说,执行环境指函数如何被调用的。
this
像是一个占位符(placeholder),它指向当方法被调用时,调用对应的方法的对象。下面有序地列出了判断
this
指向的规则。如果第一条匹配,那么就不用去检查第二条了。
➤new
绑定 - 当使用
new
关键字调用函数的时候,
this
指向新构建的对象。
function Person(name, age) {
 this.name = name;
 this.age =age;
 console.log(this);
}
const Rachel = new Person('Rachel', 30);  
// { age: 30, name: 'Rachel' }
➤显示绑定(Explicit binding) - 当使用
call
或则
apply
的时候,我们显示的传入一个对象参数,该参数会绑定到
this
。 注意:
.bind()
函数不一样。用
bind
定义一个新的函数,但是依然绑定到原来的对象。
function fn() {
 console.log(this);
}
var agent = {id: '007'};
fn.call(agent);    
// { id: '007' }
fn.apply(agent);  
// { id: '007' }
var boundFn = fn.bind(agent);
boundFn();        
// { id: '007' }
➤隐式绑定 - 当一个函数在某个环境下调用(在某个对象里),
this
指向该对象。也就是说该函数是对象的一个方法。
var building = {
 floors: 5,
 printThis: function() {
   console.log(this);
 }
}
building.printThis();  
// { floors: 5, printThis: function() {…} }
➤默认绑定 - 如果上面所有的规则都不满足,那么
this
指向全局对象(在浏览器中,就是window对象)。当函数没有绑定到某个对象,而单独定义的时候,该函数默认绑定到全局对象。
function printWindow() {
 console.log(this)
}
printWindow();  
// window object
注意:下面的情况中,
inner
函数中的
this
指向全局。
function Dinosaur(name) {
 this.name = name;
 var self = this;
 inner();
 function inner() {
   alert(this);        
   // window object — the function
has overwritten the 'this' context
   console.log(self);  
   // {name: 'Dino'} — referencing the stored
value from the outer context
 }
}
var myDinosaur = new Dinosaur('Dino');
➤词法(Lexical) this - 当是使用
=>
来定义函数时,
this
指向定义该函数时候外层的
this
。 备注:大概是和定义的词法(
=>
)有关,把它称作
Lexical this

function Cat(name) {
 this.name = name;
 console.log(this);   // { name: 'Garfield' }
 ( () => console.log(this) )();   // { name: 'Garfield' }
}
var myCat = new Cat('Garfield');

10、严格(Strict)模式

如果你使用了
"use strict"
指令,那么JavaScript代码会在严格模式下执行。在严格模式下,对于词法分析和错误处理都有特定的规则。在这里我列出它的一些优点:➤使得Debug更容易:以前会被忽略的错误现在会显示报错,比如赋值给一个不可写的全局变量或则属性;
➤避免不小心声明了全局变量:赋值给一个未定义的变量会报错;
➤避免无效使用delete:尝试去删除变量、函数或则不可删除的属性会抛出错误;
➤避免重复的属性名和参数值:对象上重复的属性和函数参数会抛出错误(在ES6中不再是这样);
➤使得
eval()
更加安全:在
eval()
中定义的变量和函数在外部作用域不可见;
➤“安全”的消除JavaScript中this的转换:如果
this
是null或则undefined不在转换到全局对象。也就是说在浏览器中使用this去指向全局对象不再可行。

11、new关键字

如果使用
new
关键字来调用函数式很特别的形式。我们把那些用
new
调用的函数叫做构造函数(constructor function)。使用了
new
的函数到底做了什么事情呢?➤创建一个新的对象
➤将对象的prototype设置为构造函数的prototype
➤执行构造函数,
this
执行新构造的对象
➤返回该对象。如果构造函数返回对象,那么返回该构造对象。
// 为了更好地理解底层,
我们来定义new关键字
function myNew(constructor, ...arguments)
{
 var obj = {}
 Object.setPrototypeOf(obj,
 constructor.prototype);
 return constructor.apply(obj,
 arguments) || obj
}
使用
new
和不使用的区别在哪里呢?
function Bird() {
 this.wings = 2;
}
/* 普通的函数调用 */
let fakeBird = Bird();
console.log(fakeBird);    
// undefined
/* 使用new调用 */
let realBird= new Bird();
console.log(realBird)    
// { wings: 2 }
为了便于对比理解,译者额外增加了测试了一种情况:
function MBird(){
 this.wings =2;
 return "hello";
}
let realMBrid = new MBird();
console.log(realMBird)
// { wings: 2 }
你会发现,这一句
return "hello"
并没有生效!

12、原型和继承

原型(Prototype)是JavaScript中最容易搞混的概念,其中一个原因是
prototype
可以用在两个不同的情形下。➤原型关系
每一个对象都有一个
prototype
对象,里面包含了所有它的原型的属性。
.__proto__
是一个不正规的机制(ES6中提供),用来获取一个对象的prototype。你可以理解为它指向对象的
parent

所有普通的对象都继承
.constructor
属性,它指向该对象的构造函数。当一个对象通过构造函数实现的时候,
__proto__
属性指向构造函数的构造函数的
.prototype
Object.getPrototypeOf()
是ES5的标准函数,用来获取一个对象的原型。➤原型属性
每一个函数都有一个
.prototype
属性,它包含了所有可以被继承的属性。该对象默认包含了指向原构造函数的
.constructor
属性。每一个使用构造函数创建的对象都有一个构造函数属性。接下来通过例子来帮助理解:
function Dog(breed, name){
 this.breed = breed,
 this.name = name
}
Dog.prototype.describe = function() {
 console.log(`${this.name} is a ${this.breed}`)
}
const rusty = new Dog('Beagle', 'Rusty');

/* .prototype 属性包含了构造函数以
及构造函数中在prototype上定义的属性。*/
console.log(Dog.prototype)  
// { describe: ƒ , constructor: ƒ }

/* 使用Dog构造函数构造的对象 */
console.log(rusty)  
//  { breed: "Beagle", name: "Rusty" }
/* 从构造函数的原型中继承
下来的属性或函数 */
console.log(rusty.describe())  
// "Rusty is a Beagle"
/* .__proto__ 属性指向构造函数的
.prototype属性 */
console.log(rusty.__proto__)    
// { describe: ƒ , constructor: ƒ }
/* .constructor 属性指向构造函数 */
console.log(rusty.constructor)  
// ƒ Dog(breed, name) { ... }
JavaScript的使用可以说相当灵活,为了避免出bug了不知道,不妨接入Fundebug线上实时监控。

13、原型链

原型链是指对象之间通过prototype链接起来,形成一个有向的链条。当访问一个对象的某个属性的时候,JavaScript引擎会首先查看该对象是否包含该属性。如果没有,就去查找对象的prototype中是否包含。以此类推,直到找到该属性或则找到最后一个对象。最后一个对象的prototype默认为null。

14、拥有 vs 继承

一个对象有两种属性,分别是它自身定义的和继承的。
function Car() { }
Car.prototype.wheels = 4;
Car.prototype.airbags = 1;

var myCar = new Car();
myCar.color = 'black';
/*  原型链中的属性也可以通过in来查看:  */
console.log('airbags' in myCar)  
// true
console.log(myCar.wheels)        
// 4
console.log(myCar.year)          
// undefined
/*  通过hasOwnProperty来查看是否拥有该属性:  */
console.log(myCar.hasOwnProperty('airbags'))  
// false — Inherited
console.log(myCar.hasOwnProperty('color'))    
// true
Object.create(obj)
 创建一个新的对象,prototype指向
obj
var dog = { legs: 4 };
var myDog = Object.create(dog);
console.log(myDog.hasOwnProperty('legs'))  
// false
console.log(myDog.legs)                    
// 4
console.log(myDog.__proto__ === dog)      
// true

15、继承是引用传值

继承属性都是通过引用的形式。我们通过例子来形象理解:
var objProt = { text: 'original' };
var objAttachedToProt = Object.create(objProt);
console.log(objAttachedToProt.text)  
// original

// 我们更改objProt的text属性,
objAttachedToProt的text属性同样更改了
objProt.text = 'prototype property changed';
console.log(objAttachedToProt.text)  
// prototype property changed

// 但是如果我们讲一个新的对象赋值给objProt,
那么objAttachedToProt的text属性不受影响
objProt = { text: 'replacing property' };
console.log(objAttachedToProt.text)  
// prototype property changed

16、经典继承 vs 原型继承

Eric Elliott的文章有非常详细的介绍:Master the JavaScript Interview: What’s the Difference Between Class & Prototypal Inheritance?
作者认为原型继承是优于经典的继承的,并提供了一个视频介绍:https://www.youtube.com/watch?v=wfMtDGfHWpA&feature=youtu.be

17、异步JavaScript

JavaScript是一个单线程程序语言,也就是说JavaScript引擎一次只能执行某一段代码。它导致的问题就是:如果有一段代码需要耗费很长的时间执行,其它的操作就被卡住了。JavaScript使用Call Stack来记录函数的调用。一个Call Stack可以看成是一摞书。最后一本书放在最上面,也最先被移走。最先放的书在最底层,最后被移走。为了避免复杂代码占用CPU太长时间,一个解法就是定义异步回调函数。我们自己来定义一个异步函数看看:
function greetingAsync(name, callback){
 let greeting = "hello, " + name ;
 setTimeout(_ => callback(greeting),0);
}
greetingAsync("fundebug", console.log);
console.log("start greeting");
我们在
greetingAsync
中构造了
greeting
语句,然后通过
setTimeout
定义了异步,
callback
函数,是为了让用户自己去定义greeting的具体方式。为方便起见,我们时候直接使用
console.log
。上面代码执行首先会打印
start greeting
,然后才是
hello, fundebug
。也就是说,
greetingAsync
的回调函数后执行。在网站开发中,和服务器交互的时候需要不断地发送各种请求,而一个页面可能有几十个请求。如果我们一个一个按照顺序来请求并等待结果,串行的执行会使得网页加载很慢。通过异步的方式,我们可以先发请求,然后在回调中处理请求结果,高效低并发处理。下面通过一个例子来描述整个执行过程:
const first = function () {
 console.log('First message')
}
const second = function () {
 console.log('Second message')
}
const third = function() {
 console.log('Third message')
}

first();
setTimeout(second, 0);
third();

// 输出:
 // First message
 // Third message
 // Second message
初始状态下,浏览器控制台没有输出,并且事件管理器(Event Manager)是空的;

first()
被添加到调用栈

console.log("First message")
加到调用栈

console.log("First message")
执行并输出“First message”到控制台

console.log("First message")
从调用栈中移除

first()
从调用栈中移除

setTimeout(second, 0)
加到调用栈

setTimeout(second, 0)
执行,0ms之后,
second()
被加到回调队列

setTimeout(second, 0)
从调用栈中移除

third()
加到调用栈

console.log("Third message")
加到调用栈

console.log("Third message")
执行并输出“Third message”到控制台

console.log("Third message")
从调用栈中移除

third()
从调用栈中移除

Event Loop 将
second()
从回调队列移到调用栈

console.log("Second message")
加到调用栈

console.log("Second message")
Second message”到控制台

console.log("Second message")
从调用栈中移除

Second()
从调用栈中移除

特别注意的是:
second()
函数在0ms之后并没有立即执行,你传入到
setTimeout()
函数的时间和
second()
延迟执行的时间并不一定直接相关。事件管理器等到
setTimeout()
设置的时间到期才会将其加入回调队列,而回调队列中它执行的时间和它在队列中的位置已经它前面的函数的执行时间有关。关注我们
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: