JavaScript 作用域
作用域
作用域规定了变量能够被访问的范围,离开了这个范围变量便不能被访问,作用域分为:
- 局部作用域(模块作用域、函数作用域、块级作用域)
- 全局作用域
局部作用域
局部作用域分为函数作用域
和块级作用域
函数作用域:
- 函数内部声明变量只能在函数内部访问,外部无法直接访问
- 函数的参数也是函数内部的局部变量
- 函数执行完毕后,函数内部的变量会被回收
function counter(x, y) {
// 函数内部声明的变量
const s = x + y
console.log(s) // 18
}
counter(10, 8)
// 访问变量 s
console.log(s)// 报错
块级作用域:
- 使用
{}
包裹的代码称为代码块。代码块内部声明的变量,外部可能无法访问 let、const
声明的变量会产生块作用域,var 不会产生块级作用域- 不同代码块之间的变量无法互相访问
{
// age 只能在该代码块中被访问
let age = 18
console.log(age) // 正常
}
for (let t = 1; t <= 6; t++) {
// t 只能在该代码块中被访问
console.log(t) // 正常
console.log(age) // 报错
}
console.log(age) // 报错
console.log(t) // 报错
全局作用域
全局作用域(Global Scope)指的是在整个脚本范围内都可访问的变量的作用域。全局变量在任何函数或块内都可以被访问和修改,因此它们是全局可访问的
- 全局作用域中的变量实际上是全局对象的属性。在浏览器环境中,全局对象是
window
,而在Node环境中,全局对象是global
- 函数内部未使用任何关键字声明的变量为全局变量(严格模式下报错)
- 页面中的
script
标签共享一个全局作用域
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>全局作用域示例</title>
</head>
<body>
<script>
"use strict"
// 这是一个全局变量,它也是window对象的一个属性
var globalVar = 'I am a global variable'
// 尝试在严格模式下声明一个全局变量
anotherGlobalVar = 'This will cause an error in strict mode' // 报错
</script>
<script src="./externalScript.js"></script> <!-- 引入一个外部脚本文件 -->
</body>
</html>
externalScript.js
代码如下
// 这个变量同样会作为全局对象window的属性
functionVar = 'This is a function variable in the global scope'
// 调用一个全局函数
function globalFunction() {
console.log(globalVar) // 可以访问在第一个<script>标签中声明的全局变量
}
globalFunction()
作用域链
嵌套关系的作用域串联起来形成了作用域链,作用域链本质是底层的变量查找机制
- 在函数被执行时,会优先查找当前函数作用域中查找变量
- 如果当前作用域查找不到,则会依次逐级查找父级作用域直到全局作用域
- 子作用域能够访问父作用域,父级作用域无法访问子级作用域
//全局作用域
let a = 1
//局部作用域
function f () {
let a = 2
//局部作用域
function g () {
a = 3
console.log(a) //3
}
g()
console.log(a) //3
}
f()
console.log(a) //1
暂时性死区
TDZ(Temporal Dead Zone)它是 ECMAScript 6(ES6)引入的一个概念,与 let
和 const
声明方式有关
工作机制:
- 初始化前访问:在
let
和const
声明的变量被初始化之前,对它们的访问将会导致ReferenceError
- 块级作用域:
let
和const
声明的变量只在它们的声明块内有效,这意味着它们在声明它们的块的开始到结束之间是不可访问的 - 暂时性死区:从代码块的开始到
let
/const
声明执行点之间的区域被称为 TDZ。在这个区域内,尝试访问这些变量将会导致错误
function test() {
// 在 TDZ 内,尝试访问 let 声明的变量将会导致错误
console.log(letVar) // ReferenceError: letVar is not defined、
// letVar 声明并初始化
let letVar = 5
// 在此之后,letVar 可以正常访问
console.log(letVar) // 5
}
垃圾回收机制
垃圾回收机制(Garbage Collection)简称 GC,是一种自动管理内存的方式。JS 中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收,如果内存没有被及时回收,就会造成内存泄露(本该被释放的内存没有被及时释放)
内存的生命周期
JS 环境中分配的内存, 一般有如下生命周期:
- 内存分配:当我们声明变量、函数、对象的时,系统会自动分配内存
- 内存使用:读写内存,也就是使用变量、函数等
- 内存回收:使用完毕,由垃圾回收自动回收不再使用的内存
全局变量一般不会回收,在页面关闭时才回收。局部变量会在不使用时被自动回收
垃圾回收机制算法
堆栈空间分配区别:
- 栈:由操作系统自动分配释放函数的参数值、局部变量等
- 堆:一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收
常见的浏览器垃圾回收算法主要有两种:引用计数法和标记清除法
引用计数:
- IE采用的引用计数算法,定义“内存不再使用”,看一个对象是否有指向它的引用,没有引用就回收对象
- 过程:
- 跟踪记录被引用的次数
- 如果被引用了一次,那么就记录次数 1,多次引用会累加
- 如果减少一个引用就减 1
- 如果引用次数为 0,则释放内存
如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,从而导致内存泄露
标记清除法:
- 现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体上一致
- 过程:
- 标记清除法将不再使用的对象定义为无法达到的对象
- 从根部(全局对象)出发定时扫描内存中的对象。 能从根部到达的对象,则是仍在使用的对象
- 那些无法由根部出发触及到的对象被标记为不再使用,稍后会被回收
垃圾回收机制详细文档:https://javascript.info/garbage-collection
闭包
在 JavaScript 中,函数总是可以访问创建它的上下文,这就叫做closure
。一个普通的函数,如果它可以访问外层作用域的自由变量,那么这个函数就是一个闭包
- 从广义的角度:JavaScript 中的函数都是闭包
- 从狭义的角度:JavaScript 中一个函数,如果访问了外层作用域的变量,那么它是一个闭包
严格上说,只有满足以下条件时,才能算是闭包:
- 访问外部变量:函数必须引用在它外部定义的变量(自由变量)
- 外部变量的生命周期:即使外部函数已经执行完毕,闭包仍然能够访问这些外部变量
- 延迟释放:由于闭包持有对外部变量的引用,这些变量不会被垃圾回收机制回收,直到闭包本身也被销毁
function createCounter() {
let count = 0 // 这是一个自由变量
return function () {
count += 1 // 闭包可以访问并修改自由变量
return count
}
}
let counter = createCounter()
console.log(counter()) // 1
console.log(counter()) // 2
闭包可以用于:实现数据的私有化、函数柯里化、数据缓存
// curry 函数的实现
function curry (fn) {
return function judge (...args) {
if (args.length === fn.length) {
return fn(...args)
}
return function (...argus) {
return judge(...args, ...argus)
}
}
}
提升
变量提升
变量提升(变量悬挂)是 JavaScript 中比较奇怪的现象,它允许在变量声明之前即被访问,其本质为:把var
变量提升到当前作用域于最前面,只提升变量声明, 不提升变量赋值
特点:
- 变量在
var
声明之前被访问,变量的值为undefined
,即变量提升只提升声明,不提升赋值 let/const
声明的变量不存在变量提升,未被声明就使用会报语法错误(TDZ)- 变量提升出现在相同作用域当中
//原代码
console.log(a) // undefined
var a = 1
//代码实际执行过程
var a
console.log(a)
a = 1
//原代码
a = 2
var a
console.log(a) //2
//代码实际执行过程
var a
a = 2
console.log(a)
函数提升
函数提升与变量提升比较类似,指的是函数在声明之前即可被调用
特点:
function
关键字声明的函数会被提升到当前作用域的最前面,且函数提升是整体提升- 函数表达式不存在函数提升的现象,但
var
声明的变量会发生变量提升 - 使用
Function
构造器创建的函数并不会被提升
fn() // fn: hello
function fn() {
console.log('fn: hello')
}
fun() // 报错
let fun = function() {
console.log('fun: hello')
}
console.log(func) // undefined
func() // 报错
var func = function () {
console.log('func: hello')
}
函数声明和变量声明使用同一个变量名称时,目前有两种说法,但不影响最终结果:
- 与优先级有关:函数的优先级高于变量的优先级
- 与优先级无关:根据声明顺序,会发生覆盖,但由于变量提升只提升声明,纵使变量声明在函数声明后,也不影响最终结果,因为只覆盖了原本的函数名,由于同名,所以没有影响
console.log(fx) // fx定义的函数
var fx = 'fx'
function fx () {
console.log('fx is a function')
}
console.log(fx) // fx
// 与以上结果一样
console.log(fx) // fx定义的函数
function fx () {
console.log('fx is a function')
}
var fx = 'fx'
console.log(fx) // fx
原理总结
JavaScript 引擎在执行代码之前,会进行一个预处理阶段,此阶段会处理所有变量和函数的声明。这意味着所有的 var
变量声明和函数声明(包括函数表达式和预声明的函数)都会在这个阶段被处理。
- 对于变量声明,JavaScript 引擎会将它们移动到当前作用域的顶部。但只有变量的声明会被提升,初始化(赋值)会保留在原位置
- 对于函数声明,仅通过
function
关键字声明的函数会被提升到它们所在作用域的顶部。函数表达式只会提升变量,不会提升函数本身
JavaScript 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined