作用域、闭包与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('test@example.com')); // 输出: 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与作用域和闭包的关联与区别,下周再做一次更新吧

相关推荐
知了清语2 分钟前
pkg.pr.new 快速验证第三方包-最新修复
前端
iFlow_AI2 分钟前
知识驱动开发:用iFlow工作流构建本地知识库
前端·ai·rag·mcp·iflow·iflow cli·iflowcli
wordbaby3 分钟前
TanStack Router 文件命名约定
前端
打工人小夏3 分钟前
vue3使用transition组件,实现过度动画
前端·vue.js·前端框架·css3
LFly_ice6 分钟前
Next-1-启动!
开发语言·前端·javascript
regon8 分钟前
第九章 述职11 交叉面试
面试·职场和发展·《打造卓越团队》
小时前端8 分钟前
谁说 AI 历史会话必须存后端?IndexedDB方案完美翻盘
前端·agent·indexeddb
wordbaby13 分钟前
TanStack Router 基于文件的路由
前端
LYFlied13 分钟前
【每日算法】LeetCode 105. 从前序与中序遍历序列构造二叉树
数据结构·算法·leetcode·面试·职场和发展
wordbaby17 分钟前
TanStack Router 路由概念
前端