# 你必须知道的js基础知识

  1. 编译的三个步骤-----Javascript 是执行前进行编译
    • 词法分析-----词法单元 token
    • 预发解析-----转换成抽象语法树 AST
    • 代码生成
    • 角色: 引擎----负责整个 Javascript 程序的编译和执行过程;统筹工作 编译器----负责语法分析和代码生成等;分析和生成工作 作用域----负责收集并维护所有声明的标识符组成的一系列查询,有一套非常严格的规则,确定当前代码对标识符的访问权限;
    • 声明过程:编译器询问作用域是否有一个该名称的的变量存在于同一个作用域的集合中,存在则忽略继续进行编译,否则要求作用域在当前作用域集合声明一个新的变量
    • 赋值过程:编译器生成所需要的代码,引擎运行时,会询问作用域是否存在该变量,如果存在就使用该变量,否则向上查找
    • RHS:取到它的源值;如 console.log(a) 只是查找并取得 a 的值(是几---获取变量的值) LHS:试图找到变量的容器本身;如 a = 2,不关心当前值,只是找赋值操作的目标(给谁---目标赋值)
  2. 作用域
    • 作用域嵌套形成作用域链「遮蔽效应」
    • 区分 RHS与 LHS 的原因
      • RHS 查不到时引擎会抛出异常:ReferenceError
      • LHS 查找不到会创建一个具有该名称的变量「非严格模式」
      • TypeError 是作用域判别成功,但操作不合理
    • 欺骗词法作用域:在运行时修改或创建新的作用域
      • eval:用于程序生成代码并运行,好像代码是写在那个位置一样,严格模式下 eval 运行有其自己的词法作用域,意味着其中的声明无法修改所在的作用域,setTimeout,setInterval,还有new Function( params, body);
      • with 重复引用一个对象的多个属性的的快捷方式,实际是根据传递给with 的对象,凭空创建了一个全新的词法作用域
      • 引擎无法在编译时对作用域查找进行优化,优化对这些是无用的。
function foo(obj){
  with(obj){
    a = 2 //LHS
  }
}
foo({a: 3});
foo({b: 3}); //执行这行时,会创建一个全局的 a = 2,在该对象作用域查找 a,未找到,会进行正常的词法作用域查找
1
2
3
4
5
6
7
  1. 作用域提升: JavaScript 语法的全局机制:预处理和指令序言
    • 编译器先收集所有声明,并用合适的作用域把它们关联起来,声明是编译阶段,赋值是执行阶段
    • 词法作用域
    • 块级作用域 try{}catch(){}性能问题,函数进行包裹其中的 this, return, break, continue 就会发生改变
    • 函数的提升----函数的隐含值;函数表达式不会提升
    • 函数会首先被提升,然后才是变量
    • 预处理:会提前处理 var、函数声明、class、const 和 let 这些语句,以确定其中变量的意义
    • var 声明永远作用于脚本、模块和函数体这个级别,在预处理阶段,不关心赋值的部分,只管在当前作用域声明这个变量
function foo(a){
  console.log(a + b);
}
var b = 2;
foo(2)
1
2
3
4
5
  • 立即执行函数表达式,匿名与具名函数,可以从调试调用栈、调用自身、可读性考虑
// 功能一样
(()=>{})()
(()=>{}())
// 将第二段定义的 def 当做参数传递给第一段代码
(function IIFE(def){

})(function def(){})
1
2
3
4
5
6
7
  1. 什么是闭包?实际应用场景?如何产生的?变量如何被回收?
    • 函数在定义时的词法作用域以外的地方被调用,闭包使得函数可以继续访问定义时的词法作用域,无论是任何手段将内部函数传递到词法作用域以外,就会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
    • 循环中的函数是在各个迭代中分别定义的,被封闭在一个共享作用域中,只有一个 i
    • 模块的实现
function foo(){
  var a = 2;
  function bar(){
    console.log(a);
  }
  return bar();
}
var baz = foo();
baz();
// 1. 循环创建了 5 次宏任务,执行宏任务时,循环已结束,执行输出最终 i 的值
// 可以加自己变量的封闭的作用域
// for循环头部的 let 声明在循环过程中会不止一次被声明,随后的每个迭代使用上一个迭代结束的值初始化这个变量*****
for(var i = 1; i<=5; i++>){
  setTimeout(function timer(){
    console.log(i)
  }, i*1000)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  1. 动态作用域

    • 动态作用域:运行时动态决定,不关心函数和作用域是如何声明及在何处声明的,只关心在何处调用的;
    • 箭头函数的 this,只是用词法作用域覆盖了 this 本来的值
    • this:隐式"传递"一个对象引用
    • this绑定规则:
      • 默认规则----独立函数调用,取决于调用位置,严格模式下 this绑定为 undefined,否则为 window
      • 隐式绑定----函数的声明方式,如对象的属性,属于对象?对象.属性()调用,隐式绑定会把this 绑定到这个上下文对象,最有最后一层在调用位置中起作用;----隐式丢失
      • 显示绑定----装箱
      • new 绑定
    • 一个函数被调用时,会创建一个活动记录(执行上下文),记录在哪里被调用、调用方式、参数信息、this,这个执行上下文会在执行的过程中用到
  2. JS 的执行上下文??

    • JS 运行需要一个运行环境,这个环境就是执行上下文,JS 的预编译就是在这个环境中
    • 分为:全局、函数、eval 执行上下文
    • 分为:
      • 创建阶段:创建词法环境、生成变量对象、建立作用域链,全都 this 指向,并绑定 this
      • 执行阶段:变量赋值、函数引用及执行代码
    • 预编译也称为执行期上下文:
      • 创建 AO 对象
      • 找形参和变量声明,将变量和形参作为 AO 属性名,值为 undefined
      • 将形参和实参想统一
      • 在函数体内找到函数生命,值赋予函数体。最后程序输出变量值的时候,就是从 AO对象中拿
    • 变量对象 AO,用来存放当前执行环境中的变量
    • 变量对象 AO的创建过程-----变量提升
      • 创建 arguments 对象,将参数名和 undefined 组成键值对
      • 遇到同名函数时,后面的会覆盖前面的
      • 检查当前环境中的变量并赋值为 undefined,遇到同名的函数声明,为了避免函数被赋值为 undefined,会忽略此声明
    • 变量对象 AO 变为活动对象-----执行阶段:变量赋值、函数引用应执行代码
  3. javascript 垃圾回收:原始数据类型存于栈中,引用数据类型存于堆中

    • ESP:扩展栈指针寄存器,用于存放函数栈顶指针
    • javascript 函数执行时,将其上下文压入栈中,ESP 上移
    • 函数执行完成,ESP 下移到下一个函数执行上下文即可,当下一个函数入栈时,会将 ESP 以上的空间直接覆盖掉----通过下移 ESP 来完成栈的垃圾回收
    • 堆中垃圾回收:
      • 新生代:存放生命时间短的对象,小对象一般会到该区域,回收频繁,使用复制算法,因为复制和清理成本高,所以该空间比较小
      • 老生代:存放生命时间长和大的对象,新生代中经过两次垃圾回收仍然是活动对象的,会晋升为到该空间,使用标记-压缩算法
      • 因为 javascript 是单线程,所以垃圾回收算好和脚本在同一线程内执行,为了避免垃圾回收影响应用的性能,V8 将标记过程拆分成多个子标记,让垃圾回收和应用交通进行
      • 标记清除法:
        • 标记内存空间中的活动对象非活动对象
        • 删除非活动对象,释放内存空间
        • 整理内存空间,避免频繁回收造成大量内存碎片
      • 复制算法:
        • 空间平均分成 from 和 to 两部分
        • 先在 from 空间进行内存分配,当空间被占满,标记活动对象,将其复制到 to 空间
        • 复制完成后,将 from 和 to 空间互换
      • 引用计数
        • 实时统计指向对象的引用数
        • 当引用数为 0时,实时回收对象-----存在循环引用数,对象不会被回收
      • 标记-压缩算法:标记,活动对象移到内存的一端,集中到一起,清理掉边界以外的内存,释放连续空间

::: 容易造成内存泄漏的情况?node 端造成内存泄漏的情况?垃圾回收机制? :::

  1. 变量、函数、类下的预处理实例讲解
// 预处理 var
var a = 1;

function foo() {
    console.log(a);
    var a = 2;
}

foo();

// 场景二

var a = 1;

function foo() {
    console.log(a);
    if(false) {
        var a = 2;
    }
}

foo();
// 场景三  当执行到var a = 2时,作用域变成了 with 语句内,这时候的 a 被认为访问到了对象 o 的属性 a,所以最终执行的结果,我们得到了 2 和 undefined

var a = 1;

function foo() {
    var o= {a:3}
    with(o) {
        var a = 2;
    }
    console.log(o.a);
    console.log(a);
}

foo();
// 场景四

// 不用 IIFE 在循环内构造了作用域,函数体内的变量 i 没办法保存
for(var i = 0; i < 20; i ++) {
    void function(i){
        var div = document.createElement("div");
        div.innerHTML = i;
        div.onclick = function(){
            console.log(i);
        }
        document.body.appendChild(div);
    }(i);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
  • 在全局(脚本、模块和函数体),function 声明表现跟 var 相似,不同之处在于,function 声明不但在作用域中加入变量,还会给它赋值。
    • function 声明出现在 if 等语句中的情况有点复杂,它仍然作用于脚本、模块和函数体级别,在预处理阶段,仍然会产生变量,它不再被提前赋值,function 在预处理阶段仍然发生了作用,在作用域中产生了变量,没有产生赋值,赋值行为发生在了执行阶段
// 这里声明了函数 foo,在声明之前,我们用 console.log 打印函数 foo,我们可以发现,已经是函数 foo 的值了
console.log(foo);
function foo(){}


// 这段代码得到 undefined。如果没有函数声明,则会抛出错误
console.log(foo);
if(true) {
    function foo(){

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
  • class 声明也是会被预处理的,它会在作用域中创建变量,并且要求访问它时抛出错误
    • class 的声明作用不会穿透 if 等语句结构,所以只有写在全局环境才会有声明作用
var c = 1;
function foo(){
    console.log(c);
    class c {}
}
foo();
1
2
3
4
5
6
  • 指令序言:"use strict"是 JavaScript 标准中规定的唯一一种指令序言,但是设计指令序言的目的是,留给 JavaScript 的引擎和实现者一些统一的表达方式,在静态扫描时指定 JavaScript 代码的一些特性。
// [Symbol.iterator]: ()=>{} 自己定义?Generator 实现?异步实现?
function sleep(duration) {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve,duration);
    })
}
async function* foo(){
    i = 0;
    while(true) {
        await sleep(1000);
        yield i++;
    }

}
for await(let e of foo())
    console.log(e);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16