实际上变量和函数声明在代码里的位置是不会改变的,而且是在编译阶段被 JavaScript 。
执行上下文是 JavaScript 执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等,在执行上下文中存在一个变量环境的对象(Viriable Environment)这样就生成了变量环境对象。接下来 JavaScript 引擎会把声明以外的代码编译为字节码
执行上下文
由变量环境、词法环境、可执行代码环境组成,在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer。
调用栈
调用栈就是用来管理函数调用关系的一种数据结构。
JavaScript 的调用栈JavaScript 引擎正是利用栈的这种结构来管理执行上下文的。在执行上下文创建好后,JavaScript 引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈。
调用栈是 JavaScript 引擎追踪函数执行的一个机制,当一次有多个函数被调用时,通过调用栈就能够追踪到哪个函数正在被执行以及各函数之间的调用关系。在分析复杂结构代码,或者检查 Bug 时,调用栈都是非常有用的。
除了通过断点来查看调用栈,你还可以使用 console.trace()
来输出当前的函数调用关系
解决栈溢出问题
function foo() { setTimeout(foo, 0) } foo()
foo像setTimeout、setInterval、promise 这样的全局函数不是js 的一部分,而是webapi 部分。当遇到 webApi 时,会将其回调函数(foo)交给webapis 处理,此时 调用栈 中foo 函数执行完毕,出栈,栈为空; 回调函数会被发送到任务队列中,等待eventloop事件循环将其捞出重新放入到堆栈中…
参考:https://juejin.im/post/5d2d146bf265da1b9163c5c9#heading-15
作用域(scope)
作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。ES6之前只有两种:全局作用域和函数作用域。ES6 为了解决变量提升带来的缺陷引入了 let 和 const 关键字,形成块作用域。
JavaScript 是如何支持块级作用域的
由于 JavaScript 的变量提升存在着变量覆盖、变量污染等设计缺陷,所以 ES6 引入了块级作用域关键字来解决这些问题。
块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域了。
在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。需要注意下,我这里所讲的变量是指通过 let 或者 const 声明的变量。这个区域中的变量并不影响作用域块外面的变量。查找一个变量的时候,沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。
作用域链
每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer。当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量,如果在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找。我们把这个查找的链条就称为作用域链。
bar 函数和 foo 函数的 outer 都是指向全局上下文的,这也就意味着如果在 bar 函数或者 foo 函数中使用了外部变量,那么 JavaScript 引擎会去全局执行上下文中查找。
词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。
词法作用域和动态作用域 1、作用域: 作用域是指程序代码中定义变量的区域,JavaScript采用词法作用域,也就是静态作用域. 2、词法作用域和动态作用域 因为JavaScript采用的是词法作用域,函数的作用域在函数定义的时候就决定了。 而与词法作用域对应的是动态作用域,函数的作用域是在函数调用的时候才决定的。 动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心他们从何处调用。 换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。
产生闭包的核心有两步:第一步是需要预扫描内部函数;第二步是把内部函数引用的外部变量保存到堆中。在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。
从图中可以看出来,当调用 bar.getName 的时候,右边 Scope 项就体现出了作用域链的情况:Local 就是当前的 getName 函数的作用域,Closure(foo) 是指 foo 函数的闭包,最下面的 Global 就是指全局作用域,从“Local–>Closure(foo)–>Global”就是一个完整的作用域链。
闭包是怎么回收的
如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。
通常,如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。
this只有这三种:
new CreateObj()
执行 new CreateObj() 的时候,JavaScript 引擎做了如下四件事:
var tempObj = {} CreateObj.call(tempObj) return tempObj
var myObj = { name : "极客时间", showThis: function(){ console.log(this) function bar(){console.log(this)} bar() } } myObj.showThis()
this总结:
最后,我们还提了一下箭头函数,因为箭头函数没有自己的执行上下文,所以箭头函数的 this 就是它外层函数的 this