精通JavaScript:从理解作用域和闭包开始

JavaScript是一门灵活且强大的编程语言,但其作用域和闭包的概念常常让初学者感到困惑。理解这些概念对于编写高效、可维护的代码至关重要。本文将深入探讨JavaScript中的作用域和闭包,帮助你掌握它们的核心原理和实际应用。

1. 作用域

作用域决定了变量、函数和对象在代码中的可访问性。JavaScript中的作用域分为以下几种:

1.1 全局作用域

全局作用域中的变量和函数可以在代码的任何地方访问。

javascript 复制代码
var globalVar = 'I am global';
​
function printGlobal() {
    console.log(globalVar); // 输出: I am global
}
printGlobal();

1.2 函数作用域

在函数内部声明的变量只能在函数内部访问。

javascript 复制代码
function myFunction() {
    var localVar = 'I am local';
    console.log(localVar); // 输出: I am local
}
myFunction();
console.log(localVar); // 报错: localVar is not defined

1.3 块级作用域

ES6引入了letconst关键字,它们支持块级作用域,块级作用域 指的是由一对花括号 {} 包围的代码块所形成的作用域。块级作用域内的变量和函数只能在该代码块内访问,外部无法访问。

javascript 复制代码
if (true) {
    let blockVar = 'I am block scoped';
    console.log(blockVar); // 输出: I am block scoped
}
console.log(blockVar); // 报错: blockVar is not defined

var 没有块级作用域

  • 使用 var 声明的变量只有函数作用域或全局作用域,没有块级作用域。
  • 即使在块内声明,变量也会提升到函数或全局作用域
javascript 复制代码
if (true) {
    var z = 30;
}
console.log(z); // 30

1.4 词法作用域

词法作用域是指函数在定义时就已经确定了它的作用域,而不是在调用时

javascript 复制代码
let outerVar = "I'm outer!";
​
function outerFunction() {
    let innerVar = "I'm inner!";
​
    function innerFunction() {
        console.log(outerVar); // 可以访问外部作用域的变量
        console.log(innerVar); // 可以访问外部作用域的变量
    }
​
    innerFunction();
}
​
outerFunction();

1.5 模块作用域

  • 模块作用域是指在 ES6 模块中,每个模块都有自己的作用域,模块内部的变量和函数默认不会被外部访问
  • 使用 exportimport 可以实现模块间的变量和函数共享
javascript 复制代码
// module.js
let moduleVar = "I'm in a module!";
​
export function moduleFunction() {
    console.log(moduleVar);
}
javascript 复制代码
// main.js
import { moduleFunction } from './module.js';
​
moduleFunction(); // 输出: I'm in a module!
console.log(moduleVar); // 报错: moduleVar is not defined

1.6 动态作用域

  • JavaScript 本身不支持动态作用域,但可以通过 thiseval 模拟类似的行为
  • 动态作用域是指函数的作用域在调用时确定,而不是在定义时
javascript 复制代码
function dynamicScope() {
    console.log(this); // this 的值在调用时确定
}
​
dynamicScope.call({ name: "Dynamic" }); // 输出: { name: "Dynamic" }

eval 执行的代码可以访问当前作用域中的变量和函数

javascript 复制代码
let x = 10;
eval("x = 20");
console.log(x); // 输出: 20

2. 作用域链

当JavaScript查找变量时,它会从当前作用域开始,逐级向上查找,直到找到变量或到达全局作用域。这种链式结构称为作用域链。

javascript 复制代码
var globalVar = 'global';
​
function outer() {
    var outerVar = 'outer';
​
    function inner() {
        var innerVar = 'inner';
        console.log(innerVar); // 输出: inner
        console.log(outerVar); // 输出: outer
        console.log(globalVar); // 输出: global
    }
    inner();
}
outer();

在这个例子中,inner函数可以访问outerVarglobalVar,因为它们位于作用域链的上层。

欢迎加入前端筱园交流群:点击加入交流群

关注我的公众号【前端筱园】,不错过每一篇推送

3.闭包

闭包(Closure) 是指一个函数能够访问并记住其词法作用域(Lexical Scope),即使这个函数在其词法作用域之外执行。闭包是 JavaScript 中非常强大的特性,它允许函数"记住"并访问定义时的环境,即使这个环境已经不再存在。

  • 词法作用域: 函数在定义时就已经确定了它的作用域,而不是在调用时,闭包能够访问定义时的作用域链
  • 函数嵌套:闭包通常发生在函数嵌套的情况下,即一个函数内部定义了另一个函数
  • 外部函数的变量被保留: 即使外部函数已经执行完毕,闭包仍然可以访问外部函数的变量
javascript 复制代码
function outer() {
    let outerVar = "I'm from outer!";
​
    function inner() {
        console.log(outerVar); // 访问外部函数的变量
    }
​
    return inner; // 返回内部函数
}
​
const closureFunc = outer(); // outer 执行完毕,但 outerVar 仍然被 inner 记住
closureFunc(); // 输出: I'm from outer!

在这个例子中,inner函数形成了一个闭包,它能够访问outer函数的outerVar变量,即使outer函数已经执行完毕。

3.1 闭包的实际应用

3.1.1 数据封装

ini 复制代码
闭包可以用于创建私有变量,防止外部直接访问

```javascript
function createCounter() {
    let count = 0;
​
    return {
        increment: function() {
            count++;
            console.log(count);
        },
        decrement: function() {
            count--;
            console.log(count);
        }
    };
}
​
const counter = createCounter();
counter.increment(); // 输出: 1
counter.increment(); // 输出: 2
counter.decrement(); // 输出: 1
```

3.1.2 回调函数

scss 复制代码
闭包常用于回调函数中,确保回调函数能够访问其定义时的上下文

```javascript
function fetchData(url, callback) {
    setTimeout(() => {
        const data = 'Some data from ' + url;
        callback(data);
    }, 1000);
}
​
fetchData('https://www.dengzhanyong.com', function(data) {
    console.log(data); // 输出: Some data from https://www.dengzhanyong.com
});
```

3.1.3 函数柯里化

php 复制代码
闭包可以用于实现函数柯里化,即将一个多参数函数转换为一系列单参数函数

```javascript
function add(a) {
    return function(b) {
        return a + b;
    };
}
​
const addFive = add(5);
console.log(addFive(3)); // 输出: 8
```

3.1.4 模块模式

ini 复制代码
闭包可以用于实现模块化,隐藏内部实现,暴露公共接口

```javascript
const module = (function() {
    let privateVar = "I'm private";
​
    return {
        getVar: function() {
            return privateVar;
        },
        setVar: function(value) {
            privateVar = value;
        }
    };
})();
​
console.log(module.getVar()); // I'm private
module.setVar("New value");
console.log(module.getVar()); // New value
```

3.2 闭包的注意事项

3.2.1 内存泄漏

scss 复制代码
闭包会导致其词法作用域中的变量无法被垃圾回收,从而可能导致内存泄漏(后文会对内存泄漏进行更详细的介绍)

```javascript
function createHeavyClosure() {
    let largeArray = new Array(1000000).fill('data');
​
    return function() {
        console.log(largeArray[0]);
    };
}
​
const heavyClosure = createHeavyClosure();
// largeArray 无法被回收,可能导致内存泄漏
```

3.2.2 性能问题

css 复制代码
过度使用闭包可能会影响性能,尤其是在循环中创建闭包时。

```javascript
for (var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i); // 输出: 10 次 10
    }, 1000);
}
```

解决方法:使用`let`或立即执行函数(IIFE)创建新的作用域

```javascript
for (let i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i); // 输出: 0 到 9
    }, 1000);
}
```

```javascript
for (var i = 0; i < 10; i++) {
    (function(j) {
      setTimeout(function() {
        console.log(j); // 输出: 0 到 9
      }, 1000);
    })(i)
}
```

这里要注意的一点是,`setTimeout` 方法本身并不是闭包,但它经常与闭包一起使用,当传递给 `setTimeout` 的函数访问了外部作用域的变量时,就会形成一个闭包。这是因为该函数"记住"了它的词法作用域,即使外部函数已经执行完毕

4. 内存泄漏

在 JavaScript 中,内存泄漏(Memory Leak) 是指程序在运行过程中,由于某些原因未能释放不再使用的内存,导致内存占用持续增加,最终可能耗尽系统内存,影响程序性能甚至导致崩溃。

在 JavaScript 中,内存泄漏通常是由于以下原因导致的:

4.1 意外的全局变量

未使用 varletconst 声明的变量会变成全局变量,即使不再使用,也不会被垃圾回收机制回收。

javascript 复制代码
function leak() {
    globalVar = "I'm a global variable!"; // 意外的全局变量
}

解决方案:始终使用 varletconst 声明变量

javascript 复制代码
function noLeak() {
    let localVar = "I'm local!";
}

4.2 未清除的定时器或回调函数

使用 setTimeoutsetInterval 时,如果未及时清除,可能会导致回调函数持续引用外部变量,从而无法释放内存。

javascript 复制代码
let data = fetchData();
setInterval(function() {
    console.log(data); // data 一直被引用
}, 1000);

解决方案:使用 clearTimeoutclearInterval 清除不再需要的定时器

javascript 复制代码
let timer = setTimeout(function() {
    console.log("Timeout!");
}, 1000);
​
clearTimeout(timer); // 清除定时器

4.3 闭包

闭包会保留对外部函数作用域的引用,如果闭包未被正确释放,可能导致内存泄漏。

javascript 复制代码
function outer() {
    let bigData = new Array(1000000).fill("data");
​
    return function inner() {
        console.log(bigData[0]); // bigData 一直被引用
    };
}
​
let closure = outer();

解决方案:确保闭包不会无限制地引用外部变量

javascript 复制代码
function outer() {
    let bigData = new Array(1000000).fill("data");
​
    return function inner() {
        console.log(bigData[0]);
    };
}
​
let closure = outer();
closure = null; // 手动释放闭包

4.4 未清理的 DOM 引用

如果 JavaScript 中保留了 DOM 元素的引用,即使从页面中移除了该元素,内存也不会被释放。

javascript 复制代码
let element = document.getElementById("myElement");
document.body.removeChild(element); // 从 DOM 中移除
console.log(element); // element 仍然被引用

解决方案:移除 DOM 元素时,确保 JavaScript 中不再保留对其的引用

javascript 复制代码
let element = document.getElementById("myElement");
document.body.removeChild(element);
element = null; // 手动释放引用

4.5 未释放的事件监听器

如果事件监听器未正确移除,可能会导致相关对象无法被垃圾回收。

javascript 复制代码
let button = document.getElementById("myButton");
button.addEventListener("click", function() {
    console.log("Button clicked!");
});
​
// 如果 button 被移除,但事件监听器未移除,可能导致内存泄漏

解决方案:在不再需要时,使用 removeEventListener 移除事件监听器

javascript 复制代码
let button = document.getElementById("myButton");
function handleClick() {
    console.log("Button clicked!");
}
​
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick); // 移除事件监听器

4.6 未清理的缓存

如果缓存未设置大小限制或清理机制,可能会导致内存占用不断增加。

javascript 复制代码
let cache = {};
function addToCache(key, value) {
    cache[key] = value;
}

解决方案:使用 LRU(最近最少使用)等算法限制缓存大小

javascript 复制代码
let cache = new Map();
const MAX_CACHE_SIZE = 100;
​
function addToCache(key, value) {
    if (cache.size >= MAX_CACHE_SIZE) {
        const oldestKey = cache.keys().next().value;
        cache.delete(oldestKey);
    }
    cache.set(key, value);
}

5. 写在最后

欢迎加入前端筱园交流群:点击加入交流群

关注我的公众号【前端筱园】,不错过每一篇推送

相关推荐
会功夫的李白11 分钟前
Electron + Vite + Vue 桌面应用模板
javascript·vue.js·electron·vite·模版
小兵张健35 分钟前
运用 AI,看这一篇就够了(上)
前端·后端·cursor
不怕麻烦的鹿丸1 小时前
node.js判断在线图片链接是否是webp,并将其转格式后上传
前端·javascript·node.js
vvilkim1 小时前
控制CSS中的继承:灵活管理样式传递
前端·css
南城巷陌1 小时前
Next.js中not-found.js触发方式详解
前端·next.js
No Silver Bullet2 小时前
React Native进阶(六十一): WebView 替代方案 react-native-webview 应用详解
javascript·react native·react.js
拉不动的猪2 小时前
前端打包优化举例
前端·javascript·vue.js
ok0602 小时前
JavaScript(JS)单线程影响速度
开发语言·javascript·ecmascript
Bigger2 小时前
Tauri(十五)——多窗口之间通信方案
前端·rust·app
倔强青铜三2 小时前
WXT浏览器插件开发中文教程(3)----WXT全部入口项详解
前端·javascript·vue.js