01 JS的基本概念
浏览器进程
一个页面的打开包括四个进程,而多个页面的时候浏览器进程、网络进程、GPU 进程等,这些都是通用的进程
每个标签页的都是独立的渲染进程,进程之间的资源( CPU、内存等)和行为( UI、逻辑等)互不共享,所以即便某个标签页崩溃了也不会影响其他标签页
而在每个标签页的渲染进程中有如下线程,各线程主要功能如图所示:
GUI渲染线程和JS引擎线程是互斥的【浏览器】Chrome浏览器的执行机制(内附相关知识点面经串联)
webWork就是应对JS的任务实在太重,造成阻塞了,JS线程就会向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM)这样里面不管如何翻天覆地都不会影响JS引擎主线程, 只待计算出结果后,将结果通信给主线程即可,perfect!
而且注意下,JS引擎是单线程的,这一点的本质仍然未改变,Worker可以理解是浏览器给JS引擎开的外挂,专门用来解决那些大量计算问题
当我们键入URI时,浏览器各进程之间的配合
数据类型
JS有以下八种数据类型:
原始值和引用值的定义方式很类似,都是创建一个变量然后赋值
但是在赋值后,能对这个变量做什么就很不同了
- 存储位置不同:原始值存储在栈内存中,引用值存储在堆内存中。
- 传递方式不同:原始值通过值传递的方式进行传递,引用值通过引用传递的方式进行传递。
- 比较方式不同:原始值通过值的比较进行比较,引用值通过引用的比较进行比较。
- 原始值不可变:原始值在被创建后无法修改,只能重新赋值。而引用值可以修改其属性和方法。
面试八股之:JS 的数据类型有哪些?数据类型的区别是什么?如何确定数据类型?
作用域
作用:让变量的名字只在某个范围内起作用,目的是提高程序的可靠性,更重要的是减少命名冲突
ES6之前
- 全局作用域
- 局部作用域(函数作用域)
全局作用域
概念:整个script标签 或者是一个单独的js文件都是全局作用域
特性:全局作用域中的对象在代码中任何位置都能访问,其生命周期伴随着页面的生命周期
函数作用域
概念:在函数内部定义的变量或者函数就是函数作用域
特点:定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。
ES6之后
- 全局作用域
- 局部作用域
- 块级作用域
块级作用域
概念:块级作用域就是使用一对大括号包裹的一段代码,比如函数、判断语句、循环语句,甚至单独的一个{}都可以被看作是一个块级作用域。
特点:代码块内部定义的变量在代码块外部是访问不到的,并且等该代码块中的代码执行完成之后,代码块中定义的变量会被销毁
//if块
if(1){}
//while块
while(1){}
//函数块
function foo(){}
//for循环块
for(let i = 0; i<100; i++){}
//单独一个块
{}
var let const
var和let声明的不是不同类型的变量,只是指出变量在相关作用域如何存在
注意:如果在函数内部没有声明 直接赋值的也是全局变量
var——ES5变量声明方式
- 在变量未赋值时,变量undefined
- 作用域——var的作用域为方法作用域
只要在方法内定义了,整个方法内定义变量前后的代码都可以使用,定义前使用var声明的变量会变量提升,不会报错,但输出为undefined
let——ES6变量声明方式
- 在声明变量前直接使用会报错
- 作用域——let的作用域为块作用域
- let不能重复声明变量,会报错;var可以,但是会覆盖
const——ES6变量声明方式
- const为常量声明方式,且声明时必须初始化赋值,在后面的代码中不能再修改该常量的值
- const实际上保证的并不是变量的值不能改,而是变量为引用数据类型时,指向的那个内存地址不得改动
面试八股之: let、const、var的区别?暂时性死区?
02 JS是如何执行的
一个例子
首先我们知道JavaScript是单线程,也就是说按照从上往下的顺序执行的,如下这段代码:
showName()
console.log(myname)
var myname = 'fyyy'
function showName() {
console.log('函数showName被执行');
}
按照顺序执行的话,showName()函数和myname变量都没有定义,所以执行都会报错
但实际结果是没有报错,所以我们知道了函数或者变量可以在定义之前使用 (因为有变量提升)
但如果删掉第三行代码,使用没有定义的变量或者函数就会执行报错
showName()
console.log(myname);
function showName() {
console.log('函数showName被执行');
}
所以我们可以得出一下结论:
- 在执行过程中,若使用了未声明的变量,那么 JavaScript 执行会报错。(删掉第三行代码)
- 在一个变量定义之前使用它,不会出错,但是该变量的值会为 undefined,而不是定义时的值。
- 在一个函数定义之前使用它,不会出错,且函数能正确执行。
第一个结论比较好理解,而第二三个结论就似乎和我们说js是按照顺序执行的前提条件矛盾
(小声bb:不是说顺序执行吗,为什么不报错?),没报错的原因就是变量提升
变量提升
在JavaScript中变量分为声明和赋值两步操作
var myname //声明部分
myname = 'fyyyy' //赋值部分
//等价于var myname = 'fyyyy'
函数也分为声明和赋值部分
//函数声明:完整的函数声明,不涉及到赋值操作
function foo(){
console.log('foo')
}
//函数表达式:先声明变量 bar再把函数赋值给bar,也可以写成箭头函数
var bar = function(){
console.log('bar')
}
var bar2 = ()=>{
console.log('bar2');
}
在了解完声明和赋值之后,所谓的变量提升就是在JavaScript代码执行过程中,把变量和函数的声明部分提升到代码开头的行为,变量被提升后会先默认赋值为undefined
//我们看到的形式
showName()
console.log(myname)
var myname = 'fyyy'
function showName() {
console.log('函数showName被执行');
}
/*
* 实际变量提升后
*/
//变量myname提升到开头,同时赋值为undefined,函数也提升到开头
var myname = undefined
function showName() {
console.log('showName被调用');
}
myname = 'fyyyy'
showName()
console.log(myname)
从概念的字面意义上来看,“变量提升”意味着变量和函数的声明会在物理层面移动到代码的最前面,但实际声明的位置在代码中并不会改变
变量提升是在编译阶段被 JavaScript引擎放入内存中,也就是说一段JavaScript代码不会直接进入执行阶段 而是先进入编译阶段,再进入执行阶段
JavaScript 执行流程图
JavaScript代码的执行流程
1.编译阶段
输入的一段JavaScript代码,在编译阶段经过编译后,会生成两部分内容:执行上下文和可执行代码
上面的例子,我的理解就是
//执行上下文部分
var myname = undefined
function showName() {
console.log('showName被调用');
}
//可执行代码
myname = 'fyyyy'
showName()
console.log(myname)
什么是执行上下文
执行上下文是JavaScript 执行一段代码时的运行环境或准备工作(说白了执行js就可以看成是要做一道菜,创建执行上下文就是要知道自己需要哪些食材嘛,而炒菜的具体的步骤,就是可执行代码嘛)
比如调用一个函数时(菜:土豆烧排骨),确定该函数在执行期间用到的this、变量、对象以及函数等(食材:土豆,排骨)
(哪些情况下代码才算是“一段”代码,才会在执行之前就进行编译并创建执行上下文呢?见下文调用栈部分)
执行上下文又包含变量环境和词法环境
可以简单记为变量环境放var变量,词法环境放let、const定义的变量和函数
变量环境可以简单理解为,在执行上下文中存在一个变量环境对象(Viriable Environment),这个对象里面保存了变量提升的内容,词法环境同理
结合一段代码来看如何生成变量环境对象和词法环境对象的:
showName()
console.log(myname)
var myname = 'fyyy'
function showName()
{
console.log('函数showName被执行');
}
- 第 1 行和第 2 行,由于这两行代码不是声明操作,所以 JavaScript 引擎不会做任何处理;
- 第 3 行,由于这行是经过 var 声明的,因此 JavaScript 引擎将在
环境对象中创建一个名为 myname 的属性,并使用 undefined 对其初始化; - 第 4 行,JavaScript 引擎发现了一个通过 function 定义的函数,所以它将函数定义存储到堆 (HEAP)中,并在环境对象中创建一个 showName 的属性,然后将该属性值指向堆中函数的位置
这样就生成了变量环境对象和词法环境
可执行代码
接下来 JavaScript 引擎会把声明以外的代码编译为字节码,字节码可以类比如下的模拟代码:
showName()
console.log(myname)
myname = 'fyyyy'
2 执行阶段
JavaScript 引擎开始执行“可执行代码”,按照顺序一行一行地执行
/*
* 可执行代码
*/
showName()
console.log(myname)
var myname = 'fyyyy'// 去掉var声明部分,保留赋值语句
function showName()
{
console.log('函数showName被执行');
}
- 首先showName(),JavaScript 引擎会在变量环境中查找这个函数,可以看到VariableEnvironment中存在showName的引用,所以JavaScript 引擎就开始执行这个函数并输出“函数 showName 被执行”结果。
- 接下来执行console.log(myname),JavaScript 引擎会在变量环境中查找这个变量,由于变量环境中存在这个变量,并且其值为 undefined,所以这时候就输出 undefined
- 接下来执行第 3 行,把“fyyyy”赋给 myname 变量,赋值后变量环境中的 myname 属性值改变为“fyyyy”
变量提升带来的问题
变量被覆盖掉
var myname = "xxxxx"
function showName(){
console.log(myname);
if(0){
var myname = "yyyyy"
}
console.log(myname);
}
showName()
通过调用栈分析,调用栈最顶端是showName的执行上下文,而showName又因为if块中的myname变量提升,所以执行上面这段代码,打印出来的是 undefined
应销毁的变量没有被销毁
function foo(){
for (var i = 0; i < 7; i++) {
}
console.log(i);
}
foo()
i 的值并未被销毁,最后打印出来的是 7。这同样也是由变量提升而导致的,在创建执行上下文阶段,变量 i 就已经被提升了,所以当 for 循环结束之后,变量 i 并没有被销毁。
如何解决变量提升带来的问题
ES6 引入了 let 和 const 关键字,从而使 JavaScript 也能像其他语言一样拥有了块级作用域。
那么let和const是如何和块级作用域结合的呢?看同样的两段代码
//var 的作用范围是整个函数
function varTest() {
var x = 1;
if (true) {
var x = 2; // 同样的变量!所以只生成了一个变量x
console.log(x); // 2
}
console.log(x); // 2
}
//改造后支持的块级作用域
function letTest() {
let x = 1;
if (true) {
let x = 2; // 不同的变量
console.log(x); // 2
}
console.log(x); // 1
}
因为 let 关键字是支持块级作用域的,所以在编译阶段,JavaScript 引擎并不会把 if 块中通过 let 声明的变量存放到letTest函数的执行上下文变量环境中,这也就意味着在 if 块中通过 let 声明的关键字,并不会提升到全函数可见。所以在 if 块之内打印出来的值是 2,跳出语块之后,打印出来的值就是 1 了
总的来说:块级作用域是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域了。
调用栈
在上文我们知道了执行上下文是JavaScript 执行一段代码时的运行环境或准备工作
而哪些情况下代码才算是“一段”代码,才会在执行之前就进行编译并创建执行上下文呢?一般来说有三种情况
- 全局执行上下文:代码开始执行时就会创建,将他压执行栈的栈底,每个生命周期内只有一份
- 函数执行上下文:当执行一个函数时,这个函数内的代码被编译,生成变量环境、词法环境等,当函数执行结束的时候该执行环境从栈顶弹出
- Eval执行上下文:用的比较少
什么是调用栈
JavaScript 中有很多函数,经常会出现在一个函数中调用另外一个函数的情况,调用栈就是用来管理函数调用关系的一种数据结构。
这段代码的调用栈如下图所示:
在执行左边这段代码之前
- JavaScript 引擎会为上面这段代码创建全局执行上下文,包含了声明的函数和变量(如右图的全局执行上下文内容所示)
- 执行上下文准备好之后,便开始执行全局代码,当执行到 func 这儿时,JavaScript 判断这是一个函数调用,那么又将执行以下操作:
- 首先,从全局执行上下文中,取出 func 函数代码。
- 其次,对 func 函数的这段代码进行编译,并创建该函数的执行上下文和可执行代码。(如右图执行栈中的func执行上下文所示)
- 最后,执行代码,输出结果。
就这样,当执行到 func 函数的时候,我们就有了两个执行上下文了——全局执行上下文和 func 函数的执行上下文,执行上下文通过调用栈这种数据结构来管理。
什么是栈溢出
而调用栈是有大小的,当入栈的执行上下文超过一定数目,JavaScript 引擎就会报错,我们把这种错误叫做栈溢出。
简单来说:栈溢出就是一直往里面压,没有被弹出,没有被垃圾回收,一直压到最大容量,就会造成栈溢出(写递归或者死循环)
参考文章:
字节前端初阶训练营