场景
1.开发一个消费程序
支持根据参数修改金额
方法1,全局变量
var money = 500
function sub(m) {
money = money - m
console.log(`消费金额{$m},剩余${money}`)
}
sub(100)
sub(200)
缺点
其他代码容易修改到全局变量
var money = 500
function sub(m) {
money = money - m
console.log(`消费金额{$m},剩余${money}`)
}
sub(100)//消费金额100,剩余400
sub(200)//消费金额200,剩余200
var money = 50 //其他开发不清楚有全局的变量,自己修改了
方法2 局部变量
优点
不影响全局
缺点\
- 每次都重新初始化
- 调用完就会被销毁,不能重复使用
function sub(m) {
var money = 500 //如果放在方法里,则每次都会创建+销毁
money = money - m
console.log(`消费金额{$m},剩余${money}`)
}
//每次结果都是一样
sub(100) //消费金额100,剩余400
sub(100) //消费金额100,剩余400
sub(100) //消费金额100,剩余400
使用闭包
优点
- 可以做到函数的局部变量重复使用一个变量的效果
- 同时不污染外部变量
实现方式:
- 在原有函数外部多包裹一层方法 (包裹要保护的变量与使用变量的内层函数)
- 原函数通过return返回
- 通过函数柯里化方式调用
function mainSub() { //1.多包一层方法
var money = 500
return function sub(m) {//2.原函数return返回,这里可以不起名,直接用匿名函数
money = money - m
console.log(`消费金额${m},剩余${money}`)
}
}
//1.需要先保存引用,再调用
let mainSubFun = mainSub(); //注意这里的内层函数sub,只是定义了,未执行
//2.通过函数柯里化方式调用
mainSubFun(100)
mainSubFun(200)
如果单独分开调用,则每次mainSub每次都是独立的一个对象,里面的money也是互相独立。
mainSub()(100)//消费金额100,剩余400
mainSub()(200)//消费金额200,剩余300 //money并没有共享
注意点
- 内层函数sub只是定义,并未调用过。
- function sub() 其实等价于 new Function,函数声明定义其实就是new一个对象,得到一个引用地址
- new Function都会同步在对象创建作用域链,当前VO暂时为空,等待执行才赋值并加入到作用域链上。 执行流程
- 由于都每次都要先调用 mainSub(),所以每次都会mainSub的内部money作用域变量。
- mainSub方法返回的是一个sub对象,这个sub对象里面保留了对mainSub函数的money变量的引用。
- 由于mainSub执行结束,mainSub的作用域对象都会释放,但是由于sub的作用域链中保留了mainSub作用域对象的引用,所以mainSub作用域下的money变量被保留下来。
所有函数调用完,只会清除离自己的作用域。
//1.首先创建全局作用域对象window,同时包含mainSub函数定义和变量mainSubFun定义
//2.function mainSub(){...} 执行了new Function(..) 多了个全局对象mainSub,同时建立对应的作用域链
function mainSub() { //外层函数 3.创建作用域对象与局部变量
var money = 500 //4.执行方法体内容
return function sub(m) {//6.内层函数 创建局部的作用域对象sub,同时建立对应的作用域链
money = money - m //7.sub执行代码
console.log(`消费金额${m},剩余${money}`)
//8.清除sub函数作用域
}
// 5.释放函数作用域对象VO,返回值
}
let mainSubFun = mainSub(); //3.执行外部函数 (1)创建作用域vo(2)执行逻辑并 返回的sub对象(3)函数销毁清空当前的作用域,由于被mainSubFun 引用着,mainSub()的作用域对象无法销毁。
mainSubFun(100)// 6.执行内部函数 (1)创建作用域vo,(2)执行逻辑(3)函数销毁清空当前的作用域
mainSubFun(200)// 7.执行内部函数 (1)创建作用域vo,(2)执行逻辑(3)函数销毁清空当前的作用域
mainSubFun = null;//8.切除对内部函数的引用。闭包和其他引用对象都会被释放
- 程序刚启动-创建全局作用域对象window和全局作用域链
- 全局函数定义mainSub与变量声明mainSubFun
- 外层函数执行-mainSub创建作用域对象与局部变量
- 外层函数执行-mainSub执行方法体内容
- 外层函数执行-mainSub清除自己函数作用域对象,返回值
- 内层函数执行-sub创建作用域与变量
- 内层函数执行-sub执行代码
- 内层函数执行-sub清除自己函数作用域
- 内层函数执行-执行结束,sub恢复原来效果
- 释放闭包的所有引用mainSubFun=null
定义
- 闭包也是一个对象
- 闭包就是每次调用外层函数时,临时创建的函数作用域对象。
- 外层函数作用域对象能留下来,是因为被内层函数对象的作用域链引用着, 无法释放。
原理
外层函数调用后,外层函数的作用域对象,被返回的内层函数的作用域链引用着,无法释放,就形成了闭包对象。
缺点
闭包逻辑很隐晦,容易找不到,所以容易造成内层泄露
解决方法
将保存内层函数对象的变量赋值为null
let mainSubFun = mainSub();
mainSubFun(100)
mainSubFun(200)
//调用结束后,把外层函数指向空
mainSubFun = null
分析闭包的方法
1.确定3个元素
- 外层函数
- 外层函数的局部变量
- 内层函数
2.外层函数返回内层函数3种方法
- return function
//匿名函数
function outerFun() {
var para = 0
return function () { //
para++
}
}
//表达式返回
function outerFun() {
var para = 0
var fun = function innerFun(){para++}
return fun
}
//表达式返回-科里化
function outerFun() {
var para = 0
var fun = function innerFun(){
para++
return fun//这是科里化的方式,返回后可以继续一直调用
}
return fun
}
let ofun = outerFun()
ofun()()()()
- 强行赋值为全局变量 , 通过不定义,直接赋值的方式
function outerFun() {
var para = 0
fun = function innerFun(){//这里不申请,让他提前到全局定义
para++
}
return fun
}
//等价于
var fun;
function outerFun() {
var para = 0
fun = function innerFun(){//这里不申请,让他提前到全局定义
para++
}
return fun
}
- 将函数包裹在对象或数组中返回
//对象方式
function outerFun() {
var para = 0
var obj = {
innerFun:function(){
para++
}
}
return obj
}
let fun = outerFun()['innerFun']
fun()
fun()
//数组方式
function outerFun() {
var para = 0
var fun = function innerFun(){para++}
var array = [fun]
return array
}
let fun = outerFun()[0]
fun()
fun()
3.外部函数返回多个内部方法
- 内部方法共用外部方法的变量
- 内部方法不共用外部方法的变量,互不干扰
栗子
1 强行把函数赋值给全局
function fun() { //外部函数
var i = 999; //fun的作用域变量,
//第一个内部函数
add = function () {
i++
};
//第二个内部函数
return function () {
console.log(i)
}
}
var fun1 = fun();
fun1(); //999
add(); //999+1
fun1(); //1000
2 多次调用方法有多个独立作用域
function outerFunc() { //外部函数
var i = 0; //作用域变量
return function () { //内部函数
i++;
console.log(i);
}
}
var outer1 = outerFunc();
var outer2 = outerFunc();
outer1(); //1
outer2(); //1
outer1(); //2
outer2(); //2
3 for 中定义的var
function fun() {
arr = []; //这里没有加 var声明,会提升到全局
for (var i = 0 ; i < 3; i++) { //循环结束时候,i = 3,i定义在fun 内部的变量
arr[i] = function () { //这里没有加 var声明,会提升到全局
console.log(i);
}
}
}
fun();
arr[0](); //3
arr[1](); //3
arr[2](); //3
4 settimeout
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i)
}, 200);
}
会默认输出 5个5, 原因\
- var i 是定义在全局,所以会被全局污染
- setTimeout的回调方法需要等 for全局执行完,所以在回调前,i已经都是5
方法1 使用自执行函数IFFE
for (var i = 0; i < 5; i++) {
(function (i) {
setTimeout(function () {
console.log(i)
}, 200);
})(i)
}
方法2 var 改成 let
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i)
}, 200);
}
js没有实际的块作用域概念,底层实现还是上面的IFFE实现。只是IFFE会自动把 for的i参数当实际参数传入到函数里。
5 科里化
要求定义函数add,实现:alert (add (1)(2)(3))
函数柯里化:可以连续给一个函数,反复传的参数,还能累计到函数內。
var add = function (x1) {
var sum = x1;
var tmp = function (x2) {
sum = sum + x2;
return tmp;
}
tmp.toString = function () {
return sum;
}
return tmp;
}
alert(add(1)(2)(3));
6 onclick事件
五个完全一样的按钮,点击那个弹出自己是第几个
方法1
var container = document.getElementsByTagName("body")[0]
for (let i = 0; i < 5; i++) {
var box = document.createElement("button")
box.innerHTML = `${i+1}`
let btnClickFunc = function (i) {
return function () {
alert(`当前点击第${i+1}`)
}
}(i)
box.onclick = btnClickFunc
container.append(box)
}
方法2 把自执行函数放在外部先执行
var container = document.getElementsByTagName("body")[0]
for (let i = 0; i < 5; i++) {
var box = document.createElement("button")
box.innerHTML = `${i+1}`
;(function (i) {
box.onclick = function () {
alert(`当前点击第${i+1}`)
}
})(i)
container.append(box)
}
7 动态生成4*4表格
动态生成4*4表格,每个表格中有坐标() - (3,3) 点击格增加次数,且 每个格互补干扰,次数通过弹窗提示
<!DOCTYPE html>
<html>
<head>
<style>
#container{
display: flex;
flex-wrap: wrap;
}
#container > div{
width: 20vw;
height: 100px;
background-color: burlywood;
margin: 5px;
}
</style>
</head>
<body>
<div id="container">
</div>
<script src="test3.js"></script>
</body>
</html>
方案1 一维数组+ onclick + 闭包
通过flexbox布局+ document.createElement动态创建div + 闭包btnClickFunc方法实现点击每个格子的数据隔离。
var container = document.getElementById("container")
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
var box = document.createElement("div")
let btnClickFunc = function(i,j) {
var sum = 0
return function () {
sum++
console.log(`第${i}---${j}格格,点击${sum}下`)
}
}(i,j)
box.onclick = btnClickFunc
container.append(box)
}
}
方案2 二维数组+ onclick + 闭包
var container = document.getElementById("container")
var array = [
[0,0,0,0],
[0,0,0,0],
[0,0,0,0],
[0,0,0,0]
]
for (let i = 0; i < array.length; i++) {
for (let j = 0; j < array[i].length; j++) {
var box = document.createElement("div")
box.innerHTML= `${i}--${j}`
let btnClickFunc = function(i,j) {
return function () {
array[i][j]++
console.log(`第${i}---${j}格格,点击${array[i][j]}下`)
}
}(i,j)
box.onclick = btnClickFunc
container.append(box)
}
}
方案2 优化
把全局变量array变为私有变量,通过IFFE自执行函数实现
由于array 被有内部函数btnClickFunc 引用着,所以不会被销毁
var container = document.getElementById("container")
;(function () {
var array = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
]
for (let i = 0; i < array.length; i++) {
for (let j = 0; j < array[i].length; j++) {
var box = document.createElement("div")
box.innerHTML = `${i}--${j}`
let btnClickFunc = function (i, j) {
return function () {
array[i][j]++
console.log(`第${i}---${j}格格,点击${array[i][j]}下`)
}
}(i, j)
box.onclick = btnClickFunc
container.append(box)
}
}
})()
8 对象中的闭包
var name="window";
var p = {
name:"Perter",
getName:function(){ //这里相当于外层函数
var self = this; //外层函数的上下文被保留下来
return function(){//这里相当于内层函数
return self.name;
}
}
}
var getName = p.getName()
var _name=getName()
console.log(_name)//输出 Perter
形成闭包
9 对象中的必包2
function fun(n,o) {
console.log(o)
return {
fun:function(m) {
return fun(m,n)
}
}
}
var a = fun(0)//undefined
a.fun(1)//0
a.fun(2)//0
a.fun(3) //0
var b = fun(0)//undefined
.fun(1)//0
.fun(2)//1
.fun(3)//2
var c = fun(0).fun(1);//undefined //0
c.fun(2) //1
c.fun(3) //1
10 对象中的闭包3
var a = 2
var obj = {
a:4,
fn1:(function () {
// console.log("this.a",this.a)
this.a*=2 //匿名函数自调用 this 指向window
var a = 3 //这里的a 是闭包
return function(){
// console.log("--this.a",this.a)
this.a*=2 //这里根据实际调用的对象,如果是 fun() 则是全局,obj.fun() 则是调用的对象
a*=3 //这里访问的是闭包的a
console.log(a)
}
})()
}
var fn2 = obj.fn1;
console.log(a)
fn2() //this 指向window ,执行的是闭包返回的函数
obj.fn1() //由于是obj.调用,所以this 指向obj ,执行的是闭包返回的函数
console.log(a)
console.log(obj.a)
// 输出结果
// 4
// 9
// 27
// 8
// 8
最终的结果图