一文详解JavaScript闭包典型应用

目录
  • 1.应用
    • 1.1 模拟私有变量
    • 1.2 柯里化
    • 1.3 偏函数
    • 1.4 防抖
    • 1.5 节流
  • 2.性能问题
    • 2.1 内存泄漏
    • 2.2 常见的内存泄漏
  • 3.闭包与循环体
    • 3.1 这段代码输出啥
    • 3.2 改造方法
  • 4.总结

1.应用

以下这几个方面是我们开发中最为常用到的,同时也是面试中回答比较稳的几个方面。

1.1 模拟私有变量

我们都知道JS是基于对象的语言,JS强调的是对象,而非类的概念,在ES6中,可以通过class关键字模拟类,生成对象实例。

通过class模拟出来的类,仍然无法实现传统面向对象语言中的一些能力 —— 比如私有变量的定义和使用。

我们通过看这样一个User类来了解私有变量(伪代码,不能直接运行)

class User{
  constructor(username,password){
  // 用户名
  this.username = username
  // 密码
  this.password = password
  }

  login(){
    // 使用axious进行登录请求
    axios({
      method: 'GET',
      url: 'http://127.0.0.1/server',
      params: {
        username,
        password
      },
    }).then(response => {
      console.log(response);
    });
  }
}

在这个User类里,我们定义了一些属性,和一个login方法,我们尝试输出password这个属性。

  let user = new User('小明',123456)
  user.password  // 123465

我们发现,登录密码这么关键敏感的信息,竟然可以通过一个简单的属性就可以拿到,这就意味着,后面人只有拿到user这个对象,就可以非常轻松的获取,甚至改写他的密码。 在实际的业务开发中,这是一个非常危险的操作,我们需要从代码的层面保护password。

像password这样变量,我们希望它只在函数内部,或者对象内部方法访问到,外部无法触及。 这样的变量,就是私有变量,私有变量一般使用 _ 或双 _ 定义。

在类里声明变量的私有性,我们可以借助闭包实现,我们的思路就是把我们把私有变量放在最外层立即执行函数中,并通过立即执行User这个函数,创造了一个闭包作用域的环境。

// 利用IIFE生成闭包,返回user类
const User = (function () {
    // 定义私有变量_password
    let _password

    class User {
        constructor(username, password) {
            // 初始化私有变量_password
            _password = password
            this.username = username
        }

        login() {
            console.log(this.username, _password)

        }
    }

    return User
})()

let user = new User('小明',123465)
console.log(user.username); // 小明
console.log(user.password); // undefined
console.log(user._password); //undefined
user.login(); // 小明 undefined

在这段代码中,私有变量_password被好好的保护在User这个立即执行函数内部,此时实例暴露的属性已经没有_password,通过闭包,我们成功利用了自由变量模拟私有变量的效果。

1.2 柯里化

定义一个函数,该函数返回一个函数。 柯里化是把接收 n个参数的1个函数改造为只接收1个参数的n个互相嵌套的函数的过程。也就是从fn(a,b,c)变成fn(a)(b)(c)。

我们通过以下案例进行深入理解:以慕课网为例,我们使用site(站点)、type(课程类型)、name(课程名称)三个字符串拼接的方式为课程生成一个完整版名称。对应方法如下:

function generateName(site,type,name){
  return site + type + name
}

我们看到这个函数需要传递三个参数,此时如果我是课程运营负责人,如我只负责“体系课”的业务,那么我每次生成课程时,都会固定传参site,像这样传参:

generateName('体系课',type,name)

如果我是细分工种的前端助教,我仅仅负责“体系课”站点下的“前端”课程,那么我进行传参就是这样:

generateName('体系课','前端',name)

我们不难发现,调用generateName时,真正的变量只有一个,但是我每次不得不把前两个参数手动传一遍。此时,我们的柯里化就出现了,柯里化可以帮助我们在必要情况下,记住一部分参数。

function generateName(site){
  // var site = '体系课'
  return function(type){
    // var type = '前端'
    return function(name){
      // var name = '零基础就业班'
      return prefix + type + name
    }
  }
}

// 生成体系课专属函数
var salesName = generateName('体系课');

// “记住”site,生成体系课前端课程专属函数
var salesBabyName = salesName('前端')

// 输出 '体系课前端零基础就业班'
res = salesBabyName('零基础就业班')
console.log(res)

我们可以看到,在生成体系课专属函数中,我们将site作为实参传递给generateName函数中,将site的值保留在generateName内部作用域中。

在生成体系课前端课程函数中,将type的值保留在salesBabyName函数中,最终调用salesBabyName函数,输出。

这样一来,原有的generateName (site, type, name)函数经过柯里化变成了generateName(site)(type)(name)。通过后者这种形式,我们可以记住一部分形参,选择性的传递参数,从而编写出更符合预期,复用性更高的函数。

function generateName(site){
   // var site = '实战课'
   return function(type){
     // var type = 'Java'
     return function(name){
       // var name = '零基础'
       return site + type + name
      }
    }
}

// "记住“site和type,生成实战课java专属函数
var shiZhanName = generateName('实战课')('Java')
console.log(shiZhanName);

// 输出 '实战课java零基础'
var res = shiZhanName('零基础')
console.log(res)

 // 啥也不记,直接生成一个完整课程
var itemFullName = generateName('实战课')('大数据')('零基础')
console.log(itemFullName);

1.3 偏函数

偏函数和柯里化类似,如果理解了柯里化,那么偏函数就小菜一碟了。

柯里化是将一个n个参数的函数转化成n个单参数函数,这里假如你有三个入参,你得嵌套三层函数,且每层函数只能有一个入参。柯里化的目标是把函数拆解为精准的n部分。

偏函数相比之下就比较随意了,偏函数是固定函数中的某一个或几个参数,然后返回一个新的函数。假如你有三个入参,你可以只固定一个入参,然后返回另一个入参函数。也就是说,偏函数应用是不强调 “单参数” 这个概念的。

仍然是上面的例子,原函数形式调用:

function generateName(site,type,name){
  return site + type + name;
}

// 调用时传入三个参数
var itemFullName = generateName('体系课', '前端', '2022')

偏函数改造:

function generateName(site){
    return function(type,name){
      return site + type + name
    }
}
// 把3个参数分两部分传入
var itemFullName = generateName('体系课')('前端', '2022')

1.4 防抖

在浏览器的各种事件中,有一些容易频繁触发的事件,比如scroll、resize、鼠标事件(比如 mousemove、mouseover)、键盘事件(keyup、keydown )等。频繁触发回调导致大量的计算会引发页面抖动甚至卡顿,影响浏览器性能。防抖和节流就是控制事件触发的频率的两种手段。

防抖的中心思想是:在某段时间内,不管你触发了多少次回调,我都只执行最后一次。

// fn是我们需要包装的事件回调, delay是每次推迟执行的等待时间
function debounce(fn, delay) {
  // 定时器
  let timer = null

  // 将debounce处理结果当作函数返回
  return function () {
    // 保留调用时的this上下文
    let context = this
    // 保留调用时传入的参数
    let args = arguments
    // 每次事件被触发时,都去清除之前的旧定时器
    if(timer) {
        clearTimeout(timer)
    }
    // 设立新定时器
    timer = setTimeout(function () {
      fn.apply(context, args)
    }, delay)
  }
}
// 用debounce来包装scroll的回调
const better_scroll = debounce(() => console.log('触发了滚动事件'), 1000)
document.addEventListener('scroll', better_scroll)

1.5 节流

节流的中心思想是:在某段时间内,不管你触发了多少次回调,我都只认第一次,并在计时结束时给予响应,也就是隔一段时间执行一次。

// fn是我们需要包装的事件回调, interval是时间间隔的阈值
function throttle(fn, interval) {
  // last为上一次触发回调的时间
  let last = 0

  // 将throttle处理结果当作函数返回
  return function () {
      // 保留调用时的this上下文
      let context = this
      // 保留调用时传入的参数
      let args = arguments
      // 记录本次触发回调的时间
      let now = +new Date()

      // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
      if (now - last >= interval) {
      // 如果时间间隔大于我们设定的时间间隔阈值,则执行回调
          last = now;
          fn.apply(context, args);
      }
    }
}
// 用throttle来包装scroll的回调
const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)
document.addEventListener('scroll', better_scroll)

2.性能问题

以上我们讲解了闭包的常见应用,可见闭包是一个非常强大的特性,但人们对其也有诸多误解。一种耸人听闻的说法是闭包会造成内存泄露,所以要尽量减少闭包的使用。真的是这样吗?

2.1 内存泄漏

该释放的变量没有释放,依然占据着内存空间,导致内存占用不断攀高,带来性能恶化,系统崩溃等一系列问题,这种现象叫做内存泄漏。

闭包里的变量是我们需要的变量本就不该释放,而又怎么称之为内存泄漏呢?

所以有关内存泄露问题,是谣言,是误传。

这个误传来源于IE,IE在我们使用完闭包之后,依然会受不了里面的变量,而这是IE的bug,不是闭包问题。

如果还不放心,我们来看以下例子:

function f1(){
    var num  = Math.randon();
    function f2(){
      return num
    }
    return f2
}

var f = f1();
f();

上面这段代码,f2函数中存在对变量num的引用,所以num变量并不会回收,我们可以,在函数调用后,可以把外部引用关系置空,如下:

function f1(){
    var num  = Math.randon();
    function f2(){
      return num
    }
    return f2
}

var f = f1();
f();
f = null;

事实上,闭包导致的内存泄漏是误传,闭包中引用的变量,其实也就相当于一个全局变量,并不会构成内存泄漏问题,内存泄漏大多原因是由于代码不规范导致。

2.2 常见的内存泄漏

2.21 不必要的全局变量

function f1() {
  name = '小明'
}

在非严格模式下引用未声明的变量,会在全局对象中创建一个新变量,在浏览器中,全局对象是window,这就意味着name这个变量将泄漏到全局。全局变量是在网页关闭时才会释放,这样的变量一多,内存压力也会随之增高。

2.22 遗忘清理的计时器

程序中我们经常会用到计时器,也就是setInterval和setTimeout

var timeId = setInterval(function(){
  // 函数体
},1000)

在计时器中,定时器内部逻辑是是无穷无尽的,当定时器囊括的函数逻辑不再被需要、而我们又忘记手动清除定时器时,它们就会永远保持对内存的占用。因此当我们使用定时器时,一定要明确计时器在何时会被清除,并使用 clearInterval(timeId)手动清除定时器。

2.23 遗忘清理的dom元素引用

var divObj = document.getElementById('mydiv')

// dom删除myDiv
document.body.removeChild(divObj);
console.log(divObj);
// 能console出整个div 说明没有被回收,引用存在

// 移出引用
divObj = null;
console.log(divObj)
// null

3.闭包与循环体

闭包和循环体的结合,是闭包最为经典的一种考察方式。

3.1 这段代码输出啥

我们来看一个大家非常熟悉的题目,以上6行代码输出什么?

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

如果你是刚入门的新手,你可能会给出这样的答案:

0 1 2 3 4 5

给出这样答案的同学,内心一般都是这样想:for循环输出了0-4个i的值,最后一行打印5,setTimeout这个好像在哪见过,但具体咋回事印象不深了,干脆直接忽略好了。

对于基础还不错的同学,对于setTimeout函数用法特性还有印象,很快就给出了“进化版”答案:

5 0 1 2 3 4

这一部分的同学是这样想的:for循环逐个输出0-4的值,但是setTimeout把输入延迟了1s,所以最后一行先执行,先输出5,然后过了1000ms,0-4会逐个输出。

如果你对JS中的for循环、同步与异步区别、变量作用域、闭包有正确理解,就知道正确答案应该是:

5 5 5 5 5 5

我们试着分析一下正确答案,seTimeout内函数延迟1000ms后执行,最后一行console先输出,最后一行输出5,所以第一个值是5。

for(var i =0;i<5;i++){
  // 5<5? 不满足
}
console.log(i) // 5

for循环里setTimeout执行了5次,函数延迟1000ms执行,大家看这个函数,它自身作用域压根就没有i这个变量,根据作用域链查找规则,要想输出i,需要去上层查找。

setTimeout(function() {
   console.log(i);
}, 1000);

但是,这个函数第一次被执行也是1000ms以后的事情了,此时它试图向上一层作用域(这里也就是全局作用域)去找一个叫i的变量,此时for循环已执行完毕,i也进入了最终状态5。所以当1000ms后,这个函数真正被执行的时候,引用到的i值已经是5了。 此时,这段代码的作用域状态示意如下:

对应的作用域关系如下:

接下来的连续四次,都会有一个一模一样的setTimeout回调被执行,它输出的也是同一个全局的i,所以说每一次输出都是5。

3.2 改造方法

循环了五次,每次却输出一个值,这种输出效果显然不好。如果我们希望让i从0-4依次被输出,我们改如何改造呢?

方案一:利用setTimeout中第三个参数

开头我们先复习一下setTimeout参数用法:

setTimeout(function(arg1,arg2){
  console.log(arg1);
  console.log(arg2);
},delay,arg1,arg2)
  • function(必须):调用函数执行的代码块
  • delay(可选):函数调用延迟的毫秒值,默认是0,意味着马上执行
  • arg1,...arg2(可选):附加参数,当计时器启动时,会作为参数传递给function

我们来看例子:

setTimeout(function(a,b){
  console.log(a);  // 1
  console.log(b);  // 2
},1000,1,2)

需要注意的一点是,附加参数只支持在ie9及以上浏览器,如要兼容,需要引入一段MDN提供的兼容旧IE代码

利用setTimeout的第三个参数,i作为形参传递给setTimeout的j,由于每次传入的参数是从for循环里面取到的值,所以会依次输出0~4:

for(var i=0; i<5; i++){
  setTimeout(function(j){
    console.log(j) // 0 1 2 3 4
  },1000,i)
}

方案二:使用闭包

使用闭包,我们往往会用到匿名函数。匿名函数也叫一次性函数,在函数定义时执行,且只执行一次。我们在setTimeout外面套一个匿名函数,利用匿名函数的实参来缓存每一个循环的i值。

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

当输出j时,引用的是外部函数传递的变量i,这个i是根据循环来的,执行setTimeout时已经确定了里面i的值,进而确定了j的值。

方案三:使用let

for(let i= 0; i<5; i++){
  setTimeout(function(){
    console.log(i)
  },1000)
 }

for循环每次循环产生一个新的块级作用域,每个块级作用域的变量是不同的。函数输出的是自己的上一级(循环产生的块级作用域)下i的值。

4.总结

  • 作用:模拟私有变量、柯里化、偏函数、防抖、节流、实现缓存。
  • 模拟私有变量:将私有变量放在外在的立即执行函数中,并通过立即执行U这个函数,创造一个闭包环境(私有变量:只允许函数内部,或对象方法访问的变量)。
  • 柯里化:把接受n个参数的一个函数转化成只接受一个参数n个函数互相嵌套的函数过程,目标是把函数拆解为精准的n部分,也就是将fn(a,b,c)转化成fn(a)(b)(c)的过程。
  • 偏函数:固定函数中的某一个或几个参数,然后返回一个新的函数,不强调但函数。
  • 防抖:只执行最后一次。
  • 节流:隔一段时间执行一次。
  • 缓存变量:计时器打印问题。
  • 闭包造成内存泄露问题是误传,误传来源于IE浏览器bug,可以放心大胆使用。

以上就是一文详解JavaScript闭包典型应用的详细内容,更多关于JavaScript闭包典型应用的资料请关注我们其它相关文章!

(0)

相关推荐

  • 关于javascript解决闭包漏洞的一个问题详解

    目录 解决闭包漏洞的一个问题 问题原理: 方法一: 方法二: 解决办法: 解决方法二: 总结 解决闭包漏洞的一个问题 在不修改下面代码的情况下,修改obj的内容 var o = (()=>{ var obj = { a:1, b:2, }; return { get :(n)=>{ return obj[n] } } })() 上面代码就是一个典型的闭包模式.屏蔽掉obj本身.只能访问闭包返回的数据而不能去修改数据源本身,但是他的数据源是一个对象,这就会出现一个漏洞!!!!,而上面的代码就会出

  • JavaScript 中的作用域与闭包

    目录 一.JavaScript 是一门编译语言 1.1 传统编译语言的编译步骤 1.2 JavaScript 与传统编译语言的区别 二.作用域(Scope) 2.1 LHS查询 和 RHS查询 2.2 作用域嵌套 2.3 ReferenceError 和 TypeError (1)ReferenceError (2)TypeError (3)ReferenceError 和 TypeError 的区别 小结 三.词法作用域 3.1 词法阶段 3.2 词法作用域 查找规则 3.3 欺骗词法 ——

  • JavaScript三大重点同步异步与作用域和闭包及原型和原型链详解

    目录 1. 同步.异步 2. 作用域.闭包 闭包 作用域 3. 原型.原型链 原型(prototype) 原型链 如图所示,JS的三座大山: 同步.异步 作用域.闭包 原型.原型链 1. 同步.异步 JavaScript执行机制,重点有两点: JavaScript是一门单线程语言 Event Loop(事件循环)是JavaScript的执行机制 JS为什么是单线程 最初设计JS是用来在浏览器验证表单操控DOM元素的是一门脚本语言,如果js是多线程的,那么两个线程同时对一个DOM元素进行了相互冲突

  • Javascript的作用域、作用域链以及闭包详解

    一.javascript中的作用域 ①全局变量-函数体外部进行声明 ②局部变量-函数体内部进行声明 1)函数级作用域 javascript语言中局部变量不同于C#.Java等高级语言,在这些高级语言内部,采用的块级作用域中会声明新的变量,这些变量不会影响到外部作用域. 而javascript则采用的是函数级作用域,也就是说js创建作用域的单位是函数. 例如: 在C#当中我们写如下代码: static void Main(string[] args) { for (var x = 1; x < 1

  • 一文剖析JavaScript中闭包的难点

    目录 一.作用域基本介绍 1. 全局作用域 2. 函数作用域 3. 块级作用域 二.什么是闭包 1. 闭包的基本概念 2. 闭包产生的原因 3. 闭包的表现形式 三.如何解决循环输出问题 1. 利用 IIFE 2. 使用 ES6 中的 let 3. 定时器传入第三个参数 一.作用域基本介绍 ES6之前只有全局作用域与函数作用域两种,ES6出现之后,新增了块级作用域. 1. 全局作用域 在JavaScript中,全局变量是挂载在window对象下的变量,所以在网页中的任何位置你都可以使用并且访问到

  • 一文详解JavaScript闭包典型应用

    目录 1.应用 1.1 模拟私有变量 1.2 柯里化 1.3 偏函数 1.4 防抖 1.5 节流 2.性能问题 2.1 内存泄漏 2.2 常见的内存泄漏 3.闭包与循环体 3.1 这段代码输出啥 3.2 改造方法 4.总结 1.应用 以下这几个方面是我们开发中最为常用到的,同时也是面试中回答比较稳的几个方面. 1.1 模拟私有变量 我们都知道JS是基于对象的语言,JS强调的是对象,而非类的概念,在ES6中,可以通过class关键字模拟类,生成对象实例. 通过class模拟出来的类,仍然无法实现传

  • 详解JavaScript闭包问题

    闭包是纯函数式编程语言的传统特性之一.通过将闭包视为核心语言构件的组成部分,JavaScript语言展示了其与函数式编程语言的紧密联系.由于能够简化复杂的操作,闭包在主流JavaScript库以及高水平产品代码中日益流行起来. 一.变量的作用域 在介绍闭包之前,我们先理解JavaScript的变量作用域.变量的作用域分为两种:全局变量和局部变量. 1.全局变量 var n = 999; //全局变量 function f1() { a = 100; //在这里a也是全局变量 alert(n);

  • 一文详解JavaScript中prototype的使用

    目录 prototype初步认识 函数有prototype属性,函数创建的对象没有 获得当前对象的属性 父和子的扩展 子的proto和prototype的区别 扩展得到的东西到底从哪来的 prototype初步认识 在学习JavaScript中,遇到了prototype,经过一番了解,知道它是可以进行动态扩展的 function Func(){}; var func1 = new Func; console.log(func1.var1) //undefined Func.prototype.v

  • 一文详解JavaScript 如何将 HTML 转成 Markdown

    目录 npm script 参数配置 前言: 本篇带来:在 JavaScript 如何将 HTML 转成 Markdown?先收藏,总有一天要用到!! npm 我们主要是借助 Turndown这个库来实现的 npm 安装 npm i turndown es6 import 引入: import TurndownService from 'turndown' CommonJs require 引入: const TurndownService = require('turndown'); 接下来我

  • 详解JavaScript作用域 闭包

    JavaScript闭包,是JS开发工程师必须深入了解的知识.3月份自己曾撰写博客<JavaScript闭包>,博客中只是简单阐述了闭包的工作过程和列举了几个示例,并没有去刨根问底,将其弄明白! 现在随着对JavaScript更深入的了解,也刚读完<你不知道的JavaScript(上卷)>这本书,所以乘机整理下,从底层和原理上去刨一下. JavaScript并不具有动态作用域,它只有词法作用域.词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的.了解闭包前,首先我

  • 详解JavaScript匿名函数和闭包

    概述 在JavaScript前端开发中,函数与对其状态即词法环境(lexical environment)的引用共同构成闭包(closure).也就是说,闭包可以让你从内部函数访问外部函数作用域.在JavaScript,函数在每次创建时生成闭包.匿名函数和闭包可以放在一起学习,可以加深理解.本文主要通过一些简单的小例子,简述匿名函数和闭包的常见用法,仅供学习分享使用,如有不足之处,还请指正. 普通函数 普通函数由fucntion关键字,函数名,() 和一对{} 组成,如下所示: function

  • 详解JavaScript作用域、作用域链和闭包的用法

    1. 作用域 作用域是指可访问的变量和函数的集合. 作用域可分为全局作用域和局部作用域. 1.1 全局作用域 全局作用域是指最外层函数外面定义的变量和函数的集合. 换言之,这些最外层函数外面定义的变量和函数在任何地方都能访问. 举个例子: // 最外层定义变量 var a = 1; console.log(a); // 最外层可以访问 function fnOne() { // 最外层函数 console.log(a); // 函数内可以访问 function fnTwo() { // 子函数

  • 详解JavaScript私有类字段和TypeScript私有修饰符

    JavaScript私有类字段和隐私需求 在过去,JavaScript 没有保护变量不受访问的原生机制,当然除非是典型闭包. 闭包是 JavaScript 中许多类似于私有模式(如流行的模块模式)的基础.但是,近年来 ECMAScript 2015 类被使用后,开发人员感到需要对类成员的隐私进行更多控制. 类字段提案(在撰写本文时处于第 3 阶段)试图通过引入私有类字段来解决问题. 让我们看看它们是什么样子的. 一个 JavaScript 私有类字段的例子 这是一个带有私有字段的 JavaScr

  • 详解JavaScript基于面向对象之继承实例

    javascript面向对象继承的简单实例: 作为一门面向对象的语言,继承自然是它的一大特性,尽管javascript的面向对象的实现机制和和c#和java这样典型的面向对象不同,但是继承的基本特点还是具有的,简单的说就是获得父级的方法和属性,下面是一段简单的实例,大家有兴趣可以分析一下: window.onload = function(){ function parent(age,name){ this.age = age; this.name = name; } parent.protot

  • 详解Javascript中prototype属性(推荐)

    在典型的面向对象的语言中,如java,都存在类(class)的概念,类就是对象的模板,对象就是类的实例.但是在Javascript语言体系中,是不存在类(Class)的概念的,javascript中不是基于'类的',而是通过构造函数(constructor)和原型链(prototype chains)实现的.但是在ES6中提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板.通过class关键字,可以定义类.基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能

随机推荐

其他