【前端知识梳理-JS】深入js执行机制

1. 代码执行顺序

先来看下如下代码的执行结果,看是否符合你的预期:

js 复制代码
showName()
console.log(myname)
console.log(myage)
var myname = '哈哈'
function showName() {
    console.log('showName被执行');
}

结论

  • 未声明的变量报错,js执行报错
  • 定义前使用不报错,值为undefined, 而非定义的值
  • 函数定义前使用,不报错且正常执行

变量提升

声明与赋值

js 复制代码
var test = function(){
  console.log('test1')
}
function test(){
  console.log('test')
}

test()

JS代码执行流程

总结

  • JavaScript 代码执行过程中,会发生变量提升,因为 JavaScript 代码在执行之前需要先编译。
  • 在编译阶段,变量和函数会被存放到变量环境中,变量的默认值为 undefined;
  • 在代码执行阶段,JavaScript 引擎会从变量环境中去查找自定义的变量和函数。
  • 如果在编译阶段,存在两个相同的函数,前面的会被覆盖。

2. 块级作用域

作用域

指变量与函数的可访问范围,控制着其可见性及生命周期

变量提升带来的问题

1. 变量被覆盖

js 复制代码
var myname = "哈哈"
function showName(){
  console.log(myname);
  if(0){
   var myname = "呵呵"
  }
  console.log(myname);
}
showName()

2. 变量不及时销毁

js 复制代码
function test(){
  for (var i = 0; i < 7; i++) {

  }
  console.log(i); 
}
test()

解决方案

使变量仅在代码块内生效(即块级作用域

使用letconst关键字时浏览器的处理

js 复制代码
function test(){
    var a = 1
    let b = 2
    {
      let b = 3
      var c = 4
      let d = 5
      console.log(a)
      console.log(b)
    }
    console.log(b) 
    console.log(c)
    console.log(d)
}   
test()

执行步骤分析:

  1. 编译并执行上下文

暂时无法在文档外展示此内容

2. 执行代码

let和const是否存在变量提升?

不存在

总结

  • Javascript引擎通过区分变量环境和词法环境,同时支持变量提升和块级作用域
  • 变量访问顺序 词法环境栈顶 -> 词法环境栈底 -> 变量环境

3. 作用域链与闭包

js 复制代码
function say() {
    console.log(myName)
}

function test() {
    var c1 = say
    var myName = "呵呵"
    c1()
}
var myName = "哈哈"
var obj1 = {
    a1: 1,
    fn1: function () {
        console.log(this.a1)
    }
}
var obj2 = {
    a1: 2,
    fn2: obj1.fn1
}
obj2.fn2()
test()

4. 相关概念

调用栈、执行上下文、变量环境、词法环境

每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer。

函数执行上下文使用外部引用的变量,会通过outer向外查找,这个查找链条就是作用域链

5. js执行过程中的作用域链是如何决定的

由代码中函数声明的位置(词法作用域 )决定,词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。

上面代码中,虽然test 函数调用了 say 函数,但是 say 函数的外部引用并不是 test 函数的执行上下文,而是全局执行执行上下文。

js 复制代码
function smile() {
    var myName = "name1"
    let test1 = 100
    if (1) {
        let myName = "name2"
        console.log(test)
    }
}

function say() {
    var myName = "name3"
    let test = 2
    {
        let test = 3
        smile()
    }
}
var myName = "name4"
let myAge = 10
let test = 1
say()

变量查找链:当前执行上下文词法环境 -> 当前执行上下文变量环境 -> 外部引用outer词法环境 -> 外部引用outer变量环境

6. 闭包

1. 浏览器如何处理闭包

js 复制代码
function test() {
    var myName = "name"
    let test1 = 1
    const test2 = 2
    var innerTest = {
        getName:function(){
            console.log(test1)
            return myName
        },
        setName:function(newName){
            myName = newName
        }
    }
    return innerTest
}
var fn = test()
fn.setName("name1")
fn.getName()
console.log(fn.getName())

当执行到 test 函数内部的return innerTest这行代码时调用栈的情况

根据词法作用域的规则,内部函数 getName 和 setName 总是可以访问它们的外部函数 test 中的变量

所以当 innerTest 对象返回给全局变量 fn 时,虽然 test 函数已经执行结束,但是 getName 和 setName 函数依然可以使用 test 函数中的变量 myName 和 test1。所以当 test 函数执行完成之后,执行栈状态如下:

虽然test函数从栈顶弹出,但是会在内存中留下一个包,这个包是test函数的专属包,且只能通过getName 和 setName方法访问,这个包也就是test函数的闭包

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

2. 闭包的内存模型分析

js如何存储数据

执行流程再分析

  1. 执行 test 函数,先编译并创建执行上下文。
  2. 编译过程遇到内部函数setName,JavaScript 引擎还要对内部函数做一次快速的词法扫描,发现该内部函数引用了 test 函数中的 myName 变量,由于是内部函数引用了外部函数的变量,所以 JavaScript 引擎判断这是一个闭包,于是在堆空间创建换一个"closure(test)"的对象(这是一个内部对象,JavaScript 是无法访问的),用来保存 myName 变量。
  3. 接着继续扫描到 getName 方法时,发现该函数内部还引用变量 test1,于是 JavaScript 引擎又将 test1 添加到"closure(test)"对象中。这时候堆中的"closure(test)"对象中就包含了 myName 和 test1 两个变量了。
  4. 由于 test2 并没有被内部函数引用,所以 test2 依然保存在调用栈中。

当执行到 test 函数时,闭包就产生了

当 test 函数执行结束之后,返回的 getName 和 setName 方法都引用"clourse(test)"对象,所以即使 test 函数退出了,"clourse(test)"依然被其内部的 getName 和 setName 方法引用。所以在下次调用fn.setName或者fn.getName时,创建的执行上下文中就包含了"clourse(test)"。

产生闭包的核心两步:

第一步是需要预扫描内部函数;第二步是把内部函数引用的外部变量保存到堆中。

闭包是怎么回收的

如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。

如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。

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

相关推荐
崔庆才丨静觅2 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax