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,所以需要进行一些额外配置,才可以携带 cookie
客户端请求需要设置 withCredentials 选项
fetch 设置
credentials: 'include'
, // 允许携带 cookie,'same-origin':只在同源请求时发送 cookieaxios 设置
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-Modified 和 If-Modified-Since
Last-Modified 是服务返回的头,代表最后一次修改时间
浏览器请求资源的时候,会在请求头中包含 If-Modified-Since,携带上次的 Last-Modified 时间,询问服务器自从上次更新之后资源是否有更新,如果更新了,返回新的资源和新的 Last-Modified;如果没有更新,返回 304 Not Modified,浏览器使用缓存
Etag 和 If-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 的时候,执行上下文并不会从栈里被删除,而是被挂起,等待下次恢复
生成器并不是通过并发或多线程实现暂停和恢复的,而是通过保存和恢复执行上下文,以及对控制流的精细控制实现的