作用域

JS作用域机制

参于到其中的有:

  • 引擎

    从头到尾负责整个JS程序的编译及执行过程

  • 编译器

    引擎的好朋友之一,负责语法分析以及代码生成等脏活累活

  • 作用域

    引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限

以var a =2为例

变量的赋值会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果它之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值

LHS查询与RHS查询

RHS查询等同于找到某个变量的值

而LHS查询试图找到变量的容器本身,从而可以对其赋值

例如:

1
2
console.log(a); //这里对a的引用就是一个RHS引用
a = 2; //这里对a的引用则是LHS引用,我们并不关心当前值是生命,只是想为=2这个赋值操作找到一个目标

作用域嵌套

在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或者抵达最外层的作用域(全局作用域)为止

异常

在变量还没有声明的情况下,LHS和RHS的行为是不一样的

1
2
3
4
5
6
7
8
9
10
11
12
function foo(a){
console.log(a+b);
b = a;
}
foo(2);
//第一次对b进行RHS查询是无法找到该变量的
//如果RHS查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出ReferenceError
//相较之下,当引擎执行LHS查询时,如果在顶层(全局作用域)中也无法找到目标变量
//全局作用域中就会创建一个具有该名称的变量,并且返还给引擎(非严格模式)
//a = 100;

//另一方面,如果RHS查询到了一个变量,但是试图对这个变量的值进行不合理的操作,会出现Typeerror(非函数进行函数调用)

欺骗词法

欺骗词法作用域会导致性能下降

eval

接受一个字符串为参数,将其内容视为书写在那个位置的代码

1
2
3
4
5
6
function foo(str,a){
eval(str); //欺骗
console.log(a,b);
}
var b = 2;
foo("var b =3;",1) //1,3

实际情况中,可以非常容易地根据程序逻辑动态地将字符拼接在一起之后再传递进去

eval(..)通常被用来执行动态创建的代码

在严格模式下,eval(..)在运行时由其自己的词法作用域,意味着其中的声明无法修改所在的作用域

with

通常被当做重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身

1
2
3
4
5
6
7
8
9
10
11
12
13
var obj = {
a:1,
b:2,
c:3
};
obj.a = 2;
obj.b = 3;
obj.c = 4;
with(obj){
a = 3;
b = 4;
c = 5
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function foo(obj){
with(obj){
a = 2;
}
}
var o1 = {
a:3
}
var o2 = {
b:3
};
foo(o1);
console.log(o1.a) //2

foo(o2);
console.log(o2.a); //undefined
console.log(a); //2 --泄露到全局了

eval会修改其所处的词法作用域,而with根据传递的对象凭空创建了一个新的词法作用域

运用eval和with会影响到JS引擎对代码的优化(因为不知道优化之后是否会被改变,所以就不做任何优化),故而运行速度会比较缓慢,而且在严格模式下也不适用

函数作用域

一般情况下,在函数内部声明的变量和函数在外部是无法访问到的

这种”隐藏内部实现“的方法也符合开发中最小限度地暴露必要内容的API设计思想,即将具体内容私有化

同时也能规避冲突

1
2
3
4
5
6
7
8
9
10
function foo(){
function bar(a){
i = 3; //覆盖for中的i
console.log(a+i);
}
for(var i =0;i<10;i++){
bar(i*2);
}
}
foo();

可以选择一个对象用作全局命名空间,所有需要暴露给外界的功能都会成为这个命名空间的属性

或者用模块管理的机制将库的标识符显示地导入到另一个特定的作用域中

添加包装函数虽然可以隐藏一些细节,但是具名函数这个名称也会污染作用域,而且必须显式调用

所以可以利用IIFE及匿名函数解决这个问题

1
2
3
4
5
6
var a = 2;
(function(){
var a = 3;
console.log(a); //3
})();
console.log(a); //2

区分函数声明和函数表达式最简单的方法是看Function关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果它是声明中的第一个词,那么就是函数声明,否则就是一个函数表达式

函数表达式可以匿名,而函数声明不可以省略函数名—在JS中这是非法的

下面来看立即执行函数表达式的应用

1
2
3
4
5
6
7
(function IIFE(def){  //传入一个函数
def(window); //调用这个函数
}(function def(global)){ //函数的内部细节
var a = 3;
console.log(a);
console.log(global.a);
})

块作用域

1.上面提到的with也是块作用域的一个例子(块作用域的一种形式),用with从对象中创建出的作用域仅在with声明中而非外部作用域中有效

2.try/catch中的catch分句会创建一个块作用域,其中声明的变量仅在catch内部有效

1
2
3
4
5
6
try{
undefined(); //执行非法操作来强制制造一个异常
}catch(err){
console.log(err); //能够正常执行
}
console.log(err) ;//ReferenceError:err not found

3.let

let是在ES6中引入的用来解决JS中没有块作用域的问题的

使用let在用{}包裹之后,外面的部分不能访问到里面的信息,形成暂时性死区

学会用let也能避免一些由于闭包导致元素无法被回收的情况

let定义的变量不会挂载在window上

不能进行重复声明

4.const

const与let基本相似,但是const定义的值是一个常量,并不能对它进行修改

否则会报错


作用域
https://blog-theta-ten.vercel.app/2021/08/29/作用域/
作者
Chen
发布于
2021年8月29日
许可协议