JS的作用域和作用域链
之前的文章中,我介绍了关于JSDOM相关的知识,本篇文章将讲述JS的另一部分的知识----作用域(scope)和作用域链
作用域(scope)
1.什么是作用域
作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。即在对应的作用域内,存在着仅在该作用域存在和使用的变量,函数和对象。
举个简单的例子。
function A(){ var a='这是作用域A' console.log(a) } A(); // 这是作用域A console.log(a); // Uncaught ReferenceError: a is not defined
上面的例子中,我们只在A方法中声明了变量a,并未在全局作用域中声明,因此全局作用域并不存在a变量,所以报错。也就是说作用域的存在会将变量进行隔离,不同作用域的同名变量不会相互影响。
ES5中只存在全局作用域和函数作用域,ES6引入了块级作用域的概念,通过let和const来体现。
2.全局作用域和函数作用域
在代码中任何地方都能访问到的对象拥有全局作用域。
函数作用域是指声明在函数内部的变量,和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到。
我们通过几个例子来看一下全局作用域和函数作用域。
var b='这是最外层变量' function B(){ var inb='这是函数作用域B的变量'; function inB(){ // 这是内层函数 console.log(inb); } inB() } console.log(b); // 这是最外层变量 B(); // 这是函数作用域B的变量 console.log(inb) // Uncaught ReferenceError: inb is not defined inB(); // Uncaught ReferenceError: inB is not defined
第一个例子介绍了在最外层的函数和在最外层函数里的变量都拥有全局的作用域,内部的函数可以使用这些最外层变量和最外层函数。
function C(){ c='未定义直接赋值的变量' var inc = '内层变量c' } C(); // 执行函数。 console.log(c); // 未定义直接赋值的变量 console.log(inc); // Uncaught ReferenceError: inc is not defined
第二个例子介绍了未定义直接赋值的变量自动声明为全局变量。
另外,我们知道window拥有着自己的函数和属性,这些函数和属性拥有全局作用域。
全局作用域有一个比较大的缺点:如果我们定义了多个全局变量,将会污染全局命名空间,有可能会产生命名冲突。
作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行。我们看个例子:
function D(){ var d='这是第一层作用域' function D2(){ var d2='这是第二层作用域' console.log(d2); // 这是第二层作用域 console.log('在第二层作用域使用第一层的变量 ---',d); // 在第二层作用域使用第一层的变量 --- 这是第一层作用域 } D2() console.log(d) // 这是第一层作用域 console.log('在第一层作用域使用第二层的变量---',d2); // Uncaught ReferenceError: d2 is not defined } D()
值得注意的是:块语句(大括号“{}”中间的语句),如 if 和 switch 条件语句或 for 和 while 循环语句,不像函数,它们不会创建一个新的作用域。在块语句中定义的变量将保留在它们已经存在的作用域中。
if(true){ var e='块语句变量' } console.log(e) // 块语句变量
这便是变量提升。这在JS中是非成重要和着重注意的一环。
3.块级作用域
在上面提到了,块级作用域实在ES6版本中新增加的一个作用域,通过let和const声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:
- 在一个函数内部。
- 在一个代码块(由一对花括号包裹)内部。
let 声明的语法与 var 的语法一致。你基本上可以用 let 来代替 var 进行变量声明,但会将变量的作用域限制在当前代码块中。块级作用域有以下几个特点:
- 声明变量不会提升到代码块顶部
function f(flag){ if(flag){ let value='let声明的变量' console.log(value); // let声明的变量 }else{ console.log(value); // Uncaught ReferenceError: value is not defined } console.log(value); // Uncaught ReferenceError: value is not defined }
- 禁止重复声明
function g(){ let a='第一个声明' console.log(a); let a="第二个声明" // Uncaught SyntaxError: Identifier 'a' has already been declared console.log(a); } g()
但如果两个声明在不同的嵌套的作用域内的就可以使用
function g() { var a = '第一个声明' console.log(a); if (true) { let a = '第二个声明' console.log(a) } } g()
- 循环中的绑定块作用域的妙用
最常见的情况可能就是多个标签的监听的问题。
例如有多个按钮,点击时控制台打印对应按钮序号。
<button class="btns">1</button>
<button class="btns">2</button>
<button class="btns">3</button>
<script>
var btns=document.getElementsByClassName('btns');
for(var i=0;i<btns.length;i++){
btns[i].onclick=function(){
console.log(i+1)
}
}
</script>
当向上面的代码一样使用var进行for循环时,打印的全部都为‘4’。
但如果将var->let 就会满足我们的要求。
<button class="btns">1</button>
<button class="btns">2</button>
<button class="btns">3</button>
<script>
var btns=document.getElementsByClassName('btns');
for(let i=0;i<btns.length;i++){
btns[i].onclick=function(){
console.log(i+1)
}
}
</script>
作用域链
1.什么是自由变量
首先认识一下什么叫做 自由变量 。
var a ='我是不在函数作用域的变量' function ab(){ var b='我是在函数作用域的变量' console.log(a); // 我是不在函数作用域的变量 console.log(b); // 我是在函数作用域的变量 } ab()
上面的代码中,console.log(a)要得到 a 变量,但是在当前的作用域中没有定义 a(可对比一下 b)。当前作用域没有定义的变量,这成为 自由变量 。
自由变量的值如何得到 —— 向父级作用域寻找。
2. 什么是作用域链
如果父级也没呢?再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是 作用域链 。
var b = '我是全局变量' function B1() { var b1 = '我是b1函数作用域变量' function B2() { var b2 = '我是b2函数作用域变量' function B3() { console.log(b2) // 我是b2函数作用域变量 console.log(b1) // 我是b1函数作用域变量 console.log(b) // 我是全局变量 } B3() } B2() } B1()
3. 关于自由变量的取值
关于自由变量的值,上文提到要到父作用域中取,其实有时候这种解释会产生歧义。
var c = '变量c' function C() { console.log(c) } function show(fn) { var c = '10' (function () { fn() // 变量c 而不是 10 })() } show(C);
在 C函数中,取自由变量 x 的值时,要到哪个作用域中取?——要到创建 fn 函数的那个作用域中取,无论 fn 函数将在哪里调用。
相比而言,用这句话描述会更加贴切:要到创建这个函数的那个域”。
作用域中取值,这里强调的是“创建”,而不是“调用”,切记切记——其实这就是所谓的"静态作用域"
作用域与执行上下文
许多开发人员经常混淆作用域和执行上下文的概念,误认为它们是相同的概念,但事实并非如此。
我们知道 JavaScript 属于解释型语言,JavaScript 的执行分为:解释和执行两个阶段,这两个阶段所做的事并不一样:
解释阶段:
- 词法分析
- 语法分析
- 作用域规则确定
执行阶段:
- 创建执行上下文
- 执行函数代码
- 垃圾回收
JavaScript 解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,但是执行上下文是函数执行之前创建的。执行上下文最明显的就是 this 的指向是执行时确定的。而作用域访问的变量是编写代码的结构确定的。
作用域和执行上下文之间最大的区别是:
执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变。
一个作用域下可能包含若干个上下文环境。有可能从来没有过上下文环境(函数从来就没有被调用过);有可能有过,现在函数被调用完毕后,上下文环境被销毁了;有可能同时存在一个或多个(闭包)。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。

浙公网安备 33010602011771号