3. Js

21 min

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)

深拷贝: 复制对象的所有层级。拷贝后的对象与原对象完全独立,修改其中一个不会影响另一个。

  1. JSON 方法(适用于简单对象): 深拷贝 JSON.parse(JSON.stringify(obj)

    缺点:

    • 无法拷贝 函数/undefined/Symbol(会丢失)、
    • 无法拷贝 NaN/Infinity/null(变为 null)、
    • 无法拷贝 reg/Error/map/set/weakmap(变为空对象),
    • 无法拷贝 date(变为字符串)。
    • 会丢失对象的原型链
  2. 递归实现: 深拷贝 思路是

    • 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
    • 其他认为 obj,
      Object.keys(obj).forEach(key => {
        clonedObj[key] = deepClone(obj[key]);
      })

弊端:循环引用无法解决 两个或者多个对象相互持有对方的引用,就形成了引用循环,导致的结果是内存泄漏。

报错:Maximum call stack size exceeded

解决方案:建立映射表,new weakmap()

  1. 终极方案: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

恶心的继承??????不想看!!!!

通过原型链实现继承的方式:

  1. 子类调用父类构造函数 Parent.call(this, name)
  2. 继承原型 Child.prototype = Object.create(Parent.prototype)
  3. 修复 constructor 指向 Child.prototype.constructor = Child

如何判断对象的原型链中是否包含某个原型

使用 instanceofisPrototypeOf

  1. obj instanceof Constructor 判断 obj 是否是 Constructor 的实例
  2. 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 时又带来了偏差
  • 受事件循环影响,计时器的回调函数只能在主线程空闲时运行,因此又带来了偏差
  • 有什么解决方案?
    1. 在 setTimeout 中加入时间校正
    2. requestAnimationFrame
    3. web worker
    4. performance.now 替代 Date
      • performance.now 返回一个以 毫秒为单位的时间戳,但精度可以达到微秒; Date 只到 毫秒
      • performance.now 是从浏览器/程序启动到当前的时间,与系统时钟无关,不会因用户手动修改时间或系统同步时间而改变;Date.now() 则是基于系统当前时间,可能因系统时间调整而发生跳变或回退

介绍下 requestIdleCallback

requestIdleCallback 是浏览器提供的一个 API,用于在浏览器空闲时执行低优先级任务。它允许开发者在不影响用户体验的情况下执行后台任务,例如数据预加载、日志记录等

  • 为什么 react 没有用 requestIdleCallback?要自定义 scheduler
    1. 兼容性问题,一个是不是所有浏览器都支持,另一个是 React 是一个跨平台,跨浏览器的解决方案,不能依赖于特定 API
    2. 无法保证优先级
    3. 无法保证任务的执行时间
    4. 自定义调度器(基于 MessageChannel)提供了更高的灵活性和性能优化。

打印 timeRemaining() 的时候会发现,部分时间超过了 16.66ms,可能是 50ms 等,这是为什么呢? 虽然每一帧绘制的时间约为 16.66ms,但是如果屏幕没有刷新,那么浏览器会安排长度为 50ms 左右的空闲时间。

为什么是 50ms?

根据研究报告表明,用户操作之后,100ms 以内的响应给用户的感觉都是瞬间发生,也就是说不会感受到延迟感,因此将空闲时间设置为 50,浏览器依然还剩下 50ms 可以处理用户的操作响应,不会让用户感到延迟。

介绍下 webwoker,限制有哪些,应用场景

是一种在浏览器中运行的独立线程,用于执行耗时的任务而不阻塞主线程(UI 渲染线程);主线程和 Worker 通过 postMessageonmessage 进行双向通信。

Web Worker 的限制有哪些?

  1. 无法操作 DOM
  2. 受限的 API
  3. 同源限制
  4. 通信开销

Web Worker 的实际使用场景

  1. 复杂计算:如大数据处理、图像处理、加密解密等。
  2. 文件处理:如文件解析(CSV、JSON)、压缩和解压缩。
  3. 实时数据处理:如音视频流处理、实时图表渲染。
  4. Web 应用性能优化:如在 React/Vue 应用中,将复杂逻辑放入 Worker 中执行。

介绍一下闭包,使用场景

内存泄漏:有垃圾没有被回收;

垃圾:不用的东西就叫垃圾;

闭包:函数 + 词法环境;一个函数可以访问其定义时作用域中变量的机制,即使这个函数在外部被调用时,依然能访问那个作用域

用于封装私有变量、实现柯里化、事件绑定状态管理等场景

闭包与内存泄漏的关系

闭包中的参数不会被回收吗?

很简单的问题,如果闭包还在用,那么他所携带的参数也就不算是垃圾,不应该被回收,如果闭包不再被引用,且参数不被别的地方使用,那么这些参数就会被回收

闭包与内存泄漏的关系就是:

  1. 有一些本该被回收的函数没有被回收,从而导致其关联的词法环境也无法被回收,最终造成内存泄漏;
  2. 当多个函数共享一个词法环境的时候,可能会造成词法环境的膨胀,从而出现无法访问且无法回收的内存空间

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 或者服务端存储

  1. domain 和 path
    • 如果未设置 domain 属性,Cookie 的作用域默认为当前域名(不包括子域名)
    • 如果设置了 domain 属性,Cookie 可以被指定域名及其子域名访问。
    • 设置了 path 确保只有特定路径下的页面能够访问 Cookie
  2. samesite 跨站:域名不一致,同源策略作为浏览器的安全基石,其「同源」判断是比较严格的,相对而言,Cookie 中的「同站」判断就比较宽松:只要两个 URL 的有效顶级域名 + 1 相同即可,不需要考虑协议和端口。例如 taobao.com 等。 跨站要不要把 cookie 带过去,就取决于 SameSite;
    • None 必须配合 secure,否则无效
    • Lax 默认值,只有 GET 请求和导航请求会带上 Cookie,其他类型的跨站请求不会带上 Cookie,对超链接放行,比如 a 的 href
    • strict:阻止所有 cookie 发送
  3. http-only:属性可以使 Cookie 只能通过 HTTP 请求访问,不能通过 JavaScript 访问。这可以防止跨站脚本攻击(XSS)通过 JavaScript 读取 Cookie
  4. 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
  1. Object.defineProperty 和 Object.assign 区别?
    • assign 是浅拷贝,只复制值,不控制属性行为。
    • defineProperty 可以控制属性行为,更底层。
  2. 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;