一、作用域
作用域,即变量
(变量作用域又称上下文)和函数
生效(能被访问
)的区域
或集合。换句话说,作用域决定了代码区块中变量和其他资源的可见性。
function myFunction() {
let inVariable = "函数内部变量";
}
myFunction();//要先执行这个函数,否则根本不知道里面是啥
console.log(inVariable); // Uncaught ReferenceError: inVariable is not defined
上述例子中,函数myFunction
内部创建一个inVariable
变量,当我们在全局访问这个变量的时候,系统会报错。这就说明我们在全局是无法获取到(闭包除外
)函数内部的变量。
我们一般将作用域分成:
- 全局作用域
- 函数作用域
- 块级作用域
全局作用域
任何不在函数中或大括号中声明的变量,都在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问。
// 全局变量
var greeting = 'Hello World!';
function greet() {
console.log(greeting);
}
// 打印 'Hello World!'
greet();
函数作用域
函数作用域也叫局部作用域
,如果一个变量是在函数内部声明
的它就在一个函数作用域下面。这些变量只能在函数内部访问
,不能在函数以外去访问
function greet() {
var greeting = 'Hello World!';
console.log(greeting);
}
// 打印 'Hello World!'
greet();
// 报错: Uncaught ReferenceError: greeting is not defined
console.log(greeting);
块级作用域
ES6引入了let
和const
关键字,和var
关键字不同,在大括号中使用let
和const
声明的变量存在于块级作用域中。在大括号之外不能访问这些变量。
{
// 块级作用域中的变量
let greeting = 'Hello World!';
var lang = 'English';
console.log(greeting); // Prints 'Hello World!'
}
// 变量 'English'
console.log(lang);
// 报错:Uncaught ReferenceError: greeting is not defined
console.log(greeting);
Let、const和Var的区别
-
var是全局作用域(
对于大括号中来说
),let 只在当前代码块内有效。 -
let变量不能重复声明,var可以
-
var有变量提升,let没有变量提升(let必须先声明再使用)
-
所以在
for循环
时,用var初始化i
只会得到最后一轮循环的值,因为var是全局的只有一个,用let初始化则可以得到每一轮的值,为什么let可以?因为js引擎内部会记住上一轮的值,下一轮在此基础上计算 -
另外, for 循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
for (let i = 0; i < 3; i++) { let i = 'abc'; console.log(i); } // abc abc abc
暂时性死区
只要块级作用域内存在 let 命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
上面代码中,存在全局变量 tmp ,但是块级作用域内 let 又声明了一个局部变量 tmp ,导致后者绑定这个块级作用域,所以在 let 声明变量前,对 tmp 赋值会报错。
ES6明确规定,如果区块中存在 let 和 const 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
总之,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称TDZ)。
typeof x; // ReferenceError
let x;
上面代码中,变量 x 使用 let 命令声明,所以在声明之前,都属于 x 的“死区”,只要用到该变量就会报错。因此, typeof 运行时就会抛出一个 ReferenceError 。
作为比较,如果一个变量根本没有被声明,使用 typeof 反而不会报错。
typeof undeclared_variable // "undefined"
块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)不再必要了。
// IIFE 写法
(function () {
var tmp = ...;
...
}());
// 块级作用域写法
{
let tmp = ...;
...
}
js立即执行函数主要是用于隔离作用域
二、词法作用域
我们先来看下面这个栗子
var a = 2;
function foo(){
console.log(a)
}
function bar(){
var a = 3;
foo();
}
bar()
大家认为应该输出多少呢?答案是2
词法作用域,又叫静态作用域,变量被创建时就确定好了
,而非执行阶段确定的。也就是说我们写好代码时它的作用域就确定了,JavaScript
遵循的就是词法作用域。相同层级的 foo
和 bar
就没有办法访问到彼此块作用域中的变量,所以输出2。
注意:重点是 被创建时就确定好了,而不是执行时。也就是说在创建foo的时候,函数里没有找到a,那么就到外面去找,此时a=2,ok,已经确定好了。如果我们把var a = 2;删除掉,会直接报错a is not defined
.看起来foo在bar声明了a=3后可以找到3,但其实在创建时,js怎么知道你有个bar里调用3呢?此时foo里找不到a的定义,外面也没有,当然就未定义咯。
在此强调,词法作用域就是作用域是由书写代码时函数声明的位置来决定的。编译阶段就能够知道全部标识符在哪里以及是如何声明的,所以词法作用域是静态的作用域,也就是词法作用域能够预测在执行代码的过程中如何查找标识符。
eval()和with可以通过其特殊性用来“欺骗”词法作用域,不过正常情况下都不建议使用,会产生性能问题。同时严格模式下也无法生效。了解即可。
首先了解一下:遮蔽效应:在多层的嵌套作用域中可以定义同名的标识符,即内部的标识符“遮蔽”了外部的标识符
eval(…):对一段包含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在运行时)
function foo(str, a) {
eval( str ); // 欺骗!相当于 var b = 3;
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3
//严格模式下,eval(..) 在运行时有其自己的词法作用域,意味着其中的声明无法修改所在的作用域
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 块可以将一个对象处理为词法作用域,但是这个块内部正常的 var声明并不会被限制在这个块的作用域中,而是被添加到 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 被泄漏到全局作用域上了!
//当我们传递 o1 给 with 时,with 所声明的作用域是 o1,而这个作用域中含有一个同 o1.a 属性相符的标识符。但当我们将 o2 作为作用域时,其中并没有 a 标识符,因此进行了正常的 LHS 标识符查找。 o2 的作用域、foo(..) 的作用域和全局作用域中都没有找到标识符 a,因此当 a=2 执行时,自动创建了一个全局变量(因为是非严格模式)
三、作用域链
当在Javascript
中使用一个变量的时候,首先Javascript
引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。
如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错