JS 100问

一、什么是闭包?闭包有哪些实际运用场景?闭包是如何产生的?闭包产生的变量如何被回收?

二、前端错误如何捕获,promise的错误是如何捕获的

三、HTTP的状态码

四、请说一下浏览器缓存,http缓存,什么是强缓存,什么是协商缓存,Cache-Control中max-age、no-cache的作用

五、什么是ES6迭代器

六、什么是 JavaScript 中的事件委托?

七、简述js的事件循环队列

八、常用设计模式有哪些

九、JavaScript 中构造函数有什么用?

十、数组去重方法

十一、数组扁平化

十二、局部刷新

十三、ilter/map/reduce有几个参数,第三个是干什么的?三种方法map、reduce和filter的区别是什么?

十四、promise的方法有哪些?promise的并行执行和顺序执行

十五、promise和async/await的区别

十六、promise的then属于微任务还是宏任务?

十七、set、map的区别

十八、什么是原型链

十九、深拷贝和浅拷贝的区别?怎么实现浅拷贝?怎么实现深拷贝?

二十、数据类型

二十一、防抖和节流

二十二、0.1 + 0.2、0.1 + 0.3和0.1 * 0.2分别等于多少?并解释下为什么?

二十三、JavaScript 作用域链

二十四、自执行函数

二十五、New函数做了些什么?new实现

二十六、null 和 undefined 不同之处?

二十七、js变量提升

二十八、函数提升与变量提升的优先级

二十九、 “var", "let", "const"的用法和区别?

三十、作用域

(function () {
  var val = 1;
  var json = {
    val: 10,
    dbl: function () {
      // 上级作用域一定是栈不是堆,所以它的作用域是全局
      val *= 2; // => 全局的val = 1 * 2 = 2
      // 如果把上一句代码:val *= 2改为this.val *= 2,这时候的this就是json
    },
  };
  json.dbl();
  alert(json.val + val); 
})();

var num = 10;
var obj = { num: 20 };
obj.fn = (function (num) {
  this.num = num * 3;
  num++;
  return function (n) {
    this.num += n;
    num++;
    console.log(num);
  };
})(obj.num);
var fn = obj.fn;
fn(5);
obj.fn(10);
console.log(num, obj.num);

三十一、fetch和axios的区别

三十二、JavaScript中的this-----------

三十三、Promise 链式调用的核心原理

三十四、JavaScript 中的箭头函数是什么?------

三十五、同步与异步的区别

三十六、什么是宏任务什么是微任务?

三十七、暂存性死区

三十八、浏览器的同源策略,怎么解决跨域

三十九、造成内存泄漏的操作有哪些?

四十、JS垃圾回收机制---------

四十一、cookies、sessionStorage、localStorage 的区别是什么?

四十二、对回流和重绘的理解?

四十三、Symbol标签是什么?

四十四、什么是柯里化

四十五、原生bind call apply的区别--------

四十六、Get post 的区别

四十七、Https和http区别

四十八、Slice和splice的区别

四十九、浏览器内多个标签页之间的通信方式有哪些?

五十、网页应用从服务器主动推送到客户端有那些方式?

五十一、请你解释一个为什么10.toFixed(10)会报错?

五十二、window对象和document对象有什么区别?

五十三、举例说明数组和对象的迭代方法分别有哪些?

五十四、举例说明js如何实现继承?

五十五、解释下为什么{} + [] === 0为true?

五十六、要实现一个js的持续动画,你有什么比较好的方法?

五十七、不用第三方库,说说纯js怎么实现读取和导出excel?

五十八、准确说出'1,2,3,4'.split()的结果是什么(包括类型和值)?

五十九、分析('b' + 'a' + +'a' + 'a').toLowerCase()返回的结果

六十、原生的字符串操作方法有哪些?请列举并描述其功能

六十一、请描述下函数的执行过程-------

六十二、什么是词法分析?请描述下js词法分析的过程?-----

六十三、你知道1和Number(1)的区别是什么吗?

六十四、1 和 Number(1) 的区别是什么?

六十五、BLOB URL 是什么?为什么要使用它?-----

六十六、ArrayBuffer 的理解及与 Array 的区别------------

六十七、清空数组的方式及区别

六十八、onload 和 DOMContentLoaded 的执行顺序

六十九、用户刷新网页时 JS 请求的缓存处理

七十、NaN 的含义及 typeof NaN 的结果

七十一、前端计算十万级数据的解决方案

七十二、constructor 和 instanceof 的区别---------

七十三、跳出循环的 return、break、continue 的区别

七十四、Object.freeze 的用途

七十五、所有事件都有冒泡吗

七十六、分析 !+[]+!![]+!![]+!![]+!![]+!![] 的结果

七十七、020-088 的计算过程及结果

七十八、JS sort 方法的应用场景

七十九、用户刷新/跳转/关闭时发送统计数据

八十、获取指定日期毫秒数的方法

八十一、JS 对象深比较

八十二、虚拟 DOM 是否最快?

八十三、unshift 和 push 的区别

八十四、JS 字符串截取方法

八十五、ResizeObserver 的用途

八十六、分片上传的实现

八十七、切换页面后保持 setInterval 准确

八十八、渲染大数据不卡顿的方案

八十九、说说你对promise的了解

九十、['1','2','3'].map(parseInt)的结果什么?

九十一、JS延迟加载的方式有哪些?

九十二、callee和caller的区别

九十三、typeof和instanceof的区别

九十四、Object.is和===区别

九十五、if(a==1 && a==2)成立的条件

九十六、创建一个长度为 100 且值为对应下标的数组

九十七、为何优先使用链式 setTimeout 替代 setInterval?

九十八、如何判断JS的变量类型

九十九、冒泡排序和选择排序

一百、interface和type的区别

一百零一、数组有什么方法

一百零二、什么是graphql?

一百零三、什么是Proxy?

一百零四、什么是Object.defineProperty()?

=========================================================

一、什么是闭包?闭包有哪些实际运用场景?闭包是如何产生的?闭包产生的变量如何被回收?
闭包就是指函数可以访问函数作用域外的变量。

for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, 1000);
}
console.log(i);

可以使用自执行函数结合闭包和let来解决

for(var i = 0;i<5;i++){
  (function(i){
    setTimeout(function () {
      console.log(i);
      }, 1000);
  })(i)
}

闭包的形成需满足以下两个核心条件:

函数嵌套结构

外层函数内部定义了内层函数,且内层函数被返回或传递到外部作用域。
示例:

function outer() {
  let outerVar = 10;
  function inner() {
    console.log(outerVar); // 引用外层变量
  }
  return inner;
}
const closureFn = outer(); // 产生闭包

内部函数引用外层变量

内层函数通过作用域链访问外层函数的局部变量或参数。
此时外层函数执行完毕后,其变量因被内层函数引用而无法被销毁,形成闭包

回收闭包可以采取把闭包函数赋值为null;

二、前端错误如何捕获,promise的错误是如何捕获的
window.onerror:捕获try catch以外的错误
window.adeventListener('error'):捕获try catch以外的错误
window.adeventListener('unhandledrejection'):捕获promise的错误
try catch:局部捕获

三、HTTP的状态码
1xx: 信息性响应
100:服务器收到请求,客户端可以继续发送
101:服务器同意根据客户端请求切换协议
2xx:成功
3xx:重定向
301:资源永久移动到新位置
302:资源按临时移动到新位置
304:资源未修改,使用缓存版本
4xx:客户端错误
400:请求语法错误
401:未授权访问
403:禁止访问
404:资源未找到
5xx:服务器错误
500:服务器内部错误
503:服务器暂时不可用

四、请说一下浏览器缓存,http缓存,什么是强缓存,什么是协商缓存,Cache-Control中max-age、no-cache的作用
浏览器缓存是指浏览器在用户本地存储之前请求过网页和数据,以便下次访问时可以直接从本地读取,从而减少对服务器的请求,提高加载速度。
HTTP缓存是浏览器缓存的一个方式,主要包括了强缓存和协商缓存
强缓存:
强缓存是指浏览器在请求资源时,首先先检查本地缓存,如果缓存未过期就直接使用缓存资源。强缓存通过以下两种HTTP响应头实现。
Expires: 表示资源的过期时间,是一个绝对的时间,XXXX-XX-XX,这个缺点是依赖客户端本地的时间,如果客户端本地时间与服务器时间不一致,那么会有缓存失效的问题。
Cache-control:是一个相对的时间,优先级高于Expires
1、max-age:资源最大的缓存时间,比如max-age=3600 表示缓存1个小时
2、no-cache:表示不强缓存
3、no-store:表示禁止缓存,每次都从服务器获取资源

协商缓存:
当强缓存失效后,浏览器会向服务器发送请求。服务器通过校验资源的标识来判断资源是否更新。如果资源未更新,则返回304,浏览器就直接使用本地缓存;如果资源已经更新,则返回200和新资源。
If-None-match(ETag值):上一次响应中返回的ETag值,如果相同则返回304,如果不同则返回新资源和200
Last-Modified/If-Modified-Since:服务器返回资源是携带资源最后修改的时间,浏览器再次请求的时候携带上次的Last-Modified进行对比。

什么是ES6迭代器
一、定义与核心特性
基本概念

迭代器(Iterator) 是ES6 引入的 标准化接口,用于 顺序访问 可迭代对象(如Array、Set、Map、String 等)中的元素。
它是一个 特殊对象,包含 next()方法,每次调用返回 { value: 当前元素值, done: 是否迭代完成 },当 done: true 时迭代终止。
核心作用

为不同数据结构提供 统一的访问协议,屏蔽底层差异,简化遍历逻辑。
支持 for...of循环、展开运算符(...)等新特性,实现 自动化迭代消费。
二、工作原理与实现
遍历机制

迭代器通过 指针控制 逐步访问数据结构:
初始指针指向数据起始位置;
每次调用 next() 指针后移,返回当前元素;
遍历结束指针指向末尾,done 标记为 true。
代码示例:

const arr = ['a', 'b'];
const iterator = arr[Symbol.iterator](); // 获取迭代器
console.log(iterator.next()); // { value: 'a', done: false }
console.log(iterator.next()); // { value: 'b', done: false }
console.log(iterator.next()); // { value: undefined, done: true }

可迭代对象(Iterable

任何实现 Symbol.iterator方法的对象称为 可迭代对象,该方法需返回一个迭代器实例。
JavaScript 原生支持的可迭代对象包括:Array、String、Set、Map、arguments
自定义实现

const myIterableObject = {
  data: ['a', 'b', 'c'],
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.data.length) {
          return { value: this.data[index++], done: false };
        }
        return { done: true };
      }
    };
  }
};

// 使用示例
for (const item of myIterableObject) {
  console.log(item); // 依次输出 'a', 'b', 'c'
}

for...of 循环(最常用):

for (const item of set) {
  console.log(item); // 依次输出 'a', 'b', 'c'
}

六、什么是 JavaScript 中的事件委托?
是指通过事件冒泡的机制,把事件直接绑定到父节点上面,而不是给每个子节点都绑定事件,点击子节点时会向上冒泡,父节点就可以监听到事件,并且可以根据事件的目标元素定位到具体的子元素是谁。这种可以减少事件监听器的数量,提升性能。

七、简述js的事件循环队列
首先要知道同步任务和异步任务,因为JS是单线程的,一次只能执行一个任务,如果某个任务执行时间长就会阻塞后面的任务。所以才有了同步任务和异步任务。
同步任务:完成当前任务才会执行下一个任务,执行顺序和排列顺序是一致的;
异步任务:每个任务有一个或者多个回调函数,前一个任务结束,不是执行后一个任务,而是执行的回调函数,后一个任务也不是等前一个函数的回调函数执行后再执行,所以程序的执行顺序和任务的排列顺序是不一致的。

事件队列和事件循环:
同步任务直接进行主线程,异步任务进入Event Table并且注册回调函数,然后会移入Event Quene,当主线程的任务执行完毕,就会
Event Quene中读取任务,然后把读取出来的任务放入主线程中,然后执行对应的回调任务,如此循环就形成了JS的事件循环机制。

宏任务和微任务:
这里会把setTimeoutscriptsetintervalpostMessage等放入宏任务中
promise.then则会放入微任务中

当执行栈的任务清空,主线程会优先从微任务中寻找是否有任务,如果有就先执行微任务中的任务,直到微任务清空,然后会去寻找宏任务中是否还有任务,如果有就会把宏任务的第一个任务加入到执行栈中,然后又回去检查微任务是否执行完毕,如此循环,直到所有的任务执行完毕。

八、常用设计模式有哪些
创建型:
单例模式
工厂模式
构造函数模式
原型模式
建造者模式

结构型:
装饰器模式
代理模式
适配器模式
组合模式

行为型:
观察者模式
迭代器模式

九、JavaScript 中构造函数有什么用?
构造函数主要用于创建对象,并初始化对象的属性和方法。

function Person(name,age){
  this.name  = name
  this.age = age
}
const person = new Person('x',2);

十、数组去重方法

  1. Set数据结构法(ES6推荐)
const uniqueArr = [...new Set(arr)];  // 单行实现去重

特点:

代码简洁,时间复杂度O(n)
无法处理NaN和对象类型重复问题

  1. filter + indexOf
const uniqueArr = arr.filter((item, index) => arr.indexOf(item) === index);  // 保留首次出现元素

注意:

兼容性较好(需ES5+支持)
无法区分1和'1'等类型差异

  1. reduce累积法
const uniqueArr = arr.reduce((acc, cur) => 
  acc.includes(cur) ? acc : [...acc, cur], []
);  // 显式判断存在性

适用场景:

需自定义去重逻辑时
支持链式操作(如结合map/filter)

十一、数组扁平化
ES6的flat方法

function func(arr){
  return arr.flat(Infinity)
}

字符串转换

arr.toString().split(",").map((Item)=>Number(item))

reduce递归遍历

function flatten(){
  return arr.reduce((pre,current)=>{
     if(Array.isArray(current){
        return pre.concat(flatten(current))
      }else{
      return pre.concat(current)
    }
  }
}

十二、局部刷新
使用Ajax,在不需要重新加载整个页面的情况下更新页面的某一个部分,提升用户体验,减少数据传输量,提高页面响应速度。
React/Vue 使用v-ifv-show 等 或者state的控制来动态显示页面内容

十三、 filter/map/reduce有几个参数,第三个是干什么的?三种方法map、reduce和filter的区别是什么?
filtermapreducefind;
filtermapfind:3个参数 第一个是当前值 第二个是当前index 第三个是原始数组
reduce: 4个参数 第一个是累加器 第二个是当前值 第三个是当前index 第四个是原始数组

原始数组是可以在数据处理的过程中做一些判断
示例:

[1, 3, 5, 7].filter((item, index, arr) => item > arr[index-1]);  
// 输出 [3,5,7](判断是否大于前一个元素)  

map:在映射过程中获取原数组,用于动态调整转换规则(如根据数组长度计算)
示例:

[10, 20, 30].map((item, index, arr) => item + arr.length);  
// 输出 [13,23,33](元素值 + 数组长度)  

find:在查找过程中依赖原数组的全局状态(如判断元素与其他元素的关系)
示例:

[2, 4, 6, 8].find((item, index, arr) => item === arr[index+1]/2);  
// 输出 4(因为 8 是 4 的两倍)  

reduce的第四个参数

[1, 2, 3].reduce((acc, cur, index, arr) => {  
  return acc + cur * arr.length; // 累加器 + 当前值 × 数组长度  
}, 0);  
// 计算过程:0+1×3=3 → 3+2×3=9 → 9+3×3=18,最终输出 18 

十四、promise的方法有哪些?promise的并行执行和顺序执行
promise.race:执行的最快的一个promise,无论是否成功或者失败
promise.all:所有promise执行完成后执行,必须都成功
promise.allSetttled:所有promise执行完成后执行,无论是否都成功
promise.any:任意一个返回成功的promise
promise.resolve:创建一个已完成的promise
promise.reject:创建一个已拒绝的promise

promise.allpromise.allSetttled可以做并行执行
promise.then:链式调用就可以做顺序执行

十五、promise和async/await的区别
风格:promise链式调用;async/await风格类似于同步代码,结构清晰
错误:promise使用catch捕获错误,无法定位源头;async/await使用try/catch捕获错误
并发:promise使用promise.all;async/await需要借助promise.all
可读性:promise链式调用,容易产生回调地狱;async/await可读性更高,顺序执行逻辑直观

十六、promise的then属于微任务还是宏任务?
微任务

十七、set、map的区别
一、核心区别
特性 | Set | Map
存储内容 | 唯一值的集合(无重复元素)| 键值对的集合(键唯一,值可重复)
键类型 | 直接存储值(无键) | 键可以是任意类型(对象、函数等)
操作值 | 无键值对,直接通过值操作 | 必须通过键访问或修改值
去重机制 | 自动去重(基于值严格相等) | 键唯一,值可重复
初始化方式

new Set([1,2,3])    
new Map([['key1',1], ['key2',2]])

二、方法对比
操作 | Set | Map
添加元素 |add(value) |set(key, value)
删除元素 | delete(value) | delete(key)
检查存在性 | has(value) | has(key)
清空 | clear() | clear()
元素数量 | size属性 | size属性
三、使用场景

  1. Set的典型场景
    去重数组:快速过滤重复值
const unique = [...new Set([1,2,2,3])]; // [1,2,3]

成员存在性检查:比数组的includes()更高效

const tags = new Set(["js", "css"]);
tags.has("js"); // true

集合运算:交集、并集、差集

const a = new Set([1,2]);
const b = new Set([2,3]);
const intersection = new Set([...a].filter(x => b.has(x))); // Set {2}
const intersection = new Set([...a].filter(x => !b.has(x))); // Set {1}
const intersection = new Set([...a,...b].filter(x => !b.has(x))); // Set {1,3}
const intersection = new Set([...a,...b]); // Set {1,2,3}
  1. Map的典型场景
    键值对存储:键可以是对象、函数等复杂类型
const objKey = { id: 1 };
const map = new Map();
map.set(objKey, "data"); // 键是对象

保留插入顺序:遍历顺序与添加顺序一致(普通对象无此特性)

const map = new Map([['a',1], ['b',2]]);
for (const [key, val] of map) { console.log(key); } // 'a' → 'b'

替代对象:当需要频繁增删键值对时性能更优
四、底层实现差异
特性 | Set | Map
内存占用 | 存储单一值,结构更轻量 | 存储键值对,占用更高
查找效率 | O(1)(哈希表实现) | O(1)(哈希表实现)
迭代顺序 | 按插入顺序遍历 | 按插入顺序遍历
五、代码示例
Set示例

const set = new Set();
set.add(1).add(2).add(2); // Set {1,2}
console.log([...set]);    // [1,2]

Map示例

const map = new Map();
map.set('name', 'Alice').set({ id: 1 }, true);
console.log(map.get('name')); // 'Alice'

总结
需要唯一值集合 → 用Set
需要复杂键值对 → 用Map
需要高性能查找 → 优先选Set/Map而非数组或普通对象

JavaScript中数据结构的有序性对比

一、无序的数据结构
Set(集合)
无序特性:
存储唯一值,但元素不按插入顺序排列,无法通过索引访问。
底层通过哈希表实现,元素顺序由哈希算法决定。
示例:

const set = new Set([3, 1, 2]);  
console.log([...set]); // 输出可能为 [3,1,2](顺序不固定)

二、有序的数据结构
Map(映射)

有序特性:
键值对严格按插入顺序排列,遍历时与添加顺序一致。
支持通过迭代器或forEach按顺序访问键值对。
示例:

const map = new Map();  
map.set('a', 1).set('b', 2);  
for (const [key] of map) { console.log(key); } // 输出顺序:'a' → 'b'

数组(Array)

有序特性:
元素按索引顺序存储,可通过下标直接访问。
插入、删除操作会动态调整后续元素的索引位置。
对比总结
数据结构 | 是否有序 | 底层实现 | 典型场景
Set | 无序(哈希表决定顺序) | 哈希表 | 去重、存在性检查
Map | 有序(插入顺序) | 哈希表+双向链表 | 键值对存储、有序遍历
数组 | 有序(索引顺序) | 连续内存空间 | 顺序操作、索引访问

十八、什么是原型链
原型链(Prototype Chain) 是 JavaScript 实现继承的核心机制。每个对象通过 [[Prototype]](通过__proto__Object.getPrototypeOf()访问)指向其原型对象,形成链式结构。属性查找时,JavaScript 会沿着这条链向上追溯,直到找到目标或到达链顶(null)。

关键规则与示例

  1. 原型层级关系
    构造函数:通过prototype 属性指向其原型对象;
    实例:通过 __proto__指向构造函数的原型;
    原型对象:自身的__proto__ 指向更高层原型(如 Object.prototype),最终指向null
    代码示例:
function Person(name) {
  this.name = name;
}

// 为构造函数的原型添加方法
Person.prototype.sayHi = function() { 
  console.log(`Hi, I'm ${this.name}`);
};

const alice = new Person("Alice");

// 实例的原型链指向关系
console.log(alice.__proto__ === Person.prototype);     // true
console.log(Person.prototype.__proto__ === Object.prototype);  // true
console.log(Object.prototype.__proto__);               // null
  1. 继承与查找流程
    当访问对象的属性或方法时:
    检查对象自身属性;
    未找到则沿原型链逐层向上查找;
    若到 Object.prototype仍未找到,返回 undefined
    示例:
alice.sayHi();  // 调用原型链上的方法,输出 "Hi, I'm Alice"
alice.toString();  // 继承自 Object.prototype 的方法

原型链的显式操作

  1. 修改原型链
    设置原型:使用 Object.create()Object.setPrototypeOf()
const parent = { age: 30 };
const child = Object.create(parent);  // child.__proto__ = parent
console.log(child.age);  // 30(通过原型链继承)
  1. 检测原型关系
    instanceof 运算符:检测构造函数的prototype 是否在对象的原型链上;
    Object.prototype.isPrototypeOf():直接检测原型关系。
console.log(alice instanceof Person);  // true
console.log(Person.prototype.isPrototypeOf(alice));  // true

十九、深拷贝和浅拷贝的区别?怎么实现浅拷贝?怎么实现深拷贝?

区别 | 浅拷贝 | 深拷贝
复制层级:浅拷贝仅复制第一层,嵌套的引用类型仍使用共享内存地址;递归复制所有层级的属性,嵌套的引用类型生成完全独立的新对象。
内存引用:新旧对象共享嵌套属性的内存,修改嵌套属性会互相影响;互相独立不影响
数据类型:基本类型直接复制,引用类型复制地址;无论基本还是引用都递归复制值
性能:速度快;速度慢
浅拷贝:Object.assign()slice()、展开运算符、concat()
深拷贝:
JSON.parse(JSON.stringify(obj)):无法处理函数、undefined、Date等类型
原生structuredClone():支持数据类型全面 不能克隆函数和DOM(使用cloneNode(true)方法),以及兼容性不太好


function deepClone(target) {
  // 处理DOM节点
  if (target instanceof Node) {
    return target.cloneNode(true);  // 深度克隆DOM树
  }
  
  // 处理函数对象
  if (typeof target === 'function') {
    return cloneFunction(target);  // 函数克隆逻辑
  }

  // 默认使用structuredClone
  return structuredClone(target);  // 处理常规对象
}

function cloneFunction(fn) {
  return new Function('return ' + fn.toString())();  // 通过字符串重构函数
}

还有就是使用递归遍历生成新的对象

二十、数据类型
基本类型:boolean number string undefined null symbol
存放于栈内存,值是不可变
引用类型:object function Array Date promise
存放于堆内存中,值是可改变

二十一、防抖和节流
防抖:事件停止触发后延迟执行,多次执行只执行最后一次
节流:固定时间间隔执行一次,无论触发频率如何

function debounce(func, delay) {
  let timeoutId;
  return function(...args) {
    const context = this;
    clearTimeout(timeoutId); // 清除之前的计时器
    timeoutId = setTimeout(() => {
      func.apply(context, args); // 延迟执行
    }, delay);
  };
}

节流

function throttle(func, limit) {
  let lastExecTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastExecTime >= limit) {
      func.apply(this, args);
      lastExecTime = now;
    }
  };
}

二十二、0.1 + 0.2、0.1 + 0.3和0.1 * 0.2分别等于多少?并解释下为什么?
0.1 + 0.2 = 0.30000000000004
0.1+0.3 = 0.4 //因为二进制误差相互抵消
0.1 * 0.2 = 0.02000000000004
因为计算机使用二进制表示小数,0.1和0.2在二进制中表示的无限循环小数,因为IEEE 754标准。超出的位数会被截断,导致精度丢失

可以用(0.1 * 10 + 0.2 * 10 ) / 10 来处理

二十三、JavaScript 作用域链
一、基本概念
作用域链(Scope Chain)是 JavaScript 中用于变量查找的链式结构,由当前作用域及其所有父级作用域组成。当访问一个变量时,引擎会沿着作用域链逐级向上查找,直到找到该变量或到达全局作用域。

二、核心特性
词法作用域
作用域链在函数定义时确定,而非调用时,这种静态作用域特性称为词法作用域。例如:

function outer() {
  let x = 10;
  function inner() {
    console.log(x); // 通过作用域链访问outer的x
  }
  return inner;
}
const fn = outer();
fn(); // 输出10

嵌套结构
每个函数在创建时会保存其外部作用域的引用,形成嵌套链式结构。内部函数可访问外部变量,反之则不行。

与执行上下文的关系
函数调用时会创建执行上下文,其作用域链由函数自身的变量对象(AO)和定义时的作用域链组成

二十四、自执行函数
是JS中定义后会被立即执行的函数模式,主要用于创建独立的作用域、避免变量污染等

作用:
隔绝作用域
模块化开发
自动初始化逻辑

二十五、New函数做了些什么?new实现
创建一个空对象

let newObj = {}

绑定原型链

newObj.__proto__ = Constructor.prototype;

绑定this并执行构造函数

let result = Constructor.apply(newObj,args)

处理返回值:
如果返回对象和函数则直接返回结果;
如果返回的是基本类型,则返回新的对象

function myNew(Constructor,...args){
  let newObj = Object.create(Constructor.prototype);
  let result = Constructor.apply(newObj,args)
  return result instanof Object ? result :newObj
}

二十六、null 和 undefined 不同之处?
null:类型为object,一般是开发者主动赋值,转换为数值是0
undefined: 类型为undefined,JS默认赋值,转换为数值是NaN

null == undefined //true
null === undefined //false

二十七、js变量提升
函数:优先级最高,会被提升到最顶部
var:优先级次之,会被提升到顶部,但是访问值是undefined
let:声明提升但没有初始化,生命访问会报错

函数声明会提升到顶部,函数表达式则等同于varlet会报错

二十八、函数提升与变量提升的优先级
函数提升的优先级最高,然后是变量提升

console.log(foo);  //函数
var foo = 1;
console.log(foo)   //1
function foo(){
  console.log(xxx)
}

二十九、 “var", "let", "const"的用法和区别?
var 函数作用域,函数内有效 初始值undefined 声明前可访问 变量提升
let/const块级作用域{}内有效 无初始值 声明前不可访问 变量提升但是不初始化

三十、作用域

(function () {
  var val = 1;
  var json = {
    val: 10,
    dbl: function () {
      // 上级作用域一定是栈不是堆,所以它的作用域是全局
      val *= 2; // => 全局的val = 1 * 2 = 2
      // 如果把上一句代码:val *= 2改为this.val *= 2,这时候的this就是json
    },
  };
  json.dbl();
  alert(json.val + val); // 10 + 2 = 12
})();
// 作用域:栈内存、执行上下文,这仨是一个玩意。var json 等于个对象,首先对象是堆内存,所以json是堆不叫栈,作用域只能是栈

// => "12"

var num = 10;
var obj = { num: 20 };
obj.fn = (function (num) {
  this.num = num * 3;
  num++;
  return function (n) {
    this.num += n;
    num++;
    console.log(num);
  };
})(obj.num);
var fn = obj.fn;
fn(5);
obj.fn(10);
console.log(num, obj.num);

知识点:obj.fn被赋值为 IIFE 的返回值(即闭包函数),因此调用 obj.fn()实际执行的是闭包逻辑

三十一、fetch和axios的区别
fetch:原生Api,不支持IE,需手动处理JSON解析和响应状态码,仅网络故障才触发错误处理,不支持请求拦截,数据格式处理需要手动设置,默认不带Cookie
axios:第三方库,支持IE,自动解析JSON,HTTP错误状态码都会自动触发catch,支持请求拦截,数据格式自动化处理,可直接使用响应数据,默认携带Cookie。

三十二、JavaScript中的this
全局环境:window 严格模式undefined
普通函数:window 严格模式undefined
对象方法调用:指向调用该方法的对象
构造函数:指向新创建的实例对象
事件处理函数:指向触发事件的DOM
显示绑定:callapplybind强制绑定
箭头函数:继承非箭头函数的this

函数嵌套中的this丢失:

const obj = {
  name:"obj",
  run:function(){
    setTimeout(function(){
    console.log(this.name) //undefined
    },100)
  }
obj.run()
   setTimeout(()=>{
    console.log(this.name) //箭头函数继承外层的this
    },100)

   setTimeout(function(){
    console.log(this.name) 
    }.bind(obj),100)//bind绑定this对象

方法赋值导致的this丢失

const run = obj.run;
run() //this 变为 window或者undefined

三十三、Promise 链式调用的核心原理
Promise 的链式调用能力源于其每次返回新 Promise 的设计,结合值穿透和状态继承机制,实现了异步操作的线性编排。这种模式显著提升了异步代码的可读性和可维护性

三十四、JavaScript 中的箭头函数是什么?
箭头函数(Arrow Function) 是 ES6 引入的简化函数语法,通过 => 符号定义,常用于替代匿名函数表达式。其核心特性包括:
简写语法:省略 function 关键字;
词法作用域:箭头函数无独立的 this、arguments、super 或 new.target,这些值由外层作用域决定

三十五、同步与异步的区别
JS是单线程的,在主线程中只能一个一个任务按顺序执行,任务会阻塞进程,这样的任务叫做同步任务。
异步任务,如提交后无需等待结果,后续代码立即执行,通过回调获取结果,不会阻塞主线程

三十六、什么是宏任务什么是微任务?
异步任务又分为宏任务和微任务,宏任务则是setTimeoutsetInterval等 由浏览器触发,多个任务队列,按优先级执行
微任务则是promise.thenMutationObserver等,由JS引擎触发,单一任务队列,按顺序执行,不阻塞渲染

三十七、暂存性死区
let/const 会变量提升到顶部,但是不会初始化数据,这个时候访问就会报错,这个就是TDZ暂存形死区

三十八、浏览器的同源策略,怎么解决跨域
同一个协议、域名、端口称之为同源
跨域可以用jsonpnginx/nodejs代理、CORS

三十九、造成内存泄漏的操作有哪些?
闭包,未清除的事件监听和定时器,全局变量,Dom元素引用没有清理

使用谷歌开发工具的Memory面板分析堆快照

四十、JS垃圾回收机制
一、核心机制
JavaScript 通过自动垃圾回收(Garbage Collection, GC)管理内存,开发者无需手动释放资源。其核心逻辑是识别并清理不再使用的内存对象,防止内存泄漏并优化性能。

二、垃圾回收算法
标记-清除(Mark-Sweep)

工作原理:
从根对象(如全局对象、活动执行上下文)出发,标记所有可达对象;
清除未被标记的对象,释放其内存空间。
优势:解决循环引用问题(如两个对象互相引用但无外部引用)。
缺陷:可能产生内存碎片,需后续整理
三、V8引擎的优化策略
分代回收
新生代(副垃圾回收器):
存放生存周期短的对象(如局部变量);
采用 Scavenge算法,将内存分为“对象区”和“空闲区”,通过复制存活对象完成回收并整理内存。
老生代(主垃圾回收器):
存放生存周期长的对象(如全局变量、闭包变量);
采用 标记-清除 + 整理算法,减少碎片化。
增量标记与惰性清理
将标记过程拆分为多个小步骤,避免长时间阻塞主线程。
四、内存生命周期与回收场景
内存分配策略
栈内存:存储基本数据类型和执行上下文(如函数调用栈),通过移动ESP 指针快速释放。
堆内存:存储引用类型(如对象、数组),依赖垃圾回收器管理。
回收触发时机
周期性执行:垃圾回收器按固定时间间隔运行,避免频繁操作影响性能。
内存不足时:当堆内存接近上限时触发回收。
五、开发注意事项(避免内存泄漏)
意外的全局变量

未声明的变量会挂载到 window,需显式置为 null 解除引用。

function leak() {
  leakedVar = '意外全局变量'; // 错误:未声明导致全局泄漏
  window.globalObj = {};    // 需手动释放:globalObj = null;
}

闭包引用未释放

闭包引用的外部变量会持续占用内存,需及时解除引用。
DOM 引用未清理

移除 DOM 元素时需同步删除其 JavaScript 引用。

const element = document.getElementById('node');
document.body.removeChild(element); // 移除 DOM
element = null;                     // 解除引用

定时器/事件监听未清除

组件销毁时需清理定时器和事件监听。
总结
JavaScript 垃圾回收通过标记-清除算法为核心,结合V8的分代回收与增量优化,实现高效内存管理。开发者需关注全局变量、闭包、DOM 引用等场景,避免内存泄漏

四十一、cookies、sessionStorage、localStorage 的区别是什么?
cookies:存储大小比较小,存储到cookie文件夹中,每次访问相同域名时。会把cookie传递到服务器,可以设置生命周期。
sessionStorage:存储大小一般是5M,存储到sessionStorage数据库中,会话结束后会被清空;
localStorage: 存储大小一般是5M,存储到localStorage数据库中,永久保存不会清空

四十二、对回流和重绘的理解?
回流是指DOM需要重新计算和重新布局页面中的元素,比如删除和添加元素或者改变DOM的尺寸;
重绘是指需要改变DOM的样式即视觉属性发生变化,但是不影响布局,比如更改颜色
减少重绘重排次数;
避免使用table布局,推荐使用flex和grid布局
批量修改DOM或者虚拟DOM技术

四十三、Symbol标签是什么?

Symbol类型核心作用解析
Symbol 是 JavaScript 引入的一种基本数据类型(第 7 种原始类型),主要用于生成唯一且不可变的值,解决对象属性命名冲突问题,并支持特殊行为的自定义。
以下是其核心用途:

一、核心特性
唯一性

通过 Symbol()创建的每个值都是唯一的,即使传入相同描述符也不相等。

const s1 = Symbol('key');
const s2 = Symbol('key');
console.log(s1 === s2); // false

不可变性

Symbol值不可修改,且无法通过new实例化(非构造函数)。
二、核心用途
避免对象属性名冲突

作为对象属性的键时,Symbol 可确保属性名唯一,防止被无意覆盖。

const uid = Symbol('id');
const user = {
  [uid]: '123', // 唯一键名
  name: 'Alice'
};

定义内置特殊行为

通过内置Symbol 值(如 Symbol.iterator、Symbol.toStringTag)可自定义对象的迭代、类型标识等行为。

const obj = {
  [Symbol.iterator]: function* () { yield 1; yield 2; } // 自定义迭代逻辑
};

消除魔术字符串(Magic Strings

使用 Symbol 替换代码中重复出现的字符串常量,提升可维护性。

const LOG_LEVEL = {
  DEBUG: Symbol('debug'),
  ERROR: Symbol('error')
};

全局共享Symbol

通过 Symbol.for(key) 创建或获取全局注册的Symbol,实现跨模块共享。

const globalSym = Symbol.for('globalKey'); // 全局注册
const sameSym = Symbol.for('globalKey');
console.log(globalSym === sameSym); // true 

模拟私有属性(非严格安全)

结合闭包或模块作用域,Symbol可间接实现对象属性的“私有性”(需注意通过 Object.getOwnPropertySymbols 仍可被访问)。
三、与其他数据类型的对比
特性 Symbol String/Number
唯一性 ✅ 每个值唯一 ❌ 值可能重复
作为对象属性键 ✅ 支持唯一键名 ✅ 字符串键名
可遍历性 ❌ 默认不可枚举 ✅ 可枚举
总结
Symbol 的核心价值在于生成唯一标识符,解决属性名冲突问题,并通过内置Symbol值自定义对象行为。其特性使其适用于模块化开发、框架设计及需要唯一键的高级场景,但需注意其并非严格意义上的私有属性实现方案

四十四、什么是柯里化
接收多个参数的函数编程接受一个单一参数的函数,并且返回接受余下参数的结果的新函数的技术。

function add(a){
  return function(b){
    return function(c){
      return a+b+c
    }
  }
}
add(1)(2)(3)

const add2 = a => b => c => a+b+c //es6
function add(){
    let args = [...arguments]
    const adder = ()=>{
        args.push(...arguments)
        return adder
    }
    adder.toString = ()=>args.reduce((a,b)=>a+b,0)
    return adder
}
可以实现 add(1,2,3)和add(1,2)(3)以及add(1)(2)(3)

四十五、原生bind call apply的区别
参数格式:
call:逗号分隔,参数列表
bind:参数列表逗号分隔,后续还可以增加参数

const _function = func.bind(context,arg1)
_function(arg2) //追加参数arg2

apply:数组或者类数组
执行时机:
call:立即执行函数
bind:返回新函数,需要手动调用执行
apply:立即执行函数
返回值:
call:无
bind:返回了绑定this的新韩淑
apply:无

bind多次调用bind,新的this绑定会失效,会以首次绑定为准

const obj = {x:1}
const obj2 = function(){console.log(this.x)}
const bundFunc = obj2.bind(obj);
bundFunc()  //1
bundFunc.bind({x:2})() //1

四十六、Get post 的区别
参数传递方式:明文URL传递;参数存储在请求体,不可见;
安全性:明文容易被缓存和记录,不适合传递敏感信息;参数不直接暴露,相对更隐蔽;
数据长度限制:受URL长度限制;理论上无限制;
缓存与历史记录:可被浏览器缓存、保留历史记录和书签;默认不缓存。

四十七、Https和http区别
协议基础:基于SSL/TLS加密传输;明文传输
默认端口:443;80
安全性:加密传输,防篡改,防身份伪造;数据易被篡改;
证书:需要CA颁发的数字证书;无需证书;
SEO:会优先索引HTTPS的站点;

四十八、Slice和splice的区别
区别 | slice | splice
核心功能:提取数组或者字符串的指定范围的内容;对数组进行删除、替换、插入操作;
副作用:不修改原数组/字符串;直接修改原数组;
返回值:新的数组/字符串;被删除的数组,无删除则为空
splice: startIndex 起始位置 负数表示倒数
deleteCount:删除的数量,如果是0 表示插入
...items:插入新的元素或者替换原有元素;
特殊行为:
slice:支持负数索引,表示倒数的数量,如果不传任何参数,则表示浅拷贝;
splice:支持负数索引,表示从倒数第几个开始。

四十九、浏览器内多个标签页之间的通信方式有哪些?
postMessage集合window.addeventListener('message',()=>{})webSocketlocalStorage/sessionStorage结合window.addeventListener('storage',()=>{})BroadcastChannel结合postMeesage

//发送方
const bc = new BroadcastChannel('test');
bc.postMessage({type:"test",data:"hello"})
//接收方
const bc = new BroadcastChannel('test');
bc.onmessage = (message)=>{}

五十、网页应用从服务器主动推送到客户端有那些方式?
webSocket(双向)、SSE(单向)、长轮询、短轮询;

五十一、请你解释一个为什么10.toFixed(10)会报错?
js解释器会将第一个.当作小数点来处理,如果不想它报错,就需要把10用()包裹起来。或者10..toFixed(2)用两个小数点

五十二、window对象和document对象有什么区别?
本质:浏览器窗口,标签页的顶层全局对象,代表整个浏览器环境;window的子属性,代表的是当前加载的HTML文档,是DOM树的根节点
核心职责:控制浏览器窗口的行为,管理全局的作用域;操作页面内容

五十三、举例说明数组和对象的迭代方法分别有哪些?

forEach

没有返回值

var a = [1,2,3,4,5]
var b = a.forEach((item) => {
   item = item * 2
})
console.log(b) // undefined

无法中断执行
forEach 遍历过程中无法中断执行,如果希望符合某种条件时,就中断遍历,要使用for循环。

var arr = [1, 2, 3];
 
for (var i = 0; i < arr.length; i++) {
  if (arr[i] === 2) break;
  console.log(arr[i]);
}

跳过空位
forEach()方法也会跳过数组的空位。

var a = [null, , undefined]
for (let i = 0; i < a.length; i++) {
    console.log('a', a[i]) // null undefined undefined
}
a.forEach(item => {
    console.log('item', item) // null undefined
});

上面代码中,forEach()方法不会跳过undefined和null,但会跳过空位。而for循环不会跳过空位,会认为是undefined。

改变数组情况
下面来看几个例子:

var a = [1,2,3,4,5]
a.forEach((item) => {
    item = item * 2
})
console.log(a) // [1,2,3,4,5]

这里原数组并没有发生改变。

var a = [1,'1',{num:1},true] 
a.forEach((item, index, arr) => {
    item = 2 
}) 
console.log(a) // [1,'1',{num:1},true]

这里修改item的值,依然没有修改原数组。

var a = [1,'1',{num:1},true] 
a.forEach((item, index, arr) => { 
    item.num = 2 
    item = 2
}) 
console.log(a)  // [1,'1',{num:2},true]

当修改数组中对象的某个属性时,发现属性改变了。其他值依旧没有改变。

为什么会这样呢?
这里就要引入栈(stack)内存和堆(heap)内存的概念了,对于JS中的基本数据类型,如String,Number,Boolean,Undefined,Null是存在于栈内存中的,在栈内存中储存变量名及相应的值。而Object,Array,Function存在于堆内存中,在堆内存中储存变量名及引用位置。

在第一个例子中,为什么直接修改item无法修改原数组呢,因为item的值并不是相应的原数组中的值,而是重新建立的一个新变量,值和原数组相同。因此,如果item是基础数据类型,那么并不会改变数组里面的值,如果是引用类型,那么item和数组里面的值是指向同一内存地址,则都会被改变。
在第二个例子中,数组中的对象的值也没有改变,是因为新创建的变量和原数组中的对象虽然指向同一个地址,但改变的是新变量的值,也就是重新赋值,即新对象的值为2,原数组中的对象还是{num:1}。
在第三个例子中,由于对象是引用类型,新对象和旧对象指向的都是同一个地址,所以新对象把num变成了2,原数组中的对象也改变了。

var a = [1,2,3,4,5] 
a.forEach((item, index, arr) => { 
    arr[index] = item * 2 
}) 
console.log(a)  // [2,4,6,8,10]

在回调函数里改变arr的值,原数组改变了。

这个例子和例三其实同理,参数中的arr也只是原数组的一个拷贝,但是arr是引用类型。如果修改数组中的某一项则原数组也改变因为指向同一引用地址,而如果给参数arr赋其他值,那就是重新赋值,则原数组不变。如下:

var a = [1,2,3,4,5] 
a.forEach((item, index, arr) => { 
    arr = 2
}) 
console.log(a)  // [1,2,3,4,5]

map

有返回值
返回一个经过处理后的新数组,但不改变原数组的值。

var a = [1,2,3,4,5]
var b = a.map((item) => {
    return item = item * 2
})
console.log(a)  // [1,2,3,4,5]
console.log(b)  // [2,4,6,8,10]

无法中断执行
同forEach,无法中断执行

跳过空位
同forEach,会跳过空位

改变数组情况
map中可改变原数组的情况和原理与forEach相同

注意事项
虽然map和forEach不能像for循环那样采用break跳出整个循环,但可以曲线救国,采用try...catch并搭配throw error的方式来跳出整个循环(强制退出,非必要不建议)。

以foreach 循环为例:

var arr = [1,2,3]
var newArr = []
arr.forEach((item,index)=>{
    try{
        if(index > 1) {
            throw new Error('文本小于2')
        }
        newArr.push(item)
    }catch (e){
        // throw e
    }
})
console.log(newArr) [1,2]

性能对比
for 循环当然是最简单的,因为它没有任何额外的函数调用栈和上下文;

forEach 其次,因为它其实比我们想象得要复杂一些,它的函数签名实际上是array.forEach(function(currentValue, index, arr), thisValue)它不是普通的 for 循环的语法糖,还有诸多参数和上下文需要在执行的时候考虑进来,这里可能拖慢性能

map 最慢,因为它的返回值是一个等长的全新的数组,数组创建和赋值产生的性能开销较大。

对象用
for inObject.keysObject.valuesObejct.entries

一、for...in 循环
特点:遍历对象及其原型链上的所有可枚举属性
典型用法:

for (const key in obj) {
  if (obj.hasOwnProperty(key)) { // 过滤原型链属性
    console.log(key, obj[key]);
  }
}

注意点:属性顺序不可预测,需配合 hasOwnProperty()使用
二、Object.keys()
特点:返回对象自身可枚举属性名的数组(不包含原型链属性)
典型用法:

Object.keys(obj).forEach(key => {
  console.log(key, obj[key]);
});

三、Object.values()
特点:返回对象自身可枚举属性值的数组
典型用法:

Object.values(obj).forEach(value => {
  console.log(value);
});

四、Object.entries()
特点:返回对象自身可枚举属性的键值对数组(格式为 [key, value])
典型用法:

Object.entries(obj).forEach(([key, value]) => {
  console.log(key, value);
});

五十四、举例说明js如何实现继承?
JavaScript 实现继承的 5 种核心方式与示例

  1. 原型链继承
    实现原理:将子构造函数的原型指向父构造函数的实例,通过原型链继承父类的属性和方法。
    代码示例:
function Parent() {  
  this.name = 'Parent';  
  this.colors = ['red', 'blue'];  
}  
Parent.prototype.sayName = function() {  
  console.log(this.name);  
};  

function Child() {  
  this.type = 'Child';  
}  
// 核心代码:子类原型指向父类实例  
Child.prototype = new Parent();  

const child1 = new Child();  
child1.colors.push('green');  
const child2 = new Child();  
console.log(child2.colors); // ['red', 'blue', 'green'](共享引用类型属性)  

缺点:
所有子类实例共享父类实例的引用类型属性。
无法向父类构造函数传参。

  1. 构造函数继承
    实现原理:在子类构造函数中通过 call() 或 apply() 调用父类构造函数,实现属性拷贝。
    代码示例:
function Parent(name) {  
  this.name = name;  
  this.colors = ['red', 'blue'];  
}  

function Child(name) {  
  Parent.call(this, name); // 核心代码:调用父类构造函数  
}  

const child1 = new Child('Child1');  
child1.colors.push('green');  
const child2 = new Child('Child2');  
console.log(child2.colors); // ['red', 'blue'](不共享引用类型属性)  

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

  1. 组合继承(原型链 + 构造函数)
    实现原理:结合原型链继承方法的复用性与构造函数继承属性的独立性。
    代码示例:
function Parent(name) {  
  this.name = name;  
  this.colors = ['red', 'blue'];  
}  
Parent.prototype.sayName = function() {  
  console.log(this.name);  
};  

function Child(name) {  
  Parent.call(this, name); // 构造函数继承属性  
}  
Child.prototype = new Parent(); // 原型链继承方法  

const child = new Child('Child');  
child.sayName(); // 'Child'  

缺点:
父类构造函数被调用两次(性能问题)。

  1. 寄生组合继承(推荐)
    实现原理:通过 Object.create() 创建父类原型的副本,避免重复调用父类构造函数。
    代码示例:
function Parent(name) {  
  this.name = name;  
}  
Parent.prototype.sayName = function() {  
  console.log(this.name);  
};  

function Child(name) {  
  Parent.call(this, name);  
}  
// 核心代码:优化原型链继承  
Child.prototype = Object.create(Parent.prototype);  
Child.prototype.constructor = Child; // 修复构造函数指向  

const child = new Child('Child');  
child.sayName(); // 'Child'  

优点:
解决组合继承的性能问题,是 ES5 时代的最佳实践。

  1. ES6 class 继承
    实现原理:使用 extends 和 super 关键字实现语法糖级继承。
    代码示例:
class Parent {  
  constructor(name) {  
    this.name = name;  
  }  
  sayName() {  
    console.log(this.name);  
  }  
}  

class Child extends Parent {  
  constructor(name) {  
    super(name); // 核心代码:调用父类构造函数  
  }  
}  

const child = new Child('Child');  
child.sayName(); // 'Child'  

优点:
语法简洁,底层仍基于寄生组合继承。

五十五、解释下为什么{} + [] === 0为true?
{}会被优先转换为空代码块,就没有值, []会被转换为toString成 ”“ 然后进一步被转换成0 所以最后是true
0+[] === 0 是false 是因为[]被转换为字符串 0+字符串 就变成了”0“

五十六、要实现一个js的持续动画,你有什么比较好的方法?
requestAnamiateFrameGSAP

五十七、不用第三方库,说说纯js怎么实现读取和导出excel?
读取的话只能读取CSV文件,用fileReaderApi;
导出则是把csv格式的文件转换为Bloba标签下载

五十八、准确说出'1,2,3,4'.split()的结果是什么(包括类型和值)?
是一个数组,字符串数组

['1,2,3,4'] //split 方法如果不传参数,默认会将整个字符串作为一个元素放入数组中
['1','2','3','4'] //用逗号分隔

五十九、分析('b' + 'a' + +'a' + 'a').toLowerCase()返回的结果
关键在于+'a' = NaN
所以是('b' + 'a' + NaN + 'a').toLowerCase() = 'banana'

六十、原生的字符串操作方法有哪些?请列举并描述其功能
一、基础操作与索引访问
索引操作
charAt(index):返回指定索引位置的字符,若索引越界则返回空字符串。
charCodeAt(index):返回指定位置字符的 Unicode 编码(越界返回 NaN)。
at(index):支持正负索引,负数表示从末尾倒数(如 at(-1) 获取最后一个字符)。
字符串长度
length:直接获取字符串的字符数量(空格和特殊字符均计入)。
二、字符串查找与截取
子串查找
indexOf(substr, start):返回子串首次出现的索引,未找到返回 -1。
lastIndexOf(substr, start):从后向前查找子串,返回最后一次出现的索引。
切片与截取
slice(start, end):提取 startend-1的子串,支持负数索引。
substring(start, end):类似slice,但自动处理负数为 0。
三、格式转换与拼接
大小写转换
toLowerCase():将字符串全部转为小写。
toUpperCase():将字符串全部转为大写。
拼接与重复
concat(str1, str2,...):拼接多个字符串(等效于 + 操作符)。
repeat(count)`:重复字符串指定次数

 "a".repeat(3)   //"aaa"

四、分割与替换
分割字符串
split(separator, limit):按分隔符拆分字符串为数组,limit限制拆分次数。

'a,b,c'.split(','); // ["a", "b", "c"]

替换内容
replace(searchValue, newValue):替换首个匹配的子串(支持正则表达式)。

'hello'.replace('l', 'x'); // "hexlo"

五、高级功能
模板字符串
使用反引号包裹,支持变量嵌入和多行文本:

const name = 'Alice';
console.log(`Hello, ${name}!`); // "Hello, Alice!"

快速反转字符串

组合 splitreversejoin 实现字符串反转:

'hello'.split('').reverse().join(''); // "olleh"

六十一、请描述下函数的执行过程
1、创建执行上下文
当js代码执行时,引擎会创建一个全局执行上下文。每当函数被调用都会有一个新的执行上下文被创建
2、创建变量对象
在函数执行上下文中,会创建一个变量对象VO,该对象用于存储函数内部使用的所有变量和参数,对于函数执行上下文,每个函数都有自己的变量对象,如果是在全局的函数,这个对象成为全局对象window
3、确定作用域链
作用域链决定了哪些变量可以被访问,作用域链由当前执行上下文的变量对象与所有父执行上下文的变量对象组成
4、确定this
在函数执行上下文中,this的值取决于函数的调用方式。在全局代码中this通常指向全局对象,在函数内部,this的值取决于如何调用该函数,如直接调用、作为对象方法调用、使用call/apply/bind方法调用。
5、代码执行:
js引擎开始逐行执行函数体中的代码,这包括了任何变量赋值、函数调用等操作。
6、垃圾回收
当函数执行完毕,其执行上下文会被销毁,相关的内存也会被释放。

function exampleFunction(a, b) {
    var c = a + b;
    return c;
}
 
var result = exampleFunction(2, 3); // 调用函数

执行过程:
创建执行上下文:为exampleFunction创建一个新的执行上下文。

创建变量对象:在函数执行上下文中,创建一个包含参数a和b的变量对象。

确定作用域链:作用域链包含当前函数的变量对象和全局变量对象。

确定this的值:在这个例子中,由于exampleFunction是通过普通方式调用的,this的值将是全局对象(例如,在浏览器中是window)。但在严格模式下(使用'use strict'),this将是undefined

代码执行:
参数ab被赋值为2和3。
变量c被创建并赋值为5(2 + 3)。
返回结果5。
垃圾回收:函数执行完毕后,其执行上下文被销毁,相关内存被释放。
返回结果:将返回的结果(5)赋值给变量result

六十二、什么是词法分析?请描述下js词法分析的过程?
先来看个常见的面试题如下:

var a = 10;
function test(){
    alert(a);  //undefined
    var a = 20;
    alert(a); //20
}
test();

疑问:为什么呢?test()执行时,虽然a=20没有赋值,但是父级作用域里是有a=10的,不应该是undefined呀,js是按顺序执行的,此时的var a = 20;根本没有执行,所以应该是10?

分析:众所周知,js代码是自上而下执行的,JavaScript并不是传统的块级作用域,而是函数作用域。JavaScript引擎会在代码执行前进行词法分析,所以事实上,js运行分为此法分析和执行两个阶段。

JavaScript代码运行前有一个类似编译的过程即词法分析,词法分析主要有三个步骤:
分析参数
再分析变量的声明
分析函数声明

具体步骤如下:

函数在运行的瞬间,生成一个活动对象(Active Object),简称AO

分析下面这个栗子:

var a = 10;
function test(a){
    alert(a);           //function a (){}
    var a = 20;
    alert(a);           //20
    function a (){}
    alert(a);           //20
 }
test(100);

词法分析:

第一步,分析函数参数:

形式参数:AO.a = undefined
接收实参:AO.a = 100
第二步,分析局部变量:

4行代码有var a,但是此时已有AO.a = 100,所以不做任何修改,即AO.a = 100
第三步,分析函数声明:

6行代码有函数a,则将function a(){}赋给AO.a,即AO.a = function a(){}
执行代码时:

3行代码运行时拿到的a时词法分析后的AO.a,即AO.a = function a(){};
4行代码:将20赋值给a,此时a=20;
5行代码运行时a已经被赋值为20,结果20
6行代码是一个函数表达式,所以不做任何操作;
7行代码运行时仍是20

var a = 10;
function test(a){
    var a;               //证明词法分析第二步。
    alert(a);           //100
    a = 20;
    alert(a);           //20
}
test(100);
var a = 10;
function test(a){
    alert(a);         //100
    var a = 20;
    alert(a);         //20
    a = function(){}        //是赋值,只有在执行时才有效
    alert(a);         //function(){}
}
test(100);

执行结果同上

var a = 10;
function test(a){
    alert(a);                //100
    var a = 20;
    alert(a);                //20
    var a = function(){}        //是赋值,只有在执行时才有效
    alert(a);                //function(){}
}
test(100);

补充说明:函数声明与函数表达式

//函数声明
function a(){
}
//函数表达式
var b = function(){
}

ab在词法分析时,区别:

a在词法分析时,就发挥作用;
b只有在执行阶段,才发挥作用。
词法作用域

所谓词法作用域是说,其作用域为在定义时(词法分析时)就确定下来的,而并非在执行时确定。白话就是在函数未执行前,函数执行的顺序已经被确定,而不是类似JAVA一样,是在执行前根本不知道执行顺序。

六十三、你知道1和Number(1)的区别是什么吗?
本质没有区别 但是如果是 new Number(1) 就不行了

// 原始值与函数调用等效性
console.log(1 === Number(1));  // true

// 对象包装的特殊性
const objNum = new Number(1);
console.log(objNum.valueOf() === 1);  

六十四、addEventListener 的第三个参数详解
addEventListener 的第三个参数支持 布尔值(兼容旧API)或 配置对象(现代写法),通过灵活组合可实现事件捕获、单次执行等高级功能。是否捕获阶段触发、是否只执行一次、是否阻止默认行为。

六十五、BLOB URL 是什么?为什么要使用它?
BLOB URL:是以blob:开头的特殊URL,用于浏览器中动态引用二进制数据(如媒体文件、文件等),无需依赖服务器存储。其核心技术特征包括:
指向浏览器内存或临时存储的BlobFile对象;
生成唯一标识符确保数据独立性;
生命周期与创建它的文档绑定,需手动释放内存以避免泄露。

结合FileReaderBlob.slice()实现大文件分片上传。
动态生成csv/json文件并提供下载链接。

// 示例:创建并释放 BLOB URL  
const blob = new Blob(['Hello World'], { type: 'text/plain' });  
const blobURL = URL.createObjectURL(blob);  
// 使用完成后立即释放  
URL.revokeObjectURL(blobURL);  

六十六、ArrayBuffer 的理解及与 Array 的区别
ArrayBuffer的本质是一个底层二进制数据容器;
内存结构:代表一段连续的原始二进制数据缓存区,内存长度固定且长度不可变。
不可直接操作:需通过视图(如 TypedArray 或 DataView)读写数据,例如 Int8Array(8位整数)、Float32Array(32位浮点数)等
高效性:连续内存分配,适合处理大规模二进制数据(如图像、音视频流);

// 创建 8 字节的 ArrayBuffer  
const buffer = new ArrayBuffer(8);  
// 使用 Int32Array 视图操作前 4 字节  
const int32View = new Int32Array(buffer);  
int32View[0] = 42;  

应用场景:WebGl图形渲染、webSocket二进制通信、文件分片上传
区别:
数据类型:ArrayBuffer 仅存储二进制数据;Array可存储任意类型数据
内存管理:ArrayBuffer连续内存空间,直接操作字节级数据,性能更高;Array堆内存动态分配,灵活性更强
动态调整长度:ArrayBuffer不可调整长度,创建即固定长度,Array长度可动态变化
操作方式:需要通过视图(如 TypedArray 或 DataView)读写数据;Array则是使用push/pop等方法;

六十七、清空数组的方式及区别
array.length = 0 直接修改原数组,速度最快,直接操作内存,保留引用关系;
splice(0,arr.length) 直接修改原数组,涉及元素删除逻辑,速度稍慢,保留引用关系;
arr = []创建新数组,可以彻底断开与原数据的关联,存在额外的内存分配开销,可能会导致内存泄漏。

六十八、onload 和 DOMContentLoaded 的执行顺序
DOMContentLoaded事件先于onload事件触发,两者触发时机与页面加载阶段密切相关。
DOMContentLoaded:HTML文档完全解析且DOM树构建完成后触发、无需等待样式表图片等外部资源加载完成。
onload:整个页面及其所有资源完全加载后才会触发;

六十九、用户刷新网页时 JS 请求的缓存处理
用户刷新网页时,JS 请求的缓存处理涵盖浏览器、CDN、代理服务器及 Service Worker 多层机制,具体行为由 HTTP 头策略和刷新方式共同决定。开发者可通过版本控制、缓存头配置及 Service Worker 管理优化资源加载效率与实时性。
对于强缓存和协商缓存:可以使用HTTP控制缓存,比如强缓存可以用Cache-Control、协商缓存则可以使用If-None-Match(Etag值)或者If-modified-Since(last-Modified)时间向服务器验证资源更新状态,若没有更新则返回304;
刷新行为对缓存的影响:
普通刷新:
浏览器优先检查强缓存,若未过期则直接使用本地文件,若过期则触发协商缓存验证。
CDN/代理服务器可能返回缓存副本,需要依赖Cache-Control配置更新
强制刷新:
浏览器忽略强缓存和协商缓存,直接向服务器请求,头部携带Cache-Control:no-cache
CDN/代理服务器可能同步更新缓存副本;
禁用缓存的方式:
Cache-Control:no-store或者max-age=0
版本号控制:
在JS文件URL中添加哈希或者时间戳参数,绕过缓存机制;

七十、NaN 的含义及 typeof NaN 的结果
NaN 表示无效或未定义的数学运算结果。
typeof NaN的值是number;这是因为NaN是数字类型(number)中的一个特殊值,用于标识数值运算中的错误状态。
非自反性:NaN === NaN = false 是唯一不自等的值;
传播性:NaN 的运算结果都是NaN;
检测NaN的方法:
isNaN() 比如isNaN("av") == true;因为这里会强制把转换对象转换为数字再判断,如果转换的结果是NaN,那么返回的结果就是true
Number.isNaN()更加严格的检测NaN,避免误判Number.isNaN(NaN) == true
value!==value只有NaN符合;

七十一、前端计算十万级数据的解决方案

七十二、constructor 和 instanceof 的区别
constructor对象的属性,指向创建它的构造函数,直接关联构造函数,可靠性较低,易受原型修改影响
instanceof运算符,检查对象是否为某构造函数的实例或在原型链中,检测对象的原型链是否包含目标构造函数的prototype属性;

function Person() {}  
const person1 = new Person();  
console.log(person1.constructor === Person);   // true(直接引用)
console.log(person1 instanceof Person);        // true(原型

原型链继承

function Student() {}  
Student.prototype = Object.create(Person.prototype);  
const student1 = new Student();  
console.log(student1.constructor === Student); // false(原型被覆盖后指向 Person
console.log(student1 instanceof Student);       // true(原型链存在关联)
console.log(student1 instanceof Person);        // true(继承关系)

手动修改原型

function Car() {}  
const car1 = new Car();  
Car.prototype = {}; // 重写原型  

console.log(car1.constructor === Car);         // false(原型的 constructor 被覆盖)
console.log(car1 instanceof Car);     //false 原型链已断开

七十三、跳出循环的 return、break、continue 的区别
终止循环的层级

break:仅跳出当前层循环,外层循环继续执行(适用于嵌套循环)。

for (let i = 0; i < 3; i++) {  
  if (i === 1) break;  
  console.log(i); // 输出 0  
}  

return:直接终止整个函数,跳出所有循环层级。

function test() {  
  for (let i = 0; i < 3; i++) {  
    if (i === 1) return;  
    console.log(i); // 输出 0  
  }  
  console.log("函数结束"); // 不会执行  
}  
跳过循环与继续循环

continue:跳过当前迭代的剩余代码,进入下一次循环。

for (let i = 0; i < 3; i++) {  
  if (i === 1) continue;  
  console.log(i); // 输出 0, 2  
}  

函数返回值
return 可携带返回值(如 return 5;),而 break 和 continue 无此特性。

七十四、Object.freeze 的用途
冻结对象,防止对象被修改,冻结后无法添加/删除属性、修改属性;在共享数据场景中(如全局状态),冻结后保证数据来源可信。
默认浅冻结,仅冻结第一层级的属性,嵌套的对象仍可修改。

七十五、所有事件都有冒泡吗
并非所有都有,比如focus/blurmouseenter/mouseleave就去掉了冒泡事件。

七十六、分析 !+[]+!![]+!![]+!![]+!![]+!![] 的结果
+[] = 0;
[] = true
!+[] = !0 -> 1
1+1+1+1+1+1 = 6
两次逻辑非!!会将操作数强制转换为布尔类型:

!![] → Boolean([]) → true  

七十七、020-088 的计算过程及结果
020 会根据8进制进行转换成16
088 因为超出8进制范围,会被转换为NaN

16+NaN = NaN

七十八、JS sort 方法的应用场景
数值排序:

[5,4,2].sort((a,b)=>a-b)

字符串排序:

['d','b'].sort()  //['b','d']

对象属性排序:

users.sort((a,b)=>a.age-b.age)

随机排序:

array.sort(()=>Math.random()-0.5)

七十九、用户刷新/跳转/关闭时发送统计数据
navigator.sendBeacon:可靠性高,不会阻塞页面,存在兼容问题。数据长度限制64KB,无需响应,跨域支持。

window.addEventListener("unload", function() {  
  const data = JSON.stringify({ event: "page_close", timestamp: Date.now() });  
  navigator.sendBeacon("/log", data);  
});  

XMLHttpRequest/XHR:可靠性中等,会阻塞页面,不存在兼容问题,长度无限制
Img标签:可靠性最低,有些浏览器会忽略,不会阻塞页面,不存在兼容问题,URL长度会有限制

八十、获取指定日期毫秒数的方法 ----------------
Date.parse:

Date.parse('2023-10-01')//返回时间戳

Date对象+getTime

new Date('2024-10-01').getTime()

一元运算符:

+new Date('2023-10-01');  

八十一、JS 对象深比较
一、深比较的必要性
JavaScript 中的对象通过 === 或 == 比较时,仅判断引用是否相同(即是否为同一内存地址),而非内容是否一致。例如:

const obj1 = { a: 1 }, obj2 = { a: 1 };  
console.log(obj1 === obj2); // false  

因此,必须通过深比较(递归检查所有属性值)判断对象内容是否相等。
二、深比较的实现方法
1、JSON.stringify简单比较(简单场景)
通过序列化对象为字符串进行对比,适合简单场景:

function simpleDeepEqual(obj1, obj2) {  
  return JSON.stringify(obj1) === JSON.stringify(obj2);  
}  

优点:代码简洁,无需递归。
缺点:
忽略 undefined、函数、Symbol 等特殊值;
属性顺序不同可能误判(如 {a:1,b:2} 和 {b:2,a:1});
无法处理循环引用(如 obj.self = obj)。
2、递归遍历比较(复杂场景)

function deepEqual(obj1, obj2, visited = new WeakMap()) {
  // 处理循环引用
  if (visited.has(obj1) || visited.has(obj2)) {
    return obj1 === obj2;
  }
  visited.set(obj1, true);
  visited.set(obj2, true);

  // 类型不同直接返回 false
  if (typeof obj1 !== typeof obj2) return false;
  if (obj1 === obj2) return true;

  // 处理特殊类型(Date、RegExp等)
  if (obj1 instanceof Date && obj2 instanceof Date) 
    return obj1.getTime() === obj2.getTime();
  if (obj1 instanceof RegExp && obj2 instanceof RegExp) 
    return obj1.toString() === obj2.toString();

  // 处理数组和对象
  if (typeof obj1 === 'object' && obj1 !== null) {
    const keys1 = Object.keys(obj1);
    const keys2 = Object.keys(obj2);
    if (keys1.length !== keys2.length) return false;

    for (const key of keys1) {
      if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key], visited)) {
        return false;
      }
    }
    return true;
  }

  // 其他类型直接比较值
  return obj1 === obj2;
}

优点:
支持嵌套对象、数组、循环引用检测;
精确匹配属性类型和顺序。
缺点:
性能较低(尤其对大对象);
需手动处理特殊对象(如 Date、RegExp)。

八十二、虚拟 DOM 是否最快?
真实的DOM操作才是最快的,但是消耗的性能是非常宝贵的,所以用虚拟DOM,来减少DOM操作的次数。

八十三、unshift 和 push 的区别
unshift是末尾添加
push是开头添加

八十四、JS 字符串截取方法
substring(start, end):
不接受负数,自动调整参数顺序。

'hello'.substring(1, 3); // 'el'  
substr(start, length):

第二个参数为截取长度。

'hello'.substr(1, 3); // 'ell'  

slice(start, end):
支持负数参数(从末尾计算)。

'hello'.slice(-3); // 'llo'  

八十五、ResizeObserver 的用途
监听元素尺寸变化:

const observer = new ResizeObserver(entries => {  
  entries.forEach(entry => {  
    console.log('尺寸变化:', entry.contentRect);  
  });  
});  
observer.observe(document.getElementById('target'))

主要应用于响应式布局调整,Canvas自适应容器大小。
八十六、分片上传的实现
核心实现原理:
文件分片切割,使用File.prototype.slice实现切割blob分片;
分片上传与合并:前端并发或者串行上传分片至服务器,每个分片携带唯一标识、分片序号等信息。
文件切割与表示生成:

async function splitFile(file,chunkSize = 5*1024*1024){
  const totalChunks = file.size / chunkSize;
  const chunks = [];
  for(let i = 0;i<=totalChunks;i++){
    const start = i * chunkSize;
    const end = Math.min(file.size,start+chunkSize);
    chunks.push(file.slice(start,end)
  }
  const fileHash = await calcFileHash(file)  //生成唯一的hash
   returen {chunks,fileHash}
}

分片上传逻辑

async function uploadChunks(chunks,fileHash){
  const uploaded = new Set();
  for(let i = 0;i<=chunks.length-1;i++){
    if(uploaded.has(i))continue;//跳过已经上传分片
    const formData = new FormData();
    formData.append('chunk',chunk[i]);
     formData.append('hash',fileHash);
     formData.append('index',i);
     await fetch('/upload',{meth:"post",body:formData});
     uploaded.add(i)
  }
}

秒传机制:前端在上传之前先请求文件是否已经上传过,如果上传过了就跳过
生成hash可以使用浏览器原生的crypto.subtle.digest方法

async function calculateFileHash(file, algorithm = 'SHA-256') {
  const buffer = await file.arrayBuffer();
  const hashBuffer = await crypto.subtle.digest(algorithm, buffer);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}

// 使用示例
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  const hash = await calculateFileHash(file);
  console.log('File Hash:', hash);
}

八十七、切换页面后保持 setInterval 准确
web Workers:

// 主线程  
const worker = new Worker('timer.js');  
worker.postMessage('start');  

// timer.js  
self.onmessage = () => {  
  setInterval(() => {  
    self.postMessage('tick');  
  }, 1000);  
};  

基于时间戳动态调整

let lastTime = Date.now();  
function check() {  
  const now = Date.now();  
  if (now - lastTime >= 1000) {  
    lastTime = now;  
    // 执行任务...  
  }  
  requestAnimationFrame(check);  
}  
check();

八十八、渲染大数据不卡顿的方案
1、分页加载
2、虚拟滚动加载
3、数据冻结Object.freeze避免Vue/React的响应式追踪
4、渲染流水线优化将渲染任务拆分为多个宏任务,通过 requestAnimationFrame 分批执行

function batchRender(data, chunkSize = 100) {
  let index = 0;
  function renderChunk() {
    const end = Math.min(index + chunkSize, data.length);
    for (; index < end; index++) {
      // 渲染逻辑
    }
    if (index < data.length) requestAnimationFrame(renderChunk);
  }
  renderChunk();
}

5、渲染技术增强GPU加速:
对渲染容器启用CSS硬件加速(transform:translateZ(0)
6、 Web Worker多线程:
将数据计算与渲染分离,避免阻塞主线程
7、Canvas/WebGl替代DOM
超大数据集使用Canvas和WebGl绘制,避免DOM性能瓶颈

八十九、说说你对promise的了解
Promise 是用于管理异步操作的标准化方案,相较于传统的回调函数和事件机制,其通过链式调用和状态隔离提供了更清晰的逻辑控制。
Promise 通过状态机模型、链式调用和统一错误处理,成为现代 JavaScript 异步编程的核心方案。尽管存在无法中途取消和错误静默必须需要设置.catch内部一场才会冒泡到外层的局限,但其结构化控制能力显著提升了代码可维护性。结合async/await语法,可进一步实现接近同步代码的直观性

九十、['1','2','3'].map(parseInt)的结果什么?
map 函数的参数传递机制
map 的回调函数会自动传递三个参数:
当前元素值
元素索引
原数组

当直接传递parseInt时,实际调用形式为:

['1','2','3'].map((value, index, array) => 
  parseInt(value, index) // 第二个参数被误用为基数(radix)
);

parseInt的基数陷阱
parseInt(string, radix)的第二个参数radix表示进制基数(2~36)。
具体调用过程如下:

第一次迭代:parseInt('1', 0)
radix=0时,默认按十进制解析 → 1
第二次迭代:parseInt('2', 1)
基数为 1 是非法值(有效范围 2~36) → NaN
第三次迭代:parseInt('3', 2)
二进制下只能解析 0 或 1,3 非法 → NaN

九十一、JS延迟加载的方式有哪些?
一、脚本属性控制
1、defer属性:
异步下载脚本,延迟到HTML解析完成后按文档顺序执行,不阻塞页面渲染,适用依赖DOM的初始化脚本。
2、async属性:
异步下载脚本,下载完毕后就立即执行,执行顺序不确定且可能中断页面解析,适用独立脚本

二、动态脚本加载
1、动态创建<script>标签
通过JS创建并插入script元素,按需触发加载,优势是精确控制加载时机。

三、传统优化策略
1、脚本放置于页面底部,减少渲染组赛的时间,但是不能完全避免
2、setTimeout延迟执行,仅仅延迟脚本的执行时间,不延迟脚本的加载

九十二、callee和caller的区别
维度 callee caller
依赖对象 arguments对象的属性 函数自身的属性(如 func.caller
严格模式限制 禁用(报 TypeError) 禁用(返回 null 或报错)
替代方案 命名函数表达式或模块化设计 通过代码结构优化或调试工具替代

// 阶乘函数(避免函数名耦合问题)
const factorial = function(n) {
  if (n <= 1) return 1;
  return n * arguments.callee(n - 1);  // 替代函数名调用
};
console.log(factorial(5)); // 输出: 120
// 严格模式兼容的递归实现
const factorial = function f(n) {
  if (n <= 1) return 1;
  return n * f(n - 1);  // 直接使用函数名替代 callee
};
console.log(factorial(5));  // 输出: 120

//在面向对象编程中,通过类名或静态方法实现自我引用:

class MathUtils {
  static factorial(n) {
    if (n <= 1) return 1;
    return n * MathUtils.factorial(n - 1);  // 通过类名调用
  }
}
console.log(MathUtils.factorial(5));  // 输出: 120

九十三、typeof和instanceof的区别
typeof

用于检测变量的基本数据类型(如 number/string/boolean)和 undefined、function,返回类型名称的字符串
对 null 返回 "object"(历史遗留问题)
示例:

typeof 42;        // "number"  
typeof null;      // "object"(陷阱)

instanceof

用于检测对象是否为特定构造函数的实例(通过原型链追溯),返回布尔值
无法检测原始类型(如 42 instanceof Number 返回 false)
示例:

[] instanceof Array;   // true  
"abc" instanceof String; // false(原始类型)

typeof的缺陷:

所有对象(包括 Array/Date)均返回 "object",无法细化
特殊场景如 typeof null 的陷阱需人工处理
instanceof的短板:

无法检测原始类型的包装对象(如 new Number(42) 需显式创建)
原型链污染可能导致误判(如 [] instanceof Object 为 true)

原型链污染的本质
通过篡改原型链上的属性或方法,所有继承该原型的对象均会受到影响,导致instanceof的类型检测失效或被误导。例如:

// 污染原型链后可能导致 instanceof 误判
Array.prototype.__proto__ = {};  
console.log([] instanceof Array); // 可能返回 false

替代类型检测方式:
使用 Object.prototype.toString
直接通过对象的内置 [[Class]] 标识判断类型,不受原型链污染影响:

function safeTypeCheck(target) {
  return Object.prototype.toString.call(target).slice(8, -1);
}
safeTypeCheck([]); // "Array"

防御原型链篡改:
1、冻结关键原型对象
使用 Object.freeze 禁止修改全局原型链:

Object.freeze(Object.prototype);
Object.prototype.newProp = 42; // 严格模式下报错

使用无原型对象
创建无原型链依赖的数据容器,隔离污染风险:

const safeObj = Object.create(null); // 无 __proto__ 属性

九十四、Object.is和===区别
常规是一样的,均不会隐式的转换类型,需要严格相同,对引用对象都是通过内存地址判断是否指向同一个对象。
区别在于特殊数值的处理:NaN±0;

// 特殊场景对比
console.log(NaN === NaN);          // false  
console.log(Object.is(NaN, NaN));  // true

console.log(+0 === -0);            // true  
console.log(Object.is(+0, -0));    // false

// 常规场景对比
const a = { name: "test" }, b = a;
console.log(a === b);              // true(内存地址相同)
console.log(Object.is(a, b));      // true

console.log(5 === 5);              // true  
console.log(Object.is(5, 5));      // true

九十五、if(a==1 && a==2)成立的条件

  1. 通过重写 valueOf() 方法
const a = {
  value: 0,
  valueOf() {
    return ++this.value; // 递增属性并返回新值
  }
};
console.log(a == 1 && a == 2); 

原理:
每次执行 a == x 时,触发 valueOf()方法调用,通过递增 value 属性实现值动态变化。
关键点:
valueOf() 的优先级高于toString()
方法内部的 this.value++保证了每次比较时值的递增。

九十六、创建一个长度为 100 且值为对应下标的数组
在 JavaScript 中,创建一个长度为 100 且值为对应下标的数组,可以通过以下 高效且简洁 的方式实现:

方法 1:Array.from + 回调函数(推荐)

const array = Array.from({ length: 100 }, (_, index) => index);
// 输出: [0, 1, 2, ..., 99]

优点:
一步到位:直接通过 Array.from 的第二个参数(map 函数)生成目标数组;
无空槽问题:生成的数组已完全初始化,避免稀疏数组(sparse array)的潜在问题;
兼容性:支持所有现代浏览器及 Node.js。
方法 2:扩展运算符 + keys()

const array = [...Array(100).keys()];
// 输出: [0, 1, 2, ..., 99]

优点:
语法简洁:利用 keys()生成下标迭代器,结合扩展运算符转为数组;
直观性:代码意图清晰,适合快速实现。
方法 3:fill() + map()(传统方式)

const array = Array(100).fill().map((_, index) => index);
// 输出: [0, 1, 2, ..., 99]

注意事项:
需先调用fill()填充 undefined,否则 map() 会跳过空槽;
性能略低:需额外执行 fill() 初始化。

九十七、为何优先使用链式 setTimeout 替代 setInterval?
一、setInterval的固有缺陷
时序失控风险
setInterval会无视回调执行耗时,强制按固定间隔触发后续任务。若回调执行时间超过间隔设定,多个任务会在事件队列中堆积,导致执行时序混乱甚至界面卡顿。
示例场景:回调需 150ms 完成,但间隔设为 100ms → 后续回调被迫延迟或堆积。

误差累积问题
由于 JavaScript单线程特性,主线程繁忙时 setInterval的执行间隔会逐渐产生误差,无法保证时间精度。长期运行的定时器(如实时数据同步)误差会不断叠加。

错误传播隐患
setInterval的回调抛出未捕获异常时,定时器仍持续运行,错误可能被后续逻辑放大。

二、链式 setTimeout 的优势
动态间隔控制
链式调用通过在回调结束时重新注册定时器,确保每次间隔从回调完成时开始计算,避免任务堆积。

function scheduleTask() {
  setTimeout(() => {
    // 任务逻辑
    scheduleTask(); // 重新注册
  }, 100);
}
scheduleTask();

灵活调整间隔
可基于运行时状态(如网络延迟、用户交互)动态调整间隔时间,实现自适应调度。

let delay = 1000;
function adaptiveSchedule() {
  setTimeout(() => {
    if (isNetworkSlow) delay *= 2;
    adaptiveSchedule();
  }, delay);
}

错误隔离性
单次回调异常仅影响当前任务,不会中断后续调度(需配合 try...catch

九十八、如何判断JS的变量类型

function getType(value) {
  if (value === null) return 'null';
  const type = typeof value;
  if (type !== 'object') return type;
  if (Array.isArray(value)) return 'array';
  const rawType = Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
  return rawType === 'object' ? value.constructor.name.toLowerCase() : rawType;
}

九十九、冒泡排序和选择排序

  1. 从数组第一个元素开始,比较相邻的两个元素
  2. 如果前一个 > 后一个,交换它们
  3. 对每一对相邻元素重复步骤1-2,直到最后一对
  4. 重复上述过程(每次排除已排序的末尾元素)直到无交换发生
function bubbleSort(arr) {
  let n = arr.length;
  let lastSwapIndex = n - 1;
  for (let i = 0; i < n - 1; i++) {
    let isSwapped = false;
    let currentSwapIndex = 0;
    for (let j = 0; j < lastSwapIndex; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        isSwapped = true;
        currentSwapIndex = j;
      }
    }
    if (!isSwapped) break; // 提前终止
    lastSwapIndex = currentSwapIndex;
  }
  return arr;
}
  1. 在未排序序列中找到最小元素
  2. 将其与未排序序列的第一个元素交换
  3. 将已排序序列扩展一个元素,未排序序列减少一个元素
  4. 重复步骤1-3直到所有元素排序完成
function selectionSort(arr) {
  const n = arr.length;
  for (let i = 0; i < n - 1; i++) {
    let minIndex = i;
    for (let j = i + 1; j < n; j++) {
      if (arr[j] < arr[minIndex]) minIndex = j;
    }
    if (minIndex !== i) [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
  }
  return arr;
}

一百、interface和type的区别

  1. 定义方式与适用范围
    interface
    专为定义对象结构设计(属性、方法、函数签名),支持类实现(implements)和继承(extends)。
interface User {
  name: string;
  greet(): void;
}

type
可定义任意类型别名,包括原始类型、联合类型、元组等:

type ID = string | number;        // 联合类型
type Point = [number, number];    // 元组
type Callback = () => void;       // 函数类型
  1. 声明合并
    interface
    支持同名接口的自动合并,常用于扩展第三方类型:
interface User { name: string; }
interface User { age: number; }   // 合并为 { name: string; age: number }

type
禁止重复定义,同一类型别名多次声明会报错:

type User = { name: string };
type User = { age: number };      // ❌ Error: Duplicate identifier
  1. 继承与扩展
    interface
    通过 extends 实现继承,语法更直观:
interface Animal { name: string; }
interface Dog extends Animal { bark(): void; }

type
使用交叉类型 & 实现类似效果,灵活性更高:

type Animal = { name: string };
type Dog = Animal & { bark(): void };
  1. 适用场景
    优先interface
    定义对象结构(如类、函数参数)
    需要声明合并扩展类型时
    优先 type
    定义联合类型、元组等复杂类型
    需要条件类型或映射类型时

一百零一、数组有什么方法
改变原数组的方法:push/pop/shift/unshift、splice、sort/reverse、fill
返回新数组的方法:map、filter、slice、concat
遍历迭代器方法:forEach/map/filter支持链式调用

一百零二、什么是graphql?
GraphQL的核心概念和特点
按需获取数据:GraphQL允许客户端精确地请求所需的数据,服务器只返回这些数据,避免了过度加载或不足加载的问题
减少冗余:通过精确地请求数据,GraphQL减少了数据传输的冗余,提高了网络效率
灵活性和扩展性:GraphQL定义了数据的类型和结构,使得API更加灵活和可扩展,支持自定义数据模型和查询规范
实时数据更新:GraphQL支持实时数据查询和订阅,客户端可以实时获取数据更新
GraphQL的应用场景和优势
高效数据获取:GraphQL允许在一个请求中获取所有需要的数据,避免了多次请求的麻烦
减少网络负载:通过精确请求数据,减少了不必要的网络传输,降低了服务器的负载
减少客户端代码冗余:客户端可以精确地请求所需的数据,减少了不必要的代码和数据处理
GraphQLRESTful API的比较
灵活性:GraphQL提供了更高的灵活性,客户端可以自定义查询,而RESTful API则需要预先定义好的接口。
一、环境准备与依赖安装
安装核心依赖

npm install @apollo/client graphql  # Apollo Client + GraphQL 核心包

二、Apollo Client 配置
创建客户端实例 (src/apolloClient.js)

import { ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql',  // 服务端 GraphQL 地址
  cache: new InMemoryCache()  // 自动缓存管理 :ml-citation{ref="4,5" data="citationList"}
});

export default client;

全局注入 Provider (src/index.js)

import { ApolloProvider } from '@apollo/client';
import client from './apolloClient';

ReactDOM.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>,
  document.getElementById('root')
);  // 包裹根组件启用数据交互 

三、数据请求与组件集成
定义 GraphQL 查询

import { gql } from '@apollo/client';

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
    }
  }
`;  // 声明式数据需求描述 

组件中执行查询

import { useQuery } from '@apollo/client';

function UserList() {
  const { loading, error, data } = useQuery(GET_USERS);
  
  if (loading) return <p>加载中...</p>;
  if (error) return <p>错误: {error.message}</p>;

  return (
    <ul>
      {data.users.map(user => (
        <li key={user.id}>{user.name} - {user.email}</li>
      ))}
    </ul>
  );  // 自动触发请求与状态管理
}

四、变更操作实践
定义 Mutation

const ADD_USER = gql`
  mutation AddUser($input: UserInput!) {
    addUser(input: $input) {
      id
      name
    }
  }
`;  // 参数化输入类型定义 

执行数据变更

import { useMutation } from '@apollo/client';

function AddUserForm() {
  const [addUser] = useMutation(ADD_USER);
  const [name, setName] = useState('');
  
  const handleSubmit = () => {
    addUser({ variables: { input: { name } } });
  };

  return (
    <input value={name} onChange={e => setName(e.target.value)} />
    <button onClick={handleSubmit}>提交</button>
  );  // 触发服务端数据变更 :ml-citation{ref="2,6" data="citationList"}
}

五、高级配置建议
功能 实现方式
身份认证 通过ApolloClientheaders 配置携带 Token 信息
缓存策略 自定义 InMemoryCache 配置实现精准缓存更新
错误处理 全局配置ApolloClientonError 回调统一处理网络/业务错误

一百零三、什么是Proxy?
一、基本概念
Proxy(代理)是 ES6 引入的对象拦截机制,允许开发者通过代理层自定义对象的基本操作行为(如属性读写、函数调用等)。
核心作用:在目标对象与外部访问之间建立“拦截层”,实现精细化控制。
类比:类似于现实中的中介,对外屏蔽目标对象的直接操作。
二、语法与组成

const proxy = new Proxy(target, handler);

target:需代理的目标对象(可以是对象、数组、函数等)。
handler:定义拦截行为的对象,包含特定“陷阱方法”(trap)。
三、常见拦截操作
陷阱方法 拦截行为 示例场景
get(target, prop) 拦截属性读取 日志记录、动态计算属性
set(target, prop, value) 拦截属性赋值 数据验证、自动触发更新
apply(target, thisArg, args) 拦截函数调用 函数调用权限控制
has(target, prop) 拦截 in 操作符 隐藏私有属性
四、典型应用场景
数据响应式
Vue3 使用 Proxy 替代 Object.defineProperty实现更高效的响应式系统。
动态属性处理
拦截未定义属性的访问,返回默认值或执行逻辑。
操作日志与调试
记录对象的所有读写操作。
五、与 Reflect 的配合
Proxy常与 Reflect搭配使用,确保拦截操作后仍能正确执行默认行为

const target = { _secret: 123, value: 100 };
const handler = {
  get(target, prop) {
    if (prop.startsWith('_')) throw new Error('私有属性禁止访问');
    return Reflect.get(target, prop);
  },
  deleteProperty(target, prop) {
    if (prop === 'value') return false;
    return Reflect.deleteProperty(target, prop);
  }
};

const proxy = new Proxy(target, handler);
console.log(proxy.value); // 正常输出 100
console.log(proxy._secret); // 抛出错误
delete proxy.value; // 操作失败

一百零四、什么是Object.defineProperty()?
Object.defineProperty() 是 JavaScript 中用于精确控制对象属性行为的方法,允许开发者定义或修改属性的特性(如可枚举性、可写性等)。
核心作用:通过属性描述符(descriptor)实现对属性的精细化管控。
返回值:返回被修改后的目标对象。

一、核心步骤
明确目标属性特性
根据需求选择数据描述符(value + writable)或存取描述符(get + set),二者不可混用。

配置描述符选项

value:属性初始值(默认 undefined
writable:是否可修改(默认 false
enumerable:是否可枚举(如出现在 for...in 循环中,默认 false
configurable:是否可删除或重新配置(默认 false
调用方法绑定到对象
使用 Object.defineProperty(obj, prop, descriptor) 应用配置。

二、典型场景与代码示例

  1. 创建不可变属性
const obj = {};
Object.defineProperty(obj, 'id', {
  value: '123',
  writable: false,    // 禁止修改
  configurable: false // 禁止删除或重新定义
});

obj.id = '456'; // 静默失败(严格模式报错)
delete obj.id;  // 静默失败
  1. 动态计算属性(存取器)
const user = {
  firstName: 'John',
  lastName: 'Doe'
};

Object.defineProperty(user, 'fullName', {
  get() {
    return `${this.firstName} ${this.lastName}`;
  },
  set(value) {
    [this.firstName, this.lastName] = value.split(' ');
  },
  enumerable: true // 允许枚举
});

console.log(user.fullName); // "John Doe"
user.fullName = 'Jane Smith';
console.log(user.firstName); // "Jane"
  1. 隐藏私有属性
const api = {
  _secretKey: 'abc123',
  publicMethod() { /* ... */ }
};

Object.defineProperty(api, '_secretKey', {
  enumerable: false // 隐藏属性
});

console.log(Object.keys(api)); // 只输出 ["publicMethod"]

三、关键注意事项
默认行为差异

通过 Object.defineProperty()定义的属性默认 writable: false、enumerable: false、configurable: false
普通赋值(如 obj.prop = value)默认所有特性为 true。
严格模式下的行为
违反 writable: falseconfigurable: false时,严格模式会抛出错误,非严格模式静默失败。
性能考量
频繁调用可能影响性能,适合初始化时配置关键属性。
与 Proxy 的对比
Object.defineProperty()只能拦截已知属性,而 Proxy 可拦截动态属性

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容