作用域一般理解,是指一个变量的作用范围
作用域分为:
- 全局作用域
- 函数作用域
注:在ES5中没有块级作用域,但通过let声明的变量具有块级作用域,除作用域外,它的其他方面与通过var进行声明的变量没有区别。
全局作用域:
- 全局作用域在页面打开时被创建,页面关闭时被销毁
- 编写在script标签中的变量和函数,作用域为全局,在页面任意位置都可以访问到
- 在全局作用域中有全局对象window,代表一个浏览器窗口,由浏览器创建,可以直接调用
- 全局作用域中声明的变量和函数作为window对象的属性和方法保存
函数作用域:
- 调用函数时,函数作用域被创建,函数执行完毕,函数作用域被销毁
- 每调用一次函数就会创建一个新的函数作用域,它们之间是相互独立的
- 在函数作用域中可以访问到全局作用域的变量,在函数外无法访问到函数作用域内的变量
- 在函数作用域中访问变量、函数时,会先在自身作用域中寻找,如果没找到,则会到函数上一级的作用域中寻找,一直找到全局作用域
执行期的上下文:
当函数代码执行的前期,会创建一个执行期上下文的内部对象AO(作用域)
这个内部的对象是预编译的时候创造出来的
在全局代码执行的前期会创建一个执行期的上下文对象GO(作用域)
ps: 作用域链会被保存到一个隐式的属性[scope]中去,这个属性是给js引擎来访问的。
预编译:作用域的创建阶段
函数作用域预编译:
1、创建AO对象
2、找形参和变量的声明,作为AO对象的属性名, 它们的值是undefined
3、实参和形参相统一
4、找函数声明,如果函数声明和变量声明一致的话会覆盖。
全局作用域的预编译:
1、创建GO对象
2、找变量声明,将变量名作为GO对象的属性名 值是undefined
3、找函数声明,值赋予函数体
例题:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script>
function fn(a, c) {
console.log(a);
var a = 123;
console.log(a);
console.log(c);
function a() {};
if (false) {
var d = 678;
}
console.log(d);
console.log(b);
var b = function (){};
console.log(b);
function c () {};
console.log(c);
}
fn(1, 2)
</script>
</body>
</html>
上面的例题不难看出变量声明使用的都是var,它们的作用域有函数内的,也有全局的。这里我们要提到一个变量提升机制。
变量提升机制:在函数作用域或全局作用域中通过关键字var声明的变量,无论是在哪里声明的,都会被当成在当前作用域顶部声明的变量。
function getValue(condition) {
if (condition) {
var value= "aa"
return value
} else {
//此处可访问变量value,值为undefined
return null
}
//此处可访问变量value,值为undefined
}
在预编译阶段,Javascript引擎会将bule的声明提升到函数顶部,初始化操作依旧在原处执行,所以在else和if之外可以访问到变量value,但无法获得它的初始值。
但这样的变量提升有时却会导致程序出现Bug,所以ES6将引入块级作用域来强化变量声明周期的控制。
下面我们来看下ES6新引入的块级作用域
块级声明
块级声明用于声明在指定块的作用域之外无法访问的变量。块级作用域存在于:函数内部、块中(字符{和}之间的作用域)
let声明
let声明的用法与var相同,用let代替var来声明变量,就可以把变量作用域限制在当前代码块中,由于let声明不会被提升,因此需要将let声明语句放在封闭代码块的顶部,以便整个代码块都可以访问。
function getValue(condition) {
if (condition) {
let value = 'aa'
return value
} else {
//变量value在此处不存在
return null
}
//变量value在此处不存在
}
假设作用域中已经存在某个标识符,此时再使用let关键字它就会抛出错误。
所以同一作用域中不能用let重复定义已经存在的标识符,那么不同作用域呢?看下例子
由于这里的let是在if块内声明了新变量value,因此不会抛出错误,内部块中的value会遮蔽全局作用域中的value,后者只有在if块外才能访问到。
const声明
ES6提供了const关键字,使用const声明的是常量,值一旦被设定后不可更改,因此使用const声明的常量必须进行初始化。
const与let
const和let声明的都是块级标识符,与let一样不会被提升到作用域顶部。它们之间的不同在于,无论是严格模式还是非严格模式,都不可以为const定义的常量再赋值,但如果const定义了一个对象,这个对象的值却可以修改。
注:绑定person的值是一个包含一个属性的对象,改变person.name是修改person包含的值,不会抛出任何错误。但如果直接给person赋值,等于要改变person的绑定,就会抛出错误。const声明不允许修改绑定,但允许修改绑定的值。
临时死区(Temporal Dead Zone)
TDZ常用于描述描述let和const的不提升效果。
Javascript引擎在扫描代码发现变量声明时,要么将它们提升至作用域顶部(遇到var声明),要么将声明放到TDZ中(遇到let和const)。访问TDZ中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从TDZ中移除,才能正常访问。
TDZ只是块级绑定的特色之一,下面我们看看循环中使用块级绑定特色。
循环中的块作用域绑定
先来看个bug
显然这段代码的预期输出结果应当是0~9,但控制台却打印了10次10,这是因为循环里的每次迭代同时共享着i,循环内部创建的函数全都保留对相同变量的引用。循环结束时变量i的值为10,所以每次调用console.log(i)时都会打印10。
先来看第一种解决办法,使用立即调用函数表达式(IIFE),强制生成计数器变量的副本
在循环内部,IIFE为接受的每一个变量i都创建一个副本并存储为变量value,这个变量的值就是响应迭代创建的函数所使用的值。
而ES6仅仅需要将bug代码中的var换成let。
每次循环的时候let声明都会创建一个新变量i,并将其初始化为i的当前值,所以循环内部创建的每个函数都能得到属于它们自己的i的副本。
全局作用域的绑定
let和const与var的另一个区别是它们在全局作用域中的行为。当var被用于全局作用域时,它会创建一个全局变量作为全局对象的属性,这就意味着使用var可能会无意中覆盖一个已经存在的全局属性,但在跨frame或跨window访问代码时我们仍会使用var定义全局变量。
如果你在全局作用域中使用let或const,会在全局作用域下创建一个新的绑定,但该绑定不会添加为全局对象的属性,只是遮蔽了全局变量。
其他:
函数参数有自己的作用域和临时死区,其与函数体的作用域是各自独立的。也就是说参数的默认值不可访问函数体内声明的变量。