常见基础面试题
数据类型
原始类型:number,string,boolean,null,undefined,bigint,symbol
引用类型:Object
- 标准普通对象:
Object
- 标准准特殊对象:
Array,Date,Math,Error,RegExp
- 非标准特殊对象(包装类型):
Number,String,Boolean
- 可执行对象:
Function
Symbol应用
- 对象唯一值属性
- 唯一标识统一管理(pinia 中的 storeId)
- iterator底层实现机制
类型检测方式
- typeof:所有数据类型,在计算机底层都是按照‘64 位二进制’进行存储的, typeof是按照二进制值进行检测类型的
详细转换机制
- 若二进制前三位是‘0’,并且实现了
call
方法,则返回function
,没有实现则返回object
- null是 64 个 ‘0’,
typeof null
---> 'object'(局限性) - 检测未被声明的变量,返回
undefined
:::
const isObj = option => option !== null && (typeof option === 'object' || typeof option === 'function')
数据类型转换
其他类型转为数字类型
规则:
宇符串转换为数宇:空宇符串变为0,如果出现任何非有效数宇宇符,结果都是NaN
把布尔转换为数宇:
true->1 ; false->0
null->0 ; undefined->NaN
Symbol无法转换为数宇,会报错:
Uncaught TypeError
BigInt转为数字,会去除“n”(超过安全数宇的,会按照科学计数法处理)
把对象转换为数字:
- 先调用对線的
Symbol.toPrimitive
这个方法,如果不存在这个方法 - 再调用对泉的
valueof
获取原始值,如果获取的值不是原始值,比如数组 - 再调用对線的
toString
把其变为字符串 - 最后再把宇符串基于Number方法转换为数字
Number.parseInt(val, radix)
Number.parseFloat(val)
Number.parseInt(null)
Number.parseInt(undefined) // -> NaN
/* 规则:val 必须是字符串,不是则自动转为字符串;
然后再 从左往右 找符合 radix(进制)有效数字,一个没找到则返回 NaN,
遇到一个非有效数字字符,则停止查找,并 parseInt(之前符合的字符串) parseInt('12px') -> 12
*/
其他类型转为字符串
隐式转换:String(val)
let a = '10'
const n = a++ // 10(数字) ++a / a++ 一定是数字运算
// +出现在左边,转换为数字 +'10'->10
其他类型转为布尔类型
除了falsy值: 0、-0、NaN、null、undefined、空串
,其余都是 true
相等与全等区别
相等(==):两边数据类型不同,会先进行类型转换,再进行比较
- 对象与字符串:对象转字符串
Symbol.toPrimitive -> valueOf -> toString
,再比较 null == undefined --> true
;null/undefined 与其他任何值都不相等- 对象与对象,比较的是内存地址是否相同
- NaN 与其他,NaN 不与任何值相等,
NaN == NaN --> false
- 除了以上情况,只要两边类型不一致,剩下的都是转为数字,再进行比较
全等(===):两个操作数类型相同,值也需相同才返回 true
可用==的情况:使用时相当于 obj.a === null || obj.a === undefined
- 判断对象的属性是否存在
obj.a == null
- 判断函数的参数是否存在
arg == null
面试题
//题1:
[] == false // -> 0 == 0 -> false
![] == false // -> false == false -> true
//题2:
var a = ?;
if(a == 1 && a == 2 && a == 3){
console.log('OK')
}
//解法一:利用==会进行类型转换,对象转为数字会经历:
//Symbol.toPrimitive -> valueOf -> toString
var a = {
i: 0,
[Symbol.toPrimitive](){
return ++this.i
}
}
//解法二:重写 toString
var a = [1, 2, 3]
a.toString = a.shift
//解法三: 数据劫持
var i = 0
Object.defineProperty(window, 'a', {
get(){
return ++i
}
})
怎么解决精度问题
根本原因:所有数据类型在计算机底层都是以64位二进制存储的,可能出现无限循环,超过 64 位部分被丢弃
- 将数字转为整数【扩大系数法】
- 第三方库:Math.js, decimal.js, big.js
var/let/const 区别
var | let | const | |
---|---|---|---|
变量提升 | 支持 | 不支持 | 不支持 |
块级作用域 | 没有 | 有 | 有 |
暂存性死区 | 没有 | 存在 | 存在 |
能否修改 | 变量的值不能修改 | ||
变量的值不能修改 | 允许 | 不允许 | 不允许 |
函数声明会覆盖其他的同名的变量声明。如果有多个函数声明,则是由最后的一个函数声明覆盖之前所有的声明。
const
声明必须初始化
只读常量,变量的值不能修改
简单类型 :值就保存在变量指向的那个内存地址,等同于常量。
引用类型:变量指向的内存地址,保存的只是一个指向实际数据的指针;
const 只能保证指针固定,不能控制指向的数据结构。
暂存性死区
暂存性死区
暂存性死区:使用 let / const
定义该变量之前的区域,
不能在声明前访问,作用域内被声明,不受外部影响
基于 let/const 变量声明,在词法解析阶段就已经明确了未来上下文中必定会有相关变量声明;
如果在声明前使用,则报错 Uncaught ReferenceError
this指向
- 全局作用域中或者普通函数中指向全局对象window
- 立即执行函数必定指向window
- 构造函数中指向对象实例
- 事件绑定指向事件源对象
- 方法中谁调用就指向谁
- 定时器回调为普通函数指向window,箭头函数指向声明时所在外部作用域
- forEach等回调函数默认指向
window
,指定第二个参数存在则指向第二个参数 - 严格模式指向
window
的变为undefined
防抖与节流
防抖:频繁触发某个事件时,在设定的时间内再次触发,会重新清除上一次定时器,重新开启定时器开始计时。
节流:频繁触发某个事件时,保证设定的时间内只会执行一次,只有等执行完才会打开节流阀执行下一个事件。
防抖
// 实现原理:每次触发事件时,取消之前的定时器,重新计时
function debounce(func, delay = 500) {
let timer = null
return function (...args) {
if (timer)
clearTimeout(timer) // 清除上一次
timer = setTimeout(() => {
func.apply(this, args)
}, delay)
}
}
防抖拓展写法
function debounce(func, delay = 500, immediate = false) {
// 参数判断处理
if (typeof func !== 'function')
throw new TypeError('func is not a funciton')
// debounce(func,true)
if (typeof delay === 'boolean') {
immediate = delay
delay = undefined
}
delay = +delay
Number.isNaN(delay) ? 500 : delay // 若不是数字则默认 500
if (typeof immediate !== 'boolean')
immediate = false
let timer = null
return function (...args) {
// 第一次自执行完,timer 已经有值,
const now = !timer && immediate
if (timer)
clearTimeout(timer) // 清除上一次
timer = setTimeout(() => {
// 结束边界触发
if (!immediate)
func.apply(this, args)
// 清除最后一个定时器
timer = clearTimeout(timer)
}, delay)
// 若为立即执行,则第一次,开启边界触发
if (now)
func.apply(this, args)
}
}
节流
// 实现原理:每次触发事件时,判断当前是否存在等待执行的延时函数
// 方法一:定时器存在则什么都不做
function throttle1(func, delay) {
let timer = null
return function (...args) {
if (timer)
return
timer = setTimeout(() => {
func.apply(this, ...args)
timer = null
}, delay)
}
}
// 方法二:
function throttle2(func, delay) {
let flag = true // 节流阀:开启状态
return function (...args) {
if (!flag)
return
flag = false // 已经在处理:关闭
setTimeout(() => {
func.apply(this, args)
flag = true // 处理完:重新打开
}, delay)
}
}
// 实现三:当前时间-上次执行时间 > 设定时间,才执行
function throttle3(func, delay) {
let start = 0
return function (...args) {
const now = Data.now()
if (now - start > delay) {
func.apply(this, args)
start = now
}
}
}
应用
防抖:输入框搜索、编辑框实时保存,(手机息屏策略,王者荣耀回城操作) 节流:滚动到底部加载更多、图标跟随鼠标(地铁发车时间,王者荣耀技能冷却)
前端本地存储方式
区别
存储大小 | 存储时间 | 同源策略 | 在同一浏览器的相同域名、不同端口号下 | |
---|---|---|---|---|
localStorage | 5M | 键值对以字符串形式存储,数据长期保留,不主动删除一直存在 | 受限制 | 不可以共享 |
sessionStorage | 5M | 会话结束自动清除(浏览器窗口关闭时) | 协议隔离 | 跳转的页面可以共享 |
cookie | 4kb | 设置到期时间,过期清除 | 受限制 | 可以共享 |
用法
localStorage
localStorage.setItem('key', 'value') // 添加数据
const val = localStorage.getItem('key') // 读取数据
localStorage.removeItem('key') // 移除单个
localStorage.clear() // 移除所有
sessionStorage
sessionStorage.setItem('key', 'value') // 保存数据到 sessionStorage
const data = sessionStorage.getItem('key')// 获取数据
sessionStorage.removeItem('key') // 删除
sessionStorage.clear() // 删除所有
cookie
const cookies = document.cookie // 同源下获取所有cookie
document.cookie = 'key=value' // 添加
document.cookie = 'key=; expires=xxx' // 设定到期时间自动清除
Generator
一是,function
关键字与函数名之间有一个星号;
二是,函数体内部使用yield
表达式,定义不同的内部状态。
function* helloWorldGenerator() {
const a = yield 'hello'
console.log(a) // 123
const b = yield 'world'
console.log(b) // undefined
return 'ending'
}
const hw = helloWorldGenerator()
hw.next()
hw.next(123) // a = 123
nex()
传参会赋值给上一次yield
的的返回值
异步流程同步化
function* test() {
const res1 = yield new Promise((resolve) => {
setTimeout(() => {
resolve('第一秒执行')
}, 1000)
})
console.log(res1) // 第一秒执行
const res2 = yield new Promise((resolve) => {
setTimeout(() => {
resolve('第二秒执行')
}, 1000)
})
}
function generatorRunner(fn) {
const generator = fn()
const step = generator.next()
// 定义递归函数
function loop(stepArg, generator) {
// 获取本次 yield 右侧的结果
const value = stepArg.value
if (value instanceof Promise) {
// 如果是 Promise 对象就在 then 函数的回调中获取本次程序结果
// 并且等待回调执行的时候进入下一次递归
value.then((promiseValue) => {
if (!stepArg.done) {
loop(generator.next(promiseValue), generator)
}
})
}
}
loop(step, generator)
}
requestAnimationFrame
60HZ/1000ms = 16.67ms(显示屏每毫秒刷新频率)
起因:setTimeout/setInterval是异步 API,设置的时间间隔没办法保证。
解决:为了设置更精确动画时间间隔、达到平滑动画效果。用法类似 setTimeout
API, 本质采用的是系统时间间隔,而不是 JS 执行时间间隔。
希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。 该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
用法:
浅拷贝和深拷贝
解释
浅拷贝:只拷贝一层,属性为对象时只复制值,不复制对象本身,指向同一对象 深拷贝:完全拷贝一份新的,新对象更改不会影响到旧对象
浅拷贝
const newObj = Object.assign({}, obj) // 只拷贝对象自身的可枚举的属性
const newObj1 = [...obj]
// 数组
Array.prototype.slice()
Array.prototype.concat()
const arr = [
{
name: 'zhan',
age: 23
},
123,
456
]
const newArr1 = arr.slice()
const newArr2 = arr.concat()
// 改变内部的引用类型两者都会有影响
// 方式一:
// 会忽略undefined,function,RegExp,Date
const newObj = JSON.parse(JSON.stringify(obj))
// 方式二:
function deepClone(obj) {
if (obj === null || typeof obj !== 'object')
return obj
if (obj.constructor === Date)
return new Date(obj)
if (obj.constructor === RegExp)
return new RegExp(obj)
const newObj = Array.isArray(obj) ? [] : {}
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key))
newObj[key] = deepClone(obj[key])
}
return newObj
}
事件捕获和冒泡机制
事件传播过程:事件捕获 ---> 目标阶段---> 事件冒泡
事件捕获:从根元素(html)向内传播,直到目标元素,途中会触发绑定事件捕获元素的回调。element.addEventListener(event, function, true)
。
事件冒泡:从目标元素向外传播,直到根元素,途中会触发绑定事件冒泡元素的回调。onclick
、addEventListener
默认绑定冒泡阶段。
阻止冒泡:event.stopPropagation()
阻止默认行为:event.preventDefault()
,例如:a标签跳转,表单按钮数据提交至服务器。
阻止同一节点其他后绑定事件执行:event.stopImmediatePropagation()
Promise链式调用
是指在一个Promise对象上多次调用then方法,每个then方法都可以接收上一个Promise对象的返回值,
并返回一个新的Promise对象,从而形成一个Promise链 ,从而实现多个异步操作的顺序执行和数据传递。
如果then方法中抛出了异常,那么后续的then方法会被跳过,直接执行catch方法。
如果then方法中返回了一个Promise对象,那么后续的then方法会等待该Promise对象的状态发生改变后再执行。
可以避免回调地狱,使得代码更加清晰易读。