前端高频考题(js)

0 阅读14分钟

一、延迟加载JS有哪些方式

屏幕截图 2025-06-01 162218.png

  • defer:等html全部解析完成,才会执行js代码,顺次执行js脚本

屏幕截图 2025-06-01 162325.png

  • async:async是和html解析同步的(一起解析),不是顺次执行js脚本(谁先加载完谁先执行)

屏幕截图 2025-06-01 162338.png

二、js的数据类型有哪些

  • 基本类型:string、number、boolean、undefined、null、symbol、bigint
  • 引用类型:object

三、null和undefined的区别

  1. 作者在设计js的时候是先设计null的(因为设计之初借鉴了java语言)
  2. null会被隐式转换成0,很不容易发现错误
  3. 先有null后有undefined,出来undefined是为了填补之前的坑

四、==和===有什么不同

  • ==:比较的是值
    • string == number || boolean || number ... 都会隐式转换
    • 通过valueof转换(valueof()方法通常由JS在后台自动调用,并不显式地出现在代码中)
  • ===:除了比较值,还比较类型

五、js的微任务和宏任务

  • js是单线程的语言
  • 流程:同步 -> 事件循环【微任务和宏任务】 -> 微任务 -> 宏任务 -> 微任务 ...
    • 微任务:promise.then、process.nextTick ...
    • 宏任务:setTimeout、setInterval、setImmediate ...
for(var i=0;i<3;i++){
    setTimeout(function(){
        console.log(i);
    },1000*i)
}
// 3 3 3

屏幕截图 2025-06-01 204555.png

setTimeout(function(){
    console.log('1') // 宏任务
})

new Promise((resolve)=>{
    console.log('2') // 同步
}).then(()=>{
    console.log('3') // 微任务
}).then(()=>{
    console.log('4') // 微任务
})

console.log('5') // 同步

// 2 5 3 4 1

六、js作用域

  1. 除了函数外,js是没有块级作用域的
  2. 作用域链:内部可以访问外部的变量,但是外部不能访问内部的变量(内部有优先找内部的)
  3. 注意声明变量是用var还是没有写(window.)
  4. js有变量提升的机制(变量悬挂声明)
  5. 优先级:声明变量 > 声明普通函数 > 参数 > 变量提升

怎么做

  • 先看本层作用域有没有此变量(注意变量提升)
  • js除了函数之外没有块级作用域
(function(){
    var a = b = 10;
})()
console.log(a); // undefined
console.log(b); // 10
function c(){
    var b = 1;
    function a(){
        conaole.log(b); // undefined
        var b = 2;
        console.log(b); // 2
    }
    a();
    console.log(b); // 1
}
var name = 'a';
(function(){
    if(typeof name == 'undefined'){
        var name = 'b';
        console.log('111' + name); /// 111b
    }else{
        console.log('222' + name)
    }
})()
function fun(a){
    var a = 10;
    function a(){}
    console.log(a); // 10
}
fun(100)
}

七、js对象

  1. 对象是通过new操作符构建出来的,所以对象之间不相等
  2. 对象注意:引用类型
  3. 所有的key都是字符串类型
  4. 对象如何找属性|方法:对象本身 -> 构造函数中 -> 对象原型中 -> 构造函数原型中 -> 对象上一层原型中
console.log([1,2,3] === [1,2,3]) // false
var obj1 = {
    a:'hello'
}

var obj2 = obj1;
obj2.a = 'world';
console.log(obj1); // {a:world''}
(function(){
    console.log(a); // undefined
    var a = 1;
})()
var a = {};
var b = {
    key:'a';
}
var c = {
    key:'c';
}

a[b] = '123';
a[c] = '456';

console.log(a[b]); // 456
function Fun(){
    this.a = '在Fun函数中添加的';
}
Fun.prototype.a = '这是Fun原型添加的';
let obj = new Fun();
obj.a = '对象本身';
obj.__proto__.a = '这是对象原型添加的';

console.log(obj.a);

八、js作用域+this指向+原型链

var x = 10;
var obj = {
  x: 20,
  fn: function() {
    var x = 30;
    return function() {
      console.log(this.x);
    };
  }
};

obj.fn()(); // 10
var fn2 = obj.fn;
fn2()(); // 10

九、js判断变量是不是数组有哪些方法

  • isArray
var arr = [1,2,3];
console.log(Array.isArray(arr));
  • instanceof
var arr = [1,2,3];
console.log(arr instanceof Array);
  • 原型prototype
var arr = [1,2,3];
console.log(Object.prototype.toString.call(arr).indexOf('Array') > -1);
  • isPrototypeOf()
var arr = [1,2,3];
console.log(Array.prototype.isPrototypeOf(arr));
  • constructor
var arr = [1,2,3];
console.log(arr.constructor.toString().indexOf('Array') > -1);

十、slice和splice是用来干嘛的

  • slice 是用来截取的,不改变原数组
const arr = [1, 2, 3, 4, 5];

// 从索引2开始截取到末尾
console.log(arr.slice(2)); // [3, 4, 5]

// 从索引1截取到索引3(不包含3)
console.log(arr.slice(1, 3)); // [2, 3]

// 使用负数索引
console.log(arr.slice(-2)); // [4, 5](从倒数第2个元素到末尾)
console.log(arr.slice(1, -1)); // [2, 3, 4](从索引1到倒数第1个元素前)

  • splice 是用来插入、删除、替换的,会修改原数组
const arr = [1, 2, 3, 4, 5];

// 删除元素
const deleted = arr.splice(2, 2); // 从索引2开始删除2个元素
console.log(arr); // [1, 2, 5]
console.log(deleted); // [3, 4]

// 插入元素(不删除)
arr.splice(1, 0, 'a', 'b'); // 从索引1开始插入元素
console.log(arr); // [1, 'a', 'b', 2, 5]

// 替换元素
arr.splice(2, 1, 'x'); // 从索引2开始删除1个元素,插入'x'
console.log(arr); // [1, 'a', 'x', 2, 5]

十一、js数组去重

  • set
var arr1 = [1,2,3,1];
function unique(arr){
    return [...new Set(arr)]
    // return Array.from(new Set(arr1))
}

  • for循环
var arr2 = [1,2,3,1];
function unique(arr){
    var brr = [];
    for(var i=0;i<arr.length;i++){
        if(brr.indexOf(arr[i]) == -1){
        brr.push(arr[i]);
        }
    }
    return brr;
}

十二、多维数组的最大值

var arr = [[1,2,3,4],[5,6,7,8],[9,10,11,12]];

function fe(arr){
    var brr = [];
    arr.forEach((item,index)=>{
        brr.push(Math.max(...item))
    })
    return brr;
}

十三、给字符串新增方法

String.prototype.addPrefix = function(str){
    return str + this;
}

console.log('world'.addPrefix('hello')) // helloworld

十四、字符串出现最多次数的字符及次数

var str = 'aaabbbbcccdddddddd'

var obj = {};
for(var i=0;i<str.length;i++){
    var char = str.charAt(i);
    if(obj[char]){
        obj[char]++;
    }else{
        obj[char]=1;
    }
}
// 统计最大值
var max = 0;
for(var key in obj){
    if(max < obj[key]){
        max = obj[key];
    }
}
// 比对
for(var key in obj){
    if(obj[key] == max){
        console.log('最多的字符串为' + key);
        console.log('出现的次数为' + max);
    }
}

十五、new操作符具体做了什么

function Fun(age,name){
    this.age = age;
    this.name = name;
}

function create(fn,...args){
    // 1.创建一个空对象
    let obj = {};
    // 2.将空对象的原型,指向于构造函数的原型
    Object.setPrototypeOf(obj,fn.prototype);
    // 3.将空对象作为构造函数的上下文(改变this指向)
    let result = fn.apply(obj,args);
    // 4.对构造函数有返回值的处理判断
    return result instanceof Object ? result : obj;
}

console.log(create(Fun,18,'张三'))

十六、闭包

  1. 闭包是什么:闭包是一个函数加上到创建函数的作用域的连接,闭包关闭了函数的自由变量
  2. 闭包解决什么问题:
  • 内部函数可以访问到外部函数
var lis = document.getElementsByTagName('li');
for(var i=0;i<lis.length;i++){
    (function(i){
        lis[i].onclick = function(){
            alert(i);
        }
    })(i)
} // 也可以直接使用let i=0

3. 闭包的缺点

  • 变量会驻留在内存中,造成内存损耗问题(把闭包的函数设置为null)

十七、原型链

  1. 原型可以解决什么问题
  • 对象共享属性和共享方法
  1. 谁有原型
  • 函数有 prototype
  • 对象有 __proto__
  1. 对象查找属性或者方法的顺序
  • 现在对象本身属性查找 -> 再到构造函数中查找 -> 再到__proto__中查找 -> 再到prototype中查找 -> 再到原型的原型 -> 最后到null
  1. 原型链的使用
  • 扩展内置对象的功能
  • 复用方法与属性

十八、深拷贝和浅拷贝

共同点:复制

  • 浅拷贝:只复制引用,而未复制真正的值
var arr1 = ['a','b','c'];
var arr2 = arr1;

  • 深拷贝:是复制真正的值,不同引用
var obj1 = {
    a:1;
    b:2;
}

var obj2 = JSON.parse(JSON.stringify(obj3));

十九、var、let、const

  • 共同点

    • 都可以声明变量
  • 区别1

    • var 具有变量提升的机制
    • let、const 没有变量提升的机制
  • 区别2

    • var 可以多次声明同一个变量
    • let、const 不可以多次声明同一个变量
  • 区别3

    • var、let 声明变量
    • const 声明常量
    • var、let 声明的变量可以再次赋值,但是cosnt不可以再次赋值了
  • 区别4

    • var 声明的变量没有自身的作用域
    • let、const 声明的变量有自身的作用域

二十、箭头函数和普通函数有什么区别

  • this 指向的问题
    • 箭头函数中的this是在箭头函数定义时就决定的,而且不可修改(call、apply、bind)
    • 箭头函数的this指向外层第一个普通函数的this
  • 箭头函数不能new(不能当作构造函数)
  • 箭头函数 prototype 为 undefined,没有原型

二十一、sort()背后的原理是什么

  • V8引擎sort函数只给出了两种排序 InsertionSort(数量小于10的数组) 和 QuickSort(数量大于10的数组)
  • 之前的版本是:插入排序和快排,现在是冒泡
var arr1 = arr2.sort(function(a,b){
    return a-b
}) // 从小到大

二十二、js数组常见方法

  • 添加 / 删除元素:

    • push (...items) — 从结尾添加元素,
    • pop () — 从结尾提取元素,
    • shift () — 从开头提取元素,
    • unshift (...items) — 从开头添加元素,
    • splice (pos, deleteCount, ...items) — 从 index 开始:删除 deleteCount 元素并在当前位置插入元素。
    • slice (start, end) — 它从所有元素的开始索引 "start" 复制到 "end"(不包括 "end")返回一个新的数组。
    • concat (...items) — 返回一个新数组:复制当前数组的所有成员并向其中添加 items 。如果有任何 items 是一个数组,那么就取其元素。
  • 查询元素:

    • indexOf/lastIndexOf (item, pos) — 从 pos 找到 item,则返回索引否则返回 -1。
    • includes (value) — 如果数组有 value,则返回 true,否则返回 false。
    • find/filter (func) — 通过函数过滤元素,返回 true 条件的符合 find 函数的第一个值或符合 filter 函数的全部值。
    • findIndex 和 find 类似,但返回索引而不是值。
  • 转换数组:

    • map (func) — 从每个元素调用 func 的结果创建一个新数组。
    • sort (func) — 将数组排序排列,然后返回。
    • reverse () — 在原地颠倒数组,然后返回它。
    • split/join — 将字符串转换为数组并返回。
    • reduce (func, initial) — 通过为每个元素调用 func 计算数组上的单个值并在调用之间传递中间结果。

二十三、虚拟dom

  • 浏览器操作dom是很消耗性能的,需经历 解析 HTML,CSS → 构建 DOM,CSSDOM 树 → 生成渲染树render → 布局(Layout)→ 绘制(Paint)→ 合成(Composite)  等流程,直接操作真实 DOM 会触发多次重排 / 重绘,性能显著下降。
  • 虚拟dom:描述元素和元素之间的关系,创建一个js对象模拟真实的dom,如果组件内有响应式数据,数据发生改变的时候,render函数会生成一个新的虚拟dom,这个新的虚拟dom会和旧的虚拟dom进行比对,找到需要修改虚拟dom的内容,然后去对应真实dom中修改
  • diff算法:虚拟dom比对时使用,返回一个patch对象,这个对象的作用就是储存两个节点不同的地方,最后用patch里记录的信息更新真实的dom

Diff 算法采用深度优先遍历同层比较策略,遵循以下原则:

  1. 树分层比较:只比较同层级节点。
  2. 不同类型节点:直接替换整个节点及其子节点。
  3. 相同类型节点:仅更新节点属性和内容,复用子节点。
  4. 列表节点优化:通过key属性识别相同节点,减少不必要的移动和创建。

二十四、call、apply、bind的区别

  • 都是改变this指向和函数的调用,call和apply的功能类似,只是传参的方法不同
  • call 方法传的是一个参数列表
  • apply 传递的是一个数组
  • bind 传参后不会立刻执行,会返回一个改变了this指向的函数,这个函数还是可以传参的,bind()()

二十五、ES6新特性

  1. 新增块级作用域(let,const)
  2. 新增定义类的语法糖(class)
  3. 新增一种基本数据类型(symbol)
  4. Promise
  5. 新增模块化(import,export)
  6. 箭头函数

二十六、TDZ

特性letconstvar
块级作用域
变量提升✅(但存在 TDZ)✅(但存在 TDZ)
TDZ
必须初始化
允许重复声明
可重新赋值

二十七、some 和 every

  • some() :只要数组中至少有一个元素满足条件,就返回true,否则返回false
  • every() :只有数组中所有元素都满足条件,才返回true,否则返回false

二十八、cookie、localStorage、sessionStorage 的区别

特性cookielocalStoragesessionStorage
存储容量一般约 4KB,同一域名下数量有限制一般为 5MB - 10MB一般为 5MB - 10MB
数据有效期可设置过期时间,不设置则为会话级别,浏览器关闭即消失持久化存储,手动删除才会消失仅在当前会话有效,浏览器窗口关闭数据删除
作用域同一域名下所有页面可访问,通过 path 属性可限制访问路径同一域名下所有页面共享,不同域名无法访问同一域名下,不同浏览器窗口或标签页(非 window.open 打开)无法共享,window.open 打开的子窗口与父窗口可共享
数据传输每次 HTTP 请求会自动发送到服务器,增加额外开销仅存储在本地,需通过 JavaScript 代码手动发送到服务器仅存储在本地,需通过 JavaScript 代码手动发送到服务器
用途身份验证、记录登录状态、跟踪用户行为等存储用户配置信息、本地缓存数据等临时存储当前会话中需共享的数据,如多步骤表单填写过程中的数据
安全性易受 XSS 和 CSRF 攻击,可通过设置 HttpOnly 提高安全性可能受恶意软件或社会工程学手段攻击,可加密存储数据提高安全性安全性与 localStorage 类似,因生命周期短,数据泄露风险相对较小

Cookie 本身是受同源策略限制的,默认情况下不允许跨域访问,但可以通过特定配置实现跨域场景下的 Cookie 传递与访问。以下是详细说明:

完全跨域(如 a.com 和 b.com

需通过 CORS(跨域资源共享)  配合后端配置实现:

  • 前端请求:需在 AJAX 请求中添加 withCredentials: true(表示允许携带跨域 Cookie)。

    fetch('https://b.com/api', {
      method: 'GET',
      credentials: 'include' // 或 XMLHttpRequest 的 withCredentials: true
    });
    
  • 后端响应:需返回以下 HTTP 头,允许跨域携带 Cookie:

    http

    Access-Control-Allow-Origin: https://a.com  // 必须指定具体域名,不能用 *
    Access-Control-Allow-Credentials: true     // 允许携带 Cookie
    
    • 注意:Access-Control-Allow-Origin 不能设为 *,必须明确指定前端域名,否则浏览器会拒绝接收 Cookie。

二十九、在哪里存 taken

存储位置优势劣势
Cookie方便在客户端和服务器间传递,设置 HttpOnly 可防 XSS 攻击增加 HTTP 请求流量开销,易受 CSRF 攻击
LocalStorage存储容量大,数据持久化,便于后续会话使用可被客户端脚本读取修改,有安全风险,需手动管理过期时间
SessionStorage数据仅在当前会话有效,窗口关闭自动删除,安全风险低不同窗口或标签页间无法共享,不适用多窗口共享登录状态场景
内存在应用各组件中访问快速,不受同源策略限制,页面关闭数据清除应用异常崩溃或关闭时 Token 丢失,复杂应用中状态管理较复杂

三十、js垃圾回收机制

标记清除(Mark and Sweep)

这是最基础的算法,现代引擎在此基础上优化:

  1. 标记阶段:从根对象开始,递归标记所有可达对象。
  2. 清除阶段:遍历堆内存,回收未被标记的对象。
  3. 内存整理(可选):移动对象以合并碎片空间。

标记整理(Mark and Compact)

在标记清除的基础上,回收后将存活对象移动到连续内存空间,减少碎片。

分代回收(Generational Garbage Collection)

基于 “多数对象生命周期短暂” 的观察,将对象分为:

  • 新生代(Young Generation) :新创建的对象(如局部变量),频繁垃圾回收。
  • 老生代(Old Generation) :长期存活的对象(如全局变量),较少垃圾回收。

三十一、一个DOM事件从产生到触发的流程是怎么样的

  • 事件捕获:事件产生后,浏览器会从文档根节点(document  开始,向下逐层传播到目标元素的父级。
  • 事件委托:当子元素触发事件后,事件冒泡到父元素,父元素的监听器通过 event.target 判断事件来源并处理。
  • 事件冒泡:返回根节点

三十二、object和map的区别

特性ObjectMap
键的类型字符串或 Symbol任意类型(包括对象、函数等)
顺序性不保证顺序(ES2015+ 字符串键按插入顺序,但非强制)键值对按插入顺序排列,迭代时保持顺序
默认键原型链上的属性可能造成冲突(如 toString无默认键,仅包含显式添加的键值对
键值对数量需手动计算(如 Object.keys(obj).length直接通过 size 属性获取
性能大规模数据操作时性能较差频繁增删键值对场景下性能更优
迭代支持需手动转换为数组(如 Object.entries()直接支持 for...offorEach 等迭代方法
序列化可通过 JSON.stringify() 序列化默认不支持序列化,需手动处理