作用域、闭包与this指向问题

一、作用域

作用域概念

在js中,作用域就是变量和函数可访问范围 ,作用域控制着变量和函数的可见性生命周期

常见作用域

1. Global 作用域(常见)
js 复制代码
<script>
var a = '曾小白'
</script>

通过var声明一个变量a,打断点的时候可以看到,scope里面有global类型的作用域,也就是全局作用域,里面保存了var声明的变量,在浏览器的console.log中,可以输入变量名a访问,也可以输window.a进行访问。

2. Local 作用域(常见)

声明一个函数,在函数内声明一个变量b,调用这个函数的时候,可以看到scope里面有local类型的作用域,也就是本地作用域

js 复制代码
// 函数作用域
function functionScopeExample() {
    // 函数内部变量,只能在函数内部访问
    const b = 'I am a function variable';
    console.log('1: ', functionVariable);

    // 嵌套函数,展示作用域链
    function nestedFunction() {
        let nestedVariable = 'I am a nested function variable';
        console.log('2: ', nestedVariable);
        console.log('3: ', functionVariable);
    }

    nestedFunction();
}

// 下面是打印结果
1:  I am a function variable
2:  I am a nested function variable
3:  I am a function variable
3.block作用域(常见)

es6 加入了块语句,它也同样会生成作用域,在debugger的时候放到Block作用域里面,if、while、for 等语句都会生成 Block 作用域:

js 复制代码
// 块级作用域(使用 let 和 const 声明)
{
    // 块级变量,只能在块内访问
    let blockVariable = 'I am a block variable';
    const constantVariable = 'I am a constant in block';
    console.log('Inside block: ', blockVariable);
    console.log('Inside block: ', constantVariable);
}
4. Script作用域
js 复制代码
<script>
var a = '曾小白'
const b = 'I am a script variable'
let c = 'I am a script variable too'
</script>

以上代码访问

  • a: '曾小白'
  • b: 'I am a script variable'
  • c: 'I am a script variable'
  • window.a: '曾小白'
  • window.b: undefined
  • window.c: undefined

且在debugger的时候,a被放在Global作用域,b和c被放在Script作用域。 这就是浏览器环境下用 let const 声明全局变量时的特殊作用域,script 作用域。可以直接访问这个全局变量,但是却不能通过 window.xx 访问。

5. Catch Block 作用域
js 复制代码
try{
  throw new Error('something went wrong')
}catch{
debugger
}.finally{
  const a = 'this is finally'
}

debugger的时候发现,catch语句会生成一个Catch Block 作用域,里面能访问具体的错误对象。finally里面的a会在Block 作用域里面。

6. Closure作用域

其实就是我们常说的闭包的作用域,一个函数返回另一个函数的,返回的函数引用了外层函数的变量,就会以闭包的形式保存下来。

js 复制代码
// 闭包作用域
function outerFunction() {
    let outerVariable = 'I am from outer function';
    return function innerFunction() {
        // 内部函数可以访问外部函数的变量,形成闭包
        return outerVariable;
    };
}

上面的例子就是内部函数访问了外部函数的变量outerVariable,通过debugger可以发现变量outerVariable被保存在scops的Closure作用域里面,其实就是初始化js代码的时候,给闭包用到的变量储存到Closure作用域里,执行的时候就能从Closure作用域找到需要的变量。 注意:当返回的函数有 eval 的时候,会把所有的变量都放到里面。JS 引擎就会形成特别大的 Closure,会导致性能问题,所以在闭包里面尽量不要去使用eval

作用域链

概念:在 JavaScript 里,作用域链是一种机制,用于在代码执行过程中查找变量。当访问一个变量时,JavaScript 引擎会先在当前作用域里查找该变量,如果没找到,就会到包含当前作用域的外层作用域继续查找,如此层层递进,直到全局作用域。这个由当前作用域及其所有外层作用域组成的链条,就被称作作用域链。

作用域链的形成和函数的词法作用域紧密相关,函数在定义时就确定了其外层作用域,而不是在调用时。这意味着函数无论在哪里被调用,它都能访问其定义时所处作用域的变量。

js 复制代码
// 全局作用域
let globalVariable = 'This is a global variable';

// 定义一个函数,该函数内部包含另一个函数
function outerFunction() {
    // 外层函数作用域
    let outerVariable = 'This is an outer variable';

    function innerFunction() {
        // 内层函数作用域
        let innerVariable = 'This is an inner variable';

        // 尝试访问不同作用域的变量
        console.log(innerVariable); // 访问当前作用域的变量
        console.log(outerVariable); // 访问外层函数作用域的变量
        console.log(globalVariable); // 访问全局作用域的变量
    }

    return innerFunction;
}

// 调用 outerFunction 并获取 innerFunction
const closureFunction = outerFunction();

// 调用 innerFunction
closureFunction();

innerFunction 中,当访问 innerVariable 时,JavaScript 引擎会在当前作用域(innerFunction 作用域)中找到该变量;当访问 outerVariable 时,当前作用域没有该变量,引擎会到外层作用域(outerFunction 作用域)中查找;当访问 globalVariable 时,前面的作用域都没有该变量,引擎会到全局作用域中查找。

通过这种方式,JavaScript 引擎利用作用域链完成了变量的查找。

二、闭包

定义与形成条件

  • 定义:闭包是函数与其声明时的词法环境的组合,允许内部函数访问并保留外部作用域的变量

  • 形成条件

    • 函数嵌套且内部函数访问外部变量。

    • 内部函数在外部函数外被调用(如通过返回值或事件回调)

闭包的生命周期

  • 创建阶段:外部函数执行时生成变量对象。

  • 维持阶段:内部函数持有对外部变量的引用,阻止垃圾回收。

  • 释放阶段:当闭包不再被引用时,变量被回收

闭包应用场景

  • 模块化开发:通过闭包封装私有变量(如计数器、缓存机制)
js 复制代码
// 定义一个函数,返回一个包含计数器操作方法的对象
function createCounter() {
    // 私有变量,用于存储计数
    let count = 0;

    // 返回一个对象,包含增加计数、减少计数和获取当前计数的方法
    return {
        increment: function() {
            count++;
            return count;
        },
        decrement: function() {
            count--;
            return count;
        },
        getCount: function() {
            return count;
        }
    };
}

// 使用示例
const counter = createCounter();
console.log(counter.getCount()); // 输出: 0
counter.increment();
console.log(counter.getCount()); // 输出: 1
counter.decrement();
console.log(counter.getCount()); // 输出: 0

计数器可以记录调用次数,通过闭包将计数变量封装为私有变量,外部只能通过提供的方法来访问和修改计数。

js 复制代码
// 定义一个函数,接受一个计算函数作为参数,返回一个带缓存功能的函数
function createCache(fn) {
    // 私有变量,用于存储缓存
    const cache = {};

    return function(...args) {
        // 将参数转换为字符串作为缓存的键
        const key = JSON.stringify(args);

        // 检查缓存中是否已经存在该键
        if (cache[key] === undefined) {
            // 如果不存在,调用原始计算函数进行计算,并将结果存入缓存
            cache[key] = fn(...args);
        }

        // 返回缓存中的结果
        return cache[key];
    };
}

// 示例计算函数:计算斐波那契数列
function fibonacci(n) {
    if (n <= 1) {
        return n;
    }
    return fibonacci(n - 1) + fibonacci(n - 2);
}

// 创建带缓存功能的斐波那契计算函数
const cachedFibonacci = createCache(fibonacci);

// 使用示例
console.log(cachedFibonacci(10)); // 首次计算,会将结果存入缓存
console.log(cachedFibonacci(10)); // 直接从缓存中获取结果,避免重复计算

缓存机制可以避免重复计算,将计算结果存储在缓存中,下次需要相同计算结果时直接从缓存中获取。

  • 高阶函数:工厂函数生成定制化逻辑(如校验器、防抖/节流函数)
js 复制代码
// 校验器工厂函数
function createValidator(rule) {
    // 闭包保存规则
    return function(value) {
        switch (rule) {
            case 'isEmail':
                const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
                return emailRegex.test(value);
            case 'isPhone':
                const phoneRegex = /^1[3-9]\d{9}$/;
                return phoneRegex.test(value);
            case 'isNumber':
                return !isNaN(value) && isFinite(value);
            default:
                return false;
        }
    };
}

// 使用示例
const emailValidator = createValidator('isEmail');
console.log(emailValidator('[email protected]')); // 输出: true
console.log(emailValidator('invalid-email'));    // 输出: false

const phoneValidator = createValidator('isPhone');
console.log(phoneValidator('13800138000'));      // 输出: true
console.log(phoneValidator('1234567890'));       // 输出: false

校验器可以根据不同的规则对输入数据进行验证。通过工厂函数,我们可以根据不同的规则生成不同的校验器。

js 复制代码
// 防抖函数工厂
function debounce(func, wait) {
    // 闭包保存定时器 ID
    let timeout;
    return function(...args) {
        const context = this;
        // 清除之前的定时器
        clearTimeout(timeout);
        // 设置新的定时器
        timeout = setTimeout(() => {
            func.apply(context, args);
        }, wait);
    };
}

// 使用示例
function search(input) {
    console.log(`Searching for: ${input}`);
}

const debouncedSearch = debounce(search, 300);

// 模拟多次输入
debouncedSearch('a');
debouncedSearch('ab');
debouncedSearch('abc');
// 只会在最后一次调用 300ms 后执行 search 函数

防抖函数用于限制函数的调用频率,在一定时间内如果多次触发函数,只会在最后一次触发后的一段时间后执行。

js 复制代码
// 节流函数工厂
function throttle(func, limit) {
    // 闭包保存上次执行时间
    let inThrottle;
    return function(...args) {
        const context = this;
        if (!inThrottle) {
            func.apply(context, args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit);
        }
    };
}

// 使用示例
function handleScroll() {
    console.log('Scroll event handled');
}

const throttledScroll = throttle(handleScroll, 500);

// 模拟频繁滚动事件
window.addEventListener('scroll', throttledScroll);

使用闭包时候的注意事项

闭包在 JavaScript 里十分实用,但使用时若不注意,可能会引发内存泄漏、变量共享等问题。

1. 内存泄漏问题

闭包会让函数保存对其外部作用域变量的引用,这就使得这些变量在外部函数执行完毕后也不会被垃圾回收机制回收。若闭包持续存在且引用了大量数据,就可能造成内存泄漏。

解决办法: 当闭包不再使用时,及时解除对闭包的引用,让垃圾回收机制可以回收相关变量。

js 复制代码
function createLargeDataClosure() {
    const largeData = new Array(1000000).fill('data');
    return function() {
        console.log(largeData.length);
    };
}

const closure = createLargeDataClosure();
// 此时 largeData 不会被垃圾回收,因为闭包引用了它
js 复制代码
let closure = createLargeDataClosure();
closure();
// 不再使用闭包,解除引用
closure = null;
2. 变量共享问题

多个闭包可能会共享同一个外部变量,若其中一个闭包修改了该变量,会影响到其他闭包。

js 复制代码
function createClosures() {
    const result = [];
    for (var i = 0; i < 3; i++) {
        result.push(function() {
            console.log(i);
        });
    }
    return result;
}

const closures = createClosures();
closures.forEach(closure => closure()); 
// 输出 3, 3, 3,而不是预期的 0, 1, 2

解决办法 :使用 let 替代 var 声明变量,因为 let 有块级作用域。

js 复制代码
function createClosures() {
    const result = [];
    for (let i = 0; i < 3; i++) {
        result.push(function() {
            console.log(i);
        });
    }
    return result;
}

const closures = createClosures();
closures.forEach(closure => closure()); 
// 输出 0, 1, 2

或者借助立即执行函数表达式(IIFE)创建独立作用域。

js 复制代码
function createClosures() {
    const result = [];
    for (var i = 0; i < 3; i++) {
        result.push((function(index) {
            return function() {
                console.log(index);
            };
        })(i));
    }
    return result;
}

const closures = createClosures();
closures.forEach(closure => closure()); 
// 输出 0, 1, 2
3. 性能问题

闭包的创建和使用会带来一定的性能开销,特别是在频繁创建闭包或者闭包逻辑复杂的情况下。

解决办法

  • 避免在循环或者高频事件处理函数中频繁创建闭包。
  • 对闭包内的复杂逻辑进行优化,减少不必要的计算。
4. 代码可读性和维护性

过多嵌套的闭包会让代码变得复杂,降低代码的可读性和维护性。

解决办法

  • 合理设计代码结构,避免过深的闭包嵌套。
  • 给闭包函数添加清晰的注释,解释其功能和作用。

结语:本来想在本文一并讲讲this指向问题,this与作用域和闭包的关联与区别,下周再做一次更新吧

相关推荐
@PHARAOH1 小时前
HOW - 在 Mac 上的 Chrome 浏览器中调试 Windows 场景下的前端页面
前端·chrome·macos
独行soc2 小时前
2025年渗透测试面试题总结-某服面试经验分享(附回答)(题目+回答)
linux·运维·服务器·网络安全·面试·职场和发展·渗透测试
月月大王3 小时前
easyexcel导出动态写入标题和数据
java·服务器·前端
JC_You_Know4 小时前
多语言网站的 UX 陷阱与国际化实践陷阱清单
前端·ux
Python智慧行囊4 小时前
前端三大件---CSS
前端·css
Jinuss4 小时前
源码分析之Leaflet中Marker
前端·leaflet
成都渲染101云渲染66664 小时前
blender云渲染指南2025版
前端·javascript·网络·blender·maya
聆听+自律4 小时前
css实现渐变色圆角边框,背景色自定义
前端·javascript·css
行走__Wz4 小时前
计算机学习路线与编程语言选择(信息差)
java·开发语言·javascript·学习·编程语言选择·计算机学习路线
-代号95274 小时前
【JavaScript】二十九、垃圾回收 + 闭包 + 变量提升
开发语言·javascript·ecmascript