JavaScript考题
1. js延迟加载的方式有哪些?
延迟(异步)加载:async、defer,例如:
<script async src='script.js'></script>
<script defer src='script.js'></script>
- defer :
defer脚本的下载是异步的,不会阻塞HTML解析。它的执行会被推迟,严格等到整个HTML文档解析完毕(DOMContentLoaded事件之前),并且按照在文档中出现的顺序依次执行。 - async :
async脚本的下载是异步的,不会阻塞HTML解析。但一旦下载完成,它会立即暂停HTML解析,执行该脚本,执行完后恢复解析。多个async脚本之间没有固定的执行顺序,谁先下载完谁先执行。
2. js数据类型有哪些?
基本类型:string、number、boolean、undefined、null、symbol、bigint
引用类型:object
NaN是一个数值类型,但是不是一个具体的数字。
关于数据类型的考题
考题一:
console.log( true + 1 ); //2
console.log( 'name'+true ); //nameture
console.log( undefined + 1 );//NaN
console.log( typeof undefined );//undefined
考题二:
console.log( typeof(NaN) ); //number
console.log( typeof(null) );//object
3. null和undefined的区别
一、奇怪点
有点奇怪的是,JavaScript语言居然有两个表示"无"的值:undefined和null。这是为什么?
二、历史原因
1995年JavaScript诞生时,最初像Java一样,只设置了null作为表示"无"的值。根据C语言的传统,null被设计成可以自动转为0。
但是,JavaScript的设计者,觉得这样做还不够,主要有以下两个原因:
1. null像在Java里一样,被当成一个对象。但是,JavaScript的数据类型分成原始类型(primitive)和合成类型(complex)两大类,作者觉得表示"无"的值最好不是对象。
2. JavaScript的最初版本没有包括错误处理机制,发生数据类型不匹配时,往往是自动转换类型或者默默地失败。作者觉得,如果null自动转为0,很不容易发现错误。
因为 null被设计成一个会自动转换的“对象值”,既不适合表示原始类型的空,其自动转换的特性也容易隐藏错误。所以,引入了另一个全新的原始值——undefined,来担任“未定义”或“系统缺省值”这个角色,从而在语义和安全性上,与表示“空值”的 null区分开
这里注意:先有null后有undefined,出来undefined是为了填补之前的坑。
三、具体区别
JavaScript的最初版本是这样区分的:null是一个表示"无"的对象(空对象指针),转为数值时为0;undefined是一个表示"无"的原始值,转为数值时为NaN。
四、总结:
1. 作者在设计js的都是先设计的null(为什么设计了null:最初设计js的时候借鉴了java的语言)
2. null会被隐式转换成0,很不容易发现错误。
3. 先有null后有undefined,出来undefined是为了填补之前的坑。
具体区别:
JavaScript的最初版本是这样区分的:
1. null是一个表示"无"的对象(空对象指针),转为数值时为0;
2. undefined是一个表示"无"的原始值,转为数值时为NaN。
4. typeof和instanceof的区别
总结:
typeof 用于判断数据类型,返回一个字符串,适用于原始数据类型。
instanceof 用于判断对象是否是某个类的实例,适用于引用类型的判断。
关于instanceof一道考题:如何实现一个自己的instanceof
<script type="text/javascript">
const instanceofs = (target,obj)=>{
let p = target;
while( p ){
if( p == obj.prototype ){
return true;
}
p = p.__proto__;
}
return false;
}
console.log( instanceofs( [1,2,3] , Object ) )
</script>
5. JS判断变量是不是数组,你能写出哪些方法?
方式一:Array.isArray()
const arr = [1, 2, 3];
console.log(Array.isArray(arr));
方式二:isPrototypeOf():用于判断当前对象是否为另外一个对象的原型,如果是就返回 true,否则就返回 false。
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 )
方式四:Object.prototype.toString.call()
const arr = [1, 2, 3];
console.log(Object.prototype.toString.call(arr) === '[object Array]');
方式五:instanceof
const arr = [1, 2, 3];
console.log(arr instanceof Array);
6. ==和===区别
-
基本情况
== : 比较的是值 当使用 == 运算符比较两个值时,如果两个值的数据类型不同,JavaScript 会尝试进行隐式类型转换,将其中一个值转换为另一个值的数据类型,然后再进行比较。 通过valueOf转换(valueOf() 方法通常由 JavaScript 在后台自动调用,并不显式地出现在代码中。) === : 除了比较值,还比较类型 -
总结区别
1. 相等运算符(==):判断两个值是否相等,但不考虑数据类型的差异。如果两个值的数据类型不同,== 会尝试进行类型转换,然后再比较值是否相等。 2. 严格相等运算符(===):也用于比较两个值是否相等,但会严格比较值和数据类型。如果两个值的数据类型不同,=== 不会进行类型转换,而是直接返回 false。 -
另外提示
相等运算符(==)和严格相等运算符(===)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0,所以ES6新增了Object.is()方法。
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
7. js运行机制考题
一、js运行机制了解吗?
JavaScript 的运行机制是指 JavaScript 引擎如何执行代码的过程,其中包括了事件循环(Event Loop)和任务队列(Task Queue)等概念。下面是 JavaScript 的运行机制的简要介绍:
-
同步和异步任务:JavaScript 是单线程的,意味着一次只能执行一个任务。任务分为同步和异步两种:
同步任务会按照代码顺序依次执行,中间不会被其他任务中断。 异步任务会在后台执行,不会阻塞后续代码的执行。
1.1 异步任务在后台执行,是否与一次执行一个任务的单线程相冲突?
-
事件循环(Event Loop):
JavaScript 引擎通过事件循环来管理任务的执行顺序。 事件循环的目的是确保任务按照正确的顺序执行,并且处理异步任务的执行顺序。JS分为同步任务和异步任务 同步任务都在主线程上执行,形成一个执行栈 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。 -
任务队列(Task Queue):
任务队列分为宏任务队列(macrotask queue)和微任务队列(microtask queue)两种。 宏任务队列用于存放宏任务,如定时器回调、事件监听回调等。 微任务队列用于存放微任务,如 Promise 的 then() 和 catch() 方法产生的回调等。 -
执行过程:
当 JavaScript 代码执行时,同步任务会立即执行,异步任务会被放入任务队列中。 当前任务执行完毕后,事件循环会从任务队列中取出任务执行,执行完毕后检查微任务队列是否有任务,有则立即执行微任务。 循环上述过程,直到所有任务执行完毕。
- 主线程运行时会产生执行栈, 栈中的代码调用某些api时,它们会在事件队列中添加各种事件(当满足触发条件后,如ajax请求完毕)
- 而栈中的代码执行完毕,就会读取事件队列中的事件,去执行那些回调
- 如此循环
- 注意,总是要等待栈中的代码执行完毕后才会去读取事件队列中的事件
二、js微任务和宏任务有哪些?
常见的微任务包括:
Promise 的 then() 和 catch() 方法产生的回调。
async/await 中的 await 关键字后面的代码。
process.nextTick
Mutation Oberserver(DOM 变更观察 )
常见的宏任务包括:
定时器(setTimeout、setInterval)的回调。
事件监听(如点击事件、键盘事件)的回调。
I/O 操作(如文件读写、网络请求)的回调。
渲染事件(requestAnimationFrame)的回调。
同步任务:
函数调用,
变量赋值,
算数运算
三、js微任务和宏任务的执行顺序考题
考题1:
const first = () =>
new Promise((resolve, reject) => {
console.log(1); // 属于同步任务
const p = new Promise((resolve, reject) => {
console.log(2); // 属于同步任务
resolve(5);
});
resolve(6);
p.then((arg) => {
console.log(arg); // 属于异步任务中的微任务
});
});
first().then((arg) => {
console.log(arg); // 属于异步任务中的微任务
});
setTimeout(() => { // 属于异步任务中的宏任务
console.log(3);
}, 0);
console.log(7); // 属于同步任务
// 同步任务 1 2 7
// 宏任务 3
// 微任务 5 6
// 注意: 函数调用本身是同步的,函数内部的代码(比如变量赋值、console.log、普通函数调用等)默认也是同步执行的
考题2:
console.log(1);
setTimeout(()=>{ // 宏任务里面套微任务微任务会在该宏任务后面执行
console.log(2);
Promise.resolve().then(()=>{
console.log(3);
});
});
new Promise((resolve,reject)=>{ // 这里是同步任务
console.log(4);
resolve();
}).then((data)=>{
console.log(5)
})
setTimeout(()=>{
console.log(6)
})
console.log(7);
// 先执行同步任务 147 异步微任务 5 异步宏任务 2 (嵌套宏任务 3) 6
// 注意: new Promise(()=>{ ... }) 这里是同步任务
考题3
console.log(1);
setTimeout(()=>{ // 宏任务里面套微任务
console.log(2);
Promise.resolve().then(()=>{
console.log(3);
});
});
new Promise((resolve,reject)=>{
console.log(4);
resolve(5);
}).then((data)=>{
console.log(data)
Promise.resolve().then(()=>{
console.log(6);
}).then(()=>{
// new Promise((resolve,reject)=>{
// console.log(7);
// resolve()
setTimeout(()=>{
console.log(8)
})
})
})
setTimeout(()=>{
console.log(9)
})
console.log(10);
考题4
new Promise((resolve)=>{
// 这里注意 有 resolve 才会执行 then 方法,而且什么时候调用 resolve,什么时候执行 .then 方法
// 这里同步里面放宏任务,成功的将 setresolve then3 放到宏任务队列
setTimeout(()=>{console.log('setresolve');resolve();})
}).then(()=>{
console.log('then3');
})
setTimeout(()=>{
console.log('set1');
Promise.resolve().then(()=>{
console.log('then2');
setTimeout(()=>{
console.log('set3');
})
})
})
setTimeout(()=>{
console.log('set2');
})
new Promise((resolve)=>{
console.log('pr1');
resolve()
}).then(()=>{
console.log('then1');
})
// 打印 pr1 then1 then3 setresolve set1 then2 set2 set3
考题5
console.log(1)
setTimeout(()=>{
console.log(2);
new Promise((resolve)=>{
console.log(3);
resolve()
}).then(()=>{
console.log(4);
})
})
new Promise((resolve)=>{
console.log(5);
resolve()
}).then(()=>{
console.log(6);
})
setTimeout(() => {
console.log(7);
new Promise((resolve, reject) => {
console.log(8);
resolve()
}).then(()=>{
console.log(9);
})
});
考题6
const first = () =>
new Promise((resolve, reject) => {
console.log(1);
const p = new Promise((resolve, reject) => {
console.log(2);
resolve(5);
});
resolve(6);
p.then((arg) => {
console.log(arg);
});
});
first().then((arg) => {
console.log(arg);
});
setTimeout(() => {
console.log(3);
}, 0);
console.log(7);
考题7
console.log(1);
setTimeout(()=>{
console.log(2);
Promise.resolve().then(()=>{
console.log(3);
});
});
new Promise((resolve,reject)=>{
console.log(4);
resolve();
}).then((data)=>{
console.log(5)
})
setTimeout(()=>{
console.log(6)
})
console.log(7);
考题8
new Promise((resolve,reject)=>{
console.log(4);
resolve(5);
}).then((data)=>{
console.log(data)
Promise.resolve().then(()=>{
console.log(6);
}).then(()=>{
// new Promise((resolve,reject)=>{
// console.log(7);
// resolve()
setTimeout(()=>{
console.log(8)
})
})
})
setTimeout(()=>{
console.log(9)
})
console.log(10);
考题9
new Promise((resolve) => {
setTimeout(() => { console.log('setresolve'); resolve(); }) // 这里注意 有 resolve 才会执行 then 方法,而且什么时候调用 resolve,什么时候执行 .then 方法
}).then(() => {
console.log('then3');
})
setTimeout(() => {
console.log('set1');
Promise.resolve().then(() => {
console.log('then2');
setTimeout(() => {
console.log('set3');
})
})
})
setTimeout(() => {
console.log('set2');
})
new Promise((resolve) => {
console.log('pr1');
resolve()
}).then(() => {
console.log('then1');
})
考题10
setTimeout(function () {
console.log('定时器开始啦')
});
new Promise(function (resolve) {
console.log('马上执行for循环啦');
for (var i = 0; i < 10000; i++) {
i == 99 && resolve(i);
}
}).then(function (data) {
console.log('执行then函数啦')
console.log(data);
});
console.log('代码执行结束');
setTimeout(() => {
console.log(1)
}, 1)
setTimeout(() => {
console.log('xxx')
})
setTimeout(() => {
console.log(0)
}, 0)
8. js数组考题
8.1 什么是类数组?怎么转换成真正的数组?
类数组是一种类似数组的对象,但它不是真正的JavaScript数组。 类数组的主要特点是:
- 它具有一个length属性,这个属性表示了类数组中元素的数量。
- 类数组可以包含从0开始的自然递增整数作为键名。
- 类数组可以像真正的数组一样进行遍历,例如使用for循环。
- 但是,类数组不能调用一些真正的数组方法,如push、pop、forEach等,因为它们没有这些方法,要使用就要先转为数组,下面是转换方法
Array.from(new Set([1,2,1,2,3]))
Array.from( arguments )
8.2 js数组去重
-
new Set
const array = [1, 2, 2, 3, 4, 4, 5]; const uniqueArray = [...new Set(array)]; console.log(uniqueArray); // [1, 2, 3, 4, 5] -
使用 Array.filter() 和 indexOf()
const array = [1, 2, 2, 3, 4, 4, 5]; const uniqueArray = array.filter((value, index, self) => self.indexOf(value) === index); console.log(uniqueArray); // [1, 2, 3, 4, 5] -
使用 Array.reduce() 和 includes()
const array = [1, 2, 2, 3, 4, 4, 5]; const uniqueArray = array.reduce((acc, currentValue) => acc.includes(currentValue) ? acc : [...acc, currentValue], []); console.log(uniqueArray); // [1, 2, 3, 4, 5] -
双重循环
const array = [1, 2, 2, 3, 4, 4, 5]; const uniqueArray = []; for (let i = 0; i < array.length; i++) { if (uniqueArray.indexOf(array[i]) === -1) { uniqueArray.push(array[i]); } } console.log(uniqueArray); // [1, 2, 3, 4, 5]
8.3 数组进行扁平化,并且去重
const nestedArray = [1, 2, [3, 4], [5, 6, [7, 8]], 9, 9, 2];
// 扁平化
const flattenedArray = nestedArray.flat(Infinity);
// 去重
const uniqueArray = [...new Set(flattenedArray)];
console.log(uniqueArray); // Output: [1, 2, 3, 4, 5, 6, 7, 8, 9]
8.4 在数组中,找出重复次数最多的元素
function findMostFrequentElement(arr) {
let countMap = {};
let maxCount = 0;
let mostFrequentElement = null;
arr.forEach((element) => {
countMap[element] = (countMap[element] || 0) + 1;
if (countMap[element] > maxCount) {
maxCount = countMap[element];
mostFrequentElement = element;
}
});
return mostFrequentElement;
}
// 示例数组
const array = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4];
const mostFrequentElement = findMostFrequentElement(array);
console.log(`重复次数最多的元素是: ${mostFrequentElement}`);
8.5 js数组常用方法
push(): 向数组末尾添加一个或多个元素,并返回新的长度。
pop(): 删除数组末尾的元素,并返回被删除的元素。
shift(): 删除数组头部的元素,并返回被删除的元素。
unshift(): 向数组头部添加一个或多个元素,并返回新的长度。
splice(): 从指定位置开始删除或插入元素,可以改变原数组。
sort(): 排序
reverse(): 反转排序
concat(): 连接两个或多个数组,返回一个新数组。
slice(): 返回数组的指定部分,不会改变原数组。
forEach(): 遍历数组并对每个元素执行指定操作。
map(): 遍历数组并生成一个新数组,新数组的元素是对原数组中每个元素调用指定函数的结果。
filter(): 遍历数组并返回符合条件的元素组成的新数组。
find(): 返回数组中第一个满足条件的元素。
findIndex(): 返回数组中第一个满足条件的元素的索引。
reduce(): 对数组中的每个元素执行一个累加器函数,将其结果汇总为单个返回值。
some(): 检测数组中是否至少有一个元素满足指定条件。
every(): 检测数组中的所有元素是否满足指定条件。
8.6 数组转字符串的方法
- join()
const arr = [1, 2, 3, 4, 5];
const str = arr.join(); // "1,2,3,4,5"
const strWithDash = arr.join('-'); // "1-2-3-4-5"
2. toString()
const arr = [1, 2, 3, 4, 5];
const str = arr.toString(); // "1,2,3,4,5"
3. JSON.stringify()
const arr = [1, 2, 3, 4, 5];
const str = JSON.stringify(arr); // "[1,2,3,4,5]"
8.7 js数组长度为10,删除比如第五个怎么办?
- 方式一:
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 删除第五个元素(索引为4)
arr.splice(4, 1);
console.log(arr); // [1, 2, 3, 4, 6, 7, 8, 9, 10]
2. 方式二:
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let newArr = arr.slice(0, 4).concat(arr.slice(5));
console.log(arr); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log(newArr); // [1, 2, 3, 4, 6, 7, 8, 9, 10]
8.8 slice是干嘛的,splice是否会改变原数组
- slice() 方法:
- slice() 方法返回一个新数组,其中包含从开始索引到结束索引(不包括结束索引)的元素,原始数组不会被修改。
- slice() 方法接受两个参数,第一个参数是开始索引(包含),第二个参数是结束索引(不包含)。
- 如果省略第二个参数,则 slice() 方法会一直提取到数组末尾。
- slice() 方法不改变原数组,而是返回一个新的数组。
- 索引可以传递一个负值,如果传递一个负值,则从后往前计算;-1 倒数第一个,-2 倒数第二个
const arr = [1, 2, 3, 4, 5];
const slicedArr = arr.slice(1, 4); // 返回 [2, 3, 4]
console.log(arr); // [1, 2, 3, 4, 5],原数组不变
2. splice() 方法:
- splice() 方法用于插入、删除或替换数组的元素,并可以改变原数组。
- splice() 方法接受多个参数,第一个参数是起始索引,第二个参数是要删除的元素个数(如果为0,则不删除任何元素),从第三个参数开始是要插入的元素。
- splice() 方法返回一个包含被删除元素的数组。
- splice() 方法会直接修改原数组。
let arr = [1, 2, 3, 4, 5];
let removed = arr.splice(1, 2); // 删除索引位置1和2的元素
console.log(arr); // [1, 4, 5],原数组被修改
console.log(removed); // [2, 3],被删除的元素
总结:
slice() 方法用于提取原数组的一部分,不改变原数组。
splice() 方法用于对原数组进行插入、删除或替换操作,会改变原数组。
8.9 map和forEach的区别
-
返回值: map 方法会返回一个新的数组,该数组包含经过回调函数处理后的每个元素。 forEach 方法不返回任何值(没有返回值),它仅用于遍历数组中的每个元素。
-
对原数组的影响:
map 不会改变原数组,它会返回一个新的数组。 forEach 不会返回新数组,但它会对原数组进行操作,修改原数组。
- 使用场景:
map 通常用于对数组中的每个元素进行转换,并返回一个新的数组。 forEach 用于遍历数组中的每个元素,但通常不用于创建新数组,而是用于执行对数组中元素的操作。
总结:
使用 map 时,如果需要对数组中的每个元素进行转换并获得一个新的数组,可以使用 map。
使用 forEach 时,如果只需要遍历数组中的元素并执行操作,不需要返回新数组,可以使用 forEach。
8.10 find和filter的区别
- filter方法:
- filter方法用于过滤数组中满足指定条件的所有元素,并返回一个新的数组,该数组包含所有满足条件的元素。
- filter方法不会改变原始数组,而是返回一个新的数组,其中包含符合条件的元素。
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(num => num % 2 === 0); // 返回 [2, 4]
2. find方法:
- find方法用于查找数组中满足指定条件的第一个元素,并返回该元素,如果找不到符合条件的元素,则返回undefined。
- find方法通常用于查找数组中满足特定条件的单个元素。
const numbers = [1, 2, 3, 4, 5];
const firstEvenNumber = numbers.find(num => num % 2 === 0); // 返回 2
总结:
filter方法用于过滤数组中的元素,返回一个新数组包含所有满足条件的元素,如果不满足条件返回空数组
find方法用于查找数组中第一个满足条件的元素并返回,如果找不到则返回undefined。
8.11 some和every的区别
- some 方法:
- some 方法用于检查数组中是否至少有一个元素满足指定条件,如果至少有一个元素满足条件,则返回 true,否则返回 false。
- some 方法会遍历数组中的每个元素,并对每个元素应用指定的条件,只要有一个元素满足条件,即返回 true。
const numbers = [1, 2, 3, 4, 5];
const hasEvenNumber = numbers.some(num => num % 2 === 0); // 返回 true,因为数组中有偶数
2. every 方法:
- every 方法用于检查数组中的所有元素是否都满足指定条件,如果所有元素都满足条件,则返回 true,否则返回 false。
- every 方法会遍历数组中的每个元素,并对每个元素应用指定的条件,只有当所有元素都满足条件时才返回 true。
const numbers = [2, 4, 6, 8, 10];
const allEvenNumbers = numbers.every(num => num % 2 === 0); // 返回 true,因为数组中所有元素都是偶数
总结:
some 方法用于检查数组中是否至少有一个元素满足条件,而 every 方法用于检查数组中的所有元素是否都满足条件。根据具体的需求,可以选择使用其中之一来进行条件检查。
9. js字符串考题
9.1 字符串转换为数组的方法
可以使用 split() 方法将字符串转换为数组。 split()方法将字符串分割成子串,并返回一个包含分割后的子串的数组。
const str = "apple,banana,orange";
const arr = str.split(",");
console.log(arr); // ["apple", "banana", "orange"]
9.2 给字符串新增方法实现功能:给字符串对象定义一个addPrefix函数,当传入一个字符串str时,它会返回新的带有指定前缀的字符串,例如:
console.log( 'world'.addPrefix('hello') )
//控制台会输出helloworld
答案:
String.prototype.addPrefix = function( value ){
return value + this
}
console.log( 'world'.addPrefix('hello') )
9.3 找出字符串出现最多次数的字符以及次数
function findMostFrequentChar(str) {
// 使用对象来存储字符及其出现的次数
let charMap = {};
let maxChar = '';
let maxCount = 0;
// 遍历字符串,统计每个字符出现的次数
for (let char of str) {
charMap[char] = (charMap[char] || 0) + 1;
if (charMap[char] > maxCount) {
maxChar = char;
maxCount = charMap[char];
}
}
return { char: maxChar, count: maxCount };
}
// 输入字符串
let inputString = "Your input string here";
let result = findMostFrequentChar(inputString);
console.log(`The most common character is '${result.char}' with a count of ${result.count}.`);
10. 闭包
什么是闭包:闭包(Closure)是指有权访问另一个函数作用域中的变量的函数。当一个函数能够访问并记住在其外部函数作用域中定义的变量,即使外部函数已经返回,这些变量也依然存在,这就形成了一个闭包。
闭包的优点:
记忆功能:闭包可以保存状态,如计数器、缓存等。
延长变量生命周期:闭包使得变量的生命周期与函数的执行时间相关,而不是仅在声明时。
实现高阶函数:闭包常用于创建柯里化函数或函数工厂。
闭包的缺点:
内存消耗:由于闭包会持有外部变量,可能会导致内存泄漏,尤其是在循环中创建大量闭包。
性能影响:闭包会增加代码复杂性,可能导致额外的开销。
难以调试:由于闭包内部的变量不易察觉,可能会影响代码的可读性和维护性。
闭包的使用场景:
模块化开发:通过闭包创建私有变量和方法,实现模块化的代码结构。
函数式编程:高阶函数、柯里化、延迟执行等场景。
缓存和回调:例如,实现异步操作(如AJAX请求)的回调函数,可以使用闭包保存状态。
事件处理程序:在事件监听器中,可以使用闭包存储与特定事件相关的状态。
计数器和迭代器:闭包可以用来维护循环中的状态,实现迭代器或计数器。
防抖(Debounce): 防抖确保函数在用户停止触发事件后的一段时间内只调用一次。如果用户在设定的时间间隔内再次触发,那么计时器会被重置,直到用户停止触发才会再次调用。
function debounce(func, delay) {
let timeoutId;
return function() {
const context = this;
const args = arguments;
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(context, args);
}, delay);
};
}
// 使用防抖处理输入框的自动完成
document.getElementById('searchInput').addEventListener('input', debounce(handleSearch, 300));
11. 防抖和节流
前端防抖(Debounce)和节流(Throttle)是两种常用的性能优化技术,用于控制事件触发频率,避免过多的事件触发导致性能问题。
- 防抖(Debounce):在事件触发后等待一定时间再执行处理函数,如果在等待时间内再次触发了事件,则重新计时。防抖常用于输入框输入事件、滚动事件等频繁触发的事件。
function debounce(func, delay) {
let timeoutId;
return function() {
const context = this;
const args = arguments;
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(context, args);
}, delay);
};
}
// 使用示例
const debouncedFunction = debounce(() => {
console.log('Debounced function is called');
}, 300);
// 调用防抖函数
debouncedFunction();
2. 节流(Throttle):在一定时间内只允许事件触发一次,如果在这段时间内多次触发了事件,只有第一次触发的事件会被处理。节流常用于滚动触发加载更多数据、窗口大小改变事件等。
function throttle(func, delay) {
let shouldExecute = true;
return function() {
if (!shouldExecute) return;
shouldExecute = false;
func.apply(this, arguments);
setTimeout(() => {
shouldExecute = true;
}, delay);
};
}
// 使用示例
const throttledFunction = throttle(() => {
console.log('Throttled function is called');
}, 300);
// 调用节流函数
throttledFunction();
12. 原型和原型链了解吗?
原型(prototype):
- 在 JavaScript 中,每个对象都有一个原型(prototype)。对象可以继承另一个对象的属性和方法,这是通过原型实现的。每个对象都有一个指向它的原型的内部链接。当你试图访问一个对象的属性时,如果这个对象本身没有这个属性,JavaScript 就会去查找原型链上的对象。
原型链(prototype chain):
- 原型链是一种机制,用于在 JavaScript 中实现对象之间的继承。当你访问一个对象的属性或方法时,如果这个对象本身没有这个属性或方法,JavaScript 就会沿着原型链向上查找。原型链是一种链表结构,原型链顶端是null。
13. js实现继承的方式
-
原型链继承:
缺点:原型中包含的引用值会在所有实例间共享 这是因为 在使用原型实现继承时,原型实际上变成了另一个类型的实例
function Parent(){
this.age = 20;
}
function Child(){
this.name = '张三'
}
Child.prototype = new Parent();
let o2 = new Child();
console.log( o2,o2.name,o2.age );
2. 借用构造函数继承:
缺点:只能继承父类的实例属性和方法,不能访问父类原型上定义的方法
function Parent(){
this.age = 22;
}
function Child(){
this.name = '张三'
Parent.call(this);
}
let o3 = new Child();
console.log( o3,o3.name,o3.age );
3. 组合继承(原型链继承 + 借用构造函数继承):
缺点:解决原型链继承和借用构造函数继承的缺点,但是造成了多构造一次的性能开销
function Parent(){
this.age = 100; // 这里可以被继承
}
// 在父类原型上添加方法的方式无法被子类继承
Parent.prototype.sayHello = function() {
console.log('Hello from Parent prototype, ');
};
function Child(){
Parent.call(this);
this.name = '张三'
}
Child.prototype = new Parent();
let o4 = new Child();
console.log( o4,o4.name,o4.age );
4. ES6 类继承:
class Parent{
constructor(){
this.age = 18;
}
}
class Child extends Parent{
constructor(){
super();
this.name = '张三';
}
}
let o1 = new Child();
console.log( o1,o1.name,o1.age );
14. new操作符做了什么?
- 创建了一个空的对象
- 将空对象的原型,指向于构造函数的原型
- 将空对象作为构造函数的上下文(改变this指向)
- 对构造函数有返回值的处理判断
function Fun( age,name ){
this.age = age;
this.name = name;
}
function create( fn , ...args ){
//1. 创建了一个空的对象
var obj = {}; // var obj = Object.create({})
//2. 将空对象的原型,指向于构造函数的原型 === obj._proto_ = fn.prototype
Object.setPrototypeOf(obj,fn.prototype);
//3. 将空对象作为构造函数的上下文(改变this指向)
var result = fn.apply(obj,args);
//4. 对构造函数有返回值的处理判断
return result instanceof Object ? result : obj;
}
console.log( create(Fun,18,'张三') )
15. js改变this指向的方式有哪些?
在 JavaScript 中,有几种常见的方式可以改变函数中 this 的指向:
- 使用 call() 方法:
call() 方法允许你显式指定函数执行时的 this 值,同时可以传入参数列表。
例如:func.call(thisArg, arg1, arg2, ...)
2. 使用 apply() 方法:
apply() 方法和 call() 类似,不同之处在于它接收一个参数数组而不是一系列参数。
例如:func.apply(thisArg, [arg1, arg2, ...])
3. 使用 bind() 方法:
bind() 方法创建一个新的函数,其中 this 的值被设置为传递给 bind() 的第一个参数,而其他参数将作为新函数的参数传递。
例如:var newFunc = func.bind(thisArg);
16. 说一下call、apply、bind区别
在 JavaScript 中,call、apply 和 bind 是用来改变函数执行时的上下文(即 this 的指向)的方法。它们的主要区别在于传入参数的方式和返回值。
call 方法:
- call 方法允许你调用一个函数,同时可以指定函数内部的 this 指向,并且可以传入单个或多个参数。
- 语法:function.call(thisArg, arg1, arg2, ...)
- thisArg 为函数执行时 this 的值,后面的参数是传给函数的参数列表。
- call 方法会立即执行函数。 // 实现原理: // 函数的 this是由调用方式决定的。我们可以让一个对象去“临时调用”这 // 个函数,从而改变 this 指向。将函数作为某个对象(比如传入的 thisArg) // 的一个属性来调用,然后立刻删除该属性 Function.prototype.myCall = function (thisArg, ...args) { thisArg = thisArg || window; const fnSymbol = Symbol('fn'); // 1. 改变 this 指向 thisArg[fnSymbol] = this; // 2. 计算执行结果 const result = thisArgfnSymbol; delete thisArg[fnSymbol]; // 3. 返回结果 return result; };
apply 方法:
- apply 和 call 作用相同,不同之处在于传入参数的方式。
- 语法:function.apply(thisArg, [argsArray])
- thisArg 为函数执行时 this 的值,argsArray 是一个数组,包含传给函数的参数。
- apply 方法会立即执行函数。
// 模拟实现 apply
Function.prototype.myApply = function (thisArg, argsArray) {
thisArg = thisArg || window;
// 1. 改变this指向
const fnSymbol = Symbol('fn');
thisArg[fnSymbol] = this; // 这里的this指的外面Function
// 2. 传递参数
argsArray = argsArray || [];
const result = thisArg[fnSymbol](...argsArray); // 注意这里传入的数组,在接收时需要
// 3. 返回结果
delete thisArg[fnSymbol];
return result;
};
bind 方法:
- bind 方法会创建一个新的函数,其中 this 的值被绑定在 bind 的第一个参数上。
- 语法:function.bind(thisArg, arg1, arg2, ...)
- thisArg 为函数执行时 this 的值,后面的参数是被绑定的参数。
- bind 方法不会立即执行函数,而是返回一个新函数,你可以稍后调用该函数。
// 模拟实现 bind
Function.prototype.myBind = function (thisArg, ...bindArgs) {
const originalFunc = this;
return function (...callArgs) { // 1.返回一个新函数
if (new.target) { // 2.判断是否是 new 调用(new 操作符调用 bind 返回的函数时,this 指向实例,而非 thisArg)
return new originalFunc(...bindArgs, ...callArgs); // 3.如果是构造函数调用,忽略绑定的 thisArg,this 指向新创建的对象
} else {
return originalFunc.apply(thisArg, [...bindArgs, ...callArgs]); // 4.普通调用,使用 apply 绑定 thisArg
}
};
};
总结:
call 和 apply 的作用是立即执行函数,改变函数内部的 this 指向,并传入参数。
bind 方法会创建一个新函数,其中 this 被永久绑定,你可以稍后调用这个函数。
1. 返回的区别:call和apply是立即执行函数,而bind会创建一个新函数(要执行这个函数)
2. 参数的区别:call和bind的参数是单个多个,而apply第二个参数是数组
17. 深拷贝和浅拷贝区别?浅拷贝有哪些?请实现一个深拷贝
-
概念
深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是在编程中经常遇到的概念,它们之间的区别在于拷贝的深度。在 JavaScript 中,通常用于复制对象或数组。
-
浅拷贝(Shallow Copy):
浅拷贝只复制对象或数组的引用,而不是对象或数组本身。这意味着,如果原始对象中有引用类型的数据(如对象、数组),则浅拷贝后的对象中的引用类型数据仍然是原始对象中的引用,修改拷贝后对象中的引用类型数据会影响到原始对象。
常见的浅拷贝方法包括:Object.assign()、展开运算符等等。
// Object.assign()
const original = { a: 1, b: { c: 2 } };
const copy = Object.assign({}, original);
original.b.c = 3;
console.log(copy.b.c); // 输出 3,copy.b 仍然引用原对象的 b
// 展开运算符 (...)
const original = { a: 1, b: { c: 2 } };
const copy = { ...original };
original.b.c = 3;
console.log(copy.b.c); // 输出 3
3. 深拷贝(Deep Copy):
深拷贝会递归地复制所有的引用类型数据,包括对象中的对象、数组中的数组等,从而生成一个全新的对象或数组,对拷贝后的对象的修改不会影响到原始对象。
常见的深拷贝方法包括:JSON.parse() 和 JSON.stringify()、使用 lodash 库的、手动递归复制
-
JSON.parse() 和 JSON.stringify()
- 注意:
1. 无法拷贝函数,undefined,Symbol,
- 当这些值出现在对象中时,如果属性值为undefined、函数或symbol,则在序列化时会被忽略(在对象中则删除该属性)
- 当这些值出现在数组中时,这些值会被转换为null,从而在数组中保留一个位置 2. 无法处理循环引用,对象中引用自身的属性,会报错 3. 会丢失特殊对象类型的信息 Date对象变成字符串 Regex,Set,Map,Error,Blob等会变成{}或者丢失 NaN,Infinity会变成null
* 适用场景:1. 性能一般,不适合拷贝非常大的函数 2. 仅适用于纯JSON安全的数据结构(无函数,无特殊对象,无循环引用) ``` //JSON.parse() 和 JSON.stringify() const original = { a: 1, b: { c: 2, d: [3, 4] } }; const deepCopy = JSON.parse(JSON.stringify(original)); original.b.c = 5; console.log(deepCopy.b.c); // 输出 2,深拷贝成功 ``` - 注意:
1. 无法拷贝函数,undefined,Symbol,
2. 简单的实现一个深拷贝的函数示例:
// 优点:
// 可以自定义处理更多类型(如 Date,RegExp 等)
// 通过WeakMap可以解决循环引用
// 缺点:
// 无法完美处置所有内置对象 Map,Set,Blob,Dom节点等
// 代码复杂,需要处理边界问题
// 对性能要求极高场景不适合
function deepCopy(obj, map = new WeakMap()) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (map.has(obj)) { // 如果这个对象已经被拷贝过了,直接返回之前保存的拷贝
return map.get(obj);
}
let copy = Array.isArray(obj) ? [] : {};
map.set(obj, copy); // 先把当前对象和它的拷贝存入 WeakMap,防止循环引用导致的无限递归
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopy(obj[key], map);
}
}
return copy;
}
// 使用示例
let obj = {
a: 1,
b: {
c: 2
}
};
let objCopy = deepCopy(obj);
objCopy.b.c = 3;
console.log(obj.b.c); // 输出 2,深拷贝后修改拷贝对象不会影响原始对象
3. 插件
// npm i lodash
import cloneDeep from 'lodash.clonedeep';
const original = { a: 1, b: { c: 2 }, date: new Date() };
const copy = cloneDeep(original);
// 支持几乎所有对象类型(对象,数组,Date,RegExp,Map,Set等)
// 正确处理循环引用
// 增加包的体积,可以仅引入 lodash.clonedeep进行单独引入
// npm i rfdc
import rfdc from 'rfdc';
const clone = rfdc();
const original2 = { a: 1, b: { c: 2 } };
const copy2 = clone(original2);
// 性能极高,比 lodash 还快(特别适合普通对象/数组)
// 支持循环引用(可配置)
// 缺点: 不处理特殊对象(如 Date、RegExp、Map、Set 等,默认当作普通对象处理)
4. 浏览器原生方法
// 现代浏览器原生方法
// 从 Chrome 98+、Firefox 94+、Edge 98+、Node.js 17+ 开始,JavaScript
// 原生提供了 structuredClone()方法,用于结构化克隆(比 JSON 更强大)
// 优点:
// 支持更多类型:Date、RegExp、Map、Set、ArrayBuffer、Blob(部分)、循环引用等
// 缺点:
// 不支持函数、DOM 节点、Error 对象等
// 适用场景:
// 现代浏览器或 Node.js 环境 不含函数,但包含 Date、Map、Set 等结构化数据的深拷贝
const original = { a: 1, b: { c: 2 }, date: new Date(), arr: [3, 4] };
const copy = structuredClone(original);
18. js如何比较2个相同的键值是不是完全一样
- 使用 JSON.stringify()
function areObjectsEqual(obj1, obj2) {
return JSON.stringify(obj1) === JSON.stringify(obj2);
}
// 示例
const objA = { a: 1, b: { c: 2 } };
const objB = { a: 1, b: { c: 2 } };
const objC = { a: 1, b: { c: 3 } };
console.log(areObjectsEqual(objA, objB)); // true
console.log(areObjectsEqual(objA, objC)); // false
2. 使用 lodash 库
const _ = require('lodash');
const obj1 = { a: { x: 1 }, b: 2 };
const obj2 = { a: { x: 1 }, b: 2 };
const obj3 = { a: { x: 2 }, b: 2 };
console.log(_.isEqual(obj1, obj2)); // true
console.log(_.isEqual(obj1, obj3)); // false
19. 图片异步上传
<el-upload
:action="' '"
:limit="1"
:on-remove="handleRemove"
:http-request="httpRequest"
:before-upload="beforeUploadVideo"
ref="upload"
>
<div class="flex-align">
<el-button size="mini" type="primary">选择文件</el-button>
<span style="color:#2761FF;margin-left:20px;" @click.stop="downLoadHerf">
<!-- <a :href="excel" download="excel上传模块.xlsx">数据包模板下载</a> -->
数据包模板下载
</span>
</div>
<div slot="tip" class="el-upload__tip">
只可上传.xlsx/.xls文件
</div>
</el-upload>
handleRemove() {
this.excelFile = ''
},
httpRequest(file) {
this.fd.set('file', file.file)
this.isSubmitExcel = false
this.excelFile = file.file
this.$refs.upload && this.$refs.upload.clearValidate('file')
},
beforeUploadVideo(file) {
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
this.isSubmitExcel = true
this.$message.warning('上传文件大小不能超过2MB哦!')
return false
}
},
大文件分片上传:
1.把需要上传的文件按照一定的规则,分割成相同大小的数据块
2.初始化一个分片上传任务,返回本次分片上传的唯一标识
3.按照一定的规则把各个数据块上传
4.发送完成后,服务端会判断数据上传的完整性,如果完整,那么就会把数据库合并成原始文件
断点续传:
服务端返回,从哪里开始 浏览器自己处理