闭包
1.创建函数
开辟一个堆内存
把函数体中的代码当做字符串存储进去
把堆内存的地址赋值给函数名/变量名
函数在哪创建的,那么它执行的时候需要查找上级作用域是谁
2.函数执行
形成一个全新的私有作用域(执行一次形成一个,多个之间不会产生影响)
形参赋值&变量提升
代码执行(把所属堆内存中的代码字符串拿出来一行行执行)
遇到一个变量,首先看它是否为私有变量(形参和在私有作用域中声明的变量是私有变量)是私有的就操作自己变量即可,不是私有的则向上级作用域中查找,,一直找到全局作用域为止=>作用域链查找机制
私有变量和外界的变量没有必然关系,可以理解为被私有占内存保护起来了,这种机制其实就是 闭包保护机制
3.关于堆栈内存释放问题
函数执行就会形成占内存(从内存中分配的一块空间,)如果内存都不销毁释放,很容易就导致内存溢出(内存爆满,电脑卡死了)堆栈内存的释放问题是学习 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
- 栈内存释放
// 打开浏览器形成的全局作用域就是栈内存
// 手动执行函数形成的私有作用域就是栈内存
// 基于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 等方式暴露给别人即可
//原生js
var xxx(function (){
//自己的代码
return xxx;
})()
//原生js
var xxx(function (){
//自己的代码
return xxx;
})()
闭包的运行机制
我们通过题目来详细了解闭包的运行机制,以及代码执行流程
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
练习
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
现在来做这种题是不是觉得非常简单,这里我就不过多追溯了。
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
再来一题
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
变态题
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 坚持时出现的暂时性死区问题
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
// 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 具有块级作用域还体现在
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();
当大括号包裹变量时,外部是无法访问到大括号内部的变量信息的