一、作用域

(一)作用域(JS基础中也有部分讲解)

  1. 定义:详见JS基础
  2. 分类:全局作用域和局部作用域
  3. 代码演示:
    function test() {
    let a = 1;      // 此处的a为局部变量
    console.log(a);   // 1
    }
    console.log(a);     // 报错提示a未定义,a is not defined

(二)局部作用域

1、局部作用域——函数作用域

(使用varletconst,都有函数作用域的说法)

function test() {
    let a = 1;      // 此处的a为局部变量
    console.log(a);   // 未调用函数不打印1,若调用函数则会打印1
}
console.log(a);     // 报错提示a未定义,a is not defined

2、局部作用域——块作用域

块作用域是ES6的东西,只有letconst声明的变量才有块作用域
块:一对大括号,比如:{}if(){}for(){}

(1)大括号:{}

{
    // 这里的大括号就形成了一个块级作用域
    // 这里声明的变量的有效范围只在这个大括号中
    let a = 1;
    console.log(a);     // 1
}
console.log(a);       // a is not defined

(2)if语句:if(){}

if(true){
    let a = 1;
    console.log(a);     // 1
}
console.log(a);       // a is not defined

(3)for语句:for(){}

for(let i = 1;i <= 3;i++){
    // 此处定义的i为局部变量,只能在for循环语句中使用
    console.log(i);     // 1,2,3
}
console.log(a);       // a is not defined

3、对比函数作用域与块作用域

for(var i = 1;i < 3;i++){
    console.log(i);     // 1,2
}
console.log(i);       // 3
for(;i < 6;i++){
    console.log(i);     // 3,4,5
}

(二)全局作用域

  1. 位置:在<script></script>标签之中的变量 ,或者直接在js文件中(没有放入任何大括号中,即最外层)的变量
  2. 全局变量:在全局作用域中声明的变量可以在其他任何作用域中访问
  3. 注意:
    1. 为window对象动态添加的属性默认也是全局的,不推荐!
      window.a = 1;
    2. 函数中未使用任何关键字声明的变量为全局变量,非常不推荐!!!
      a = 1;
    3. 尽可能减少全局变量的声明,防止全局变量的污染

(三)作用域链

  1. 本质:底层的变量查找机制

    解释:在函数被执行时,会优先查找当前函数作用域中查找变量,如果当前作用域查找不到则会一次主机查找父级作用域直到全局作用域

  2. 总结:

    1. 嵌套关系的作用域串联起来形成了作用域链
    2. 相同作用域链中按着从小到大的规则查找变量
    3. 字作用域能够访问父作用域,父作用域无法访问子作用域

(四)JS垃圾回收机制

1、垃圾回收机制的基本概念

  1. 定义:垃圾回收机制(Garbage Collection),简称GC。JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收。如果不了解JS的内存管理机制。非常容易导致内存泄漏(内存无法被回收)的情况,也就是不在用到的内存没有及时被释放,这就叫内存泄漏。
  2. 举例:死循环  ➡  内存不断被占用导致程序卡死

2、内存的生命周期

注意:

  1. 全局变量一般不会被回收(但是关闭页面的话会被回收)
  2. 一般情况下局部变量的值不用了就会被自动回收

(1)内存分配

当我们声明变量、函数、对象的时候,系统会自动为其分配内存

// 为变量分配内存
let a = 11;
let str = 'String';
// 为对象分配内存
let person = {
    age:18,
    uname:'Ricardo'
}
// 为函数分配内存
function sum(a,b){
    return a+b;
}

(2)内存使用

即读写内存,也就是使用变量、函数等

... // 上接(1)中的代码

// 使用变量
console.log(a);
console.log(str);

// 函数调用,临时为函数再分配一小块内存,用于运行函数中的局部变量
sum(1,2);

(3)内存回收

使用完毕,由垃圾回收自动回收不再使用的内存

... // 上接(2)中的代码

// 到这一行,函数执行完毕,函数中的局部变量就会被释放掉(把占用的内存回收了)
sum(1,2);

3、垃圾回收算法

  • 说明:所谓垃圾回收,核心思想就是如何判断内存是否已经不会被使用了,如果是,则视为垃圾释放掉

(1)引数计数法

注意:(低版本IE浏览器采用的方法),定义“内存不再被使用”的标准为:一个对象是否有指向它的引用
缺陷:嵌套引用,即如果两个对象相互引用,尽管他们已不再使用,垃圾回收器也不会进行回收,导致内存泄漏

  • 算法解释:

    1. 跟踪每个值被引用的次数
    2. 如果这个值被引用了一次那么就记录次数1
    3. 多次引用会累加
    4. 如果减少一个引用就减1
    5. 如果引用次数是0则释放内存
  • 举例1:

    代码解释:缺陷:嵌套引用,即如果两个对象相互引用,尽管他们已不再使用,垃圾回收器也不会进行回收,导致内存泄漏

// 举例1
// 声明变量,代码运行的时候就会分配内存,存储对象的内容
// 同时,有一个变量只想找个对象,所以找个对象的引用次数为1
let person = {
  age : 18,
  name : 'Ricardo'
};

// 下面的代码意思是让p也指向这个对象,所以这个对象的引用次数为2 
let p = person;

// 下面的代码意思是让person不再指向对象,则该对象的引用次数减1,引用次数为1
person = null;

// 下面的代码意思是让p不在指向对象,则该对象的引用次数减1,引用次数为0
p = null;
// 到这里,对象引用次数为0,则垃圾回收期会自动回收对象所占用的内存
  • 举例2:

    代码解释:缺陷:因为引用次数永远也不会是0,这样的相互引用如果说大量存在的话就会导致大量的内存泄漏

function fn(){
  let o1 = {};
  let o2 = {};
  // o1、o2的属性相互引用,导致回收内存时,两个属性的引用次数都是1,导致无法回收,从而导致内存泄漏
  o1.a = o2;
  o2.a = o1;
  return '引用技术无法回收';
};
fn();

(2)标记清除法

现代浏览器(大多数浏览器与新版本的IE浏览器)通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的

- 算法解释
    1. 标记清除算法将“不再使用的对象”定义为“无法达到的对象”。
    2. 就是从根部(在 JS 中就是全局对象)出发定时扫描内存中的对象(可达)。 凡是能从根部到达的对象,都是还需要使用的 。
    3. 那些无法由根部出发触及到的对象被标记为不再使用(不可达),稍后进行回收。
- *扩展:《现代JsvaScript》,此书中对标记清除法有详细介绍*

4、闭包(会引起内存泄漏)

  1. 定义:闭包 = 内层函数(要使用外层函数的变量) + 外层函数的变量

  2. 举例:

    • 基础例子:

      function outer(){
      const a = 1;     // 构成闭包的要素之一,外层函数的变量(外层函数中得有一个变量)
      function f(){
      console.log(a); // 构成闭包的要素之二,内层函数中使用外层函数的变量
      };
      f();
      };
      outer();
    • 真正使用时的写法:

      function outer (){
      let a = 10;
      function fn(){
      console.log(a);
      };
      return fn;
      };
      let test =  outer();   test = function fn(){console.log(a);}
      test();         // 调用test{}就相当于调用了fn()函数
    • 简约写法(与真正代码的写法效果一致)

      function outer (){
      let a = 10;
      return function fn(){
      console.log(a);
      };
      };
      let test =  outer();   test = function fn(){console.log(a);}
      test();         // 调用test{}就相当于调用了fn()函数
  3. 闭包的应用:实现数据的私有

(1)举例1:统计函数调用次数,函数调用一次,就++

代码1:反例_次数 i 可以被修改,最后显示的次数非需求中所要的次数

let i = 1;
function outer(){
  function fn(){
    i++;
    console.log(`fn函数被调用了${i}次`);
    return fn;
  }
};

(2)举例2:将变量i的值封装到外函数内部

function outer(){
  let i = 1;
  function fn(){
    console.log(`fn函数被调用了${i}次`);
    i++;
  }
  return fn;
};
let test = outer();   // 调用函数outer(),得到它的返回值,所以test = fn函数   此时用于存放outer函数的内存未被释放,而此处的test为全局变量指向fn函数,所以fn函数可达即内存不会被回收 ==>  这也会导致内存的泄漏,所以闭包不要随便使用
test();         调用test函数,相当于调用了fn函数

5、变量提升和函数提升★★★

  1. 代码运行的时候会把函数的声明、创建提升至当前作用域的最开头;代码运行的时候会把(var声明的)变量的声明过程(没有赋值过程)提升至当前作用域的开头(在函数之后)

    PS:var的变量提升只把声明提升至最前面,之后再次声明的var不会将undefined覆盖原变量,只有当后面的var有赋值时才会覆盖原变量

  2. 举例:

    • 原代码顺序:
      console.log(a);
      fn();
      var a = 10;
      function fn(){
      console.log(123);
      };
    • 变量提升后的顺序
      function fn() {
      console.log(123);
      };
      var a;      // 只是把声明变量的过程提升至开头,PS:只有var有变量提升,let和const没有变量提升,所以代码按顺序执行
      // 然后再运行其他代码
      console.log(a);
      fn();
      a = 10;     // 赋值顺序仍然保留在原位置
      // 最后运行结果:undefined、123
  3. 变量提升的特点

    1. 提升的时候,把函数提升到最前面,其次是var声明的变量
    2. 提升的时候,只会把声明提升至当前作用域的最前面
  4. letconst提升问题

    • 小知识:在官方文档中,创建变量分为三部:创建 ---> 声明 ---> 赋值

      在大众的认识中,只有声明与赋值两步,即将创建与声明合并

    • 代码演示+讲解:

      // 以下代码运行会报错:Cannot access 'a' before initialization
      let a = 10;
      function fn(){
      // 此处会有let创建变量a的过程,会提升到当前作用域的最前面
      console.log(a);
      // 在隐式的创建a之后,在声明变量a之前,使用a,表示进入了暂时性死区,报错:Cannot access 'a' before initialization,表示在初始化之前不可访问变量a,这就代表a被创建了:若变量a未被创建则会在父工作域中查找变量a(10),若变量a不存在则提示应该为a is not defined,即变量a未定义
      let a = 1;
      };
      fn();

面试小tips

  • 问:var、let、const声明的变量有没有提升——(面试官不咋地)
  • 答1:(强硬的反问)请解释一下什么是变量提升?变量提升是非官方词汇,如何解释变量有没有提升
  • 答2:变量的创建分为创建、声明、赋值三个步骤,来源出处于ECMAScript官方文档编写。var会把创建和声明提升到前面,let知会把创建过程提升到前面(如果在创建之后,声明之前使用变量则会进入暂时性死区)