Skip to content

闭包

1.创建函数

  • 开辟一个堆内存

  • 把函数体中的代码当做字符串存储进去

  • 把堆内存的地址赋值给函数名/变量名

  • 函数在哪创建的,那么它执行的时候需要查找上级作用域是谁

    2.函数执行

  • 形成一个全新的私有作用域(执行一次形成一个,多个之间不会产生影响)

  • 形参赋值&变量提升

  • 代码执行(把所属堆内存中的代码字符串拿出来一行行执行)

  • 遇到一个变量,首先看它是否为私有变量(形参和在私有作用域中声明的变量是私有变量)是私有的就操作自己变量即可,不是私有的则向上级作用域中查找,,一直找到全局作用域为止=>作用域链查找机制

  • 私有变量和外界的变量没有必然关系,可以理解为被私有占内存保护起来了,这种机制其实就是 闭包保护机制

    3.关于堆栈内存释放问题
    函数执行就会形成占内存(从内存中分配的一块空间,)如果内存都不销毁释放,很容易就导致内存溢出(内存爆满,电脑卡死了)堆栈内存的释放问题是学习 JavaScript 的核心知识之一

  • 堆内存释放问题

javascript
// 创建一个引用类型值,就会创建一个堆内存
// 如果当前创建的堆内存不被其他东西所占用,浏览器就会在空闲的时候,查找每一个内存的引用状况,不被占用的都会回收释放掉
let obj = {
  name: 'anjing',
}
let oop = obj
// 此时obj和oop都占用对象的堆内存,想要释放堆内存,需要手动解除变量和值的关联(null:空对象指针)
obj = null
oop = null
// 创建一个引用类型值,就会创建一个堆内存
// 如果当前创建的堆内存不被其他东西所占用,浏览器就会在空闲的时候,查找每一个内存的引用状况,不被占用的都会回收释放掉
let obj = {
  name: 'anjing',
}
let oop = obj
// 此时obj和oop都占用对象的堆内存,想要释放堆内存,需要手动解除变量和值的关联(null:空对象指针)
obj = null
oop = null
  • 栈内存释放
javascript
// 打开浏览器形成的全局作用域就是栈内存
// 手动执行函数形成的私有作用域就是栈内存
// 基于es6的let const 形成的块级作用域也是栈内存

// 全局栈内存:关掉页面的时候才会销毁
// 私有栈内存:一般情况下,函数只要执行完成,形成的私有栈内存就会被销毁释放掉 2.但是一旦栈内存某个东西(一般都是堆地址)被私有作用域意外的事物占用,则当前的私有栈内存不能立即被释放销毁,特点:私有作用域的私有变量等信息也保留起来了
// 市面上认为的闭包是:函数执行形成不能被释放的私有栈内存,这样才是闭包
function fn() {
  //...
}
fn() // 函数执行形成栈内存,执行完成栈内存销毁

function x() {
  return function () {
    //....
  }
}
let f = x() // f占用x执行形成的栈内存中的一个东西,(返回小函数对应的堆,则x执行形成的栈内存不能被释放了
// 打开浏览器形成的全局作用域就是栈内存
// 手动执行函数形成的私有作用域就是栈内存
// 基于es6的let const 形成的块级作用域也是栈内存

// 全局栈内存:关掉页面的时候才会销毁
// 私有栈内存:一般情况下,函数只要执行完成,形成的私有栈内存就会被销毁释放掉 2.但是一旦栈内存某个东西(一般都是堆地址)被私有作用域意外的事物占用,则当前的私有栈内存不能立即被释放销毁,特点:私有作用域的私有变量等信息也保留起来了
// 市面上认为的闭包是:函数执行形成不能被释放的私有栈内存,这样才是闭包
function fn() {
  //...
}
fn() // 函数执行形成栈内存,执行完成栈内存销毁

function x() {
  return function () {
    //....
  }
}
let f = x() // f占用x执行形成的栈内存中的一个东西,(返回小函数对应的堆,则x执行形成的栈内存不能被释放了

闭包的两个作用 
从性能角度讲,我们真实项目中应该减少对闭包的使用,(因为闭包会产生不释放的栈内存,过多使用容易导致内存溢出或者减低性能)

  • 保护

  • 保存

    1.jQuery 前端非常经典的类库:提供了大量的方法供开发人员使用
    =>为了防止全局变量污染(解释:导入 JQ 后,它里面有大量的方法,如果这些方法不保护起来,用户编写的方法很容易和 JQ 方法相同产生冲突,产生冲突可以理解为全局变量污染,)JQ 的方法和变量需要闭包来保护起来。
    在真实项目中,我们一般把自己写的内容放到一个闭包中,这样可以有效防止自己的代码和别人代产生冲突(全局变量污染:真实项目要尽可能减少对全局变量的使用,);如果需要把自己的东西给别人用,基于 return 和 window.xxx 等方式暴露给别人即可

javascript
//原生js
var xxx(function (){
   //自己的代码
  return xxx;
})()
//原生js
var xxx(function (){
   //自己的代码
  return xxx;
})()

闭包的运行机制

我们通过题目来详细了解闭包的运行机制,以及代码执行流程

javascript
var n = 0
function a() {
	var n = 10
	function b() {
		n++
		console.log(n)
	}
	b()
	return b //11
}
var c = a() //11
c() //12
console.log(n) //0
var n = 0
function a() {
	var n = 10
	function b() {
		n++
		console.log(n)
	}
	b()
	return b //11
}
var c = a() //11
c() //12
console.log(n) //0

1.首先在全局作用域下,先变量提升,将带有 var function  的变量提到最前面 然后让 n 赋值为 0,等变量提升,和变量赋值关联结束时,到了函数 a (需了解函数底层运行机制)开辟了一块堆内存,里面包含的是函数 a 里面所有的代码,存储方式是字符串存储,a 指向的是函数 a(不过指向的是地址)然后执行 c = a(),即将函数 a 执行然后赋值给 a,则函数开辟块全新的私有栈内存,将函数堆内存中的字符串代码执行(里面也包含了形参赋值,和变量提升,然后又开辟了一块堆内存,里面包含的是函数 b 的字符串代码),然后执行代码 n = 10,然后执行函数 b,又开辟了一块全新的私有栈内存用来执行函数 b(n++,由于 n 不是私有的,需要往上级作用域中查找,即在函数 a 中查找 n,然后 n = 10+ 1= 11,然后 return b ,即返回了一个函数 b,此时 c 执行了函数 b,此时函数 a 的私有栈内存中的作用域是不能立即销毁的,因为 c 指向的是函数函数 b,而 b 又由函数 a 得到,然后执行 c(),既执行的是函数 b,然后到 n++,然后向上级作用域中查找,由于头开始执行函数 b 的时候,n 已经等于 11,所有此时 n++,n 等于 12
练习

javascript
var a=10,b = 11,c = 12;
 function test(a) {
     a=1;
     var b =2;
     c= 3;
 }
 test(10);
 console.log(a); //10
 console.log(b); //11
 console.log(c) //3
var a=10,b = 11,c = 12;
 function test(a) {
     a=1;
     var b =2;
     c= 3;
 }
 test(10);
 console.log(a); //10
 console.log(b); //11
 console.log(c) //3

现在来做这种题是不是觉得非常简单,这里我就不过多追溯了。

javascript
var a = 9
function fn() {
	a = 0
	return function (b) {
		return b + a++
	}
}
var f = fn()
console.log(f(5)) //5
console.log(fn()(5)) //5
console.log(f(5)) //6
console.log(a) //2
var a = 9
function fn() {
	a = 0
	return function (b) {
		return b + a++
	}
}
var f = fn()
console.log(f(5)) //5
console.log(fn()(5)) //5
console.log(f(5)) //6
console.log(a) //2

再来一题

javascript
function fn(i) {
	return function (n) {
		console.log(n + i++)
	}
}
var f = fn(10)
f(20) //30  i =11
fn(20)(40) //20+41 = 60
fn(30)(50) //80
f(30) // 41 30+11
function fn(i) {
	return function (n) {
		console.log(n + i++)
	}
}
var f = fn(10)
f(20) //30  i =11
fn(20)(40) //20+41 = 60
fn(30)(50) //80
f(30) // 41 30+11

变态题

javascript
var num = 10 // 全局变量 10
var obj = {num: 20} // 开辟堆内存
obj.fn = (function (num) {
	this.num = num * 3
	num++
	return function (n) {
		this.num += n
		num++
		console.log(num)
	}
})(obj.num)
var fn = obj.fn
fn(5) // 22
obj.fn(10) //23
console.log(num, obj.num) //65 30
var num = 10 // 全局变量 10
var obj = {num: 20} // 开辟堆内存
obj.fn = (function (num) {
	this.num = num * 3
	num++
	return function (n) {
		this.num += n
		num++
		console.log(num)
	}
})(obj.num)
var fn = obj.fn
fn(5) // 22
obj.fn(10) //23
console.log(num, obj.num) //65 30

变量提升

当浏览器开辟出宫代码执行的占内存后,代码并没有自上而言下立即执行,而是继续做了一些事情: 把当前作用域中所有带 var funcion 关键字的进行提前声明和定义=>变量提升机制

  • 带 var 的只是提前声明 var a 如果只声明并没有赋值,默认为 undefined

  • 带 function 的不仅是声明,而且还定义了 a = 13 定义其实就是赋值,准确来说就是让变量和某个值进行关联

    1.let const 不存在变量提升机制
    创建变量的种方式中,var function 有变量提升,而 let const import class 不存在这个机制
    2.var 运行重复声明(或执行上下文中)
    在相同的作用域中或执行上下文中 如果使用 var function 关键词声明变量并且重复声明,是不会影响的 ,但是使用 let const 就不行,浏览器会效验当前作用域中是否已经存在这个变量了,如果已经存在了,则再次基于 let 重新声明就会报错
    (在浏览器开辟占内存供代码自上而下执行之前,不仅有变量提升的操作,还有很多其他的操作,如词法解析或者词法检测,如果发现错误,则所有代码不执行
    3.let 可以解决 typeof 坚持时出现的暂时性死区问题

javascript
let a = 12;
function fn() {
  console.log(a); // uncaught referenceError:cannot access 'a' before initialization
  let a = 13; //词法解析,类似于变量提升,已经知道了当前私有栈中有个 Let a ,此时的私有栈中出现的A都是私有的形参赋值&变量提升
}
fn();
console.log(a); //12
// 在当前作用域下,如果创建变量,使用的是let const,一定不能再创建代码前面使用这些变量,否则报错referenceError:cannot access 'a' before initialization
let a = 12;
function fn() {
  console.log(a); // uncaught referenceError:cannot access 'a' before initialization
  let a = 13; //词法解析,类似于变量提升,已经知道了当前私有栈中有个 Let a ,此时的私有栈中出现的A都是私有的形参赋值&变量提升
}
fn();
console.log(a); //12
// 在当前作用域下,如果创建变量,使用的是let const,一定不能再创建代码前面使用这些变量,否则报错referenceError:cannot access 'a' before initialization

Let

javascript
// let 所在的大括号是一个块作用域(私有作用域)
if(1===1) {
  var a= 12;
   let b = 13; //有块级作用域。私有块
}
console.log(a)
console.log(b) //b is noe defined
// let 所在的大括号是一个块作用域(私有作用域)
if(1===1) {
  var a= 12;
   let b = 13; //有块级作用域。私有块
}
console.log(a)
console.log(b) //b is noe defined

let 具有块级作用域还体现在

javascript
let n = 12;
function fn() {
  if (1) {
    let n = 123;
    console.log(n);
  }
  console.log(n); //12
}
fn();
let n = 12;
function fn() {
  if (1) {
    let n = 123;
    console.log(n);
  }
  console.log(n); //12
}
fn();

当大括号包裹变量时,外部是无法访问到大括号内部的变量信息的