前言
- 在javascript中,有着一种被称为作用域(scope)的特性。它也是理解闭包的前置知识,本文着重于理解作用域,以便在下一篇文章中,学习并理解闭包。
作用域(Scope)
作用域是什么?
- 作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期(和函数作用域、块级作用域相关)。通俗的理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。而作用域又分为全局作用域、函数作用域和块级作用域。
1.全局作用域
- 一个 html 页面就是一个全局作用域,打开页面的时候, 作用域就生成了, 关闭页面,全局作用域关闭。任何地方都能访问到的对象拥有全局作用域。在函数外面定义的变量就属于全局变量,拥有全局作用域。此外,window的属性也拥有全局作用域。
例如下段代码中, i 就存在于全局作用域。
var i = 1;
function fun() {
console.log(i);
}
fun()//输出1
复制代码
学习到这里时,突然想到一个有意思的问题:在不通过 get/post 等方式提交的情况下,如何将全局作用域如何跨页面引用到变量呢?
向老师的讨教后,终于得到了想要的结果。
在一个HTML文件里script
块中写下
var name = '小王';
window.localStorage.setItem('username',name)//本地储存
复制代码
打开浏览器控制台输入 name 得到下图,关闭当前HTML窗口
在另外一个HTML文件script
块中写下
var name = localStorage.getItem('username');
console.log(name);//小王
复制代码
打开浏览器控制台得到下图
以上示例利用了本地存储,能够跨页面引用到变量。
2.函数作用域
《你不知道的JavaScript》这样描述函数作用域:
1.函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。
2.外部作用域无法访问包装函数内部的任何内容。
我们首先来看一个例子
function foo(a) {
var b = 2;
console.log(b);
console.log(a);
function bar() {
var c = 3;
console.log(a);
console.log(b);
console.log(c);
}
bar()
}
// console.log(a,b,c);//三个全部失败
// console.log(b);//b is not defined
// bar()// bar is not defined
// foo(1)//2,1,1,2,3
复制代码
- 通过以上结果可以得到,a,b,c,bar都在
foo(..)
函数里面,无法从foo(..)
外部进行访问,也就是说无法从全局作用域中进行访问。但是这些标识符在foo(..)
内部是可以被访问的,同样在bar(..)
内部也可以访问(假设在bar
内部没有同名的标识符声明)。
由此我们可以总结:
- 外部作用域无法访问包装函数内部的任何内容。
- 对于一个嵌套的作用域,内部的作用域可以访问外部作用域的内容。
函数作用域的功能
- 隐藏内部实现
- “隐藏内部实现”就是从所写的代码中挑选一个任意的片段,然后用函数声明对它进行包装,把这些代码“隐藏”起来,也就是说,这段代码的任何声明(变量或函数)都将绑定在这个新创建的包装函数的作用域中,而不是先前所在的作用域中。
- 如果所有变量和函数都在全局作用域中,会暴露过多的变量或函数,而这些变量或函数本该是私有的,正确的代码应该是可以阻止对这些变量或函数进行访问的。
- 例
function doSomething(a) { b = a + doSomethingElse(a * 2); console.log(b * 3); } function doSomethingElse(a) { return a - 1; } var b; doSomething(2);//15 复制代码
代码片段中,
b
和doSomethingElse(..)
应该是doSomething(..)
内部具体实现的“私有”内容。放在外部作用域对b
和doSomethingElse(..)
的“访问权限”不仅没有必要,还有危险。利用“隐藏内部实现”可以“合理”的将这些私有变量隐藏在doSomething(..)
内部,例如function doSomething(a) { function doSomethingElse(a) { return a - 1; } var b; b = a + doSomethingElse(a * 2); console.log(b * 3); } doSomething(2);//15 复制代码
- 规避冲突
- 我们先看一个例子
function foo() {
function bar(a) {
i = 3;//声明一个本地变量(var i = 3;),即修改了for循环所属作用域中的 i
console.log(a + i);
}
for (var i = 0; i < 10; i++){
bar(i * 2);//无限循环
}
}
foo();
复制代码
bar(..)
内部的赋值表达式i = 3
覆盖了声明在foo(..)
内部 for 循环中的i
,使i
被固定设置为3,永远满足小于10这个条件。- 如果将
bar(..)
内i = 3
设置为var i = 3
同名变量或设置为var j = 3
这样的不同名变量,就可以避免错误发生。究其原因在于,var i = 3
会为bar(..)
作用域内声明一个本地变量。若此处本地变量名仍为 i ,则也可以叫它为“遮蔽变量”。
3.块级作用域
- 尽管函数作用域是最常见的作用域单元,但其他类型的作用域单元也是存在的,并且通过使用其他类型的作用域单元甚至可以实现维护起来更加优秀,简洁的代码。
在介绍块级作用域之前,我们通过 var
和 let
来做一个小实验
let 情况下
{
let i = 1;
}
console.log(i);//i is not defined
复制代码
var 情况下
{
var c = 2;
}
console.log(c);//2
复制代码
同样是声明一个变量,并用 { }
将其放入,用 let
和 var
声明的变量在 { }
之外产生的效果完全不同。
接下来,我们带着问题,进入块级作用域的学习。
《你不知道的javascript》里解释块级作用域:
块作用域是一个用来对之前的最小授权原则进行扩展的工具,将代码从在函数中隐藏信息扩展为在块中隐藏。
我们再次考虑前面的问题,为什么 let 声明的变量i在用 { }
包裹起来之后,在全局作用域调用i
会报错,显示i
未定义呢?
- 原因就是
i
在块级作用域之中,当你在运行console.log(i)
时,i
已经结束了他的运行时间。
那么又有一个问题随之而来,为什么 var
定义的变量 i
成功定义了呢?
- 其实,
let
可以理解为更完美的var
。var
声明的变量的作用域是整个封闭函数。而let
声明的变量的作用域只是外层块,而不是整个外层函数。
理解了上面内容后,我们通过 let
来理解块级作用域
let
关键字可以将变量绑定到所在的任意作用域中。换句话说,let
为其声明的变量隐式劫持了所在的块作用域。
那么,块级作用域的概念就呼之欲出了:
- 块级作用域是为了使变量的声明离使用的地方接近,最大限度的本地化,防止一个变量污染到整个函数作用域或全局作用域中。
如何创建一个块级作用域
这里举几个例子
let
- let 关键字可以将变量变动到所在任意作用域(通常是 {..} 内部),换句话说, let 为其声明的变量隐式的劫持了所在的作用域。
with关键字
- 用with从对象中创建出的作用域仅在with声明中而非外部作用域中
try/catch
-
try/catch的catch分句会创建一个块作用域,其中声明的变量仅在catch中内部有效。
总结
- 作用域是指在程序中定义变量的区域。
- 有三种作用域别是全局作用域、函数作用域、块级作用域。
- 在函数外面定义的变量就属于全局变量,拥有全局作用域
- 函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用),外部作用域无法访问包装函数内部的任何内容。
- 块级作用域是为了使变量的声明离使用的地方接近,最大限度的本地化,防止一个变量污染到整个函数作用域或全局作用域中。可以用 let 等关键字创建块级作用域。
注:本文是博主初学JS所学知识,有错误之处在所难免,且有多处知识点未解释明白,欢迎大佬指正补充,本人也会定期更新新的文章以及修改前文错误,欢迎大家来评论区一起讨论学习~