1.浏览器中的Javascript执行机制
Javascript代码的执行顺序
- 在执行过程中,若使用了未声明的变量,那么 JavaScript 执行会报错。
- 在一个变量定义之前使用它,不会出错,但是该变量的值会为 undefined,而不是定义时的值。
- 在一个函数定义之前使用它,不会出错,且函数能正确执行。
变量提升
- 在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。
- 实际上变量和函数声明在代码里的位置是不会改变的,而且是在编译阶段被 JavaScript 引擎放入内存中。
- 一段 JavaScript 代码在执行之前需要被 JavaScript 引擎编译,编译完成之后,才会进入执行阶段。
编译阶段
一段js代码 -> 编译 -> 执行
- 一段代码可以分成两部分:
- 变量提升的部分
- 执行上下文 : js执行一段代码时的运行环境 :this、变量、对象、函数
- 变量环境
- 在编译阶段存放
var
声明的变量和函数
- 在编译阶段存放
- 词法环境
- 在编译阶段存放
let / const
声明的变量 - 作用域块中通过 let 声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量,比如在作用域外面声明了变量 b,在该作用域块内部也声明了变量 b,当执行到作用域内部时,它们都是独立的存在。
- 词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。
- 查找变量的方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。
- 在编译阶段存放
- 变量环境
- 执行上下文 : js执行一段代码时的运行环境 :this、变量、对象、函数
- 执行部分的代码
- 可执行代码
- 执行过程:
- 当执行到某个函数时,JavaScript 引擎便开始在变量环境对象中查找该函数 由于变量环境对象中存在该函数的引用,所以 JavaScript 引擎便开始执行该函数,并输出“函数xx被执行”结果。
- 当执行到某个变量时,JavaScript 引擎继续在变量环境对象中查找该对象,由于变量环境存在该变量,并且其值为 undefined,所以这时候就输出 undefined。
- 当执行到某个赋值语句时,把xx赋给该变量,赋值后变量环境中的该属性值改变为xx
- 可执行代码
- 变量提升的部分
- 同名现象:
- 一段代码如果定义了两个相同名字的函数,那么最终生效的是最后一个函数。
- 如果遇到函数时,发现变量对象中已有同名属性,则函数会覆盖这个属性。
- 相反地如果编译遇到var 声明变量时且变量对象中已有同名属性,则会忽略这个var,不会覆盖这个属性。
- 函数提升要比变量提升的优先级要高一些,且不会被变量声明覆盖,但是会被变量赋值之后覆盖。
执行上下文
- 当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。
- 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。
- 当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行上下文。
调用栈
- 调用栈就是用来管理函数调用关系的一种数据结构。
- 函数调用
- 即运行一个函数
- 具体流程:
- 当执行上下文准备好之后(代码中全局变量和函数都保存在全局上下文的变量环境中。),开始执行全局代码,当执行到某个函数时,js判断这是一个函数调用,那么便执行以下操作:
- 首先,从全局执行上下文中,取出该函数代码。
- 其次,对该函数的这段代码进行编译,并创建该函数的执行上下文和可执行代码。
- 最后,执行代码,输出结果。
- 就这样,当执行到该函数的时候,我们就有了两个执行上下文了——全局执行上下文和该函数的执行上下文。
在执行 JavaScript 时,可能会存在多个执行上下文。JavaScript 引擎通过一种叫栈的数据结构来管理这些执行上下文。
- js调用栈(call stack)
- JavaScript 引擎正是利用栈的这种结构来管理执行上下文的。在执行上下文创建好后,JavaScript 引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈。
- 调用栈是 JavaScript 引擎追踪函数执行的一个机制:
- 当一次有多个函数被调用时,通过调用栈就能够追踪到哪个函数正在被执行以及各函数之间的调用关系。
var a = 2
function add(b,c){
return b+c
}
function addAll(b,c){
var d = 10
result = add(b,c)
return a+result+d
}
addAll(3,6)
-
- 执行流程:
- 第一步,创建全局上下文,并将其压入栈底。
- 变量 a、函数 add 和 addAll 都保存到了全局上下文的变量环境对象中。
- 全局执行上下文压入到调用栈后,JavaScript 引擎便开始执行全局代码了。首先会执行 a=2 的赋值操作,执行该语句会将全局上下文变量环境中 a 的值设置为 2。
- 第二步是调用 addAll 函数。
- 当调用该函数时,JavaScript 引擎会编译该函数,并为其创建一个执行上下文,最后还将该函数的执行上下文压入栈。
- addAll 函数的执行上下文创建好之后,便进入了函数代码的执行阶段了。
- 这里先执行的是 d=10 的赋值操作,执行语句会将 addAll 函数执行上下文中的 d 由 undefined 变成了 10。
- 第三步,当执行到 add 函数调用语句时,同样会为其创建执行上下文,并将其压入调用栈。
- 当 add 函数返回时,该函数的执行上下文就会从栈顶弹出,并将 result 的值设置为 add 函数的返回值,也就是 9。
- 紧接着 addAll 执行最后一个相加操作后并返回,addAll 的执行上下文也会从栈顶部弹出,此时调用栈中就只剩下全局上下文了。
- 整个 JavaScript 流程执行结束了。
在开发中,如何利用好调用栈
- 利用浏览器查看调用栈的信息
- 当在执行一段复杂代码的时候,可能很难从代码文件中分析其调用关系,这时候可以在想要查看的函数中加入断点,当执行到该函数的时候(执行流程就暂停了,这时可以通过右边“call stack”来查看当前的调用栈的情况),就可以查看该函数的调用栈了。
- 除了通过断点来查看调用栈,你还可以使用 console.trace() 来输出当前的函数调用关系,比如在示例代码中的 add 函数里面加上了 console.trace(),你就可以看到控制台输出的结果。
- 栈溢出(Stack Overflow)
- 调用栈是有大小的,当入栈的执行上下文超过一定数目,JavaScript 引擎就会报错,我们把这种错误叫做栈溢出。
- 调用栈有两个指标,最大栈容量和最大调用深度,
- 特别是在写递归代码的时候,就很容易出现栈溢出的情况。
- 超过了最大栈调用大小(Maximum call stack size exceeded)。
- 可以使用一些方法来避免或者解决栈溢出的问题,比如把递归调用的形式改造成其他形式,或者使用加入定时器的方法来把当前任务拆分为其他很多小任务。
块级作用域
- 作用域(scope)
- 作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。
- ES6之前:
- 全局作用域:全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
- 函数作用域:函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。
- 变量提升的问题
- 变量容易在不被察觉的情况下被覆盖掉
- 总结:
- 块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现。
- 变量提升:
- var的创建和初始化被提升,赋值不会被提升。
- let的创建被提升,初始化和赋值不会被提升。
- function的创建、初始化和赋值均会被提升。
作用域链
- 在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer。
- 查找变量的查找链条就称为作用域链。
词法作用域
- 词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
- 所以在 JavaScript 执行过程中,其作用域链是由词法作用域决定的。
- 词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。
闭包
- 在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。
- JavaScript 引擎会沿着“当前执行上下文–>函数闭包–> 全局执行上下文”的顺序来查找变量。
- 调用内部函数时(如果函数包括对变量的修改),会修改外部函数闭包中的变量的值。
- 外部函数执行完成之后,其执行上下文从栈顶弹出了,但是由于返回的内部函数中使用了该外部函数内部的变量,所以这个变量依然保存在内存中。这像极了该内部函数背的一个专属背包,无论在哪里调用了该内部函数,它们都会背着这个外部函数的专属背包。之所以是专属背包,是因为除了该内部函数之外,其他任何地方都是无法访问该背包的,我们就可以把这个背包称为该外部函数的闭包。
this
- 在对象内部的方法中使用对象内部的属性是一个非常普遍的需求。但是 JavaScript 的作用域机制并不支持这一点,基于这个需求,JavaScript 又搞出来另外一套 this 机制。
- 执行上下文:变量环境、词法环境、outer(外部引用)、this。
- this 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 this。
- 全局执行上下文中的 this
- 全局执行上下文中的 this 是指向 window 对象的。这也是 this 和作用域链的唯一交点,作用域链的最底端包含了 window 对象,全局执行上下文中的 this 也是指向 window 对象。
- 函数执行上下文中的 this
- 默认情况下调用一个函数,其执行上下文中的 this 也是指向 window 对象的。
- 设置函数执行上下文中的 this 值:
- 通过函数的 call 方法设置:通过函数的 call 方法来设置函数执行上下文的 this 指向。还可以使用 bind 和 apply 方法。
- 通过对象调用方法设置:使用对象来调用其内部的一个方法,该方法的 this 是指向对象本身的。
- 通过构造函数中设置:new CreateObj()
- this设计的缺陷
- 嵌套函数中的 this 不会从外层函数中继承
var myObj = {
name : "a",
showThis: function(){
console.log(this)
function bar(){console.log(this)}
bar()
}
}
-
* 函数 bar 中的 this 指向的是全局 window 对象,而函数 showThis 中的 this 指向的是 myObj 对象。 * 可以通过一个小技巧来解决这个问题 * 声明一个变量 self 用来保存 this
var myObj = {
name : "a",
showThis: function(){
console.log(this)
var self = this
function bar(){
self.name = "b"
}
bar()
}
}
-
- 这个方法的的本质是把 this 体系转换为了作用域的体系。
- 也可以使用 ES6 中的箭头函数来解决这个问题:
var myObj = {
name : "a",
showThis: function(){
console.log(this)
var bar = ()=>{
this.name = "b"
console.log(this)
}
bar()
}
}
-
- ES6 中的箭头函数并不会创建其自身的执行上下文,所以箭头函数中的 this 取决于它的外部函数。
- this 没有作用域的限制,这点和变量不一样,所以嵌套函数不会从调用它的函数中继承 this。
- 要解决这个问题,有两种思路:
- 第一种是把 this 保存为一个 self 变量,再利用变量的作用域机制传递给嵌套函数。
- 第二种是继续使用 this,但是要把嵌套函数改为箭头函数,因为箭头函数没有自己的执行上下文,所以它会继承调用函数中的 this。
- 普通函数中的 this 默认指向全局对象 window。
- 函数执行上下文中的 this 默认指向全局对象,因为这样会打破数据的边界,造成一些误操作。
- 如果要让函数执行上下文中的 this 指向某个对象,最好的方式是通过 call 方法来显示调用。
- 通过设置 JavaScript 的“严格模式”来解决。在严格模式下,默认执行一个函数,其函数的执行上下文中的 this 值是 undefined,这就解决上面的问题了。
- setTimeOut
- setTimeOut() 函数内部的回调函数,this指向全局函数。
- 如果被setTimeout推迟执行的回调函数是某个对象的方法,那么该方法中的this关键字将指向全局环境,而不是定义时所在的那个对象。
笔记内容来自极客时间李兵老师的《浏览器工作原理与实践》 学习收获了很多 感谢老师