JavaScript

事件循环机制

事件循环练习

由于js是单线程,为防止代码阻塞,将代码分为同步和异步,同步代码会直接放入执行栈中执行,异步代码(如setTimeout)放入宿主环境(浏览器,Node)中,时机到了(点击事件即点击后,setTimeout即时间结束后)以后将回调函数放入任务队列中,执行栈中的代码执行完后就会去任务队列中查看有无异步代码要执行。反复循环查看执行,这个过程就是事件循环。

js又把异步任务分为宏任务(由宿主环境发起,如script,事件,网络请求Ajax/Fetch,setTimeout()/setInterval())和微任务(由JS引擎发起,如Promise,Promise本身同步,then/catch回调函数异步)

注意⚠️

  • 排前面的 script 先执行,执行其内部的【同】,再执行其【微】,接着就轮到下一个大的宏,也就是执行下一个 script,【同】、【微】......顺序执行完后,再从头开始,看第一个 script 是否有需要执行的【宏】,再去下一个 script 中找 【宏】,等大家宏结束后,进入下一轮循环。

  • async函数里面的属于同步代码,await后的代码属于异步微任务

Practice

1️⃣

console.log('A');·
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
Promise.resolve().then(() => setTimeout(() => console.log('D'), 0));
console.log('E');
点击查看答案
A → E → C → B → D

2️⃣

const promise = new Promise((resolve, reject) => {
console.log(1);
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
点击查看答案
 1 → 2 → 4

3️⃣

async function async1() {
 console.log('1');
 await async2();
 console.log('2');
}
async function async2() { console.log('3'); }
setTimeout(() => console.log('4'), 0);
async1();
new Promise(resolve => {
 console.log('5');
 resolve();
}).then(() => console.log('6'));
console.log('7');
点击查看答案
 1 → 3 → 5 → 7 → 2 → 6 → 4

4️⃣

new Promise((resolve) => {
 console.log(1);
 resolve(3);
 Promise.resolve().then(() => {
 console.log(4);
 });
}).then((num) => {
 console.log(num);
});
setTimeout(() => {
 console.log(6);
});
Promise.resolve().then(() => {
 console.log(5);
});
console.log(2);
点击查看答案
 1 → 2 → 4 → 3 → 5 → 6

5️⃣

console.log('start');
setTimeout(() => {
 console.log('Timeout1');
}, 1000);
Promise.resolve().then(() => {
 console.log('Promise1');
});
Promise.resolve().then(() => {
 console.log('Promise2');
 setTimeout(() => {
 Promise.resolve().then(() => {
 console.log('Promise3');
 })
 console.log('Timeout2');
 }, 0);
});
console.log('end');
点击查看答案
 start → end → Promise1 → Promise2 → Timeout2 → Promise3 → Timeout1
function app() {
 setTimeout(() => {
 console.log("1-1");3
 Promise.resolve().then(() => {
 console.log("2-1");5
 });
 });
 console.log("1-2"); 1
 Promise.resolve().then(() => {
 console.log("1-3"); 2
 setTimeout(() => {
 console.log("3-1"); 4
 });
 });
}
点击查看答案
 1-2 → 1-3 → 1-1 → 3-1 → 2-1

内存管理

JS有如下数据类型:
原始数据类型:String, Number, Boolean, Null, Undefined, Symbol
引用数据类型:Object
而存放这些数据的内存又可以分为两部分:栈内存(Stack)和堆内存(Heap)。原始数据类型存在栈中,引用类型存在堆中。

栈内存

栈是一种只能一端进出的数据结构,先进后出,后进先出。

堆内存

JS中原始数据类型的内存大小是固定的,由系统自动分配内存。但是引用数据类型,比如Object, Array,他们的大小不是固定的,所以是存在堆内存的。JS不允许直接操作堆内存,我们在操作对象时,操作的实际是对象的引用,而不是实际的对象。可以理解为对象在栈里面存了一个内存地址,这个地址指向了堆里面实际的对象。所以引用类型的值是一个指向堆内存的引用地址。

函数也是引用类型,当我们定义一个函数时,会在堆内存中开辟一块内存空间,将函数体代码以字符串的形式存进去。然后将这块内存的地址赋值给函数名,函数名和引用地址会存在栈上。

垃圾回收

垃圾回收就是找出那些不再继续使用的变量,然后释放其占用的内存,垃圾回收器会按照固定的时间间隔周期性执行这一操作。JS使用垃圾回收机制来自动管理内存,但是他是一把双刃剑:

优势: 可以大幅简化程序的内存管理代码,降低程序员负担,减少因为长时间运行而带来的内存泄漏问题。
劣势:程序员无法掌控内存,JS没有暴露任何关于内存的API,我们无法进行强制垃圾回收,更无法干预内存管理。

引用计数

引用计数是一种回收策略,它跟踪记录每个值被引用的次数,每次引用的时候加一,被释放时减一,如果一个值的引用次数变成0了,就可以将其内存空间回收。
使用引用计数会有一个很严重的问题:循环引用。循环引用指的是对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用。

function problem(){ 
 var objectA = {};
 var objectB = {}; 
 objectA.a = objectB;
 objectB.b = objectA; 
}

在这个例子中,objectA 和 objectB 通过各自的属性相互引用;也就是说,这两个对象的引用次数都是 2。当函数执行完毕后,objectA 和 objectB 还将继续存在,因为它们的引用次数永远不会是 0。

标记—清除算法

算法分为 “标记”和“清除” 两个阶段,最终目的是识别并释放 “不再需要” 的内存。

  1. 标记阶段:区分 “有用” 和 “无用” 的变量
    标记规则:
    垃圾回收器会从根对象(Roots) 开始遍历所有变量(根对象通常是全局对象,如浏览器中的window、Node.js 中的global)。
  • 所有能被根对象直接或间接访问到的变量,标记为 “有用”(处于 “进入环境” 状态,即仍在执行环境中被使用)。
  • 无法被根对象访问到的变量,标记为 “无用”(处于 “离开环境” 状态,即已脱离执行环境,不再被使用)。
  1. 清除逻辑:
    垃圾回收器会遍历内存中所有变量,将标记为 “无用” 的变量占用的内存释放,并将这些内存空间归还给操作系统,供后续使用。

闭包

当一个内部函数引用了外部函数的变量时,就形成了闭包。

闭包的优点/特点

  • 通过闭包可以让外部环境访问到函数内部的局部变量
  • 通过闭包可以让全局变量持续保存下来,不随着它的上下文一起销毁

通过此特性,我们可以解决一个全局变量污染的问题, 早期在 JavaScript 还无法进行模块化的时候,在多人协作时,如果定义过多的全局变量有可能造成全局变量命名冲突,使用闭包来解决功能对变量的调用将变量写到一个独立的空间里面,从而能够一定程度上解决全局变量污染的问题。

闭包经典面试题一

for (var i = 1; i <= 3; i++) {
 setTimeout(function () {
 console.log(i);
 }, 1000);
}

var 的作用域特性:

var 声明的变量没有块级作用域(仅函数作用域或全局作用域),因此整个循环共享同一个 i 变量。

  • 循环执行时:
    三次迭代都会将 setTimeout 放入任务队列,延迟 1 秒执行。
    但每次迭代的回调函数引用的是同一个 i 变量。
  • 定时器触发时:
    由于循环执行速度极快,1 秒后所有定时器触发时,循环早已结束,此时 i 的值已经变为 4(循环终止条件是 i > 3)。
    因此所有回调函数打印的都是 4。

解决方法

for (var i = 1; i <= 3; i++) {
 (function (index) {
 setTimeout(function () {
 console.log(index);
 }, 1000);
 })(i)
}

let 的作用域特性:

let 声明的变量具有块级作用域(每个循环迭代都会创建独立的变量副本)。

  • 循环执行时:
    每次迭代都会创建一个新的 i 变量,并将其值(1、2、3)绑定到对应的定时器回调函数中。
    每个回调函数形成的闭包捕获的是当前迭代的 i 值。
  • 定时器触发时:
    每个回调函数访问的是自己闭包中的 i 副本(分别为 1、2、3),因此依次打印:1 2 3
    闭包面试题

this指向

函数中的this指向取决于函数如何调用,它指向当前函数的运行环境

判断this的唯一依据就是此刻函数的执行由哪个对象调用

  • 如果函数以参数形式传递,则this会指向windows,函数是一个局部变量,找不到他的调用者

原型链

1.原型

在js中,每一个对象(函数也是对象)都有一个特殊的属性叫做原型(prototype),它指向另一个对象,这个对象(Test.prototype)被称为原型对象, 原型对象是用来共享属性和方法的

2.原型对象:

(1)原型对象有一个constructor属性指向构造函数本身(Test)。
(2)原型对象是一个普通的对象,它包含属性和方法。
(3)原型对象的属性和方法会被继承到所有通过原型链与它相连的对象。

function Test(name, age){
 this.name = name
 this.age = age
}
 
Test.prototype.say = function(){
 console.log('我能说话')
}
var obj3 = new Test('Jack', 26)
var obj4 = new Test('Rose', 25)
 
obj3.say() // 我能说话
obj4.say() // 我能说话
console.log(obj3.say === obj4.say) // true
 

构造函数和实例之间就初步构成了这样一个关系,如图:

3.隐式原型__proto__

在js中,每个对象都有一个__proto__ 属性(左右两边两个短下划线),这个__proto__就被称为隐式原型。

console.log(obj3.__proto__ === Test.prototype)
// true

(1)每个js对象都有一个隐藏的原型对象属性__proto__,它指向创建它的构造函数的原型对象(Test.prototype)
(2)proto__存在的意义在于为原型链查找提供方向,原型链查找靠的是__proto,而不是prototype

function Test(name, age){
 this.name = name
 this.age = age
}
 
Test.prototype.say = function(){
 console.log('我能说话')
}
var obj3 = new Test('Jack', 26)
 
 
1, 构造函数是? 实例是?
2, obj3.constructor === Test true or false?
3, obj3.__proto__ === Test ?
4, Test.prototype === obj3.__proto__ ?
5, obj3.__proto__.constructor === Test ?
 
// 1, Test obj3 2,true 3,false 4,true 5,true

4.原型链

Test的原型对象Test.prototype会不会也有一个隐式原型__proto__

Test.prototype.__proto__ === Object.prototype
// true

(1) Test.prototype的隐式原型(proto)就是Object.prototype
(2) 所有的对象,包括构造函数的原型对象,最终都继承自 Object.prototype,这是js原型链的顶点
此时的关系图:

Object.prototype作为原型链的顶端,位于原型链的最末端。因此,它不再有自己的原型,所以Object.prototype.proto 指向null,表示原型链的终点

  • 原型链的终点是null
  • Object.prototype.proto === null

    每个对象都有一个原型(prototype),它指向另外一个对象,而指向的对象又存在属性(proto)指向另外一个对象。当我们访问对象(obj3)的属性时,会先在对象定义的属性中进行查找,没找到就会沿着__proto__一路向上查找,最终形成一个链式结构,这整个链式结构就叫做原型链
    如果在原型链中找到了这个属性,就返回找到的属性值;如果整个原型链都没找到这个属性值,则返回 undefined,没找到方法直接报错
function Test(name, age){
 this.name = name
 this.age = age
}
Test.prototype.say = function(){
 console.log('我能说话')
}
var obj3 = new Test('Jack', 26)
var obj4 = new Test('Rose', 24)
 
1, Test.prototype === ( ) ?
2, obj3.__proto__.__proto__ === ( ) ?
3, obj3.__proto__ === obj4.__proto__ ?
4, Test.prototype.__proto__ === ( ) ?
5, obj4.__proto__.constructor === ( ) ?
6, Object.prototype.__proto__ === ( ) ?
7, obj3.say === obj4.say ?
 
 
// 1, obj3.__proto__ 或 obj4.__proto 2,Object.prototype 3, true (二者都由Test new出来,在原型链上都指向 Test.prototype)
// 4, Object.prototype 5, Test 6, null (终点) 7,true (同问题3)

原型链面试题

作者:Minutiae原文地址:https://www.cnblogs.com/L178/p/18977971/JavaScript

%s 个评论

要回复文章请先登录注册