JS 预编译与作用域

393 阅读5分钟

前提

本文最后将通过实战一些题目来进行对于 JS 预编译和作用域的一个诠释。

先来看看一道题:

console.log('----------- 第一题 -----------');
var x1 = 1;

function func1(x1, y1 = function () {
  x1 = 3;
  console.log(x1);
}) {
  console.log(x1);
  var x1 = 2; 
  y1();
  console.log(x1);
}
func1();
console.log(x1);

输出结果:

undefined
3
2
1

这道题其实涉及到的就是“JS 预编译与作用域”的知识点,要能够理解这道题,首先得先认识到什么是“JS 预编译”

JS 预编译

这里简单介绍一下“JS 预编译”是什么。

在 JS 运行之前,会经历 JS 的预编译过程,预编译的时候会涉及到 2 个对象,分别是:

  • GO 对象全称为 global object(全局对象,等同于window)
  • AO 对象全称为:activation object (活跃对象/执行期上下文)

GO 对象

在开始预编译时产生的对象,比 AO 对象先产生,用于存放全局变量,也称为全局作用域。

GO 预编译三步骤:

  1. 生成 GO 对象
  2. 将变量声明的变量名当做 GO 对象的属性名,值为 undefined
  3. 将声明函数的函数名当做 GO 对象的属性名,值为函数体

AO 对象

AO 预编译三步骤:

  1. 产生 AO 对象
  2. 将函数的参数以及函数里面声明的变量当做 AO 对象的属性名,值全部为 undefined
  3. 将实参的值赋值给形参。
  4. 在函数里面声明的函数,函数名作为 AO 对象的属性名,值赋值给函数体。(若参数名和函数名重叠,则函数体值会覆盖参数值)

了解到什么是“JS 预编译” 后,就来看看下面的几道题吧

实战

console.log('----------- 第一题 -----------');
var x1 = 1;

function func1(x1, y1 = function () {
  x1 = 3; // 这里是赋值给 OA 对象 x1 属性
  // 需要注意的是这里输出的是 y1 这个参数所在的参数作用域,
  // 虽然和函数的 x1 在预编译的时候,都在 OA 对象定义了,但 var x1 = 2 真正执行的时候,会产生局部作用域的变量,
  // 这里的 x1 是参数作用域下的,是参数的独立空间,改的是参数空间内的 x1 的值,其实就是 OA 对象上 x1 属性
  // 预编译的时候,参数 和 函数内部的变量都会在 OA 对象上生成同一个 属性,但实际运行的时候,
  // 分为参数作用域(OA 对象)和局部作用域(函数内部的变量),两边的值有独立的存储空间的,分别存在的
  console.log(x1);
}) {
  console.log(x1); // 预编译第三步,形实参相统一,将实参的 undefined 保存在 OA 上的 x1,x1 => undefined,这时候取的的 OA 对象的属性 x1
  var x1 = 2; // 赋值 2 到局部变量,这里是一个独立的局部变量
  y1(); // 参数作用域 y1 执行 -> 参数作用域 x1 => 3
  console.log(x1); // 局部作用域 x1 => 2
}
func1();
console.log(x1); // 全局作用域 x1 => 1;

console.log('----------- 第二题 -----------');
var x2 = 1;

function func2(x2, y2 = function () {
  x2 = 3; // 这里是赋值给 OA 对象 x2 属性
  console.log(x2);
}) {
  console.log(x2); // 预编译第三步,形实参相统一,将实参的 undefined 保存在 OA 上的 x2,x2 => undefined,这时候取的的 OA 对象的属性 x2
  // var x2 = 2;
  y2(); // 参数作用域 y2 执行 -> 参数作用域 x2 => 3
  console.log(x2); // 参数作用域 x2 => 3
}
func2();
console.log(x2); // 全局作用域 x2 => 1

console.log('----------- 第三题 -----------');
var x3 = 1;

function func3(z3, y3 = function () {
  x3 = 3; // 这里是赋值给 GO 对象 x3 属性
  console.log(x3);
}) {
  console.log(x3); // 预编译第二步,将局部变量 x3 保存在 OA 上,这时候并未赋值,x3 => undefined,这时候取的的 OA 对象的属性 x3
  var x3 = 2; // 赋值 2 到局部变量,这里是一个独立的局部变量
  y3(); // 参数作用域 y3 执行 -> GO 全局作用域 x3 => 3
  console.log(x3); // 局部作用域 x3 => 2
}
func3();
console.log(x3); // 全局作用域 x3 => 3

console.log('----------- 第四题 -----------');
var x4 = 1;

function func4(x4 = 4, y4 = function () {
  x4 = 3; // 这里是赋值给 OA 对象 x4 属性,
  console.log(x4);
}) {
  console.log(x4); // 预编译第三步,形实参相统一,将实参的 undefined 保存在 OA 上的 x4,,这时候并未赋值,x4 => undefined,这时候取的的 OA 对象的属性 x4
  var x4 = 2; // 赋值 2 到局部变量,这里是一个独立的局部变量
  y4(); // 参数作用域 y4 执行 -> 参数作用域 x4 => 4,就是 OA 上的 y4
  console.log(x4); // 局部作用域 x4 => 2
}
func4();
console.log(x4); // 全局作用域 x4 => 4

console.log('----------- 第五题 -----------');
var x5 = 1;

function yy5() {
  x5 = 3; // 这里是赋值给 GO 全局对象 x5 属性,
  console.log(x5);
}

function func5(x5 = 4, y5 = yy5) {
  console.log(x5); // 预编译第三步,形实参相统一,将实参的 undefined 保存在 OA 上的 x5,这时候并未赋值,x5 => undefined,这时候取的的 OA 对象的属性 x5
  var x5 = 2; // 赋值 2 到局部变量,这里是一个独立的局部变量
  y5(); // 执行的是 GO 全局对象的属性 yy5 函数,执行后所执行 x5 为 GO 全局对象的属性
  console.log(x5); // 局部作用域 x5 => 2
}
func5();
console.log(x5); // 全局作用域 x5 => 3

运行结果:

----------- 第一题 -----------
undefined
3
2
1
----------- 第二题 -----------
undefined
3
3
1
----------- 第三题 -----------
undefined
3
2
3
----------- 第四题 -----------
4
3
2
1
----------- 第五题 -----------
4
3
2
3

总结

从函数 func 局部作用域 -> 全局作用域

一开始从 func 函数局部作用域开始找 ->

局部作用域没有 -> 上升到从参数作用域上去找,也就是 OA 对象 ->

OA 参数作用域没有 -> 上升到从全局作用域上去找,也就是 GO 对象 ->

GO 全局作用域没有 -> undefined

本文通过上述实战,如果能够理解下来,那应该就能认识到“JS 预编译与作用域”了,希望能帮助到各位读者。

参考