作用域和闭包
作用域
全局作用域
一个页面就是一个完整的执行环境,里面就存在唯一的一个作用域,就是全局作用域,
全局作用域的本质是全局对象对的属性,在浏览器中全局对象是 window,
在全局环境使用function
或var
声明变量或函数相当于为全局对象添加属性或方法
var a = 2
console.log(a === window.a) //true 直接添加到window对象下成了其属性
函数作用域
除了全局作用域,还有函数作用域。
里面的所有函数只有在函数作用域内才可以调用,
外部不能访问内部函数作用域的变量。
let a2 = "这是foo外"
function foo() {
let a1 = "这是foo内"
console.log(a1, a2)
}
foo() //"这是foo内" "这是foo外"
console.log(a1, a2) //"报错" "这是foo外"
在函数内部可以同时访问 a1,a2
但是在函数外部只能访问 a2,a1 不能访问。
若函数参数实参同名,相当于声明一个同名的局部变量并在执行时赋值:
let num = 2
function foo(num) {
num += 2
}
foo(4)
console.log(num) //2,没有发生变化
//-----------------------------------------
function foo1() {
num += 2
}
foo1(4)
console.log(num) //4,发生变化
不同作用域的同名变量不会影响。
声明时上下文
此时有一个全局作用域,里面有 2 个函数作用域如下:
在任何地方都可以访问全局作用域下的变量。
var a = "a是全局变量"
function foo1() {
var a = "foo1里面的a"
console.log(a)
}
function foo2() {
var a = "foo2里面的a"
foo1()
}
foo2()
函数的作用域是在函数声明时确定的。在调用时依然是回访声明时的上下文。
es6 块作用域
除了函数可以生成一个函数作用域之外,
代码块 for(){}/while{}的小括号和花括号也可以生成一个块级作用域
{
let a = 1
console.log(a) //1
}
console.log(a) //a is not defined
变量泄漏
function
和var
是 es5 遗留的声明方式,他们会无视块作用域
而如果声明不用关键字,则直接写相当于给 window 添加属性或方法
块作用域最常用的地方见本章末尾。
IIFE
IIFE(立即执行的匿名函数表达式)
函数申明的时候要么是立即执行要么是写上函数名,在合适的位置调用。
//需要将匿名函数用()包裹
;
(function() {
console.log("ok")
})() //一般写法
//还有别的写法
; + (function() {
console.log(1)
})() //void和算数运算符也可
;
(function() {
console.log(1)
})() //()包裹范围变大
//别的函数
void(() => {
console.log("ok")
})() //箭头函数需要括号
void(function main() {
console.log("ok")
})() //具名函数直接加名字
void(async function main() {
console.log(await ajax())
})() //异步函数直接加async
我们之前讲函数的时候讲到过函数加括号表示立即执行。
但是为了不引起歧义需要将函数整体括起来,此时这个函数中形成一个单独的立执行匿名函数作用域,函数名和函数里面的值都不可以访问,可以说整个立执行的区域形成了一个安全区。
比如我们整体将 JS 代码写成一个立执行函数,那么你的 JS 代码就是安全的不可修改的。
垃圾回收机制
JavaScript 是具有自动垃圾收集机制的,无需手动管理:
标记清除(mark-and-sweep)
JavaScript 最常用的是标记清除:当变量进入环境(作用域),则将变量标记为进入环境,当变量离开环境的时候,将其标记为离开环境,内存被回收。
引用计数(reference counting)
已经不会被变量引用计数为 0,内存被回收。
如果一个值的引用出现闭环的话,这个值的引用不会变为 0,循环引用使得值的内存永远得不到回收。意味着永远占用内存。
所以:对于不需要的值,或者使用完的值,请手动将变量指向清除。
var obj = document.getElementById("box")
obj.style.color = "blue"
obj = null //清空指向
引用类型将在后面讲到。
闭包
内部函数能访问外部函数的作用域:
function foo(c) {
var num = c
return function A() {
num++
return num
}
}
foo(5)() //6
foo(5)() //6
foo(5)() //6
var b = foo(5) //b保留function A声明时的上下文
b() //6
b() //7
b() //8
var c = foo(5) //c保留另一份function A声明时的上下文
c() //6
c() //7
c() //8
变量 b 保留下来了一个函数 A 可以访问自己在声明时的上下文(foo 的作用域)里面的变量 num 并对他进行改变。
上面就是一个简单的累加器。
函数可以保留自己的作用域并可以访问它的作用域,就产生了闭包。
闭包的影响: 会使得原有的作用域不消失,导致内存泄漏(可分配的内存的资源减少)。
现代计算机内存足够大,浏览器优化足够好,这个情况几乎不可能发生的。
闭包的作用
封装,私有化属性
function create() {
var Gin = {
money: 100,
}
console.log("你的钱是:" + Gin.money)
return {
add: function() {
Gin.money += 10
console.log("你的钱是:" + Gin.money)
},
lost: function() {
Gin.money -= 5
console.log("你的钱是:" + Gin.money)
},
get: function() {
return Gin.money
},
}
}
var yinshi = create() //"你的钱是:100"
yinshi.add() //"你的钱是:110"
yinshi.lost() //"你的钱是:105"
var yinshi2 = create()
yinshi2.get() //100
在这里 Gin 这个对象完全变成了一个私有对象,里面的值已经无法直接获取,可以通过已经定义好的自定义的方法来访问它。
JavaScript 的闭包不一定要 return。也可以直接为定义更外部的作用域(如 window)的属性。
还有最常见的循环绑定的问题:
function foo() {
var arr = []
for (var i = 0; i < 10; i++) {
arr[i] = function() {
console.log(i)
}
}
return arr
}
var mylist = foo() //mylist是一个存了10个函数的数组
//此时mylist数组里的每个函数打印出来都会是10
function foo() {
var arr = []
for (var i = 0; i < 10; i++) {
arr[i] = (function(j) {
return function() {
console.log(j)
}
})(i)
}
return arr
}
var mylist = foo()
//此时mylist数组里的每个函数打印出来就是对应的i的值
特别的,利用 let 具有不穿透块作用域的性质,会被 for 的花括号限制:
function foo() {
var arr = []
for (let i = 0; i < 10; i++) {
arr[i] = function() {
console.log(i)
}
}
return arr
}
var mylist = foo() //mylist是一个存了10个函数的数组
//无需闭包,此时mylist数组里的每个函数打印出来就是对应的i的值
在 for 循环中使用定时器(等其他异步代码)的时候和这个上述的循环绑定事件类似,
为了达到预期效果需要使用闭包或者在 for 循环中使用let
。