JavaScript中的作用域

词法作用域

LHS和RHS

若查找的目的是对变量进行赋值则引擎会使用LHS查询

若目的是获取变量的值则使用LHS查询

如题,LHS查询的目的是对变量赋值,则可以理解为寻找变量的容器本身

RHS查询则应理解为retrieve his source value取其源值

我们可以简单的理解为LHS是赋值语句左边部分,注重的是变量本身。而RHS则是赋值语句右边部分,注重的是所要赋给变量的值

1
2
3
4
5
function foo(a){
console.log( a+b )
}
var b = 2
foo(2) // 4

此例中执行foo时,引擎会从当前作用域(foo作用域)寻找b,对其进行RHS引用。而作用域中自然无法找到,所以引擎会向上级作用域,本例中也就是全局作用域进行查找。若可以找到b则对其进行RHS引用,未找到则报错(ReferenceError

ReferenceError同作用域判别失败有关

TypeError则代表作用域判别成功,但对结果的操作是非法的

词法阶段

我们刚才提到了作用域,为了帮助理解我们举个栗子

1
2
3
4
5
6
7
8
function foo(a){				// ①
var b = a * 2 // ②
function bar(c){
console.log(a, b, c) // ③
}
bar(b * 3)
}
foo(2) // 2, 4, 12

①包含着整个全局作用域,其中只有1个标识符:foo

②包含着foo所创建的作用域,其中有3个标识符:abarb

③包含着bar所创建的作用域,其中只有1个标识符:c

由此可知,所谓的作用域包含了参数(arguments)、变量(variable)、方法(function)。

当我们进行LHSRHS查找时,若本层作用域中未找到则会自动向上,直至顶层的全局作用域。RHS未找到则报ReferenceError,而LHS未找到则会自动帮你创建一个全局的变量供你使用

欺骗词法作用域

eval()

eval函数可以接受一个字符串作为参数。神奇之处在于他会将其中的内容视为好像在书写时就存在于此位置的代码

在执行eval(str)之后的代码时,引擎并不在意代码str是以动态形式插入进来的,只会像往常一般,遵循着一层层向上的词法作用域进行查找。这就造成了一个隐患:欺骗词法

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会将原本的词法作用域进行修改,使得本来要向父级作用域进行RHS查找的变量b可以被操控改变为任何值。更加严重的是,若b处于该作用域父级的同时还处于全局作用域的子级,那么我们将无法在此作用域中找到正确的b

with

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo(obj){
with(obj){
a = 2
}
}
var o1 = { a: 3}
var o2 = { b: 3}

foo(o1)
console.log(o1.a) // 2 o1.a值被改变

foo(o2)
console.log(o2.a) // undefined
console.log(a) // 2 —— a被泄漏到了全局作用域上

foo函数中的with看似是对变量a进行了简单的词法引用,实际却是一个LHS引用,并将2赋值给o1.a

可是a又是怎么被泄漏变成一个全局变量的呢?

with可以将一个没有属性或有多个属性的对象处理为一个完全隔离的此法作用域。因此此对象的属性也会被处理为定义在此作用域中的词法标识符

当我们传递o1with时,with所声明的作用域为o1。而此作用域中含一个a。单我们将o2作为作用域时其中无a,因此执行a = 2时自动创建了一个全局变量

函数作用域

函数作用域:属于这个函数的全部变量都可以在整个函数的范围内使用及复用

作用

隐藏

由于函数作用域的存在导致了我们想要访问函数的内部方法或变量时无法绕过该函数,那么我们不妨换一种思路:将想要封装的代码块用函数包裹起来就可以完成“隐藏”功能

本着最小特权原则,我们应该尽可能少的暴露必要内容,以防止别处对其的调用或更改。将其存储至函数中可以使得该方法更加安全,保证其私密性

规避冲突

由于我们每层都会有一个存储参数、变量与方法的作用域,而不同作用域之间可以有同名变量,所以我们可以利用这一点,将一些同名而不同意义的变量存储至不同作用域,以防止其被覆盖

优化

上述我们提到了通过函数封装代码块,但是想要调用里边的方法时却无法绕开这个函数,这也造成了这个函数的名称本身污染了所在作用域。其次,必须显式的通过函数名才能调用这个函数,完成其中代码的运行。所以我们渴望一种不需要函数名就能直接运行其中代码的数据结构

1
2
3
4
(function foo(){
var a = 3
console.log(a)
})

我们在函数外边嵌套了一个(),函数就会被当作一个函数表达式,而非一个函数声明来处理

函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处

命名函数表达式中函数名称只能作为函数提作用域内的局部变量,外部无法访问

函数表达式的特殊之处在于他不像函数声明一般被绑定在所在作用域,而是被绑定在函数表达式自身的函数中。也就是说该函数表达式中的代码只能在所在位置中访问,而无法被外部作用于访问

Ps:函数表达式一般写法为

1
2
3
4
5
6
7
8
9
// 匿名函数表达式
var add = function f(a, b) {
return a + b;
};

// 当然也可创建命名函数表达式 Named function expression
var add = function func(a, b) {
return a + b;
};

如上所述(关于作用域那段),命名函数表达式中函数名称只能作为函数体作用域内的局部变量,外部不可访问

1
2
3
4
5
6
7
8
9
10
11
var a = function pp(v) {
v++;
if (v>3) {
return v;
} else {
return pp(v);
}
}

a(1); // 4
pp(1); // ReferenceError: pp is not defined

块作用域

JavaScript中的问题

js中没有块级作用域的体现,而我们所最熟悉的循环代码事实上虽然实现了功能,但是并不符合传统面向对象对于块级作用域的理解

1
2
3
for(var i=0; i<10; i++){
console.log(i)
}

当使用var声明变量时,因为js中特殊的变量提升机制,导致它写在哪里都一样,因为其最终都会属于外部作用域。要确保没有在其他地方意外使用i只能靠自觉

with

with创建出的作用域仅在with声明作用域中有效

try…catch

try...catch中的catch分句也会创建一个块级作用域。

let

let声明的变量隐式劫持了所在的块级作用域

{}

同时为了解决此问题我们可以将希望拥有独立作用域的代码块外嵌套一层大括号,以此达到外部作用域无法访问的目的

const

const也会创建块级作用域变量。但其创建的变量无法修改

声明提升

1
2
console.log(a)
var a = 2 // undefined
  • 只有声明(无论是变量声明还是函数声明)会被提升,赋值(a = 2)仍会被留在原地
  • 函数会被首先提升,然后才是变量

今天我们讲的内容一定要记在小本子上背牢了。下次咱们会讲闭包,没有今天关于作用域的概念将会寸步难行~
在这里插入图片描述