作用域
局部作用域
定义:局部作用域的意思就是,变量只能在它的代码块或者函数内部访问,而不能在外部访问,局部作用域的变量在函数或代码块执行完后会销毁,不会影响全局作用域变量。
function fn(){
let a = 1;
console.log(a); // 10
}
test();
console.log(a); // 报错
局部作用域的特性
- 避免变量污染:局部作用域变量不会影响外部代码,防止变量污染全局作用域。
- 生命周期:局部变量在函数或代码块执行完后会被销毁,释放内存。
- 作用域链:局部作用域可以访问外部作用域的变量,但外部作用域无法访问局部变量。
全局作用域
定义:全局作用域指的是变量或函数在整个程序的任何地方都可以访问,且不会被局部作用域所限制。
全局作用域的特点
- 声明在任何函数或代码块外部的变量,默认具有全局作用域。
- 全局变量在整个程序执行期间都存在,不会被自动销毁(除非手动删除或页面刷新)。
- 全局作用域的变量会成为
window
(浏览器环境)或global
(Node.js 环境)对象的属性。 - 在任何地方(函数、代码块等)都可以访问全局变量。
作用域链
定义:作用域链是js中的变量查找机制!
是一个查找机制!!指的是当一个变量在当前作用域找不到时,js会沿着作用域的层级结构向上查找,直到找到该变量或到达全局作用域。
作用域链的工作原理
- 当访问一个变量时,JavaScript 先在当前作用域查找。
- 如果找不到,就沿着“作用域链”向上查找父级作用域。
- 一直查找直到全局作用域,如果全局作用域也没有定义这个变量,就会报错
ReferenceError: variable is not defined
。
let name = "全局的名字";
function sayName() {
let name = "局部的名字"; // 局部变量遮蔽了全局变量
console.log(name); // "局部的名字"
}
sayName();
console.log(name); // "全局的名字"
JS垃圾回收机制
定义:也称作GC(Garbage Collection),负责清除不再使用的对象、变量等,以释放内存。
有两种垃圾回收机制:
-
标记清除(现代浏览器常用):标记所有的变量,代码运行时,移除仍在使用的变量的标记(从根节点开始,可以到达的变量),然后那些被遗忘的变量,就存有标记,垃圾回收机制会清除这些变量,释放内存。
-
引用计数(早期):每个对象有一个引用计数,某个变量引用该对象的时候标记+1,某个变量不引用对象-1,当某个对象标记为0的时候,回收对象。(存在问题,就是循环引用问题,例:当一个函数内对象互相引用导致)
function createCycle() {
let obj1 = {};
let obj2 = {};
obj1.ref = obj2;
obj2.ref = obj1; // obj1 和 obj2 互相引用
}
createCycle();
// obj1 和 obj2 作用域结束,但相互引用,无法回收(内存泄漏)
垃圾回收的触发时机
JavaScript 垃圾回收不是实时运行的,浏览器会在合适的时机触发 ,比如:
- 内存不足时(浏览器检测到占用内存过高)。
- 定期执行(浏览器内部有 GC 触发策略)。
- 函数执行结束(局部变量离开作用域)。
- 浏览器空闲时(优化性能,减少卡顿)。
闭包
定义:一个函数对周围状态的引用捆绑在一起,内层函数中访问到其外层函数的作用域
闭包 = 内层函数+外层函数的变量
常见的闭包形式,外部变量可以访问函数内部的变量
function outer(){
let a = 10;
function fn(){
console.log(a)
}
fn();
}
// outer(); === fn === function fn(){}
const fun = outer() // 等价于 const fun = function fn(){}
fun() //调用函数 能输出 10 ,表明实现了外部函数,使用了内部的值
闭包应用场景1 :内部变量不希望被外部访问改动的情况
如场景:有一个函数,想实现记录用户访问方法的次数,就是每次访问这个函数,变量就+1
let i = 0;
function count (){
i++;
console.log('访问了第',i,'次')
}
count();
这样子可以实现,但是,变量i可以被外部访问,也就是说会容易导致变量被改动,导致功能的不准确问题。
使用闭包后
function count(){
let i = 0; //声明变量
function fn (){
i++;
console.log('访问了第',i,'次')
}
return fn;
}
const fun = count();
fun();
这样子就能实现i变量不被外部访问到,但是又可以实现计数功能。
闭包会造成内存泄漏,因为按照标记清除法来进行垃圾回收的时候,fun永远属于全局的,只有页面刷新才会被回收,然后fun又可以找到fn,fn里面又使用到了i,就会导致i变量不会被回收。
扩展-防抖、节流代码运用闭包思想
复习到这里,我又想起了防抖、节流,也是使用了这一思想,在这里
防抖(在事件触发后,等待一定时间再执行函数,如果在这个时间内又触发了事件,就重新计时。)
function debounce(func, delay) {
let timeout;
return function(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), delay);
};
}
// 使用示例
const handleResize = debounce(() => {
console.log('窗口调整大小');
}, 300);
window.addEventListener('resize', handleResize);
比如这里的变量timeout,就不会被回收掉
节流(规定一个时间间隔,在这个时间间隔内只允许执行一次函数。即使事件被多次触发,也只会在指定的时间间隔内执行一次。)
function throttle(func, limit) {
let lastFunc;
let lastRan;
return function(...args) {
const context = this;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(() => {
if ((Date.now() - lastRan) >= limit) {
func.apply(context, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
}
// 使用示例
const handleScroll = throttle(() => {
console.log('滚动事件触发');
}, 1000);
window.addEventListener('scroll', handleScroll);
变量提升&&函数提升
变量提升的话就是var声明的变量,会被前置,但是未赋值
简单理解了一下
console.log(myVar); // 输出: undefined
var myVar = 5;
console.log(myVar); // 输出: 5
function example() {
console.log(innerVar); // 输出: undefined
var innerVar = 10;
console.log(innerVar); // 输出: 10
}
example();
函数提升
函数声明在文件地方,都会被提升到全局,也就是说可以在头部先使用,然后后面再定义
fn()
function fn(){
console.log('函数提升')
}
但是下面这种声明方法不能提前,下面这种是赋值声明。
fun()
var fun = function(){
console.log('函数表达式')
}