一、作用域
1、基本概念
是什么?
指变量、对象和函数在【代码中的可访问性范围】。
有什么用?
理解作用域对【编写高效和无错误的代码】至关重要
分类
局部作用域(函数作用域、块作用域)、全局作用域
涉及到那些知识点
作用域链、JS垃圾回收机制、闭包、变量提升等。
2、全局作用域
是什么?
在代码的任何地方都可以访问的变量和函数。
怎么产生?
(1)【<script>标签和.js文件的最外层】声明的变量和函数
(2)【window 对象动态添加的属性】默认也是全局的
注意事项
实际开发中尽可能少的声明全局变量、防止全局变量被污染
3、局部作用域
【1】函数作用域
是什么?
只能在【变量或函数 所处的 函数内部】被访问的变量和函数。
有什么用?
用于限制变量的可见性,避免与【其他函数或全局作用域中的变量产生冲突】
怎么产生?
(1)【函数内部】声明的变量和函数
(2)【函数的参数】 也是函数内部的局部变量
【2】块作用域
是什么?
块级作用域是【指在 {} 代码块内部】声明的变量(var声明的除外 、var 声明的变量不会产生块级作用域),在该块外部无法访问。
怎么产生?
【在 {} 代码块内部】声明的变量(var声明的除外)
示例
// var 声明变量 不形成块级作用域、 i 是全局作用域的
for(var i = 1; i < 10; i++) {
console.log(i, 'i');
setTimeout(() => {
console.log(i, 'setTimeout i');
}, 100)
}
// let 声明变量 形成块级作用域
for(let i = 1; i < 10; i++) {
console.log(i, 'i');
setTimeout(() => {
console.log(i, 'setTimeout i');
}, 100)
}
第一段代码输出结果:
- 先打印
1
到9
,每个数字后面跟着'i'
。 - 当
setTimeout
的回调函数执行时,由于var
使得i
是全局作用域的,最终i
的值是10
,所以将会打印10
,并且会重复打印 9 次。
第二段代码输出结果:
- 先打印
1
到9
,每个数字后面跟着'i'
。 - 由于
let
使得i
在每次循环迭代中都具有块作用域,因此在setTimeout
执行时,每个回调函数的i
都是当前迭代的值。
二、作用域链
基本概念
作用域链本质是【底层的变量查找机制】
查找原则
在函数被执行时、会优先查找【当前函数作用域中查找变量】
在同一作用域内,变量按【声明的顺序进行查找】。
如果当前作用域查找不到则会【依次逐级查找父级作用域】直到全局作用域
三、变量提升
1、基本概念
变量提升 JavaScript 的一个机制,JavaScript 引擎在执行代码时,会先查找 函数 和 var 声明的变量 提升到当前所处作用域顶部。
***变量提升只提升声明,不提升赋值***
2、注意点
函数提升
(1) 函数表达式不存在提升的现象
(2) 函数提升出现在相同的作用域中
变量提升
(1) 变量在未声明即被访问时会报语法错误
(2) 变量在 var 声明之前即被访问、变量的值为 undefined
(3) let/const 声明的变量不存在变量提升
(4) 变量提升出现在当前相同作用域当中
(5) 实际开发中推荐先声明再访问变量
3、DEMO
// 全局作用域
var globalVar = "I'm a global variable";
function testHoisting() {
console.log(globalVar); // 输出: "I'm a global variable"
// 变量提升
console.log(localVar); // 输出: undefined (因为声明被提升)
var localVar = "I'm a local variable";
console.log(localVar); // 输出: "I'm a local variable"
// 函数提升
console.log(innerFunction()); // 输出: "Inner function called!"
function innerFunction() {
return "Inner function called!";
}
// let 和 const 的提升
try {
console.log(letVar); // ReferenceError: Cannot access 'letVar' before initialization
} catch (e) {
console.log(e.message); // 捕获错误信息
}
let letVar = "I'm a let variable";
const constVar = "I'm a const variable";
console.log(letVar); // 输出: "I'm a let variable"
console.log(constVar); // 输出: "I'm a const variable"
}
testHoisting();
console.log(globalVar); // 输出: "I'm a global variable"
四、JavaScript 的垃圾回收机制
基本概念
JavaScript 的垃圾回收机制是指【JavaScript引擎会自动检测和清理不再使用的内存空间】,以【防止内存泄漏和提高性能】。JavaScript 使用一种自动内存管理的方式,开发者无需手动释放内存。
内存的生命周期
(1) 【内存分配】: 当我们声明变量、函数、对象的时候、系统会自动为他们分配内存
(2) 【内存使用】: 即读写内存、也就是使用变量、函数等
(3) 【内存回收】: 使用完毕、由【垃圾回收器】自动回收不再使用的内存
注意点
(1) 全局变量一般不会回收(关闭页面回收)
(2) 一般情况下【局部变量的值】、不用了、会被【自动回收】掉
内存泄漏
程序中分配的【内存】由于某种原因程序【未释放】或【无法释放】叫做内存泄漏
JavaScript 垃圾回收机制算法
1. 引用计数法 (早期少数浏览器使用)
原理:
- 每个对象维护一个引用计数器,记录有多少个引用指向该对象。
- 当一个对象被引用时,引用计数加一;当引用被移除时,引用计数减一。
- 当引用计数为零时,说明没有任何引用指向该对象,可以安全地释放它的内存。
优点:
- 实时性:能立即回收不再使用的对象,减少内存占用。
缺点:
- 循环引用问题:如果两个对象相互引用,即使它们不再被其他对象引用,它们的引用计数也不会变为零,从而导致内存泄漏。
2. 标记清除法 (大多数现在浏览器使用)
原理:
- 标记阶段:从根对象(如全局对象、活动函数的变量等)开始,递归地遍历所有可达的对象,并将其标记为“活着”。
- 清除阶段:遍历所有对象,将那些未被标记的对象视为“死去”的,释放它们所占用的内存。
优点:
- 能够处理循环引用的问题,因为只要对象不可达,就会被回收。
缺点:
- 性能开销:标记和清除的过程可能会消耗较多的 CPU 资源,导致应用程序在垃圾回收期间可能出现性能波动。
五、闭包
基本概念
闭包(closure)是一个函数以及其捆绑的周边环境状态的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。
简单来讲、一个函数 内部函数 引用了 外部函数的变量就会形成闭包。
作用
1、" 保护"作用
说明:
闭包可以创建私有变量,避免外部访问,从而保护变量不被随意修改。这种封装特性使得数据更安全,减少了全局作用域污染。
示例:
count
是一个私有变量,只有通过 increment
、decrement
和 getCount
方法才能访问和修改,保护了变量的安全性。
function createCounter() {
let count = 0; // 私有变量
return {
increment: function() {
count++;
console.log(count);
},
decrement: function() {
count--;
console.log(count);
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
counter.increment(); // 输出: 1
counter.increment(); // 输出: 2
console.log(counter.getCount()); // 输出: 2
// 直接访问 count 是不可能的
// console.log(counter.count); // undefined
2、" 保存"作用
说明:
闭包能够保存外部函数的变量状态,即使外部函数已经执行完毕。这使得我们可以在异步操作或定时器中保留某个状态。
示例:
timerId
被闭包保护并保存。即使 makeTimer
函数执行结束,timerId
的值依然可以被 start
和 stop
方法访问和修改。
function makeTimer() {
let timerId = 0;
return {
start: function() {
timerId = setInterval(() => {
console.log(`Timer ID: ${timerId}`);
}, 1000);
},
stop: function() {
clearInterval(timerId);
console.log('Timer stopped');
}
};
}
const timer = makeTimer();
timer.start(); // 每秒输出 Timer ID
setTimeout(() => {
timer.stop(); // 3秒后停止计时器
}, 3000);
产生的问题
内存泄漏
闭包会保持对其外部作用域的引用,javaScript 的垃圾回收机制无法清除释放内存。从而产生内存泄漏问题、故使用闭包时需及时清理引用。
function createCounter() {
let count = 0;
return {
increment: function() {
count++;
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
// 使用完后,清理引用
counter.increment = null;
变量状态混淆
当多个闭包引用同一个外部变量时,可能会导致所有闭包共享该变量的最终值。
const functions = [];
for (var i = 0; i < 3; i++) {
functions.push(function() {
console.log(i);
});
}
// 输出结果是 3, 3, 3
functions.forEach(func => func());
性能问题
过度使用闭包可能会导致性能下降。每次创建闭包都会产生额外的函数和作用域,过多的闭包可能导致内存和性能的开销增加。