作用域是可访问变量的集合。JavaScript中函数和对象也算是变量,简单来说,作用域就是一个区域,包含了其中变量,常量,函数等等定义信息和赋值信息,以及这个区域内代码书写的结构信息。作用域决定了这些变量的可访问性(可见性),作用域就是一个独立的地盘,让变量不会外泄、暴露出去,也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
ES6之前作用域分为全局作用域和局部作用域,ES6新增了块级作用域
1.全局作用域
在代码中任何地方都能访问到的变量的集合称为全局作用域,拥有全局作用域的变量称为全局变量,一般来说以下几种情形拥有全局作用域
- 直接编写在 script 标签之中定或者是一个单独的 JS 文件中的代码拥有全局作用域
var outVariable = "我是最外层变量";
function outFun() { //最外层函数
var inVariable = "内层变量";
function innerFun() { //内层函数
console.log(inVariable);
}
innerFun();
}
console.log(outVariable); //我是最外层变量
outFun(); //内层变量
console.log(inVariable); //inVariable is not defined
innerFun(); //innerFun is not defined
- 所有末定义直接赋值的变量会被自动声明为拥有全局作用域
function outFun2() {
variable = "未定义直接赋值的变量";
var inVariable2 = "内层变量2";
}
outFun2();
console.log(variable); //未定义直接赋值的变量
console.log(inVariable2); //inVariable2 is not defined
虽然variable是在函数内部的,但是它并未被定义
- 所有window对象的属性拥有全局作用域
一般情况下,window对象的内置属性都拥有全局作用域,例如window.name、window.location、window.top等等。
全局变量其实会转化为window对象的属性。
<script>
var a=1
console.log(window.a); //1
</script>
全局作用域在页面打开时创建,页面关闭时销毁。
全局作用域的弊端:如果我们写了很多行 JS 代码,变量定义都没有用函数包括,那么它们就全部都在全局作用域中。这样就会污染全局命名空间, 容易引起命名冲突。
2.局部作用域
也叫做函数作用域,即定义在函数中的变量的集合,拥有局部作用域的变量称为局部变量,全局作用域不能访问到局部作用域中的变量
调用函数时创建函数作用域,函数执行完毕之后,函数作用域销毁,每调用一次函数就会创建一个新的函数作用域,它们之间是相互独立的。
<script>
var num = 10;
function nu(){
var num = 20;
var num2=30
console.log(num);
}
nu();//20
comsole.log(num2)//num2 is not defined
console.log(num);//10
</script>
num2是局部变量,外面访问不到,两个num一个在全局作用域下,另一个在局部作用域下,虽然两个变量的变量名相冲突,但是并没有影响。所以,在不同的作用域下,变量名相同也不受影响,这样就很有效的减少了命名冲突。 jQuery、Zepto 等库的源码,所有的代码都会放在(function(){....})()中。因为放在里面的所有变量,都不会被外泄和暴露,不会污染到外面,不会对其他的库或者 JS 脚本造成影响,这是函数作用域的一个体现。
3.块级作用域
在{}中声明的变量,如if{....},for{...},function{....},当通过新增命令let和const声明时,该变量就有了块级作用域,外面的作用域是不能访问到块级作用域。
function fun() {
var a = 10
console.log(c)
if (3 < 4) {
let c = 20
}
}
fun()//c is not defined
块级变量有如下特点
- 声明变量不会提升到代码块顶部
<script>
console.log(a)
let a = 10
</script>// Cannot access 'a' before initialization
如果是用var来声明,就可以提升变量声明,此时结果是undefined
- 禁止重复声明
如果一个标识符已经在代码块内部被定义,那么在此代码块内使用同一个标识符进行 let 声明就会导致抛出错误。
var count = 30;
let count = 40; // Uncaught SyntaxError: Identifier 'count' has already been declared
但如果在嵌套的作用域内使用 let 声明一个同名的新变量,则不会抛出错误。
var count = 30;
if (condition) {
let count = 40;
// 其他代码
}
- 循环中的绑定块作用域的妙用
开发者可能最希望实现for循环的块级作用域了,因为可以把声明的计数器变量限制在循环内,例如:
for (let i = 0; i < 10; i++) {
// ...
}
console.log(i);
// ReferenceError: i is not defined
上面代码中,计数器i只在for循环体内有效,在循环体外引用就会报错。
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
如果使用let,声明的变量仅在块级作用域内有效,最后输出的是 6。
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
相当于下面这样写:
var a = [];
{ let k;
for (k = 0; k < 10; k++) {
let i = k; //注意这里,每次循环都会创建一个新的i变量
a[i] = function () {
console.log(i);
};
}
}
a[6](); // 6var a = [];
{ let k;
for (k = 0; k < 10; k++) {
let i = k; //注意这里,每次循环都会创建一个新的i变量
a[i] = function () {
console.log(i);
};
}
}
a[6](); // 6
4.作用域链
函数也可以看做是对象,js中有些对象的隐式属性是无法访问到的,[[scope]]是与作用域有关的隐式属性,在函数创建时生成,它是函数存储作用域链的容器
<script>
function a(){
function b(){
var b=1;
}
var a=1;
b();
}
var c=3;
a();
</script>
当函数a被定义时,系统会生成函数a的[[scoped]]属性,该属性会存储函数的作用域链,作用域链的第0位存储当前环境下的全局执行期上下位GO,GO里面存着全局下的所有属性和方法
当函数a被执行时(前一刻), 作用域链的顶端存储a函数生成的函数执行期上下文AO,同时第一位存储GO,查找变量是在函数a中存储的作用域链中从顶端开始向下依次查找
当函数b被定义时, 它也会生成自己的[[scope]]属性存储自己的作用域链,此时的作用域链是和a函数被执行时的作用域链是一样的
当函数b被执行时(前一刻) ,它会生成自己的AO,存储在作用域链的最顶端
函数b执行结束后,它的AO会被销毁,即重新回到被定义的状态
函数a执行完后,它的AO被销毁,因为函数b是在AO中被定义的,所以函数b的[[scope]]也不存在了,函数a回到被定义时的状态
5.闭包
当内部函数被返回到外部并保存时,一定会产生闭包,闭包会产生原来的作用域链不释放,过度的闭包可能会导致内存泄漏或者加载过慢。
<script>
function test1(){
function test2(){
var b=2;
console.log(a);
}
var a=1;
return test2;
}
var c=3;
var test3=test1();
test3();
</script>
test1函数被定义的时候,系统生成[[scope]]属性,存放作用域链,作用域链有GO
当test1被执行时候,test1生成自己的AO,同时test2被定义,生成自己的[[scope]]并存储作用域链
当test1执行结束后,test1的AO并不会被销毁,因为我们把test2函数返回到全局变量test3上,而test2的作用链有test1的AO
当test3执行的时候,test2的作用域链上增加自己的AO,当打印a时,会在作用域链上从上往下找,这时在test1的AO上找到a