3. Js
js 基础类型有哪些
number,string,bool,undefined,null,symbol(ES6), bigint(es10)
运算规则 TODO
算数运算
- 转化为数字再运算
- 特殊情况
- string + number = str 拼接
- NaN + 任何 = NaN
转换
[] + {} = [].valueOf() + {}.valueOf() = [] + {} = [].toString() + {}.toString() = '' + '[object object]' = '[object object]'
// {} 被 js 理解为块级作用域了
{} + [] = +[] = +'' = +0 = 0介绍下浅拷贝和深拷贝?深拷贝的方案?
基础类型,这类数据类型在内存中占据固定大小,保存在栈内存中,基本数据类型,直接复制值。 引用类型的数据存储在栈中的是对象的引用地址,而真实的数据存放在堆内存中。
浅拷贝: 只复制对象的第一层属性。引用数据类型(如对象或数组),只复制引用地址,拷贝后的对象与原对象共享同一块内存。
- 浅拷贝
Object.assign({}, obj) - 使用扩展运算符:浅拷贝
{ ...obj } [].concat(arr),arr.slice(0)
深拷贝: 复制对象的所有层级。拷贝后的对象与原对象完全独立,修改其中一个不会影响另一个。
JSON 方法(适用于简单对象): 深拷贝
JSON.parse(JSON.stringify(obj)缺点:
- 无法拷贝 函数/undefined/Symbol(会丢失)、
- 无法拷贝 NaN/Infinity/null(变为 null)、
- 无法拷贝 reg/Error/map/set/weakmap(变为空对象),
- 无法拷贝 date(变为字符串)。
- 会丢失对象的原型链
递归实现: 深拷贝 思路是
- null 或者非 obj 直接 return object
- 特殊类型的处理
- Array
obj.map(item => deepClone(item)) - Date
new Date(obj.getTime()) - RegExp
new RegExp(obj.source, obj.flags) - Set
new Set([...obj].map(item => deepClone(item))) - Map
- symbol
- Array
- 其他认为 obj,
Object.keys(obj).forEach(key => { clonedObj[key] = deepClone(obj[key]); })
弊端:循环引用无法解决 两个或者多个对象相互持有对方的引用,就形成了引用循环,导致的结果是内存泄漏。
报错:Maximum call stack size exceeded
解决方案:建立映射表,new weakmap()
- 终极方案:structuredClone,caniuse 上兼容性也达到 93% 了,还不错了 缺点是:无法拷贝 函数,undefined,symbol,DOM
介绍下原型和原型链
原型链是由多个对象的原型逐级连接形成的链式结构,当访问对象的属性或方法时,JavaScript 会沿着原型链逐层查找,直到找到目标属性或到达链的顶端(即 null); 每个对象都有个隐式原型(__proto__)或者 getPrototypeOf, 每个函数都有个原型(prototype 对象),用于存放一些属性和方法,实现继承
p.proto: Person.prototype
Person.prototype.proto: Object.prototype
Object.prototype.proto: null
Person.proto: Function.prototype
Object.proto:Function.prototype
Function.prototype.proto: Object.prototype
Function__proto__: Function.prototype
恶心的继承??????不想看!!!!
通过原型链实现继承的方式:
- 子类调用父类构造函数
Parent.call(this, name) - 继承原型
Child.prototype = Object.create(Parent.prototype) - 修复 constructor 指向
Child.prototype.constructor = Child
如何判断对象的原型链中是否包含某个原型
使用 instanceof 或 isPrototypeOf:
obj instanceof Constructor判断 obj 是否是 Constructor 的实例Constructor.prototype.isPrototypeOf(obj)判断 obj 的原型链中是否包含 Constructor.prototype
介绍下函数柯里化
接受多个参数的函数转换为接受单一参数的函数。 每次调用返回一个新函数,直到所有参数被提供为止。 优点是参数复用,可以固定部分参数,延迟计算
介绍下几种循环
按循环性能由高到低:
- for 可退出,知道循环次数的情况下最好使用他(性能最优)
- while/dowhile 可退出,适用于循环次数不确定,但需要满足某个条件时的场景
- forEach 不能退出,没有返回值;适用于循环次数未知,或者计算循环次数比较麻烦情况下使用效率更高,与之相对应是还有 map,区别是有返回值。
- for of 可退出,遍历可迭代对象(obj 不行),与 forEach() 不同的是,它可以正确响应 break、continue 和 return 语句,性能要好于 for in,但仍然比不上普通 for 循环,这个方法避开了 for in 循环的所有缺陷,for of 循环出的是 value。
- for in 可退出,遍历数组或者对象(循环出的是 key,是 string),通常用来遍历对象(包括原型链上的属性),建议不要用该方法来遍历数组,因为它的效率是最低的。在 ECMAScript 规范中定义了 「数字属性应该按照索引值⼤⼩升序排列,字符串属性根据创建时的顺序升序排列。
哪些地方不能使用箭头函数
一句话:用于消除函数的二义性 在 js 中(js 的设计缺陷,function 和 class 不分),函数有两种用法,第一种就是我们常见的指定序列,定义一个函数再执行,第二种用法就是将他当作构造器,用 new 来创建,这个也能用来创建对象的实例,这就是函数的二义性。
ES6 提出 Class 语法和箭头函数,Class 这个就是直接告诉我们,这不是一个简单的函数,无法使用指定序列来调用,会报错,必须 new ,箭头函数就是个简单的执行序列,和面向对象无关,所以他的局限性,或者说什么场景下无法使用箭头函数,其实就是和面向对象有关的场景下 this,new 等就不适用箭头函数了,也没有 arguments
Js 事件模型
早期的浏览器是没有事件流的,ie 和网景在事件处理上存在分歧,
- ie 认为事件是从目标节点向根节点冒泡传播的,目标节点最早触发,根节点最后触发;
- 网景提出了事件捕获模型,认为事件先从根节点触发,
这种差异导致了跨浏览器兼容问题,为了解决这个问题,W3C 提出了标准化事件模型,默认情况下,事件以冒泡的方式触发,可通过 addEventListener 第三个参数修改为 true 改为 捕获。
分为三个阶段:
- 捕获阶段(根节点向目标元素传播,经过每一个父节点)addEventListener true
- 目标阶段(到达目标阶段)
- 冒泡阶段(目标元素向上冒泡回到根节点)默认都冒泡
聊一下 js 事件循环,两句话
单线程是异步产生的原因
事件循环是异步的实现方式
如何理解 JS 的异步
JS 是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。承担着诸多的工作,比如渲染页面,执行 js 都在其中。
如果用同步的方式,极有可能导致主线程产生阻塞,从而导致消息队列中很多其他任务无法得到执行。这样一来,一方面会导致繁忙的主线程白白消耗事件,另一方面导致页面无法及时更新,给用户造成卡死的假象。
所以浏览器采用异步的方式来解决。具体做法是当某些任务发生时,比如计时器,网络等,主线程会将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行; 在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。
事件循环是什么
事件循环又叫做消息循环,是浏览器渲染主线程的工作方式; 在 Chrome 的源码中,他开启了一个不会结束的 for 循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可; 过去把消息队列简单分为宏任务和微任务,这种说法已无法满足目前浏览器的复杂程度; 根据 W3C 官方的解释: 每个任务有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同队列。队列有优先级之分,在每一次循环中,由浏览器自行决定取哪个队列的任务; 但浏览器必须有一个微队列,微队列的任务最高优先级,必须优先调度执行;
任务没有优先级,在消息队列中先进先出; 消息队列有优先级
计时器能做到精确计时吗,为什么
- 不是原子钟,无法做到精确计时;
- 按照 W3C 标准,浏览器实现计时器,如果嵌套层级超过 5 层,则会带有 4ms 的最少时间,这样在计时时间少于 4ms 时又带来了偏差
- 受事件循环影响,计时器的回调函数只能在主线程空闲时运行,因此又带来了偏差
- 有什么解决方案?
- 在 setTimeout 中加入时间校正
- requestAnimationFrame
- web worker
- performance.now 替代 Date
- performance.now 返回一个以 毫秒为单位的时间戳,但精度可以达到微秒; Date 只到 毫秒
- performance.now 是从浏览器/程序启动到当前的时间,与系统时钟无关,不会因用户手动修改时间或系统同步时间而改变;Date.now() 则是基于系统当前时间,可能因系统时间调整而发生跳变或回退
介绍下 requestIdleCallback
requestIdleCallback 是浏览器提供的一个 API,用于在浏览器空闲时执行低优先级任务。它允许开发者在不影响用户体验的情况下执行后台任务,例如数据预加载、日志记录等
- 为什么 react 没有用 requestIdleCallback?要自定义 scheduler
- 兼容性问题,一个是不是所有浏览器都支持,另一个是 React 是一个跨平台,跨浏览器的解决方案,不能依赖于特定 API
- 无法保证优先级
- 无法保证任务的执行时间
- 自定义调度器(基于 MessageChannel)提供了更高的灵活性和性能优化。
打印 timeRemaining() 的时候会发现,部分时间超过了 16.66ms,可能是 50ms 等,这是为什么呢? 虽然每一帧绘制的时间约为 16.66ms,但是如果屏幕没有刷新,那么浏览器会安排长度为 50ms 左右的空闲时间。
为什么是 50ms?
根据研究报告表明,用户操作之后,100ms 以内的响应给用户的感觉都是瞬间发生,也就是说不会感受到延迟感,因此将空闲时间设置为 50,浏览器依然还剩下 50ms 可以处理用户的操作响应,不会让用户感到延迟。
介绍下 webwoker,限制有哪些,应用场景
是一种在浏览器中运行的独立线程,用于执行耗时的任务而不阻塞主线程(UI 渲染线程);主线程和 Worker 通过 postMessage 和 onmessage 进行双向通信。
Web Worker 的限制有哪些?
- 无法操作 DOM
- 受限的 API
- 同源限制
- 通信开销
Web Worker 的实际使用场景
- 复杂计算:如大数据处理、图像处理、加密解密等。
- 文件处理:如文件解析(CSV、JSON)、压缩和解压缩。
- 实时数据处理:如音视频流处理、实时图表渲染。
- Web 应用性能优化:如在 React/Vue 应用中,将复杂逻辑放入 Worker 中执行。
介绍一下闭包,使用场景
内存泄漏:有垃圾没有被回收;
垃圾:不用的东西就叫垃圾;
闭包:函数 + 词法环境;一个函数可以访问其定义时作用域中变量的机制,即使这个函数在外部被调用时,依然能访问那个作用域
用于封装私有变量、实现柯里化、事件绑定状态管理等场景
闭包与内存泄漏的关系
闭包中的参数不会被回收吗?
很简单的问题,如果闭包还在用,那么他所携带的参数也就不算是垃圾,不应该被回收,如果闭包不再被引用,且参数不被别的地方使用,那么这些参数就会被回收
闭包与内存泄漏的关系就是:
- 有一些本该被回收的函数没有被回收,从而导致其关联的词法环境也无法被回收,最终造成内存泄漏;
- 当多个函数共享一个词法环境的时候,可能会造成词法环境的膨胀,从而出现无法访问且无法回收的内存空间
typeof vs instanceOf
typeof null = "object"是一个已知的 JavaScript 设计缺陷。- typeof 对于数组、对象、null 都返回 “object”,无法区分具体的对象类型。
- instanceof 用于检测某个对象是否是某个构造函数的实例,返回一个布尔值
一般使用 typeof 检测基本数据类型。使用 instanceof 检测对象的具体类型。对于特殊情况(如 null 和数组),需要额外处理。
function checkType(value) {
if (value === null) return "null";
if (Array.isArray(value)) return "array";
return typeof value;
}介绍一下 webStorage
cookie 存储容量最小,只有 4kb,必须是字符串,可设置过期时间,会跟随请求一起发送
storage 的存储容量一般在 5MB 左右,也必须是字符串,其中 localStorage 是永久的,sessionStorage 是会话级的,会随浏览器关闭而自动销毁
indexDB 存储容量较大,一般取决于硬盘,大数据量的存储,就不建议放在 storage 里,可用考虑 indexDB 或者服务端存储
Cookie - 如何保证安全
- domain 和 path
- 如果未设置 domain 属性,Cookie 的作用域默认为当前域名(不包括子域名)
- 如果设置了 domain 属性,Cookie 可以被指定域名及其子域名访问。
- 设置了 path 确保只有特定路径下的页面能够访问 Cookie
- samesite 跨站:域名不一致,同源策略作为浏览器的安全基石,其「同源」判断是比较严格的,相对而言,Cookie 中的「同站」判断就比较宽松:只要两个 URL 的有效顶级域名 + 1 相同即可,不需要考虑协议和端口。例如 taobao.com 等。 跨站要不要把 cookie 带过去,就取决于 SameSite;
- None 必须配合 secure,否则无效
- Lax 默认值,只有 GET 请求和导航请求会带上 Cookie,其他类型的跨站请求不会带上 Cookie,对超链接放行,比如 a 的 href
- strict:阻止所有 cookie 发送
- http-only:属性可以使 Cookie 只能通过 HTTP 请求访问,不能通过 JavaScript 访问。这可以防止跨站脚本攻击(XSS)通过 JavaScript 读取 Cookie
- secure:只有在通过 HTTPS 协议的安全连接中,Cookie 才会被发送
token 存放在哪里
便捷性考虑,存放在 localStorage 是没问题的,安全不是 token 负责的,token 的职责只是做请求会话认证的,Token 是防止篡改的 不是防泄露的
单点登录,超纲了
是一种身份验证和授权机制,使用户能够在一个会话中使用多个应用或系统而不必多次登录。它简化了用户的登录体验,
Math.max 入参为数组要怎么实现
- Math.max.apply(null, arr);
- Math.max.apply(…arr);
介绍下 Object.defineProperty
用于精确地添加或修改对象的属性,不仅能设置值,还可以定义这个属性的行为(是否可枚举、是否可写、是否可配置等)。 是 Vue 2 响应式系统的核心,也常用于底层封装、兼容处理、属性控制等场景。
const obj = {};
Object.defineProperty(obj, 'name', {
value: 'Alice',
writable: false
});
obj.name = 'Bob';
console.log(obj.name); // "Alice"const user = {};
Object.defineProperty(user, 'password', {
value: '123456',
enumerable: false
});
console.log(Object.keys(user)); // []
console.log(user.password); const obj = {
_age: 18
};
Object.defineProperty(obj, 'age', {
get() {
console.log('get age');
return this._age;
},
set(val) {
console.log('set age', val);
this._age = val;
}
});
obj.age = 20; // set age 20
console.log(obj.age); // get age → 20- Object.defineProperty 和 Object.assign 区别?
- assign 是浅拷贝,只复制值,不控制属性行为。
- defineProperty 可以控制属性行为,更底层。
- defineProperty 实现响应式的原理?
- 拦截对象属性的 get 和 set,在访问时收集依赖,在设置时触发更新。
如何判断对象为空
判断对象为空可以使用 Object.keys(obj).length === 0,这是最主流的写法。如果要更严谨地判断包括 Symbol 类型的属性,我们可以使用 Reflect.ownKeys(obj).length === 0。此外在做判断前建议进行类型和 null 检查。
更简单一点的还有 JSON.stringify(obj) === '{}', Object.getOwnPropertyNames(obj).length === 0;
use strict 模式
普通函数调用,this 在严格模式下是 undefined,非严格是 window;