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. 函数体中的预编译过程:

  1. 创建AO对象 (AO Activation Object) -> 函数执行上下文

  2. 找形参和变量声明,将变量和形参名作为 AO 属性名,值为 undefined。

  3. 将实参值和形参统一

  4. 在函数体里面找函数声明,值赋予函数体。

3. 全局中的预编译过程:

  1. 创建GO对象 (Global Object) -> 全局执行上下文 GO === window

  2. 找形参和变量声明,将变量和形参名作为 AO 属性名,值为 undefined。

  3. 在函数体里面找函数声明,值赋予函数体。

// 函数体中的预编译

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
posted @ 2019-07-16 14:22  胤小飞  阅读(96)  评论(0)    收藏  举报