在开始之前,先了解几个概念。
每个正在执行的函数都有一个执行环境,记录了函数执行过程中的各项信息。 除了全局执行环境外,其余函数的执行环境都会随着函数的执行而被创建,函数的执行结束而被销毁。 所有的执行环境会存放在执行环境栈中,只有栈顶的执行环境才有执行权。
每个函数都有各自的作用域、作用域链、变量对象、执行环境。 其中,作用域链在函数初始化完成后便存在,而作用域、变量对象、执行环境只有在函数被执行时才创建。 执行结束后,函数的作用域、作用域链、执行环境被销毁;而变量对象仍有可能留在内存中(如果函数内部有闭包,则函数执行结束后变量对象仍然留在内存,直到闭包执行结束,该变量对象才会被销毁)。
先来看如下代码:
var 全局变量 = "柴毛毛";
function 外层函数(){
var 局部变量1 = "大闲人";
return function(){
var 局部变量2 = "是傻逼";
return 全局变量+局部变量1+局部变量2;
};
}
var 函数 = 外层函数();
函数();
上述代码对应的内存模型如下:
当初始化全局执行环境时,会进行如下操作:
当调用“外层函数”时,会进行如下操作:
调用闭包时,会进行如下操作:
当上述代码执行到“return 全局变量+局部变量1+局部变量2;”时,此时执行环境栈的栈顶是闭包的执行环境,因此通过闭包的作用域链寻找这三个变量的值。 查找过程首先从作用域链的顶部开始,首先在闭包变量对象中寻找“全局变量”的值,若没有,则去外层函数的变量对象中查找;若仍未找到,则去全局变量对象中查找,直到找到为止;若在全局变量对象中仍未找到,则查找失败。 若在某一个变量对象中找到该值,则立即停止查找。 PS:查找过程必须从作用域链的头部开始,依次向后查找。
JS没有块级作用域。因此,在if-else、while、switch-case语句中通过var定义的变量都属于当前所在的函数。
严格来说,JS中只有两种作用域:全局作用域 和 函数作用域。 但还有两种特殊的作用域:catch、with。 来看如下代码:
function fn(person) {
with(person){
var personInfo = name+age+location;
}
}
上述代码在函数中使用with语句,with后需要有一个对象,从而在with语句中使用该对象中的属性就不需要通过person.xxx访问,直接访问其属性即可。 并且,JS没有块级作用域,因此在with、catch中创建的变量将属于离它们最近的函数! 那么,这种功能JS是如何实现的呢?
当执行到一个with语句时,会JS会为其创建一个变量对象,这个变量对象中包含with语句后传入的那个对象的全部属性。 紧接着,为with语句创建一个指向该变量对象的作用域,并添加到当前函数/全局作用域链的头部。 当with语句块结束,该变量对象就会被销毁,作用域也会被弹出。 因此,with语句能临时性延长当前函数/全局作用域链的长度,在with语句块中就可以不带前缀访问对象的属性,因为with中传入的对象已经作为一个变量对象被添加到当前作用域链的头部,通过作用域链的查找规则就能找到该变量对象中的属性。
那么with语句块有何用呢? 如果你要大量用到一个对象的属性,重复写person.xxx太繁琐了,这种情况下你可以使用with语句。 但在严格模式下是禁止使用with语句的,只要了解原理就好,平时尽量避免使用。
catch语句块原理一样,请自行脑补吧。
我花了很长时间画这几张图,各位且看且珍惜。