1. 进程和线程
进程和线程都是操作系统中的基本概念。
1.0 程序 Program 的概念
当我们执行一个被编译过的程序,操作系统会立刻生成一个进程去执行这个程序。一个程序是存储在磁盘中的被动的实体。它需要用户通过鼠标点击或者通过命令行的方式去执行一个程序。
一个程序可以有很多的进程。
1.1 什么是进程(Process)
CPU执行被加载进电脑内存中的磁盘里的程序代码,这种状态,就是一个进程。
一个进程可以被描述为一个程序在电脑中运行的实体。
简单来说,进程就是一个被执行的程序(一个程序执行的一个过程)。
进程是操作系统进行资源分配和调度的一个独立单位(基本单位)。它是应用程序运行的实例。每个进程至少有一个线程,即主线程。
1.2 什么是线程 (Thread)
线程被包含在进程之中,是进程中的实际运作单位。
一个进程可以拥有多个线程,这些线程共享进程的资源。
线程是操作系统能够进行运算调度的最小单位。
1.3 进程和线程的关系
线程的运行是建立在进程的运行的基础上。
线程是进程中的实际执行单元,操作系统会进一步将CPU时间分配给进程中的各个线程。
一个进程至少包含一个线程,即主线程。
下面举一个简单的例子,比如我们打开qq音乐听歌,qq音乐就可以理解为一个进程,在qq音乐中我们可以边听歌边下载这里就是多线程,听歌是一个线程,下载是一个线程。如果我们再打开vscode来写代码这就是另外一个进程了。
1.4 JavaScript 是单线程还是多线程?
JavaScript是单线程。
它作为浏览器脚本语言的主要用途是与用户交互,操作DOM,这决定了它只能是单线程,否则会带了复杂的同步问题。比如,假如JavaScript有两个线程,一个线程在DOM节点上添加内容,一个线程删除了这个DOM节点,这时浏览器就需要考虑以哪个线程为准的问题了。2. JavaScript 的执行环境
2. 执行环境
2.1 什么是执行环境?
执行环境是 JavaScript 最为重要的一个概念。
执行环境定义了 变量 或 函数 有权访问的其他数据,决定了它们各自的行为。
每个执行环境都有一个与之关联的变量对象,执行环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个变量对象,但解析器在处理数据时会在后台使用它。
全局执行环境 是最外围的一个执行环境。根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样。在Web浏览器中,全局环境被认为是window对象。因此,所有全局变量和函数都是作为window对象的属性和方法保存的。
函数执行环境 是JavaScript执行流进入一个函数时, 函数的执行环境会被推入一个 环境栈 中。而在函数执行之后,栈将其从环境栈中弹出,然后把控制权返回给之前的执行环境。JavaScript程序中的执行流正是由这个方便的机制控制着。
某个执行环境中的所有代码执行完毕后,该环境被销毁,从环境栈中弹出,保存在其中的变量和函数定义也随之销毁。(全局执行环境知直到应用程序退出——例如关闭网页或浏览器时)
2.2 执行环境和作用域的关系?
我们知道JavaScript中有两种作用域:全局作用域 和 函数作用域
作用域是指变量和函数的可见性和可访问性的范围,作用域决定了在何处可以访问到特定的变量和函数。
因为作用域的关系,当我们在代码某处去读取(查找)变量时,JavaScrpt 会现在当前作用域查找,如果没有找到,会向上级查找父作用域,直到全局作用域也没有找到,则会报错。
当代码在一个环境中执行时,会创建变量对象的一个作用域链,它的用途就是保证对执行环境有权访问的所有变量和函数的有序访问。
作用域链的前端,始终都是当前执行的代码所在的执行环境的变量对象。
如果这个环境是函数,则将其活动对象作为变量对象,最开始只包含一个变量, 即 arguments 对象 (全局执行环境中则不存在)。
作用域链的下一个变量对象来自外部(包含)环境,而再下一个变量对象则来自下一个外部(包含)环境,这样,一直延续到全局执行环境。
全局执行环境的变量对象是作用域链中的最后一个对象。
每一个执行环境在本质上都是一种作用域层级。
注意:执行环境也就是执行上下文。通常这两个代表一个意思。
var a = 100
function fn1() {
var a = 10
function fn2() {
}
}
3. JavaScript 中的 this
3.1 this 出现的场景
(1)全局环境中:直接在代码里面打印 this,在浏览器的控制台我们可以看到是 window 对象。注意:在严格模式开启下,会打印出 undefined。
console.log(this); // window
(2)构造函数中:当我们使用构造函数(JavaScript中的类)去创建新的对象时, this 指向的是新创建出的对象实例。
function Cat(name) {
this.name = name
console.log(this)
}
let cat = new Cat('小花');
(3) 普通函数中:有很多种情况的出现,比如:
// 情况1 函数作为对象的方法
var obj = {
name: 'cdx',
sayHi: function() {
console.log(this) // { name: 'cdx', sayHi: f }
console.log('hi, i am ' + this.name)
}
}
obj.sayHi(); // 'hi, i am cdx'
// 情况2 函数作为对象的属性
var name = '作用域测试'
function getThis() {
console.log(name); // '作用域测试'
console.log(this.name);
return this
}
const obj1 = {
name: 'ckn'
}
const obj2 = {
name: 'lxh'
}
obj1.getThis = getThis;
obj2.getThis = getThis;
obj1.getThis(); // 'ckn' { name: 'ckn', getThis: f }
obje.getThis(); // 'lxh' { name: 'lxh', getThis: f }
// 情况3 事件处理
let btn = document.getElementById('btn');
btn.onclick = funcion() {
console.log(this) // 这个按钮的DOM对象
}
其实 情况1(函数作为对象的方法) 和 情况2(函数作为对象的属性) 本质是类似的,都是函数作为某某对象的方法被调用,此时 this 的指向很容易明白就是该对象自身。
情况1 和 情况2 有唯一的区别就在于,两个函数产生的作用域是不同的(因为在JavaScript看来函数本身也是一个值,也有自己的作用域)。
函数执行会有函数作用域的产生,函数执行时所在的作用域,是声明时(定义时)所在的作用域,而不是调用时所在的作用域。
情况3 其实换种方式来理解也很容易明白, 把获取的DOM对象(btn)当作对象, 它的 onclick 属性的值是一个方法(函数),这样情况就和 情况1 类似了,所以this也是指向调用函数的对象自身。
有意思的来了,看下面的代码
function foo() {
console.log(this)
};
const obj = { name: 'ckn', foo: foo };
const btnDOM = document.getElementById('btn');
foo(); // window
obj.foo(); // { name: 'ckn', foo: f };
// 点击按钮后 打印出的是按钮DOM对象
同样一个函数,this 会有多种情况出现。
这就是 JavaScript 的一大特点: 函数存在 [ 定义时上下文 ] 和 [ 运行时上下文 ] 以及 上下文是可以改变的(使用call、apply、bind方法)!
定义时上下文指的是函数被声明时所在的环境。这通常决定了函数的默认
this绑定。函数的这个上下文是静态的,也就是说,一旦函数被定义,它的上下文就已经确定了,不会改变。
运行时上下文指的是函数实际被调用时所在的环境。这个上下文决定了
this的最终值。运行时上下文是动态的,它取决于函数是如何被调用的。
注意:函数的 this 值的最终指向是运行时绑定的(也就是最终调用的时候)!
3.2 为什么会有 this ,设计 this 的目的是?
我们知道JavaScript允许在函数体内部,引用当前环境的其他变量(函数内部可以访问外部数据)。
由于函数可以在不同环境中运行,所以需要一种机制,能够在函数体内部获取当前的运行环境,所以,this 就出现了。this 被设计的目的就是在函数体内部,指代函数当前的运行环境。
在全局环境,this 就是 window。
在函数环境,对象的方法时 是 对象自身,构造函数时 是 新创建的实例对象。
3.3 箭头函数中 this 的特殊性
箭头函数没有自己的this绑定。它们会捕获定义时上下文中的this值,并在运行时使用这个值。这意味着箭头函数内部的this始终与其定义时的上下文一致,不受调用方式的影响。
4. JavaScript的执行机制包括几个关键部分
4.0 同步任务 和 异步任务
同步任务: 在主线程上排队执行的任务,它们会一个接着一个顺序执行,等待前一个任务完成后才会执行下一个任务。同步任务是顺序执行的,它们可能会阻塞主线程(特别耗时的同步任务会影响用户体验,比如长时间的计算)
异步任务:不进入主线程,不会阻塞主线程,而进入任务队列的任务。
4.1 调用栈(Call Stack)
JavaScript的执行是基于调用栈的。当代码执行时,每个新调用的函数会添加到调用栈的顶部,当函数执行完毕,它会从调用栈中弹出。
调用栈是JavaScript引擎用来跟踪当前执行的函数调用顺序的数据结构。
调用栈是执行环境栈的一个具体实例,它展示了当前正在执行的函数调用序列。而执行环境栈是一个更广泛的概念,它包括了调用栈,并且还可能包括其他类型的执行上下文,如全局上下文、eval上下文等。在大多数日常讨论中,这两个术语可以互换使用,但理解它们之间的细微差别有助于更准确地把握JavaScript的执行机制
4.2 事件循环(Event Loop)
JavaScript是单线程的,它通过事件循环来处理异步操作,如网络请求、定时器(setTimeout 和 setInterval)、DOM事件绑定等。事件循环允许JavaScript在等待异步操作完成时继续执行其他任务。
4.3 任务队列(Task Queue)
异步操作完成后,它们的回调函数会被放入任务队列中等待执行。事件循环会不断检查任务队列,一旦调用栈为空,就会将队列中的回调函数移至调用栈中执行。
4.4 宏任务 和 微任务
在JavaScript中,宏任务包括 I / O 操作、网络请求、setTimeout等。
而微任务包括 Promise.then()等。
微任务的优先级高于宏任务,主线程会先执行微任务,如果有则把微任务全部执行,然后再去执行宏任务。