【前端面经】JS篇(持续更新补全~)

72 阅读21分钟

JS设计模式

单例模式、原型模式、观察者模式/订阅者模式等;

原型和原型链

原型和原型链的理解

原型:对象的基本模型,用于开发程序的部分结构。在JS中,每个对象都有一个与之相关的原型对象,可以从中继承属性和方法;E获取对象原型:Object.getPrototypeOf()

原型链:是JS中对象继承机制中的一部分,允许对象通过其原型对象访问属性和方法。原型链是一个链式结构,其中每个对象都可链接到其原型对象(通过__proto__属性实现),从而形成一个从对象到Object.prototype的继承链;

重写、修改原型

重写和修改对象的原型(prototype),用于扩展对象的功能或修改其默认行为。但是一般情况建议修改自己定义的对象原型哦~

原型链指向

由于Object是构造函数,所以原型链的终点的Object.prototype.__proto__,而Object.prototype.__proto__ ===null输出true,即原型链的终点是null。

如何获取对象非原型链上的属性

hasOwnProperty():判断属性是否属于原型链的属性

JS数据类型

数据类型种类

8种数据类型:Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt;

  • Symbol:创建后独一无二的数据类型;
  • BigInt:数字类型数据,可以表示任意精度格式的整数;

分类

  • 基本数据类型:栈存储;
  • 引用数据类型:堆存储;

类型检测

  • typeof:数组、对象、null都会判断为object
  • instanceof:测试一个对象在其原型链中是否存在一个构造函数的prototype属性,只能正确判断引用数据类型。([] instanceof Array);
  • constructor:用于判断数据类型;对象实例可以根据constructor访问它的构造函数; -Object.prototype.toString.call():使用Object对象的原型方法toString来判断数据类型;

JS的类型转换机制

当JS尝试将对象转换为原始值时,首先会调用valueOf方法,如果还未转换为原始值,则会尝试调用toString()

  • 隐式类型转换:+==/===&/|/**^**等操作符处理;
  • 显示类型转换:Number()String()/toString()、**Boolean()**等;
[] == ![] 
![] == true
[] == false
'' == false
0 == 0

为什么obj.toString()和Object.prototype.toString()的结果不一样

toString是Object的原型方法,而Array、function等类型作为Object的实例,都重写了toString方法。不同的对象类型调用toString方法时,根据原型链原理,调用的都是重写以后的toString方法。

判断数组的方法

  • Object.prototype.toString.call()
  • obj.__proto__ === Array.prototype
  • Array.isArray()
  • obj instance of Array
  • Array.prototype.isPrototypeOf(obj)

setmap

在JavaScript中,`Set``Map`是两种内置的数据结构。
  • new Set():主要用于创建集合(元素唯一)。作用:去重、存储不同类型元素、数组与Set转换;
  • new Map():主要用于创建映射关系(键值对)。作用:遍历对象、简化数组操作等;

JS数组方法

  • forEach...等常用的;
  • splice(index, delNum, ...items):从index开始,删除delNum个元素,在当前位置插入元素;
  • slice(start, end):复制从startend的元素,返回一个新数组;
  • reduce(func, initial):通过为每个元素调用func计算数组上的单个值,并在调用之间传递中间结果;
  • array.fill(value, startm end):从start到end用value重复填充数据;
  • array.copyWithin(target, start, end):将其元素从start到end在target位置复制到本身(覆盖所有;)
  • array.from():将伪数组转换为真数组;
  • array.of():将一组值转换为数组;

类数组和数组的区别

类数组和数组都是处理数据集合的概念。区别:

  1. 数据结构
    • 数组用于存储一系列元素,允许通过索引访问,长度固定,可动态调整;
    • 类数组像数组,可通过索引访问连续元素,但不是真正的数组,length属性不可写;
  2. 内置方法
    • 数组有许多内置方法用于操作增删改查、映射、过滤等;
    • 类数组由于不是真正的数组,仅可使用Arrary.prototype上的方法,可通过Array.prototype.slice.call()或者扩展运算符[...array]转化为数组;
  3. 来源
    • 数组是通过JS创建的;
    • 类数组是由DOM操作(document.querySelectorAll()返回的NodeList)或者函数参数(arguments对象在es6之前的函数中是类数组对象)、其它API调用产生的;
  4. 属性
    • 数组除元素外,还具有length属性,表示数组中元素数量;类数组也有一个length属性,但并不一定等同于元素数量(NodeList中表示列表中的节点数量);
  5. 类型检查
    • 数组可使用多方法迭代;
    • 类数组仅可支持使用for循环进行迭代;

var、let、const的区别

  • 块级作用域;
  • 变量提升、暂时性死区;
  • 重复声明;
  • 给全局添加属性;
  • 初始值设置:var、let可以不用设置初始值,但是const必须设置;

深拷贝和浅拷贝

  • 浅拷贝:拷贝数据,复制引用,双向影响
    • Object.creat(x)Object.assign(target, ...sources)
    • [].Concat(x)[...array]
  • 深拷贝:独立存在,互不影响
    • JSON.stringify(obj):不能拷贝特殊类型,不能处理循环引用;
    • structuredClone()
// 手写深拷贝
const user ={
    name:{
        firstName: "牛",
        lastName: "蜗"
    },
    age:18
}
function deep(obj){
    let newObj = {};
    for(let key in obj){
        if(obj.hasOwnProperty(key)){//只拷贝显示具有的属性
            if(obj[key] instanceof Object){//obj[key]是不是对象
                newObj[key]=deep(obj[key]);//递归拷贝---深拷贝最核心的部分
            }else{
                newObj[key]=obj[key];
            }
        }
    }
    return newObj;
}
const newUser = deep(user);
user.name.firstName = "牛牛";
console.log(newUser)

new 操作符的原理

  1. 创建一个新的空对象;
  2. 设置原型:将对象的原型设置为函数的prototype 对象;
  3. 让函数的this指向这个对象,执行构造函数的代码(添加属性);
  4. 判断函数的返回值类型,如果是值类型,则返回创建的对象;如果是引用类型,则返回这个引用类型的对象;
function objectFactory() {
  let newObject = null;
  let constructor = Array.prototype.shift.call(arguments);
  let result = null;
  // 判断参数是否是一个函数
  if (typeof constructor !== "function") {
    console.error("type error");
    return;
  }
  // 新建一个空对象,对象的原型为构造函数的 prototype 对象
  newObject = Object.create(constructor.prototype);
  // 将 this 指向新建对象,并执行函数
  result = constructor.apply(newObject, arguments);
  // 判断返回对象
  let flag = result && (typeof result === "object" || typeof result === "function");
  // 判断返回结果
  return flag ? result : newObject;
}
// 使用方法
objectFactory(构造函数, 初始化参数);

对执行上下文的理解

JS引擎使用执行上下文栈来管理执行上下文;

分类

  • 全局执行上下文:一个程序中只有一个;
  • 函数执行上下文
  • eval函数执行上下文

执行步骤 解析 → 创建 → 执行:

  • 全局上下文:变量定义,函数声明
  • 函数上下文:变量定义,函数声明,thisarguments

this指向问题

this :执行上下文中的一个属性,指向最后一次调用这个方法的对象

箭头函数与普通函数有什么区别

  • 箭头函数更简洁;
  • 箭头函数没有自己的this,只在在自己作用域的上一层继承this
  • 箭头函数继承来的this指向永远不会改变;
  • callapplybind不能改变函数中的this指向;
  • 箭头函数不能作为构造函数使用;
  • 箭头函数没有自己的arguments
  • 箭头函数没有prototype

实现call、apply、bind函数

callapply作用一样,且传参中,第一个参数均表示函数体内this指向,仅在第二个参数不同:apply接受数组或者类数组;call接受枚举值,依次传入;

  • call实现
Function.prototype.myCall = function(context) {
  // 判断调用对象
  if (typeof this !== "function") {
    console.error("type error");
  }
  // 获取参数
  let args = [...arguments].slice(1),
    result = null;
  // 判断 context 是否传入,如果未传入则设置为 window
  context = context || window;
  // 将调用函数设为对象的方法
  context.fn = this;
  // 调用函数
  result = context.fn(...args);
  // 将属性删除
  delete context.fn;
  return result;
};

  • apply实现
Function.prototype.myApply = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  let result = null;
  // 判断 context 是否存在,如果未传入则为 window
  context = context || window;
  // 将函数设为对象的方法
  context.fn = this;
  // 调用方法
  if (arguments[1]) {
    result = context.fn(...arguments[1]);
  } else {
    result = context.fn();
  }
  // 将属性删除
  delete context.fn;
  return result;
};

  • bind实现
Function.prototype.myBind = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  // 获取参数
  var args = [...arguments].slice(1),
    fn = this;
  return function Fn() {
    // 根据调用方式,传入不同绑定值
    return fn.apply(
      this instanceof Fn ? this : context,
      args.concat(...arguments)
    );
  };
};

闭包

闭包:指有权访问另一个函数作用域中变量的函数。闭包实际就是函数和声明函数的词法环境组合。

创建闭包最常用的方式就是在一个函数内部创建另一个函数,创建的函数可以访问到当前函数的局部变量。

作用

  • 创建私有变量;
  • 延长变量的生命周期

缺点

  • 会造成内存泄漏

作用

  • 使在外部能够访问到函数内部的变量,可以使用这种方法来创建私有变量;
  • 使已经运行结束的函数上下文中,变量对象继续留在内存中(因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收)。
  • 实现回调函数和高阶函数

—— 循环中使用闭包解决var定义函数问题


// setTimeout是异步函数,所以会先执行循环,所以会输出一堆5
for(var i = 0; i < 5; i++) {
    setTimeout(function timer(){
        console.log(i)
    }, i*1000)
}

// 使用闭包解决
for(var i = 0; i < 5; i++) {
    (function(){
        setTimeout(function timer() {
            console.log(i)
        }, i * 1000)
    })(i); // 立即执行函数,将当前的i值作为参数传入
}

解决办法:

  1. 使用闭包
  2. 使用setTimeout的第三个函数,这个参数会被当成timer参数传入;
  3. 使用let操作符

对作用域、作用域链的理解

作用域

  • 全局作用域
  • 函数作用域
  • 块级作用域

作用域链

本质就是一个指向变量对象的指针列表。在当前作用域中查找所需变量,找不到,则表示该变量是只有变量,即可向上查找,直到访问到window对象后终止。

作用:保证对执行环境有权访问的所有变量和函数的有序访问。通过作用域链,可以访问到外层环境的变量和函数。

浏览器的垃圾回收机制

浏览器的垃圾回收,即JS运行代码时,需要分配内存空间来储存变量和值。当变量不再参与运行,系统就会回收被占用的内存空间。

回收机制

  • 自动回收:定期对不再使用的变量、对象所占用的内存进行释放;
  • 全局变量的生命周期会持续到页面卸载;局部变量声明在函数体呢,声明周期从函数执行开始到结束,函数执行结束后即释放,除了闭包情况

垃圾回收的方式

  • 标记清楚:
  • 引用计数

减少垃圾回收

  • 优化数组:清空,使用length=0处理;
  • 优化对象:不再使用则设置为null;
  • 函数优化:可复用函数,外置封装;

内存泄漏

常见内存泄漏情况:

  • 意外的全局变量;
  • 被遗忘的计时器或回调函数清除;
  • 脱离DOM的引用:获取一个DOM,而后元素被删除,但是一直保留了对该元素的引用,导致无法被回收;
  • 不合理使用闭包

for...in和for...of(ES6新增)的区别

  • for...in获取的是对象的键名,for...of获取的是对象的键值;
  • for...in会遍历对象的整个原型链,性能很差;for...of只遍历当前对象;
  • for...in会返回数组中所有可枚举的属性,for...of只返回数组的下标对应的属性值。

综上,for...in只适用遍历对象,for...of可用来遍历数组、类数组对象、字符串、Map等;

AMD 和 CommonJS的区别

ES6模块与CommonJS模块有什么共同点和区别

异步编程的实现方法和优缺点

方法

  • 回调函数:缺点:可能会造成回调低于;
  • Promise:同步的立即执行函数。缺点:有时会造成多个then的链式调用,导致代码的语意不明确;
  • generator:允许函数执行过程中挂起和恢复;
  • async函数:将异步逻辑转换为同步顺序来写(await通过返回一个Promise来实现同步的效果),可以自动执行;

优点

  1. 非阻塞:提高了程序的响应性和吞吐量;
  2. 更好的资源利用;
  3. 代码简洁

缺点

  • 错误处理有些复杂;
  • 调试困难;
  • 性能开销:比如创建和销毁Promise对象等;

promise的理解和优缺点

Promise:是JS中用来处理异步操作的的对象,表示了一个异步操作的最终完成状态和结果值。Promise能够简化异步编程的复杂性,当涉及多个异步操作时需要按照特定顺序执行则可考虑该对象使用(使用 .all() 或 .race() 方法)。

状态:一旦改变,就不会再变

  • pending:进行中
  • fullfilled:已成功
  • rejected:已失败

优点

  1. 解决回调地狱;
  2. 错误处理:使用.catch(),轻松捕获;
  3. 可组合性;
  4. 状态不可变

缺点

  1. 错误容易被忽略;
  2. 不易于调试;
  3. 滥用可能导致性能问题;
  4. 不支持取消;
  5. 代码冗余。

Promise.all和Promise.race的区别和使用场景

  1. Promise.all()
    • 将多个Promise实例包装成一个新的Promise实例,传入的Promise顺序和返回成功的结果数组的顺序是一致的,但是失败则返回最先reject失败状态的值
    • 使用场景:发送多个请求,且要求请求顺序获取和使用数据时;
  2. Promise.race()
    • 赛跑。Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。
    • 使用场景:当做某个请求,超时则取消,则使用该方法。

注意:  Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。

事件循环

事件循环是一个单线程循环,用于监视调用堆栈并检查是否有工作即将在任务队列中完成。

如果调用堆栈为空并且任务队列中有回调函数时,则将回调函数调出并推送到调用堆栈中执行。

localStoragesessionStorage的异同

localStorage和sessionStorage都是本地存储的方法。localStorage的存取是同步操作哦~

相同点

  1. 键值对存储
  2. 数据类型:都存储字符串类型的数据;
  3. 容量限制:通常为5MB或更多;
  4. 同源策略:都受同源策略的限制;

不同点

  1. 生命周期:localStorage的数据没有过期时间,除非主动删除,所以常用于存储需要长期保存的数据;sessionStorage的数据在页面会话期间存在,当页面关闭时则会被清除。所以常用于存储与会话相关的临时数据。
  2. 存储位置:具体情况因浏览器而异;
  3. 事件监听:两者都支持事件监听,如 storage 事件,当存储区域中的数据发生变化时,可以触发该事件。
  4. 安全性:两者都支持事件监听,如 storage 事件,当存储区域中的数据发生变化时,可以触发该事件。但是,由于 sessionStorage 的数据在页面会话结束后会被清除,因此它可能在某些情况下比 localStorage 更安全一些(例如,当用户离开敏感页面时)。
  5. 数据共享:只读的localStorage 属性允许你访问一个Document 源(origin)的对象 Storage,同一网站多个标签页中数据都可以共享;sessionStorage在浏览器打开期间一直保持,并且重新加载或恢复页面仍会保持原来的页面会话,但sessionStorage 不能在多个窗口或标签页之间共享数据,但是当通过 window.open 或链接打开新页面时(不能是新窗口),新页面会复制前一页的 sessionStorage

ES6和ES7的新特性有哪些?

ES6新特性主要有:

  • 块级作用域:letconst关键字;
  • 箭头函数:=>语法自动绑定了定义时的this指向;
  • 解构赋值:允许从数组或对象中提取值,并将它们赋给一系列变量;
  • 默认参数:允许在函数定义时为参数提供默认值;
  • 扩展运算符:...;
  • 类和模块:引入了class关键字,支持extends关键字实现继承,super关键字调用父类方法,引入了模块化的概念,通过importexport方便导入导出模块;
  • Promise对象
  • Symbol数据类型,用于创建唯一的标识符;
  • ProxyReflect:提供了在对象上设置陷阱和反射性的操作对象的能力;

ES7新特性:

  • 指数运算符:**,用于数的幂计算(2**3===8);
  • Array.prototype,includes()方法:判断包含,返回布尔值;
  • Object.values()Object.entries()

数据结构算法有哪些?

  1. 查找算法
    • 线性查找:从开始到结束,逐个检查;
    • 二分查找:要求数据结构已排序,每次查找都取中间元素与目标比较,根据结果决定左右继续;
    • 哈希查找:利用哈希函数将键映射到数组中的索引;
  2. 排序算法
    • 冒泡排序:重复遍历,调整顺序;
    • 插入排序:在已排序序列中国,从后向前扫描,找到相应位置插入;
    • 选择排序:最大/最小值抽取,重复继续;
    • 快速排序
    • 归并排序
  3. 插入删除更新算法
  4. 遍历算法
    • 深度有限搜索DFS):用于遍历或搜索树或图。会尽可能深的搜索树的分支;
    • 广度优先搜索(BFS):用于遍历或搜索树或图。从根节点开始,探索最近的邻居节点;
  5. 递归算法:通过函数调用自身来解决问题;
  6. 动态规划算法
  7. 图算法
    • 最小生成树算法:在图的所有子集中找到一棵包含所有顶点的树,且所有边的权值之和最小;
    • 最短路径算法:在图中找到从一个顶点到另一个顶点的最短路径;
  8. 字符串匹配算法

数组和链表有哪些区别?

数据和链表是两种常用的数据结构,区别如下:

  • 存储方式:
    • 数组:在内存中是一段连续的存储空间,大小在创建时候就固定了;
    • 链表:在内存中不是连续存储的,每个节点都包含了一个数据域和一个指向下一个节点的指针域;在运行中大小可改变;
  • 访问方式
    • 数组:索引访问,复杂度O(1);
    • 链表:从头节点开始,直到找到目标节点,复杂度O(n);
  • 插入和删除:
    • 数组:插入删除操作需要移动大量元素,复杂度O(n);
    • 链表:插入删除只需要修改相关节点,复杂度O(1)
  • 空间利用率
    • 数组:只存储数据本身,利用率较高,但是设置过大容易导致内存浪费;
    • 链表:利用率较低,但是由于可以动态分配内存空间,避免了内存浪费;
  • 应用场景
  • 数组:适用于需要频繁访问元素且元素数量固定不变的;
  • 链表:适用于需要频繁进行插入和删除操作的元素、数据可能动态变化的场景。

全局函数有哪些?

min()max()abs()pow()range()parseInt()format()``escape() 和 unescape()等;

location是怎么匹配的,匹配规则有哪些

  • location /:匹配所有请求,因为所有的地址都以/开头
  • location /documents/:匹配任何以/documents/开头的地址,匹配符合后可能还会继续搜索;
  • location ^~/images/:匹配任何以/images/开头的地址,匹配符合后停止搜索正则;
  • location ~* .(gif|jpg|jpeg)$:匹配所有以gif,jpg或jpeg结尾的请求;
  • location的匹配顺序:先普通,再正则。即普通的location之间的匹配顺序按最大前缀匹配,正则location之间的匹配顺序按配置文件中的物理顺序匹配。

单项数据流和双向数据绑定的原理,区别

  1. 单项数据流:是指数据在应用程序中的流动方向是单项的,通常是父组件流向子组件,而不会反向流动:
    • 数据更新只能通过父组件对子组件的传递;
    • 明确、可控,使数据流向易于追踪和调试;
  2. 双向数据绑定:是指数据在视图和模型之间可以双向同步更新:
    • ViewModel包含Observe个Compiler,监听器对所有数据的属性进行监听,解析器对每个元素节点的指令进行扫描和解析,视图或数据模型发生变化时,将同步另一方的自动更新;
    • 方便,但也可能导致数据变更来源不明确;

前端跨域处理

前端跨域处理是解决前端开发过程中由于浏览器的同源策略,而无法直接访问不同源(协议、域名、端口)的资源的问题。

解决方法

  1. JSONP:利用<script>标签不受同源策略限制的特点,通过动态插入<script>标签来请求跨域资源。

    • 实现
      • 客户端,创建一个<script>标签,将跨域请求的URL作为src属性。
      • 服务器端:返回的数据需要以函数调用的形式包裹,客户端定义相应的回调函数来接收数据。
    • 缺点:只支持GET请求。
  2. 跨域资源共享(CORS):通过服务器端设置响应头信息来实现跨域请求。

    • 服务器端
      • Access-Control-Allow-Origin:指定允许跨域请求的源,可以是具体的域名或通配符*
      • Access-Control-Allow-Methods:指定允许使用的请求方法,如GET、POST等;
      • Access-Control-Allow-Headers:指定允许在请求中使用的头部字段;
    • 客户端:浏览器会自动处理;
    • 优点:支持所有类型的HTTP请求,是W3C推荐的标准跨域解决方案。
  3. 代理(Proxy):通过在服务器端设置一个代理服务器,前端与代理服务器通信,代理服务器与目标服务器通信,从而绕过浏览器的同源策略限制。

    • 实现:使用NodeJS的中间件或者Nginx等服务器软件来设置代理
    • 优点:灵活性强,可以配置复杂的路由规则;
  4. PostMessage:使用window对象的postMessage方法在不同源的窗口之间发送和接收数据。。

    • 实现
      • 发送方:使用postMessage方法发送数据,并指定接收方窗口的引用。
      • 接收方:监听message事件来接收数据。
    • 场景:适用于两个页面之间的跨域通信,如iframe内的页面与父页面之间的通信
  5. WebSocket:网络通信协议,可以在单个TCP连接上进行全双工通信。

    • 跨域处理:WebSocket不受同源策略限制,可直接使用;
    • 优点:支持实时通信,适合需要频繁传输数据的应用场景
  6. 服务器端反向代理:服务器端设置。优点:对前端透明,无需修改前端配置;

  7. document.domain + iframe:通过设置document.domain属性使不同子域的页面具有相同的域,从而可以相互访问。

    • 场景:适用于主域相同、子域不同的跨域应用场景
  8. location.hash + iframe

  • 原理:通过iframelocation.hash来实现跨域通信。
  • 实现步骤:
    • 在一个页面中嵌入一个iframe,并设置其src属性为另一个域的页面。
    • 通过修改iframe的location.hash值来传递数据,监听hashchange事件来接收数据。

JS继承

在JS中,继承通常不是一个直接内置在语言中的特性,可以通过原型链、构造函数、组合继承、原型式继承、寄生式继承、寄生组合式继承等方式模拟实现。

  • 原型链继承:当试图访问一个对象的属性时,如果该对象本身没有这个属性,那就回在原型对象中查找;
  • 构造函数继承:在子类型构造函数的内部调用超类型构造函数。即可以使用callapply,将父对象的构造函数绑定在子对象上。
function Parent() { 
    this.name = 'parent'; 
} 
function Child() {
    Parent.call(this); 
    // 继承Parent 
    this.age = 'child'; 
} 
let child = new Child(); 

console.log(child.name); // 输出'parent'
  • 原型式继承:借助原型可以基于已有对象创建新对象。
function object(o) {
    function F() {} 
    F.prototype = o;
    return new F(); 
} 
let person = { 
    isHuman: true,
    printIntroduction: function() { 
        console.log("My name is " + this.name); 
    } 
};
let me = object(person);

JS节流和防抖

面向对象的理解

dom事件流,一个按钮绑定了冒泡和捕获,点击后触发顺序是什么?

多接口请求如何优化

不同接口的时序性如何保证?

金额精度丢失前后端如何处理?

node内存泄漏怎么解决

websocket

websocker消息质量是什么?012段消息是什么?

websoket通信机制是什么?

面向过程编程和面向对象编程有什么差异?怎么理解,函数式编程呢?