Skip to content

JavaScript 作用域

作用域

作用域规定了变量能够被访问的范围,离开了这个范围变量便不能被访问,作用域分为:

  • 局部作用域(模块作用域、函数作用域、块级作用域)
  • 全局作用域

局部作用域

局部作用域分为函数作用域块级作用域

函数作用域:

  • 函数内部声明变量只能在函数内部访问,外部无法直接访问
  • 函数的参数也是函数内部的局部变量
  • 函数执行完毕后,函数内部的变量会被回收
js
function counter(x, y) {
  // 函数内部声明的变量
  const s = x + y
  console.log(s) // 18
}
counter(10, 8)
// 访问变量 s
console.log(s)// 报错

块级作用域:

  • 使用 {} 包裹的代码称为代码块。代码块内部声明的变量,外部可能无法访问
  • let、const声明的变量会产生块作用域,var 不会产生块级作用域
  • 不同代码块之间的变量无法互相访问
js
{
  // 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标签共享一个全局作用域
html
<!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代码如下

js
// 这个变量同样会作为全局对象window的属性
functionVar = 'This is a function variable in the global scope'

// 调用一个全局函数
function globalFunction() {
  console.log(globalVar) // 可以访问在第一个<script>标签中声明的全局变量
}

globalFunction()

作用域链

嵌套关系的作用域串联起来形成了作用域链,作用域链本质是底层的变量查找机制

  • 在函数被执行时,会优先查找当前函数作用域中查找变量
  • 如果当前作用域查找不到,则会依次逐级查找父级作用域直到全局作用域
  • 子作用域能够访问父作用域,父级作用域无法访问子级作用域
js
//全局作用域
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)引入的一个概念,与 letconst 声明方式有关

工作机制:

  • 初始化前访问:在 letconst 声明的变量被初始化之前,对它们的访问将会导致 ReferenceError
  • 块级作用域letconst 声明的变量只在它们的声明块内有效,这意味着它们在声明它们的块的开始到结束之间是不可访问的
  • 暂时性死区:从代码块的开始到 let/const 声明执行点之间的区域被称为 TDZ。在这个区域内,尝试访问这些变量将会导致错误
js
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,则释放内存

如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,从而导致内存泄露

标记清除法

  • 现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体上一致
  • 过程:
    • 标记清除法将不再使用的对象定义为无法达到的对象
    • 从根部(全局对象)出发定时扫描内存中的对象。 能从根部到达的对象,则是仍在使用的对象
    • 那些无法由根部出发触及到的对象被标记为不再使用,稍后会被回收

image-20230522150049358

垃圾回收机制详细文档:https://javascript.info/garbage-collection

闭包

在 JavaScript 中,函数总是可以访问创建它的上下文,这就叫做closure。一个普通的函数,如果它可以访问外层作用域的自由变量,那么这个函数就是一个闭包

  • 从广义的角度:JavaScript 中的函数都是闭包
  • 从狭义的角度:JavaScript 中一个函数,如果访问了外层作用域的变量,那么它是一个闭包

严格上说,只有满足以下条件时,才能算是闭包:

  • 访问外部变量:函数必须引用在它外部定义的变量(自由变量)
  • 外部变量的生命周期:即使外部函数已经执行完毕,闭包仍然能够访问这些外部变量
  • 延迟释放:由于闭包持有对外部变量的引用,这些变量不会被垃圾回收机制回收,直到闭包本身也被销毁
js
function createCounter() {
  let count = 0 // 这是一个自由变量
  return function () {
    count += 1 // 闭包可以访问并修改自由变量
    return count
  }
}

let counter = createCounter()
console.log(counter()) // 1
console.log(counter()) // 2

闭包可以用于:实现数据的私有化、函数柯里化、数据缓存

javascript
// 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)
  • 变量提升出现在相同作用域当中
js
//原代码
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 构造器创建的函数并不会被提升
js
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')
}

函数声明和变量声明使用同一个变量名称时,目前有两种说法,但不影响最终结果:

  • 与优先级有关:函数的优先级高于变量的优先级
  • 与优先级无关:根据声明顺序,会发生覆盖,但由于变量提升只提升声明,纵使变量声明在函数声明后,也不影响最终结果,因为只覆盖了原本的函数名,由于同名,所以没有影响
js
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

最近更新