# 你必须知道的js基础知识
- 编译的三个步骤-----Javascript 是执行前进行编译
- 词法分析-----词法单元 token
- 预发解析-----转换成抽象语法树 AST
- 代码生成
- 角色:
引擎----负责整个 Javascript 程序的编译和执行过程;统筹工作
编译器----负责语法分析和代码生成等;分析和生成工作
作用域----负责收集并维护所有声明的标识符组成的一系列查询,有一套非常严格的规则,确定当
前代码对标识符的访问权限; - 声明过程:编译器询问作用域是否有一个该名称的的变量存在于
同一个作用域的集合中,存在则忽略继续进行编译,否则要求作用域在当前作用域集合声明一个新的变量 - 赋值过程:编译器生成所需要的代码,引擎运行时,会询问作用域是否存在该变量,如果存在就使用该变量,否则向上查找
- RHS:取到它的源值;如 console.log(a) 只是查找并取得 a 的值(是几---获取变量的值) LHS:试图找到变量的容器本身;如 a = 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
2
3
4
5
6
7
- 作用域提升: 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
2
3
4
5
- 立即执行函数表达式,匿名与具名函数,可以从调试调用栈、调用自身、可读性考虑
// 功能一样
(()=>{})()
(()=>{}())
// 将第二段定义的 def 当做参数传递给第一段代码
(function IIFE(def){
})(function def(){})
1
2
3
4
5
6
7
2
3
4
5
6
7
- 什么是闭包?实际应用场景?如何产生的?变量如何被回收?
- 函数在定义时的词法作用域以外的地方被调用,闭包使得函数可以继续访问定义时的词法作用域,无论是任何手段将内部函数传递到词法作用域以外,就会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
- 循环中的函数是在各个迭代中分别定义的,被封闭在一个共享作用域中,只有一个 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
动态作用域
- 动态作用域:运行时动态决定,不关心函数和作用域是如何声明及在何处声明的,只关心在何处调用的;
- 箭头函数的 this,只是用词法作用域覆盖了 this 本来的值
- this:隐式"传递"一个对象引用
- this绑定规则:
- 默认规则----独立函数调用,取决于调用位置,严格模式下 this绑定为 undefined,否则为 window
- 隐式绑定----函数的声明方式,如对象的属性,属于对象?对象.属性()调用,隐式绑定会把this 绑定到这个上下文对象,最有最后一层在调用位置中起作用;----隐式丢失
- 显示绑定----装箱
- new 绑定
- 一个函数被调用时,会创建一个活动记录(执行上下文),记录在哪里被调用、调用方式、参数信息、this,这个执行上下文会在执行的过程中用到
JS 的执行上下文??
- JS 运行需要一个运行环境,这个环境就是执行上下文,JS 的预编译就是在这个环境中
- 分为:全局、函数、eval 执行上下文
- 分为:
- 创建阶段:创建词法环境、生成变量对象、建立作用域链,全都 this 指向,并绑定 this
- 执行阶段:变量赋值、函数引用及执行代码
- 预编译也称为执行期上下文:
- 创建 AO 对象
- 找形参和变量声明,将变量和形参作为 AO 属性名,值为
undefined - 将形参和实参想统一
- 在函数体内找到函数生命,值赋予函数体。最后程序输出变量值的时候,就是从 AO对象中拿
- 变量对象 AO,用来存放当前执行环境中的变量
- 变量对象 AO的创建过程-----变量提升
- 创建 arguments 对象,将参数名和 undefined 组成键值对
- 遇到同名函数时,后面的会覆盖前面的
- 检查当前环境中的变量并赋值为 undefined,遇到同名的函数声明,为了避免函数被赋值为 undefined,会忽略此声明
- 变量对象 AO 变为活动对象-----执行阶段:变量赋值、函数引用应执行代码
javascript 垃圾回收:原始数据类型存于栈中,引用数据类型存于堆中
- ESP:扩展栈指针寄存器,
用于存放函数栈顶指针 - javascript 函数执行时,将其上下文压入栈中,ESP 上移
- 函数执行完成,ESP 下移到下一个函数执行上下文即可,当下一个函数入栈时,会将 ESP 以上的空间直接覆盖掉----通过下移 ESP 来完成栈的垃圾回收
- 堆中垃圾回收:
- 新生代:存放生命时间短的对象,小对象一般会到该区域,回收频繁,使用复制算法,因为复制和清理成本高,所以该空间比较小
- 老生代:存放生命时间长和大的对象,新生代中经过两次垃圾回收仍然是活动对象的,会
晋升为到该空间,使用标记-压缩算法 - 因为 javascript 是单线程,所以垃圾回收算好和脚本在同一线程内执行,为了避免垃圾回收影响应用的性能,V8 将标记过程拆分成多个子标记,让垃圾回收和应用交通进行
- 标记清除法:
- 标记内存空间中的
活动对象与非活动对象 - 删除非活动对象,释放内存空间
- 整理内存空间,避免频繁回收造成大量内存碎片
- 标记内存空间中的
- 复制算法:
- 空间平均分成 from 和 to 两部分
- 先在 from 空间进行内存分配,当空间被占满,标记活动对象,将其复制到 to 空间
- 复制完成后,将 from 和 to 空间互换
- 引用计数
- 实时统计指向对象的引用数
- 当引用数为 0时,实时回收对象-----存在循环引用数,对象不会被回收
- 标记-压缩算法:标记,活动对象移到内存的一端,集中到一起,清理掉边界以外的内存,释放连续空间
- ESP:扩展栈指针寄存器,
::: 容易造成内存泄漏的情况?node 端造成内存泄漏的情况?垃圾回收机制? :::
- 变量、函数、类下的预处理实例讲解
// 预处理 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
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
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
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16