JS作用域、执行上下文、递归与闭包
目录
作用域
作用域指一个变量的作用范围。它是静态的(相对于上下文对象), 在编写代码时就确定了。
作用:隔离变量,不同作用域下同名变量不会有冲突。
全局作用域
直接编写在script标签中的JS代码,都在全局作用域。
在全局作用域中:
-
在全局作用域中有一个全局对象window,它代表的是一个浏览器的窗口,它由浏览器创建我们可以直接使用。
-
创建的变量都会作为window对象的属性保存。
<script>
var a = 10;
</script>
-
创建的函数都会作为window对象的方法保存。
<script>
function say(){
}
</script>
全局作用域中的变量都是全局变量,在页面的任意的部分都可以访问到。
函数作用域
调用函数时创建函数作用域,函数执行完毕以后,函数作用域销毁。每调用一次函数就会创建一个新的函数作用域,他们之间是互相独立的。
在函数作用域中可以访问到全局作用域的变量,在全局作用域中无法访问到函数作用域的变量。简单讲就是里面可以访问外面,但是外面不能访问里面。
在函数中要访问全局变量可以使用window对象。
<script>
var a = 10;
function say(){
console.log(window.a);
}
</script>
提醒1:
在函数作用域也有声明提前的特性:
-
使用var关键字声明的变量,会在函数中所有的代码执行之前被声明
-
函数声明也会在函数中所有的代码执行之前执行
因此,在函数中,没有var声明的变量都会成为全局变量,而且并不会提前声明。
<script>
var a = 10;//全局
function say(){
var a = 5;//局部变量
b = 20;//全局
console.log(window.a);
}
</script>
提醒2:定义形参就相当于在函数作用域中声明了变量。
<script>
var a = 10;//全局
function say(b){
//b为局部
}
</script>
执行上下文
函数执行上下文
在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象(虚拟的, 存在于栈中)。
(1)对局部数据进行预处理:
形参变量==>赋值(实参)==>添加为执行上下文的属性
arguments==>赋值(实参列表), 添加为执行上下文的属性
var定义的局部变量==>undefined, 添加为执行上下文的属性
function声明的函数 ==>赋值(fun), 添加为执行上下文的方法
this==>赋值(调用函数的对象)
(2)开始执行函数体代码
执行上下文栈
1.在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象
2.在全局执行上下文(window)确定后, 将其添加到栈中(压栈)
3.在函数执行上下文创建后, 将其添加到栈中(压栈)
4.在当前函数执行完后,将栈顶的对象移除(出栈)
5.当所有的代码执行完后, 栈中只剩下window
作用域与执行上下文的区别
区别1:
全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时
全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建
函数执行上下文是在调用函数时, 函数体代码执行之前创建
区别2:
作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化
执行上下文是动态的, 调用函数时创建, 函数调用结束时就会自动释放
联系:
执行上下文(对象)是从属于所在的作用域
全局上下文环境==>全局作用域
函数上下文环境==>对应的函数使用域
递归
一个函数通过名字调用自身的情况,好处是代码简洁。在树的前序,中序,后序遍历算法中,递归的实现明显要比循环简单得多。
注意:
-
找规律,很容易找到规律的使用递归特别方便
-
出口,必须得有个已知的,没有出口就会一直循环
缺点:
-
函数调用导致的时间和空间消耗大
-
存在重复计算,效率低
-
调用栈可能会溢出
<script>
//递归实现阶乘
function sayhi(num) {
if (num <= 1){
return 1;
}else {
return num * sayhi(num-1);
}
}
console.log(sayhi(5));
</script>
<script>
//递归:一个函数可不可以自己调用自己
function f1() {
console.log("hello");
f1();
}
//递归就是老和尚讲故事,死循环了,必须要给一个结束的条件才有意义
var i = 0;
function f1() {
console.log("hello");
i++;
if (i < 10) {
f1();
}
// }
// f1();
//
//求n个数的累加,我们可以采取递归来替代循环
function getSum(n) {
if (n === 1) {
return 1;
}
return n + getSum(n-1);
}
console.log (getSum(100));
//递归调用比较占用资源,循环次数太多不适合使用
//
//输入一个数,求这个数的各位数字之和
//先取余再相除在取整
function getSum(n) {
//结束条件
if (n < 10) {
return n;
}
return n % 10 + getSum(parseInt(n/10));
}
console.log(getSum(140));
</script>
闭包
有权访问另外一个函数作用域中的变量的函数。当内部函数被保存在外部时,将会生成闭包。
产生闭包的条件
-
1.函数嵌套
-
2.内部函数引用了外部函数的数据(变量/函数)。
缺点:闭包导致原有作用链不释放,导致内存泄露(漏得越多,等价占用的内存越多,剩下的可用内存就少了)。
全局变量和局部变量,该特殊之处在于函数内部可以读取全局变量,但是函数外部是不能读取局部变量的。函数内部声明变量的时候,一定要使用var命令。这就导致一个问题,如果我们需要从外部读取函数内的局部变量的时候,就必须采用一种特殊的方式,在函数内部再定义一个函数。
<script>
function f1(){
var n=22;
function f2(){
alert(n); // 22
}
}
</script>
我们只需要将f2作为返回值,就可以读取f1的内部变量了。
上述f2就是闭包,也就是说闭包是一个可以读取其他函数内部变量的函数;即一个函数内部的函数。
闭包的作用
- 读取函数内部的变量,实现公有变量;
- 可以做缓存,使得变量的值始终保持在内存中;
- 可以实现封装,属性私有化;
- 模块化开发,防止污染全局变量
举例1:
<script>
function f1() {
var a = 1;
function f2() {
a ++;
console.log(a);
}
return f2;
}
var f = f1();
f();//2
f();//3
f();//4
f();//5
</script>
ps:外部函数f1执行完毕后,变量a并没有消失,而是保存在了内存中。
内部采用函数表达式,没有产生闭包的原因如下:
<script>
function f1() {
var re = [];
for (var i=0; i < 10; i++){
re[i] = function () {
console.log(i);
};
}
return re;
}
var f = f1();
for (var j=0; j < 10; j++){
f[j]();
}
// 输出为10个10
</script>
ps:f1执行完了之后外部i值已经是10了,函数内部只是引用 ,只有外部调用的时候才会运行函数,那时候i就已经是10了,而且10个函数都指向同一个AO。
举例2:将所有的数据和功能都封装在一个函数内部(私有的),只向外暴露一个包含n个方法的对象或函数。
<script>
function myModule() {
//私有数据
var msg = 'hello'
//操作私有数据的函数
function doSomething() {
console.log('doSomething() ');
}
function doOtherthing() {
console.log('doOtherthing() '))
}
//通过【对象字面量】的形式进行包裹,向外暴露多个函数
return {
doSomething1: doSomething,
doOtherthing2: doOtherthing
}
}
</script>
使用注意
- 内存消耗大,理性使用
- 不要随便改变父函数内部变量的值
内存泄漏
内存泄漏:占用的内存没有及时释放。内存泄露积累多了就容易导致内存溢出。
常见的内存泄露:
-
1.意外的全局变量
-
2.没有及时清理的计时器或回调函数
-
3.闭包
内存溢出(一种程序运行出现的错误)
内存溢出:当程序运行需要的内存超过了剩余的内存时,就出抛出内存溢出的错误。