前提
本文最后将通过实战一些题目来进行对于 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 预编译三步骤:
- 生成 GO 对象
- 将变量声明的变量名当做 GO 对象的属性名,值为
undefined
- 将声明函数的函数名当做 GO 对象的属性名,值为函数体
AO 对象
AO 预编译三步骤:
- 产生 AO 对象
- 将函数的参数以及函数里面声明的变量当做 AO 对象的属性名,值全部为
undefined
。 - 将实参的值赋值给形参。
- 在函数里面声明的函数,函数名作为 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 预编译与作用域”了,希望能帮助到各位读者。