08. JS 作用域
一、window 是全局的域
二、预编译
三、作用域精解
四、闭包(closure)
五、内存溢出和内存泄露
六、练习
作用域
作用域定义:变量(变量作用域又称上下文)和函数生效(能被访问)的区域
一、window 是全局的域
1. imply global 暗示全局变量:即任何变量,如果变量未经声明就赋值,此变量就为全局对象(window)所有。
a = 10; // 没有声明,直接赋值,不报错,可以使用。
---> window.a = 10;
function test(){
var a = b = 123;
}
test();
// console.log(window.b); --> 123 // b 并没有声明直接赋值的,所以是全局变量
// console.log(window.a); --> undefined // a 在函数里面声明的,所以是局部变量。
2. 一切声明的全局变量,全是 window 的属性。
var a = 123;
==> window.a = 123;
二、预编译
1. JS 运行三部曲:
- 语法分析 ==> 计算机通篇扫描一遍,看看是否有语法错误
- 预编译 ==>
- 解析执行 ==> 解释一行执行一行....
函数声明整体提升 --> 提升到逻辑的最前面
变量声明提升 --> 因为只是声明提升了,所以如果在 var 之前调用变量的值只会是 undefined。
2. 函数体中的预编译过程:
-
创建AO对象 (AO Activation Object) -> 函数执行上下文
-
找形参和变量声明,将变量和形参名作为 AO 属性名,值为 undefined。
-
将实参值和形参统一
-
在函数体里面找函数声明,值赋予函数体。
3. 全局中的预编译过程:
-
创建GO对象 (Global Object) -> 全局执行上下文
GO === window -
找形参和变量声明,将变量和形参名作为 AO 属性名,值为 undefined。
-
在函数体里面找函数声明,值赋予函数体。
// 函数体中的预编译
function fn(a){
console.log(a); // function a(){ } ==> 去AO对象里面拿
var a = 123; // AO 对象中的 a 的值变成了 a : 123
console.log(a); // 123
function a(){ } // 预编译看过了直接跳过
console.log(a); // 123
var b = function(){ } // AO 对象中的 b : function(){}
console.log(b); // function(){}
function d(){
}
}
fn(1);
// function a(){}
// 123
// 123
// function(){}
// 预编译发生在函数执行的前一刻
// 1.
AO{ } // 创建AO对象
// 2. 找形参和变量声明,值为 undefined。
AO{
a:undefined,
b:undefined,
}
// 3. 将实参值和形参统一
AO{
a:1, // 将实参中的值传到形参中。
b:undefined,
}
// 4. 在函数体里面找函数声明,值赋予函数体。
AO{
a:function a(){ }, // a 原来有值,但是被覆盖。
b:undefined,
d:function d(){}
}
// GO AO 生成循序
// AO 只有在执行函数的时候才会生成
// 1. 先生成 GO 对象
//GO{
// b: 123
//}
function test(){
var a = b = 123;
}
test();
// 2. 执行函数的时候生成 AO 对象
//AO{
// a: undefined
//}
-
举例:
//GO(){
// a:undefined -> 10,
// c: 234,
//}
function(){
console.log(b); // undefined
if(a){ // 因为 AO 中没有 a,它会往上在 GO 中寻找
var b = 100; // 不会管 if 只要有函数声明就会提升并且赋值 undefined.
}
console.log(b); // GO 中 a = undefined 所以 if 不会执行,b 还是 undefined
c = 234; // 因为没有 var 所以是全局变量,应该在 GO 里面。
console.log(c); // 234 // AO 中没有会去 GO 中寻找。
}
var a;
test();
// AO(){
// b:undefined
// }
a = 10;
console.log(c); // 234
三、作用域精解
[[scope]]:每个 JS 函数都是一个对象,对象中有些属性我们可以访问,但有些不可以,这些属性仅供 JS 引擎存取,[[scope]] 就是其中一个。它指的就是我们所说的作用域,其中存储了运行期上下文的集合。
作用域链:[[scope]] 中所存储的执行期上下文对象的集合,这个集合呈链式链接,我们把这种链式连接叫做作用域链。
[[scope]]里面存的是一个作用域链,作用域链是由多个执行上下文链接形成的。
function test(){
}
test.[[scope]] // 隐式属性作用域,不可调用,仅供JS引擎存取。
运行期上下文:当函数执行时,会创建一个称为执行期上下文的内部对象(AO)。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕,它所产生的执行上下文会被销毁。
查找变量:从作用域链的顶端依次向下查找。在哪个函数中查找变量,就上哪个函数作用域链的顶端依次向下查找,因为最上面始终是你正在执行的 AO,在 AO 中没有找到才会向下面查找。
function a(){
function b(){
var b = 234;
}
var a = 123;
b();
}
var glob = 100;
a();
// a 定义的时候 a.[[scope]] --> 0 : GO{}
// a doing(执行) a.[[scope]] --> 0 : aAO{} // 访问变量就是在执行的作用域链中查找
// 1 : GO{} // 在 a doing 中是看不到 bAO 的,这就是为什么外面的是无法访问里面函数定义的变量的原因。
// b 定义 b.[[scope]] --> 0 : aAO{}
// 1 : GO{}
// b doing b.[[scope]] --> 0 : bAO{}
// 1 : aAO{}
// 2 : GO{}
// 如果再执行一次 b 函数 ,除了自己生成的 AO 是新的以外都是一样的。
// b doing b.[[scope]] --> 0 : newbAO{}
// 1 : aAO{}
// 2 : GO{}
1. a 函数刚刚被定义的时候 a.[[scope]] 里面就存了第 0 位 Global Object(GO),因为它所在的是全局的作用域中。
2. a 函数执行的时候 a.[[scope]] 第 0 位变成了 AO ,第 1 位变成了 GO。
3. b 函数是在 a 的环境下定义的,直接就继承(引用)了 a 函数的作用域链。
4. b 函数执行时,会生成自己的 AO ,并且将它放到了自己作用域链的最顶端,后面的就会自然下移。
5. 执行函数结束后。
-
最后
b函数执行完成后,就会销毁自己执行时临时生成的AO,回归到刚定义的时候。 -
随后
a函数也执行完成了,就会销毁自己执行时临时生成的AO,但是它的AO中有函数b,也会一同销毁,最终也会回归到刚定义的时候。 -
下次再次执行函数
a的时候,就会循环之前的步骤,生成的作用域链和b函数都是全新的。
// 疑问 b函数作用域链 1 位上的 AO 和 a函数作用域链 0 位上的 AO 是同一个吗?
function a(){
function b(){
var b = 234;
a = 0; // 0 位上没有找到变量 a,在 1 位上找到并且赋值 0。
}
var a = 123;
b();
console.log(a); // 0 --> 在 a 函数的作用域链 0 位中找到变量 a 并且输出
四、闭包(closure)
-
嵌套函数,把里面的函数保存到外部就必然会生成闭包。然后在外部执行这个函数,一定能够调用里面那个函数的变量。
-
注意:闭包会导致原有作用域链不释放,造成内存泄露(过多的占用内存,导致系统可用内存变小)。
- 能不用闭包九不用,及时释放。(
f = null; // 让内部函数成为垃圾对象 --> 回收闭包)
- 能不用闭包九不用,及时释放。(
function a(){
function b(){
var bbb = 234;
console.log(aaa);
}
var aaa = 124;
return b;
}
var glob = 100;
var demo = a();
demo();
a 函数中没有 b 函数的执行语句,所以只是被定义。但是在外面有个变量 demo 将 a 函数的结果也就是 return b 保存了,同时也就保存了它的作用域链(AO,GO)。
-
a 函数执行完成后,它就会销毁自身
AO,但是b函数并没有销毁,因为它已经保存到demo中了。 -
然后执行
demo会生成新AO并且放到b函数保存过来的作用域链的最顶端。 -
输出
aaa因为bAO中没有就往下在aAO中查找并输出。
// demo doing b.[[scope]] --> 0 : bAO{}
// 1 : aAO{}
// 2 : GO{}
-
* 闭包不是非要用`return` ,只要能让里面函数保存到外部就可以。
var demo;
function test(){
var abc = 100;
function a(){
console.log(abc);
}
demo = a; // 将里面函数保存给全局变量保存起来,这也是闭包。
}
test();
demo(); // 100
常见的闭包使用方法:
-
将一个函数作为另一个函数的返回值
-
将函数作为实参传递给另一个函数调用。
闭包1:将一个函数作为另一个函数的返回值
function fn1() {
var a = 2
function fn2() {
a++
console.log(a)
}
return fn2
}
var f = fn1(); //执行外部函数fn1,返回的是内部函数fn2
f() // 3 //执行fn2
f() // 4 //再次执行fn2
闭包2. 将函数作为实参传递给另一个函数调用
function showDelay(msg, time) {
setTimeout(function() { //这个function是闭包,因为是嵌套的子函数,而且引用了外部函数的变量msg
alert(msg)
}, time)
}
showDelay('atguigu', 2000)
闭包的作用:
1. 实现公有变量(变量的值始终保持在内存中)
- eg:函数累加器(不依赖外部变量,并且能够反复执行)
function add(){
var count = 0;
function demo(){
count ++;
console.log(count);
}
return demo;
}
var counter = add();
counter(); // 1
counter(); // 2
counter(); // 3
counter(); // 4
.......
2. 可以做缓存(存储结构)
- eg:eater
// 多个函数同时和一个函数形成闭包,它们之间的变量可以共用(不包括执行时自身AO中产生的变量)
function test(){
var food = "apple"; // 就相当于储存到了一个仓库,下次想要的时候就去那个仓库拿。
var obj = {
eatFood : function(){
if(food != ""){
console.log("I am eating " + food);
food = "";
}else{
console.log("There is nothing!");
}
},
pushFood : function(myFood){
food = myFood;
}
}
return obj;
}
var person = test(); // obj 对象中的两个函数保存的都是同一个 testAO。
person.eatFood();
person.eatFood();
person.pushFood("banana");
person.eatFood();
// I am eating apple
// There is nothing!
// I am eating banana
3. 可以实现封装,属性私有化。
- 构造函数内定义局部变量和特权函数,其实例只能通过特权函数访问此变量
function Person(name){
var _name = name; // 私有属性
this.getName = function(){ //操作私有属性的函数
return _name;
}
}
var person = new Person('Joe');
- 这种方式的优点是实现了私有属性的隐藏,Person 的实例并不能直接访问 _name 属性,只能通过特权函数 getName 获取:
console.log(person._name); // undefined
console.log(person.getName()); //'Joe'
- 缺陷:私有变量和特权函数只能在构造函数中创建。通常来讲,构造函数的功能只负责创建新对象,方法应该共享于prototype上。特权函数本质上是存在于每个实例中的,而不是prototype上,增加了资源占用。
function Person(name,age,sex){
var a = 0; // 外部是访问不到的,
this.name = name;
this.age = age;
this.sex = sex;
function sss(){
a++ ;
console.log(a);
}
this.say = sss; // 形成闭包 ,将变量 a 弄成私有的,调用 say 方法就可以使用 a 变量。
}
var oPerson = new Person();
oPerson.say(); // 1
oPerson.say(); // 2
var oPerson1 = new Person();
oPerson1.say(); // 1
4. 模块化开发,防止污染全局变量。
// 用闭包形成私有化
var name = 'bcd';
var init = (function(){ // 形成闭包,变量变成私有,不会跟全局变量冲突。
// init 入口函数,初始化。
var name = 'abc';
function callName(){
console.log(name);
}
return function(){
callName();
}
}())
var initZheng = (function(){
var name = 123;
function callName(){
console.log(name);
}
return function(){
callName(); // 启用前面的函数
}
}())
init(); // abc
initZheng(); // 123
// 各自私有化,不冲突。
// 以后如果有重复使用的功能,可以把功能提取到一个闭包里面,使用的时候复制过来使用就可以。
Example :
function a(){
var num = 100;
function b(){
num ++;
console.log(num);
}
return b;
}
var demo = a(); // 将函数 b 连同它的作用域链保存到 demo(闭包)。
demo(); // 101 // 执行 b 函数生成新的 bAO 但是里面没有变量 num,就去 aAO 中寻找,发现并累加输出,结束后销毁 bAO。
demo(); // 102 // 再次执行 b 函数,又会生成新 bAO,里面还是没有变量 num,在 aAO 中找到,这时候 aAO 中的 num 因为刚才的累加已经是 101 了,再进行累加就是 102.
Example:非常重要,解决闭包带来的麻烦。
// 问题
function test(){
var arr = []; // 保存进来的是 10 组函数体。
for(var i = 0; i < 10; i ++){
arr[i] = function(){ // 这条函数体在这个for循环里是没有执行的,是什么都无关紧要。
// 只是定义了函数并且将 testAO和GO 放进了自己的 [[scope]] 中。
document.write(i + " ");
}
} // for 循环结束后变量 i = 10 并存到了 testAO 中。
return arr;
}
var myArr = test();
for(var j = 0; j < 10; j ++){
myArr[j]();
}
// 10 10 10 10 10 ....
----------------------------------------------------
// 解决
function test(){
var arr = [];
for(var i = 0; i < 10; i ++){
(function(j){ // 在这生成了变量 j 并且和实参 i 绑定了。
arr[j] = function(){ // 这个时候定义并且保存到 arr[j] 里面的函数所拥有的 scope 中不再是 testAO和GO,而是对应的立即执行函数AO(里面有变量j)和GO。
document.write(j + " ");
}
}(i));
}
return arr;
}
var myArr = test();
for(var j = 0; j < 10; j ++){
myArr[j](); // 执行数组中保存的函数的时候,每次都会用到立即执行函数AO中的 j 变量。
}
// 0 1 2 3 4 5 6 7 8 9
五、内存溢出和内存泄露
内存溢出:一种程序运行出现的错误。当程序运行需要的内存超过了剩余的内存时, 就出抛出内存溢出的错误。
var obj = {};
for (var i = 0; i < 10000; i++) {
obj[i] = new Array(10000000); //把所有的数组内容都放到obj里保存,导致obj占用了很大的内存空间
console.log("-----");
}
内存泄漏:占用的内存没有及时释放。
-
注意:内存泄露的次数积累多了,就容易导致内存溢出。
-
常见的内存泄露:
-
意外的全局变量
-
没有及时清理的计时器或回调函数
-
闭包
-
情况1举例:
// 意外的全局变量
function fn() {
a = new Array(10000000);
console.log(a);
}
fn();
情况2举例:
// 没有及时清理的计时器或回调函数
var intervalId = setInterval(function () { //启动循环定时器后不清理
console.log('----')
}, 1000)
// clearInterval(intervalId); //清理定时器
情况3举例:
function fn1() {
var a = 4;
function fn2() {
console.log(++a)
}
return fn2
}
var f = fn1()
f()
// f = null //让内部函数成为垃圾对象-->回收闭包
六、练习
1.
var x = 1;
y = z = 0;
function add(n){
return n = n + 1;
}
y = add(x); // 预编译后执行的
function add(n){ // 函数声明提升的时候已经将前面的覆盖了。
return n = n + 3;
}
z = add(x);
console.log(x); // 1
console.log(y); // 4
console.log(z); // 4

浙公网安备 33010602011771号