通过JavaScript执行机制去学习闭包,执行上下文,作用域,作用域链。

Posted 三水草肃

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了通过JavaScript执行机制去学习闭包,执行上下文,作用域,作用域链。相关的知识,希望对你有一定的参考价值。

函数执行中的变量和函数:

  1. 在执行过程中,若使用了未声明的变量,那么 javascript 执行会报错。
  2. 在一个变量定义之前使用它,不会出错,但是该变量的值会为 undefined,而不是定义时的值。
  3. 在一个函数定义之前使用它,不会出错,且函数能正确执行。

下面是关于同名变量和函数的两点处理原则:

  1. 如果是同名的函数,JavaScript编译阶段会选择最后声明的那个。
  2. 如果变量和函数同名,那么在编译阶段,变量的声明会被忽略

变量提升:JS代码在执行过程中,JS去引擎把变量的声明部分和函数的声明部分提升到了代码开头的行为,变量被提升后,会给变量设置默认值,这个默认值就是undefined
在定义之前使用变量和函数是因为:变量和函数在执行之前都提升到了代码开头

下面代码的原因是

  1. 编译阶段:遇到一个函数,把函数体存放到变量环境中,接下来遇到同名函数,继续存放到变量环境中,但是变量环境中已经存在一个同样的函数了。那么后面的函数体覆盖前面的函数体
  2. 执行阶段:限制性第一函数,从变量环境中查找asdasd函数,变量中只保存了第二个函数,所以最终调用的是第二个函数了。 打印两个 1
  3. 一段代码如果定义了两个相同名字的函数,那么最终生效的是最后一个函数。
function asdasd() 
    console.log(2)

asdasd() // 1
function asdasd() 
    console.log(1)



asdasd() // 1

下面的是因为如果变量和函数同名,那么在编译阶段,变量的声明会被忽略

```javascript
showName() // 1
var showName = function() 
    console.log(2)

function showName() 
    console.log(1)

总结:

  1. js代码执行过程中,需要先做变量提升,而之所以做变量提升,是因为js代码在执行之前需要先编译
  2. 在编译阶段,变量和函数会被存放到变量环境中,变量的默认值会被设置为undefined,代码执行阶段,js引擎会从变量环境中查找自定义的变量和函数
  3. 在编译阶段,存在两个相同的函数,那么最终存在变量环境中的最后定义的那个。这是因为后面的会覆盖掉之前定义的
  4. 先编译,后执行。
  5. 函数提升要比变量提升的优先级要高一些,且不会被变量声明覆盖,但是会被变量赋值之后覆盖。

执行之前就进行编译创建执行上下文:

  1. 当JS执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且再整个页面的生存周期内,全局执行上下文只有一份
  2. 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁
  3. 当执行eval函数的时候,eval的代码也会被编译,并创建执行上下文
var a = 2 
 function add()
 var b = 10
 return a+b
 
 add()
  1. 首先,从全局执行上下文,取出add函数代码
  2. 其次,对add函数的这段代码进行编译,并创建该函数的执行上下文和可执行代码
  3. 最后,执行代码,输出结果
  4. 一共有两个执行上下文 — 全局执行上下文, add函数的执行上下文。他们是通过栈的数据结果来管理的

– 就这样,当执行到 add 函数的时候,我们就有了两个执行上下文了——全局执行上下文和 add 函数的执行上下文。他们是通过栈的数据结果来管理的

JS函数执行机制

  1. 每调用一个函数,JS引擎会为其创建执行上下文,并把该执行上下文压入调用栈,然后JS引擎开始执行函数代码
  2. 如果在一个函数A中调用了另外一个函数B,那么JS引擎会为B函数创建执行上下文,并将B函数的执行上下文压入栈顶
  3. 当前函数执行完毕后,JS引擎会将该函数的执行上下文弹出栈
  4. 当分配的调用栈空间被占满后,会引发 堆栈溢出 问题

作用域:

作用域:作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期,通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期

  1. 全局作用域:对象在代码中的任何地方都可以被访问,其生命周期随着页面的生命周期
  2. 函数作用域:函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问,函数执行结束之后,函数内部定义的变量会被销毁
  3. 块级作用域:就是使用一对大括号包裹的一段代码,比如函数判断语句,循环语句,单独的一个 都可以看作是块级作用域

变量提升所带来的问题

  1. 由于变量提升问题,使用JS来编写和其它语言相同逻辑的代码,都有可能导致不一样的执行结果
  2. 变量容易在不被察觉的情况下被覆盖掉
  3. 函数执行上下文中有变量就是用函数上下文中的变量

ES6如何解决变量提升带来的缺陷

  1. 引入了let和const关键字,有了块级作用域。作用域块声明的变量不影响块外面的变量
 var myname = " 极客时间 "
function showName()
  console.log(myname);// undefined 主要是if判断里面的var变量提升了,但是没有执行里面的赋值,所以是undefined.
  console.log(this.myname); //  极客时间 "
  if(0)
   var myname = " 极客邦 "
  
  console.log(myname); // undefined

showName()
// 因为 showName的函数执行上下文中,函数执行上下文中有变量就是用函数上下文中的变量
function foo()
    for (var i = 0; i < 7; i++) 
    
  console.log(i); 
  

  foo() // 7 7 7 7 7 7 7
  function varTest() 
    var x = 1;
    if (true) 
      var x = 2;  // 同样的变量!
      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
  

  function fooaa()
    var a = 1 // 变量环境
    let b = 2 // 词法环境
    
      let b = 3 // 词法环境
      var c = 4 // 变量环境
      let d = 5 // 词法环境
      console.log(a,'-') // 1
      console.log(b,'-') // 3
    
    console.log(b,'-') // 2
    console.log(c,'-') //4
    // console.log(d,'-') //  d is not defined 
   
fooaa()

JavaScript 引擎是如何同时支持变量提升和块级作用域的。

  1. 块级作用域通过词法环境的栈结构来实现的,变量提升是通过变量环境来实现的。两者结合,JS引擎支持变量提升和块级作用域
  2. 函数内部通过var声明的变量,在编译阶段全都存放到变量环境里了
  3. 通过let声明的变量,在编译阶段会被存放到词法环境中
  4. 在函数的作用域中,通过let声明的变量并没有被存放到词法环境中
  5. 查找流程:如果在词法环境中找到了,就直接返回给JS引擎,如果没有查找到,那么继续在变量环境中查找。

理解词法作用域和动态作用域:

  • 词法作用域等同于静态作用域,被创建的时候就确定好了。JS遵循的就是词法作用域
  • 动态作用域:this是javascript中的关键字,能实现类似于动态作用域的效果。函数运行时其内部自动生成this对象。

理解JavaScript的作用域和作用域链:

  1. 作用域是运行时代码的某些特定部分中变量,函数和对象的可访问性:作用域是一个独立的地盘,让变量不会外泄,暴露出去。隔离变量,不同作用域下的同名变量不会有冲突。
    全局作用域,函数作用域,块级作用域
  2. 作用域链:如果当前没有寻找到变量,变一层一层的向上寻找,直到找到全局作用域还是没有找到, 就是作用域链。

作用域链和闭包:

  • 每个执行上下文的变量环境,都包含了一个外部引用,用来指向外部的执行上下文。我们把这个外部引用称为outer,当一段代码用了一个变量,js引擎首先会在 “当前的执行上下文”中查找该变量。
  • 作用域链:每一个函数中使用了外部变量,那么JS引擎会在全局执行上下文中查找,我们把这个查找的链条就成为作用域链
function bar() 
    console.log(myName)

function foo() 
    var myName = " 极客邦 "
    bar()

var myName = " 极客时间 "
foo() // 极客时间

疑问:那为什么bar在foo函数里,而bar没有使用foo的执行上下文呢。这个问题就需要词法作用域来解释了

  • 词法作用域:词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它能够预测代码在执行过程中如何查找标识符的。
    词法作用域是代码阶段就决定好的,和函数是怎么调用的没有关系。

闭包的概念

  • 在JS中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包,比如外部函数是foo。那么这些变量的集合就成为foo函数的闭包。

闭包销毁:

  1. 闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭,但如果这个闭包以后不再使用的话,就会造成内存泄漏
  2. 如果引用闭包的函数是个局部变量,等函数销毁时,在下次JS引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么JS引擎的垃圾回收器就会回收这块内存。

所以闭包的使用原则: 如果该闭包会一直使用,那么它可以作为全局变量而存在,但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它称为一个局部变量。

什么是内存泄漏,哪些常见操作会造成内存泄漏

  • 内存泄露是指一块被分配的内存既不能使用,又不能回收,一直在内存里,直到浏览器进程结束。
  • 内存泄露。
    1. 全局变量引起的内存泄漏
    2. 闭包引起的内存泄漏
    3. dom 清空或删除时,事件未清除导致的内存泄漏
    4. 定时器没有关闭或者回调函数。

[腾讯二面]了解v8引擎吗,一段js代码如何执行的(B_Cornelius)

作者:冴羽链接:https://juejin.cn/post/6844904097556987917来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

在执行一段代码时,JS 引擎会首先创建一个执行栈
然后JS引擎会创建一个全局执行上下文,并push到执行栈中, 这个过程JS引擎会为这段代码中所有变量分配内存并赋一个初始值(undefined),在创建完成后,JS引擎会进入执行阶段,这个过程JS引擎会逐行的执行代码,即为之前分配好内存的变量逐个赋值(真实值)。

如果这段代码中存在function的声明和调用,那么JS引擎会创建一个函数执行上下文,并push到执行栈中,其创建和执行过程跟全局执行上下文一样。但有特殊情况,即当函数中存在对其它函数的调用时,JS引擎会在父函数执行的过程中,将子函数的全局执行上下文push到执行栈,这也是为什么子函数能够访问到父函数内所声明的变量。

还有一种特殊情况是,在子函数执行的过程中,父函数已经return了,这种情况下,JS引擎会将父函数的上下文从执行栈中移除,与此同时,JS引擎会为还在执行的子函数上下文创建一个闭包,这个闭包里保存了父函数内声明的变量及其赋值,子函数仍然能够在其上下文中访问并使用这边变量/常量。当子函数执行完毕,JS引擎才会将子函数的上下文及闭包一并从执行栈中移除。

最后,JS引擎是单线程的,那么它是如何处理高并发的呢?即当代码中存在异步调用时JS是如何执行的。比如setTimeout或fetch请求都是non-blocking的,当异步调用代码触发时,JS引擎会将需要异步执行的代码移出调用栈,直到等待到返回结果,JS引擎会立即将与之对应的回调函数push进任务队列中等待被调用,当调用(执行)栈中已经没有需要被执行的代码时,JS引擎会立刻将任务队列中的回调函数逐个push进调用栈并执行。这个过程我们也称之为事件循环。

共勉,一起努力。

以上是关于通过JavaScript执行机制去学习闭包,执行上下文,作用域,作用域链。的主要内容,如果未能解决你的问题,请参考以下文章

前端开发中javascript闭包会引发内存泄漏么?

JS深入理解闭包/作用域(scope)作用域链/执行上下文和执行栈

JavaScript 学习-24.函数闭包(closure)

Javascript中的闭包

Javascript中的闭包(转载)

JavaScript执行:闭包和执行上下文到底是怎么回事?