第 1 题:输出以下代码执行结果,大致时间就好
function wait() {
return new Promise(resolve =>
setTimeout(resolve, 10 * 1000)
)
}
async function main() {
console.time();
await wait();
await wait();
await wait();
console.timeEnd();
}
main();
解析:
先说结果,大概30秒多点,30秒是因为每个等待10秒,同步执行。
其实还有一个变种:
function wait() {
return new Promise(resolve =>
setTimeout(resolve, 10 * 1000)
)
}
async function main() {
console.time();
let a = wait();
let b = wait();
let c = wait();
await a;
await b;
await c;
console.timeEnd();
}
main();
这个的运行时间是10s多一点,这是因为:a,b,c的异步请求会按顺序发起。而这个过程是不需要互相依赖等待的。等到wait的时候,其实是比较那个异步耗时最多。就会等待最长。最长的耗时就是整体的耗时。
如果在业务中,两个异步没有依赖关系。应该是后面这种写法。
第 2 题:接口如何防刷
解析:去了京东、腾讯、虾皮等厂子都问到了这个问题。
我们之前也遇到过,基本就是后端工程师限制用户的接口调用次数,面试官希望可以有更多的答案。
1:网关控制流量洪峰,对在一个时间段内出现流量异常,可以拒绝请求
2:源ip
请求个数限制。对请求来源的ip
请求个数做限制
3:http
请求头信息校验;(例如host
,User-Agent
,Referer
)
4:对用户唯一身份uid进行限制和校验。例如基本的长度,组合方式,甚至有效性进行判断。或者uid具有一定的时效性
5:前后端协议采用二进制方式进行交互或者协议采用签名机制
6:人机验证,验证码,短信验证码,滑动图片形式,12306形式
第 3 题:用 setTimeout 实现 setInterval,阐述实现的效果与 setInterval 的差异
function mySetInterval() {
mySetInterval.timer = setTimeout(() => {
arguments[0]()
mySetInterval(...arguments)
}, arguments[1])
}
mySetInterval.clear = function() {
clearTimeout(mySetInterval.timer)
}
mySetInterval(() => {
console.log(11111)
}, 1000)
setTimeout(() => {
// 5s 后清理
mySetInterval.clear()
}, 5000)
第 4题:webpack 打包 vue 速度太慢怎么办?
说说我的处理方式吧,纯经验之谈
1.使用webpack-bundle-analyzer
对项目进行模块分析生成report,查看report后看看哪些模块体积过大,然后针对性优化,比如我项目中引用了常用的UI库element-ui和v-charts等
2.配置webpack的externals
,官方文档的解释:防止将某些import
的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖。
所以,可以将体积大的库分离出来:
// ...
externals: {
'element-ui': 'Element',
'v-charts': 'VCharts'
}
3.然后在main.js
中移除相关库的import
4.在index.html
模板文件中,添加相关库的cdn
引用,如:
<script src="https://unpkg.com/element-ui@2.10.0/lib/index.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/v-charts/lib/index.min.js"></script>
经过以上的处理,再尝试编译打包,会发现速度快了一些。
第 5 题:为什么for循环嵌套顺序会影响性能?
var t1 = new Date().getTime()
for (let i = 0; i < 100; i++) {
for (let j = 0; j < 1000; j++) {
for (let k = 0; k < 10000; k++) {
}
}
}
var t2 = new Date().getTime()
console.log('first time', t2 - t1)
for (let i = 0; i < 10000; i++) {
for (let j = 0; j < 1000; j++) {
for (let k = 0; k < 100; k++) {
}
}
}
var t3 = new Date().getTime()
console.log('two time', t3 - t2)
解析:
两个循环的次数的是一样的,但是 j 与 k 的初始化次数是不一样的
第一个循环的 j 的初始化次数是 100 次,k 的初始化次数是 10w 次
第二个循环的 j 的初始化次数是 1w 次, k 的初始化次数是 1000w 次
所以相同循环次数,外层越大,越影响性能
第六题:统计 1 ~ n 整数中出现 1 的次数
解析:
应该是说1-n的时候中间出现1的次数,比如1到11的时候 1、10、11这种 其实就算4次了,因为11中有两个1
function findOne(n){
let count = 0;
for(let i=0;i<=n;i++){
count+=String(i).split('').filter(item=>item==='1').length
}
return count;
}
第 七 题:Vue 的响应式原理中 Object.defineProperty 有什么缺陷?为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?
解析:
Object.defineProperty无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;
Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy可以劫持整个对象,并返回一个新的对象。
Proxy不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
第 8题:在输入框中如何判断输入的是一个正确的网址。
function isUrl(url) {
try {
new URL(url);
return true;
}catch(err){
return false;
}}
第 9 题:不用加减乘除运算符,求整数的7倍
虽然这样做没什么技术含量,但至少符合题意吧哈哈
function(x){
let arr = Array(x)
let resultArr = [...arr,...arr,...arr,...arr,...arr,...arr,...arr]
return resultArr.length
}
思考
当需要进行避免使用加减乘除的数学运算的时候,通常的方法有:位运算、Eval/Function 传参 hack、进制转换配合字符串转换操作等等,我们参考题目来源中提到的几种方式以及其他大佬们提供的解法来看下这道题。
位运算加法 - 连续7次相加
从上面的表可以看出一种实现简单的多位二进制整数加法的算法如下:
m 和 n 是两个二进制整数,求 m + n:
用与运算求 m 和 n 共同为 “1” 的位:m' = m & n
用异或运算求 m 和 n 其中一个为 “1” 的位:n' = m ^ n
如果 m' 不为 0,那么将 m' 左移一位(进位),记 m = m' << 1,记 n = n',跳回到步骤 1
如果 m' 为 0,那么 n' 就是我们要求的结果。
function bitAdd(m, n){
while(m){
[m, n] = [(m & n) << 1, m ^ n];
}
return n;
}
bitAdd(45, 55); //100
既然实现了加法,我们就可以很容易地将“乘7”这个操作通过循环加法实现出来:
function multiply7(num){
let sum = 0;
for(var i = 0; i < 7; i++){
sum = bitAdd(sum, num);
}
return sum;
}
multiply7(7); //49
这里变量 i 自增的时候用了加号,我们用数组将它代替掉:
function multiply7(num){
let sum = 0;
let counter = new Array(7); // 得到 [empty × 7]
while(counter.length){
sum = bitAdd(sum, num);
counter.shift();
}
return sum;
}
multiply7(7); //49
位运算加法 8+(-1)
既然有加法,我们也可以用 “目标数的8倍减去目标数的1倍” 来实现 “乘7” 的目的。首先贴出位运算加法常见的做法(此段完全引用自原文):
let multiply7 = (num) => bitAdd(num << 3, -num);
multiply7(7); //49
避免使用负号(减号),我们用补码来代替 ( num * -1) 的操作:
let multiply7 = (num) => bitAdd(num << 3, bitAdd(~num, 1));
multiply7(7); //49
黑科技 hack 手段
在 JavaScript 语言里,还有很多黑科技手段,比如我们可以使用字节码的形式来代替 “ * ” 乘号。
let multiply7_1 = (num) =>
new Function(["return ",num,String.fromCharCode(42),"7"].join(""))();
let multiply7_2 = (num) =>
eval([num,String.fromCharCode(42),"7"].join(""));
setTimeout(["window.multiply7_3=(num)=>(7",String.fromCharCode(42),"num)"].join(""))
multiply7_1(7);// 49
multiply7_2(7);// 49
multiply7_3(7);// 49
进制转换
参考位运算我们进行一下思考:二进制整数向左位移一位、末尾补0,可以得到其2倍值;十进制整数向左位移一位、末尾补0,可以得到其10倍值;那么我们也可以依此法来进行七进制整数进位补0,来得到7倍值!
let multiply7_4 =
(num)=>parseInt([num.toString(7),'0'].join(''),7);
multiply7_4(7);// 49
感悟
题目本身并没有进行非常强的要求,但是我们可以通过一个题目延展成若干个知识点:
位运算
补码
字节码
函数构造器 constructor
eval 方法
setTimeout 方法
toString 和 parseInt 在进制操作上的方法
相似的,类似的从使用方式上做限制的题目也可以用这种方法进行处理解答。
不算完整的答案
可以使用三类方式:位运算加法、JS hack、进制转换。实现方式分别如下:
/* -- 位运算 -- */
// 先定义位运算加法
function bitAdd(m, n){
while(m){
[m, n] = [(m & n) << 1, m ^ n];
}
return n;
}
// 位运算实现方式 1 - 循环累加7次
let multiply7_bo_1 = (num)=>
{
let sum = 0,counter = new Array(7); // 得到 [empty × 7]
while(counter.length){
sum = bitAdd(sum, num);
counter.shift();
}
return sum;
}
// 位运算实现方式 2 - 二进制进3位(乘以8)后,加自己的补码(乘以-1)
let multiply7_bo_2 = (num) => bitAdd(num << 3, -num) ;
/* -- JS hack -- */
// hack 方式 1 - 利用 Function 的构造器 & 乘号的字节码
let multiply7_hack_1 = (num) =>
new Function(["return ",num,String.fromCharCode(42),"7"].join(""))();
// hack 方式 2 - 利用 eval 执行器 & 乘号的字节码
let multiply7_hack_2 = (num) =>
eval([num,String.fromCharCode(42),"7"].join(""));
// hack 方式 3 - 利用 SetTimeout 的参数 & 乘号的字节码
setTimeout(["window.multiply7_hack_3=(num)=>(7",String.fromCharCode(42),"num)"].join(""))
/* -- 进制转换 -- */
// 进制转换方式 - 利用 toString 转为七进制整数;然后末尾补0(左移一位)后通过 parseInt 转回十进制
let multiply7_base7 =
(num)=>parseInt([num.toString(7),'0'].join(''),7);