字节青训营-深入理解js

147 阅读12分钟

js基本概念

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

浏览器进程与渲染进程的展开

image.png

补充一下进程与线程的区别

  • 进程:操作系统是以进程为单位执行任务。进程一般由程序,数据集合和进程控制块三部分组成。

  • 线程:进程的一部分。程序执行的最小单位。而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。线程中可以共享地址空间,全局变量,打开的文件等,每个线程拥有自己的寄存器,局部变量,堆栈等。

  1. 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
  2. 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
  3. 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见;
  4. 调度和切换:线程上下文切换比进程上下文切换要快得多

js基本概念

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

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

image.png

  1. 面向对象,函数式:这两个东西与面向对象编程和函数式编程是一个概念,前面的老师讲过编程范式,如果认真听的话应该还记得这件事,简单复习下,

面向对象编程的思维方式是把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承和多态来演示事物事件的联系。

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

// 将字符串转换为大写并添加感叹号
const toUpperCase = str => str.toUpperCase();
const exclaim = str => `${str}!`;
const shout = str => exclaim(toUpperCase(str));

console.log(shout('hello world')); // 输出: HELLO WORLD!
  1. 解释类语言 JIT

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

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

  1. 安全,性能差

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

数据类型

分为基本数据类型,和引用数据类型,老师课程里说的区别,我就不写了,如果不清楚的话,需要去单独补习一下~~

image.png

作用域

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

image.png

变量提升

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

举例

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

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

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

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

函数声明也会被提升,下面是一个例子:

foo(); // 输出: Hello, world!

function foo() {
  console.log('Hello, world!');
}

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

function foo() {
  console.log('Hello, world!');
}

foo(); // 输出: Hello, world!

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

a()
var a = foo(); // 输出: Hello, world!

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

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

// 变量提升 
var a = undefined

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

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

改为let,做一个实验

a()
let a = foo(); 

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

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

js是怎么执行的

补充老师课堂说的小知识点;

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

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

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

image.png

执行上下文

词法环境中存储了当前执行上下文中所有的本地变量、函数声明、形参等信息,以及对外部词法环境的引用。

image.png

具体例子详细说明


var scop = 'global'; // 声明了一个全局变量 `scop` 并将其赋值为字符串 `'global'`
func() // 调用了名为 `func` 的函数。由于这个函数在后面定义,所以它可以在这里调用

function func() { // 义一个名为 `func` 的函数。
  var funcVar = 'func'; // 函数内部声明了一个局部变量 `funcVar` 并将其赋值为字符串 `'func'`。
  console.log('这是一个函数', funcVar); 
  // 调用了 `console.log` 函数,
  // 输出字符串 `'这是一个函数'` 和变量 `funcVar` 的值。
}
var company = '字节' // 声明了一个全局变量 `company` 并将其赋值为字符串 `'字节'`

image.png

创建执行上下文时做了什么?

image.png

词法环境、变量环境、outer
  • 词法环境: 存放let const 定义的变量以及函数。

  • 变量环境:存放var 定义的变量。

  • outer:指向外部函数的一个指针。

image.png

堆和栈

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

引用类型:则是地址传递

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

ps: 这个里面面试中经常问的考点就是深拷贝与浅拷贝,自荐一下,想要了解的同学可以快速查看,传送门:javascript中的深拷贝与浅拷贝,常见方法以及优缺点,手动实现深拷贝函数 - 掘金 (juejin.cn)

image.png

补充ESP指针

image.png

js进阶知识点

闭包

内部函数引用外部函数的变量或参数。本质是一个没有被回收的对象。 举例:

var n = 10
function fn(){
    var n =20
    function f() {
       n++;
       console.log(n)
     }
    return f
}

var x = fn()
x() // 21

image.png

为了避免内存泄漏,可以在不再需要闭包时将其设置为 null。这样,闭包所引用的外部变量就不再被引用,它们就可以被垃圾回收器回收。

this

在 JavaScript 中,this 关键字的指向取决于函数的调用方式。下面是一些常见的情况:

  1. 在全局作用域中,this 指向全局对象。在浏览器中,全局对象是 window 对象。
console.log(this === window); // 输出 true
  1. 在函数中,this 的指向取决于函数的调用方式。如果函数作为一个对象的方法被调用,那么 this 指向这个对象。
let obj = {
  value: 'I am an object property',
  method: function() {
    console.log(this.value);
  }
};

obj.method(); // 输出 'I am an object property'
  1. 如果函数不是作为对象的方法被调用,那么 this 指向全局对象。
function func() {
  console.log(this === window);
}

func(); // 输出 true

4、new 构造函数中的this 关键字指向新创建的对象

本质需要理解new 操作符做了什么

  • 创建临时对象
  • 将this指向临时对象 (mycar也就等同于这个对象)
  • 执行构造函数
  • 返回临时对象

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}

let myCar = new Car('zhangsan', 'bc', 2022);

console.log(myCar.make); // 输出 'zhangsan'
console.log(myCar.model); // 输出 'bc'
console.log(myCar.year); // 输出 2022


下面是this的补充知识点

  1. 在严格模式下,如果函数不是作为对象的方法被调用,那么 this 的值为 undefined
'use strict';

function func() {
  console.log(this);
}

func(); // 输出 undefined
  1. 可以使用 callapplybind 方法显式地设置 this 的值。
function func() {
  console.log(this.value);
}

let obj1 = { value: 'I am object 1' };
let obj2 = { value: 'I am object 2' };

func.call(obj1); // 输出 'I am object 1'
func.apply(obj2); // 输出 'I am object 2'

let boundFunc = func.bind(obj1);
boundFunc(); // 输出 'I am object 1'

最后可以看下老师的图 很清晰~~

image.png

垃圾回收

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

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

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

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

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

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

老生代垃圾回收

整个流程就采用的就是上文所说的标记整理算法

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

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

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

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

image.png

事件循环

都是一些基本概念,防止有同学不清楚,还是普及一下 0. js代码主要分为两大类: 同步代码、异步代码

  1. 异步代码又分为:微任务与宏任务

  2. 事件循环Event Loop执行机制

    • 1.进入到script标签,就进入到了第一次事件循环.

    • 2.遇到同步代码,立即执行

    • 3.遇到宏任务,放入到宏任务队列里.

    • 4.遇到微任务,放入到微任务队列里.

    • 5.执行完所有同步代码

    • 6.执行微任务代码

    • 7.微任务代码执行完毕,本次队列清空

    • 寻找下一个宏任务,重复步骤1

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

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

    微任务主要包含:Promise、MutaionObserver、process.nextTick(Node.js 环境)

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

练习题

console.log('start');
    setTimeout(() => {
      console.log('children2');
      Promise.resolve().then(() => {
        console.log('children3');
      })
    }, 0);

    new Promise(function (resolve, reject) {
      console.log('children4');
      setTimeout(function () {
        console.log('children5');
        resolve('children6')
      }, 0)
    }).then((res) => {
      console.log('children7');
      setTimeout(() => {
        console.log(res);
      }, 0)
    })
    
 // start children4 children2 children3 children5 children7 children6