前端高频面试题2:JavaScript/TypeScript
1.什么是类数组对象
一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。常见的类数组对象有 arguments 和 DOM 方法的返回结果,还有一个函数也可以被看作是类数组对象,因为它含有 length 属性值,代表可接收的参数个数。
常见的类数组转换为数组的方法有这样几种:
(1)通过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arrayLike);
(2)通过 call 调用数组的 splice 方法来实现转换
Array.prototype.splice.call(arrayLike, 0);
(3)通过 apply 调用数组的 concat 方法来实现转换
Array.prototype.concat.apply([], arrayLike);
(4)通过 Array.from 方法来实现转换
Array.from(arrayLike);
2.Js有哪些基本数据类型
简单类型
Number
String
Boolean
Symbol
Null
Undefined
复杂数据类型
Object
Array
Function
Map
Set
3.Js判断类型的几种方式,有什么区别
方法 | 支持类型 | 跨框架可靠性 | 原始类型 | 引用类型细分 | 特殊说明 |
typeof | 原始类型、函数 | 可靠 | ✔️ | ❌ | null 返回 "object" |
instanceof | 引用类型 | 不可靠 | ❌ | ✔️ | 依赖原型链 |
Object.prototype.toString | 所有类型 | 可靠 | ✔️ | ✔️ | 最全面 |
1.typeof
作用:返回变量的原始类型(字符串形式)。 语法:
typeof variable
返回值:
"undefined"
(未定义)
"boolean"
(布尔值)
"string"
(字符串)
"number"
(数字)
"bigint"
(BigInt类型)
"symbol"
(Symbol类型)
"function"
(函数)
"object"
(对象、数组、null
等)
"object"
(未识别的ES6+类型,如Map
、Set
)特点:
对原始类型(除
null
)有效,但对引用类型无法细分(如数组、对象、null
均返回"object"
)。
typeof null === "object"
(历史遗留问题)。无法区分对象的具体类型(如
Date
、RegExp
)。适用场景:快速判断原始类型或函数。
2.instanceof
作用:检测变量是否为某个构造函数的实例(基于原型链)。 语法:
variable instanceof Constructor
返回值:true
或false
。特点:
适合判断引用类型(如数组、自定义类)。
无法判断原始类型(如
1 instanceof Number
为false
,除非用new Number(1)
创建)。跨框架/窗口(iframe)时会失效,因为不同全局环境下的构造函数不共享原型。
若原型链被修改,结果可能不准确。
3.Object.prototype.toString.call()
作用:返回变量的内部
[[Class]]
类型标识。 语法:Object.prototype.toString.call(variable)
返回值:形如"[object Type]"
的字符串,如"[object Array]"
、"[object Null]"
。特点:
最全面的类型判断方法,支持所有内置类型(包括
null
和undefined
)。可通过
Symbol.toStringTag
自定义类型标签(ES6+),但大部分内置对象不受影响。对原始类型和引用类型均有效。
适用场景:需要精确判断所有类型(包括ES6+新增类型)。
4. Js的事件循环(Event Loop)是什么
Js是一种单线程语言(HTML5提供了Web Worker可以开启子线程),同步代码直接交由Js引擎执行,异步代码则交由浏览器或Node.js执行。
关键概念:
- 调用栈(Call Stack):用于跟踪当前正在执行的函数(执行上下文)。当函数被调用时,它被压入栈;当函数返回时,它被弹出栈。
- 任务队列(Task Queue):也称为宏任务队列(Macro Task Queue),包括 setTimeout、setInterval、I/O、事件回调等。
- 微任务队列(Micro Task Queue):包括 Promise 的回调(then/catch/finally)、MutationObserver、queueMicrotask 等。微任务在当前宏任务结束后立即执行,且会清空整个微任务队列,直到队列为空。
- 渲染(Render)步骤:在事件循环中,可能会在宏任务和微任务之间进行页面渲染(UI 更新),但这不是事件循环规范的一部分,而是浏览器行为。
事件循环执行过程:
- 首先依次执行调用栈中的全部同步代码
- 处理微任务:
- 调用栈清空后会执行全部微任务
- 微任务执行期间加入的微任务也会顺次执行
- 渲染更新(浏览器行为与Js事件循环无关)
- 执行一个宏任务
- 重复循环
常见的宏任务与微任务
- 宏任务
- setTimeout/setInterval
- I/O操作
- 浏览器渲染
- 事件回调(click等事件)
- 脚本执行(不同的script标签)
- 微任务
- Promise相关方法(Promise.then等)
- MutationObserver(Dom变化监视)
5.下面代码的输出顺序是什么(事件循环)
async function async1() {console.log('async1 start');await async2(); // 后续为微任务console.log('async1 end');
}async function async2() {console.log('async2');
}console.log('script start');setTimeout(function() {console.log('setTimeout');
}, 0) // 宏任务async1();new Promise(function(resolve) {console.log('promise1');resolve();
}).then(function() { // 微任务console.log('promise2');
});console.log('script end');
// 顺序如下
script start // 第一个同步任务
async1 start // async1方法中的第一个同步任务
async2 // await修饰的方法立即执行,输出async2,async1方法进入微任务队列
promise1 // 执行new Promise,输出promise1,并进入微任务队列
script end // 执行同步代码
async1 end // 执行微任务队列中的第一个任务
promise2 // 执行微任务队列的第二个任务
setTimeout // 执行宏任务
6.Js原生如何获取cookie
document.cookie // 结果是一个Json字符串,可使用JSON.parse()转换
7.Promise.all/allSettled/any/race的区别
Promise.all
核心:等待所有 Promise 全部完成(fulfilled),或遇到第一个失败(rejected)。
结果:
全部成功时:返回一个数组,元素为每个 Promise 的成功值(顺序与输入一致)。
遇到失败时:立即拒绝(reject),返回第一个失败的 Promise 的原因。
适用场景:多个异步操作强依赖,需要所有结果都成功(如:并发请求多个接口)。
示例:
Promise.all([promise1, promise2]) .then(values => console.log(values)) // 所有成功:[value1, value2].catch(error => console.log(error)); // 任一失败(返回第一个错误)
Promise.allSettled
核心:等待所有 Promise 全部完成(无论成功或失败)。
结果:永远成功(resolve),返回一个数组,每个元素为对象:
{ status: "fulfilled", value: <result> }
(成功)
{ status: "rejected", reason: <error> }
(失败)适用场景:需知道每个操作的最终状态(无论成功失败)(如:批量操作后汇总结果)。
示例:
Promise.allSettled([promise1, promise2]).then(results => { results.forEach(result => { if (result.status === "fulfilled") console.log(result.value); else console.error(result.reason); }); });
Promise.any
核心:等待第一个 成功(fulfilled)的 Promise,或所有都失败。
结果:
任一成功时:返回第一个成功的值。
全部失败时:拒绝(reject)并抛出
AggregateError
,包含所有错误信息。适用场景:获取最快成功的操作(如:多服务器请求,取最快响应)。
示例:
Promise.any([promise1, promise2]).then(value => console.log(value)) // 第一个成功的值.catch(errors => console.log(errors)); // 所有失败(AggregateError 包含错误数组)
Promise.race
核心:等待第一个 完成(无论成功或失败)的 Promise。
结果:返回第一个完成的 Promise 的结果(可能是成功值或失败原因)。
适用场景:竞速场景(如:请求超时控制)。
示例:
// 实现超时控制 const timeout = new Promise((_, reject) =>setTimeout(() => reject("Timeout"), 1000) ); Promise.race([fetchData(), timeout]).then(data => console.log(data)) // 在超时前完成.catch(error => console.log(error)); // 超时或请求失败
对比表格
方法 行为描述 成功条件 失败条件 返回值类型 Promise.all 所有成功或任一失败(短路) 全部成功 第一个失败 数组(成功值) Promise.allSettled 等待所有完成(无论成败) 永不失败 - 对象数组(包含状态和值/原因) Promise.any 等待第一个成功或所有失败 第一个成功 全部失败 第一个成功的值 Promise.race 等待第一个完成(无论成败) 第一个完成的结果是成功 第一个完成的结果是失败 第一个完成的结果 关键区别
all
vsallSettled
:all
会在遇到失败时立即终止,而allSettled
始终等待所有结果。
any
vsrace
:any
只关心第一个成功(忽略失败),race
关心第一个完成(无论成败)。错误处理:
any
在所有失败时返回AggregateError
;其他方法直接返回单条错误原因。根据实际需求选择合适的方法:需要全部成功用
all
,容忍失败用allSettled
,取最快成功用any
,竞速场景用race
。
8.原型对象、构造函数、实例之间的关系
对象 | 指向关系 | 属性/方法 |
构造函数 | prototype → 原型对象 | Person.prototype |
原型对象 | constructor → 构造函数 | Person.prototype.constructor |
实例 | __proto__ → 原型对象 | person.__proto__ |
9.Proxy和Object.defineProperty的区别
Object.defineProperty
: 只能对已存在的属性进行劫持,无法拦截新增的属性和删除的属性(需要通过Vue.set
和Vue.delete
实现响应式),由于劫持基于属性级别,对于大规模对象或者数组来说会导致性能下降;(Vue2响应式的实现方法)
Proxy
: 劫持整个对象,并返回一个代理对象,提高了初始化性能,同时可以拦截新增属性和删除属性。(Vue3响应式的实现方法)
10.使用Proxy代理一个对象,是对这个对象所有层级进行了劫持吗
不会,proxy只代理外层对象,内层对象需要单独proxy代理
11.简单数组去重的方法
- 使用
Set
只运行存储唯一值的特点
const arr = [1, 2, 2, 3, 3, 4, 5, 5, 6]
const uniqueArr = [...new Set(arr)]
// 或 const uniqueArr = Array.from(new Set(arr))
-
使用
includes
方法检查新数值中是否存在该元素
const arr = [1, 2, 2, 3, 3, 4, 5, 5, 6]
const uniqueArr = []
arr.forEach(val => { if(!uniqueArr.includes(val)) { uniqueArr.push(val) }
}
-
使用
indexOf
方法,检查新数组中是否存在该元素
const arr = [1, 2, 2, 3, 3, 4, 5, 5, 6]
const uniqueArr = []
arr.forEach(val => { if(uniqueArr.indexOf(val) === -1) { uniqueArr.push(val) }
}
-
使用
filter
方法过滤第一次出现的元素
const arr = [1, 2, 2, 3, 3, 4, 5, 5, 6]
const uniqueArr = arr.filter((val, index, self) => self.indexOf(val) === index
}
-
使用
reduce
方法遍历数组元素
const arr = [1, 2, 2, 3, 3, 4, 5, 5, 6]
const uniqueArr = arr.reduce((res, current) => {if(!res.includes(current) {res.push(current) } return res
}, [])
12.对象数组如何去重
-
id(或其他键相同算为重复)
-
使用
Map
-
function uniqueById(arr) { return [ ...new Map( arr.map(item => [item.id, item]) ).values() ];
} // 使用示例
const users = [ { id: 1, name: 'John' },{ id: 2, name: 'Jane' }, { id: 1, name: 'Johnny' }, // 相同id { id: 3, name: 'Alice' }
];
console.log(uniqueById(users));
// [
// { id: 1, name: 'Johnny' },
// { id: 2, name: 'Jane' },
// { id: 3, name: 'Alice' }
// ]
- 使用
filter
+缓存
function uniqueById(arr) { const seen = new Set(); return arr.filter(item => { const duplicate = seen.has(item.id); seen.add(item.id); return !duplicate; });
}
- 使用
reduce
function uniqueById(arr) { return Object.values(arr.reduce((acc, item) => { acc[item.id] = item; return acc; }, {}) );
}
- 完全相同算重复
-
使用JSON.stringify(简单对象)
-
function deepUnique(arr) { const seen = new Set(); return arr.filter(item => { const serialized = JSON.stringify(item); return seen.has(serialized) ? false : seen.add(serialized); });
}
// 使用示例
const items = [ { id: 1, name: 'Apple' }, { id: 2, name: 'Banana' }, { id: 1, name: 'Apple' }, // 相同对象 { id: 1, name: 'apple' } // 不同对象
];
console.log(deepUnique(items));
// [
// { id: 1, name: 'Apple' },
// { id: 2, name: 'Banana' },
// { id: 1, name: 'apple' }
// ]
- Lodash的isEqual方法(复杂对象推荐)
// 需要安装lodash:npm install lodash
import _ from 'lodash';
function deepUnique(arr) { return arr.reduce((acc, item) => { const isDuplicate = acc.some(accItem => _.isEqual(accItem, item)); return isDuplicate ? acc : [...acc, item]; }, []);
}
创建于2025.6.2,后续继续更新