闭包
闭包就是指有权访问另一个函数作用域中的变量的函数。
在后台执行环境中,闭包的作用域链包含着它自己的作用域,包含函数的作用域和全局作用域。
作用:暴露局部变量。
JS 中的闭包是什么?
- 闭包的表现形式
//形式一
function(){
local++
console.log(local)
}
//形式二
function foo(){
var local = 1
function bar(){
local++
returnlocal
}
return bar
}
var fnc = foo()
fnc()
示例1引入闭包
var fnArr = [];
for(var i = 0; i < 2; i++){
fnArr[i] = function(){
return i;
}
//以上代码的函数其实并没有执行,一直在赋值而已。for循环遍历之后
//才调用函数,此时,i已经等于2,所以调用函数输出的是全局变量2.
//数组的每一项是一个函数
//fnArr[1]是一个函数
}
console.log(fnArr[1]()); //输出结果为2
console.log(fnArr[0]()); //输出结果为2
console.log(fnArr[2]()); //报错,fnArr[2] is not a function
捕获.PNG
以上代码进行改装,使其输出的就是对应的
i
值。
//方法1:
var fnArr = [];
for(var i = 0; i < 2; i++){
(function(i){
fnArr[i] = function(){
return i;
}
})(i) // 立即执行函数
}
// 代码改写
var fnArr = [];
function fn1(i){
fnArr[i] = function fn11(){
return i
}
}
function fn2(){
fnArr[i] = function fn22(){
return i
}
}
fn1(0)
fn2(1)
利用作用域链理解一下代码:
- 当执行一个函数,就会初始化一个活动对象。
- 当在执行上下文A创建函数fn,fn的作用域链就指向A的活动对象。
globalContext = {
AO: {
fnArr: [fn11,fn22];
fn1: function;
fn2: function;
}
}
fn1.[[scope]] = globalContext.AO
fn2.[[scope]] = globalContext.AO
fn1Context = {
AO: {
i: 0
fn11: function;
}
scope: fn1.[[scope]]
}
fn11.[[scope]] = fn1Context.AO
fn2Context = {
AO: {
i: 1
fn22: function;
}
scope: fn2.[[scope]]
}
fn22.[[scope]] = fn1Context.AO
fn11Context = {
AO: {
}
scope: fn11.[[scope]]
}
fn22Context = {
AO: {
}
scope: fn22.[[scope]]
}
示例2引入闭包
function fn(){
var s = 1
function sum(){
++s
console.log(s)
}
return sum
}
// 以上代码可以改写成
function(){
var s = 1
return function(){
++s
console.log(s)
}
}
var mySum = fn()
mySum() // 2
mySum() //重新调用的话,就会初始化一个新的sumContext 3
mySum() // 4
var mySum2 = fn()
mySum2() // 2
mySum2() // 3
利用作用域链解释:
- 再一次重新调用函数所产生的执行上下文与之前的没有任何关系。
globalContext = {
AO: {
fn: function
mySum: sum
mySum2: undefined
}
}
fn.[[scope]] = globalContext.AO
fnContext = {
AO: {
s: 2
sum: function
}
scope: fn.[[scope]]
}
sum[[scope]] = fnContext.AO
sumContext = {
AO: {}
scope:sum.[[scope]]
}
//调用函数会初始化一个新的执行上下文,与前一个没有任何关系
fn-Context = {
AO: {
s: 1
sum: function
}
scope: fn.[[scope]]
}
sum.[[scope]] = fn-Context.AO
变量(内存)的生命周期
- 默认作用域消失时,内存就被回收。
- 全局变量的生命周期:
var a = 1;
// 浏览器一行行执行代码,当执行到这一行a的值 1 就出现在内存中了
// window窗口关闭(关闭页面),a就消失
// 刷新页面的时候,之前的a也消失不见了,执行代码会出现新的a
生命周期不会超过页面的生命周期
- 局部变量的生命周期:
function f1(){
var a = 1;
return undefined;//所有的函数不写return,默认return undefined。
}
// 函数f1被调用的时候,a才存在(出生)。此时,a不存在
f1();
//浏览器调用f1,当执行到a所在的一行,a才会出现在内存中。a的取值是1.
//当a所在的环境作用域不在的时候,a就不存在了。当函数执行return之后,跳出函数的执行环境,a就不存在了。
f1();
//再一次调用f1,则产生了新的a,与原来的a没有任何关系
- 如果变量被引用着,则不能回收。
function f1(){
var a = {name: 'a'};
var b = 2;
window.xxx = a; //这不是覆盖
}
f1();
// 函数执行完b就死了
// 函数执行完,a还存在。
console.log(a)
//此时,a可以被引用,a的值为1,window死了,a才会死。
//也可以认为,变量名死了,但真正的内存还存在
window.xxx = {name:'b'}
//此时a已经没有被引用了,就消失了
var作用域
- 就近原则
在当前的作用域找是否有同名的,没有则在父级作用域找
var a
function f1(){
var a //a的值为1,只看父级作用域,而且就近不是指的代码近。
function f2(){
a = 1
}
function f3(){
var a
}
}
函数的作用域的就近原则:
function f2(){}
function f1(){
functon f2(){
}
f2() //指的是f1当中的f2
}
- 词法作用域
只要看层级关系,分析语句的词法,就可以确定a到底是谁,不需要执行代码
var a //2
function f1(){
var a
function f2(){
var a
f3()
a = 1
}
}
function f3(){
a = 2 //指的是第一个a,和f3执行与否没有任何关系
}
- 同名的不同变量
function f1(){
var a
function f2(){
var a
a = 1
}
}
//f2中的a与f1中的a只是名字相同,但是没有任何的关系
立即执行函数
- 如果想得到一个独立的作用域,必须声明一个函数
目的:不产生全局变量
function f1(){
var a
a = 1
console.log(a)
}
- 如果想运行函数内的代码,必须执行函数
function f1(){
var a
a = 1
console.log(a)
}
f1()//f1()此时还是一个全局变量
- 为了避免产生全局变量
function(){
var a
a = 1
console.log(a)
}()
//声明了一个匿名函数,没有全局变量。
//此时的函数浏览器运行时会报错,语法错误。
- 函数写法改进(立即执行函数),避免语法报错
!function(){
var a
a = 1
console.log(a)
}()
- 立即执行函数传参
第一种情况
var a = 2
!function(a){
a = 1 //形参声明的a
//形参是给第一个参数赋值
//这里面的a是一个新的作用域,与外面的a没有任何关系
console.log(a) //1
}(/*没有传参*/)
console.log(a) //2
第二种情况
var a = 100
!function(a){ //此处的a与调用传入的参数a没有任何关系,只是名字相同
console.log(a) //这个a是上面离他最近的a
}(a) //此处的a为全局作用域下的a,取值为100
变量提升
- 浏览器在执行代码之前,会把所有的声明提升到作用域的顶部。
function a(){}
var a = 100
console.log(a) // 100
//变量提升
var a
function(){}
a = 100
console.log(a) //100
var a = 100
function a(){}
console.log(a) // 100
//变量提升
var a
function(){}
a = 100
console.log(a) //100
var a = 100
var a = function(){}
function a(){}
console.log(a)
//变量提升
var a
var a
function(){}
a = 100
a = function(){}
console.log(a) // function(){}
function f1(){
a = 1
var a
}
f1()
//相当于
function f1(){
var a
a = 1
}
f1()
var a = 100
f1()
function f1(){
var b = 2
if(b === 1){
var a //a=99指的是此处的a
}
a = 99
}
//声明提升
var a
function f1(){
var b
var a
b = 2
if(b === 1){
}
a = 99
}
a = 100
f1()
- 提升变量很重要!(一定要动手提升变量。)
时机(异步)
- 先写的代码后执行,后写的代码先执行。
button.onclick = function f(){
console.log(1)//发生点击事件的时候才会执行代码
}
console.log(2)//代码一定会执行
//先console.log(2),之后在console.log(1).
//当用户点击按钮时,浏览器会执行函数f()
//button.onclick(event),浏览器手动的执行这句话
//button.onclick.call(target,event)
内存泄露
内存泄露的例子
var fn = function(){
var a = {
name: 'a'
}
var b = {
name: 'b'
}
return function(){
return a
}
}()
console.log(fn())
//b如果没有被回收,就是内存泄露,谁也不能访问b了,IE会出现内存泄露
如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素无法被销毁。
function assign(){
var element = document.getElementById('some')
element.onclick = function(){
alert(element.id)
}
}
由于匿名函数保存了一个对于assign()的活动对象的引用,因此就会导致无法减少element对的引用数,只要匿名函数存在,element的引用数至少也是1,因此它所占用的内存就永远不会被回收。
function assign(){
var element = document.getElementById('some')
var id = element.id
element.onclick = function(){
alert(id)
}
element = null
}
在上面的代码中,通过把element.id的一个副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用,但是此时还不能解决内存泄露的问题。闭包会引用包含函数的整个活动对象,而其中包含着element。即使闭包不直接引用element,包含函数的活动对象中仍会保存一个引用。因此,有必要把element变量设置成null,这样就可以消除对DOM对象的引用,顺利的减少其引用数,确保正常回收其占用的内存。
面试题
闭包是造成问题的原因,立即执行函数是解决问题的方法。
var items = document.querySelectorAll('li')
for(var i = 0; i < items.length; i++){
items[i].onclick = function(){
console.log(i)
}
}
//运行结果:无论i为多少,console.log(i)的值都为6.
//变量提升
var items
var i
for(i = 0; i < items.length; i++){
//i == 0,1,2,3,4,5
items[i].onclick = function(){
console.log(i) //C
}
}
// i == 6
console.log(i) //D 6
//D一定会执行,C在D后面执行
i打印出值都为6.PNG
解决办法:
方法一:
var items
var i
for(i = 0; i < items.length; i++){
!function(i){
items[i].onclick = function(){
console.log(i)
}
}(i)
}
方法二:
var items
var i
items = document.querySelectorAll('li')
for(i = 0; i < items.length; i++){
items[i].onclick = function(i){
return function(){
console.log(i)
}
}(i)
//自执行函数是有返回值的
//循环的时候把i的值赋值给了新的i,新的i是不会i++.
//console.log(i)的执行是很后面的,当执行的时候,i已经为6了。
}
网友评论