This time I will bring you a detailed explanation of scope and closure. What are the precautions for using scope and closure? The following is a practical case, let's take a look.
There are two main situations of execution context:
Global code: In a section of <script>
tag , there is a global execution context. What is done is: variable definition, function declaration
Function code: There is a context in each function. What is done is: variable definition, function declaration, this, arguments
PS: Pay attention to the difference between "function declaration" and "function expression".
Determine window as the global execution context before executing global code.
(1) Preprocess global data: (no value assignment)
Global variables defined by var ==>undefined, added as properties of window
function declares a global function ==> assignment (fun), and adds it as a window method
this==> assignment (window )
(2) Start executing the global code
Before calling the function and preparing to execute the function body, create the corresponding function execution context object (virtual, existing on the stack middle).
(1) Preprocess local data:
Formal parameter variable==>Assignment (actual parameter)==>Add as an attribute of the execution context
arguments==>Assignment (actual parameter list), add local variables defined as properties
var of the execution context==> ;undefined, add a function declared as an attribute of the execution context
The parser will pass an implicit parameter into the function every time it calls the function. This implicit parameter is this.
Depending on how the function is called, this will point to different objects: [Important] 1. When called as a function, this will always be window . For example,window.fun();
function Foo(name) { //this = {}; this.name = name; //return this; } var foo = new Foo();
var obj = { name: 'A', printName: function () { console.log(this.name); } } obj.printName();
function fn() { console.log(this); //this === window } fn();
作用:隔离变量,不同作用域下同名变量不会有冲突。
作用域的分类:
全局作用域
没有块作用域(ES6有了)
if (true) { var name = 'smyhvae';}console.log(name);
上方代码中,并不会报错,因为:虽然 name 是在块里面定义的,但是 name 是全局变量。
直接编写在script标签中的JS代码,都在全局作用域。
在全局作用域中:
在全局作用域中有一个全局对象window,它代表的是一个浏览器的窗口,它由浏览器创建我们可以直接使用。
创建的变量都会作为window对象的属性保存。
创建的函数都会作为window对象的方法保存。
全局作用域中的变量都是全局变量,在页面的任意的部分都可以访问到。
变量的声明提前:
使用var关键字声明的变量( 比如 var a = 1
),会在所有的代码执行之前被声明(但是不会赋值),但是如果声明变量时不是用var关键字(比如直接写a = 1
),则变量不会被声明提前。
举例1:
console.log(a); var a = 123;
打印结果:undefined
举例2:
console.log(a); a = 123; //此时a相当于window.a
程序会报错:
函数的声明提前:
使用函数声明
的形式创建的函数function foo(){}
,会被声明提前。
也就是说,它会在所有的代码执行之前就被创建,所以我们可以在函数声明之前,调用函数。
使用函数表达式
创建的函数var foo = function(){}
,不会被声明提前,所以不能在声明前调用。
很好理解,因为此时foo被声明了,且为undefined,并没有给其赋值function(){}
。
所以说,下面的例子,会报错:
调用函数时创建函数作用域,函数执行完毕以后,函数作用域销毁。
每调用一次函数就会创建一个新的函数作用域,他们之间是互相独立的。
在函数作用域中可以访问到全局作用域的变量,在全局作用域中无法访问到函数作用域的变量。
在函数中要访问全局变量可以使用window对象。(比如说,全局作用域和函数作用域都定义了变量a,如果想访问全局变量,可以使用window.a
)
提醒1:
在函数作用域也有声明提前的特性:
使用var关键字声明的变量,会在函数中所有的代码执行之前被声明
函数声明也会在函数中所有的代码执行之前执行
因此,在函数中,没有var声明的变量都会成为全局变量,而且并不会提前声明。
举例1:
var a = 1; function foo() { console.log(a); a = 2; // 此处的a相当于window.a } foo(); console.log(a); //打印结果是2
上方代码中,foo()的打印结果是1
。如果去掉第一行代码,打印结果是Uncaught ReferenceError: a is not defined
提醒2:定义形参就相当于在函数作用域中声明了变量。
function fun6(e) { console.log(e); } fun6(); //打印结果为 undefined fun6(123);//打印结果为123
区别1:
全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时
全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建
函数执行上下文是在调用函数时, 函数体代码执行之前创建
区别2:
作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化
执行上下文是动态的, 调用函数时创建, 函数调用结束时就会自动释放
联系:
执行上下文(对象)是从属于所在的作用域
全局上下文环境==>全局作用域
函数上下文环境==>对应的函数使用域
当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用(就近原则)。如果没有则向上一级作用域中寻找,直到找到全局作用域;如果全局作用域中依然没有找到,则会报错ReferenceError。
外部函数定义的变量可以被内部函数所使用,反之则不行。
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Title</title> <script> //只要是函数就可以创造作用域 //函数中又可以再创建函数 //函数内部的作用域可以访问函数外部的作用域 //如果有多个函数嵌套,那么就会构成一个链式访问结构,这就是作用域链 //f1--->全局 function f1(){ //f2--->f1--->全局 function f2(){ //f3---->f2--->f1--->全局 function f3(){ } //f4--->f2--->f1---->全局 function f4(){ } } //f5--->f1---->全局 function f5(){ } } </script></head><body></body></html>
理解:
多个上下级关系的作用域形成的链, 它的方向是从下向上的(从内到外)
查找变量时就是沿着作用域链来查找的
查找一个变量的查找规则:
var a = 1 function fn1() { var b = 2 function fn2() { var c = 3 console.log(c) console.log(b) console.log(a) console.log(d) } fn2() } fn1()
在当前作用域下的执行上下文中查找对应的属性, 如果有直接返回, 否则进入2
在上一级作用域的执行上下文中查找对应的属性, 如果有直接返回, 否则进入3
再次执行2的相同操作, 直到全局作用域, 如果还找不到就抛出找不到的异常
闭包就是能够读取其他函数内部数据(变量/函数)的函数。
只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。
上面这两句话,是阮一峰的文章里的,你不一定能理解,来看下面的讲解和举例。
当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量或函数时, 就产生了闭包。
使用chrome调试查看
理解一: 闭包是嵌套的内部函数(绝大部分人)
理解二: 包含被引用变量 or 函数的对象(极少数人)
注意: 闭包存在于嵌套的内部函数中。
1.函数嵌套
2.内部函数引用了外部函数的数据(变量/函数)。
来看看条件2:
function fn1() { function fn2() { } return fn2; } fn1();
上面的代码不会产生闭包,因为内部函数fn2并没有引用外部函数fn1的变量。
PS:还有一个条件是外部函数被调用,内部函数被声明。比如:
function fn1() { var a = 2 var b = 'abc' function fn2() { //fn2内部函数被提前声明,就会产生闭包(不用调用内部函数) console.log(a) } } fn1(); function fn3() { var a = 3 var fun4 = function () { //fun4采用的是“函数表达式”创建的函数,此时内部函数的声明并没有提前 console.log(a) } } fn3();
将一个函数作为另一个函数的返回值
将函数作为实参传递给另一个函数调用。
function fn1() { var a = 2 function fn2() { a++ console.log(a) } return fn2 } var f = fn1(); //执行外部函数fn1,返回的是内部函数fn2 f() // 3 //执行fn2 f() // 4 //再次执行fn2
当f()第二次执行的时候,a加1了,也就说明了:闭包里的数据没有消失,而是保存在了内存中。如果没有闭包,代码执行完倒数第三行后,变量a就消失了。
上面的代码中,虽然调用了内部函数两次,但是,闭包对象只创建了一个。
也就是说,要看闭包对象创建了一个,就看:外部函数执行了几次(与内部函数执行几次无关)。
function showDelay(msg, time) { setTimeout(function() { //这个function是闭包,因为是嵌套的子函数,而且引用了外部函数的变量msg alert(msg) }, time) } showDelay('atguigu', 2000)
上面的代码中,闭包是里面的funciton,因为它是嵌套的子函数,而且引用了外部函数的变量msg。
作用1. 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)
作用2. 让函数外部可以操作(读写)到函数内部的数据(变量/函数)
我们让然拿这段代码来分析:
function fn1() { var a = 2 function fn2() { a++ console.log(a) } return fn2 } var f = fn1(); //执行外部函数fn1,返回的是内部函数fn2 f() // 3 //执行fn2 f() // 4 //再次执行fn2
作用1分析:
上方代码中,外部函数fn1执行完毕后,变量a并没有立即消失,而是保存在内存当中。
作用2分析:
函数fn1中的变量a,是在fn1这个函数作用域内,因此外部无法访问。但是通过闭包,外部就可以操作到变量a。
达到的效果是:外界看不到变量a,但可以操作a。
比如上面达到的效果是:我看不到变量a,但是每次执行函数后,让a加1。当然,如果我真想看到a,我可以在fn2中将a返回即可。
回答几个问题:
问题1. 函数执行完后, 函数内部声明的局部变量是否还存在?
答案:一般是不存在, 存在于闭中的变量才可能存在。
闭包能够一直存在的根本原因是f
,因为f
接收了fn1()
,这个是闭包,闭包里有a。注意,此时,fn2并不存在了,但是里面的对象(即闭包)依然存在,因为用f
接收了。
问题2. 在函数外部能直接访问函数内部的局部变量吗?
不能,但我们可以通过闭包让外部操作它。
产生: 嵌套内部函数fn2被声明时就产生了(不是在调用)
死亡: 嵌套的内部函数成为垃圾对象时。(比如f = null,就可以让f成为垃圾对象。意思是,此时f不再引用闭包这个对象了)
将所有的数据和功能都封装在一个函数内部(私有的),只向外暴露一个包含n个方法的对象或函数。
模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能。
(1)myModule.js:(定义一个模块,向外暴露多个函数,供外界调用)
function myModule() { //私有数据 var msg = 'Smyhvae Haha' //操作私有数据的函数 function doSomething() { console.log('doSomething() ' + msg.toUpperCase()); //字符串大写 } function doOtherthing() { console.log('doOtherthing() ' + msg.toLowerCase()) //字符串小写 } //通过【对象字面量】的形式进行包裹,向外暴露多个函数 return { doSomething1: doSomething, doOtherthing2: doOtherthing }}
上方代码中,外界可以通过doSomething1和doOtherthing2来操作里面的数据,但不让外界看到。
(2)index.html:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>05_闭包的应用_自定义JS模块</title></head><body><!--闭包的应用 : 定义JS模块 * 具有特定功能的js文件 * 将所有的数据和功能都封装在一个函数内部(私有的) * 【重要】只向外暴露一个包含n个方法的对象或函数 * 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能--><script type="text/javascript" src="myModule.js"></script><script type="text/javascript"> var module = myModule(); module.doSomething1(); module.doOtherthing2();</script></body></html>
同样是实现方式一种的功能,这里我们采取另外一种方式。
(1)myModule2.js:(是一个立即执行的匿名函数)
(function () { //私有数据 var msg = 'Smyhvae Haha' //操作私有数据的函数 function doSomething() { console.log('doSomething() ' + msg.toUpperCase()) } function doOtherthing() { console.log('doOtherthing() ' + msg.toLowerCase()) } //外部函数是即使运行的匿名函数,我们可以把两个方法直接传给window对象 window.myModule = { doSomething1: doSomething, doOtherthing2: doOtherthing }})()
(2)index.html:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>05_闭包的应用_自定义JS模块2</title></head><body><!--闭包的应用2 : 定义JS模块 * 具有特定功能的js文件 * 将所有的数据和功能都封装在一个函数内部(私有的) * 只向外暴露一个包信n个方法的对象或函数 * 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能--><!--引入myModule文件--><script type="text/javascript" src="myModule2.js"></script><script type="text/javascript"> myModule.doSomething1() myModule.doOtherthing2()</script></body></html>
上方两个文件中,我们在myModule2.js
里直接把两个方法直接传递给window对象了。于是,在index.html中引入这个js文件后,会立即执行里面的匿名函数。在index.html中把myModule直接拿来用即可。
总结:
当然,方式一和方式二对比后,我们更建议采用方式二,因为很方便。
但无论如何,两种方式都采用了闭包。
缺点:函数执行完后, 函数内的局部变量没有释放,占用内存时间会变长,容易造成内存泄露。
解决:能不用闭包就不用,及时释放。比如:
f = null; // 让内部函数成为垃圾对象 -->回收闭包
总而言之,你需要它,就是优点;你不需要它,就成了缺点。
内存泄漏:占用的内存没有及时释放。内存泄露积累多了就容易导致内存溢出。
常见的内存泄露:
1.意外的全局变量
2.没有及时清理的计时器或回调函数
3.闭包
情况1举例:
// 意外的全局变量 function fn() { a = new Array(10000000); console.log(a); } fn();
情况2举例:
// 没有及时清理的计时器或回调函数 var intervalId = setInterval(function () { //启动循环定时器后不清理 console.log('----') }, 1000) // clearInterval(intervalId); //清理定时器
情况3举例:
<script type="text/javascript"> function fn1() { var arr = new Array[100000]; //这个数组占用了很大的内存空间 function fn2() { console.log(arr.length) } return fn2 } var f = fn1() f() f = null //让内部函数成为垃圾对象-->回收闭包</script>
内存溢出:当程序运行需要的内存超过了剩余的内存时,就出抛出内存溢出的错误。
//内存溢出 var obj = {} for (var i = 0; i < 10000; i++) { obj[i] = new Array(10000000); //把所有的数组内容都放到obj里保存,导致obj占用了很大的内存空间 console.log('-----') }
相信看了本文案例你已经掌握了方法,更多精彩请关注php中文网其它相关文章!
推荐阅读:
The above is the detailed content of Detailed explanation of scope and closure. For more information, please follow other related articles on the PHP Chinese website!