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

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

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

二、js的数据类型有哪些
- 基本类型:string、number、boolean、undefined、null、symbol、bigint
- 引用类型:object
三、null和undefined的区别
- 作者在设计js的时候是先设计null的(因为设计之初借鉴了java语言)
- null会被隐式转换成0,很不容易发现错误
- 先有null后有undefined,出来undefined是为了填补之前的坑
四、==和===有什么不同
- ==:比较的是值
- string == number || boolean || number ... 都会隐式转换
- 通过valueof转换(valueof()方法通常由JS在后台自动调用,并不显式地出现在代码中)
- ===:除了比较值,还比较类型
五、js的微任务和宏任务
- js是单线程的语言
- 流程:同步 -> 事件循环【微任务和宏任务】 -> 微任务 -> 宏任务 -> 微任务 ...
- 微任务:promise.then、process.nextTick ...
- 宏任务:setTimeout、setInterval、setImmediate ...
css
for(var i=0;i<3;i++){
setTimeout(function(){
console.log(i);
},1000*i)
}
// 3 3 3

javascript
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作用域
- 除了函数外,js是没有块级作用域的
- 作用域链:内部可以访问外部的变量,但是外部不能访问内部的变量(内部有优先找内部的)
- 注意声明变量是用var还是没有写(window.)
- js有变量提升的机制(变量悬挂声明)
- 优先级:声明变量 > 声明普通函数 > 参数 > 变量提升
怎么做
- 先看本层作用域有没有此变量(注意变量提升)
- js除了函数之外没有块级作用域
javascript
(function(){
var a = b = 10;
})()
console.log(a); // undefined
console.log(b); // 10
javascript
function c(){
var b = 1;
function a(){
conaole.log(b); // undefined
var b = 2;
console.log(b); // 2
}
a();
console.log(b); // 1
}
javascript
var name = 'a';
(function(){
if(typeof name == 'undefined'){
var name = 'b';
console.log('111' + name); /// 111b
}else{
console.log('222' + name)
}
})()
kotlin
function fun(a){
var a = 10;
function a(){}
console.log(a); // 10
}
fun(100)
}
七、js对象
- 对象是通过new操作符构建出来的,所以对象之间不相等
- 对象注意:引用类型
- 所有的key都是字符串类型
- 对象如何找属性|方法:对象本身 -> 构造函数中 -> 对象原型中 -> 构造函数原型中 -> 对象上一层原型中
arduino
console.log([1,2,3] === [1,2,3]) // false
ini
var obj1 = {
a:'hello'
}
var obj2 = obj1;
obj2.a = 'world';
console.log(obj1); // {a:world''}
(function(){
console.log(a); // undefined
var a = 1;
})()
css
var a = {};
var b = {
key:'a';
}
var c = {
key:'c';
}
a[b] = '123';
a[c] = '456';
console.log(a[b]); // 456
ini
function Fun(){
this.a = '在Fun函数中添加的';
}
Fun.prototype.a = '这是Fun原型添加的';
let obj = new Fun();
obj.a = '对象本身';
obj.__proto__.a = '这是对象原型添加的';
console.log(obj.a);
八、js作用域+this指向+原型链
ini
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
ini
var arr = [1,2,3];
console.log(Array.isArray(arr));
- instanceof
ini
var arr = [1,2,3];
console.log(arr instanceof Array);
- 原型prototype
javascript
var arr = [1,2,3];
console.log(Object.prototype.toString.call(arr).indexOf('Array') > -1);
- isPrototypeOf()
ini
var arr = [1,2,3];
console.log(Array.prototype.isPrototypeOf(arr));
- constructor
less
var arr = [1,2,3];
console.log(arr.constructor.toString().indexOf('Array') > -1);
十、slice和splice是用来干嘛的
- slice 是用来截取的,不改变原数组
arduino
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 是用来插入、删除、替换的,会修改原数组
arduino
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
sql
var arr1 = [1,2,3,1];
function unique(arr){
return [...new Set(arr)]
// return Array.from(new Set(arr1))
}
- for循环
ini
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;
}
十二、多维数组的最大值
lua
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;
}
十三、给字符串新增方法
javascript
String.prototype.addPrefix = function(str){
return str + this;
}
console.log('world'.addPrefix('hello')) // helloworld
十四、字符串出现最多次数的字符及次数
ini
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操作符具体做了什么
javascript
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,'张三'))
十六、闭包
- 闭包是什么:闭包是一个函数加上到创建函数的作用域的连接,闭包关闭了函数的自由变量
- 闭包解决什么问题:
- 内部函数可以访问到外部函数
ini
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
- 闭包的缺点
- 变量会驻留在内存中,造成内存损耗问题(把闭包的函数设置为null)
十七、原型链
- 原型可以解决什么问题
- 对象共享属性和共享方法
- 谁有原型
- 函数有 prototype
- 对象有
__proto__
- 对象查找属性或者方法的顺序
- 现在对象本身属性查找 -> 再到构造函数中查找 -> 再到__proto__中查找 -> 再到prototype中查找 -> 再到原型的原型 -> 最后到null
- 原型链的使用
- 扩展内置对象的功能
- 复用方法与属性
十八、深拷贝和浅拷贝
共同点:复制
- 浅拷贝:只复制引用,而未复制真正的值
ini
var arr1 = ['a','b','c'];
var arr2 = arr1;
- 深拷贝:是复制真正的值,不同引用
ini
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的数组)
- 之前的版本是:插入排序和快排,现在是冒泡
css
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 算法采用深度优先遍历 和同层比较策略,遵循以下原则:
- 树分层比较:只比较同层级节点。
- 不同类型节点:直接替换整个节点及其子节点。
- 相同类型节点:仅更新节点属性和内容,复用子节点。
- 列表节点优化 :通过
key
属性识别相同节点,减少不必要的移动和创建。
二十四、call、apply、bind的区别
- 都是改变this指向和函数的调用,call和apply的功能类似,只是传参的方法不同
- call 方法传的是一个参数列表
- apply 传递的是一个数组
- bind 传参后不会立刻执行,会返回一个改变了this指向的函数,这个函数还是可以传参的,bind()()
二十五、ES6新特性
- 新增块级作用域(let,const)
- 新增定义类的语法糖(class)
- 新增一种基本数据类型(symbol)
- Promise
- 新增模块化(import,export)
- 箭头函数
二十六、TDZ
特性 | let | const | var |
---|---|---|---|
块级作用域 | ✅ | ✅ | ❌ |
变量提升 | ✅(但存在 TDZ) | ✅(但存在 TDZ) | ✅ |
TDZ | ✅ | ✅ | ❌ |
必须初始化 | ❌ | ✅ | ❌ |
允许重复声明 | ❌ | ❌ | ✅ |
可重新赋值 | ✅ | ❌ | ✅ |
二十七、some 和 every
some()
:只要数组中至少有一个元素 满足条件,就返回true
,否则返回false
。every()
:只有数组中所有元素 都满足条件,才返回true
,否则返回false
。
二十八、cookie、localStorage、sessionStorage 的区别
特性 | cookie | localStorage | sessionStorage |
---|---|---|---|
存储容量 | 一般约 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)。phpfetch('https://b.com/api', { method: 'GET', credentials: 'include' // 或 XMLHttpRequest 的 withCredentials: true });
-
后端响应:需返回以下 HTTP 头,允许跨域携带 Cookie:
http
arduinoAccess-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)
这是最基础的算法,现代引擎在此基础上优化:
- 标记阶段:从根对象开始,递归标记所有可达对象。
- 清除阶段:遍历堆内存,回收未被标记的对象。
- 内存整理(可选):移动对象以合并碎片空间。
标记整理(Mark and Compact)
在标记清除的基础上,回收后将存活对象移动到连续内存空间,减少碎片。
分代回收(Generational Garbage Collection)
基于 "多数对象生命周期短暂" 的观察,将对象分为:
- 新生代(Young Generation) :新创建的对象(如局部变量),频繁垃圾回收。
- 老生代(Old Generation) :长期存活的对象(如全局变量),较少垃圾回收。
三十一、一个DOM事件从产生到触发的流程是怎么样的
- 事件捕获:事件产生后,浏览器会从文档根节点(
document
) 开始,向下逐层传播到目标元素的父级。 - 事件委托:当子元素触发事件后,事件冒泡到父元素,父元素的监听器通过
event.target
判断事件来源并处理。 - 事件冒泡:返回根节点
三十二、object和map的区别
特性 | Object | Map |
---|---|---|
键的类型 | 字符串或 Symbol | 任意类型(包括对象、函数等) |
顺序性 | 不保证顺序(ES2015+ 字符串键按插入顺序,但非强制) | 键值对按插入顺序排列,迭代时保持顺序 |
默认键 | 原型链上的属性可能造成冲突(如 toString ) |
无默认键,仅包含显式添加的键值对 |
键值对数量 | 需手动计算(如 Object.keys(obj).length ) |
直接通过 size 属性获取 |
性能 | 大规模数据操作时性能较差 | 频繁增删键值对场景下性能更优 |
迭代支持 | 需手动转换为数组(如 Object.entries() ) |
直接支持 for...of 、forEach 等迭代方法 |
序列化 | 可通过 JSON.stringify() 序列化 |
默认不支持序列化,需手动处理 |