注意词法作用域和 this 绑定机制的区别,函数的词法作用域完全取决于函数声明时所处的位置,而 this 绑定完全取决于函数的调用方式(调用位置和调用方法)。
一、什么是作用域
作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询。
LHS 和 RHS 都会从当前执行的作用域中开始查找,如果没有找到,就会向上级作用域继续查找目标标识符,直至抵达全局作用域(顶层),无论找没找到都会停止。
不成功的 RHS 会导致抛出 ReferenceError 异常。不成功的 LHS 会导致自动隐式地创建一个全局变量(非严格模式下),或者抛出 ReferenceError 异常(严格模式下)。如果 RHS 查询成功找到了一个变量,但是尝试对这个变量进行不合理的操作,就会抛出 TypeError 异常。
作用域共有两种主要的工作模型,第一种是被大多数语言所采用的词法作用域,另一种叫做动态作用域。
二、词法作用域
查找变量:无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定(this 不遵循词法作用域查找)。并且词法作用域查找只会查找以及标识符。(如果代码中引用了 foo.bar.baz
,词法作用域只会查找 foo 标识符,找到这个变量后,对象属性访问规则会分别接管 bar 和 baz 属性的访问。)
欺骗词法:(目的是在运行时‘修改’词法作用域)。因为无法确定词法作用域被进行了怎样的修改,这种做法会阻碍JS引擎在编译阶段进行的性能优化,会使代码运行变慢。
- eval() – 非严格模式
考虑非严格模式下的代码:
function foo(str, a) {
eval(str); // 欺骗词法作用域
console.log(a, b);
}
var b = 2;
foo("var b = 3;", 1); // 1, 3
而在严格模式的程序中,eval 在运行时有自己的词法作用域。这意味着在 eval 中的声明无法修改所在的作用域。
function foo(str) {
"use strict";
eval(str);
console.log(a);
}
foo("var a = 2;"); // ReferrenceError: a is not defined
- with
with 通常被当作重复引用同一个对象中的多个属性的快捷方式,它使我们不需要重复引用对象本身。例如:
var obj = {
a: 1,
b: 2,
c: 3
};
// 单调的重复"obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 简单的快捷方式
with (obj) {
a = 3;
b = 4;
c = 5;
}
with 可以将一个没有或者有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对象的属性也会被处理为定义在这个作用域中的词法标识符。
分析下述代码:
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 (a 被泄漏到全局作用域中了)
为什么变量 a 会被泄漏到全局作用域中呢? 因为在执行 foo(o2)
时,with 代码块中的 a = 2
在执行时,对标识符 a 进行了正常的 LHS 查找,首先查找 with 代码块构建的 o2 对象作为的词法作用域,并未找到同名属性 a ,就再依次向上级作用域进行查找,最终在全局作用域中任然没有找到,于是就创建了一个全局变量 a 并对其进行了赋值。
三、函数作用域和块作用域
这里已经很熟悉了,就不过多赘述,只记几个新发现的知识盲区
函数作用域
- 立即执行函数(IIFE)。
立即执行函数属于函数表达式。
// 代码块1:
var a = 2;
function foo() {
var a = 3;
console.log(a); // 3
}
foo();
console.log(a); // 2
上述代码中的 foo 变量是属于全局作用域的。其中 foo 函数使用的函数声明的方式定义的。
// 代码块2:
var a = 2;
(function foo() {
var a = 3;
console.log(a); // 3
})();
foo();
console.log(a); // 2
上述代码中的 foo 变量是属于 foo 函数自身作用域的,它只能在函数内部被访问。其中 foo 函数使用的函数表达式的方式定义的。
**立即执行函数的另一个比较普遍的进阶用法是,把它当做函数调用并传递参数进去。**例如:
// 代码块2:
var a = 2;
(function IIFE(global) {
var a = 3;
console.log(a); // 3
console.log(global.a); // 2
})(window);
console.log(a); // 2
- 匿名函数。
匿名函数不属于函数声明,而属于函数表达式。
块作用域
- with 从对象中创建出来的作用域是一个块级作用域;
- try/catch 中的 catch 分句会创建一个块作用域;
- let 声明的变量会被绑定到
{}
内部构成块作用域,且使用 let 进行声明的变量不会在块作用域中提升。const 声明的常量也会被绑定到{}
内部构成块作用域。
四、提升
复习一下‘提升’相关的知识
- 变量声明会被提升。
- 函数声明会被提升,但是函数表达式不会被提升。
- 函数声明会比变量声明优先被提升。
参考资料
- 《你不知道的JavaScript》