深入理解JS | 字节青训营

59 阅读11分钟

1. js基本概念

1.1 基本特点

  • 借鉴C语言的基本语法
  • 借鉴java的数据类型和内存管理
  • 借鉴scheme语言,将函数提升到一等公民身份
  • 借鉴self语言,使用基于原型(ProtoType)的继承机制

image.png

1.2. 浏览器的进程模型

image.png

1.3 进程与线程的区别

  • 进程:操作系统是以进程为单位执行任务。进程一般由程序,数据集合和进程控制块三部分组成。
  • 线程:进程的一部分。程序执行的最小单位。而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。线程中可以共享地址空间,全局变量,打开的文件等,每个线程拥有自己的寄存器,局部变量,堆栈等。
  1. 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
  2. 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
  3. 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见;
  4. 调度和切换:线程上下文切换比进程上下文切换要快得多

1.3 js特点

  1. 单线程: GUI线程和JS线程不能同时执行,互斥。 image.png

  2. 动态,弱类型 定义变量不需要指定类型,而在其他语言中,比如C,C++都需要指定类型。

image.png 3. 面向对象,函数式:这两个东西与面向对象编程和函数式编程是一个概念。 面向对象编程的思维方式是把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承和多态来演示事物事件的联系。

image.png 在JS中使用函数式编程,要求我们尽量使用纯函数,将大部分的逻辑的主要功能进行抽离,封装成新的函数。在调用该函数的时候只需传入需要的配置,就能实现向实例中添加新功能的作用。

  1. 解释类语言 JIT

JS是一种解释型语言,程序在运行时边编译边运行,因此需要用到解释器才可执行代码,且执行效率天生比编译型语言低

JIT:即时编译,是一种编译技术,它可以在运行时将代码编译为机器语言,以提高程序的执行效率。

  1. 安全,性能差

JavaScript是一种基于对象和事件驱动并具有相对安全性的客户端脚本语言。它不允许访问本地的硬盘,不能将数据存入服务器,不允许对网络文档进行修改和删除。边解释,边执行,所以性能会相对差。

2. js的数据类型

image.png

  • 在js里面复杂的数据类型,赋值的都是地址
  • 基础数据类型赋值的是原始的值
const a = {
    name: "wwww",
}
const b = a;
b.name = '111';//复杂的数据类型赋值的是地址
console.log(a, b);//{ name: '111' } { name: '111' }

const str = '111';
let newStr = str;
newStr = '22';//基础数据类型赋值的是原始的值
console.log(str, newStr);//111 22
  • 在js里面,复杂的数据类型原始值,是可以被改变的。基础数据类型中是不可以被改变的。
// 在js里面,复杂的数据类型原始值,是可以被改变的。
// 在基础数据类型中是不可以被改变的
const arr = [1, 2];
arr.push(3);

const str='ststststst';
str.slice(0,2)
console.log(arr,str);//[ 1, 2, 3 ] ststststst

3. js的作用域

  • 规定了变量分为可访问性和可见性,作用域分为动态作用域和静态作用域,js是静态作用域,通过作用域可以知道代码如何查找标识符。
  • js中的作用域分为三种,全局作用域,函数作用域,块作用域。

image.png

3.1 变量提升

变量提升(hoisting)是JavaScript中的一个特性,它指的是在执行代码之前,变量和函数声明会被移动到它们所在作用域的顶部。

console.log(x);//undefined
var x = 10;

console.log(a);//直接报错
let a=10;

在这个例子中,我们在声明变量 x 之前就使用了它。由于变量提升的原因,这段代码实际上相当于:

var x;
console.log(x); // 输出: undefined
x = 10;
复制代码

变量 x 被提升到了它所在作用域的顶部,并且被初始化为 undefined。因此,在声明变量 x 之前使用它不会报错,而是输出 undefined

  • var有变量提升
  • let、 const没有变量提升,提前访问会报错
  • function函数可以先调用再定义
  • 赋值给变量的函数无法提前调用
  1. 函数声明也会被提升,举例:
show();//"哈哈哈
function show() {
    console.log("哈哈哈");
}
  1. 在这个例子中,我们在声明函数 show 之前就调用了它。由于函数声明提升的原因,这段代码实际上相当于:
function show() {
    console.log(""哈哈哈");
}
show();//"哈哈哈

函数 show 被提升到了它所在作用域的顶部,因此,在声明函数 show 之前调用它不会报错,而是正常输出 "哈哈哈

  1. 例子:
a()
var a = show(); // 输出: Hello, world!

function show() {
  console.log('Hello, world!');
}
// Uncaught TypeError: a is not a function

如果像上面那样将函数赋值给a,还是会报错,因为上面这段代码相当于

// 变量提升 
var a = undefined

a() // undefined不是一个function
var a = show(); // 输出: Hello, world!

function show() {
  console.log('Hello, world!');
}
// Uncaught TypeError: a is not a function

改为let之后:

a()
let a = show(); 

function show() {
  console.log('Hello, world!');
}
// Cannot access 'a' before initialization

let a = show();,这里使用了 let 关键字来声明变量 a。使用 let 和 const 声明的变量不会被提升,所以会在第一行 a() 会报错,因为在这一行之前并没有定义变量 a。在 JavaScript 中,如果你试图访问一个未定义的变量,就会抛出一个错误。

4. js怎么执行的

4.1js的执行

1、AST为什么不能直接转为机器码呢?还要先转为字节码才可以转为机器码呢?

字节码的代码量比机器码少得多,可以节约内存的开销。

2、图中的最右侧部分其实就是我们说的JIT,JIT具有热编译机制,当JVM发现某个方法或代码块运行特别频繁的时候,就会认为这是“热点代码”(Hot Spot Code)。JIT会把部分“热点代码”编译成本地机器相关的机器码,并进行优化,然后再把编译后的机器码缓存起来,以备下次使用。

image.png

4.2 执行上下文

当JS 引擎解析到可执行代码片段(通常是函数调用)的时候,就会先做一些执行前的准备工作,这个准备工作,就叫做“执行上下文 (execution context 简称也叫执行环境)"

image.png

  • 全局执行上下文:代码开始执行时就会创建,将他压执行栈的栈底,每个生命周期内只有一份
  • 函数执行上下文: 当执行一个函数时,这个函数内的代码会被编译,生成变量环境、词法环境等,当函数执行结束的时候该执行环境从栈顶弹出

image.png

4.3 创建执行上下文做了什么

image.png

  • 词法环境(存放函数,let,const定义的变量): 基于ECMAScript 代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成
  • 变量环境(存放var定义的变量): 变量环境和词法环境的一个不同就是前者被用来存储函数声明和变量 (let 和 const)绑定而后者只用来存储 var 变量绑定
  • Outer: 指向外部变量环境的一个指针

image.png

image.png

4.4 堆和栈

image.png 基本类型:采用的是值传递

引用类型:则是地址传递

  • 引用类型的数据的地址指针是存储于栈中的,将存放在栈内存中的地址赋值给接收的变量。
  • 当我们想要访问引用类型的值的时候,需要先从栈中获得对象的地址指针,然后,在通过地址指针找到堆中的所需要的数据(保存在堆内存中,包含引用类型的变量实际上保存的不是变量本身,而是指向该对象的指针)。

image.png

5. js的进阶知识点

5.1 闭包

image.png

function showname() {
    const name = '好好';
    const dep = '你好vue';
    return function () {
        console.log(dep);
        return name;

    }
}
const getNmae = showname();
console.log(getNmae());
// 你好vue
// 好好

5.2 this

  1. 普通函数的this,指向window
function show() {
    console.log(this);//普通函数指向的是window
}

show();
  1. 嵌套函数的this,也是指向window
function show() {
    function showname() {
        console.log(this);//嵌套函数指向的也是window

    }
    showname();
}

show();
  1. 对象调用时,this,也是指向obj对象
const obj = {
    name: 'hello',
    showname() {
        console.log(this.name);//这里的this,指向obj对象
    }
}

const obj1 = {
    name: "word",
}
//改变obj的this指向
obj.showname.apply(obj1);
  1. new 构造函数中的this 关键字指向新创建的对象
function showname() {
    this.name = 'hello';
    console.log(this.name);
}
const getname = new showname();

6 垃圾回收

6.1 v8对GC(回收垃圾的)的优化

  1. 分代式垃圾回收:v8 的垃圾回收策略主要基于分代式垃圾回收机制,V8 中将堆内存分为新生代和老生代两区域。

  2. 新生代垃圾回收:用了一种复制式的方法即 Cheney算法Cheney算法 将堆内存一分为二,一个是处于使用状态的空间我们暂且称之为 使用区,一个是处于闲置状态的空间我们称之为 空闲区

新加入的对象都会存放到使用区,当使用区快被写满时,就需要执行一次垃圾清理操作,使用区和空闲区互相交换。

  • 生命周期较长的对象或者空闲区空间占用超过了 25% ,那么这个对象会被直接晋升到老生代空间中,原因是当完成 垃圾 回收后,空闲区将翻转成使用区,继续进行对象内存的分配,若占比过大,将会影响后续内存分配。

  • 新生代回收使用并行回收提高效率并行回收依然会阻塞主线程

  1. 老生代垃圾回收
  • 整个流程就采用的就是上文所说的标记整理算法

  • 采用增量标记、惰性清理,并发回收提高效率

  • 增量标记是指将一次垃圾回收的过程分为很多个小部分,每次执行完一小块,就让应用程序执行一会,交替执行。增量标记采用三色标记法(暂停与恢复)与写屏障(增量中修改引用)

惰性清理指的是让js脚本先执行,也无需一次性清理完所有非对象内存。

  1. 并发回收:主线程在执行 JavaScript 的过程中,辅助线程能够在后台完成执行垃圾回收的操作,辅助线程在执行垃圾回收的时候,主线程也可以自由执行而不会被挂起

image.png

7. 事件循环

js代码主要分为两大类: 同步代码、异步代码。 异步代码又分为:微任务与宏任务

事件循环Event Loop执行机制

  1. 进入到script标签,就进入到了第一次事件循环.
  2. 遇到同步代码,立即执行 -
  3. 遇到宏任务,放入到宏任务队列里.
  4. 遇到微任务,放入到微任务队列里.
  5. 执行完所有同步代码
  6. 执行微任务代码.
  7. 微任务代码执行完毕,本次队列清空
  8. 寻找下一个宏任务,重复步骤1

以此反复直到清空所有宏任务,这种不断重复的执行机制,就叫做事件循环

  • 宏任务主要包含:script( 整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)
  • 微任务主要包含:Promise、MutaionObserver、 process.nextTick(Node.js 环境)

执行过程:在所有同步任务执行完毕之后,异步任务会优先执行所有已经存在任务队列中的微任务。在所有的微任务执行完毕之后,再去宏任务队列中执行一个(注意是一个)宏任务,执行完一个宏任务之后会再去微任务队列中检查是否有新的微任务,有则全部执行,再回到宏任务队列执行一个宏任务,以此循环。

image.png

例子:

console.log("同步代码1");

setTimeout(() => {//执行的时候,放到定时器执行的线程里面去。
    console.log('children1');// 微任务
}, 0)//0,表示,放到线程里面去,会马上放到宏任务的队列里面去。这里的0秒,最小其实是4毫秒
new Promise((reslove) => {
    console.log('同步代码2');
    reslove();
}).then(() => {
    console.log('promise.then');
})
console.log('同步代码3');

// 同步代码1
// 同步代码2
// 同步代码3
// promise.then
// children1

例子2:

console.log('start');//同步代码一
    setTimeout(() => {//宏任务1
      console.log('children2');
      Promise.resolve().then(() => {
        console.log('children3');
      })
    }, 0);

    new Promise(function (resolve, reject) {
      console.log('children4');//同步代码二
      setTimeout(function () {//宏任务2
        console.log('children5');
        resolve('children6')
      }, 0)
    }).then((res) => {//微任务1
      console.log('children7');
      setTimeout(() => {
        console.log(res);
      }, 0)
    })
    
 // start children4 children2 children3 children5 children7 children6