草庐IT

JavaScript 作用域和闭包

飞仔FeiZai 2023-03-28 原文

一、作用域

JavaScript 中的作用域指的是变量和函数的可访问范围。JavaScript 使用词法作用域,即作用域由代码的书写结构决定,而不是运行时环境。

二、闭包

JavaScript 中,闭包是一个函数对象,它可以访问定义该函数的作用域里的变量,即使函数已经返回。闭包的特点是,它可以在其相关环境不存在时保留变量。闭包可以被保存到变量中并在以后使用。它具有两个特征,一是可以访问外部函数的变量,二是它可以在外部函数执行结束后继续执行。闭包可以用来实现私有变量,记忆函数(缓存),高阶函数等功能。

简单来说,闭包是一个能够访问其他函数作用域中变量的函数。

三、闭包的应用

1、封装私有变量

闭包的一个常见用途是构建私有变量。当你使用闭包封装变量时,外部代码就无法访问这些变量了。

下面是一个使用闭包来创建私有变量的示例:

function createCounter() {
  let count = 0;
  return function() {
    return count++;
  }
}

let counter = createCounter();
console.log(counter()); // 0
console.log(counter()); // 1
console.log(counter()); // 2

在这个例子中,createCounter 函数返回了一个闭包。这个闭包访问了创建时所在作用域中的变量 count。外部代码不能访问这个变量,因此这个变量就成为了一个私有变量。

2、高阶函数

高阶函数是指一个函数,其参数或返回值是一个函数。闭包可以用来捕获函数作用域中的变量,并将它们传递给高阶函数。

下面是一个使用闭包来构建高阶函数的示例:

function createMultiplier(x) {
    return function(y) {
        return x * y;
    }
}

let double = createMultiplier(2);
console.log(double(3)); //6
console.log(double(5)); //10
let triple = createMultiplier(3);
console.log(triple(2)); //6
console.log(triple(3)); //9

在这个例子中,createMultiplier是一个高阶函数,它返回了一个闭包。 这个闭包捕获了它定义时所在作用域中的变量 x 并在闭包内部运用这个变量进行运算。

3、延迟计算(高阶函数的一种具体实现)

通过闭包,可以将函数和其词法环境存储在变量中,直到需要执行函数时再调用它。

function makeAdder(x) {
    return function (y) {
        return x + y;
    };
}
const add5 = makeAdder(5);
console.log(add5(3)); // 8

4、实现私有属性和私有方法

闭包还有一个重要的应用场景就是实现面向对象编程中的私有属性和私有方法。

以下是一个使用闭包实现私有属性和私有方法的例子:

function Person(name) {
    let _name = name;
    this.getName = function() {
        return _name;
    }
    this.setName = function(name) {
        _name = name;
    }
}
let p = new Person('John');
console.log(p.getName()); // John
p.setName('Mike');
console.log(p.getName()); // Mike

在这个例子中,_name 是私有变量,getName() 和 setName() 是私有方法。由于它们是在构造函数中定义的,所以它们可以访问到 _name 变量。而外部无法直接访问 _name 变量。

闭包的使用能够帮助我们实现面向对象编程中的私有属性和私有方法,使代码更加安全和可维护。

5、实现计数器(封装私有变量的一种具体实现)

function makeCounter() {
    let count = 0;
    return function() {
        return count++;
    };
}
const counter = makeCounter();
console.log(counter()); // 0
console.log(counter()); // 1
console.log(counter()); // 2

6、实现防抖函数

通过闭包可以实现一个防抖函数,在指定时间内只会执行一次。

function debounce(fn, delay) {
    let timer = null;
    return function() {
        clearTimeout(timer);
        timer = setTimeout(() => {
            fn.apply(this, arguments);
        }, delay);
    };
}
const debouncedFn = debounce(() => {
    console.log('Debounced function called.');
}, 1000);

7、实现节流函数

通过闭包可以实现一个节流函数,每隔一段时间执行一次。

function throttle(fn, delay) {
    let timer = null;
    return function() {
        if (!timer) {
            timer = setTimeout(() => {
                fn.apply(this, arguments);
                timer = null;
            }, delay);
        }
    };
}
const throttledFn = throttle(() => {
    console.log('Throttled function called.');
}, 1000);

8、实现自执行函数

通过闭包可以实现一个自执行函数,可以在定义时立即执行。

(function () {
    console.log('Self-executing function called.');
})();

9、实现单例模式

单例模式是一种常用的设计模式,它保证一个类只有一个实例,并且提供了一个全局访问点来访问这个实例。使用闭包可以很容易地实现单例模式。

下面是一个使用闭包实现单例模式的例子:

const Singleton = (function() {
    let instance;
    function createInstance() {
        return {};
    }
    return {
        getInstance: function() {
            if (!instance) {
                instance = createInstance();
            }
            return instance;
        }
    };
})();
console.log(Singleton.getInstance() === Singleton.getInstance()); // true

在这个例子中,Singleton 函数返回了一个对象,它包含一个 getInstance() 方法。这个方法用于获取单例的实例。如果实例不存在,则调用 createInstance() 创建一个新的实例。因为 createInstance() 和 instance 变量都在闭包中定义,所以只能被 getInstance() 方法访问到。

这样就可以保证单例模式的特性了,在多次调用 getInstance() 方法时,都会返回同一个实例。

10、实现模块模式

通过闭包可以实现模块模式,将代码封装在单独的模块中,保证变量和函数不会污染全局作用域。

const myModule = (function () {
    let _privateValue = 'Hello, World!';
    function _privateFunction() {
        console.log(_privateValue);
    }
    return {
        publicFunction: function () {
            _privateFunction();
        }
    };
})();
myModule.publicFunction(); // 'Hello, World!'
console.log(_privateValue); // ReferenceError: _privateValue is not defined

11、实现类似于块级作用域的效果

通过闭包可以实现类似于块级作用域的效果,将变量和函数封装在一个单独的作用域中,防止变量污染。

for (var i = 0; i < 5; i++) {
    (function(index) {
        setTimeout(() => {
            console.log(index);
        }, 1000 * i);
    })(i);
}

12、实现缓存(记忆函数)

通过创建一个闭包并将需要缓存的数据存储在其中,可以在需要使用数据时直接从缓存中获取,而不需要重新计算或重新读取。

下面是一个使用闭包实现缓存的例子:

function expensiveFunction() {
    console.log('Calculating...');
    return Math.random();
}

function cache(fn) {
    let cache = {};
    return function (arg) {
        if (!cache[arg]) {
            cache[arg] = fn(arg);
        }
        return cache[arg];
    };
}

const cachedFunction = cache(expensiveFunction);
console.log(cachedFunction(5)); // Calculating... 0.5
console.log(cachedFunction(5)); // 0.5

在这个例子中,我们有一个计算代价高昂的函数 expensiveFunction(),它每次都会计算一个随机数。我们使用 cache() 函数将其包装在闭包中,并将计算结果存储在缓存中。当调用 cachedFunction() 函数时,如果缓存中已经有结果,则直接返回缓存中的结果,而不需要重新计算。如果缓存中没有结果,则调用 expensiveFunction() 计算结果并将其存储在缓存中。

在这个例子中,我们将参数作为缓存的键,这样就可以缓存不同参数的结果。

闭包缓存的好处是:

  • 可以避免重复计算或重复读取
  • 可以提高程序的性能
  • 可以将缓存逻辑与主程序逻辑分离,更加清晰

缺点是:

  • 缓存数据会占用内存
  • 缓存会在某些时候过期,导致数据不准确
  • 如果缓存过大,会导致性能下降

使用闭包实现缓存是一种常用的优化方式,可以有效地提高程序的性能。然而,也需要注意缓存的维护与管理,避免缓存过大或过期导致的数据不准确的问题。

除了使用闭包实现缓存还有其他方法,比如使用Map,WeakMap,localStorage来存储缓存数据。

使用闭包实现缓存的方法是一种常用的优化方式,可以有效地提高程序的性能。通过将缓存逻辑封装在闭包中,可以将缓存逻辑与主程序逻辑分离,使程序更加清晰。

四、回调函数属于闭包?

请参考《JavaScript 回调函数属于闭包?》

 

闭包是 JavaScript 中一种强大的特性,理解和掌握它的使用方式可以帮助我们编写出更加高效、可维护的代码。

需要注意的是,闭包会增加函数作用域链的长度,因此如果闭包中存在大量的变量或者被频繁使用,可能会导致内存占用过多,影响性能。

除此之外,闭包也可能会导致作用域链上的变量不能及时被垃圾回收机制回收,这就是所谓的闭包内存泄漏问题。所以,使用闭包时,需要注意内存使用的问题。

在使用闭包时需要注意一些问题,例如:

  • 使用完闭包后,应该及时释放不再使用的闭包。
  • 避免在循环中使用闭包,这可能会导致不同的闭包引用同一个变量。
  • 在使用闭包时,需要谨慎使用 this 和 arguments 变量,因为它们可能会被闭包引用。

同时闭包的应用也会影响到代码的可读性和可维护性,需要在使用时考虑清楚。

如果遇到需要闭包且涉及到回调函数,可以使用箭头函数来简化代码,而箭头函数本身也是闭包的一种,但是其处理方式与普通的函数不同,对作用域和this的绑定等也有区别。

另外,在 ECMAScript6 中,新增了 let 和 const 命令,它们可以用来声明块级作用域的变量。let 和 const 命令能够更好地处理变量提升问题,并且使用块级作用域可以更好地控制变量的生命周期,减少内存泄漏的风险。

在编写代码时,应该根据实际需要来选择使用 var、let 或 const 命令声明变量。使用 var 命令时,应该尽量避免使用全局变量;使用 let 和 const 命令时,应该尽量避免变量提升问题。

 

总的来说,JavaScript的闭包是一种非常强大的特性,可以帮助我们更好地处理作用域、私有变量、高阶函数等问题。但是,使用闭包也需要注意一些性能和内存使用的问题。

有关JavaScript 作用域和闭包的更多相关文章

  1. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

  2. ruby - Ruby 中的闭包和 for 循环 - 2

    我是Ruby的新手,有些闭包逻辑让我感到困惑。考虑这段代码:array=[]foriin(1..5)array[5,5,5,5,5]这对我来说很有意义,因为i被绑定(bind)在循环之外,所以每次循环都会捕获相同的变量。使用每个block可以解决这个问题对我来说也很有意义:array=[](1..5).each{|i|array[1,2,3,4,5]...因为现在每次通过时都单独声明i。但现在我迷路了:为什么我不能通过引入一个中间变量来修复它?array=[]foriin1..5j=iarray[5,5,5,5,5]因为j每次循环都是新的,我认为每次循环都会捕获不同的变量。例如,这绝对

  3. ruby-on-rails - "assigns"在 Ruby on Rails 中有什么作用? - 2

    我目前正在尝试学习RubyonRails和测试框架RSpec。assigns在此RSpec测试中做什么?describe"GETindex"doit"assignsallmymodelas@mymodel"domymodel=Factory(:mymodel)get:indexassigns(:mymodels).shouldeq([mymodel])endend 最佳答案 assigns只是检查您在Controller中设置的实例变量的值。这里检查@mymodels。 关于ruby-o

  4. ruby-on-rails - 使用 javascript 更改数据方法不会更改 ajax 调用用户的什么方法? - 2

    我遇到了一个非常奇怪的问题,我很难解决。在我看来,我有一个与data-remote="true"和data-method="delete"的链接。当我单击该链接时,我可以看到对我的Rails服务器的DELETE请求。返回的JS代码会更改此链接的属性,其中包括href和data-method。再次单击此链接后,我的服务器收到了对新href的请求,但使用的是旧的data-method,即使我已将其从DELETE到POST(它仍然发送一个DELETE请求)。但是,如果我刷新页面,HTML与"new"HTML相同(随返回的JS发生变化),但它实际上发送了正确的请求类型。这就是这个问题令我困惑的

  5. ruby - 字符串文字前面的 * 在 ruby​​ 中有什么作用? - 2

    这段代码似乎创建了一个范围从a到z的数组,但我不明白*的作用。有人可以解释一下吗?[*"a".."z"] 最佳答案 它叫做splatoperator.SplattinganLvalueAmaximumofonelvaluemaybesplattedinwhichcaseitisassignedanArrayconsistingoftheremainingrvaluesthatlackcorrespondinglvalues.Iftherightmostlvalueissplattedthenitconsumesallrvaluesw

  6. ruby - 为什么这个 eval 在 Ruby 中不起作用 - 2

    你能解释一下吗?我想评估来自两个不同来源的值和计算。一个消息来源为我提供了以下信息(以编程方式):'a=2'第二个来源给了我这个表达式来评估:'a+3'这个有效:a=2eval'a+3'这也有效:eval'a=2;a+3'但我真正需要的是这个,但它不起作用:eval'a=2'eval'a+3'我想了解其中的区别,以及如何使最后一个选项起作用。感谢您的帮助。 最佳答案 您可以创建一个Binding,并将相同的绑定(bind)与每个eval相关联调用:1.9.3p194:008>b=binding=>#1.9.3p194:009>eva

  7. ruby-on-rails - Spring 不起作用。 [未初始化常量 Spring::SID::DL] - 2

    我无法运行Spring。这是错误日志。myid-no-MacBook-Pro:myid$spring/Users/myid/.rbenv/versions/1.9.3-p484/lib/ruby/gems/1.9.1/gems/spring-0.0.10/lib/spring/sid.rb:17:in`fiddle_func':uninitializedconstantSpring::SID::DL(NameError)from/Users/myid/.rbenv/versions/1.9.3-p484/lib/ruby/gems/1.9.1/gems/spring-0.0.10/li

  8. ruby - 在 Mechanize 中使用 JavaScript 单击链接 - 2

    我有这个:AccountSummary我想单击该链接,但在使用link_to时出现错误。我试过:bot.click(page.link_with(:href=>/menu_home/))bot.click(page.link_with(:class=>'top_level_active'))bot.click(page.link_with(:href=>/AccountSummary/))我得到的错误是:NoMethodError:nil:NilClass的未定义方法“[]” 最佳答案 那是一个javascript链接。Mechan

  9. ruby-on-rails - Simple_form 必填字段不起作用 - Ruby on Rails - 2

    我在RoR应用程序中有一个提交表单,是使用simple_form构建的。当字段为空白时,应用程序仍会继续下一步,而不会提示错误或警告。默认情况下,这些字段应该是required:true;但即使手动编写也行不通。该应用有3个步骤:NewPost(新View)->Preview(创建View)->Post。我的Controller和View的摘录会更清楚:defnew@post=Post.newenddefcreate@post=Post.new(params.require(:post).permit(:title,:category_id))ifparams[:previewButt

  10. javascript - jQuery 的 jquery-1.10.2.min.map 正在触发 404(未找到) - 2

    我看到有关未找到文件min.map的错误消息:GETjQuery'sjquery-1.10.2.min.mapistriggeringa404(NotFound)截图这是从哪里来的? 最佳答案 如果ChromeDevTools报告.map文件的404(可能是jquery-1.10.2.min.map、jquery.min.map或jquery-2.0.3.min.map,但任何事情都可能发生)首先要知道的是,这仅在使用DevTools时才会请求。您的用户不会遇到此404。现在您可以修复此问题或禁用sourcemap功能。修复:获取文

随机推荐