前端八股文自救指南——JS——Day3

122 阅读10分钟

JS

async & await

async/await其实是Generator的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。通过async关键字声明一个异步函数, await 用于等待一个异步方法执行完成,并且会阻塞执行async 函数返回的是一个 Promise 对象,如果在函数中 return 一个变量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。如果没有返回值,返回 Promise.resolve(undefined)

  • 相较于Promise代码可读性高,Promise虽然摆脱了回调地狱,但自身的链式调用会影响可读性。
  • 相对Promise更优雅,传值更方便。
  • 对错误处理友好,可以通过try/catch捕获,Promise的错误捕获⾮常冗余

ES6

1. 块级作用域
  • let 和 constlet 和 const 用于声明变量,它们具有块级作用域,只在声明它们的块内有效。const 声明常量,一旦赋值就不能再重新赋值(对于引用类型,其内部属性可以修改)。
2. 箭头函数
  • 简洁语法:箭头函数提供了更简洁的函数定义方式,适合简单的函数逻辑。它没有自己的 thisargumentssuper 或 new.targetthis 值继承自外层函数。
3. 模板字符串
  • 字符串插值:使用反引号(`)定义模板字符串,可以在字符串中嵌入表达式,使用 ${} 语法。
4. 解构赋值
  • 数组解构:可以从数组中提取值并赋值给变量。

  • 对象解构:可以从对象中提取属性并赋值给变量。

5. 默认参数
  • 函数参数默认值:在函数定义时可以为参数设置默认值,当调用函数时没有提供该参数,就会使用默认值。
6. 扩展运算符
  • 数组扩展:可以将数组展开为多个元素,常用于数组的合并、复制等操作。

  • 对象扩展:可以将对象的属性展开,常用于对象的合并、复制等操作。

7. 类和继承
  • 类定义:引入了 class 关键字,用于定义类,使得 JavaScript 有了更接近传统面向对象语言的类语法。

  • 继承:使用 extends 关键字实现类的继承。

8. Promise 对象
  • 异步处理Promise 用于处理异步操作,避免回调地狱,使异步代码更易读和维护。它有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。
9. 模块化
  • import 和 export:引入了模块系统,使用 export 关键字导出模块中的变量、函数或类,使用 import 关键字导入其他模块的内容。
10. 迭代器和生成器
  • 迭代器:允许自定义对象的迭代行为,通过实现 Symbol.iterator 方法。
  • 生成器:使用 function* 定义生成器函数,通过 yield 关键字暂停和恢复函数的执行。

原型

  • prototype : js通过构造函数来创建对象,每个构造函数内部都会一个原型prototype属性,它指向另外一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。
  • proto: 当使用构造函数创建一个实例对象后,可以通过__proto__访问到prototype属性。
  • constructor:实例对象通过这个属性可以访问到构造函数

原型链

原型和原型链的基本概念

在 JavaScript 中,每个函数都有一个 prototype 属性,这个属性指向一个对象,这个对象包含了可以由该函数的所有实例共享的属性和方法。这个对象就是我们所说的原型对象。每个对象都有一个内部的  __proto__  属性,指向创建该对象的函数的原型对象。这就形成了一个链式的结构,即原型链。

当我们尝试访问一个对象的属性时,如果这个对象本身没有这个属性,那么 JavaScript 会沿着这个对象的原型链向上查找,直到找到这个属性或者到达原型链的末端。

原型链的图解

b0909151cdfb4428afdb705241e89a4f.png

原型链可以通过图解的方式更直观地理解。在原型链的图解中,我们可以看到:

  • 每个构造函数都有一个 prototype 属性,它指向一个原型对象。
  • 每个实例对象都有一个  __proto__  属性,它指向创建该实例对象的构造函数的原型对象。
  • 原型对象本身也是一个对象,它的  __proto__  属性指向 Object.prototype
  • Object.prototype 的  __proto__  属性是 null,表示原型链的末端。

原型链继承

利用Object.create()方法,将子类的原型指向父类,实现继承父类的方法属性,修改时也不影响父类。

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'green', 'blue'];
}
Parent.prototype.sayName = function() {
  console.log(this.name);
};
function Child(name, age) {
	// 执行父类构造函数
  Parent.call(this, name);
  this.age = age;
}
// 将子类的原型  指向父类
Child.prototype = Object.create(Parent.prototype);
// 此时的狗早函数为父类的 需要指回自己
Child.prototype.constructor = Child;

Child.prototype.sayAge = function() {
  console.log(this.age);
};
var child1 = new Child('Tom', 18);
child1.sayName(); // 'Tom'
child1.sayAge(); // 18

闭包

闭包是指有权访问另一个函数作用域中的变量的函数。简单来说,即使该函数已经执行完毕,其作用域内的变量也不会被销毁,而是会被闭包所引用。

原理

在 JavaScript 中,每个函数都会形成一个自己的作用域。当一个函数内部定义了另一个函数时,内部函数会形成一个闭包,它可以访问外部函数的变量和参数。这是因为 JavaScript 的作用域链机制,内部函数的作用域链包含了外部函数的作用域,所以即使外部函数执行结束,其作用域内的变量也不会被垃圾回收机制回收,而是会被闭包保留下来。

作用
  • 读取函数内部的变量:通过闭包可以在函数外部访问函数内部的变量。
  • 让这些变量的值始终保持在内存中:闭包可以让函数内部的变量的值始终保持在内存中,不会因为函数执行结束而被销毁,从而实现数据的持久化。
缺点
  • 创建的变量不能被回收,容易消耗内存,使用不当会导致内存溢出

  • 解决:  在不需要使用的时候把变量设为null

call( ) & bind( ) & apply( )

  • 三者都可以用作改变this指向
  • callapply的区别在于传参,callbind都是传入对象。apply传入一个数组。
  • callapply改变this指向后会立即执行函数,bind在改变this后返回一个函数,不会立即执行函数,需要手动调用。(注意:连续调用bind时,this上下文由首个bind的参数决定)

浏览器垃圾回收机制

浏览器垃圾回收机制(Garbage Collection,简称 GC)是浏览器自动管理内存的一种机制,其目的是识别并回收不再使用的内存空间,以避免内存泄漏和提高内存使用效率。以下为你详细介绍浏览器垃圾回收机制的相关内容。

内存生命周期

在了解垃圾回收机制之前,需要先了解内存的生命周期,主要分为三个阶段:

  1. 分配内存:当声明变量、函数、对象等时,浏览器会为它们分配内存。
  2. 使用内存:即读写内存,也就是使用变量、函数等。
  3. 释放内存:当这些变量、函数等不再被使用时,需要释放它们所占用的内存,以便内存可以被重新利用。垃圾回收机制主要负责这一阶段。
常见的垃圾回收算法
标记清除算法(Mark and Sweep)
  • 原理

    • 标记阶段:从根对象(如全局对象 window)开始,遍历所有可以访问到的对象,并将这些对象标记为 “活动对象”。
    • 清除阶段:遍历整个内存空间,将未被标记的对象(即不可达对象)视为垃圾,并回收它们所占用的内存空间。
  • 示例

let obj1 = { name: 'object1' };
let obj2 = { name: 'object2' };
obj1.ref = obj2; 
obj2.ref = obj1; 

// 现在将 obj1 和 obj2 的引用都置为 null
obj1 = null;
obj2 = null;

在上述代码中,原本 obj1 和 obj2 相互引用,但当将它们的引用都置为 null 后,从根对象无法访问到这两个对象,在标记清除算法的下一次执行时,它们将被标记为不可达对象并被清除。

  • 缺点:标记清除算法会产生内存碎片,即回收后的内存空间会被分割成许多不连续的小块,当需要分配较大的连续内存空间时,可能会因为找不到足够大的连续空间而导致内存分配失败。
标记整理算法(Mark and Compact)
  • 原理

    • 标记阶段:与标记清除算法相同,从根对象开始遍历,标记所有活动对象。
    • 整理阶段:将所有活动对象向内存的一端移动,使它们连续存储,然后清除掉边界之外的内存空间。
  • 优点:解决了标记清除算法产生内存碎片的问题,使得内存空间更加连续,有利于后续的内存分配。

  • 缺点:整理阶段需要移动对象,会带来一定的性能开销。

引用计数算法(Reference Counting)
  • 原理:每个对象都有一个引用计数,当有一个新的引用指向该对象时,引用计数加 1;当引用被移除时,引用计数减 1。当引用计数为 0 时,该对象被视为垃圾,可以被回收。

  • 示例

let obj = { name: 'example' }; 
let anotherObj = obj; 

// 移除引用
obj = null;
anotherObj = null; 

在上述代码中,最初 obj 和 anotherObj 都引用同一个对象,该对象的引用计数为 2。当将 obj 和 anotherObj 都置为 null 后,对象的引用计数变为 0,此时该对象可以被回收。

  • 缺点:无法处理循环引用的问题。例如,两个对象相互引用,即使它们在其他地方没有引用,它们的引用计数也不会为 0,从而导致内存泄漏。
let obj1 = {};
let obj2 = {};
obj1.ref = obj2;
obj2.ref = obj1;

在这个例子中,obj1 和 obj2 相互引用,它们的引用计数都为 1,即使它们在其他地方没有引用,引用计数算法也无法回收它们。

现代浏览器的垃圾回收策略

现代浏览器通常会结合多种垃圾回收算法,根据不同的情况选择合适的算法。例如,对于新生代(新创建的对象)和老生代(存活时间较长的对象)采用不同的回收策略:

  • 新生代(副垃圾回收器):对象通常存活时间较短,使用标记清除算法或复制算法(将内存分为两个相等的区域,每次只使用其中一个,当该区域满时,将活动对象复制到另一个区域,然后清空当前区域)进行回收,效率较高。
  • 老生代(主垃圾回收器):对象存活时间较长,使用标记清除算法和标记整理算法相结合的方式进行回收,以平衡内存碎片和性能开销。
哪些情况会导致内存泄漏
  • 意外的全局变量:由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。

  • 被遗忘的计时器或回调函数:设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。

  • 脱离 DOM 的引用:获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。

  • 闭包:不合理的使用闭包,从而导致某些变量一直被留在内存当中。

浏览器垃圾回收机制是一个复杂的系统,其目的是在保证内存使用效率的同时,尽可能减少对程序性能的影响。开发者在编写代码时,也应该注意避免不必要的内存占用和内存泄漏问题。