跳到主要内容

JS 基础知识

关于 JS 的一些总结, 持续更新中……

基础知识

defer 与 async 的区别

按照惯例, 所有  script  元素都应该放在页面的  head  元素中。这种做法的目的就是把所有外部文件(CSS  文件和  JavaScript  文件)的引用都放在相同的地方。可是, 在文档的  head  元素中包含所有  JavaScript  文件, 意味着必须等到全部  JavaScript  代码都被下载、解析和执行完成以后, 才能开始呈现页面的内容(浏览器在遇到  body  标签时才开始呈现内容)。

对于那些需要很多  JavaScript  代码的页面来说, 这无疑会导致浏览器在呈现页面时出现明显的延迟, 而延迟期间的浏览器窗口中将是一片空白。为了避免这个问题, 现在  Web  应用程序一般都全部 JavaScript  引用放在  body  元素中页面的内容后面。这样一来, 在解析包含的  JavaScript  代码之前, 页面的内容将完全呈现在浏览器中。而用户也会因为浏览器窗口显示空白页面的时间缩短而感到打开页面的速度加快了。 有了  defer  和  async  后, 这种局面得到了改善。

defer (延迟脚本) 延迟脚本:defer  属性只适用于外部脚本文件。 如果给  script  标签定义了 defer  属性, 这个属性的作用是表明脚本在执行时不会影响页面的构造。也就是说, 脚本会被延迟到整个页面都解析完毕后再运行。因此, 如果  script  元素中设置了  defer  属性, 相当于告诉浏览器立即下载, 但延迟执行。

async(异步脚本) 异步脚本:async  属性也只适用于外部脚本文件, 并告诉浏览器立即下载文件。 但与  defer  不同的是:标记为  async  的脚本并不保证按照指定它们的先后顺序执行。 所以总结起来, 两者之间最大的差异就是在于脚本下载完之后何时执行, 显然  defer  是最接近我们对于应用脚本加载和执行的要求的。

defer  是立即下载但延迟执行, 加载后续文档元素的过程将和脚本的加载并行进行(异步), 但是脚本的执行要在所有元素解析完成之后, DOMContentLoaded  事件触发之前完成。async  是立即下载并执行, 加载和渲染后续文档元素的过程将和  js  脚本的加载与执行并行进行(异步)。

什么是闭包?闭包有什么作用?

一个函数和对其周围状态(lexical environment, 词法环境)的引用捆绑在一起(或者说函数被引用包围), 这样的组合就是闭包(closure)。也就是说, 闭包让你可以在一个内层函数中访问到其外层函数的作用域。在  JavaScript  中, 每当创建一个函数, 闭包就会在函数创建的同时被创建出来。

闭包的用处:

  • 匿名自执行函数
  • 结果缓存
  • 封装
  • 实现类和继承

事件委托以及冒泡原理

事件委托, 又被称之为事件代理。在  JavaScript  中, 添加到页面上的事件处理程序数量将直接关系到页面整体的运行性能。导致这一问题的原因是多方面的。 首先, 每个函数都是对象, 都会占用内存。内存中的对象越多, 性能就越差。其次, 必须事先指定所有事件处理程序而导致的  DOM  访问次数, 会延迟整个页面的交互就绪时间。 对事件处理程序过多问题的解决方案就是事件委托。 事件委托利用了事件冒泡, 只指定一个事件处理程序, 就可以管理某一类型的所有事件。例如, click  事件会一直冒泡到  document  层次。也就是说, 我们可以为整个页面指定一个  onclick  事件处理程序, 而不必给每个可单击的元素分别添加事件处理程序。 事件冒泡(event bubbling), 是指事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收, 然后逐级向上传播到较为不具体的节点(文档)。

作用域, 作用域链

  • 作用域: 规定了如何查找变量, 也就是确定当前执行代码对变量的访问权限。换句话说, 作用域决定了代码区块中变量和其他资源的可见性。(全局作用域、函数作用域、块级作用域)
  • 作用域链: 从当前作用域开始一层层往上找某个变量, 如果找到全局作用域还没找到, 就放弃寻找 。这种层级关系就是作用域链。(由多个执行上下文的变量对象构成的链表就叫做作用域链, 学习下面的内容之后再考虑这句话)

bind, call, apply

这三个方法, 都是为了操作 this, 改变 this 指向, 那么问题来了, 剪头函数中可以使用他们吗?

可以使用, 但是并没有作用, 箭头函数中的 this 和函数的 this 有本质区别, 箭头函数中的 this 是函数外最近的那个 this, 因此, apply、call 方法其实并不会起作用。

  • bind
fn.bind(obj, param1, param2, ...)

bind 方法, 是创建一个新的函数, 调用他, 第一个参数用来提供设置 this 的值, 如果不提供该值则 this 指向 window, 返回的也是一个函数, 只有执行该函数, 才会使得 this 指向发生改变, 这点是个 callapply的区别。例子:

function addFun(a, b) {
return a + b;
}

function subFun(a, b) {
return a - b;
}

addFun.bind(subFun, 3, 7); // 这时, 并不会返回 10
addFun.bind(subFun, 3, 7)(); // 调用后, 返回 10
  • call
fn.call(obj, param1, param2, ...)

call 方法

  • apply
fn.apply(obj, [param1,param2,...])

调用 callapply 的对象, 必须是一个函数 Function, 它们的区别, 主要体现在参数的写法上, 前者从第二个参数开始可以传递 n 个参数, 后者只能传递两个参数, 不过第二个参数为一个参数的数组。

函数声明和函数表达式的区别

直接上代码

// 函数声明式
function greeting(){
console.log("hello world");  
}

// 函数表达式
const greeting = function() {
console.log("hello world");
}

// 剪头函数
const greeting = () => {
console.log("hello world");
}

主要区别:

  • 以函数声明的方法定义的函数,函数名是必须的,而函数表达式的函数名是可选的。(函数声明整体会被提升到当前作用域的顶部, 函数表达式也提升到顶部但是只有其变量名提升)
  • 以函数声明的方法定义的函数,函数可以在函数声明之前调用,而函数表达式的函数只能在声明之后调用。
  • 以函数声明的方法定义的函数并不是真正的声明, 它们仅仅可以出现在全局中, 或者嵌套在其他的函数中, 但是它们不能出现在循环, 条件或者 try/catch/finally 中, 而函数表达式可以在任何地方声明。换句话说, 函数声明不是一个完整的语句, 所以不能出现在 if-else, for循环, finally, try catch 语句以及 with 语句中。

ES6 箭头函数的特性

1、更简洁的语法, 例如

  • 只有一个形参就不需要用括号括起来
  • 如果函数体只有一行, 就不需要放到一个块中
  • 如果  return  语句是函数体内唯一的语句, 就不需要  return  关键字

2、箭头函数没有自己的  this, arguments, super

3、箭头函数  this  只会从自己的作用域链的上一层继承  this。

ES6 中的 Set 和 Map

  • Set 类数组, 但是他的成员都是唯一的, 没有重复的值, 常用于 数组去重, 对象属性去重等

    • size 属性, 获取成员总数,
    • add 添加成员, 返回 set 本身
    • delete 删除成员, 返回 boolean 表示是否成功
    • has 是否存在成员 返回 boolean
    • clear 清除成员, 没有返回值
    • keys() values() entries() 得到 set 没有 key, 或者说键名就是值, 所以 keys 得到的还是值, 等同于 values, entries 得到的是键值对
  • WeakSet 结构与 Set 类似, 也是不重复的值的集合。但是, 它与 Set 有两个区别。

    • WeakSet 的成员只能是对象, 而不能是其他类型的值
    • WeakSet 中的对象都是弱引用
      • 垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。也正因为如此, 由于垃圾回收的不确定性, 可能导致运行前后成员个数不一致, 所以 WeakSet 不能进行遍历。
      • 没有 Set 的 size 方法, 原因如上。
  • Map 对象, 本质是键值对的集合, 方法有 set, get, has, delete, size, clear, keys, values, entries, 等

  • WeakMap 对象, 结构与 Map 结构类似,也是用于生成键值对的集合。

    • WeakMap 只接受对象作为键名(null 除外),不接受其他类型的值作为键名。
    • WeakMap 的键名所指向的对象,不计入垃圾回收机制。

ES6 Promise

Promise 对象代表一个异步操作, 有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果, 可以决定当前是哪一种状态, 任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来, 它的英语意思就是“承诺”, 表示其他手段无法改变。 一旦状态改变, 就不会再变, 任何时候都可以得到这个结果。Promise 对象的状态改变, 只有两种可能:从pending变为fulfilled和从pending 变为 rejected。只要这两种情况发生, 状态就凝固了, 不会再变了, 会一直保持这个结果, 这时就称为 resolved(已定型)。如果改变已经发生了, 你再对 Promise 对象添加回调函数, 也会立即得到这个结果。这与事件(Event)完全不同, 事件的特点是, 如果你错过了它, 再去监听, 是得不到结果的。 缺点:

  • 首先, 无法取消Promise, 一旦新建它就会立即执行, 无法中途取消。
  • 其次, 如果不设置回调函数, Promise 内部抛出的错误, 不会反应到外部。
  • 第三, 当处于 pending 状态时, 无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

ES6 新增的数组方法, 对象方法

  • 对象方法:
Object.keys();
Object.values();
Object.entries();
Object.fromEntries();
is(); // 判断书否相等(全等)
assign(); // 对象合并, 如果存在则覆盖原值
  • 数组方法:
array.keys(), array.values(), array.entries(); // 分别是 数组key, 值和 key和值
find(),
findIndex(), // 查找
from(), // 类数组对象转为数组
of(), // 将一组值, 转换为数组
fill(); // 填充数组
include(); // 是否包含返回 true或false
flat(); // 抹平数组, 参数为Infinity 则可将多维数组抹平为一维数组
flatMap(); // 方法对原数组的每个成员执行一个函数, 返回新数组, 不改变原数组

Map 、 WeakMap 和 普通对象的区别

  • 键的类型:
    • Map 的键可以是任何类型, WeakMap 的键必须是对象类型, 而 Object 的键必须是字符串或 Symbol 类型。
  • 垃圾回收:
    • Map 中的键是强引用, 即使键对象没有其他引用, 也不会被垃圾回收。
    • 而 WeakMap 中的键是弱引用, 当键对象没有其他引用时, 可能会被垃圾回收。
    • Object 中的属性是强引用, 即使没有其他引用, 也不会被垃圾回收。
  • 可枚举性:
    • Object 的属性是可枚举的, 可以使用 for-in 或 Object.keys()等方法来遍历属性。
    • 而 Map 和 WeakMap 中的键是不可枚举的。
  • 方法和操作
    • Object 具有一些特定于对象的方法和操作, 例如 Object.keys()和 Object.values()等。
    • Map 和 WeakMap 提供了一些特定于映射的方法和操作, 例如 Map.has()和 WeakMap.delete()等。
  • 继承
    • Object 具有原型继承, 即属性可以从原型链中继承。
    • 而 Map 和 WeakMap 不具有原型继承, 它们是独立的数据结构。

IntersectionObserver

IntersectionObserver 可以用于检测元素是否进入视口,可以用于实现无限滚动、懒加载等功能。

const myObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
console.log(`${entry.target.id} is now visible`);
observer.unobserve(entry.target);
}
});
});
const myElement = document.getElementById("myElement");
myObserver.observe(myElement);
信息

使用场景:在 Web 应用中,可能需要实现无限滚动、懒加载等功能,使用 IntersectionObserver 可以方便地实现这些功能。

设计模式

经常听到一句话, '写代码良好的封装,就要高内聚,低耦合' 什么是高内聚,低耦合? 即五大基本原则(SOLID)的简写:

  • S: 聚合单一功能,即为单一功能原则。
  • O: 内部修改关闭,外部扩展开放,即为开放封闭原则。
  • L: 合成复用,子类继承可替换父类,即为里式替换原则
  • I: 耦合多个接口,不如独立拆分,即为接口隔离原则。
  • D: 高层模块不依赖底层模块,即为依赖反转原则。

什么是设计模式呢? 其实就是 在软件设计、开发过程中,针对特定场景、特定问题的较优解决方案。

单例模式

单例模式: 一个类只有一个实例,并提供一个访问他的全局访问点。

class Singleton {
let _instance = null;
static getInstance() {
if (!Singleton._instance) {
Singleton.instance = new Singleton()
}
// 如果这个唯一的实例已经存在,则直接返回
return Singleton._instance
}
}

const s1 = Singleton.getInstance()
const s2 = Singleton.getInstance()

console.log(s1 === s2) // true

优缺点:

  • 优点
    • 节约资源,保证访问的一致性
  • 缺点
    • 扩展性不友好,因为单例模式一般自行实例化,没有接口

使用场景

  • 如果一个类实例化过程消耗资源比较多,可以使用单例避免性能浪费
  • 需要公共状态,可以使用单例保证访问一致性

常见例子

  1. Vuex 中的全局唯一的 store 就是单例模式,声明一个类,类中提供一个 store

工厂模式

工厂模式: 根据不同的参数,返回不同类的实例 将对象的创建与对象的实现分离。实现复杂,但使用简单。工厂会给我们提供一个工厂方法,我们直接去调用即可。 主要组成:

  • 访问者:访问工厂方法
  • 工厂:负责返回实例
  • 产品:访问者从工厂拿到的实例
class Product {
constructor(name) {
this.name = name;
}
init() {
console.log("init");
}
fn1() {
console.log("fn1");
}
fn2() {
console.log("fn2");
}
}

class Creator {
create(name) {
return new Product(name);
}
}

let create = new Creator();
let p = create.create("p");
p.init();
p.fn1();
p.fn2();

优缺点:

  • 优点:
    • 良好的封装,访问者无需了解创建过程,代码结构清晰
    • 扩展性良好,通过工厂方法隔离了用户和创建流程,符合开闭原则
    • 解耦了高层逻辑和底层产品类,符合最少知识原则,不需要的就不要去交流
  • 缺点: 给系统增加了抽象性,带来了额外的系统复杂度,不能滥用。

使用场景

  • 对象创建比较复杂,访问者无需了解创建过程。
  • 需要处理大量具有相同/类似属性的小对象

常见例子

  1. Vue-Routre 使用工厂模式来获得响应路由控制类的实例,然后 this.history 中保存实例
  2. React 中使用 document.createElement 创建 DOM, 只需要传递标签名,就会返回 VNode 元素

适配器模式

适配器模式:用于解决兼容问题,接口/方法/数据不兼容,将其转换成访问者期望的格式进行使用。

// 常数据处理,传入指定的数据然后按照指定规则输出我们期待得到的数据格式
const arr = [
{
type: "男装",
quantity: 500,
},
{
type: "女装",
quantity: 600,
},
{
type: "鞋子",
quantity: 550,
},
];
const xAxisAdapter = (arr) => {
return arr.map((item) => item.type);
};
const yAxisAdapter = (arr) => {
return arr.map((item) => item.quantity);
};

优缺点:

  • 优点: 可以使原有逻辑得到更好的复用,有助于避免大规模改写现有代码;
  • 缺点: 会让系统变得零乱,明明调用 A,却被适配到了 B,如果滥用,那么对可阅读性不太友好

使用场景

  • 同时存在多种格式,旧有接口格式不满足现在需要
  • 增加适配器可以更好使用旧接口
  • 想要使用一个已经存在的对象,但是接口不满足需求,那么可以使用适配器模式转换成你需要的接口
  • 想要创建一个可以复用的对象,而且确定需要和一些不兼容的对象一起工作,这种情况可以使用适配器模式。

常见例子

  1. 封装公共请求,兼容 get 和 post,根据参数判断使用哪种请求
  2. vue 的 computed 处理数据
  3. 兼容性处理

装饰器模式

装饰器模式: 在不改变原对象的基础上,增加新属性/方法/功能,通常用于解决继承关系过于复杂或不适用的情况。

// 简单的数据缓存
class DataService {
fetchData() {
console.log("Fetching data from server...");
return [1, 2, 3];
}
}

class DataCacheDecorator {
constructor(dataService) {
this.dataService = dataService;
this.cache = null;
}

fetchData() {
if (this.cache === null) {
console.log("Cache not exist...");
this.cache = this.dataService.fetchData();
} else {
console.log("Data retrieved from cache");
}
return this.cache;
}
}

let dataService = new DataService();
dataService = new DataCacheDecorator(dataService);

console.log(dataService.fetchData());
console.log(dataService.fetchData());
// 输出
// Cache not exist...
// Fetching data from server...
// [1, 2, 3]
// Data retrieved from cache
// [1, 2, 3]

优缺点:

  • 优点: 对象的核心职责和装饰功能区分开,可以通过动态增删装饰去除目标对象中的装饰逻辑
  • 缺点: 如果多层多个装饰器,可能不同的装饰器在不同的文件中,需要一步步去查找来源, 阅读起来会有些麻烦。

使用场景

  • JS/TS ES7 中的 装饰器模式,使用了特定的 @装饰器名称 来使用,可读性提升
  • 动态地扩展对象的功能

常见例子

  • 实现缓存逻辑
  • 权限控制
  • 日志记录

策略模式

策略模式: 定义一系列计算方法,根据输入的参数决定使用哪个方法。

// 计算方法的实现
const discountMap = {
"discount200-20": function (price) {
return price - Math.floor(price / 200) * 20;
},
"discount300-50": function (price) {
return price - Math.floor(price / 300) * 50;
},
"discount500-100": function (price) {
return price - Math.floor(price / 500) * 100;
},
};

// 计算方法的使用
function priceCalculate(discountType, price) {
return discountMap[discountType] && discountMap[discountType](price);
}

优缺点:

  • 优点
    • 策略相互独立,可以互相切换。提高了灵活性以及复用性
    • 不需要使用 if-else 进行策略选择,提高了维护性。
    • 可扩展性好,满足开闭原则
  • 缺点
    • 策略相互独立,一些复杂的算法逻辑无法共享,造成资源浪费
    • 用户在使用策略时,需要了解具体的策略实现。不满足最少知识原则,增加了使用成本。

使用场景

  • 算法需要自由切换的场景。
  • 需要多重判断,可以考虑策略模式规避多重条件判断。

常见例子

  • 表单校验:执行校验规则和校验规则配置分开;
  • 前端动画类:将渲染动画、动画配置以及动画控制分开

代理模式

代理模式:为其他对象提供一种代理以控制对这个对象的访问。 常用的代理模式:

  • 虚拟代理: 图片懒加载,
  • 缓存代理: 缓存异步接口返回数据
  • 保护代理: 下面的例子
class Car {
drive() {
return "driving";
}
}

class CarProxy {
constructor(driver) {
this.driver = driver;
}
drive() {
// 保护代理,仅18岁才能开车
return this.driver.age < 18 ? "too young to drive" : new Car().drive();
}
}

观察者模式

观察者模式: 主要包括一个 subject 对象和一个 observer 对象,也就是观察者和被观察者的关系,将有关状态的改变通知给观察者。

提示

观察者必须订阅内容改变的事件,定义一对多的依赖关系

// 观察者模式 被观察者Subject 观察者Observer Subject变化 notify观察者
let observerIds = 0; // 观察者id
let observedIds = 0; // 被观察者id

// 被观察者Subject
class Subject {
constructor() {
this.observers = [];
this.id = observedIds++;
}

// 添加观察者
addObserver(observer) {
this.observers.push(observer);
}

// 移除观察者
removeObserver(observer) {
this.observers = this.observers.filter((obs) => {
return obs.id !== observer.id;
});
}

// 通知notify观察者
notify() {
this.observers.forEach((observer) => observer.update(this));
}
}

// 观察者Observer
class Observer {
constructor() {
this.id = observerIds++;
}

update(subject) {
// 更新
console.log("观察者" + this.id + `-检测到被观察者${subject.id}发生了变化`);
}
}

let mObserved=new Observer();
let mObserver1=new Observer();
let mObserver2=new Observer();

mObserved.addObserver(mObserver1);
mObserved.addObserver(mObserver2);

mObserved.notify();
// 输出
// 观察者0-检测到被观察者0变化
// 观察者1-检测到被观察者0变化

优缺点:

  • 优点: 目标变化就会通知观察者。
  • 缺点: 目标和观察者耦合在一起,两者必须同时存在才能达到效果。

使用场景

  • DOM 事件监听

发布订阅模式

发布订阅模式: 基于一个主题/事件通道,希望接收通知的对象(称为subscriber)通过自定义事件订阅主题,被激活事件的对象(称为publisher)通过发布主题事件的方式被通知。

信息

相较于观察者模式,多了调度中心的机制,由调度中心发送消息给订阅者

class Event {
constructor() {
this.eventEmitter = {};
}

// 订阅
on(type, fn) {
if (!this.eventEmitter[type]) {
this.eventEmitter[type] = [];
}
this.eventEmitter[type].push(fn);
}

// 取消订阅
off(type, fn) {
if (!this.eventEmitter[type]) {
return;
}
this.eventEmitter[type] = this.eventEmitter[type].filter((event) => {
return event !== fn;
});
}

// 发布
emit(type) {
if (!this.eventEmitter[type]) {
return;
}
this.eventEmitter[type].forEach((event) => {
event();
});
}
}

优缺点:

  • 优点
    • 注册的订阅行为由发布者决定何时调用,订阅者无需持续关注,由发布者负责通知
    • 发布者无需知道消息的接受者,只需遍历订阅该消息类型的订阅者发送消息,解耦了发布者和订阅者之间的联系,互不持有,都依赖于抽象。
  • 缺点
    • 创建订阅者需要一定的时间和内存。
    • 可以理解为复杂化的观察者模式:弱化了联系,难以维护调用关系,增加了理解成本

使用场景

  • 依赖模块不稳定、依赖关系不稳定
  • 各模块相互独立

常见例子

  • DOM 事件监听(严格来讲实际是发布订阅模式)
  • 数据绑定(Vue)
  • Promise 对象