你不知道的JavaScript底层逻辑

大家好,我是睡个好jo。我把在最近的JavaScript学习中,学到了包括编译过程、作用域、词法作用域、以及var与let的区别等在JS中的底层逻辑,我在这里把我的学习分享给大家。

编译:从源码到可执行代码的旅程

首先,请大家分析一下下面这段代码

    console.log(num); 
    var num = 10; 

相信各位大佬都会认为它会报错,其实不然,在js中它会输出一个 undefined,我们一步一步分析 在js的编译过程中,大致分为三个步骤

  1. 词法分析:首先,将源代码分割成一个个有意义的符号单元。这个过程会识别出字符串、数字、关键字、运算符等,并将它们分类。

  2. 解析:紧接着,解析器将这些词法单元按照语言的语法规则组织起来,构建出抽象语法树。

  3. 生成代码:最后一步,依据AST生成可执行的机器代码(在JavaScript中,这可能是字节码)。

根据这三个步骤分析上面的代码后得到的代码如下

var num 
console.log(num)
num = 10

结果如下

image.png

这其实是var的声明提升

作用域:变量的生存空间

JavaScript中有三种主要的作用域:

  • 全局作用域:在整个程序中都可访问的变量。
  • 函数作用域:函数内部定义的变量只在该函数内部可见。
  • 块级作用域(ES6引入):在{} + let || {} + const 所在的代码块中有效。

块级作用域

示例:

if(true) {  
    let a = 1;  
}
console.log(a); 

如果代码执行流离开这个块,变量a就会被销毁

暂时性死区

let和const关键字引入了块级作用域变量声明,它们在声明之前是不可访问的,这个不可访问的区域就是“暂时性死区”

 

let a = 1;  
{
    console.log(a); // 暂时性死区 
    let a = 2;  
}

 

 

作用域的规则

内部作用域访问外部作用域: 当在一个嵌套的作用域内(比如在一个函数内部),你可以无障碍地访问外部作用域(包括全局作用域和任何包围当前作用域的函数或块级作用域)中声明的变量和函数。这是因为外部作用域为内部作用域提供了必要的环境和资源。

外部作用域不能直接访问内部作用域: 相反方向上,外部作用域无法直接访问内部作用域的变量或函数。这样做是为了保护内部状态不被外部无意中修改,以及避免名称冲突,保证每个代码块的独立性。一旦内部作用域(如一个函数执行完毕),其局部变量通常会被销毁,除非通过闭包等机制特意保留。

我们拿一个例子来分析:

var a = 1; // 全局变量 全局作用域

function foo() { // 函数作用域
    var a = 2; // 局部变量
    console.log(a); // 输出 2
}   
foo();  

console.log(a); // 输出 1   
  1. 全局作用域:var a = 1; 定义了一个全局变量a,其值为1。变量a在整个代码文件中,只要没有被更内部的作用域覆盖,都是可以拿到的。

  2. 函数foo的作用域:当调用函数foo()时,定义了一个函数作用域。在这个函数内部,var a = 2; 又定义了一个局部变量a,这个a与全局的a是两个不同的变量,尽管它们名字相同。因为局部作用域可以访问外部作用域的变量,但在这个例子中,当在函数内部输出a的值时,它指的是局部变量a,其值为2,而非全局的a。

  3. 执行foo();时,输出的是2,是因为console.log(a);位于函数foo内部,访问的是局部变量a。

  4. 函数执行完毕后的作用域恢复:当foo()函数执行完,foo作用域就会销毁,foo()里的a也不再存在。但不影响全局变量a,它依然存在且值为1。

  5. 再次输出全局变量a:最后,console.log(a);位于函数外部,它输出的是全局变量a的值,即1。

也可以用栈的角度分析:

  1. 初始化:程序开始时,全局作用域对应一个基础的栈帧,其中包含全局变量a = 1。
  2. 调用foo():当foo()被调用时,此时栈的结构变为:顶层是foo的栈帧,下面是全局栈帧。如图
  3. foo()返回:foo()执行完后,栈帧被销毁,foo里的a = 2也随之消失。栈里只有全局栈帧。
  4. 最终的console.log(a): 再次执行console.log(a),由于foo的栈帧已不存在,查找变量a直接在全局栈帧中找到,输出全局变量a = 1。

image.png

词法作用域:静态的绑定规则

词法作用域:变量在代码中声明的位置,作用域的规则在代码编写时就已经确定,不会在运行时改变。

欺骗词法作用域的技巧

  • eval(): 它能把一个字符串变成JavaScript代码,让变量和函数和在当前作用域直接定义的一样。 效果如下:
function main(a,str){
    eval(str) 
    console.log(a,b);
}
main(1, 'var b=2');

  • with(): 当修改对象中不存在的属性时,这个属性会被泄漏到全局,变成全局变量

示例:

function foo(obj){
    with (obj){
        a = 2;  
    }   
}
var o1 = {
    a:1
}
var o2 = {
    b:2
}   

foo(o2)
console.log(a);

 

正常来说 应该报错,但with让变量a泄露到全局中了

var与let:时代变迁的选择

在ES6之前,var是声明变量的唯一方式,但有两大缺点:变量提升(声明在使用前,初始值为undefined )和在全局作用域下污染window对象。而let的出现解决了这些问题:

  • 不存在声明提升: let声明的变量在声明语句被执行前是不可访问的。
  • 块级作用域: 限制了变量的生命周期,使其更加可控。
  • 避免污染全局: 在全局作用域中使用let声明的变量不会成为window的属性。

const和let: const 在用法上 跟let 一样 但const 声明的变量不允许修改(常量)

总结回顾

  • 编译过程 揭示了JavaScript代码从源码到可执行代码的转换机制,包括词法分析、解析、代码生成三个关键步骤。这有助于我们理解为什么诸如变量提升这样的现象会发生。

  • 作用域与词法作用域 是JavaScript变量行为的核心概念。词法作用域基于代码的静态结构确定变量访问规则,使得开发者能够预测和控制变量的可见性,是静态类型语言和动态类型语言中的一个重要特性。 var、let、const的对比 显示了不同变量声明方式对作用域、提升行为及变量可变性的不同处理。let 和 const 强化了块级作用域的概念,减少了潜在的错误和全局污染问题,推荐在现代JavaScript开发中优先使用。

  • 作用域链与闭包 虽未深入讨论,但它们是理解JavaScript中作用域和数据持久化的关键,特别是在处理异步操作、模块化设计等方面。

  • 避免使用eval()与with(), 这两个功能虽然能改变作用域的行为,但可能导致安全问题、性能下降及代码难以理解和维护,现代JavaScript实践中通常建议避免使用。

那么本次的分享就到此结束了,希望对各位有所帮助!!!

文章转自:https://juejin.cn/post/7372105071574155283

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值