前言
讲根底不容易,本文心愿通过 9 个 demo 和 18 张图,和大家一起学习或温故 JavaScript 执行机制,本文纲要:
- hoisting 是什么
- 一段 JavaScript 代码是怎么被执行的
- 调用栈是什么
文末有总结大图。
如果对本文有什么疑难或发现什么错漏的中央,可在评论区留言~
本文是夯实根底系列的上篇,请关注接下来的中篇~
如果对你有帮忙,心愿三连~
hoisting 是什么
先来个总结图压压惊~
注释开始~
发问环节:上面这段代码打印什么?为什么?
showSinger()
console.log('第 1 次打印:', singer)
var singer = 'Jaychou'
console.log('第 2 次打印:', singer)
function showSinger() {console.log('showSinger 函数')
}
答案是:
showSinger 函数失常执行,第 1 次打印 singer 变量是 undefined,第 2 次打印 singer 变量是 Jaychou,看上去 var 变量 singer 和函数申明 showSinger 被晋升了,像是上面的模仿:
// 函数申明被晋升了
function showSinger() {console.log('showSinger 函数')
}
// var 变量被晋升了
var singer = undefined
showSinger()
console.log('第 1 次打印:', singer) // undefined
singer = 'Jaychou'
console.log('第 2 次打印:', singer) // Jaychou
在 JavaScript 里,这种景象被称为 hoisting 晋升:var 申明的变量会晋升和函数申明会晋升,在执行代码之前会先被增加到执行上下文的顶部。
对于晋升的细节
-
let 变量和 const 变量不会被晋升,只能在申明变量之后能力应用,申明之前被称为“暂时性死区”,以下代码会报错:
console.log('打印:', singer) let singer = 'Jaychou'
- 在全局执行上下文申明的 var 变量会成为 window 对象的属性,let 变量和 const 变量不会
var singer = 'Jaychou'
console.log(window.singer) // Jaychou
let age = 40
console.log(window.age) // undefined
-
var 申明是函数作用域,let 申明和 const 申明是块作用域
if (true) { var singer = 'Jaychou' console.log(singer) // Jaychou } console.log(singer) // Jaychou if (true) { let age = 40 console.log(age) // 40 } // 报错:Uncaught ReferenceError: age is not defined console.log(age);
-
let 不容许同一个块作用域中呈现冗余申明,会报错,var 申明则容许有反复申明
let age; // Uncaught SyntaxError: Identifier 'age' has already been declared var age; var age = 10 var age = 20 var age = 30 console.log(age) // 失常打印 30
- 函数申明会被晋升,函数表达式不会(除了函数什么时候真正有定义这个区别之外,这两种语法是等价的)
// 没有问题,因为 sum 函数有被晋升
console.log(sum(10, 10));
// 函数申明
function sum(num1, num2) {return num1 + num2;}
// 会报错:Uncaught TypeError: sum1 is not a function
// 因为 sum1 函数不会被晋升
console.log(sum1(10, 10));
// 函数表达式
var sum1 = function (num1, num2) {return num1 + num2;};
晋升产生在什么时候
都在说晋升,那这个步骤是产生在什么时候?执行代码之前吗?
这就引出了上面这个问题:一段 JavaScript 代码是怎么被执行的?
一段 JavaScript 代码是怎么被执行的
发问环节:上面的 html 里的 JavaScript 代码是怎么被执行的?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
showSinger()
var singer = 'Jaychou'
console.log(singer)
function showSinger() {console.log('showSinger 函数')
}
</script>
</body>
</html>
简述:html 和 css 局部会被浏览器的 渲染引擎 拿来渲染,进行 计算 dom 树、计算 style、计算布局、计算分层、计算绘制等等
一系列的渲染操作,而 JavaScript 代码的执行由 JavaScript 引擎 负责。
市面上的 JavaScript 引擎有很多,例如 SpiderMonkey、V8、JavaScriptCore 等,能够简略了解成 JavaScript 引擎将人类可能了解的编程语言 JavaScript,翻译成机器可能了解的机器语言,大抵流程是:
是的,在执行之前,会有 编译阶段,而不是间接就执行了。
编译阶段
输出一段代码,通过编译后,会生成两局部内容:执行上下文 和可执行代码,执行上下文就是方才提的那个执行上下文,它是执行一段 JavaScript 代码时的运行环境。
执行上下文具体的分类和对应的创立机会如下:
而执行上下文具体包含什么内容,怎么寄存方才提的 var 申明变量、函数申明、以及 let 和 const 申明变量请看:
执行上下文案例剖析
联合上面的案例来具体分析:
var a = 1 // var 申明
let b = 2 // let 申明
{
let b = 3 // let 申明
var c = 4 // var 申明
let d = 5 // let 申明
console.log(a)
console.log(b)
}
console.log(b)
console.log(c)
// 函数申明
function add(num1, num2){return num1 + num2}
第一步是编译下面的全局代码,并创立全局执行上下文:
- var 申明的变量在编译阶段放到了变量环境,例如 a 和 c;
- 函数申明在编译阶段放到了变量环境,例如 add 函数;
- let 申明的变量在编译阶段放到了词法环境,例如 b(不包含其外部的块作用域)
- 外部的块作用域的 let 申明还没做解决
接下来是执行代码,执行代码到块 {}
外面时,a 已被设置成 1,b 已被设置成 2,块作用域里的 b 和 d 作为新的一层放在词法环境里
词法环境外部的小型栈构造,栈底是函数最外层的变量,进入一个块作用域后,就把该块作用域外部的变量压到栈顶;当作用域执行实现之后,该作用域的信息就会从栈顶弹出。
继续执行,执行到 console.log(a);console.log(b);
时,进入 变量查找 过程:沿着词法环境的栈顶向下查问,如果在词法环境中的某个块中查找到了,就间接返回给 JavaScript 引擎,如果没有查找到,那么持续在变量环境中查找,所以块作用域外面的 b 会找到 3:
当块作用域执行完结之后,其外部定义的变量就会从词法环境的栈顶弹出,最终执行上下文如下:
这个过程不分明的同学能够多看几次案例,有不明确的能够在评论区探讨~
调用栈是什么
方才聊到,函数执行上下文的创立机会在函数 被调用时 ,它的过程是 取出函数体的代码》对这段代码进行编译》创立该函数的执行上下文和可执行代码》执行代码输入后果
,其中编译和创立执行上下文的过程和方才演示的对全局代码的解决相似。
而调用栈就是用来 治理函数调用关系的一种数据结构,在执行上下文创立好后,JavaScript 引擎会将执行上下文压入栈中。
调用栈案例剖析
var a = 2
function add(b, c) {return b + c}
function addAll(b, c) {
var d = 10
var result = add(b, c)
return a + result + d
}
addAll(3, 6)
第一步,创立全局上下文,并将其压入栈底
接下来执行代码,a = 2 把 a 从 undefined 设为 2
第二步,调用 addAll 函数,会编译该函数,并为其创立一个执行上下文,将该函数的执行上下文压入栈中
接下来执行 addAll 函数的代码,把 d 置为 10,而后执行 add 函数
第三步,调用 add 函数,为其创立执行上下文,并压入栈中
当 add 函数返回时,该函数的执行上下文就会从栈顶弹出,并将 result 的值设置为 add 函数的返回值,也就是 9
addAll 执行最初一个相加操作后并返回,addAll 的执行上下文也会从栈顶部弹出,此时调用栈中就只剩下全局上下文
这就是调用栈经验的过程~
而平时开发过程中,打断点调试就能够看到 Call Stack 调用栈了,比方方才的 add 函数里打个断点:
总结
本文串联了申明晋升、JavaScript 编译和执行、调用栈,来讲述 JavaScript 执行机制,心愿有帮忙到大家~
本文是夯实根底系列的上篇,预报 正在码字的中篇:作用域链 + 闭包 + this。