函数闭包作用域的理解和习题

393 阅读8分钟

函数闭包作用域的理解和习题

基础知识点

1.png

1.创建函数

  • 在堆内存中开辟一块空间,有16进制地址
  • 存储一些内容到空间中
  • 作用域[[scope]] 在哪一个上下文中创建作用域就是谁
  • 函数体代码用字符串存储起来
  • 函数也是对象,也会存储键值对
    • 例如
    • name 键
    • length 变量长度
  • 把堆内存地址关联给对应的变量

image-20210429163128518.png

函数执行

  • 产生一个"全新"的"私有上下文" EC(?),上下文中有一个AO(?)私有变量对象,用来存储当前上下文中声明的私有变量[AO 是 VO 的分支]

  • 初始化作用域链 SCOPE-CHAIN

    ​ <自己当前上下文,函数作用域{上级上下文}>

  • 初始化THIS

  • 初始化ARGUMENTS

  • 形参赋值 ->形参也是私有变量,存储在AO中

  • 变量提升 ->私有上下文中声明的变量耶是私有变量,存储在AO中

  • 代码执行

image-20210429164155561.png

作用域链

函数中代码执行的时候,如果遇到一个变量,我们首先看是否为自己私有上下文中的变量(AO)

+ 是私有的:接下来对变量所做的操作,和外界都没有直接关系[保护]
+ 如果变量不是私有的:则向上级上下文中查找,如果再没有,则继续向上级上下文中查找...直到找到EC(G)全局上下文为止

如果全局(包含GO)下也没有

+ 如果是获取变量值, 报错: xxx is not defined
+ 如果是设置变量值, 则相当给GO中设置对应的属性

函数闭包作用域习题

var x = [12, 23];
function fn(y) {
    y[0] = 100;
    y = [100];
    y[1] = 200;
    console.log(y);
}
fn(x);
console.log(x); 

/*
    [100, 200]
    [100, 23]
*/
let x = 5;
function fn(x) {
    return function (y) {
        console.log(y + (++x));
    }
}
let f = fn(6);
f(7);
fn(8)(9);
f(10);
console.log(x);

/*
    14
    18
    18
    5
*/
let a = 0,
    b = 0;
function A(a) {
    A = function (b) {
        alert(a + b++);
    };
    alert(a++);
}
A(1);
A(2); 

/*
	1
	4
*/
var arr = [11,22];
function f(ary) {
    console.log(ary);
    ary[0] = 100;
    ary = [666];
    ary[0] = 0;
    console.log(ary);
}
f(arr);
console.log(arr)

/*
	[11,22]
	[0]
	[100,22]
*/
var a = 1;
var b = 1;
function f() {
    console.log(a, b)
    var a = b = 2;
    console.log(a, b)
}
f();
console.log(a, b)

/*
	undefined 1
	2 2
	1 2
*/

/*
	分析: 1.注意var a的变量提升
		  2.如果在GO中也没有定义变量,则变量定义在GO中
*/
var a = 1;
var b = 1;
function f() {
    var a;
    console.log(a, b)
    a = b = 2;
    console.log(a, b)
}
f();
console.log(a, b)
var i=0;
function A() {
    var i=10;
    function x() {
        console.log(i);
    }
    return x;
}
var y = A();
y();
function B() {
    var i=20;
    y();
}
B();

/*
	10
	10
*/
/*
	1.全部的function fn(){} 都会变量提升
	2.函数执行
	3.函数的重复定义会覆盖之前的定义
*/
fn();
function fn() { console.log(1); }
fn();
function fn() { console.log(2); }
fn();
var fn = function () { console.log(3); }
fn();
function fn() { console.log(4); }
fn();
function fn() { console.log(5); }
fn();

/*
	5
    5
    5
    3
    3
    3
*/
var b = 0; 
function fn(a){
    console.log(a,b);
    var a = b= 2;	
}
fn(1)

/*
	1 0 
*/
//相当于
var b = 0; 
function fn(a){
    var a;
    console.log(a,b);	//1,0
   	a = b= 2;	
}
fn(1)   
console.log(a, b, c);   //=>undefined undefined undefined var a = b= 1;这样写之变量提升一个a 但是用逗号就会都变量提升
var a = 12,
    b = 13,
    c = 14;
function fn(a) {
        console.log(a, b, c);	//=>10 13 14
        a = 100;
        c = 200;
        console.log(a, b, c);	//=>100 13 200
}
b = fn(10);
console.log(a, b, c); //=>12 undefined 200
var a = 1;
function fn(a) {
    console.log(a)	//=> ƒ a() { }
    var a = 2;
    function a() { }
}
fn(a);
/*
	分析:
		1.变量提升  var a; 
				  function fn(a){}
		2.代码执行
			2.1 函数中的变量提升
				  var a;	//只有声明
				  function fn(a){}	// 声明加定义
*/

函数是个大渣男

/*
	记忆法:1.除了在函数和对象的{}花括号中,变量只声明不定义
		   2.如果在大括号中let/const/function会创建一个块级私有上下文,var创建的是全局变量
		   函数是个大渣男即和全局处对象又和块级作用域搞暧昧	
	关键点:1.全局的初始化声明(只声明不定义)
		   2.块级作用域的变量声明加变量定义(var 声明 ,function声明加定义)
		   3.代码执行 碰到 function foo() {}就把之前执行过的结果返回给全局下的函数变量
		   4.之后的操作都是对块级作用域的操作
*/
{
    function foo() {}
    foo = 1;
    console.log(foo)	//=> 1
}
console.log(foo);  // => 函数

/*
	执行流程
		1.变量提升
			+ 在除了函数和对象的{}花括号中,变量只声明不定义所以全				局下有个foo 值为undefined
		2.代码执行
			+ 在块级作用域中进行变量提升(声明加定义)
			+ 碰到function foo() {}把之前的操作返回给全局的foo
				(之前执行了变量声明加定义 所以全局的foo的个函数而不是undefined)
			+ 之后foo = 1是对块级作用域的操作所以	console.log(foo) //=> 1
			+ 全局下console.log(foo);  // => 函数		
*/

{
    function foo() { }
    foo = 1;
    function foo() { }
}
console.log(foo);	//=> 1

/*------*/
{
    function foo() {}
    foo = 1;
    function foo() {}
    foo = 2;
}
console.log(foo);	//=>1

image-20210504143839353.png

es6形参赋值的坑

/*
	条件:
		@1 使用了形参赋值默认值无论赋值任何类型的值 & 无论是否传递实参,默认值是否生效了]
		@2 在函数体中,基于let/var/const声明过变量[function声明的不算 & 声明的变量也无需和形参是一样 & let/const是允许重复声明,所以变量不能和形参一致]如果上面两个条件都符合,那么函数会产生两个私有上下文:
			+ 函数私有上下文
			+ 块级私有上下文
		并且块级私有上下文的“上级上下文”,就是函数私有上下文
		如果块级上下文中某个私有变量和函数私有上下文中的某个变量名字相同了,则在块中“代码执行”之前,首先会把函数上下文中的这个变量值,同步给块级上下文中的这个变量
*/
var x = 1;
function func(x, y = function () { x = 2 }) {
    x = 3;
    y();
    console.log(x); //=>2
}
func(5);
console.log(x);	//=>1

/*----*/
var x = 1;
function func(x, y = function(){x = 2}){
    var x = 3; 
    y();
    console.log(x);	//=>3
}
func(5);
console.log(x);		//=>1

/*----*/
var x = 1;
function func(x, y = function () { x = 2 }) {
    var x;
    var y = function () { x = 4 };
    y();
    console.log(x);		//=>4
}
func(5);
console.log(x);			//=>1

1-2.png

函数形参和arguments 之间的映射

/*
	1.非严格模式下形参和arguments会一对一产生映射
	2.严格模式下会取消映射
*/

var a = 4;
function b(x, y, a) {
    console.log(a);		//=> 3
    arguments[2] = 10;	
    console.log(a);		//=> 10
}
a = b(1, 2, 3);
console.log(a);			//=>undefined

/*--严格模式下--*/
"use strict"
var a = 4;
function b(x, y, a) {
    console.log(a);		//=> 3
    arguments[2] = 10;	
    console.log(a);		//=> 3
}
a = b(1, 2, 3);
console.log(a);			//=>undefined
/*
	分析
		1.形参i赋值2 返回 function(){}
		2.test接受的是function () {
        	alert(i *= 2);
   		 }
   		3.test(5)执行所接受的函数 找不到i向上级作用域查找
*/
var test = (function (i) {
    return function () {
        alert(i *= 2);
    }
})(2);
test(5);	//=>4
function fun(n, o) {
    console.log(o);
    return {
        fun: function (m) {
            return fun(m, n);	
        	/*
        		fun 接受 function(m){} 返回值为fun(m, n)的结
        		果
        	*/	
        }
    };
}
var c = fun(0).fun(1);	
c.fun(2);
c.fun(3);

/*
	undefined
	0
	1
	1
*/

快速执行函数具名化

/*
	特殊 1.立即执行函数中的变量不能被外部访问
		2.对立即执行函数进行修改无效
		3.快立即行函数具名化的权重十分低,碰到var可以修改变量
*/
var b = 10;
(function b() {
    b = 20;
    console.log(b);//=>函数;	对b的修改无效
})();
console.log(b);	//=>10;

/*--权重--*/
var b = 10;
(function b() {
    var b = 20;
    console.log(b);//=>20
})();
console.log(b);	//=>10;

闭包在函数中的应用

闭包应用之:循环中的闭包处理方案

  • 循环事件绑定
    • 自定义属性 data-index
      • 闭包中的N种方法 (LET处理机制)
      • 事件委托
  • 循环中的定时器
    • 闭包的处理方案
    • 定时器本身处理方案

循环事件绑定用var的问题

image-20210506100557638.png

/*
	代码的问题:无论点击哪个控件都是输出3
	分析: 1.使用了var变量 i是定义在全局中的
		  2.循环结束后全局的i会变成3
		  3.btns[i].addEventListener("click",function(){
        	console.log(i)};只是绑定方法到控件,触发控件的时候调用绑定的函数这时候i找的还是全局的i = 3
    })
*/
let btns = Array.from(document.querySelectorAll(".btn"));
for(var i = 0;i<btns.length;i++){
    btns[i].addEventListener("click",function(){
        console.log(i)
    })
}

解决方法一闭包

闭包解决方案,利用闭包的“保存”机制

/*
	每一轮循环的时候,都创建一个闭包(不释放的上下文),闭包中存储自己的私有变量i,并且值是每一轮循环的索引;当点击按钮,执行对应的函数,遇到一个变量i,不要再去全局找了,而是让其去所属的闭包中查找即可...
*/
let btns = Array.from(document.querySelectorAll(".btn"));
for(var i = 0;i<btns.length;i++){
    (function(i){
        btns[i].addEventListener("click",function(){
        console.log(i)
    })})(i)
}

/*----*/
let btns = Array.from(document.querySelectorAll(".btn"));
for(var i = 0;i<btns.length;i++){
    //下一行的i变成K就不产生闭包  因为当前上下文没有东西被外部占用
    btns[i].addEventListener("click",(function(i){
        return function (){
            console.log(i)
        }
    })(i))
}

/*--forEach底层还是闭包--*/
let btns = Array.from(document.querySelectorAll(".btn"));
btns.forEach((item,i) => {
    btns[i].addEventListener("click",function(){
        console.log(i)
    })
});

image-20210506103923329.png

/*
	let底层还是闭包
	1.首先会生成一个父私有作用域来控制循环中的5个子私有作用域
*/
let btns = Array.from(document.querySelectorAll(".btn"));
for(let i = 0;i<btns.length;i++){
    btns[i].addEventListener("click",function(){
        console.log(i)
    })
}

解决方案二:自定义属性

let btnList = document.querySelectorAll('.btn');
let i = 0;
for (; i < btnList.length; i++) {
    // 最开始每轮循环的时候,给每一个按钮对象都设定一个自定义属性myIndex,存储它的索引
    btnList[i].myIndex = i;
    btnList[i].onclick = function () {
        // 每一次点击的时候,基于THIS(当前操作元素)获取之前存放的自定义属性值
        console.log(`当前点击按钮的索引:${this.myIndex}`);
    };
}
// 性能比闭包要好一些,但是也有一些性能消耗{元素对象 & 节点集合 & 绑定的方法 都是开辟的堆内存} */

解决方案三:终极方案 事件委托

解决方案三:终极方案  事件委托
// 点击每一个按钮,除了触发按钮的点击事件行为,根据冒泡传播机制,也会把body的点击事件行为触发
document.body.onclick = function (ev) {
    let target = ev.target;
    if (target.tagName === 'BUTTON' && target.className === "btn") {
        // 点击的事件源是按钮
        let index = target.getAttribute('data-index');
        console.log(`当前点击按钮的索引:${index}`);
    }
};

定时器问题

能否实现每间隔1秒输出 0 1 2?

/*
	答: 可以每隔一秒输出 但是不能输出0,1,2
	原因:定时器是宏任务 输出的时候全局i变成了3
*/
for (var i = 0; i < 3; i++) {
    setTimeout(function () {
        console.log(i);	//=>3 3 3 
    }, i * 1000); //等待时间 0ms(5~7ms)  1000ms  2000ms
}

解决方法

/*	@1   
	let 底层也是闭包 首先会生成一个父私有作用域来控制循环中的3个子私有作用域
*/
for (let i = 0; i < 3; i++) {
	setTimeout(function () {
   		 console.log(i);	//=>0 1 2
	}, i * 1000);
} 

/*
	@2 立即执行函数产生私有作用域
		之后i就不用去全局作用域查找
*/
var i = 0;
for (; i < 3; i++) {
    (function (i) {
        setTimeout(function () {
            console.log(i);
        }, i * 1000);
    })(i);
}

/*
	@3 和方法二差不多 也是闭包
*/
const fn = i => {
    return function () {
        console.log(i);
    };
};
let i = 0;
for (; i < 3; i++) {
    setTimeout(fn(i), i * 1000);
}

/*
	@4
*/
let i = 0;
for (; i < 3; i++) {
    // 设置定时器:
    //   参数1:回调函数,到时间执行的方案
    //   参数2:等待时间
    //   参数3:给回调函数“预先传递”的实参值{底层本质也是闭包 柯理化函数思想}
    setTimeout(function (i) {
        console.log(i);
    }, i * 1000, i);
}