JavaScript
1、JavaScript 中的数据类型?存储上的差别
基本类型:Number、String、Boolean、Undefined、null、symbol
引用类型:统称 Object,如:Object、Array、Function、Date、RegExp、Map、Set
存储区别:
- 基本数据类型存储在栈中
- 引用类型的对象存储于堆中
2、数据结构
什么是数据结构:数据结构是计算机存储、组织数据的方式
常见的数据结构:
- 数组(Array)
- 栈(Stack)
- 队列(Queue)
- 链表(Linked List)
- 字典
- 散列表(Hash table)
- 树(Tree)
- 图(Graph)
- 堆(Heap)
3、DOM
创建节点:
- createElement(“div”):创建新元素
- createTextNode(“content”):创建文本节点
- createAttribute(“custom”):创建属性节点
获取节点:
- querySelector(“.element”):选中单个 DOM 元素(首个)
- querySelectorAll(“p”):返回多个匹配的节点列表
更新节点:
- innerHTML:通过 HTML 片段修改 DOM 节点内部的子树,替换原来的子树
- innerText、textContent:通过字符串片段修改 DOM 节点内部的子树,无法设置任何的 HTML 标签
- style:DOM 节点的 style 属性对应的所有 CSS
添加节点:
- appendChild:把子节点添加到父节点的最后一个子节点
- insertBefore:把子节点插入到指定的位置
- setAttribute:在指定元素中添加一个属性节点
删除节点:
- removeChild:删除子节点,
parent.removeChild(self)
4、对 BOM 的理解,常见的 BOM 对象你了解哪些?
什么是 BOM:BOM 指的是浏览器对象模型,提供了独立于内容与浏览器窗口进行交互的对象
DOM:
- 文档对象模型
- DOM 就是把文档当作一个对象来看待
- DOM 的顶级对象是document
- DOM 主要学习的是操作页面元素
- DOM 是 W3C 标准规范
BOM:
- 浏览器对象模型
- 把浏览器当做一个对象来看待
- BOM 的顶级对象是window
- BOM 学习的是浏览器窗口交互的一些对象
- BOM 是浏览器厂商在各自浏览器上定义的,兼容性较差
BOM 对象:
window、location、navigator、screen、history
5、、=、Object.is()的区别,分别在什么情况使用
==:比较中会进行类型转换,在确定操作数是否相等,注:null 和 undefined 相等、存在 NaN 则返回 false
===:不会进行类型转换,即类型相同、值也需相同,注:null 和 undefined 不相等,两者与自身严格相等
object.is():与===类似,但是+0 不等于-0,NaN 等于自身
6、typeof 与 instanceof 的区别
- 返回值不同:
typeof
返回一个变量的基本类型,instanceof
返回一个布尔值 instanceof
可以准确的判断复杂引用数据类型,但是不能正确判断基础数据类型typeof
可以判断除 null 外的基本数据类型,但是引用数据类型中,除了 function 类型外,其他的都无法判断
全局通用的数据类型判断方法:Object.prototype.toString
,返回格式"[object Xxx]"
的字符串,使用方法:Object.prototype.toString.call({})
7、JavaScript 原型,原型链?有什么特点?
原型:JavaScript 常被描述为一种基于原型的语言—每个对象拥有一个原型对象(prototype)
原型链:原型对象也可能永远有原型,对从中继承方法和属性,一层一层、以此类推。这种关系被称为原型链,它解释了为何一个对象会拥有定义在其他对象中的属性和方法。
原型链的终点:null,使用Object.prototype.__proto__
可以打印出来
显式原型:prototype
隐式原型:proto(对象实例和它的构造器之间建立一个链接)
原型链图解:
8、对作用域链的理解
作用域:作用域,即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合。换句话说,作用域决定了代码区块中的变量和其他资源的可见性
作用域链:JavaScript 引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,直至找到或者全局作用域为止
作用域分成:
- 全局作用域:任何不在函数中或是大括号中声明的变量,都是在全局作用域中,全局作用域下声明的变量可以在程序的任意位置访问
- 函数作用域:也叫局部作用域,如果一个变量实在函数内部声明的它就在一个函数作用域下面,这些变量只能在函数内部访问,不能再函数之外访问
- 块级作用域:ES6 引入了 let 和 const 关键字,和 var 关键字不同,再大括号中使用 let 和 const 声明的变量存在于块级作用域中
9、谈谈 this 对象的理解
this 关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象,this 一旦确定就不可以再更改
箭头函数:箭头函数中没有 this,该 this 为外级作用域中的 this 指向
绑定规则:
- 默认绑定:严格模式下,不能将全局对象用于默认绑定,
this
会绑定到undefined
;再非严格模式下,默认绑定才能绑定到全局对象window
- 隐式绑定:this 永远指向的是最后调用它的对象
- new 绑定:使用
new
关键字生成一个实例对象,此时 this 指向这个实例对象;new
过程中return
一个对象,此时 this 指向为返回的对象 - 显式绑定:使用
apply、call、bind
函数来改变函数的调用对象
10、new 操作符具体做了什么
- 创建一个新的对象
obj
- 将对象与构建函数通过原型链连接起来
- 将构建函数中的
this
绑定到新建的对象obj
上 - 根据构建函数返回类型做判断,如果是原始值则被忽略,如果是返回对象,需要正常处理
手写 new 操作符:
const myNew = (fn, args) => {
// 1. 创建一个新对象
const obj = {}
// 2. 将新对象原型指向构造函数原型对象
obj.__proto__ = fn.prototype
// 3. 将构造函数的this指向新对象
const res = fn.apply(obj, [...args])
// 4. 根据返回值判断:对象则返回,否则返回新对象
return typeof res === 'object' ? res : obj
}
11、bind、call、apply 区别?如何实现一个 bind?
apply、call、bind
函数的作用是改变函数执行是的上下文,即改变函数运行时的 this 指向
区别:
- apply:接受两个参数,第一个参数为 this 的指向,第二个参数为函数接受的参数,以数组的形式传入;立即执行函数
- call:接受多个参数,第一个参数为 this 的指向,其余参数为函数的参数,逐个传入参数;立即执行函数
- bind:用于创建一个新函数,将 this 和部分参数绑定到原函数中;接受多个参数,第一个参数为 this 的指向,其余参数为函数的参数,逐个传入参数;不立即执行,返回改变了 this 指向的新函数
实现 bind 函数:
- 修改 this 指向
- 动态传递参数
- 返回新函数
const myBind = context => {
// 判断是否为function
if (typeof this !== 'function') {
throw new TypeError('not a function')
}
// 获取参数,默认第一个为this的指向(context),所以截取
const args = [...arguments].slice(1)
// 保存函数本身
const fn = this
// 返回新函数
return function Fn() {
// 改变this指向,合并调用myBind的参数和Fn的参数
return fn.apply(context, [...args, ...arguments])
}
}
12、执行上下文和执行栈是什么?
执行上下文:执行上下文是一种对 javascript 代码执行环境的抽象概念,也就是说只要又 javascript 代码运行,那么它就一定是运行在执行上下文中
生命周期:创建阶段 --> 执行阶段 --> 回收阶段
执行上文文类型有以下三种:
- 全局执行上下文:只有一个,浏览器中的全局对象就是 window 对象,this 指向这个全局对象
- 函数执行上下文:存在无数个,只有再函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文
- Eval 函数执行上下文:指的是运行在
eval
函数中的代码,很少用而且不建议使用
13、说说 Javascript 中的事件模型
事件流的三个阶段:
- 事件捕获阶段
- 处于目标阶段
- 事件冒泡阶段
事件模型分为三种:
- 原始事件模型:onclick,绑定速度快、只支持冒泡不支持捕获、同一类型事件只能绑定一次
- 标准事件模型:addEventListener(),可以绑定多个相同事件处理器,各自并不会冲突,设置了第三个参数(useCapture)设置为 true 就在捕获过程中执行,反之在冒泡中执行处理函数
- IE 事件模型(基本不用)
14、解释下什么是事件代理?应用场景?
事件代理:指的是把一个元素的响应事件(click、keydown)的函数委托到另一个元素
事件委托:把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,而不是目标元素;通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后再外层元素上去执行函数
适合事件委托的事件:click、mousedown、mouseup、keydown、keyup、keypress
事件委托的优点:
- 减少整个页面所需的内存,提升整体性能
- 动态绑定,减少重复工作
15、说说对闭包的理解?闭包使用场景
闭包:一个函数和对其周围状态的引用捆绑在一起或者说函数被引用包围,这样的组合就是闭包
闭包的作用:闭包让你可以在一个内层函数中访问到其外层函数的作用域,每当创建一个函数,闭包就会在函数创建的同时被创建出来,座位函数内部与外部连接起来的一座桥梁
使用场景:
- 创建私有变量
- 延长变量的生命周期
16、谈谈 JavaScript 中的类型转换机制
常见的类型转换:
- 强制转换(显式转换)
- 自动转换(隐式转换)
显式转换
- Number():只要有一个字符串无法转换成数值,整个字符串就会被转为 NaN,对象除了包含单个数值的数组外,通常都是转换成 NaN,
""==0,undefined==NaN,null==0,true==1,false==0
- parseInt():逐个解析字符,遇到不能转换的字符串就停下来,
parseInt('32a3')==32
- String():
undefined=="undefined",null=="null",{a:1}=="[object Object]",[1,2,3]=="1,2,3"
- Boolean():
undefined、null、""、+0、-0、NaN转为false
,new Boolean(false)转为true
隐式转换
发生隐式类型转换的场景:
- 比较运算(==、!=、>、<)、if、while 需要布尔值的地方
- 算术运算(+、-、*、/、%)
自动转换为布尔值:通过 Boolean()函数转换
自动转换成字符串:先将复合类型的值转为原始类型的值,再将原始类型的值转为字符串;常发生在+
运算中
自动转换成数值:null0、undefinedNaN
17、深拷贝、浅拷贝的区别?如何实现一个深拷贝?
JavaScript 中存在两大数据类型,基本类型、引用类型,基本数据保存在栈内存中,引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用(内存地址),存在栈中
浅拷贝
浅拷贝:指的是创建新的数据,这个数据有着原始数据属性值得一份精确拷贝
拷贝规则:浅拷贝是拷贝一层,属性为对象是,浅拷贝是复制,两个对象指向同一个地址
浅拷贝现象(API):object.assign(),Array.prototype.slice(),Array.prototype.concat()、拓张运算符实现的复制
深拷贝
深拷贝:开辟一个新的栈,两个对象属性完全相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
拷贝规则:深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址,即完全相同,但是在内存中的地址不一样,互不影响
深拷贝现象(API):JSON.stringify()
JSON.stringify()函数弊端:会忽略 undefined、symbol、函数
自定义深拷贝函数:
const deepClone = obj => {
if (obj === null) {
return obj
}
// 可以定义多种类型:如Set、Map等
if (obj instanceof Date) {
return new Date(obj)
}
if (obj instanceof RegExp) {
return new RegExp(obj)
}
if (typeof obj !== 'object') {
return obj
}
const cloneObj = new obj.constructor()
// 循环递归
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key])
}
}
return cloneObj
}
18、JavaScript 中如何实现函数缓存?函数缓存有哪些应用场景?
函数缓存:将函数运算过的结果进行缓存,本质上就是空间(缓存存储)换时间(计算过程),常用于缓存数据计算结果和缓存对象
实现方法:闭包、函数柯里化、高阶函数
使用场景:
- 对于昂贵的函数调用,执行复杂计算的函数
- 对于具有有限且高度重复输入范围的函数
- 对于具有重复输入值的递归函数
- 对于纯函数,即每次使用特定输入调用时返回相同输出的函数
19、JavaScript 字符串的常用方法有哪些?
增(拼接)
+
:"abc"+"123"
${}
:abc${123}
concat()
:let result = "abc".concat('123','456')
const strVal = 'hello '
console.log(strVal + 'world') // hello world
console.log(`${strVal}world`) // hello world
console.log(strVal.concat('world')) // hello world
删
- slice():第一个参数为开始索引,第二个参数为结束索引(可选),左闭右开,只有一个参数时从指定索引开始截取后面所有。
"hellow world".slice(3)
- substr():第一个参数为开始索引,第二个参数为长度(可选),只有一个参数时从指定索引开始截取后面所有。
"hellow world".substring(3)
- substring():第一个参数为开始索引,第二个参数为结束索引(可选),左闭右开,只有一个参数时从指定索引开始截取后面所有。
"hellow world".substr(3)
const strVal = 'hello world'
console.log(strVal.slice(3)) // lo world
console.log(strVal.slice(3, 7)) // lo w
console.log(strVal.substr(3)) // lo world
console.log(strVal.substr(3, 7)) // lo worl
console.log(strVal.substring(3)) // lo world
console.log(strVal.substring(3, 7)) // lo w
改
- trim()、trimLeft()、trimRight():删除前、后或前后所有空格符,在返回新的字符串
- repeat():接收一个整数参数,表示将字符串复制多少次,然后返回拼接所有副本后的结果
- padStart()、padEnd():复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件
- toLowerCase()、toUpperCase():大小写转换
const strVal = ' hello world '
console.log(strVal.trim()) //hello world
console.log(strVal.trimLeft()) //hello world
console.log(strVal.trimRight()) // hello world
const strVal2 = 'ha'
console.log(strVal2.repeat(3)) // hahaha
console.log(strVal2) // ha
const strVal3 = 'red'
console.log(strVal3.padStart(10)) // red
console.log(strVal3.padStart(10, '-')) //-------red
const strVal4 = 'Blue'
console.log(strVal4.toUpperCase()) // BLUE
console.log(strVal4.toLowerCase()) // blue
查
- chartAt():返回给定索引位置的字符,由传给方法的整数参数指定
- indexOf():从字符串开头去搜索传入的字符串,并返回位置(如果没找到,则返回-1)
- startWith():判断字符串开头是否为传入的字符串,并返回一个表示是否包含的布尔值
- includes():从字符串搜索传入的字符串,并返回一个表示是否包含的布尔值
const strVal = 'abcde'
console.log(strVal.charAt(3)) // d
console.log(strVal.indexOf('d')) // 3
console.log(strVal.startsWith('ab')) // true
console.log(strVal.startsWith('bc')) // false
console.log(strVal.includes('de')) // true
转换方法
- split():将字符串按照指定的分隔符拆分成数组中的每一项
const strVal = 'abcde'
console.log(strVal.split('c')) // ['ab', 'de']
console.log(strVal) // abcde
模板匹配方法
- match():接受一个参数,可以是一个正则表达式字符串,也可以是一个 RegExp 对象,返回数组
- search():接收一个参数,可以是一个正则表达式字符串,也可以是一个 RegExp 对象,找到则返回匹配索引,否则返回-1
- replace():接收两个参数,第一个参数为匹配的内容,第二个参数为替换的元素(可用函数)
let text = 'cat, bat, sat, fat'
let pattern = /.at/g
let matches = text.match(pattern)
console.log(matches) // ["cat", "bat", "sat", "fat"]
console.log(text.search(/at/)) // 1
console.log(text.search(/at/g)) // 1
console.log(text.replace(/at/g, 'oo')) // coo, boo, soo, foo
console.log(text) // cat, bat, sat, fat
20、数组的常用方法有哪些?
增
- push:尾部追加,改变原数组,返回新数组长度
- unshift:头部添加,改变原数组
- splice:指定位置添加,改变原数组,第一个参数为开始索引,第二个参数为删除元素的数量,其他参数为插入的元素,返回空数组
- concat:拼接,不改变原数组,返回合并后的新数组
let strVal = ['a', 'b', 'c']
strVal.push(11)
console.log(strVal) // ['a', 'b', 'c', 11]
strVal.unshift(22)
console.log(strVal) // [22, 'a', 'b', 'c', 11]
strVal.splice(1, 0, 33, 44, 55, 66)
console.log(strVal) // [22, 33, 44, 55, 66, 'a', 'b', 'c', 11]
console.log(strVal.concat(77, 88, 99)) // [22, 33, 44, 55, 66, 'a', 'b', 'c', 11, 77, 88, 99]
console.log(strVal) // [22, 33, 44, 55, 66, 'a', 'b', 'c', 11]
删
- pop:删除尾部元素,改变原数组
- shift:删除头部元素,改变原数组
- splice:删除指定位置元素,改变原数组
- slice:删除指定位置元素,不改变原数组,返回删除目标组成的新数组
let strVal = ['a', 'b', 'c']
console.log(strVal.pop()) // c
console.log(strVal) // ['a', 'b']
console.log(strVal.shift()) // a
console.log(strVal) // ['b']
let strVal1 = ['a', 'b', 'c']
console.log(strVal1.splice(1, 1)) // ['b']
console.log(strVal1) // ['a', 'c']
let strVal2 = ['a', 'b', 'c', 'd', 'e']
console.log(strVal2.slice(1)) // ['b', 'c', 'd', 'e']
console.log(strVal2) // ['a', 'b', 'c', 'd', 'e']
let strVal3 = ['a', 'b', 'c', 'd', 'e']
console.log(strVal3.slice(1, 3)) // ['b', 'c']
console.log(strVal3) // ['a', 'b', 'c', 'd', 'e']
改
- splice:传入三个参数,分别是开发位置、要删除元素的数量、要插入的任意多个元素,返回删除元素的数组,对原数组产生影响
let strVal = ['a', 'b', 'c']
const result = strVal.splice(1, 0, 5, 6, 7, 8)
console.log(result) // []
console.log(strVal) // [ 'a', 5, 6, 7, 8, 'b', 'c' ]
查
- indexOf:返回要查找的元素在数组中的位置,如果没有则返回-1
- includes:查找指定元素是否在数组中,返回一个布尔值
- find:返回第一个匹配的元素
let strVal = ['a', 'b', 'c', 'd', 'e', 'f']
console.log(strVal.indexOf('d')) // 3
console.log(strVal.indexOf('g')) // -1
console.log(strVal.includes('d')) // true
console.log(strVal.includes('g')) // false
const arr = [
{
name: 'Bob',
age: 23,
},
{
name: 'Alice',
age: 21,
},
{
name: 'Tom',
age: 22,
},
]
console.log(arr.find((item, index) => item.age < 22)) // {name: "Alice", age: 21}
排序方法
- reverse:将数组元素方向反转
- sort:接受一个比较函数,用于判断哪个值应该排在前面
let strVal = ['a', 'b', 'c', 'd', 'e', 'f']
strVal.reverse()
console.log(strVal) // ['f', 'e', 'd', 'c', 'b', 'a']
const arr = [
{
name: 'Bob',
age: 23,
},
{
name: 'Alice',
age: 21,
},
{
name: 'Tom',
age: 22,
},
]
arr.sort((a, b) => a.age - b.age)
console.log(arr) // [{name: 'Alice', age: 21}, {name: 'Tom', age: 22}, {name: 'Bob', age: 23}]
转换方法
- join:接受一个参数,即字符串分隔符,返回包含所有项的字符串
let strVal = ['a', 'b', 'c', 'd', 'e', 'f']
console.log(strVal.join('=')) // a=b=c=d=e=f
console.log(strVal) // [ 'a', 'b', 'c', 'd', 'e', 'f' ]
迭代方法
- some:对数组每一项都运行传入的测试函数,如果至少有一个元素返回 true,则这个方法返回 true
- every:对数组每一项都运行传入的测试函数,如果所有元素都返回 true,则这个方法返回 true
- forEach:对数组每一项都运行传入的函数,没有返回值
- filter:对数组每一项都运行传入的函数,函数返回 true 的项会组成数组之后返回
- map:对数组每一项都运行传入的函数,返回由每次函数调用的结果组成的数组
21、说说你对事件循环的理解
事件循环:同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程内的任务执行完毕为空,回去任务队列读取对应的任务,推入主线程执行。不断重复上述过程就是事件循环
异步任务:
- 微任务:常见的如 Promise.then(catch、finally)、MutationObserver、process.nextTick(node.js)、async await
- 宏任务:script(外层同步代码,整个脚本)、setTimout、setInterval、postMessage、回调函数、setImmediate、I/O(node.js)
流程概况:
- 同步任务在主线程排队执行,异步任务在事件队列排队等待进入主线程执行
- 遇到宏任务则推进宏任务队列,遇到微任务则推进微任务队列
- 执行完当前层所有同步代码(宏任务)后,检查当前层的微任务执行
- 继续执行下一个宏任务,执行完毕后,执行对应层次的微任务,往复循环,直至执行完毕
事件循环图解:
22、JavaScript 本地存储的方式有哪些?区别及应用
本地存储方式:
- cookie:大小一般不超过 4KB,会随着请求发送至服务器,可以自定义过期时间
- sessionStorage:与 localStorage 使用犯法一致,唯一不同的是生命周期,一旦页面(会话)关闭,sessionStorage 将会删除数据(失效)
- localStorage:大小一般为 5M(与浏览器厂商有关),不会发送至服务器,持久化本地存储,除非主动删除数据,否则数据永远不会过期
- indexedDB:不会
区别:
- 存储大小:cookie 最大只有 4KB,sessionStorage 和 localStorage 最大有 5M
- 有效时间:cookie 在过期时间之前都有效,sessionStorage 在会话(页面)关闭之前有效,localStorage 持久有效,除非主动删除数据后失效
- 数据与服务器之间的交互:cookie 的数据会自动的传递到服务器,服务器也可以写 cookie 到客户端;sessionStorage 和 localStorage 不会自动把数据发给服务器,仅在本地保存
23、大文件上传如何做断点续传?
核心原理:
- 分片上传:将大文件切割成多个小分片(如 1MB/片),还可以根据网络质量自动调整分片大小(如 1M~5M)
// 使用 Blob.slice 切割文件
const chunkSize = 1 * 1024 * 1024 // 1MB
const chunks = []
let start = 0
while (start < file.size) {
const chunk = file.slice(start, start + chunkSize)
chunks.push(chunk)
start += chunkSize
}
- 唯一标识:生成文件唯一 Hash(如 MD5/SHA-256),标识文件身份
// 计算文件 Hash(使用 SparkMD5 库加速)
const fileHash = await calculateFileHash(file)
function calculateFileHash(file) {
return new Promise(resolve => {
const spark = new SparkMD5.ArrayBuffer()
const reader = new FileReader()
reader.readAsArrayBuffer(file)
reader.onload = e => {
spark.append(e.target.result)
resolve(spark.end())
}
})
}
- 进度记录:记录已上传成功的分片序号,断网后跳过已传分片
- 并行上传:使用 Promise.all 同时上传多个分片
- 断点恢复:页面刷新后通过 Hash 恢复上传记录
- 错误重试:对失败分片自动重试(如 3 次重试机制)
// 使用 localStorage 记录已传分片
const uploadedChunks = JSON.parse(localStorage.getItem(fileHash) || '[]')
for (let i = 0; i < chunks.length; i++) {
if (uploadedChunks.includes(i)) continue // 跳过已传分片
const formData = new FormData()
formData.append('chunk', chunks[i])
formData.append('hash', fileHash)
formData.append('index', i)
await axios.post('/upload', formData)
// 记录成功分片
uploadedChunks.push(i)
localStorage.setItem(fileHash, JSON.stringify(uploadedChunks))
}
- 合并校验:所有分片上传完成后,后端合并并校验文件完整性
axios.post('/merge', {
hash: fileHash,
total: chunks.length,
filename: file.name,
})
24、ajax 原理是什么?如何实现?
ajax:异步的 JavaScript 和 XML,是一种创建交互式网页应用的网页开发技术,可以在不重新加载整个网页的情况下,与服务器交换数据,并且更新部分网页
ajax 原理:通过XmlHttpRequest对象来向服务器发送异步请求,从服务器获得数据,然后启用 JavaScript 来炒作 DOM 而更新页面
实现过程:
- 创建
XMLHttpRequest
对象 - 通过
XMLHttpRequest
对象的open()
方法与服务器建立连接 - 通过
XMLHttpRequest
对象的send()
方法,将客户端页面的数据发送给服务端 - 通过通过
XMLHttpRequest
对象的onreadystatechange
事件用于监听服务器端的通信状态,主要监听的属性为XMLHttpRequest.readyState
- 接受并处理服务端向客户端响应的数据结果
- 将处理结果更新到 HTML 页面中
XMLHttpRequest.readyState 属性状态:
- UNSENT(未打开):open()方法还未被调用
- OPENED(未发送):send()方法还未被调用
- HEADERS_RECEIVED(以获取响应头):send()方法已经被调用,响应头和响应状态已经返回
- LOADING(正在下载响应体):响应体下载中,responseText 中已经获取部分数据
- DONE(请求完成):整个请求过程已完毕
25、什么是防抖和节流?有什么区别?如何实现?
节流:n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
防抖:n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时
防抖应用场景:
- 搜索框搜索输入,只需要用户最后一次输入完,再发送请求
- 手机号、邮箱验证输入检测
- 窗口大小 resize,只需窗口跳转完成后,计算窗口大小。繁殖重复渲染
节流应用场景:
- 滚动加载,加载更多或滚到底部监听
- 搜索框,搜索联想功能
代码实现
节流
const throttled = (fn, delay = 1000) => {
let timer = null
let starttime = Date.now() // 上次执行时间点
return function () {
let curTime = Date.now() // 当前时间
let remaining = delay - (curTime - starttime) // 从上次执行时间点到当前时间点 remaining 时间差
let context = this
let args = arguments
clearTimeout(timer) // 清除定时器
if (remaining <= 0) {
// 剩余时间小于等于0,表示上次执行已经超过delay的时间,直接执行
fn.apply(context, args)
starttime = Date.now()
} else {
// 剩余时间大于0,表示上次执行时间点距离当前时间点还有 remaining 时间,在 remaining 时间后执行
timer = setTimeout(function () {
fn.apply(context, args)
}, remaining)
}
}
}
防抖
const debounce = (fn, wait, immediate) => {
let timer = null
return function () {
let context = this
let args = arguments
// 清除定时器,使用clearTimeout清除定时器,timer变量的值不会改变,仍然保持其原始的定时器标识符(ID)或对象引用,
// 即!!timer == true,!timer === false
if (timer) clearTimeout(timer)
if (immediate) {
let callNow = !timer // 判断首次是否执行
timer = setTimeout(function () {
// 延迟执行函数
timer = null
}, wait)
// 如果不加判断,那么在首次执行时,会执行两次
if (callNow) fn.apply(context, args)
} else {
timer = setTimeout(function () {
fn.apply(context, args)
}, wait)
}
}
}
26、如何判断一个元素是否在可视区域中?
实现方式,有三种常用的方法:
- offsetTop、scrollTop
- getBoundingClientRect()
- Intersection Observer API(现代浏览器)
offsetTop、scrollTop
const isInViewPortOfOne = el => {
// 兼容所有浏览器写法
const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
const offsetTop = el.offsetTop
const scrollTop = document.documentElement.scrollTop
const top = offsetTop - scrollTop
return top <= viewPortHeight
}
图解:
getBoundingClientRect
function isElementInViewport(el) {
const rect = el.getBoundingClientRect()
return rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth
}
使用 Intersection Observer API(现代浏览器)
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('元素进入可视区域')
} else {
console.log('元素离开可视区域')
}
})
})
const targetElement = document.querySelector('.target')
observer.observe(targetElement) // 监听目标元素
27、什么是单点登录?如何实现?
单点登录(SSO):是一种身份验证机制,允许用户通过一次登录即可访问多个相互信任的系统或应用,无需重复输入凭证。例如,使用 Google 账号登录 Gmail、YouTube、Drive 等服务的场景即为 SSO 的典型应用。
单点登录的核心价值:
- 用户体验提升:用户只需要登录一次,即可无缝访问多个系统
- 安全管理统一:集中管理用户权限和身份验证逻辑,减低密码泄露风险
- 减少维护成本:各系统无需独立维护用户体系
SSO 的核心:通过集中认证和令牌传递实现多系统免登
方案选择:
- 同域/子域:Cookie 共享,如
a.example.com
和b.example.com
中,用户登录主系统后,主系统在顶级域名.example.com
下设置 Cookie,如此,其他子系统共享该 Cookie,验证用户身份 - 跨域:OAuth2.0、SAML 或 JWT
28、如何实现上拉加载、下拉刷新
上拉加载:通过滚动位置判断或 intersection Observer 触发数据加载
下拉刷新:结合触摸事件和 css 动画实现手势交互
核心优化:防抖/节流、异步加载、错误处理和用户体验细节(如加载提示)
现成插件(库):better-scroll、react-pull-to-refresh
29、说说你对正则表达式的理解?应用场景?
理解:正则表达式是一种强大的文本处理工具,用于描述、匹配和操作字符串中的字符组合,在 JavaScript 中正则表达式也是对象
构建正则表达式的方式:
- 字面量创建,其由包含在斜杠之间的模式组成
const reg = /\d+/g
- 调用 RegExp 对象的构造函数,遇到特殊字符
\
需要使用\\
进行转义
const reg = new RegExp('\\d+', 'g')
应用场景:
- 数据验证
- 数据提取
- 文本替换
- 复杂文本处理
31、web 常见的攻击方式有哪些?如何防御?
常见的 web 攻击方式:
- XSS 跨站脚本攻击:攻击者将恶意代码植入到提供给其他用户使用的页面中,主要是为了盗取存储在客户端的 cookie 或者其他用于识别客户端身份的敏感信息
- CSRF 跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求
- SQL 注入攻击:通过将恶意的 sql 查询或添加语句插入到应用的输入参数中,再在后台 sql 服务器上解析执行进行的攻击
预防方式:
- XSS 跨站脚本攻击:过滤用户输入的恶劣代码后,提交给后端;后端写入数据库之前,对输入进行过滤,然后把内容给前端
- CSRF 跨站请求伪造:
- 防止不明外域的访问:同源检测、Samesite Cookie
- 提交时要求附加本域才能获取到信息:CSRF Token、双重 Cookie 验证
- SQL 注入:
- 严格检查输入变量的类型和格式
- 过滤和转义特殊字符
- 对访问数据库的 web 应用程序采用 web 应用防火墙
32、说说 JavaScript 中内存泄漏的几种情况
内存泄漏:由于疏忽或错误造成内存即不能使用也不能释放
内存泄露的情况:
- 意外的全局变量
- 未清理的定时器或回调
- 闭包中的不当引用
- 没有清理对 DOM 元素的引用:缓存了 DOM 元素的引用,即使元素被删除,引用仍存在
- 未移除的事件监听:为 DOM 元素添加事件监听后,未在元素移除时取消监听
- 第三方库未正确销毁:某些库需要手动调用销毁方法释放资源
垃圾回收机制:
- 引用计数:引擎中有一张”引用表“,保存了内存中所有的资源(通常是各种值)的引用次数,如果一个值得引用次数为 0,就表示这个值不再被用到,因此可以将这块内存释放
- 标记清除:当变量进入执行环境时,就标记该变量为”进入环境“,进入环境的变量所占用的内存就不能释放,当变量离开环境时,则将其标记为”离开环境“
33、JavaScript 如何实现继承?
- 原型链继承
- 构造函数继承(借助 call)
- 组合继承
- 原型式继承
- 寄生式继承
- 寄生组合式继承
34、说说 JavaScript 数字精度丢失的问题,如何解决?
存储方式:计算机存储双精度浮点数需要先把十进制数转换为二进制的科学计数法的形式,然后计算机以自己的规则存储二进制的科学计数法
问题根源:因为存储时有位数限制(64 位),并且某些十进制的浮点数在转换为二进制数时会出现无限循环,会造成二进制的舍入操作(0 舍 1 入),当在转换为十进制时就造成了计算误差
解决方案:
- 整数问题:使用
BigInt
或字符串传递 - 小数问题:转换为整数运算或使用
decimal.js
- 展示问题:四舍五入或精度修正
- 协同方案:前后端约定数据格式,避免二进制浮点数缺陷
35、举例说明你对尾递归的理解,有哪些应用场景
递归:在函数的定义中使用函数本身的方法,即在一个函数内部调用自身,这个函数就是递归函数
递归的核心思想:把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解
尾递归:在尾部直接调用自身的递归函数
应用场景:
- 数组求和
- 使用尾递归优化求斐波那契数列
- 数组扁平化
- 数组对象格式化
const fun = (num, total) => {
if (num > 100) {
return total
} else {
// 每一次返回的就是一个新函数,不带上一个函数的参数,也就不需要存储上一个函数
return fun(num + 1, total + num)
}
}
console.log(fun(1, 0))