JavaScript 代码运行题解析总结

89 阅读9分钟

变量提升

变量的提升,提升的是变量的声明,而不是变量的赋值。值保留在原地

  • 定义变量,JS解析代码。⚠️:隐式变量不会提升(c = 3)
// JS定义变量
var a = 1
var b = 2

// JS解析代码
var a 
var b 
a = 1
b = 2
  • 函数中定义变量的提升
// 定义一个函数,函数中存在变量
function fn(){
  var a= 1
  console.log(a)
  console.log(b)
  var b = 2
}
fn()

// JS解析函数
function fn(){
  var a 
  var b 
  a = 1
  console.log(a) // 1
  console.log(b) // undefined
  b = 2  
}
fn()

总结:变量在声明提升的时候,是全部提升到作用域的最前面,一个接着一个的。但是在变量赋值的时候就不是一个接着一个赋值了,而是赋值的位置在变量原本定义的位置。原本js定义变量的地方,在js运行到这里的时候,才会进行赋值操作,而没有运行到的变量,不会进行赋值操作。

函数提升

普通函数提升

function foo(){
 console.log(2)
}
foo() // 2

函数表达式提升(提升的是变量,函数保留在原地)

var foo = function (){
 console.log(2)
}
foo()

// 代码解析
foo()  // foo is not a function
var foo = function(){
 console.log(2)
}

函数覆盖 (如果一个函数被重复声明,后面的声明会覆盖前面的)

1:都是用function 声明,后面的会覆盖前面的

function a(){
 console.log(1)
}
a()  // 2
function a(){
 console.log(2)
}
a()  // 2

// 两个都打印出2

2:function声明又有函数表达式,返回的是函数表达式的值

var a = function (){
 console.log(1)
}
function a(){
 console.log(2)
}
a() // 1


// 相当于
var a
function a(){
  console.log(2)
}
a = function(){
 console.log(1)
}
a()

变量提升、函数提升的顺序

在作用域中,不管是变量还是函数,都会提升到作用域最开始的位置,不同的是,函数的提升后的位置是在变量提升后的位置之后的。

function fn(){
 console.log(a)
 var a = 1
 console.log(a)
 function a(){}
 console.log(a)
}
fn()
// JS 解析后的代码
function fn(){
 var a
 function a(){}
 console.log(a) // a()
 a = 1
 console.log(a) // 1
 console.log(a) // 1
}
fn()

JS--静态作用域(JS的作用域在定义的时候就确定了

var a = 1
function test(){
  console.log(a)
}
function bar (){
  var a = 2
  test()
}
bar() // 1

静态作用域执行过程

作用域查找始终从运行时所处的最内层作用域开始查找,逐级向外查找,直到遇见第一个匹配的标识符为止。

无论函数在哪里被调用,无论如何被调用,它的作用域只由函数定义所处的位置决定。

var a = 1
function fn1(){
    function fn3(){
        var a = 4
        fn2()
    }
    var a = 2
    return fn3
}
function fn2(){
    console.log(a) // 1
}
var fn = fn1()
fn()
// 解析
`fn2` 定义在全局上,当 `fn2` 中找不到变量 `a` 时,它会去全局中寻找,
与 `fn1` 和 `fn3` 毫无关系,打印 `1`.

var a = 1
function fn1(){
    function fn2(){
        console.log(a) // 2
    }
    function fn3(){
        var a = 4
        fn2()
    }
    var a = 2
    return fn3
}
var fn = fn1()
fn()
// 解析
`fn2` 是定义在函数 `fn1` 内部,因此当 `fn2` 内部没有变量 `a` 时,
它会去 `fn1` 中寻找,跟函数 `fn3` 毫无关系,如果 `fn1` 中寻找不到,
会到 `fn1` 定义的位置的上一层(全局)寻找,直至寻找到第一个匹配的标识符。
本题可以在 `fn1` 中找到变量 `a`,打印 `2`
var a = 1;
function fn1(){
    function fn3(){
        function fn2(){
            console.log(a) // undefined
        }
        var a;
        fn2()
        a = 4
    }
    var a = 2
    return fn3
}
var fn = fn1()
fn()

// 解析
`fn2` 定义在函数 `fn3` 中,当 `fn2` 中找不到变量 `a` 时,会首先去 `fn3` 中查找,
如果还查找不到,会到 `fn1` 中查找。本题可以在 `fn3` 中找到变量 `a`,
但由于 `fn2()` 执行时,`a` 未赋值,打印 `undefined`

event loop执行顺序

1.先执行同步代码,所有同步代码都在主线程上执行,形成一个执行栈。

2.当遇到异步任务时,会将其挂起并添加到任务队列中,宏任务放入宏任务队列,微任务放进微任务队列。

3.当执行栈为空时,事件循环从任务队列中取出一个任务,加入到执行栈中执行。先执行微任务,后执行宏任务

4.重复上述步骤,直到任务队列为空。

  • 同步代码先执行:console.log()、new.Promise()、
  • 异步代码---微任务:Promise.then()、async/await、process.nextTick
  • 异步任务---宏任务:setTimeout、I/O

Promise

Promise 基础

  • promise状态没有发生改变时,返回的promise是pending状态【出现promise.then(()=>{})不会执行】
new Promise((resolve,reject)=>{
 console.log('promise1')
})
promise.then(()=>{
 console.log(2)  // 不会解析
})
console.log('1',promise1)

结果:

promise1
1 Promise{<pending>}

Promise+setTimeout

new Promise()中代码执行[从上到下执行],但是状态没有发生改变,则promise.then不会执行

const promise = new Promise((resolve, reject) => {
  console.log(1);       -----> 第1步,输出1
  setTimeout(() => {
    console.log("timerStart");-----> 第4步,输出timerStart
    resolve("success");
    console.log("timerEnd");-----> 第5步,输出timerEnd
  }, 0);
  console.log(2);  -----> 第2步,输出2
});
promise.then((res) => {
  console.log(res);-----> 第6步,输出success
});
console.log(4);  -----> 第3步,输出4

当setTimeout中有异步任务中的微任务时,会执行

setTimeout(() => {
  console.log('timer1');  -----> 第2步,输出timer1
  Promise.resolve().then(() => {
    console.log('promise')   -----> 第3步,输出promise
  })
}, 0)
setTimeout(() => {
  console.log('timer2')   -----> 第4步,输出timer2
}, 0)
console.log('start')  -----> 第1步,输出start

Promise.resolve().then(() => {
  console.log('promise1');  -----> 第2步,输出promise1
  const timer2 = setTimeout(() => {
    console.log('timer2')  -----> 第5步,输出timer2
  }, 0)
});
const timer1 = setTimeout(() => {
  console.log('timer1')   -----> 第3步,输出timer1
  Promise.resolve().then(() => {
    console.log('promise2')  -----> 第4步,输出promise2
  })
}, 0)
console.log('start');  -----> 第1步,输出start

Promise中的then、catch、finally

Promise的状态一经改变就不能再次改变

.then和.catch都会返回一个新的Promise,catch不管被连接到哪里,都能捕获上层未捕捉过的错误,且catch后面的还会继续执行

const promise = new Promise((resolve,reject)=>{
 reject('error')
 resolve('success')
})
promise.then(res=>{
 console.log('then1',res)
}).catch(err=>{
 console.log('catch',err)  --->第一步  catch error
}).then(res=>{
 console.log('then2',res)   --->第二步 then2 undefined
})

Promise的状态一经改变,并且有一个值,那么后续每次调用.then或者.catch时都会拿到该值

Promise.resolve(1)
  .then(res=>{
   console.log(res)   ---> 1
   return 2   //相当于返回 return Promise.resolve(2)
  }).then(res=>{
   console.log(res)  ---> 2
  })

在Promise中,返回任意一个非promise的值都会被包裹成promise对象

Promise.resolve().then(()=>{
 return new Error('error')   //相当于return Promise.resolve(new Error('error'))
}).then(res=>{
 console.log('then',res)  ----> then error
})
Promise.reject().then(()=>{
 return new Error('error')   //相当于return Promise.reject(new Error('error'))
}).then(res=>{
 console.log('then',res)
}).catch(err=>{
 console.log('catch',err)  ----> catch error
})

.then或.catch返回值不能是promise本身,会造成死循环

const promise = Promise.resolve().then(() => {
  return promise;
})
promise.catch(console.err)   // 报错:Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>

.then或.catch的参数期望是函数,传入非函数则会发生值透传

Promise.resolve(1)
  .then(2)    // 传入的是数字类型
  .then(Promise.resolve(3))  // 传入的是对象类型
  .then(console.log)  ----> 1

.then函数中的两个参数,第一个参数处理Promise 成功的函数,第二个处理失败的函数

Promise.reject('err')
.then((res)=>{
  console.log('success',res)
},(err)=>{
  console.log('error',err)   ----> error err
}).catch(err=>{
  console.log('catch',err)  // 不会执行
})
Promise.reject('err')
.then((res)=>{
  console.log('success',res)
}).catch(err=>{
  console.log('catch',err)  ---> catch err
})

Promise.all()接收一组异步任务,并行执行,等所有异步操作执行完后才回调

Promise.race()接收一组异步任务,并行执行,只保留第一个执行完成的异步操作结果

async/await【async中的await命令是一个Promise对象,返回该对象的结果】

async中的await会阻塞async后面的代码,跳出async执行后面的

async function async1(){
  console.log('1')   ---> 第一步 1
  await async2() 
  console.log('2')  --->  第四步 2
}
async function async2(){
 console.log('3')  --->第二步 3
}
async1()
console.log('4')  ---> 第三步 4

async + setTimeout

async function async1() {
  console.log("async1 start");  ---> 第一步 
  await async2();
  setTimeout(() => {
    console.log('timer1')  ---> 第七步 
  }, 0)
  console.log("async1 end");  ---> 第四步 
}
async function async2() {
  setTimeout(() => {
    console.log('timer2')  ---> 第五步 
  }, 0)
  console.log("async2");  ---> 第二步 
}
async1();
setTimeout(() => {
    console.log('timer3')  ---> 第六步 
  }, 0)
console.log("start")  ---> 第三步 

async中,await后面的内容是一个异常或者错误,会抛出错误,终止错误结果,不会继续向下执行

字节面试题

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2");
}
console.log("script start");
setTimeout(() => {
  console.log("setTimeout");
}, 0);
requestAnimationFrame(() => {
  console.log("requestAnimationFrame");
});
async1();
new Promise(resolve => {
  console.log("promise1");
  resolve();
}).then(() => {
  console.log("promise2");
});
console.log("script end");

执行步骤详解:

  1. console.log("script start");

    • 这是同步代码,立即执行。
    • 输出: script start
  2. setTimeout(() => { console.log("setTimeout"); }, 0);

    • setTimeout是一个宏任务。它被添加到Web API中,等待0毫秒后(实际上是尽可能快地)将其回调函数放入宏任务队列。
    • 当前状态: 宏任务队列:[setTimeout回调]
  3. requestAnimationFrame(() => { console.log("requestAnimationFrame"); });

    • requestAnimationFrame (RAF) 是一个特殊的异步任务,它不属于宏任务或微任务队列。它的回调会在浏览器下一次重绘之前执行。
    • 当前状态: RAF队列:[requestAnimationFrame回调]
  4. async1();

    • 调用async1函数,进入函数内部。

    • console.log("async1 start"); :同步代码,立即执行。

    • 输出: async1 start

    • await async2();

      • 调用async2函数,进入函数内部。
      • console.log("async2"); :同步代码,立即执行。
      • 输出: async2
      • async2函数执行完毕并返回一个resolved的Promise。await会暂停async1的执行,并将async1await后面的代码(即console.log("async1 end");)作为微任务添加到微任务队列。
    • 当前状态: 微任务队列:[async1剩余代码]

  5. new Promise(resolve => { console.log("promise1"); resolve(); }).then(() => { console.log("promise2"); });

    • Promise的构造函数是同步执行的。
    • console.log("promise1"); :同步代码,立即执行。
    • 输出: promise1
    • resolve()被调用,Promise状态变为resolved。.then()的回调函数(即console.log("promise2");)被添加到微任务队列。
    • 当前状态: 微任务队列:[async1剩余代码, promise2回调]
  6. console.log("script end");

    • 这是同步代码,立即执行。
    • 输出: script end

至此,所有同步代码执行完毕,调用栈清空。事件循环开始检查微任务队列。

  1. 执行微任务队列

    • 事件循环从微任务队列中取出第一个微任务:async1await后面的代码。
    • console.log("async1 end"); :执行。
    • 输出: async1 end
    • 事件循环继续从微任务队列中取出下一个微任务:promise2的回调。
    • console.log("promise2"); :执行。
    • 输出: promise2
    • 微任务队列清空。
  2. 浏览器渲染阶段 (如果有)

    • 在执行下一个宏任务之前,浏览器可能会进行一次渲染。此时,如果存在RAF回调,它们会在渲染之前被执行。
    • console.log("requestAnimationFrame"); :执行。
    • 输出: requestAnimationFrame
    • RAF队列清空。
  3. 执行宏任务队列

    • 事件循环从宏任务队列中取出第一个宏任务:setTimeout的回调。
    • console.log("setTimeout"); :执行。
    • 输出: setTimeout
    • 宏任务队列清空。

最终输出顺序:

script start
async1 start
async2
promise1
script end
async1 end
promise2
requestAnimationFrame
setTimeout