一、作用域、作用域链
作用域(scope)
浅显的理解: 作用域就是变量的可用范围
(scope)
为什么要有作用域: 目的是防止不同范围的变量之间互相干扰
js中包含2级作用域:全局作用域 & 函数作用域。
-
全局作用域
不属于任何函数的外部范围称为全局作用域,保存在全局的作用域的变量称为全局变量。
优点:可反复使用;缺点:会造成全局污染。
-
函数作用域
一个函数内部的范围称为函数作用域,保存在函数作用域的变量为局部变量。
【注】函数形参变量也是函数内的局部变量。
优点:不会污染;缺点:无法反复使用。
-
作用域的形成
只有函数的
{}
可以形成作用域,其他情况的{}
不能形成作用域。比如对象的大括号不是作用域,对象的属性不是局部变量。【注】es6的块级作用域是实现其实是匿名函数自调用形成的,实际上也是函数作用域的原理。
作用域链(scopes / scope chain)
js规定,一个函数,及能用自己的作用域,又能用外层的作用域变量。所以需要一个访问变量的"路线图"。
每个函数在定义时,就已经规划好了自己专属的查找变量的”线路图“,蹭我作用域链。
【注】给从未声明的变量赋值,不会报错,但会生成全局变量。(非严格模式)
总结: Js中只有两种局部变量: 1. 函数内var出来的 2. 函数的形参变量 看不见var,形参里也没有, 就不是局部变量。
var a=10;
function fun(){
var a=100;
a++;
console.log(a);
}
fun(); // 101
console.log(a); // 10
// 形参题目---------
var a=10;
function fun(a){
a++;
console.log(a);
}
fun(a); //11
console.log(a); //10
作用域的本质
js中的作用域和作用域链都是对象结构:
在fun函数还未调用时,fun的作用域链:
进入fun函数时,作用域链增加一个local作用域
作用域本质
|
函数作用域释放
|
---|
总结: 函数作用域 其实是js引擎在调用函数时才临时创 建的一个作用域对象。其中保存函数 的局部变量。而函数调用完,函数作 用域对象就释放了。
所以,JS中函数作用域对象,还有个别名 ——”活动的对象(Actived Object)” 简称, AO。 所以,局部变量不可重用
二、闭包
闭包closure:能够读取其他函数内部变量的函数。
既重用变量又保护变量不被污染的一种编程方法。
只要希望给一个函数,保存一个即可反复使用,又不会被 外界污染的专属局部变量时,就用闭包。
使用闭包3步:
- 用 外层函数包裹 要保护的变量和使用变量的内层函数
- 外层函数 返回 内层函数
- 调用外层函数,用变量接住返回的内层函数
//第1步: 用外层函数包裹要保护的变量和内层函数
function mother(){
var total=1000;
//第2步: 返回内层函数对象
return function (money){
total-=money
console.log(`花了${money}还剩${total}元`)
}
}
//第3步: 调用外层函数,用变量接住内层函数对象
var pay=mother();
//pay接住的就是mother()返回出来的内层函数对象
pay(100);//应该剩900
total=0;//别人程序执行了代码
pay(100);//应该剩800
可以看出pay函数的作用域中有个闭包来自mother其中保存了total变量。
一句话概括:
闭包如何形成: 外层函数调用后, 外层函数的作用域对象, 被返回的内层函数的作用域链引用着, 无法释放, 就形成了闭包对象
闭包缺点:容易造成内存泄漏。解决办法:及时释放不用的闭包:将保存内存函数的变量赋值为null。
题目:闭包的应用
- 柯里化
// 要求定义函数add 实现add(1)(2)(4)得到1+2+4的结果。
// 函数柯里化:可以给一个函数反复传参,传的参数还可以累计到函数中
var add = function(x1){
var sum = x1;
var fun = function(x2) {
sum+=x2;
return fun;
// 因为柯里化要求可以连续传参,所以需要return出这个累加函数,确保可以继续传参时继续调用。如果直接返回sum,那么只传两个参数,该过程就结束了。
}
fun.toString = function() {
// toString方法在隐式转字符串时调用,可以返回fun中函数闭包中的sum变量的值
return sum
}
fun.valueOf = function() {
// valueOf方法在隐式转number时调用,可以通过该方法返回sum值
return sum
}
return fun; // 为了可以传入第二个参数,add()第一次调用时,需要返回函数,才能继续第二次的传参调用
}
alert(add(2)(3)(4)(1))
console.log(0+add(1)(2)(3)(3))
- 二维游戏:2048/消消乐类型
<head>
<style>
.container{
width: 200px;
height: 200px;
border-radius: 6px;
background: rgb(230, 203, 87);
}
.container div{
cursor: pointer;
width: 40px; height: 40px;
background: #fff;
float: left;
margin-left: 8px;
margin-top: 8px;
border-radius: 4px;
text-align: center;
line-height: 40px;
}
</style>
</head>
<body>
<div id="container" class="container"></div>
<script>
// 4*4个div,每个表格中都有其坐标:(0,0) - (3,3),点击格子弹出该格子的点击次数,每个格子互不干扰(闭包)
var div = document.getElementById('container');
// // 方案1:直接给每个格子添加click事件,用一个外层函数自调用包裹,定义一个自己的变量n记录点击次数,然后return一个实际的累加操作函数
// // 缺点:每个格子的n都是独立的,对于二维游戏来说,格子之间需要计算,比如2048格子之间的数值累加,此方法无法实现
// for(var r =0;r<4;r++) { // 行循环生成
// for(var c = 0;c<4;c++) { // 列循环生成
// var cell = document.createElement('div')
// cell.innerText = `${r},${c}`
// div.appendChild(cell);
// // 添加点击事件
// cell.onclick = (function() {
// var n = 0;
// return function() {
// n++;
// alert(`点击了${n}次`)
// }
// })()
// }
// }
// // 方案2:使用二维数组来记录每个格子的值,每个按钮只保存其行号和列号(坐标),当点击时,通过按钮自己保存的行号和列号来找到二维数组中自己对应的值
// // 缺点:会存在全局变量arr,容易被篡改
// var arr = [
// [0,0,0,0],
// [0,0,0,0],
// [0,0,0,0],
// [0,0,0,0]
// ]
// for(var r =0;r<4;r++) { // 行循环生成
// for(var c = 0;c<4;c++) { // 列循环生成
// var cell = document.createElement('div')
// cell.innerText = `${r},${c}`
// div.appendChild(cell);
// // 只保存自己的行号列号
// cell.onclick = (function(r,c) {
// return function() {
// arr[r][c]++
// alert(`点击了${arr[r][c]}次`)
// }
// })(r, c)
// }
// }
// 方案3:使用方案2+整个逻辑的匿名函数自调用包裹,使arr也为局部变量不会被污染
(function(){
var arr = [
[0,0,0,0],
[0,0,0,0],
[0,0,0,0],
[0,0,0,0]
]
for(var r =0;r<4;r++) { // 行循环生成
for(var c = 0;c<4;c++) { // 列循环生成
var cell = document.createElement('div')
cell.innerText = `${r},${c}`
div.appendChild(cell);
// 只保存自己的行号列号
cell.onclick = (function(r,c) {
return function() {
arr[r][c]++
alert(`点击了${arr[r][c]}次`)
}
})(r, c)
}
}
})()
</script>
</body>