一句话解释javascript闭包

1,275 阅读5分钟

(今天第一次写博客,目的是给自己做学习笔记和个人知识点总结)

什么是闭包?(直入主题

个人的解释就是函数内部再return一个函数或者内部函数要访问他的外部变量

在学闭包之前必须先了解 变量的作用域

① 函数内部可以直接读取全局变量

example:

var num = 123;
(() => console.log(num))()  
// 123

log的结果num为123

② 在函数外部无法读取函数内部的局部变量

(ps函数内部定义的变量必须要用var 等来声明,否则执行js的时候被视为全局变量)

example:

(()=>{
  var n = 999
})()

console.log(n) // error

因为我们无法直接访问函数内部的变量的值,所以要通过闭包来读取函数局部变量的值

example:

(()=> {
    var a = 123
    return ()=> a 
 })()
// 或者没有return
((time)=> {
  var a = 123
  setTimeout(()=> {
    // 处理逻辑...
  }, time)
})()
 

闭包作用?

① 访问函数内部变量的值(上面说了)

② 让闭包的值始终保存在内存中

example:

var counter = (()=>{
  var num = 999
  function  changeBy(val){
    num += val
  }
  return {
     increment: () =>{ changeBy(1) },
     decrement: () =>{ changeBy(-1) },
     result: () =>{ return  num }
  } 
})()
console.log( counter.result() )   // 999
//接下来我们改变下局部变量num值
counter.increment()   //num + 1
//再打印num的值,已经发生改变
console.log(counter.result())  // 1000
counter.decrement()
counter.decrement()
console.log(counter.result())   // 998 

在counter中,changeby被counter.increment, counter.decrement, counter.result所共享,在这个匿名自调用函数下的changeBy和num都无法被外部所访问,必须通过匿名函数返回的三个公共函数来访问。

每次声明变量的时候都会创建一个新的函数储存空间
var counter = function() {
  var num = 999
  function changeBy(val) {
    num += val
  }
  return {
    increment: () =>{ changeBy(1) },
    decrement: () =>{ changeBy(-1) },
    result: () =>{ return num }
  }
}
var n1 = counter()
var n2 = counter()
n1.result() // num = 999
n1.increment() //num + 1
// 此时再打印,num已经改变
n1.result() // num = 1000
n2.result() // num = 999  

数内部私有上述代码中分别用n1,n2接收了counter函数的返回值,但是n1和n2他们各自都是独立的,每个闭包都是引用自己词法作用域内的变量 num。

闭包内的this指向问题

example:

var name = "The Window";
var object = {
  name : "My Object",
  getNameFunc : function(){
    return function(){
      //此时的this指向全局Window
      return this.name 
    }
  }
}
alert(object.getNameFunc()()) // "The Window"
如果想要this的指向当前函数,在函数内部定义私有变量

example:

var name = "The Window";
var object = {
  name : "My Object",
  getNameFunc : function(){
    // 因为函数内部私有变量外部无法读取
    // 此时的this指向object
    var that = this
    return function(){
      return that.name
    }
  }
}
alert(object.getNameFunc()()) // "My Object"

最后分享一个闭包案例:
<body>
<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>
</body>

(ps样式就不写了,主要是看闭包运行机制)

javascript逻辑部分:
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 item = helpText[i];
    document.getElementById(item.id).onfocus = 
    function() {
      showHelp(item.help);
    }
  }
setupHelp()

循环helpText数组获取对应的每一项的<p>元素并注册onfocus事件处理函数,让最上面的<p>提示框写入对应的help提示内容。

逻辑看起来没啥问题,运行这段代码之后,无论鼠标焦点在哪个input上, 最上面的<p>元素始终显示的是数组的最后一项的‘help’信息,并没有达到想要的效果。

原因是赋值给onfocus的是闭包。用我自己的话讲就是setupHelp函数下面还有个onfocus事件处理函数形成了闭包,在setupHelp函数域中存在一个变量item,在js执行机制中,循环在 onfocus事件触发之前早已执行完成,item的值已被锁定在循环对象的最后一项。

es6箭头函数=>完美解决闭包这个问题:

看代码=>

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}
(() => {
  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)'}
  ]
    helpText.map(a => {
    return document.getElementById(a.id).onfocus = function() {
      showHelp(a.help)
    }
  })
})()

map循环使用匿名箭头函数就不存在闭包问题了。
解决这个闭包问题另一种方案就是使用更多的闭包:

example:

function showHelp(help) {
  document.getElementById('help').innerHTML = help
}

function makeHelpCallback(help) {
  return function() {
    showHelp(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 item = helpText[i]
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help)
  }
}

setupHelp()

makeHelpCallback函数为后面的每个回调提供了一个新的执行环境(不再处于setupHelp同一个函数的作用域内),help的此时指向helpText数组中所对应的值

还可以匿名函数闭包:把函数内部私有变量和闭包内的回调放在同一作用域
function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}
(() => {
  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++) {
    // document.getElementById(item.id).onfocus = callbackHelp(item.help)
    (()=> {
      var item = helpText[i]
      document.getElementById(item.id).onfocus = () => {
        showHelp(item.help)
      }
    })()
  }
 })()

性能问题

总结一点,为了没必要的使用闭包,函数内的私有变量用let关键词来代替var

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

为了避免定义没有好处的闭包,在声明函数内部定义函数的时候,可以通过函数的prototype原型上添加事件处理函数,直接上代码吧:

example:

function MyObject(name, message) {
  this.name = name.toString()
  this.message = message.toString()
  this.getName = function() {
    return this.name
  }
  this.getMessage = function() {
    return this.message
  }
}

在上面代码中,this.getName,this.getMessage两个没有必要的闭包,

为了性能不再使用无用的闭包可以修改成:

function MyObject(name, message) {
  this.name = name.toString()
  this.message = message.toString()
}
MyObject.prototype.getName = function() {
  return this.name
}
MyObject.prototype.getMessage = function() {
  return this.message
}