前言
JavaScript(JS)基础对于程序员来说非常重要,尤其是对于前端开发者和全栈开发者。对JS的基础知识的深入理解可以帮助你写出更快、更高效、更少出错的代码。在面试中,经常会现的闭包,对于初学者来说过于抽象。要想学会理解闭包,有一个非常重要的知识点要掌握——————作用域和作用域链。
1.作用域
几乎所有编程语言最基本的功能之一,就是能够储存变量当中的值,并且能在之后对这个值进行访问和修改。而这些变量储存在哪里?最重要的是,程序需要是如何找到他们?这些问题说明需要一套设计良好的规则来存储变量,并且之后可以方便的找到这些变量。这套规则被称为作用域。
Js中包含着三种作用域
1. 全局作用域2. 函数作用域
3. 块级作用域
1.1全局作用域
在函数外部声明的变量拥有全局作用域。这意味着在代码的任何地方都可以访问这些变量。
var globalVariable = "我是全局变量";
function exampleFunction() {
console.log(globalVariable); // 输出: "我是全局变量"
}
exampleFunction();
1.2函数作用域
当在函数内部声明一个变量时,该变量只能在这个函数内部被访问,它是局部作用域或函数作用域的一部分。
function foo(a){
var b =2
function bar(){
}
var c = 3
}
bar() // 失败 ReferenceError错误
console.log(a,b,c) //三个都失败 ReferenceError错误
在这个代码片段中,函数foo(...)的作用域中包含了标识符a、b、c、和bar。因此无法从foo(...)的外部对它们进行访问,也就是说,这些标识符都无法从全局作用域中进行访问。也就是说,这些标识符全都无法从全局作用域中进行访问,因此会导致ReferenceError错误。
1.3块级作用域
使用let和const关键字声明的变量在ES6及以后的版本中具有块级作用域。这意味着它们只能在声明它们的块(例如,if语句或for循环)中被访问。
var foo = true;
if(foo) {
let bar = foo * 2
bar = something(bar)
console.log(bar)
}
console.log(bar) //ReferenceError
let + {} 可以形成块级作用域,上述代码中在外部作用域中无法调用声明在块级作用域中的bar,因此最后输出的结果会发生ReferenceError错误。
2.作用域链
在JavaScript中,作用域链是一个非常核心的概念,它与变量的查找和访问机制密切相关。 作用域链的基本思想是:当代码中的一个变量被访问时,JavaScript会首先在当前作用域中查找这个变量。如果没有找到,它会继续在外层的作用域中查找,直到找到该变量或者到达全局作用域为止。
var global = "我是全局变量"
function outerFoo(){
var outer = "我是外层函数变量"
function innerFoo(){
var inner = "我是内层函数变量"
console.log(global) // 我是全局变量
}
}
在上述代码中,innerFoo()有着自己的一个局部作用域,它还可以访问outerFoo()的作用域和global所在的全局作用域。这三种作用域,形成了作用域链。
总的来说,作用域链是确保JavaScript引擎正确查找变量的机制,而这个查找过程是从当前的执行上下文开始,然后逐级向外部作用域查找,直到全局作用域。/p>
3词法作用域
简单的说,词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况是这样的)
function foo(a){
var b = a * 2
function bar(c){
console.log(a,b,c)
}
bar(b * 3)
}
foo(2) // 2, 4, 12
在这个例子中有三个逐级嵌套的作用域。为了帮助理解,可以将他们想象成几个逐级包含的气泡
- 泡泡1包含着整个全局作用域,其中只有一个标识符:foo。
- 泡泡2包含着foo所创建的作用域,其中有三个标识符:a,bar,b。
- 泡泡3包含着bar所创建的作用域,其中只有一个标识符:c。
作用域气泡由其对应的作用域块代码写在哪里决定,他们是逐级包含的。无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由其被声明时所处的位置决定
3.1欺骗词法
欺骗词法作用域会导致性能下降,在详细解释性能问题之前,先来看看这两种机制分别是什么原理。
3.1.1 eval()
JS中的eval(..)函数可以接受一个字符串为参数,并将其中的内容视为好像在书写中就存在于程序中这个位置的代码。根据这个原理来理解eval(..)是如何通过代码欺骗和假装书写时代码就在那,来实现修改词法作用域环境的。
在执行eval(..)之后的代码时,引擎并不知道或在意前面的代码是以动态形式插入进来,并对词法作用域的环境进行修改。引擎只会如往常地进行词法作用域查找。
function foo(str,a){
eval(str) //欺骗
console.log(a,b)
}
var b = 2
foo("var b = 3",1) //1,3
eval(..)调用中的"var b = 3"这段代码会被当做原本就在那里一样来处理。由于那段代码声明了一个新的变量b,因此它对已经存在的foo(..)的词法作用域进行了修改。当console.log(a,b)被执行时,会在foo(..)内部同时找到a和b,但双永远也无法找到外部的b。因此会输出“1,3”。
3.1.2 with()
with通常被当做重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。
var obj = {
a:1,
b:2,
c:3
}
//单调乏味的重复"obj"
obj.a = 2
obj.b = 3
obj.c = 4
//简单的快捷方式
with(obj){
a = 3
b = 4
c = 5
}
但实际上不仅仅是为了方便地访问对象属性。考虑如下代码:
function foo(obj){
with(obj){
a = 2
}
}
var o1 = {
a = 3
}
var o2 = {
b = 3
}
foo(o1)
console.log(o1.a) //2
foo(o2)
console.log(o2.a) // undefined
console.log(a) //2 ————a被泄露到全局作用域上了!
这个例子中创建了o1和o2两个对象。其中一个具有a属性,另外一个没有。foo(..)函数接受一个obj参数,该参数是一个对象的引用,并对这个对象引用执行了with(obj){..}。在with块内部,我们写的代码看起来只是对变量a进行简单的赋值。但实际上就是声明了一个变量a,并将2赋值给它。 但是可以注意到有一个奇怪的副作用,实际上a = 2 赋值操作创建了一个全局的变量a。
3.1.3性能
JS引擎会在编译阶段进行数项的性能优化,其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速的找到标识符。但如果引擎在代码中发现了eval()或with,它只简单的假设关于标识符位置的判断都是无效的,因为无法在词法分析阶段明确知道eval()会接收到什么代码,这些代码如何对作用域进行修改,也无法知道传递给with用来创建新词法作用域的对象的内容到底是什么
如果代码中大量使用eval()或with,那么运行起来一定会变得非常慢。无论引擎多聪明,试图将这些悲观情况的副作用限制在最小范围内,也无法避免如果没有这些优化,代码会运行得更慢的事实。
总结
当我们讨论JavaScript的作用域时,我们实际上是在讨论一个变量在何处可以被访问。以下是关于JavaScript作用域的总结:
-
词法作用域(静态作用域) :
- JavaScript使用的是词法作用域,也被称为静态作用域。
- 词法作用域是在代码书写的时候就确定了的,而不是在运行时。这意味着变量的作用域是由它在源代码中的位置来决定的。
-
全局作用域:
- 在所有函数之外声明的变量处于全局作用域。
- 全局变量可以在代码的任何地方被访问。
-
局部作用域(函数作用域) :
- 在函数内部声明的变量具有局部作用域。
- 这意味着它们只能在该函数内部被访问和修改。
-
块级作用域:
- 使用
let和const关键字声明的变量在ES6及以后的版本中具有块级作用域。 - 这意味着变量只能在声明它们的块(如if语句或for循环)中被访问。
- 使用
-
作用域链:
- 当在一个函数内部访问一个变量时,JavaScript会首先在当前作用域查找。如果没有找到,它会继续在上一级的外层作用域查找,直到达到全局作用域。
- 这个查找过程形成了所谓的作用域链。