# JavaScript 代码的执行流程
实际上变量和函数声明在代码里的位置是不会改变的,而且是在编译阶段被 JavaScript 引擎放入内存中。
# 编译阶段
变量提升部分的代码。
执行部分的代码。
经过编译后,会生成两部分内容:执行上下文(Execution context)和可执行代码。
# 变量提升(Hoisting)
所谓的变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。
# 解决变量提升
ES6 通过引入 let、 const 创建一个块级作用域,解决的变量提升造成的种种问题。
实现:在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出。
# 执行阶段
执行上下文是 JavaScript 执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等。
但有以下三种情况时,会出现执行上下文:
- 当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。
- 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。
- 当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行上下文。
在执行 JavaScript 时,可能会存在多个执行上下文,js 通过一个 栈 进行管理
在执行上下文创建好后,JavaScript 引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈。
原始类型的数据值都是直接保存在“栈”中的,引用类型的值是存放在“堆”中的。但是闭包中的变量是保存在堆中的。
也就是栈底永远会是全局上下文,遵循着先进后出的机制。
# 作用域链
每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为outer。如果在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找。我们把这个查找的链条就称为作用域链。动态作用域
# 词法作用域
在 JavaScript 执行过程中,其作用域链是由词法作用域决定的。
词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
词法作用域是代码阶段就决定好的,和函数是怎么调用的没有关系。
# 闭包
闭包其实无处不在,这次 携带着代码讲解下
function foo() {
var myName = " 极客时间 ";
let test1 = 1;
const test2 = 2;
var innerBar = {
getName: function() {
console.log(test1);
return myName;
},
setName: function(newName) {
myName = newName;
},
};
return innerBar;
}
var bar = foo();
bar.setName(" 极客邦 ");
bar.getName();
console.log(bar.getName());
根据词法作用域的规则,内部函数 getName 和 setName 总是可以访问它们的外部函数 foo 中的变量,所以当 innerBar 对象返回给全局变量 bar 时,虽然 foo 函数已经执行结束,但是 getName 和 setName 函数依然可以使用 foo 函数中的变量 myName 和 test1。
而这个变量保存的地方,别人都无法访问到,是一个 setName 和 getName 函数的专属背包,我们把这个背包称之为 foo 函数的闭包
定义:在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。
# 闭包原理
function foo() {
var d = 20;
return function inner(a, b) {
const c = a + b + d;
return c;
};
}
const f = foo();
在执行全局代码时,V8 会将全局执行上下文压入到调用栈中,然后进入执行 foo 函数的调用过程,这时候 V8 会为 foo 函数创建执行上下文,执行上下文中包括了变量 d,然后将 foo 函数的执行上下文压入栈中,foo 函数执行结束之后,foo 函数执行上下文从栈中弹出,这时候 foo 执行上下文中的变量 d 也随之被销毁。
所以正常的处理方式应该是 foo 函数的执行上下文虽然被销毁了,但是 inner 函数引用的 foo 函数中的变量却不能被销毁,那么 V8 就需要为这种情况做特殊处理,需要保证即便 foo 函数执行结束,但是 foo 函数中的 d 变量依然保持在内存中,不能随着 foo 函数的执行上下文被销毁掉。
V8 引入预解析器,比如当解析顶层代码的时候,遇到了一个函数,那么预解析器并不会直接跳过该函数,而是对该函数做一次快速的预解析,其主要目的有两个:
- 判断当前函数是不是存在一些语法上的错误
- 检查函数内部是否引用了外部变量,如果引用了外部的变量,预解析器会将栈中的变量复制到堆中
# 闭包是怎么回收的
如果引用闭包的函数是一个全局函数,那么闭包会一直存在到页面关闭。但如果这个闭包不在使用的话,就会造成内存泄露。
如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。
# This
this 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 this。
全局上下文中的 this 就指向 window
函数执行上下文中的 this
- 在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window。
- 通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身。
# this 缺点
- 嵌套函数中的 this 不会从外层函数中继承
- ES6 中的箭头函数来解决
- 声明一个变量 self 用来保存 this
- 普通函数中的 this 默认指向全局对象 window
- 严格模式 ,其函数的执行上下文中的 this 值是 undefined
- call、apply、bind