record-2

# 关于 js Number 的误差详解

  • 造成原因:js 的 Number 类型是采用双精度浮点数(即二进度),这会引起浮点数的精度问题

    • 偏大场景,例如:0.1 实际上表达为 0.1000000000000000055511151231257827021181583404541015625

    • 偏小场景,例如:1.005 实际上表达为 1.0049999999999999

    • 准确场景,能被二进制完美保存的数,0.5 可以

  • toFixed误差,因为有些数会偏大偏小,会做四舍五入会产生误差

    • 例如:1.005.toFixed(2),预期应该是1.01,实际上是1.00
  • 解决办法

    • 乘法,比如 1.005 取 2 位小数,那么就 ✖️ 100 之后四舍五入再 ➗ 100

    • 不用二进制,这也是一些三方库的做法,比如Big.js Decimal.js,他们内部实际上也是不用原生 Number 去存储数字(不用二进制),他们用字符或数组来存储数字,直接模拟十进制的存储和计算。

# map、weak map、set、weak set 区别

  • map

    • 结构,map 是键值对集合,任何值都可以作为键或值

    • 键类型任何值,对象、原始类型

    • 键值对迭代,保留了键值对的插入顺序

    • 垃圾回收,强引用,不会被回收

    • 方法,支持 .get() .set() .has() .size

  • weak map

    • 结构,键只能是对象,值无所谓

    • 键类型,键只能是对象

    • 键值对迭代,不能被迭代,也没有 size 属性

    • 垃圾回收,弱引用,键可以被回收,但是需要注意键被回收了,整个键值对也会被自动清除

    • 方法,支持 .get() .set() .has() ,不支持 .size

  • set

    • 结构,值是唯一的不能重复

    • 键类型任意值

    • 值的迭代,保留了插入顺序,可以迭代,如 for of、forEach

    • 垃圾回收,强引用,不能被回收

    • 方法,支持 .add() .delete() .has() .size

  • weak set

    • 结构,值是唯一的不能重复,只能是对象

    • 键类型,只能是对象

    • 值的迭代,不可迭代

    • 垃圾回收,弱引用,能被回收

    • 方法,支持 .add() .delete() .has() ,不支持 .size

# es5 几种实现继承的方案 是怎么演变的为什么会有这种演变

  • 原型链继承,子类的原型对象指向父类的实例

    • 缺点:引用类型共享

    • 缺点:无法向父类构造函数传递参数

function Parent() {
  this.name = 'parent';
  this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
  return this.name;
};

function Child() {}

Child.prototype = new Parent(); // 继承父类的属性和方法
Child.prototype.constructor = Child; // 修正构造函数指向

const child1 = new Child();
console.log(child1.getName()); // 'parent'
  • 借用构造函数(经典继承),通过 call apply 调用父类的构造函数

    • 优点:解决引用类型共享问题,每个子类实例都有自己独立的属性。

    • 优点:可以传递参数:可以在子类中给父类传递参数。

    • 缺点:无法继承父类原型上的方法

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}

function Child(name) {
  Parent.call(this, name); // 借用构造函数继承
}

const child1 = new Child('child1');
console.log(child1.name); // 'child1'
  • 组合继承,通过原型链继承原型方法,通过构造函数继承父类的实例属性

    • 优点:既继承了实例属性(通过 call),又继承了原型方法(通过 prototype)。

    • 优点:每个实例都有自己的属性,不会共享父类的引用类型属性。

    • 缺点:父类构造函数被调用了两次:一次在 Parent.call(),一次在 new Parent(),导致子类原型上和实例上都有父类实例属性,但只有实例上的被使用。

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
  return this.name;
};

function Child(name, age) {
  Parent.call(this, name); // 借用构造函数
  this.age = age;
}

Child.prototype = new Parent(); // 原型链继承
Child.prototype.constructor = Child; // 修正构造函数指向

const child1 = new Child('child1', 18);
console.log(child1.getName()); // 'child1'
  • 寄生组合继承,寄生组合继承是对组合继承的一种优化。其核心思想是,避免在子类的原型上调用父类的构造函数,直接继承父类的原型即可。这种方式不仅减少了内存开销,也避免了组合继承中的冗余问题。

    • 优点:避免了组合继承的缺点:只调用了一次父类的构造函数,避免了不必要的冗余属性。

    • 高效、简洁:是 ES5 中实现继承的最佳方案。

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
  return this.name;
};

function Child(name, age) {
  Parent.call(this, name); // 借用构造函数
  this.age = age;
}

// 寄生组合继承
Child.prototype = Object.create(Parent.prototype); // 继承父类的原型
Child.prototype.constructor = Child; // 修正构造函数指向

const child1 = new Child('child1', 18);
console.log(child1.getName()); // 'child1'
  • ES5 中 Object.create 的继承,Object.create 是 ES5 引入的一种新方法,用来实现基于原型的继承。它通过创建一个新对象,指定其原型为父类的原型对象。

    • 优点:Object.create 是一个非常简洁和直观的实现继承的方式。

    • 优点:通过指定原型链,避免了组合继承中冗余调用父类构造函数的问题。

    • 是寄生组合继承的一种简化形式。

function Parent(name) {
  this.name = name;
}

Parent.prototype.getName = function () {
  return this.name;
};

function Child(name) {
  Parent.call(this, name); // 借用构造函数
}

Child.prototype = Object.create(Parent.prototype); // 继承父类的原型
Child.prototype.constructor = Child; // 修正构造函数指向

const child1 = new Child('child1');
console.log(child1.getName()); // 'child1'
  • 浏览器会默认组织跨域请求携带 cookie,所以需要进行一些额外配置,才可以携带 cookie

  • 客户端请求需要设置 withCredentials 选项

    • fetch 设置 credentials: 'include', // 允许携带 cookie,'same-origin':只在同源请求时发送 cookie

    • axios 设置 withCredentials: true

  • 服务端设置 Access-Control-Allow-Credentials: true;还有设置允许跨域请求 Access-Control-Allow-Origin (注意此时不能为*)

  • 服务端设置 Set-Cookie 响应头将 cookie 返回给客户端

    res.cookie('token', '12345', {
      httpOnly: true, // 防止客户端 JavaScript 访问
      secure: true, // 仅在 HTTPS 协议下发送
      sameSite: 'None', // 允许跨域请求携带 cookie,必须设置为 'None' 才能跨域传递
    });
    

# 跨域是如何实现的

  • 由于同源策略,协议 域名 端口 有一个不一致都属于跨域

  • CORS (Cross-Origin Resource Sharing)

    • 客户端发送请求的请求头携带 Origin 字段。表示需要请求的源

      • 简单请求:get、post(application/x-www-form-urlencoded、multipart/form-data、text/plain)和 Head 请求,不包含自定义头部

      • 预检请求:put、delete、patch 和自定义请求类型,或者自定义头部的请求,浏览器会发起一个预检请求,询问浏览器是否允许跨域

    • 服务器设置

      • Access-Control-Allow-Origin:指定允许跨域请求的源(可以是具体的域名或通配符 *)

      • Access-Control-Allow-Methods:指定允许的 HTTP 方法(如 GET、POST 等)。

      • Access-Control-Allow-Headers:指定允许的请求头(自定义请求头)。

      • Access-Control-Allow-Credentials:指定是否允许发送凭证(如 cookie)。

      • Access-Control-Max-Age:指定预检请求的缓存时间,以减少预检请求的频率。

  • JSONP 只能是 get 请求,动态创建一个标签,标签的 src 是请求 url,请求 url 里面需要包含 callback=xxx 方法

    <script>
      function handleResponse(data) {
        console.log('Received data:', data);
      }
      // 动态创建 script 标签,实现跨域请求
      var script = document.createElement('script');
      script.src = 'https://example.com/data?callback=handleResponse';
      document.body.appendChild(script);
    </script>
    
  • 服务器代理

  • websocket,WebSocket 默认允许跨域通信

  • 窗口通信,是浏览器提供的 不同窗口、iframe、标签页之间进行安全通信的 API,postmessage

# 说一下浏览器缓存的流程 缓存过程中 时间戳除了可以放在请求头中 还可以放到哪里

  • 用户访问网页时,会缓存一些静态资源,如 js 图片 css html 等等,用户再次访问的时候就不需要再从服务器请求;依赖于 http 头部的缓存控制测试,分为强缓存和协商缓存

  • 强缓存

    • Expires 是 HTTP/1.0 的缓存机制,它指定资源的到期时间。它是一个绝对的时间点,表示资源在此时间之前不需要重新请求。

    • Cache-Control 是 HTTP/1.1 引入的缓存机制,具有更多的控制能力。它可以通过相对时间来指定资源的缓存时效,并提供多种控制策略。

      • Cache-Control max-age=3600 3600 秒资源有效

      • Cache-Control:no-cache 资源每次请求都需要和服务进行验证

      • Cache-Control:no-store 不需要缓存,每次都重新获取

  • 协商缓存

    • Last-ModifiedIf-Modified-Since

      • Last-Modified 是服务返回的头,代表最后一次修改时间

      • 浏览器请求资源的时候,会在请求头中包含 If-Modified-Since,携带上次的 Last-Modified 时间,询问服务器自从上次更新之后资源是否有更新,如果更新了,返回新的资源和新的 Last-Modified;如果没有更新,返回 304 Not Modified,浏览器使用缓存

    • EtagIf-None-Match

      • Etag 是服务器为资源生成的唯一标识(hash 或者版本号),资源变更,服务器生成新的 Etag

      • 浏览器请求资源的时候,会在请求头中包含 If-None-Match,携带上次的 Etag,如果变化返回新的资源和新的 Etag;如果不变返回 304 Not Modified,浏览器使用缓存

# 如何区分简单请求与复杂请求

  • 简单请求:get、post(application/x-www-form-urlencoded、multipart/form-data、text/plain)和 Head 请求,不包含自定义头部

  • 预检请求:put、delete、patch 和自定义请求类型,或者自定义头部的请求,浏览器会发起一个预检请求,询问浏览器是否允许跨域

# 内存泄漏有哪些原因导致

  • 全局变量

  • 闭包

    • 当本应销毁的函数没有销毁,导致其关联的此法环境无法销毁,造成内存泄漏

    • 多个函数共享此法环境,可能导致词法环境膨胀,js 垃圾回收会回收掉没有使用的函数,但是针对其共享的此法环境,会出现无法到达而且也无法回收的情况

  • addeventlisten 事件监听器

  • settimeout 定时器

  • 手动创建的 dom 元素 未被销毁

  • 缓存过大

# 怎么监控内存泄漏

  • 浏览器 Memory 面板 - 堆快照(Heap Snapshot)

  • Performance.memory API

# 怎么实现跨标签页通信

  • LocalStorage 和 监听 Storage 事件

  • WebSocket

  • 广播 BroadcastChannel API

  • Service Workers + postMessage

# promise 构造函数是异步执行还是同步执行

  • 是同步执行的,但是 then 等回调是异步执行

# promise 相关问题

async function p1() {
  return 3;
}

async function p2() {
  return Promise.resolve(3);
}

async function p3() {
  return await Promise.resolve(3);
}
  • p1

    • async,虽然是返回了 3,但是由于 async,导致其会隐式返回一个 Promise

    • return 3,等同于 return Promise.resolve(3)

  • p2

    • async,返回一个 Promise,由于 p2 返回的已经是 Promise 了,所以正常返回

    • return Promise.resolve(3),是一个 Promise,不需要转换了

  • p3

    • async,返回一个 Promise,但是由于 await ,所以要等 await 执行完,执行完返回 3,所以相当于p1的情况

    • return await Promise.resolve(3);,等同于 return 3, 由于 async 所以等同于 return Promise.resolve(3)

# ts 的 interface 和 type 区别

  • interface

    • 可以定义对象结构;

    • 可以继承 extends 其他的接口或类

    • 可以重复定义同一个接口,ts 会自动合并

    • 不支持联合类型、元组类型

  • type

    • 可以定义对象结构,也可以定义其他类型:类型别名、联合类型、交叉类型等;

    • 通过 & 实现类型组合

    • 不支持重复定义,会报错

    • 支持联合类型 success | error 支持元组类型 [number,string]

# 生成器的原理是什么

  • 生成器函数运行暂停执行,并且保持当前上下文的状态(包括局部变量和函数状态),这主要依赖于 闭包机制 和 状态机

  • 生成器可以看出一个状态机,每次执行到 yield 的时候,生成器会暂停,并且记录当前的执行状态,调用 next()方法会恢复上一次的执行状态

  • 生成器依赖闭包机制来保持它的内部状态,这样即使生成器被挂起,它的执行上下文不会被销毁,局部变量、函数状态得以保留

  • 控制流中断和恢复,遇到 yield 的时候,生成器函数会暂停,将控制权交给调用者,实现了控制流的中断;下一次调用 next 就会恢复控制流,并且继续执行;从而实现暂停和恢复功能

  • 每次调用 next,都是将生成器的执行上下文重新压入 js 调用栈,并从上次暂停的地方继续;yield 的时候,执行上下文并不会从栈里被删除,而是被挂起,等待下次恢复

  • 生成器并不是通过并发或多线程实现暂停和恢复的,而是通过保存和恢复执行上下文,以及对控制流的精细控制实现的