JavaScript模块化与作用域
介绍JavaScript作用域,包括全局作用域、函数作用域和块级作用域,以及ES6+新增的let、const和block scope等特性。
作用域和作用域链 - 静态
- 作用域:一个代码段所在的区域
- 作用:绑定变量在这个作用域,隔离变量,不同作用域下同名变量不会有冲突。
- 作用域链:多个作用域嵌套,就近选择,先在自己作用域找,然后去就近的作用域找。
全局作用域
在代码的任何地方都能访问到的变量被定义在全局作用域。在浏览器环境中,全局变量被定义在window对象上。
函数作用域
在函数内部定义的变量只能在函数内部访问,这就是函数作用域。这意味着,如果你在一个函数内部定义了一个变量,那么这个变量在函数外部是不可见的。
块级作用域
ES6引入了两种新的声明方式:let和const,它们与var相比,最大的区别就是它们具有块级作用域。块级作用域是指变量在最近的{}代码块内有效。
let和const
let和const都是块级作用域,它们的作用范围被限制在最近的一对花括号{}内。let允许你重新赋值,而const定义的是一个常量,一旦赋值就不能改变。
执行上下文
抽象当前JavaScript的执行环境,包括变量、this指向等信息。每当JavaScript开始执行时,都在指向上下文中运行。
执行上下文的两个阶段:创建阶段和执行阶段
在创建阶段JavaScript将var和function声明移到顶层。
在全局代码执行前,JS引擎就会创建一个栈来存储管理所有的执行上下文对象。在全局上下文(window)确定后,将其添加到栈中(压栈)。在函数执行上下文创建之后,将其添加到栈中(压栈)。当前函数执行完成后,将栈顶的对象移除(出栈)。当所有的代码执行完成后,栈中只剩下window。
变量提升
通俗来说,变量提升是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的行为。变量被提升后,会给变量设置默认值为 undefined。
在 ECMAScript6 之前,JS 引擎用 var 关键字声明变量。在 var 时代,不管变量声明是写在哪里,最后都会被提到作用域的顶端。 下面在全局作用域中声明一个num 变量,并在声明之前打印它:
console.log(num)
var num = 1
// 输出undefined,因为变量的声明被提升了
除此之外,在函数作用域中也存在变量提升;
function getNum(){
console.log(num)
var num = 1
}
getNum()
// 输出undefined,因为函数内部的变量声明会被提升至函数作用域的顶端。
函数提升:
// 函数声明式:
function foo(){}
// 变量形式声明:
var fn = function() {}
变量提升导致的问题
由于变量提升的存在,使用JavaScript来编写的其他语言逻辑代码,都有可能会导致不一样的执行结果。主要有两种.
① 变量被覆盖
var name = 'JavaScript'
function showName(){
consloe.log(name)
if(0){
var name = "CSS"
}
}
showName()
这里会输出undefined,并没有输出“JavaScript”?
首先,当刚执行 showName 函数调用时,会创建 showName 函数的执行上下文。之后,JavaScript 引擎便开始执行 showName 函数内部的代码。首先执行的是:consloe.log(name)。执行这段代码需要使用变量 name,代码中有两个 name 变量:一个在全局执行上下文中,其值是JavaScript;另外一个在 showName 函数的执行上下文中,由于if(0)永远不成立,所以 name 值是 CSS。应该先使用函数执行上下文中的变量。 因为在函数执行过程中,JavaScript 会优先从当前的执行上下文中查找变量,由于变量提升的存在,当前的执行上下文中就包含了if(0)中的变量 name,其值是 undefined,所以获取到的 name 的值就是 undefined。
这里输出的结果和其他支持块级作用域的语言不太一样,比如 C 语言输出的就是全局变量,所以这里会很容易造成误解。
② 变量没有被销毁
function foo() {
for(var i =0;i<5;i++){}
console.log(i);
}
foo()
使用其他的大部分语言实现类似代码时,在 for 循环结束之后,i 就已经被销毁了,但是在 JavaScript 代码中,i 的值并未被销毁,所以最后打印出来的是 5。这也是由变量提升而导致的,在创建执行上下文阶段,变量 i 就已经被提升了,所以当 for 循环结束之后,变量 i 并没有被销毁。
禁用变量提升
为了解决上述问题,ES6 引入了 let 和 const 关键字,从而使 JavaScript 也能像其他语言一样拥有块级作用域。let 和 const 是不存在变量提升的。下面用 let 来声明变量:
console.log(num)
letnum =1
// 输出结果:Ucaught ReferenceError:numis not defined。 const 声明,也会是一样的结果——用 let 和 const 声明的变量,它们的声明生效时机和具体代码的执行时机保持一致。
ES6 是如何通过块级作用域来解决上面的问题:
function fn(){
var num = 1;
if(true) {
var num = 2;
console.log(num); // 2
}
console.log(num); // 2
}
fn()
在这段代码中,有两个地方都定义了变量 num,函数块的顶部和 if 的内部,由于 var 的作用范围是整个函数,所以在编译阶段只生成了一个变量 num,函数体内所有对 num 的赋值操作都会直接改变变量环境中的 num 的值。
把 var 关键字替换为 let 关键字,看看效果:
function fn(){
let num = 1;
if(true) {
let num = 2;
console.log(num); // 2
}
console.log(num); // 1
}
fn()
// 因为 let 关键字是支持块级作用域的,所以,在编译阶段 JavaScript 引擎并不会把 if 中通过 let 声明的变量存放到变量环境中,这也就意味着在 if 中通过 let 声明的关键字,并不会提升到全函数可见。
JS如何支持块级作用域
- 创建执行上下文
通过 var 声明的变量,在编译阶段会被存放到变量环境中。通过 let 声明的变量,在编译阶段会被存放到词法环境中。在函数作用域内部,通过 let 声明的变量并没有被存放到词法环境中。 - 执行代码
当作用域块执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出。块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript 引擎就同时支持了变量提升和块级作用域。
作用域链
当你在一个函数内部尝试访问一个变量时,JavaScript会首先在当前的作用域查找。如果没有找到,它会去外层的作用域查找,直到找到为止。这就是作用域链。
var outerVar = "I'm outer!";
function innerFunction() {
console.log(outerVar); // 输出 "I'm outer!"
}
innerFunction();
闭包
闭包是JavaScript中一个重要的概念。当一个函数能够记住并访问所在的词法作用域,即使该函数在词法作用域外部执行,这就产生了闭包。
functionouterFunction() {
var outerVar = "I'm outer!";
function innerFunction() {
console.log(outerVar); // 输出 "I'm outer!"
}
return innerFunction;
}
var myFunction = outerFunction();
myFunction(); // 即使在outerFunction()执行完后,innerFunction()仍然可以访问outerVar,这就是闭包