掘金第一文 :畅谈 JS 中的作用域和预编译

220 阅读4分钟

JS 小白报到!今天聊聊我初识作用域和预编译的踩坑笔记。

js 引擎

通俗来说 js 引擎分为两种

  1. 浏览器
  2. node
    在此就不得不引出一个新名词 V8 —— V8 是 Google 为 Chrome 开发的开源 JavaScript 和 WebAssembly 引擎,用 C++ 写成。用一句话概括:它是最快、最广泛使用的 JS 引擎,让 Chrome 和 Node.js 飞起来的“发动机”。后面我会将它们统称为 V8 引擎。

在讲解 js 的执行前,我先引入今天的主角之一:作用域

1. 全局作用域:顾名思义就是概括整个代码的一整局区域叫做全局作用域

2. 函数作用域 (参数也是该作用域的一个有效标识):同上理解就是概括该函数所选区域叫做函数作用域

如下图显示作用域1.png

3. 块级作用域 (let,const 和 {} 语法配合使用会导致“声明的变量”处在一个作用域中)

如图作用域2.png

注意:let 和 const 的暂时性死区:当一个{} 语句中存在 let x 时,在该{} 中访问x,永远都只能访问{}内部的 x ,就算内部访问不到,也不能访问外部的x。这种规则称为暂时性死区作用域3.png

如图我们可以看得到打印出来的是 2 ,并非 1 ,这就是暂时性死区,好我知道你现在还不能直观的感受到它,那么我为你准备了第二个图 作用域4.png
在这里我们能看到他没办法打印出来结果,直接显示报错,为啥他不输出上面的 1 呢?这正好印证了什么是暂时性死区

最后再注意一点:外层作用域不能访问内层作用域,作用域只能由内往外查找

这是正常执行逻辑:由内到外 **作用域5.png
如果由外到内则会直接报错(如图)作用域6.png

js的执行

  1. 代码被 v8 读取到的第一时间,并不是执行,而是会先编译(梳理)
  • 梳理:
  1. 分词/词法分析
  2. 解析/语法分析 -- AST(抽象语法树) 获取有效标识符
  3. 生成代码

为了更好地了解 js 的执行,咱们不得不引出今天的第二个主角—— js 的预编译

编译

  1. v8引擎读取到 js 代码,先编译,再次执行
  2. js 代码被编译时,声明部分会提升到当前作用域顶部

- 我们先来谈有关函数体的编译

  1. 创建函数的执行上下文 AO 对象
  2. 找形参和变量声明,将形参和变量声明作为 AO 的属性名,值为 undefined
  3. 将实参和形参统一
  4. 在函数体内找函数声明,函数名作为 AO 的属性名,值为函数体
    这四步看似很复杂,实则一点也不简单。哈哈开个玩笑,听我跟你一一道来,听完定能一目了然
1.先随便写一串代码
  function fn(a) {
  console.log(a);  
  var a = 123
  console.log(a); 
  function a() {}
  var b = function() {}
  console.log(b);  
  function c() {}
  var c = a
  console.log(c);  
}
fn(1)
2.创建函数的执行上下文 AO 对象,找形参和变量声明,将形参和变量声明作为 AO 的属性名,值为 undefined
    AO {
    a: undefined   
    b: undefined 
    c: undefined  
}
3.将实参和形参统一,在函数体内找函数声明,函数名作为 AO 的属性名,值为函数体
  function fn(a) {
  console.log(a);  // function a() {}
  var a = 123
  console.log(a);  // 123
  function a() {}
  var b = function() {}
  console.log(b);  // function() {}
  function c() {}
  var c = a
  console.log(c);  //123
}

// AO {
//     a: undefined    1   function a() {}  123,
//     b: undefined  function() {} ,
//     c: undefined  function c() {}  123 ,
// }
fn(1)

最后输出 预编译1.png

- 接下来要讲的就是有关全局的编译

  1. 创建全局执行上下文 GO 对象
  2. 找变量声明,将变量名作为 GO 的属性,值为undefined
  3. 找函数体声明,函数名作为 GO 的属性,值为函数体
    从步骤数来看它比函数体的编译还少一步,但事实当真如此吗?还请听我我缓缓道来
1. 依旧先随便写一串代码
var a = 1
function a() {}
console.log(a); 
var b = a
console.log(b); 
a = 2
var c = b
console.log(c);
2. 创建全局执行上下文 GO 对象,找变量声明,将变量名作为 GO 的属性,值为undefined
GO: {
    a: undefined, 
    b: undefined,
    c: undefined,
}
3. 找函数体声明,函数名作为 GO 的属性,值为函数体(由于变量名和函数名都是 a ,所以无需重复,放在一起即可)
GO: {
    a: undefined  function a() {},
    b: undefined  1
    c: undefined  1
}

最后的总代码以及输出结果

// GO: {
//     a: undefined, function a() {}, 1, 2
//     b: undefined,1
//     c: undefined,1
// }


var a = 1
function a() {}
console.log(a); // 1
var b = a
console.log(b); // 1
a = 2
var c = b
console.log(c); // 1

预编译2.png

最后我再将两者结合在一起放一道简单的代码题,观众老爷们可以自己动手做做看(评论区放输出结果)

g = 100
function fn() {
  console.log(g); 
  g = 200
  console.log(g); 
  var g = 300
}
fn()
var g 

纯属个人学习笔记,才疏学浅,欢迎轻拍。 承蒙各位大佬垂阅,若肯赏个👍,感激不尽🙇🏻