1、var变量声明的编译过程
ini
var a = 2;
-
声明变量a:查看作用域中是否已有同名变量,如有则忽略该声明。同一个作用域内多次用var声明同一个变量名不会报错。
-
赋值:查找当前作用域及往上的作用域链,找到a后赋值,如果没找到,就会在最上层的作用域声明变量a后赋值
function fn() { // var a = 3 // 声明fn内的局部变量a a = 3 // 声明全局变量a }
2、LHS查询和RHS查询
作用域是一套规则,用于确定在何处、如何查找变量,查找方式可分为两种类型:LHS查询和RHS查询。
LHS查询:查找以供赋值
RHS查询:查找以供使用
css
function foo(a) {
var b = a
return a + b
}
var c = foo(2)
如以上代码,有3处LHS:c、a(隐式赋值)、b
4处RHS:foo、a、a、b(第一次a是查找后赋值给b,第二次a是查找进行加+操作)
3、为什么要区分LHS查询和RHS查询?
在变量未声明时、即所有作用域都无法找到变量的情况下,两种查询方式进行的行为不同。
css
var a = 2
console.log(a + b); // 对b的RHS查询,会报错
b = a // 对b的LHS查询,不会报错
如以上代码,对b进行了一次RHS一次LHS,进行RHS查询时,找不到变量,会抛出ReferenceError异常;进行LHS查询时,找不到变量,会创建变量后返回。(非严格模式)
(严格模式下禁止隐式创建变量,因此在LHS查询失败时也会抛出异常)
扩展:RHS抛出的异常,ReferenceError异常表示作用域判别失败,TypeError异常表示作用域判别成功,但对结果的操作非法或不合理(如对非函数类型进行函数调用)
4、词法作用域
词法作用域也称为静态作用域,在定义时确定,只关注函数在何处声明
scss
function foo() {
console.log(a);
}
var a = 2
function foo2() {
var a = 3
foo()
}
foo2() // 2
如上代码,打印值会是2,因为foo会在定义时的作用域内寻找a,最终找到的是全局变量a。
(动态作用域是在运行时确定,关注函数在何处调用的。js中没有,但this机制很像动态作用域。)
5、作用域链
要了解作用域链的概念,需要先知道什么是变量对象
变量对象是指当前作用域内的所有变量、函数、形参,所组成的一个对象
如1中的示例代码,全局变量对象FO为a、fn foo、fn foo2,foo2函数内的局部变量对象AO(active object,因为函数只有调用时才有价值 )为a,foo函数内无变量对象
作用域链是变量对象的集合,由一系列AO+VO组成的链式结构。它规定了代码访问的顺序,只能向上访问,不能向下访问
6、用eval/with修改词法作用域
(1)eval()函数,接收一个字符串作为参数,将其中的内容视为代码执行(不支持es6语法),可用于动态插入代码
非严格模式可欺骗浏览器,修改词法作用域环境
scss
function foo(str, a) {
eval(str);
console.log(a, b); // 1. 3
}
foo("var b = 3", 1);
如以上代码,var b = 3会直接在eval的位置执行,b会成为foo作用域内的变量。
严格模式下eval运行时会有自己的作用域,b会是eval作用域内的变量,此时在foo打印b就会报错b。
(2)with,可以将对象处理为隔离的词法作用域,只能在非严格模式下使用,严格模式下是被禁止的。
ini
var obj = {
a: 1,
b: 2,
c: 3,
};
// 会创建一个obj内的词法作用域
with (obj) {
a = 3;
b = 4;
c = 5;
d = 6; // 当对象不存在时,变量会被泄漏到with所处的上级作用域中
var e = 7; // 不会限制在块作用域内,会被添加到with所处的作用域中
}
这两种方法现在很少使用,也不推荐使用,因为它们会对性能造成影响。词法作用域的好处在于js引擎可以在编译阶段就确定所有变量和函数的定义位置,以便在执行时快速查找。而eval/with无法进行这一优化,会拖慢代码运行效率。
7、块级作用域
块级作用域指用{}包裹起来的代码内的作用域,常见的有if else和for循环内的代码块作用域。使用块级作用域的好处有:
(1)垃圾回收机制
ini
function process(data) {}
// let data = { a: 1 };
// process(data);
{
let data = { a: 1 };
process(data);
}
var btn = document.getElementById("btn");
btn.addEventListener("click", function (evt) {
console.log("clicked");
});
如上,执行到事件绑定时,如果不显式地声明块级作用域,js引擎会认为上面声明的变量可能仍会在绑定事件中使用,不会回收。而使用块级作用域,引擎就会知道这段代码不需要保留、可以进行回收。
(2)循环时重新绑定
在循环中,当使用let声明变量时,每次循环的i值都是独立的
ini
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i); // 01234
}, 0);
}
这是因为它的内部执行如下,在每次循环的块级作用域内,都会重新声明一个变量绑定赋值,当前i仅在当前次循环的块级作用域内生效。
ini
{
let i
for (i = 0; i < 5; i++) {
let j = i
setTimeout(() => {
console.log(j); // 01234
}, 0);
}
}