JavaScript | 闭包

[JavaScript] 闭包

在JavaScript中存在着闭包这以概念,也是前端面试中经常会提到的一个知识点,下面就来介绍一下闭包吧。

闭包是什么?

首先来看下MDN(Mozilla Developer Network)官网对于闭包这一概念的定义

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。------MDN官网(相关链接)

读起来不太好理解,实际上翻译成白话文就是:在一个作用域中可以访问另一个函数内部的局部变量的函数。

下面是闭包的一个基本使用

javascript 复制代码
function makeFunc() {
    var name = "Mozilla";
    function displayName() {
        alert(name);
    }
    return displayName;
}

var myFunc = makeFunc();
myFunc();

可以发现在displayName这个作用域下访问了另外一个函数makeFunc下的局部变量name

闭包的实现,实际上是利用了JavaScript中作用域链的概念,简单理解就是:在JavaScript中,如果在某个作用域下访问某个变量的时候,如果不存在,就一直向外层寻找,直到在全局作用域下找到对应的变量为止,这里就形成了所谓的作用域链。

闭包的特性

  1. 闭包可以访问到父级函数的变量
  2. 访问到父级函数的变量不会销毁

现在来看下闭包的相关应用,首先来看下下面这段代码:

javascript 复制代码
var age = 18;

function person(){
    age++;
    console.log(age);
}
    
person(); // 19
person(); // 20
person(); // 21

可以看到这里调用了3次函数,age的值也从18增长到了21,但是这么写会导致全局变量被污染,所以将age的定义移动到person函数内部,代码如下:

javascript 复制代码
function person() {
  var age = 18;
  age++;
  console.log(age);
}

person(); // 19
person(); // 19
person(); // 19

但是这又导致了另一个问题,变为局部变量的age不会自增了,所以那么就可以利用闭包的这个特性将每次调用时的age保存起来这样就可以实现变量的自增了,代码如下:

javascript 复制代码
function person() {
  var age = 18;
  return function(){
    age++;
    console.log(age);
  }
}

let getPersonAge = person();
getPersonAge(); // 19
getPersonAge(); // 20
getPersonAge(); // 21

可以这样理解,通过将person函数赋值给getPersonAge这个变量,可以看作如下代码

javascript 复制代码
let getPersonAge = function(){
  age++;
  console.log(age);
}

每当调用getPersonAge()函数的时候,首先要获取age变量,因为JavaScript中存在作用域链的关系,所以会从person函数下得到对应的age,因为闭包存在着闭包可以访问到父级函数的变量,且该变量不会销毁的特性所以上次的变量会被保留下来,所以可以做到自增的实现。

如果对变量不会销毁这一特性有疑问可以参考下寸志老师对于闭包的理解:

函数当作值传递,即所谓的first class对象。就是可以把函数当作一个值来赋值,当作参数传给别的函数,也可以把函数当作一个值 return。一个函数被当作值返回时,也就相当于返回了一个通道,这个通道可以访问这个函数词法作用域中的变量,即函数所需要的数据结构保存了下来,数据结构中的值在外层函数执行时创建,外层函数执行完毕时理因销毁,但由于内部函数作为值返回出去,这些值得以保存下来。而且无法直接访问,必须通过返回的函数。这也就是私有性。

作者:寸志 链接:www.zhihu.com/question/34...

闭包的应用

所以就可以根据这个特性做几个小案例测试一下。

循环注册事件

比如就可以利用闭包的特性做循环点击事件,比如下面的给输入框添加onblur事件:

需求:点击输入框,上面的提示栏显示对应的内容

html 复制代码
<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
<script>
  function showHelp(help) {
    document.getElementById('help').innerHTML = help;
  }

  function setupHelp() {
    var helpText = [
      { 'id': 'email', 'help': 'Your e-mail address' },
      { 'id': 'name', 'help': 'Your full name' },
      { 'id': 'age', 'help': 'Your age (you must be over 16)' }
    ];

    for (var i = 0; i < helpText.length; i++) {
      // var func = function (i) {
      //   document.getElementById(helpText[i].id).onfocus = function () {
      //     showHelp(helpText[i].help);
      //   }
      // };
      // func(i);
      (function (i) {
        document.getElementById(helpText[i].id).onfocus = function () {
          showHelp(helpText[i].help);
        }
      })(i);
    }
  }

  setupHelp();
</script>

PS:这里如果不想用闭包的话,可以使用ES2015中引入的let以及const关键字,或者使用forEach遍历helpText时给对应的item添加focus事件都可以解决

循环中的定时器

javascript 复制代码
var lis = document.querySelector('.test').querySelectorAll('li');
for (var i = 0; i < lis.length; i++) {
  // var fc = function (i) {
  //   setTimeout(function () {
  //     console.log(lis[i].innerHTML);
  //   }, 3000);
  // };
  // fc(i);
  (function (i) {
    setTimeout(function () {
      console.log(lis[i].innerHTML);
    }, 3000);
  })(i);
}

案例1与2的总结:利用立即执行函数所形成的闭包来保存当前循环中的i的值,进而解决异步任务所带来的i最后为4(循环结束后i的值)的问题

模拟私有方法

下面的示例展现了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量:

javascript 复制代码
Countervar Counter = function(){
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function(){
      return changeBy(1);
    },
    decrement: function(){
      return changeBy(-1);
    },
    getValue: function(){
      return privateCounter;
    }
  }
}

var counterInstance = Counter();
console.log(counterInstance.getValue()); // 0
counterInstance.increment();
counterInstance.increment();
counterInstance.increment();
console.log(counterInstance.getValue()); // 3
counterInstance.decrement();
console.log(counterInstance.getValue()); // 2

还可以将Counter存在其他变量中以便可以形成多个计数器

javascript 复制代码
var counterInstance1 = Counter();
var counterInstance2 = Counter();
// c1 计数器1
console.log(counterInstance1.getValue()); // 0
counterInstance1.increment();
counterInstance1.increment();
counterInstance1.increment();
console.log(counterInstance1.getValue()); // 3
counterInstance1.decrement();
console.log(counterInstance1.getValue()); // 2

// c2 计数器2
console.log(counterInstance2.getValue()); // 0
counterInstance2.increment();
counterInstance2.increment();
console.log(counterInstance2.getValue()); // 2
counterInstance2.decrement();
counterInstance2.decrement();
counterInstance2.decrement();
console.log(counterInstance2.getValue()); // -1

性能考量

如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响

其导致主要原因可以参考上面寸志老师的回答,这会导致变量不会被垃圾回收机制回收,造成内存消耗 以及对于不恰当的使用闭包可能会造成内存泄漏的问题

比如在定义类的时候吧对应的方法定义在了构造函数下,这样就会导致每次实例化对象的时候,每个方法都会被重新赋值:

javascript 复制代码
function Sony(camera, price){
  this.camera = camera;
  this.price = price;
  this.getCamera = function(){
    return this.camera;
  }
  this.getPrice = function(){
    return this.price;
  }
}

let s1 = new Sony('ZV-1', 5300);

通常情况下,都会将getCameragetPrice放在原型对象下

javascript 复制代码
Sony.prototypefunction Sony(camera, price){
  this.camera = camera;
  this.price = price;
}

Sony.prototype.getCamera = function(){
    return this.camera;
};

Sony.prototype.getPrice = function(){
    return this.price;
};
let s1 = new Sony('ZV-1', 5300);

不推荐使用Sony.prototype = {fun1:{},fun2:{}}的形式,这样相当于是重写了Sony.prototype这个原型对象

内存泄漏的解决方案

先来看下面这个案例

javascript 复制代码
this.name = 'WindowName'
let myObj = {
  name: 'beast senpai',
  get: function(){
    return function(){
      console.log(this); // WindowName
      return this.name;
    }
  }
}

let myObjname = myObj.get()();
console.log(myObjname); // WindowName

这里发生了内存泄漏使得this指向了Window对象(myObj.get()()这种写法和立即执行函数很类似,立即执行函数的this指向Window

解决方案1:在get函数使用that保存此时的this

javascript 复制代码
this.name = 'WindowName'
let myObj = {
  name: 'beast senpai',
  get: function(){
    let that = this;
    return function(){
      console.log(that); // myObj
      return that.name;
    }
  }
}

let myObjname = myObj.get()();
console.log(myObjname); // beast senpai

解决方案2:将get函数的返回值改回使用箭头函数的方式做返回

javascript 复制代码
this.name = 'WindowName'
let myObj = {
  name: 'beast senpai',
  get: function(){
    return ()=>{
      console.log(this); // myObj
      return this.name; 
    }
  }
}

let myObjname = myObj.get()();
console.log(myObjname); // beast senpai

消除闭包

不用的时候解除引用,避免不必要的内存占用

取消fn对外部成员变量的引用,就可以回收相应的内存空间。

javascript 复制代码
function add() {
  var count = 0
  return function fn() {
    count++
    console.log(count)
  }
}

var a = add() // 产生了闭包
a() // 1
a() // 2
a = null // 取消 a 与 fn 的联系,这个时候浏览器回收机制就能回收闭包空间

总结

闭包的作用:

  1. 延申了变量的作用范围
  2. 隐藏变量,避免全局污染

闭包的缺点:

  1. 因为垃圾回收机制的存在,会导致出现不必要的性能消耗
  2. 不恰当的使用会出现内存泄漏

参考文章:

原文:JavaScript | 闭包

相关推荐
雪碧聊技术29 分钟前
01-Ajax入门与axios使用、URL知识
前端·javascript·ajax·url·axios库
adminIvan34 分钟前
Element plus使用menu时候如何在折叠时候隐藏掉组件自带的小箭头
前端·javascript·vue.js
会发光的猪。1 小时前
【 ElementUI 组件Steps 步骤条使用新手详细教程】
前端·javascript·vue.js·elementui·前端框架
我家媳妇儿萌哒哒1 小时前
el-table合并单元格之后,再进行隔行换色的且覆盖表格行鼠标移入的背景色的实现
前端·javascript·elementui
前端青山1 小时前
webpack指南
开发语言·前端·javascript·webpack·前端框架
还是大剑师兰特3 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
一只小白菜~3 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding3 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT3 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓3 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js