作用域决定了变量、常量和参数被定义的时间和位置。函数参数的作用域仅限于函数体中。来看一个例子:
function f(x){
return x + 3;
}
f(5); // 8
x; // ReferenceError: x 未定义
单从代码上来看,x的确存在过(否则它就无法计算出x + 3的值),但是,在函数体外引用x的时候,会报错。因此,x是函数 f 的一个内部变量,它的作用域就是函数f。
当某个变量的作用域是一个给定的函数时,必须要记住:形参只有在函数被调用的时候财讯在(变成实参)。一个函数可能被调用多次:每次函数调用开始时,参数才是真实存在的,在函数返回后参数就失去作用域了。
作用域和存在
当一个变量不存在时,它是没有作用域的。也就是说,尚未声明的变量,或者只存在于函数内部的变量,是没有作用域的。
反过来呢?如果变量没有作用域,就意味着它不存在吗?不一定,这就需要区分作用域和存在这两个不同的概念。
作用域(或者可见性)指的是当前可见并且可以被正在执行的代码块(称作执行上下文)访问的标识符。
存在也指的是标识符,只不过它们保存了已经分配(预留)过内存的数据。
当某些变量不复存在时,JavaScript不一定会立即回收内存:它只是标记这些条目不需要保留,垃圾回收进程会定期去回收。在JavaScript中,不需要关心垃圾回收,垃圾回收是自动化的,除非应用有较高的性能要求,才需要考虑垃圾回收。
作用域
- 作用域:规定变量起作用的范围
- 划分规则:只有全局作用域和函数的私有作用域
- 作用域是根据书写的单词和语法来确定的,所以又叫做词法作用域
- 所以,通常我们把代码写完,就可以确定每一个作用域的范围
注意: script标签之间是全局作用域,多个script标签共享同一个作用域,但是每一个script标签中的变量、函数,它们的提升,只能够在本script标签的范围之内
。
注意: 浏览器在加载script标签的代码的时候,是一个script标签加载并执行完毕之后,再去加载执行后面的一个。
看下面例子:
<script>
var a = 10;
</script>
<script>
console.log(a); // 输出10
</script>
<script>
console.log(a); // ReferenceError: a is not defined
</script>
<script>
var a = 10;
</script>
作用域的机制
作用域是变量的生效的范围。
变量操作又分为使用变量
与 赋值变量
- 使用变量
- 出现在表达式中,赋值语句右侧。
- 访问变量规则
- 当访问变量的时候,会先查看当前作用域中是否存在该变量
- 如果有,就使用,并终止查找。
- 如果没有,就将会向上一层级作用域中寻找。
- 依次向上,直到找到,或者到了全局作用域中还没有找到,就会报错。
- 赋值变量
- 只出现在赋值语句左侧
- 赋值变量规则
- 当对一个变量进行赋值的时候,会先查看当前作用域中是否存在该变量
- 如果有,就赋值。
- 如果没有,就向上一层级查找。
- 依次向上,直到找到,或者到了全局作用域中还没有找到,
就会在全局作用域中悄悄的声明这个变量并赋值
。
// 当前是全局作用域
var num = 100; // 在全局作用域中定义的变量
// 定义一个函数
function demo() {
var num = 101; // 这是在函数作用域中定义的变量
console.log(num);
}
// 执行函数
demo(); // 101
解释:当在函数内部访问变量num的时候,先看当前作用域中是否有num找到了!于是就用101输出101。
var num = 100; // 在全局作用域中定义的变量
// 定义一个函数
function demo() {
// var num; // 因为声明提升的原因 代码其实是这样子的
console.log(num);
var num = 101; // 这是在函数作用域中定义的变量 虽然定义是在这里 但是提升了
}
// 执行函数
demo(); // undefined
解释:依旧是作用域的问题,但是因为输出代码在前面,所以有些同学可能会误以为会输出100 但是不要忘记变量的查找规则与预解析!!!
var num = 100; // 在全局作用域中定义的变量
// 定义一个函数
function demo() {
var num = 101;
}
// 执行函数
demo();
// num是多少
console.log(num); // 100
解释:作用域的问题,因为输出的代码是在全局作用域中的,所以会直接访问全局中的变量,而与函数内部的变量无关。
var num = 100; // 在全局作用域中定义的变量
// 定义一个函数
function demo() {
num = 101;
}
console.log(num); // 100
// 执行函数
demo();
// num是多少
console.log(num); // 101
解释:第一次输出时,函数还没有被调用,所以输出的是全局作用域中的初始值100。在第二次输出前,调用了函数,在被调用的函数里面,num被重新赋值了101,所以第二次输出的是101.
var a = 10;
var b = 15;
function demo(){
console.log(a); // undefined
console.log(b); // 15
var a = 11;
console.log(a); // 11
function demo2(){
console.log(a); // 11
}
demo2();
}
demo();
console.log(a); // 10
解释:首先定义了两个全局变量a和b,但是根据预解析以及查找规则,函数提升了。在函数demo内定义了与全局变量一样的变量名a,但是函数demo内的变量a是在输出语句之后定义的,所以第一个输出语句输出的是undefined。
变量屏蔽(遮蔽效应):指的是如果当前作用域与上层作用域都有同一个变量,那么在当前作用域中访问该变量是,会用当前作用域的,而不会使用上层作用域的。可以理解为把上层作用域中的变量遮挡住了。 这里先看一个两个小例子,注意对比:
var a = 10;
function demo1(){
var a = 11;
console.log(a); // 11
}
demo1();
console.log(a); // 10
var b = 10;
function demo2(){
b = 11;
console.log(b); // 11
}
demo2();
console.log(b); // 11
变量屏蔽
在JavaScript开发中,有一个常常引发混淆的场景,不同作用域中存在相同名字的变量或者常量。而当作用域一个接着一个的时候,这种场景会显得相对直观一些:
function f1(){
var x = "yellow";
console.log(x); // 打印"yellow"
}
f1();
console.log(typeof x); // 打印"undefined",这是因为x不在作用域内
function f2(){
var x = 3;
console.log(x); // 打印 3
}
f2();
console.log(typeof x); //打印"undefined",这是因为x不在作用域内
这个例子很容易理解:有两个互相独立的变量,在不同的作用域中都以x命名。
那么,如果作用域嵌套在一起会发生什么呢?
function f1(){
var x = "yellow";
console.log(x); // 打印"yellow"
function f2(){
var x = 3;
console.log(x); // 打印 3
}
f2();
console.log(x); // 打印"yellow"
}
f1();
console.log(typeof x); //打印"undefined",这是因为x不在作用域内
这个例子演示了变量屏蔽。在内部中,x是一个独立于外部块的(同名)变量,这样实际上屏蔽(或隐藏)了外部定义的x。
这里有一个关键的地方需要理解,当执行到内部块时,一个新的变量x被定义,两个变量都在作用域内:只是没有办法访问到外部块的变量(因为具有相同的变量名),相对而言,在前一个例子中,一个变量x进入作用域,然后退出,接着进入第二个作用域,x做了相同的事情。
为了更好的理解,看下面例子:
function f1(){
var x = "yellow";
var y = x; // y 和 x引用同一个对象
var z = 3;
function f2(){
var x = 5; // 外部 x 被屏蔽
console.log(x); // 5
console.log(y); // 打印"yellow",y(以及x在外部作用域)指向的对象仍存在作用域内
y = "red";
console.log(z); // 3 ,因为 z 没有被屏蔽
console.log(y); // 打印"red"
}
f2();
console.log(z); // 3
}
f1();
变量屏蔽有时候也叫变量阴影(即同一个相同名字的变量会将外部作用域的变量屏蔽起来)。而当一个变量被屏蔽时,这个被屏蔽的变量就无法再通过名字去访问了。