新的一天,加油!
每日一道笔试题,遇见不一样的自己!
第102题:请输出下列代码执行的结果
//参考:忍者秘籍第二版 console.log('script start') let promise1 = new Promise(function (resolve) { console.log('promise1') resolve() console.log('promise1 end') }).then(function () { console.log('promise2') }) setTimeout(function(){ console.log('settimeout') }) console.log('script end')
解释:
输出结果:script start->promise1->promise1 end->script end->promise2->settimeout
当JS主线程执行到Promise对象时,
- promise1 是
resolved或rejected
: 那这个 task 就会放入当前事件循环队列的microtask queue
- promise1 是
pending
: 这个 task 就会放入事件循环队列的未来的某个(可能下一个)回合的microtask queue
中 - setTimeout 的回调也是个 task ,它会被放入
macrotask queue
,即使是 0ms 的情况
第101题:请输出下列代码执行的结果
//参考:忍者秘籍第二版 async function async1(){ console.log('async1 start'); await async2(); console.log('async1 end') } async function async2(){ console.log('async2') } console.log('script start'); async1(); console.log('script end')
解释:
输出结果:script start->async1 start->async2->script end->async1 end
- async 函数返回一个 Promise 对象(await通过返回一个Promise对象来实现同步的效果),当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。
- await的含义为等待,也就是 async 函数需要等待await后的函数执行完成并且有了返回结果(Promise对象)之后,才能继续执行下面的代码。
第100题:请解释下列三个方法在判断 是否是数组类型 时的区别:
- Object.prototype.toString.call() - instanceof - Array.isArray() 参考:js高级程序设计第三版
解释:
- Object.prototype.toString.call()
每一个继承 Object 的对象都有toString
方法,如果toString
方法没有重写的话,会返回 [Object type],其中 type 为对象的类型。但是,当除了 Object 类型的对象外,其他类型直接使用 toString 方法时,会直接返回内容的字符串,所以我们需要使用call或者apply方法来改变toString方法的执行上下文。
例如:
const arr = ['abc','bca']; arr.toString(); // "abc,bca" Object.prototype.toString.call(arr); // "[object Array]"
- 结论:这种方法对于所有基本的数据类型都能进行判断,即使是 null 和 undefined 。通常,该方法常用于判断浏览器内置对象。
- instanceof
instanceof 的内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。
使用instanceof
判断一个对象是否为数组,instanceof
会判断这个对象的原型链上是否会找到对应的Array
的原型,找到返回true
,否则返回false
[] instanceof Array; // true
- 但
instanceof
只能用来判断对象类型,原始类型不可以。并且所有对象类型 instanceof Object 都是 true。
例如:
[] instanceof Object; // true
- Array.isArray()
Array.isArray()
是ES5新增的方法,当不存在Array.isArray()
,可以用Object.prototype.toString.call()
实现。
if (!Array.isArray) { Array.isArray = function(arg) { return Object.prototype.toString.call(arg) === '[object Array]'; }; }
- 同时,
Array.isArray()优于instanceof
,特别是在检测Array实例时,Array.isArray
可以检测出iframes
下的Array实例。
例如:
let iframe = document.createElement('iframe'); document.body.appendChild(iframe); xArray = window.frames[window.frames.length-1].Array; let arr = new xArray(1,2,3); // [1,2,3] Array.isArray(arr); // true arr instanceof Array; // false
第99题:实现下列功能
输入: [2,3,4,6,7,9] 输出: '2~4,6~7,9'
const nums = [2,3,4,6,7,9]; function example(num) { let result = []; let temp = num[0] num.forEach((value, index) => { if (value + 1 !== num[index + 1]) { if (temp !== value) { result.push(`${temp}~${value}`) } else { result.push(`${value}`) } temp = num[index + 1] } }) return result; } console.log(example(nums).join(','))
第98题:如何优化浏览器的Repaint和Reflow
第97题:解释下Vue是如何进行双向数据绑定的?View->Model和Model-View,原理是什么
第96题:输出下列代码执行结果
String('123') == new String('123'); //true,==时做了隐式转换,调用了toString String('123') === new String('123');//false,两者的类型不一样,前者是string,后者是object
var name = 'abc'; (function() { if (typeof name == 'undefined') { name = 'cba'; console.log(name); } else { console.log(name); } })(); 1、首先进入立即执行函数作用域当中,获取name属性 2、在当前作用域没有找到name 3、通过作用域链找到最外层,得到name属性 4、执行else的内容,输出 abc
第95题:输出下列代码执行结果
2 + "3"; 3 * "5"; [6, 3] + [3, 6]; "b" + + "c";
//解释:
2 + "3"; 加性操作符:如果只有一个操作数是字符串,则将另一个操作数转换为字符串,然后再将两个字符串拼接起来 所以值为:“23” 3 * "5"; 乘性操作符:如果有一个操作数不是数值,则在后台调用 Number()将其转换为数值 [6, 3] + [3, 6]; Javascript中所有对象基本都是先调用valueOf方法,如果不是数值,再调用toString方法。 所以两个数组对象的toString方法相加,值为:"6,33,6" "b" + + "c"; 后边的“+”将作为一元操作符,如果操作数是字符串,将调用Number方法将该操作数转为数值,如果操作数无法转为数值,则为NaN。 所以值为:"bNaN"
第94题:写出如下代码的打印结果
function Foo() { Foo.a = function() { console.log(1) } this.a = function() { console.log(2) } } Foo.prototype.a = function() { console.log(3) } Foo.a = function() { console.log(4) } Foo.a(); let obj = new Foo(); obj.a(); Foo.a();
- 解析
function Foo() { Foo.a = function() { console.log(1) } this.a = function() { console.log(2) } } // 以上只是 Foo 的构建方法,没有产生实例,此刻也没有执行 Foo.prototype.a = function() { console.log(3) } // 现在在 Foo 上挂载了原型方法 a ,方法输出值为 3 Foo.a = function() { console.log(4) } // 现在在 Foo 上挂载了直接方法 a ,输出值为 4 Foo.a(); // 立刻执行了 Foo 上的 a 方法,也就是刚刚定义的,所以 // # 输出 4 let obj = new Foo(); /* 这里调用了 Foo 的构建方法。Foo 的构建方法主要做了两件事: 1. 将全局的 Foo 上的直接方法 a 替换为一个输出 1 的方法。 2. 在新对象上挂载直接方法 a ,输出值为 2。 */ obj.a(); // 因为有直接方法 a ,不需要去访问原型链,所以使用的是构建方法里所定义的 this.a, // # 输出 2 Foo.a(); // 构建方法里已经替换了全局 Foo 上的 a 方法,所以 // # 输出 1
第93题:用 JavaScript 写一个函数,输入 int 型,返回整数逆序后的字符串。
如:输入整型 1234,返回字符串“4321”。要求必须使用递归函数调用,不能用全局变量,输入函数必须只有一个参数传入,必须返回字符串。
function fun(num){ let num1 = num / 10; let num2 = num % 10; if(num1<1){ return num; }else{ num1 = Math.floor(num1) return `${num2}${fun(num1)}` } } var a = fun(12345) console.log(a)
第92题:写出如下代码的打印结果
function changeObjProperty(o) { o.siteUrl = "http://www.baidu.com" o = new Object() o.siteUrl = "http://www.google.com" } let webSite = new Object(); changeObjProperty(webSite); console.log(webSite.siteUrl);
输出:www.baidu.com //原因:函数的形参是值传递的
第91题:前端加密的常见场景和方法
加密的目的,简而言之就是将明文转换为密文、甚至转换为其他的东西,用来隐藏明文内容本身,防止其他人直接获取到敏感明文信息、或者提高其他人获取到明文信息的难度。
通常我们提到加密会想到密码加密、HTTPS 等关键词
场景-密码传输
前端密码传输过程中如果不加密,在日志中就可以拿到用户的明文密码,对用户安全不太负责。
这种加密其实相对比较简单,可以使用 PlanA-前端加密、后端解密后计算密码字符串的MD5/MD6存入数据库;也可以 PlanB-直接前端使用一种稳定算法加密成唯一值、后端直接将加密结果进行MD5/MD6,全程密码明文不出现在程序中。
- PlanA
使用 Base64 / Unicode+1 等方式加密成非明文,后端解开之后再存它的 MD5/MD6 - PlanB
直接使用 MD5/MD6 之类的方式取 Hash ,让后端存 Hash 的 Hash 。
场景-数据包加密
应该大家有遇到过:打开一个正经网站,网站底下蹦出个不正经广告——比如X通的流量浮层,X信的插入式广告……(我没有针对谁)
但是这几年,我们会发现这种广告逐渐变少了,其原因就是大家都开始采用 HTTPS 了。
被人插入这种广告的方法其实很好理解:你的网页数据包被抓取->在数据包到达你手机之前被篡改->你得到了带网页广告的数据包->渲染到你手机屏幕。
而 HTTPS 进行了包加密,就解决了这个问题。严格来说我认为从手段上来看,它不算是一种前端加密场景;但是从解决问题的角度来看,这确实是前端需要知道的事情。
- Plan
全面采用 HTTPS
场景-展示成果加密
经常有人开发网页爬虫爬取大家辛辛苦苦一点一点发布的数据成果,有些会影响你的竞争力,有些会降低你的知名度,甚至有些出于恶意爬取你的公开数据后进行全量公开……比如有些食谱网站被爬掉所有食谱,站点被克隆;有些求职网站被爬掉所有职位,被拿去卖信息;甚至有些小说漫画网站赖以生存的内容也很容易被爬取。
- Plan
将文本内容进行展示层加密,利用字体的引用特点,把拿给爬虫的数据变成“乱码”。
举个栗子:正常来讲,当我们拥有一串数字“12345”并将其放在网站页面上的时候,其实网站页面上显示的并不是简单的数字,而是数字对应的字体的“12345”。这时我们打乱一下字体中图形和字码的对应关系,比如我们搞成这样:
图形:1 2 3 4 5
字码:2 3 1 5 4
这时,如果你想让用户看到“12345”,你在页面中渲染的数字就应该是“23154”。这种手段也可以算作一种加密。
具体的实现方法可以看一下《Web 端反爬虫技术方案》。
参考
第90题:模拟实现一个深拷贝,并考虑对象相互引用以及 Symbol 拷贝的情况
一个不考虑其他数据类型的公共方法,基本满足大部分场景
function deepCopy(target, cache = new Set()) { if (typeof target !== 'object' || cache.has(target)) { return target } if (Array.isArray(target)) { target.map(t => { cache.add(t) return t }) } else { return [...Object.keys(target), ...Object.getOwnPropertySymbols(target)].reduce((res, key) => { cache.add(target[key]) res[key] = deepCopy(target[key], cache) return res }, target.constructor !== Object ? Object.create(target.constructor.prototype) : {}) } }
主要问题是
symbol
作为key
,不会被遍历到,所以stringify
和parse
是不行的- 有环引用,
stringify
和parse
也会报错
我们另外用getOwnPropertySymbols
可以获取symbol key
可以解决问题1,用集合记忆曾经遍历过的对象可以解决问题2。当然,还有很多数据类型要独立去拷贝。比如拷贝一个RegExp,lodash是最全的数据类型拷贝了,有空可以研究一下
另外,如果不考虑用symbol
做key
,还有两种黑科技深拷贝,可以解决环引用的问题,比stringify
和parse
优雅强一些。
function deepCopyByHistory(target) { const prev = history.state history.replaceState(target, document.title) const res = history.state history.replaceState(prev, document.title) return res } async function deepCopyByMessageChannel(target) { return new Promise(resolve => { const channel = new MessageChannel() channel.port2.onmessage = ev => resolve(ev.data) channel.port1.postMessage(target) }).then(data => data) }
无论哪种方法,它们都有一个共性:失去了继承关系,所以剩下的需要我们手动补上去了,故有Object.create(target.constructor.prototype)
的操作
第89题:vue 在 v-for 时给每项元素绑定事件需要用事件代理吗?为什么?
事件代理作用主要是 2 个:
- 将事件处理程序代理到父节点,减少内存占用率
- 动态生成子节点时能自动绑定事件处理程序到父节点
//不使用事件代理,每个 span 节点绑定一个 click 事件,并指向同一个事件处理程序 <div> <span v-for="(item,index) of 100000" :key="index" @click="handleClick"> {{item}} </span> </div> //不使用事件代理,每个 span 节点绑定一个 click 事件,并指向不同的事件处理程序 <div> <span v-for="(item,index) of 100000" :key="index" @click="function () {}"> {{item}} </span> </div> // 使用事件代理 <div @click="handleClick"> <span v-for="(item,index) of 100000" :key="index"> {{item}} </span> </div> 使用事件代理无论是监听器数量和内存占用率都比前两者要少
第88题:给定两个大小为m和n的有序数组nums1和nums2。 请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))
const findMidNum = function(arr1,arr2) { for(let i=0;i<arr2.length;i++) { arr1.push(arr2[i]); } arr1 = arr1.sort((a,b)=>{return b-a;}) if(arr1.length%2===0) { return (arr1[arr1.length/2]+arr1[arr1.length/2-1])/2 }else { return arr1[(arr1.length-1)/2] } } console.log(findMidNum([1,2],[3,5,6]))
第87题:已知数据格式,实现一个函数 fn 找出链条中所有的父级 id
const data = [{ id: '1', name: 'test1', children: [ { id: '11', name: 'test11', children: [ { id: '111', name: 'test111' }, { id: '112', name: 'test112' } ] }, { id: '12', name: 'test12', children: [ { id: '121', name: 'test121' }, { id: '122', name: 'test122' } ] } ] }]; let res = []; const findId = (list, value) => { let len = list.length; for (let i in list) { const item = list[i]; if (item.id == value) { return res.push(item.id), [item.id]; } if (item.children) { if (findId(item.children, value).length) { res.unshift(item.id); return res; } } if (i == len - 1) { return res; } } };
第86题:介绍下 HTTPS 中间人攻击
中间人攻击过程如下:
- 服务器向客户端发送公钥。
- 攻击者截获公钥,保留在自己手上。
- 然后攻击者自己生成一个【伪造的】公钥,发给客户端。
- 客户端收到伪造的公钥后,生成加密hash值发给服务器。
- 攻击者获得加密hash值,用自己的私钥解密获得真秘钥。
- 同时生成假的加密hash值,发给服务器。
- 服务器用私钥解密获得假秘钥。
- 服务器用加秘钥加密传输信息 。
防范方法:
服务端在发送浏览器的公钥中加入CA证书,浏览器可以验证CA证书的有效性
第85题:实现模糊搜索结果的关键词高亮显示
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>auto complete</title> <style> bdi { color: rgb(0, 136, 255); } li { list-style: none; } </style> </head> <body> <input class="inp" type="text"> <section> <ul class="container"></ul> </section> </body> <script> function debounce(fn, timeout = 300) { let t; return (...args) => { if (t) { clearTimeout(t); } t = setTimeout(() => { fn.apply(fn, args); }, timeout); } } function memorize(fn) { const cache = new Map(); return (name) => { if (!name) { container.innerHTML = ''; return; } if (cache.get(name)) { container.innerHTML = cache.get(name); return; } const res = fn.call(fn, name).join(''); cache.set(name, res); container.innerHTML = res; } } function handleInput(value) { const reg = new RegExp(`\(${value}\)`); const search = data.reduce((res, cur) => { if (reg.test(cur)) { const match = RegExp.$1; res.push(`<li>${cur.replace(match, '<bdi>$&</bdi>')}</li>`); } return res; }, []); return search; } const data = ["上海野生动物园", "上饶野生动物园", "北京巷子", "上海中心", "上海黄埔江", "迪士尼上海", "陆家嘴上海中心"] const container = document.querySelector('.container'); const memorizeInput = memorize(handleInput); document.querySelector('.inp').addEventListener('input', debounce(e => { memorizeInput(e.target.value); })) </script> </html>
第84题:设计并实现 Promise.race()
Promise.myrace = function(iterator) { return new Promise ((resolve,reject) => { try { let it = iterator[Symbol.iterator](); while(true) { let res = it.next(); console.log(res); if(res.done) break; if(res.value instanceof Promise) { res.value.then(resolve,reject); } else { resolve(res.value) } } } catch (error) { reject(error) } }) }
第83题:实现 convert 方法,把原始 list 转换成树形结构,要求尽可能降低时间复杂度
先生成新结构map
,用原先的结构与其比较,对原结构改造。
function convert(list) { const res = [] const map = list.reduce((res, v) => (res[v.id] = v, res), {}) for (const item of list) { if (item.parentId === 0) { res.push(item) continue } if (item.parentId in map) { const parent = map[item.parentId] parent.children = parent.children || [] parent.children.push(item) } } return res } let list =[ {id:1,name:'部门A',parentId:0}, {id:2,name:'部门B',parentId:0}, {id:3,name:'部门C',parentId:1}, {id:4,name:'部门D',parentId:1}, {id:5,name:'部门E',parentId:2}, {id:6,name:'部门F',parentId:3}, {id:7,name:'部门G',parentId:2}, {id:8,name:'部门H',parentId:4} ]; const result = convert(list); console.table(result);
第82题:在输入框中如何判断输入的是一个正确的网址
主要解析http,https
:
function isUrl(url) { const a = document.createElement('a') a.href = url return [ /^(http|https):$/.test(a.protocol), a.host, a.pathname !== url, a.pathname !== `/${url}`, ].find(x => !x) === undefined }
第81题:算法题之–两数之和
给定一个整数数组和一个目标值,找出数组中和为目标值的两个数。
你可以假设每个输入只对应一种答案,且同样的元素不能被重复利用。
示例:
给定 nums = [2, 7, 11, 15], target = 9 因为 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1]
- 解析
(1). 直接遍历两次数组
//:时间复杂度为O(N*N) find2Num([2,7,11,15],9); function find2Num(arr,sum){ if(arr == '' || arr.length == 0){ return false; } let result = []; for(var i = 0; i < arr.length ; i++){ for(var j = i + 1; j <arr.length; j++){ if(arr[i] + arr[j] == sum){ result.push(i); result.push(j); } } } console.log(result); }
(2)
先将整型数组排序,排序之后定义两个指针left和right。
left指向已排序数组中的第一个元素,right指向已排序数组中的最后一个元素, 将 arr[left]+arr[right]与
给定的元素比较,若前者大,right–;若前者小,left++; 若相等,则找到了一对整数之和为指定值的元素。
//时间复杂度为O(NlogN) function find2Num(arr,sum){ if(arr == '' || arr.length == 0){ return false; } var left = 0, right = arr.length -1,result = []; while(left < right){ if(arr[left] + arr[right] > sum){ right--; } else if(arr[left] + arr[right] < sum){ left++; } else{ console.log(arr[left] + " + " + arr[right] + " = " + sum); result.push(left); result.push(right); left++; right--; } } console.log(result); }
第80题:react-router 里的 标签和 标签有什么区别
<Link>
是react-router
里实现路由跳转的链接,一般配合<Route>
使用,react-router
接管了其默认的链接跳转行为,区别于传统的页面跳转,<Link>
的“跳转”行为只会触发相匹配的<Route>
对应的页面内容更新,而不会刷新整个页面。
Link点击事件handleClick部分源码:
if (_this.props.onClick) _this.props.onClick(event); if (!event.defaultPrevented && // onClick prevented default event.button === 0 && // ignore everything but left clicks !_this.props.target && // let browser handle "target=_blank" etc. !isModifiedEvent(event) // ignore clicks with modifier keys ) { event.preventDefault(); var history = _this.context.router.history; var _this$props = _this.props, replace = _this$props.replace, to = _this$props.to; if (replace) { history.replace(to); } else { history.push(to); } }
Link做了3件事情:
- 有onclick那就执行onclick
- click的时候阻止a标签默认事件(这样子点击
<a href="/abc">123</a>
就不会跳转和刷新页面) - 再取得跳转
href(即是to)
,用history
(前端路由两种方式之一,history & hash
)跳转,此时只是链接变了,并没有刷新页面
- 而
<a>
标签就是普通的超链接了,用于从当前页面跳转到href
指向的另一个页面(非锚点情况)。
如何禁掉 <a>
标签默认事件,禁掉之后如何实现跳转
- 禁掉 a 标签的默认事件,可以在点击事件中执行
event.preventDefault()
; - 禁掉默认事件的 a 标签 可以使用
history.pushState()
来改变页面 url,这个方法还会触发页面的hashchange
事件,Router 内部通过捕获监听这个事件来处理对应的跳转逻辑。