高频面试题中的Javascript

稀饭2024-03-19interviewjavascript

为什么需要虚拟dom 🏳️‍🌈

  • 减少对真实dom无意义的改动。
  • 抽象一个界面表示方式,消除平台之间的差异,来适应不同应用端。

事件冒泡和事件捕获

事件冒泡(默认)是指在触发某个元素上的特定事件后,该事件将从最内层的元素开始,逐级向上冒泡传播到更外层的祖先元素。

事件捕获(注册事件的第三个参数设置设置为true),与事件冒泡相对应的另一种事件传播机制,事件从最外层的祖先元素开始传播,逐级向下捕获,直到达到触发事件的最内层元素。

js的事件循环

1. 同步任务: 立即执行,先压入调用栈。

2. 异步任务: 被分为宏任务和微任务,分别放入各自的队列。

3. 微任务(Promises.then,Promise.catch,resove,reject,MutationObserver): 会优先执行微任务队列中的所有任务。

4. 宏任务(setTimeout,setInterval,setImmediate): 微任务执行完毕后,事件循环才会执行宏任务队列中的任务。

查看代码 | 详细解析open in new window

常用的设计模式

  • 发布订阅模式是一种消息传递模式,也被称为消息队列模式。在该模式中,发送者(也称为发布者)将消息发布到消息中心(也称为消息代理或者事件通道),然后多个接收者(也称为订阅者)可以从中获取相应的消息。发布者和订阅者不需要知道对方的存在,只需要与消息中心进行交互,如 event bus 的 on 和 emit。
  • 观察者模式当一个对象状态发生改变时,其依赖的其他对象能够自动收到通知并做出相应的响应,如 vue 的 watch 事件。
  • 策略模式根据不同的策略执行不同的方法;将方法的的调用与方法的实现分离开来,使得它们可以相互独立地变化,同时也可以避免出现大量的条件语句。
  • 单例模式它保证某个类只有一个实例,并且提供了一个全局访问点来访问该实例。
  • 模块模式通过使用闭包来实现私有变量和方法,将相关的函数和数据组织在一起。常用于封装模块,避免全局命名空间污染,提供封装和复用性。
  • 工厂模式定义一个创建对象的工厂函数,由子类决定实例化哪个类。
  • 适配器模式用于将一个类的接口转换成另一个接口,以便那些接口不兼容的类可以一起工作;个人理解为高阶组件,对已有功能进行包装加工。

箭头函数和普通函数区别 🏳️‍🌈

  • this指向:箭头函数没有自己的this值,它会继承外层作用域的this值。而普通函数中的this值是在运行时确定的,根据调用方式的不同而有所变化。
  • arguments对象:箭头函数没有自己的arguments对象。在箭头函数中使用arguments会引用外层函数的arguments对象。
  • 构造函数:箭头函数不能用作构造函数,不能使用new关键字实例化对象(因为没有this)。普通函数可以用作构造函数,创建新的对象实例。

查看代码

构造方法和普通方法的区别

构造方法用于初始化对象的状态,而普通方法用于描述对象的行为或功能。

构造方法在对象创建时自动调用,而普通方法可以在任何时候调用。

let、const和var的区别 🏳️‍🌈

  • 作用域

    var是函数作用域,就算是在块级结构(如for循环或if 语句)中声明,它也会提升到函数的最顶部,在赋值之前使用会有一个undefined的初始值。

    letconst是块级作用域,只在声明它们的块(例如for循环、if语句或任何其他类之型的块)内可见,在赋值前使用报错。

  • 可变性varlet声明的变量可以被重新赋值;const声明的变量不可以重新赋值。

  • 重复声明letconst重复声明会报错已声明;var重复声明会覆盖之前的声明变量。

ES6 和 commonjs 的区别

  1. 语法差异:ES6使用import和export语法来实现模块化,而CommonJS使用require和module.exports语法。

  2. 加载方式:ES6模块使用静态加载,模块在编译阶段就确定,使得模块依赖关系更加清晰。而CommonJS模块使用动态加载,模块在运行时才能确定,使得模块的加载是同步的。

  3. 导出方式:ES6模块使用命名导出和默认导出的方式。可以通过export关键字来导出具体的变量、函数或类,也可以使用export default关键字来导出默认的值。而CommonJS模块只支持单一导出,通过module.exports来导出一个对象或者一个函数。

for of 和for in的区别?

for...in 用于遍历对象和数组(数组会迭代索引)及原型的可枚举属性(键),不保证属性的顺序。

for...of 用于遍历数组、字符串、Map、Set等具有迭代器接口对象的值,按照对象的迭代顺序依次访问每个元素。

Set、Map、WeakSet和WeakMap的区别

Set 是一种只能存储唯一值的集合,它不允许重复值,值可以是任何类型。

Map 是一种键值对的集合,其中键可以是任意类型的值。

WeakSet 是一种特殊的集合,不能存储原始类型的值,它只能存储对象引用,并且这些引用是弱引用,不会阻止对象被垃圾回收。


let weakSet = new WeakSet();

let obj1 = { name: "Alice" };
let obj2 = { name: "Bob" };

weakSet.add(obj1);
weakSet.add(obj2);

console.log(weakSet.has(obj1)); // true

obj1 = null; // obj1 置为 null 后,对象可以被垃圾回收

WeakMap 是一种特殊的键值对集合,其中的键是弱引用,而值可以是任意类型的值。

let weakMap = new WeakMap();

let key1 = {};
let key2 = {};

weakMap.set(key1, "value1");
weakMap.set(key2, "value2");

console.log(weakMap.get(key1)); // "value1"

key1 = null; // key1 置为 null 后,键可以被垃圾回收

js垃圾回收

  • 标记清除 进入执行环境的变量都被标记,然后执行完,清除这些标记跟变量。查看变量是否被引用;标记清除算法能够准确地识别不再使用的内存对象,包括循环引用等复杂情况.

  • 引用计数

    引用计数会记录每个值被引用的次数,当引用次数变成0后,就会被释放掉;但由于其无法解决循环引用的问题,通常不被 JavaScript 引擎采用。

  • v8的垃圾回收(分代式垃圾回收策略) 根据存活周期分为新生代和老生代,存活周期很短,经过一次垃圾回收后,就被释放回收掉为新生代;存活周期很长,经过多次垃圾回收后内存仍存在为老生代; 新生代基于标记-复制回收,通过标记活动对象复制到空闲区,清除垃圾对象; 老生代基于标记-清除和标记-整理回收,标记-清除算法用于标记并清除不再使用的对象,而标记-整理算法则在清除对象后进行内存整理,消除内存碎片;

什么是泛型

它允许在编写代码时使用未知类型来定义类、接口和方法,使用类型参数(Type Parameters)来代表未知类型。这些类型参数在使用时被实际的类型替换,从而实现了对不同类型的通用支持

class Box<T> {
  private value: T;
  constructor(value: T) {
    this.value = value;
  }
  getValue(): T {
    return this.value;
  }
}
let box1 = new Box<string>("Hello");
let box2 = new Box<number>(123);

interface 和 type 的区别

可扩展:interface可以通过implements来实现方法,通过extends关键字进行扩展,type可使用交叉类型&合并不同的type或interface,并且可被推导,因为type的类型是明确的。

type Person = {
  name: string;
};

interface apple {
    name:string
}

// 通过 extends 关键字进行扩展
interface appleInfo extends apple {
    year:number
}
const aApple:appleInfo = {
    year:50,
    name:'apple'
}

// 使用交叉类型&合并不同的type或interface
type Employee = Person & {
  age: number;
  salary: number;
};

const employee: Employee = {
  name: "Alice",
  age: 30,
  salary: 5000,
};

定义数据类型:interface一般用于对象的描述;type声明可以声明任何类型,除了对象还可以声明基本类型别名,联合类型,元组等类型。 重复声明:interface在定义了两个相同接口会合并。Type 有多个声明会报错。

总结来说,interface用于声明对象结构;type描述基本类型的数据及类型的关系扩展

interface Point { x: number, y: number }

type Point3D =Point & { z: number }

type status = 'success' | 'fail'

new关键字做了什么

function _new(constructor, ...arg) {
  // 创建一个空对象
  var obj = {};
  // 空对象的`__proto__`指向构造函数的`prototype`, 为这个新对象添加属性
  obj.__proto__ = constructor.prototype;
  // 构造函数的作用域赋给新对象
  var res = constructor.apply(obj, arg);
  // 返回新对象.如果没有显式return语句,则返回this
  return Object.prototype.toString.call(res) === '[object Object]' ? res : obj;
}

说下原型和原型链

每个js对象(除了null、undefined及Object.create(null))都有一个原型对象,当你访问一个对象的属性时,JavaScript 引擎首先会查找对象自身的属性,如果找不到,就会查找对象的原型,沿着原型链向上查找,直到找到所需属性或到达原型链的末尾(null)。如果仍然找不到,则返回undefined

如何不让某个原型属性被继承

  1. 设置同名属性,会优先使用本身的属性,不会再去找原型上属性;
  2. 使用Object.defineProperty()设置enumerable(是否可枚举)为false或configurable(是否可修改配置)设置为false

_proto_和prototype的区别

proto:这是一个对象的内部属性,指向该对象的原型,也就是它继承的属性和方法的来源。在ES6之前,可以通过Object.prototype的__proto__属性访问或设置一个对象的原型,但这种做法已经不推荐使用,因为__proto__属性在ES6中已被废弃。

prototype:这是构造函数的一个属性,它是一个对象,包含了由该构造函数创建的所有实例共享的方法和属性。当你创建一个新实例时,这个实例的_proto_属性会指向这个prototype对象。