一、JS 作用域的奥秘
(一)作用域的定义与分类
作用域是一套规则,用于规定如何查找变量以及确定当前执行代码对变量的访问权限。其主要类型如下:
1、全局作用域
作为最外围的作用域,在浏览器环境中,全局作用域即为 window 对象。具有全局作用域的变量能够在所有作用域中被访问,例如:
-
// window 的属性或方法拥有全局作用域 window.name = "globalName"; console.log(window.name); // 最外层变量或函数拥有全局作用域 var globalVar = 10; function globalFunction() { console.log("This is a global function."); } globalFunction(); // 未定义直接赋值的变量也拥有全局作用域 a = 20; console.log(a);
2、函数作用域
限定于函数内部区域。此作用域内的变量可访问全局作用域变量,但全局作用域无法访问函数内变量,且不同函数作用域中的变量相互独立,不可访问,例如:
-
function outerFunction() { var outerVar = 30; function innerFunction() { var innerVar = 40; console.log(outerVar); // 可访问 outerFunction 中的 outerVar } innerFunction(); console.log(innerVar); // 报错,无法访问 innerFunction 中的 innerVar } outerFunction();
3、块级作用域
由 ES6 的 let 和 const 引入。其具有临时死区特性,变量必须先声明后使用,例如:
-
if (true) { // 使用 let 声明变量,形成块级作用域 let blockVar = 50; console.log(blockVar); } console.log(blockVar); // 报错,无法在块外访问 blockVar
2)词法作用域
词法作用域由代码书写位置决定,函数作用域在函数定义时确立。其具备遮蔽效应,子作用域可访问父作用域,反之则不可。例如:
function outer() {
var outerVar = 60;
function inner() {
var innerVar = 70;
console.log(outerVar); // 先从 inner 作用域查找,未找到则向上查找 outer 作用域中的 outerVar
}
inner();
}
outer();
在多层嵌套作用域中可定义同名标识符,内部会遮蔽外部,如:
var global = 80;
function outer() {
var global = 90;
function inner() {
console.log(global); // 输出 90,内部 global 遮蔽外部 global
}
inner();
}
outer();
全局变量会成为全局对象属性,ES6 前可间接访问,但 `let` 声明引入块级作用域后有暂时性死区,访问全局变量可能报错,例如:
let globalLet = 100;
function test() {
console.log(globalLet); // 报错,暂时性死区导致无法访问全局变量
}
test();
3)动态作用域与 JavaScript
JavaScript 采用词法作用域,在代码书写阶段确定。与动态作用域不同,动态作用域在运行时依据调用栈确定作用域。例如:
function a() {
var x = 110;
b();
}
function b() {
console.log(x);
}
a();
在词法作用域下,`b` 函数中访问 `x` 会沿作用域链向上查找,找不到则报错;若为动态作用域,`x` 会在调用 `b` 函数处查找。
4)函数作用域
函数内部区域构成函数作用域,可访问全局变量,但外部无法访问内部变量,借此实现变量私有化,规避冲突。例如:
function privateFunction() {
var privateVar = 120;
return privateVar;
}
console.log(privateVar); // 报错,无法访问 privateFunction 中的 privateVar
函数声明会提升,函数表达式则不会。JavaScript 编译前处理声明,仅声明提升,赋值等逻辑保留原位,例如:
funcDeclaration(); // 可正常调用
function funcDeclaration() {
console.log("Function declaration called.");
}
var funcExpression = function () {
console.log("Function expression called.");
};
funcExpression();
5)块级作用域
ES6 借助 `let` 和 `const` 实现块级作用域,具备临时死区特性,变量需先声明再使用。
`let` 声明将变量绑定至所在块级作用域(通常为 `{}` 内部),块外不可访问,无提升且禁止重复声明,例如:
{
let blockLetVar = 130;
console.log(blockLetVar);
}
console.log(blockLetVar); // 报错,无法访问块内变量
let duplicateLet = 140;
let duplicateLet = 150; // 报错,重复声明
例如:`const` 同样创建块级作用域,值固定不可修改(对于基本类型),引用类型可修改属性值,例如:
const constVar = 160;
constVar = 170; // 报错,无法重新赋值
const constObj = { prop: "value" };
constObj.prop = "newValue"; // 合法,可修改对象属性值
二、JS 变量的世界
变量的类型
- 原始类型:undefined、string、number、boolean、symbol。
JavaScript 中的原始类型直接存储在栈中,占据空间小且大小固定。例如,let num = 10;这里的数字10就是 number 类型的原始值,存储在栈中。字符串类型如let str = "hello";,布尔类型let isTrue = true;,未定义类型let un;此时un的值为 undefined,还有 symbol 类型代表创建后独一无二且不可变的数据类型,主要用于解决可能出现的全局变量冲突问题。
- 引用类型:object、array、null、function。
引用类型存储在堆中,占据空间大且大小不固定。例如,let obj = { key: "value" };这里的对象存储在堆中,在栈中存储了指向堆中该实体的起始地址的指针。数组也是引用类型,如let arr = [1, 2, 3];。函数同样如此,function test() {}定义的函数存储在堆中,栈中存储其指针。而 null 虽然表示空值,但它也是一种特殊的引用类型。当定义了一个变量或函数但没有赋值或返回时,其值为 undefined;而 null 是赋值了但赋的是空值。
// 原始类型示例
let num = 10; number
let str = "hello"; string
let isTrue = true; boolean
let un;
console.log(un); undefined
let sym = Symbol("unique"); symbol
// 引用类型示例
let obj = { key: "value" };
console.log(typeof obj); // 输出 "object"
let arr = [1, 2, 3];
console.log(typeof arr); // 输出 "object"
function test() {
console.log("This is a function.");
}
console.log(typeof test); // 输出 "function"
let emptyValue = null;
console.log(emptyValue); // 输出 null
console.log(typeof emptyValue); // 输出 "object"
三、LHS 与 RHS 的探索
(一)LHS 和 RHS 的概念
- LHS 全称为 Left-hand Side,是赋值操作的左侧查询,试图找到变量的容器本身进行赋值。在 JavaScript 中,如果查找的目的是对变量进行赋值,就会使用 LHS 查询。例如在语句a = 2中,就是对变量a进行 LHS 查询,目的是为这个赋值操作找到目标容器a,以便将值 2 赋给它。
- RHS 全称为 Right-hand Side,是赋值操作的右侧查询,获取变量或函数的值。如果查找的目的是获取变量的值,就会使用 RHS 查询。比如在console.log(a)中,对变量a进行 RHS 查询,目的是取得a的值并传递给console.log函数。
(二)案例分析
- 通过多个代码案例分析 LHS 和 RHS 的查询过程及数量。
-
- 例如以下代码:
function foo(num) {
console.log(num);
}
foo(100);
在这个例子中,function foo(num)这里存在一个 LHS 查询,因为在调用foo(100)时,将实参 100 赋值给形参num,相当于num = 100,会在函数作用域中先找出num这个变量容器。而foo(100)本身是在求返回值的操作,在作用域中进行 RHS 查找这个函数是否存在,没有对其进行赋值操作,所以这是一个 RHS 查找。最后console.log(num)这里是一个 RHS 查询,因为在这行代码中,只有一个变量num被使用,在作用域中查找用于获取num的值。
所以这个代码中有 1 个 LHS 查询和 2 个 RHS 查询。
再看这个例子:
function test(num) {
var num2 = num;
return num + num2;
}
var num = test(100);
包含 LHS 的代码:var num = test(100)这一段代码是一个 LHS,因为num在赋值运算的左边,是赋值操作的目标,所以要对num变量进行 LHS 查询,查询过程由作用域(词法作用域)进行配合查找。在function test(num)中,形参num在调用test(100)时,将实参 100 赋值给形参num,也就是num = 100,因为形参num在赋值运算的左边,所以要对形参num进行 LHS 查询。var num2 = num这一段代码也是一个 LHS,因为num2在赋值运算的左边,是赋值操作的目标,所以要对num2进行 LHS 查询。
总共进行3次LHS查询。
包含 RHS 的代码:var num = test(100)这段代码虽然有 LHS 查询但同时也有 RHS,因为可以把这段代码分成两个部分来看,其中test(100)调用部分是要求得一个结果,需要知道test(100)的值是多少,根据谁是赋值操作的源头是谁则就是 RHS,从而获取test(100)的返回值。var num2 = num也同理,虽然有 LHS 查询但同时也有 RHS,因为其中num这个变量在赋值运算符右边,此时需要知道num的值,根据谁是赋值操作的源头是谁则就是 RHS。return num + num2这里按照词法作用域查找思维,需要知道num和num2的值,也就是想查找这两个变量并取得它们的值,然后进行求和操作,所以这里是 RHS 查找,需要分别对num和num2都进行 RHS 查询。
总共进行4次RHS查询。
(三)查找规则
- LHS 和 RHS 查找都会在当前执行作用域开始,若未找到则向上级作用域查找,直到全局作用域。在 JavaScript 中,无论是 LHS 查询还是 RHS 查询,都是从当前执行作用域开始进行查找。如果在当前作用域中没有找到所需的变量,就会向上一级作用域继续查找,如此一级一级向上,最后抵达全局作用域。
- 如果在全局作用域中仍然没有找到,对于 RHS 查询,引擎就会抛出ReferenceError异常;对于 LHS 查询,在非严格模式下,会在全局作用域中创建一个同名变量(如果在任何作用域中都无法找到该变量),严格模式下则会抛出ReferenceError异常。
四、编译报错的常见类型
- RangeError:范围错误,如数组索引越界、数值溢出等。例如,试图通过 Array 构造函数创建非法长度的数组,或者将错误的值传递给数字方法如 toExponential()、toPrecision()、toFixed() 等,以及将非法值传递给字符串函数如 normalize() 时,可能会引发 RangeError。
- ReferenceError:引用错误,尝试引用不存在的变量或标识符。比如引用了不存在的变量,如 a() 或 console.log(a)(其中 a 未定义),或者给一个无法被赋值的对象赋值,如 console.log("abc") = 1,都会产生 ReferenceError。
- SyntaxError:语法错误,代码不符合语言规范。常见的原因包括缺少引号、缺少右括号、大括号或其他字符对齐不当等。例如 var 2a(变量名错误)、console.log 'b')(括号不全)、function =3(关键字赋值错误)等都会引发 SyntaxError。
- TypeError:类型错误,操作涉及不兼容的数据类型。调用不是方法的对象、试图访问 null 或未定义对象的属性、将字符串视为数字或反之亦然、在使用 new 命令时参数不是构造函数等情况都会引发 TypeError。例如 var a = new 1(new 关键字后接基本类型)、123()(调用不存在的方法)、var o = {}; o.run()(调用对象不存在的方法)等。