三分钟理解JavaScript 作用域链与预编译

174 阅读4分钟

前言

JavaScript作为一种动态类型、解释型的脚本语言,其作用域和上下文的概念对于开发者来说至关重要。本文将探讨JavaScript中的对象特性函数属性作用域作用域链预编译等核心概念,并通过实例分析,帮助读者构建清晰的理解框架。

基础知识

对象

对象特点: 可以放键值对

let a = {
    age: 18,//键名:键值,,,,age为属性
}

函数也是一种特殊的对象,也可以拥有属性 以test函数为例

function test() {

}

函数天生的属性有:

test.name 函数名

test.length 传入的实参的个数

test.prototype 函数的原型

test.[[scope]] 作用域属性,给js引擎访问的,我们拿不到 ---隐式属性

当test()被调用的时候,引擎才会来看里面写了什么。此时引擎会创建一个上下文对象AO对象(Action Object)出来,用来记录有效标识符,执行完后会被回收。

AO对象被回收后,再次调用test(),又会创建一个新的AO对象

2b0943e8203689e3825181a5ca3476f.jpg

概念

作用域

函数身上的属性[[scope]],用于存储函数中的有效标识符

作用域链

作用域是执行器上下文对象的集合,这种集合呈链式连接,我们把这种链状关系称之为作用域链

为什么内层可以访问外层?---->循着作用域链查找

我们用下面这个例子帮助我们理解:

function a() {
function b() {
b = 33;
}
var a = 4399;
b();
console.log(a);
}
var glob = 100;
a();

a的定义

a.[scope]指向全局

a定义在全局,引擎读到a时,一定读到了全局作用域

357895208ec0853739b1116d6cbc8b5.jpg

a的执行

创建a的上下文对象AO

先在AO找然后再去全局找,AO优先然后是GO

9db024f3aa1f022c353c1ea08f42c3f.jpg

这个东西就是a的作用域链:

image.png

接下来我们看b

ba0e3e89887e82658a4b1cb811df646.jpg

b在a里,所以b一定能访问a

dc332b0268279d98d5eeb2dba7c611b.jpg

现在我们就很容易理解作用域链的发生机制了

image.png

预编译

代码在执行前需要进行编译操作,用于确定代码之间的各种关联

简单来说,开始写程序的时候,预编译器已经帮我们做好了很多事情,让程序写起来更快,也更容易理解。就像我们在做手工之前,先准备好所有的材料和工具一样。

变量声明

代码在执行前是一定会被编译的,不编译引擎就不会识别出来,会出现诸多问题比如变量提升

我们来看个例子:

console.log(a); 
var a = 1

变量在编译过后会声明提升,代码在引擎眼中是这样的:

var a
console.log(a);  //输出undefined
a = 1

函数声明

我们来举个函数的例子

test()
function test() {
    var a = 123
    console.log(a);
}

同样,函数声明也会整体提升

function test() {
    var a = 123
    console.log(a);
}
test()//会输出123,不会报错

例子

好的,理解了这两个概念之后我们来看下面这行代码:

var a = 1;
function fn(a) {
    var a = 2
    function a() { }
    console.log(a);
}
fn(3)

看到这段代码的第一反应:

faea83cec6f4fa001170ee2dde40387.jpg

咱先分析一下:

  • 预编译发生在全局
    1. 创建全局GO对象
    2. 找变量声明,将变量名作为GO的属性名,值为undefined
    3. 在全局找函数声明,将函数作为GO的属性名,值为函数体
  • 预编译发生在函数体内:
    1. 创建一个AO对象

    2. 找形参和变量声明,将形参和变量名作为AO的属性名,值为undefined

    3. 形参和实参统一

    4. 在函数体内找函数声明,将函数名作为AO的属性名,值为该函数体

    好,接下来我们进行编译,在原代码的基础上解释:
//全局作用域
GO:{
a:undefined-->1
fn:function(){}
}
var a = 1;
function fn(a) {
    var a = 2
    function a() { }
    console.log(a);
}
//函数调用
AO:{
    a:undefined-->3-->function(){}   
}
fn(3)//输出为2

再来分析一段:

  function fn(a) {
  console.log(a);
  var a = 4399
  console.log(a);
  function a() { }
  console.log(a);
  var b = function () { }
  console.log(b);
  function c() { }
  var c = a
  console.log(c);
}

步骤一:全局预编译

GO:{
   fn:function(){}//全局就只有一个fn
  }

步骤二:全局代码执行,遇到了函数调用,进行函数编译

AO: {
  a: undefined-- > 1-- > function a() { }
  b: undefined
  c: undefined-- > function c() { }
}

步骤三:函数编译完了之后再执行

AO: {
  a: undefined-- > 1-- > function()-->4399
  b: undefined -->function () { }
  c: undefined-- > function c() { }-->4399
}

输出结果为

image.png

这个输出结果符合我们的预期

结语

好滴,以上就是本文全部内容啦,本文我们深入理解了JavaScript作用域与上下文的工作原理以及预编译机制,包括变量提升和函数声明提升等概念。希望这些知识能帮助大家写出更高效的代码。感谢阅读,期待未来更多精彩分享~

微信图片_20240531170942.jpg