第 1 题:写 React / Vue 项目时为什么要在列表组件中写 key,其作用是什么?
key 是给每一个 vnode 的唯一 id,可以依靠 key,更准确,更快的拿到 oldVnode 中对应的 vnode 节点 第 2 题:什么是防抖和节流?有什么区别?如何实现?
防抖——触发高频事件后 n 秒后函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间; function debounce(fn) { let timeout = null; // 创建一个标记用来存放定时器的返回值 return function () { clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉 timeout = setTimeout(() => { // 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数 fn.apply(this, arguments); }, 500); }; } function sayHi() { console.log('防抖成功'); } var inp = document.getElementById('inp'); inp.addEventListener('input', debounce(sayHi)); // 防抖
节流——高频事件触发,但在 n 秒内只会执行一次,所以节流会稀释函数的执行频率。 function throttle(fn) { let canRun = true; // 通过闭包保存一个标记 return function () { if (!canRun) return; // 在函数开头判断标记是否为 true,不为 true 则 return canRun = false; // 立即设置为 false setTimeout(() => { // 将外部传入的函数的执行放在 setTimeout 中 fn.apply(this, arguments); // 最后在 setTimeout 执行完毕后再把标记设置为 true(关键) 表示可以执行下一次循环了。当定时器没有执行的时候标记永远是 false,在开头被 return 掉 canRun = true; }, 500); }; } function sayHi(e) { console.log(e.target.innerWidth, e.target.innerHeight); } window.addEventListener('resize', throttle(sayHi)); 第 3 题:介绍下 Set、Map、WeakSet 和 WeakMap 的区别?
Set——对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用 WeakSet——成员都是对象;成员都是弱引用,可以被垃圾回收机制回收,可以 用来保存 DOM 节点,不容易造成内存泄漏; Map——本质上是键值对的集合,类似集合;可以遍历,方法很多,可以跟各 种数据格式转换。 WeakMap——只接受对象最为键名(null 除外),不接受其他类型的值作为键 名;键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收, 此时键名是无效的;不能遍历,方法有 get、set、has、delete。 第 4 题:介绍下深度优先遍历和广度优先遍历,如何实现?
深度优先遍历——是指从某个顶点出发,首先访问这个顶点,然后找出刚访问 这个结点的第一个未被访问的邻结点,然后再以此邻结点为顶点,继续找它的 下一个顶点进行访问。重复此步骤,直至所有结点都被访问完为止。 广度优先遍历——是从某个顶点出发,首先访问这个顶点,然后找出刚访问这 个结点所有未被访问的邻结点,访问完后再访问这些结点中第一个邻结点的所 有结点,重复此方法,直到所有结点都被访问完为止。 //1.深度优先遍历的递归写法 function deepTraversal(node) { let nodes = [] if (node != null) { nodes.push[node] let childrens = node.children for (let i = 0; i < childrens.length; i++) deepTraversal(childrens[i]) } return nodes} //2.深度优先遍历的非递归写法 function deepTraversal(node) { let nodes = [] if (node != null) { let stack = [] //同来存放将来要访问的节点 stack.push(node) while (stack.length != 0) { let item = stack.pop() //正在访问的节点 nodes.push(item) let childrens = item.children for ( let i = childrens.length - 1; i >= 0; i-- //将现在访问点的节点的子节点存入 stack,供将来访问 ) stack.push(childrens[i]) } } return nodes} //3.广度优先遍历的递归写法 function wideTraversal(node) { let nodes = [], i = 0 if (node != null) { nodes.push(node) wideTraversal(node.nextElementSibling) node = nodes[i++] wideTraversal(node.firstElementChild) } return nodes}//4.广度优先遍历的非递归写法 function wideTraversal(node) { let nodes = [], i = 0 while (node != null) { nodes.push(node) node = nodes[i++] let childrens = node.children for (let i = 0; i < childrens.length; i++) { nodes.push(childrens[i]) } } return nodes } 第 5 题:请分别用深度优先思想和广度优先思想实现一个拷贝函数?
let _toString = Object.prototype.toStringlet map = { array: 'Array', object: 'Object', function: 'Function', string: 'String', null: 'Null', undefined: 'Undefined', boolean: 'Boolean', number: 'Number' }let getType = (item) => { return _toString.call(item).slice(8, -1) }let isTypeOf = (item, type) => { return map[type] && map[type] === getType(item) } 第 6 题:ES5 / ES6 的继承除了写法以外还有什么区别?
- ES5 的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到 this 上(Parent.apply(this))
- ES6 的继承机制完全不同,实质上是先创建父类的实例对象 this(所以必须先调用父类的 super()方法),然后再用子类的构造函数修改 this。
- ES5 的继承时通过原型或构造函数机制来实现。
- ES6 通过 class 关键字定义类,里面有构造方法,类之间通过 extends 关键字实现继承。
- 子类必须在 constructor 方法中调用 super 方法,否则新建实例报错。因为子类没有自己的 this 对象,而是继承了父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类得不到 this 对象。
- 注意 super 关键字指代父类的实例,即父类的 this 对象。
- 注意:在子类构造函数中,调用 super 后,才可使用 this 关键字,否则报错。function 声明会提升,但不会初始化赋值。Foo 进入暂时性死区,类似于 let、const 声明变量。 第 7 题:setTimeout、Promise、Async / Await 的区别
首先,我们先来了解一下基本概念: js EventLoop 事件循环机制: JavaScript的事件分两种,宏任务(macro-task)和微任务(micro-task)
宏任务:包括整体代码script,setTimeout,setInterval 微任务:Promise.then(非new Promise),process.nextTick(node中)
事件的执行顺序,是先执行宏任务,然后执行微任务,这个是基础,任务可以有同步任务和异步任务,同步的进入主线程,异步的进入Event Table并注册函数,异步事件完成后,会将回调函数放入Event Queue中(宏任务和微任务是不同的Event Queue),同步任务执行完成后,会从Event Queue中读取事件放入主线程执行,回调函数中可能还会包含不同的任务,因此会循环执行上述操作。 注意: setTimeOut并不是直接的把你的回掉函数放进上述的异步队列中去,而是在定时器的时间到了之后,把回掉函数放到执行异步队列中去。如果此时这个队列已经有很多任务了,那就排在他们的后面。这也就解释了为什么setTimeOut为什么不能精准的执行的问题了。setTimeOut执行需要满足两个条件:
- 主进程必须是空闲的状态,如果到时间了,主进程不空闲也不会执行你的回掉函数
- 这个回掉函数需要等到插入异步队列时前面的异步函数都执行完了,才会执行
上面是比较官方的解释,说一下自己的理解吧:
了解了什么是宏任务和微任务,就好理解多了,首先执行 宏任务 => 微任务的Event Queue => 宏任务的Event Queue
promise、async/await 首先,new Promise是同步的任务,会被放到主进程中去立即执行。而.then()函数是异步任务会放到异步队列中去,那什么时候放到异步队列中去呢?当你的promise状态结束的时候,就会立即放进异步队列中去了。
带async关键字的函数会返回一个promise对象,如果里面没有await,执行起来等同于普通函数;如果没有await,async函数并没有很厉害是不是 await 关键字要在 async 关键字函数的内部,await 写在外面会报错;await如同他的语意,就是在等待,等待右侧的表达式完成。此时的await会让出线程,阻塞async内后续的代码,先去执行async外的代码。等外面的同步代码执行完毕,才会执行里面的后续代码。就算await的不是promise对象,是一个同步函数,也会等这样操作 步入正题:
根据图片显示我们来整理一下流程:
1、执行console.log('script start'),输出script start; 2、执行setTimeout,是一个异步动作,放入宏任务异步队列中; 3、执行async1(),输出async1 start,继续向下执行; 4、执行async2(),输出async2,并返回了一个promise对象,await让出了线程,把返回的promise加入了微任务异步队列,所以async1()下面的代码也要等待上面完成后继续执行; 5、执行 new Promise,输出promise1,然后将resolve放入微任务异步队列; 6、执行console.log('script end'),输出script end; 7、到此同步的代码就都执行完成了,然后去微任务异步队列里去获取任务 8、接下来执行resolve(async2返回的promise返回的),输出了async1 end。 9、然后执行resolve(new Promise的),输出了promise2。 10、最后执行setTimeout,输出了settimeout。 第 8 题:Async / Await 如何通过同步的方式实现异步
async 起什么作用——输出的是一个 Promise 对象 第 9 题:JS 异步解决方案的发展历程以及优缺点。
1、回调函数(callback) 优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队 等着,会拖延整个程序的执行。) 缺点:回调地狱,不能用 try catch 捕获错误,不能 return 2、Promise 优点:解决了回调地狱的问题 缺点:无法取消 Promise ,错误需要通过回调函数来捕获 3、Generator 特点:可以控制函数的执行,可以配合 co 函数库使用 4、Async/await 优点:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题 缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使 用 await 会导致性能上的降低。
第 10 题:Promise 构造函数是同步执行还是异步执行,那么 then 方法呢?
const promise = new Promise((resolve, reject) => { console.log(1) resolve() console.log(2)})promise.then(() => { console.log(3)})console.log(4) 执行结果是:1243,promise 构造函数是同步执行的,then 方法是异步执行的 第 11 题:谈谈你对 TCP 三次握手和四次挥手的理解
TCP三次握手:
1、客户端发送syn包到服务器,等待服务器确认接收。
2、服务器确认接收syn包并确认客户的syn,并发送回来一个syn+ack的包给客户端。
3、客户端确认接收服务器的syn+ack包,并向服务器发送确认包ack,二者相互建立联系后,完成tcp三次握手。
四次握手就是中间多了一层:等待服务器再一次响应回复相关数据的过程
三次握手之所以是三次是保证client和server均让对方知道自己的接收和发送能力没问题而保证的最小次数。
第一次client => server 只能server判断出client具备发送能力
第二次 server => client client就可以判断出server具备发送和接受能力。此时client还需让server知道自己接收能力没问题于是就有了第三次 第三次 client => server 双方均保证了自己的接收和发送能力没有问题
其中,为了保证后续的握手是为了应答上一个握手,每次握手都会带一个标识 seq,后续的ACK都会对这个seq进行加一来进行确认。 第 12 题:React 中 setState 什么时候是同步的,什么时候是异步的?
1、由 React 控制的事件处理程序,以及生命周期函数调用 setState 不会同步更 新 state 。 2、React 控制之外的事件中调用 setState 是同步更新的。比如原生 js 绑定的事 件,setTimeout/setInterval 等。 第 13 题:React setState 笔试题,下面的代码输出什么?
class Example extends React.Component { constructor() { super() this.state = { val: 0 } } componentDidMount() { this.setState({ val: this.state.val + 1 }) console.log(this.state.val) // 第 1 次 log this.setState({ val: this.state.val + 1 }) console.log(this.state.val) // 第 2 次 log setTimeout(() => { this.setState({ val: this.state.val + 1 }) console.log(this.state.val) // 第 3 次 log this.setState({ val: this.state.val + 1 }) console.log(this.state.val) // 第 4 次 log }, 0) } render() { return null } } 答: 0, 0, 1, 2 第 14 题:介绍下 npm 模块安装机制,为什么输入 npm install 就可以自动安装对 应的模块?1. npm 模块安装机制
发出 npm install 命令 1 查询 node_modules 目录之中是否已经存在指定模 块 若存在,不再重新安装 若不存在 npm 向 registry 查询模块压缩包的网址 下载压缩包,存放在根目录下的.npm 目录里 解压压缩包到当前项目的 node_modules 目录 第 15 题:有以下 3 个判断数组的方法,请分别介绍它们之间的区别和优劣
Object.prototype.toString.call() 、 instanceof 以及 Array.isArray() Object.prototype.toString.call() 每一个继承 Object 的对象都有 toString 方法,如果 toString 方法没有重写的 话,会返回[Object type],其中 type 为对象的类型。但当除了 Object 类型的 对象外,其他类型直接使用 toString 方法时,会直接返回都是内容的字符串, 所以我们需要使用 call 或者 apply 方法来改变 toString 方法的执行上下文。 const an = ['Hello', 'An']; an.toString(); // "Hello,An"Object.prototype.toString.call(an); // "[object Array]" 这种方法对于所有基本的数据类型都能进行判断,即使是 null 和 undefined 。 Object.prototype.toString.call('An') // "[object String]"Object.prototype.toString.call(1) // "[object Number]"Object.prototype.toString.call(Symbol(1)) // "[object Symbol]"Object.prototype.toString.call(null) // "[object Null]"Object.prototype.toString.call(undefined) // "[object Undefined]"Object.prototype.toString.call(function(){}) // "[object Function]"Object.prototype.toString.call({name: 'An'}) // "[object Object]" Object.prototype.toString.call() 常用于判断浏览器内置对象时。 instanceof instanceof 的内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。 使用 instanceof 判断一个对象是否为数组,instanceof 会判断这个对象的原型 链上是否会找到对应的 Array 的原型,找到返回 true,否则返回 false。 [] instanceof Array; // true 但 instanceof 只能用来判断对象类型,原始类型不可以。并且所有对象类型 instanceof Object 都是 true。 [] instanceof Object; // true Array.isArray() 功能:用来判断对象是否为数组 instanceof 与 isArray 当检测 Array 实例时,Array.isArray 优于 instanceof ,因为 Array.isArray 可以 检测出 iframes var iframe = document.createElement('iframe'); document.body.appendChild(iframe); xArray = window.frames[window.frames.length - 1].Array; var arr = new xArray(1, 2, 3); // [1,2,3]// Correctly checking for ArrayArray.isArray(arr); // trueObject.prototype.toString.call(arr); // true // Considered harmful, because doesn't work though iframesarr instanceof Array; // false Array.isArray() 与 Object.prototype.toString.call() Array.isArray()是 ES5 新增的方法,当不存在 Array.isArray() ,可以用 Object.prototype.toString.call() 实现。 if (!Array.isArray) { Array.isArray = function (arg) { return Object.prototype.toString.call(arg) === '[object Array]'; }; } 第 16 题:介绍下重绘和回流(Repaint & Reflow),以及如何进行优化
- 浏览器渲染机制 浏览器采用流式布局模型(Flow Based Layout) 浏览器会把 HTML 解析成 DOM,把 CSS 解析成 CSSOM,DOM 和 CSSOM 合并就 产生了渲染树(Render Tree)。 有了 RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大 小和位置,最后把节点绘制到页面上。 由于浏览器使用流式布局,对 Render Tree 的计算通常只需要遍历一次就可以完 成,但 table 及其内部元素除外,他们可能需要多次计算,通常要花 3 倍于同 等元素的时间,这也是为什么要避免使用 table 布局的原因之一。
- 重绘 由于节点的几何属性发生改变或者由于样式发生改变而不会影响布局的,称为 重绘,例如 outline, visibility, color、background-color 等,重绘的代价是高昂的, 因为浏览器必须验证 DOM 树上其他节点元素的可见性。
- 回流 回流是布局或者几何属性需要改变就称为回流。回流是影响浏览器性能的关键 因素,因为其变化涉及到部分页面(或是整个页面)的布局更新。一个元素的 回流可能会导致了其所有子元素以及 DOM 中紧随其后的节点、祖先节点元素 的随后的回流。
我的组件
错误:错误的描述…
错误纠正
- 第一步
- 第二步
标签)回流将会引发强烈的回流,因为它 是一个子节点。这也导致了祖先的回流(div.error 和 body – 视浏览器而定)。 此外,
和也会有简单的回流,因为其在 DOM 中在回流元素之后。大部
分的回流将导致页面的重新渲染。
回流必定会发生重绘,重绘不一定会引发回流。
第 17 题:介绍下观察者模式和订阅 - 发布模式的区别,各自适用于什么场景
观察者模式中主体和观察者是互相感知的,发布-订阅模式是借助第三方来实现
调度的,发布者和订阅者之间互不感知
第 18题:聊聊 Redux 和 Vuex 的设计思想
不管是 Vue,还是 React,都需要管理状态(state),比如组件之间都有共享
状态的需要。什么是共享状态?比如一个组件需要使用另一个组件的状态,或
者一个组件需要改变另一个组件的状态,都是共享状态。
父子组件之间,兄弟组件之间共享状态,往往需要写很多没有必要的代码,比
如把状态提升到父组件里,或者给兄弟组件写一个父组件,听听就觉得挺啰嗦。
如果不对状态进行有效的管理,状态在什么时候,由于什么原因,如何变化就
会不受控制,就很难跟踪和测试了。如果没有经历过这方面的困扰,可以简单
理解为会搞得很乱就对了。
在软件开发里,有些通用的思想,比如隔离变化,约定优于配置等,隔离变化
就是说做好抽象,把一些容易变化的地方找到共性,隔离出来,不要去影响其
他的代码。约定优于配置就是很多东西我们不一定要写一大堆的配置,比如我
们几个人约定,view 文件夹里只能放视图,不能放过滤器,过滤器必须放到
filter 文件夹里,那这就是一种约定,约定好之后,我们就不用写一大堆配置文
件了,我们要找所有的视图,直接从 view 文件夹里找就行。
根据这些思想,对于状态管理的解决思路就是:把组件之间需要共享的状态抽
取出来,遵循特定的约定,统一来管理,让状态的变化可以预测。根据这个思
路,产生了很多的模式和库,我们来挨个聊聊。
第 19题:说说浏览器和 Node 事件循环的区别
其中一个主要的区别在于浏览器的 event loop 和 nodejs 的 event loop 在处理异
步事件的顺序是不同的,nodejs 中有 micro event;其中 Promise 属于 micro event
该异步事件的处理顺序就和浏览器不同.nodejs V11.0 以上 这两者之间的顺序
就相同了. function test () {
console.log('start')
setTimeout(() => {
console.log('children2')
Promise.resolve().then(() =>
{console.log('children2-1')})
}, 0)
setTimeout(() => {
console.log('children3')
Promise.resolve().then(() =>
{console.log('children3-1')})
}, 0)
Promise.resolve().then(() =>
{console.log('children1')})
console.log('end')
}test()// 以上代码在 node11 以下版本的执行结果(先执行所有的宏任
务,再执行微任务)// start// end// children1// children2// children3//
children2-1// children3-1// 以上代码在 node11 及浏览器的执行结果(顺序执
行宏任务和微任务)// start// end// children1// children2// children2-1//
children3// children3-1
第 20 题:介绍模块化发展历程
blog.csdn.net/dadadeganhu…
第 21 题:全局作用域中,用 const 和 let 声明的变量不在 window 上,那到底在哪里?如何去获取?
在 ES5 中,顶层对象的属性和全局变量是等价的,var 命令和 function 命令声
明的全局变量,自然也是顶层对象。
var a = 12;
function f(){};
console.log(window.a);
// 12console.log(window.f);
// f(){}
但 ES6 规定,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属
性,但 let 命令、const 命令、class 命令声明的全局变量,不属于顶层对象的属
性。
let aa = 1;
const bb = 2;
console.log(window.aa);
// undefinedconsole.log(window.bb);
// undefined
在哪里?怎么获取?通过在设置断点,看看浏览器是怎么处理的:
通过上图也可以看到,在全局作用域中,用 let 和 const 声明的全局变量并没
有在全局对象中,只是一个块级作用域(Script)中
怎么获取?在定义变量的块级作用域中就能获取啊,既然不属于顶层对象,那
就不加 window(global)呗。
let aa = 1;
const bb = 2;
console.log(aa);
// 1console.log(bb);
// 2
第 22 题:cookie 和 token 都存放在 header 中,为什么不会劫持 token?
1、攻击者通过 xss 拿到用户的 cookie 然后就可以伪造 cookie 了。
2、或者通过 csrf 在同个浏览器下面通过浏览器会自动带上 cookie 的特性
在通过 用户网站-攻击者网站-攻击者请求用户网站的方式 浏览器会自动带上
cookie
但是 token
1、不会被浏览器带上 问题 2 解决
2、token 是放在 jwt 里面下发给客户端的 而且不一定存储在哪里 不能通过
document.cookie 直接拿到,通过 jwt+ip 的方式 可以防止 被劫持 即使被劫持
也是无效的 jwt
第 23 题:聊聊 Vue 的双向数据绑定,Model 如何改变 View,View 又是如何改变 Model
1、从 M 到 V 的映射(Data Binding),这样可以大量节省你人肉来 update View
的代码
2、从 V 到 M 的事件监听(DOM Listeners),这样你的 Model 会随着 View
触发事件而改变
24 题:两个数组合并成一个数组
方法一
var a=[1,2,3];
var b=[4,5,6];
var c=a.concat(b);
alert(c);
方法二
es6
var newarr=[...a,...b]
第 25 题:Virtual DOM 真的比操作原生 DOM 快吗?谈谈你的想法
- 原生 DOM 操作 vs. 通过框架封装操作。
这是一个性能 vs. 可维护性的取舍。框架的意义在于为你掩盖底层的 DOM 操
作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护。
没有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的 DOM 操作层
需要应对任何上层 API 可能产生的操作,它的实现必须是普适的。针对任何一
个 benchmark,我都可以写出比任何框架更快的手动优化,但是那有什么意义
呢?在构建一个实际应用的时候,你难道为每一个地方都去做手动优化吗?出
于可维护性的考虑,这显然不可能。框架给你的保证是,你在不需要手动优化
的情况下,我依然可以给你提供过得去的性能。
- 对 React 的 Virtual DOM 的误解。
React 从来没有说过 “React 比原生操作 DOM 快”。React 的基本思维模式是
每次有变动就整个重新渲染整个应用。如果没有 Virtual DOM,简单来想就是
直接重置 innerHTML。很多人都没有意识到,在一个大型列表所有数据都变了
的情况下,重置 innerHTML 其实是一个还算合理的操作... 真正的问题是在
“全部重新渲染” 的思维模式下,即使只有一行数据变了,它也需要重置整个
innerHTML,这时候显然就有大量的浪费。
我们可以比较一下 innerHTML vs. Virtual DOM 的重绘性能消耗:
innerHTML: render html string O(template size) + 重新创建所有 DOM 元
素 O(DOM size) Virtual DOM: render Virtual DOM + diff O(template size) + 必要的 DOM
更新 O(DOM change)
Virtual DOM render + diff 显然比渲染 html 字符串要慢,但是!它依然是纯 js
层面的计算,比起后面的 DOM 操作来说,依然便宜了太多。可以看到,
innerHTML 的总计算量不管是 js 计算还是 DOM 操作都是和整个界面的大小
相关,但 Virtual DOM 的计算量里面,只有 js 计算和界面大小相关,DOM 操
作是和数据的变动量相关的。前面说了,和 DOM 操作比起来,js 计算是极其
便宜的。这才是为什么要有 Virtual DOM:它保证了 1)不管你的数据变化多
少,每次重绘的性能都可以接受;2) 你依然可以用类似 innerHTML 的思路去
写你的应用。
- MVVM vs. Virtual DOM
相比起 React,其他 MVVM 系框架比如 Angular, Knockout 以及 Vue、Avalon
采用的都是数据绑定:通过 Directive/Binding 对象,观察数据变化并保留对实
际 DOM 元素的引用,当有数据变化时进行对应的操作。MVVM 的变化检查是
数据层面的,而 React 的检查是 DOM 结构层面的。
MVVM 的性能也根据变动检测的实现原理有所不同:Angular 的脏检查使得任
何变动都有固定的 O(watcher count) 的代价;Knockout/Vue/Avalon 都采用了依
赖收集,在 js 和 DOM 层面都是 O(change):
脏检查:scope digest O(watcher count) + 必要 DOM 更新 O(DOM
change)
依赖收集:重新收集依赖 O(data change) + 必要 DOM 更新 O(DOM
change)可以看到,Angular 最不效率的地方在于任何小变动都有的和 watcher
数量相关的性能代价。但是!当所有数据都变了的时候,Angular 其实并不吃
亏。依赖收集在初始化和数据变化的时候都需要重新收集依赖,这个代价在小
量更新的时候几乎可以忽略,但在数据量庞大的时候也会产生一定的消耗。
MVVM 渲染列表的时候,由于每一行都有自己的数据作用域,所以通常都是每
一行有一个对应的 ViewModel 实例,或者是一个稍微轻量一些的利用原型继
承的 "scope" 对象,但也有一定的代价。所以,MVVM 列表渲染的初始化几乎
一定比 React 慢,因为创建 ViewModel / scope 实例比起 Virtual DOM 来说要
昂贵很多。
这里所有 MVVM 实现的一个共同问题就是在列表渲染的数据源变动时,尤其
是当数据是全新的对象时,如何有效地复用已经创建的 ViewModel 实例和
DOM 元素。
假如没有任何复用方面的优化,由于数据是 “全新” 的,MVVM 实际上需要销
毁之前的所有实例,重新创建所有实例,最后再进行一次渲染!这就是为什么
题目里链接的 angular/knockout 实现都相对比较慢。相比之下,React 的变动
检查由于是 DOM 结构层面的,即使是全新的数据,只要最后渲染结果没变,
那么就不需要做无用功。
Angular 和 Vue 都提供了列表重绘的优化机制,也就是 “提示” 框架如何有效
地复用实例和 DOM 元素。比如数据库里的同一个对象,在两次前端 API 调用
里面会成为不同的对象,但是它们依然有一样的 uid。这时候你就可以提示
track by uid 来让 Angular 知道,这两个对象其实是同一份数据。那么原来这份
数据对应的实例和 DOM 元素都可以复用,只需要更新变动了的部分。或者,
你也可以直接 track by index 的话,
后续重绘是不会比 React 慢多少的。甚至在 dbmonster 测试中,Angular 和
Vue 用了 track by $index 以后都比 React 快: dbmon (注意 Angular 默认版本
无优化,优化过的在下面)
顺道说一句,React 渲染列表的时候也需要提供 key 这个特殊 prop,本质上
和 track-by 是一回事。
- 性能比较也要看场合
在比较性能的时候,要分清楚初始渲染、小量数据更新、大量数据更新这些不
同的场合。Virtual DOM、脏检查 MVVM、数据收集 MVVM 在不同场合各有不
同的表现和不同的优化需求。Virtual DOM 为了提升小量数据更新时的性能,
也需要针对性的优化,比如 shouldComponentUpdate 或是 immutable data。
初始渲染:Virtual DOM > 脏检查 >= 依赖收集
小量数据更新:依赖收集 >> Virtual DOM + 优化 > 脏检查(无法优化) >
Virtual DOM 无优化
大量数据更新:脏检查 + 优化 >= 依赖收集 + 优化 > Virtual DOM(无
法/无需优化)>> MVVM 无优化
不要天真地以为 Virtual DOM 就是快,diff 不是免费的,batching 么 MVVM 也
能做,而且最终 patch 的时候还不是要用原生 API。在我看来 Virtual DOM 真
正的价值从来都不是性能,而是它 1) 为函数式的 UI 编程方式打开了大门;2)
可以渲染到 DOM 以外的 backend,比如 ReactNative。
- 总结
以上这些比较,更多的是对于框架开发研究者提供一些参考。主流的框架 + 合
理的优化,足以应对绝大部分应用的性能需求。如果是对性能有极致需求的特
殊情况,其实应该牺牲一些可维护性采取手动优化:比如 Atom 编辑器在文件
渲染的实现上放弃了 React 而采用了自己实现的 tile-based rendering;又比如
在移动端需要 DOM-pooling 的虚拟滚动,不需要考虑顺序变化,可以绕过框架
的内置实现自己搞一个。
第 26 题:浏览器缓存读
可以分成 Service Worker、Memory Cache、Disk Cache 和 Push Cache,那请求
的时候 from memory cache 和 from disk cache 的依据是什么,哪些数据什么
时候存放在 Memory Cache 和 Disk Cache 中?
www.jianshu.com/p/54cc04190…
第 27 题:使用迭代的方式实现 flatten 函数。
var arr=[1,2,3,[4,5],[6,[7,[8]]]]/** * 使用递归的方式处理 * wrap 内保
存结果 ret * 返回一个递归函数 * * @returns */function wrap(){
var ret=[];
return function flat(a){
for(var item of
a){ if(item.constructor===Array){
ret.concat(flat(item))
}else{
ret.push(item)
}
}
return ret
}}console.log(wrap()(arr));
第 28 题:为什么 Vuex 的 mutation 和 Redux 的 reducer 中不能做异步操作?
Mutation 必须是同步函数一条重要的原则就是要记住 mutation 必须是同步
函数。为什么?请参考下面的例子:
mutations: { someMutation (state) { api.callAsyncMethod(() =>
{ state.count++ }) }}
现在想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。
每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。
然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因
为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时
候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是
不可追踪的。
在组件中提交 Mutation 你可以在组件中使用 this.store.commit('xxx') 提交
mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为
store.commit 调用(需要在根节点注入 store)。
import { mapMutations } from 'vuex'export default {
// ... methods: {
...mapMutations([
'increment', // 将 `this.increment()` 映射为
`this.store.commit('increment')//mapMutations也支持载荷: 'incrementBy' // 将this.incrementBy(amount)映射为this.store.commit('incrementBy',
amount)`
]),
...mapMutations({
add: 'increment' // 将 `this.add()` 映射为
`this.store.commit('increment')`
})
}
}
第 29题:(京东)下面代码中 a 在什么情况下会打印 1?第 39 题:介绍下 BFC 及其应用
var a = ?;
if(a == 1 && a == 2 && a == 3){
console.log(1);
}
答:
var a = {
i: 1,
toString() {
return a.i++;
}}if( a == 1 && a == 2 && a == 3 ) {
console.log(1);
}let a = [1,2,3];
a.toString = a.shift;if( a == 1 && a == 2 && a == 3 ) {
console.log(1);}
第 30 题:在 Vue 中,子组件为何不可以修改父组件传递的 Prop
Prop
如果修改了,Vue 是如何监控到属性的修改并给出警告的。
1、子组件为何不可以修改父组件传递的 Prop 单向数据流,易于监测数据的流
动,出现了错误可以更加迅速的定位到错误发生的位置。
2、如果修改了,Vue 是如何监控到属性的修改并给出警告的。
if (process.env.NODE_ENV !== 'production') {
var hyphenatedKey = hyphenate(key);
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
(""" + hyphenatedKey + "" is a reserved attribute and cannot
be used as component prop."),
Vm
);
}
defineReactive$$1(props, key, value, function () {
if (!isRoot && !isUpdatingChildComponent)
{
warn(
"Avoid mutating a prop directly since the value will be " +
"overwritten whenever the parent component re-renders. " +
"Instead, use a data or computed property based on the prop's " +
"value. Prop being mutated: "" + key + """,
Vm
);
}
});
}
在 initProps 的时候,在 defineReactive 时通过判断是否在开发环境,如果是开
发环境,会在触发 set 的时候判断是否此 key 是否处于 updatingChildren 中被修
改,如果不是,说明此修改来自子组件,触发 warning 提示。
需要特别注意的是,当你从子组件修改的 prop 属于基础类型时会触发提示。这
种情况下,你是无法修改父组件的数据源的, 因为基础类型赋值时是值拷贝。
你直接将另一个非基础类型(Object, array)赋值到此 key 时也会触发提示(但实
际上不会影响父组件的数据源), 当你修改 object 的属性时不会触发提示,并
且会修改父组件数据源的数据。
第 31 题:下面代码输出什么
var a = 10;(function () {
console.log(a)
a = 5
console.log(window.a)
var a = 20;
console.log(a)})()
分别为 undefined 10 20,原因是作用域问题,在内部声名 var a = 20;相当于
先声明 var a;然后再执行赋值操作,这是在IIFE内形成的独立作用域,如果
把 var a=20 注释掉,那么 a 只有在外部有声明,显示的就是外部的A变量的值
了。结果A会是 10 5 5
第 32 题:实现一个 sleep 函数
比如 sleep(1000) 意味着等待 1000 毫秒,可从 Promise、Generator、Async/Await
等角度实现
const sleep = (time) => {
return new Promise(resolve => setTimeout(resolve,
time))}sleep(1000).then(() => {
// 这里写你的骚操作})
第 33 题:使用 sort() 对数组[3, 15, 8, 29, 102, 22] 进行排序,输出结果
输出:[102, 15, 22, 29, 3, 8]
解析:根据 MDN 上对 Array.sort()的解释,默认的排序方法会将数组元素转换
为字符串,然后比较字符串中字符的 UTF-16 编码顺序来进行排序。所以'102' 会
排在 '15' 前面。
第 34 题:介绍 HTTPS 握手过程
第 35 题:HTTPS 握手过程中,客户端如何验证证书的合法性
校验证书的颁发机构是否受客户端信任。
2. 通过 CRL 或 OCSP 的方式校验证书是否被吊销。
3. 3 对比系统时间,校验证书是否在有效期内。
4. 通过校验对方是否存在证书的私钥,判断证书的网站域名是否与证书颁
发的域名一致。
第 36 题:输出以下代码执行的结果并解释为什么
var obj = {
'2': 3,
'3': 4,
'length': 2,
'splice': Array.prototype.splice,
'push':
Array.prototype.push}obj.push(1)obj.push(2)console.log(obj)
结果:[,,1,2], length 为 4
伪数组(ArrayLike)
第 37 题:双向绑定和 vuex 是否冲突
在严格模式下直接使用确实会有问题。解决方案:
computed: {
message: {
set (value)
{
this.store.dispatch('updateMessage', value);
},
get () {
Return
this.store.state.obj.message
}
}}mutations: {
UPDATE_MESSAGE (state, v) {
state.obj.message = v;
}}actions: {
update_message ({ commit }, v) {
commit('UPDATE_MESSAGE', v);
}
}
第 38 题:call 和 apply 的区别是什么,哪个性能更好一些
1.Function.prototype.apply 和Function.prototype.call 的作用是一样的,区
别在于传入参数的不同;
2.第一个参数都是,指定函数体内this 的指向;
3.3 第二个参数开始不同,apply 是传入带下标的集合,数组或者类数组,
apply 把它传给函数作为参数,call 从第二个开始传入的参数是不固定的,都会
传给函数作为参数。
4.call 比apply 的性能要好,平常可以多用call,call 传入参数的格式正是内
部所需要的格式
第 39 题:为什么通常在发送数据埋点请求的时候使用的是 1x1 像素的透明 gif 图片?
-
没有跨域问题,一般这种上报数据,代码要写通用的;(排除 ajax)
-
不会阻塞页面加载,影响用户的体验,只要 new Image 对象就好了;(排
除 JS/CSS 文件资源方式上报)
-
在所有图片中,体积最小;(比较 PNG/JPG)
第 40 题:Vue 的响应式原理中 Object.defineProperty 有什么缺陷
为什么在Vue3.0 采用了Proxy,抛弃了Object.defineProperty?
Object.defineProperty 无法监控到数组下标的变化,导致通过数组下标添
加元素,不能实时响应;
Object.defineProperty 只能劫持对象的属性,从而需要对每个对象,每个
属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy 可以劫持整个对
象,并返回一个新的对象。
Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
第 41 题:冒泡排序如何实现,时间复杂度是多少, 还可以如何改进?
冒泡算法的原理:
升序冒泡: 两次循环,相邻元素两两比较,如果前面的大于后面的就交换位置
降序冒泡: 两次循环,相邻元素两两比较,如果前面的小于后面的就交换位置
js 实现:
// 升序冒泡 function maopao(arr){
const array = [...arr] for(let i = 0, len = array.length; i < len -
1; i++){
for(let j = i + 1; j < len; j++) {
if (array[i] > array[j]) {
let temp = array[i]
array[i] = array[j]
array[j] = temp
} } }return array }
看起来没问题,不过一般生产环境都不用这个,原因是效率低下,冒泡排序在
平均和最坏情况下的时间复杂度都是 O(n^2),最好情况下都是 O(n),空间复
杂度是 O(1)。因为就算你给一个已经排好序的数组,如[1,2,3,4,5,6] 它也会走
一遍流程,白白浪费资源。所以有没有什么好的解决方法呢?
答案是肯定有的:加个标识,如果已经排好序了就直接跳出循环。
优化版:
function maopao(arr){
const array = [...arr]
let isOk = true for(let i = 0, len = array.length;
i < len - 1; i++){
for(let j = i + 1; j < len; j++) {
if (array[i] > array[j]) {
let temp = array[i]
array[i] = array[j]
array[j] = temp
isOk = false
}
}
if(isOk){
Break
}
}
return array}
测试: 数组:[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
从测试结果来看: 普通冒泡排序
时间:0.044ms 优化后冒泡排序时间:0.018ms
第 42 题:某公司 1 到 12 月份的销售额存在一个对象里面
如下:{1:222, 2:123, 5:888},请把数据处理为如下结构:[222, 123, null, null, 888, null, null, null, null, null, null, null]。
let obj = {1:222, 2:123, 5:888};
const result = Array.from({ length: 12 }).map((_, index) => obj[index +
1] || null);
console.log(result)
第 43 题:要求设计 LazyMan 类,实现以下功能。
LazyMan('Tony');
// Hi I am Tony
LazyMan('Tony').sleep(10).eat('lunch');
// Hi I am Tony
// 等待了 10 秒...
// I am eating
lunchLazyMan('Tony').eat('lunch').sleep(10).eat('dinner');
// Hi I am Tony
// I am eating lunch// 等待了 10 秒...
// I am eating
dinerLazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(1
0).eat('junk food');
// Hi I am Tony// 等待了 5 秒...
// I am eating lunch
// I am eating dinner
// 等待了 10 秒...
// I am eating junk food
答:
class LazyManClass {
constructor(name) {
this.name = name
this.queue = []
console.log(Hi I am ${name})
setTimeout(() => {
this.next()
},0)
}
sleepFirst(time) {
const fn = () => {
setTimeout(() => {
console.log(等待了${time}秒...)
this.next()
}, time)
}
this.queue.unshift(fn)
return this
}
sleep(time) {
const fn = () => {
setTimeout(() => {
console.log(等待了${time}秒...)
this.next()
},time)
}
this.queue.push(fn)
return this
}
eat(food) {
const fn = () => {
console.log(I am eating ${food})
this.next()
}
this.queue.push(fn)
return this
}
next() {
const fn = this.queue.shift()
fn && fn() }}function LazyMan(name) {
return new LazyManClass(name)}
第 44 题:分析比较 opacity: 0、visibility: hidden、display: none 优劣和适用场景
display: none (不占空间,不能点击)(场景,显示出原来这里不存在的结
构)
visibility: hidden(占据空间,不能点击)(场景:显示不会导致页面结
构发生变动,不会撑开)
opacity: 0(占据空间,可以点击)(场景:可以跟 transition 搭配)
第 45 题:箭头函数与普通函数(function)的区别是什么?构造函数(function)可以使用 new 生成实例,那么箭头函数可以吗?为什么?
箭头函数是普通函数的简写,可以更优雅的定义一个函数,和普通函数相比,
有以下几点差异:
函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对
象。
不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可
以用 rest 参数代替。
不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。
不可以使用 new 命令,因为:
o 1.没有自己的 this,无法调用 call,apply。
o 2.没有 prototype 属性 ,而 new 命令在执行时需要将构造函数
的 prototype 赋值给新的对象的 proto new 过程大致是这样的:
function newFunc(father, ...rest) {
var result = {};
result.proto = father.prototype;
var result2 = father.apply(result, rest);
if (
(typeof result2 === 'object' || typeof result2 === 'function') &&
result2 !== null
) {
return result2;
}
return result;
}
第 46 题:给定两个数组,写一个方法来计算它们的交集。
例如:给定 nums1 = [1, 2, 2, 1],nums2 = [2, 2],返回 [2, 2]。
var nums1 = [1, 2, 2, 1], nums2 = [2, 2, 3, 4];
// 1.
// 有个问题,
[NaN].indexOf(NaN) === -1var newArr1 = nums1.filter(function(item) {
return nums2.indexOf(item) > -1;
});
console.log(newArr1);
// 2.
var newArr2 = nums1.filter((item) => {
return nums2.includes(item);
});
console.log(newArr2);
第 47 题:已知如下代码,如何修改才能让图片宽度为 300px ?注意下面代码不可修改。
<img src="1.jpg" style="width:480px!important;”>
答:
max-width: 300pxtransform: scale(0.625,0.625)
第 48 题:介绍下如何实现 token 加密
jwt 举例:
- 需要一个 secret(随机数)
- 后端利用 secret 和加密算法(如:HMAC-SHA256)对 payload(如账号密码)
生成一个字符串(token),返回前端
- 前端每次 request 在 header 中带上 token
- 后端用同样的算法解密
第 49 题:redux 为什么要把 reducer 设计成纯函数
redux 的设计思想就是不产生副作用,数据更改的状态可回溯,所以 redux 中处
处都是纯函数
第 50 题:如何设计实现无缝轮播
blog.csdn.net/weixin_3406…
第 51 题:模拟实现一个 Promise.finally
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(()
=> { throw reason })
);
};
第 52 题: a.b.c.d 和 a['b']['c']['d'],哪个性能更高?
应该是 a.b.c.d 比 a['b']['c']['d'] 性能高点,后者还要考虑 [ ] 中是变量的情况,
再者,从两种形式的结构来看,显然编译器解析前者要比后者容易些,自然也
就快一点。
第 53 题:ES6 代码转成 ES5 代码的实现思路是什么
ES6 转 ES5 目前行业标配是用 Babel,转换的大致流程如下:
-
解析:解析代码字符串,生成 AST;
-
转换:按一定的规则转换、修改 AST;
-
生成:将修改后的 AST 转换成普通代码。
如果不用工具,纯人工的话,就是使用或自己写各种 polyfill 了。
第 54 题: 如何解决移动端 Retina 屏 1px 像素问题
-
伪元素 + transform scaleY(.5)
-
border-image
-
background-image
-
box-shadow
第 55 题: 如何把一个字符串的大小写取反(大写变小写小写变大写),例如 ’AbC' 变成 'aBc' 。
function processString(s) {
var arr = s.split('');
var new_arr = arr.map((item) => {
return item === item.toUpperCase() ? item.toLowerCase() :
item.toUpperCase();
});
Return
new_arr.join('');
} console.log(processString('AbC')); function
swapString(str) {
var result = ''
for (var i = 0; i < str.length; i++) {
var c = str[i]
if (c === c.toUpperCase()) {
result += c.toLowerCase()
} else {
result += c.toUpperCase()
}
}
return result
} swapString('ADasfads123!@!@#') // =>
'adASFADS123!@!@#'
第 56 题: 介绍下 webpack 热更新原理,是如何做到在不刷新浏览器的前提下更新页面的
- 当修改了一个或多个文件;
- 文件系统接收更改并通知 webpack;
- webpack 重新编译构建一个或多个模块,并通知 HMR 服务器进行更新;
- HMR Server 使用 webSocket 通知 HMR runtime 需要更新,HMR 运行时
通过 HTTP 请求更新 jsonp;
- HMR 运行时替换更新中的模块,如果确定这些模块无法更新,则触发整
个页面刷新。
第 57 题: 实现一个字符串匹配算法,从长度为 n 的字符串 S 中,查找是否存在字符串 T,T 的长度是 m,若存在返回所在位置。
const find = (S, T) => {
if (S.length < T.length) return -1
for (let i = 0; i < S.length; i++) {
if (S.slice(i, i + T.length) === T) return i
}
return -1
第 58 题: 为什么普通 for 循环的性能远远高于 forEach 的性能,请解释其中的原因。
for 循环没有任何额外的函数调用栈和上下文;
forEach 函数签名实际上是
array.forEach(function(currentValue, index, arr), thisValue)
它不是普通的 for 循环的语法糖,还有诸多参数和上下文需要在执行的时候考
虑进来,这里可能拖慢性能;
第 59 题: 介绍下 BFC、IFC、GFC 和 FFC
BFC(Block formatting contexts):
块级格式上下文页面上的一个隔离的渲染区域,那么他是如何产生的呢?可以
触发 BFC 的元素有 float、position、overflow、display:table-cell/
inline-block/table-caption ;BFC 有什么作用呢?比如说实现多栏布局’ IFC(Inline formatting contexts):
内联格式上下文 IFC 的 line box(线框)高度由其包含行内元素中最高的实际高
度计算而来(不受到竖直方向的 padding/margin 影响)IFC 中的 line box 一般左右
都贴紧整个 IFC,但是会因为 float 元素而扰乱。float 元素会位于 IFC 与与 line box
之间,使得 line box 宽度缩短。 同个 ifc 下的多个 line box 高度会不同 IFC 中时
不可能有块级元素的,当插入块级元素时(如 p 中插入 div)会产生两个匿名块
与 div 分隔开,即产生两个 IFC,每个 IFC 对外表现为块级元素,与 div 垂直排
列。那么 IFC 一般有什么用呢?水平居中:当一个块要在环境中水平居中时,
设置其为 inline-block 则会在外层产生 IFC,通过 text-align 则可以使其水平居中。
垂直居中:创建一个 IFC,用其中一个元素撑开父元素的高度,然后设置其
vertical-align:middle,其他行内元素则可以在此父元素下垂直居中。
GFC(GrideLayout formatting contexts):
网格布局格式化上下文当为一个元素设置 display 值为 grid 的时候,此元素将会
获得一个独立的渲染区域,我们可以通过在网格容器(grid container)上定义
网格定义行(grid definition rows)和网格定义列(grid definition columns)属性
各在网格项目(grid item)上定义网格行(grid row)和网格列(grid columns)
为每一个网格项目(grid item)定义位置和空间。那么 GFC 有什么用呢,和 table
又有什么区别呢?首先同样是一个二维的表格,但 GridLayout 会有更加丰富的
属性来控制行列,控制对齐以及更为精细的渲染语义和控制。
FFC(Flex formatting contexts):
自适应格式上下文 display 值为 flex 或者 inline-flex 的元素将会生成自适应容器
(flex container),可惜这个牛逼的属性只有谷歌和火狐支持,不过在移动端
也足够了,至少 safari 和 chrome 还是 OK 的,毕竟这俩在移动端才是王道。Flex
Box 由伸缩容器和伸缩项目组成。通过设置元素的 display 属性为 flex 或
inline-flex 可以得到一个伸缩容器。设置为 flex 的容器被渲染为一个块级元素,
而设置为 inline-flex 的容器则渲染为一个行内元素。伸缩容器中的每一个子元
素都是一个伸缩项目。伸缩项目可以是任意数量的。伸缩容器外和伸缩项目内
的一切元素都不受影响。简单地说,Flexbox 定义了伸缩容器内伸缩项目该如
何布局。
第 60 题: 使用 JavaScript Proxy 实现简单的数据绑定
TODO:
Add To Todo List
const input = document.getElementById('input')
const text = document.getElementById('text')
const list = document.getElementById('list')
const btn = document.getElementById('btn')
let render
const inputObj = new Proxy({}, {
get (target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set (target, key, value, receiver) {
if (key === 'text') {
input.value = value
text.innerHTML = value
}
return Reflect.set(target, key, value,
receiver)
}
})
class Render {
constructor (arr) {
this.arr = arr
}
init () {
const fragment = document.createDocumentFragment()
for (let i = 0; i < this.arr.length; i++) {
const li = document.createElement('li')
li.textContent = this.arr[i]
fragment.appendChild(li)
}
list.appendChild(fragment)
}
addList (val) {
const li = document.createElement('li')
li.textContent = val
list.appendChild(li)
}
}
const todoList = new Proxy([], {
get (target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set (target, key, value, receiver) {
if (key !== 'length')
{
render.addList(value)
}
return Reflect.set(target, key, value,
receiver)
}
})
window.onload = () => {
render = new Render([])
render.init()
}
input.addEventListener('keyup', e => {
inputObj.text = e.target.value
})
btn.addEventListener('click', () =>
{
todoList.push(inputObj.text)
inputObj.text = ''
})
第 61 题:数组里面有 10 万个数据,取第一个元素和第 10 万个元素的时间相差多少
数组可以直接根据索引取的对应的元素,所以不管取哪个位置的元素的时间复
杂度都是 O(1)
得出结论:消耗时间几乎一致,差异可以忽略不计
第 62 题:输出以下代码运行结果
// example 1
var a={}, b='123', c=123;
a[b]='b';a[c]='c';
console.log(a[b]);
// example 2var a={}, b=Symbol('123'), c=Symbol('123');
a[b]='b';
a[c]='c';
console.log(a[b]);
// example 3var a={}, b={key:'123'}, c={key:'456'};
a[b]='b';a[c]='c';
console.log(a[b]);
答:
-
对象的键名只能是字符串和 Symbol 类型。
-
其他类型的键名会被转换成字符串类型。
-
对象转字符串默认会调用 toString 方法。
// example 1
var a={}, b='123', c=123;a[b]='b';
// c 的键名会被转换成字符串'123',这里会把 b 覆盖掉。a[c]='c';
// 输出 cconsole.log(a[b]);
// example 2var a={}, b=Symbol('123'), c=Symbol('123');
// b 是 Symbol 类型,不需要转换。a[b]='b';
// c 是 Symbol 类型,不需要转换。任何一个 Symbol 类型的值都是不相等的,
所以不会覆盖掉 b。a[c]='c';
// 输出 bconsole.log(a[b]);
// example 3var a={}, b={key:'123'}, c={key:'456'};
// b 不是字符串也不是 Symbol 类型,需要转换成字符串。
// 对象类型会调用 toString 方法转换成字符串 [object Object]。a[b]='b';
// c 不是字符串也不是 Symbol 类型,需要转换成字符串。
// 对象类型会调用 toString 方法转换成字符串 [object Object]。这里会把
b 覆盖掉。a[c]='c';
// 输出 cconsole.log(a[b]);
第 63 题:Vue 的父组件和子组件生命周期钩子执行顺序是什么
-
父组建: beforeCreate -> created -> beforeMount
-
子组件: -> beforeCreate -> created -> beforeMount -> mounted
-
父组件: -> mounted
-
总结:从外到内,再从内到外
第 64 题:input 搜索如何防抖,如何处理中文输入
第 65 题:介绍下 Promise.all 使用、原理实现及错误处理
const p = Promise.all([p1, p2, p3]);
Promise.all 方法接受一个数组作为参数,p1、p2、p3 都是 Promise 实例,如果
不是,就会先调用下面讲到的 Promise.resolve 方法,将参数转为 Promise 实例,
再进一步处理。(Promise.all 方法的参数可以不是数组,但必须具有 Iterator 接
口,且返回的每个成员都是 Promise 实例。)
第 66 题:var 、let 和 const 区别的实现原理是什么
var 和 let 用以声明变量,const 用于声明只读的常量;
var 和 let 用以声明变量,const 用于声明只读的常量;
var 声明的变量,不存在块级作用域,在全局范围内都有效,let 和 const
声明的,只在它所在的代码块内有效;
let 和 const 不存在像 var 那样的 “变量提升” 现象,所以 var 定义变
量可以先使用,后声明,而 let 和 const 只可先声明,后使用;
let 声明的变量存在暂时性死区,即只要块级作用域中存在 let,那么它
所声明的变量就绑定了这个区域,不再受外部的影响。
let 不允许在相同作用域内,重复声明同一个变量;
const 在声明时必须初始化赋值,一旦声明,其声明的值就不允许改变,
更不允许重复声明;如 const 声明了一个复合类型的常量,其存储的是一个引
用地址,不允许改变的是这个地址,而对象本身是可变的。
变量与内存之间的关系,主要由三个部分组成:
变量名
内存地址
内存空间
JS 引擎在读取变量时,先找到变量绑定的内存地址,然后找到地址所指向的内
存空间,最后读取其中的内容。当变量改变时,JS 引擎不会用新值覆盖之前旧
值的内存空间(虽然从写代码的角度来看,确实像是被覆盖掉了),而是重新
分配一个新的内存空间来存储新值,并将新的内存地址与变量进行绑定,JS 引
擎会在合适的时机进行 GC,回收旧的内存空间。
const 定义变量(常量)后,变量名与内存地址之间建立了一种不可变的绑定
关系,阻隔变量地址被改变,当 const 定义的变量进行重新赋值时,根据前面
的论述,JS 引擎会尝试重新分配新的内存空间,所以会被拒绝,便会抛出异常。
第 67 题:请实现一个 add 函数,满足以下功能。
add(1);
// 1add(1)(2);
// 3add(1)(2)(3);
// 6add(1)(2, 3);
// 6add(1, 2)(3);
// 6add(1, 2, 3);
// 6
答:
实现 1:
function currying(fn, length) {
length = length || fn.length; // 注释 1
return function (...args) { // 注释 2 return
args.length >= length // 注释 3
? fn.apply(this, args) // 注释 4
: currying(fn.bind(this, ...args), length - args.length) // 注释
5 }}
实现 2:
const currying = fn =>
judge = (...args) =>
args.length >= fn.length
? fn(...args)
: (...arg) => judge(...args, ...arg)
其中注释部分
注释 1:第一次调用获取函数 fn 参数的长度,后续调用获取 fn 剩余参数的
长度
注释 2:currying 包裹之后返回一个新函数,接收参数为 ...args
注释 3:新函数接收的参数长度是否大于等于 fn 剩余参数需要接收的长度
注释 4:满足要求,执行 fn 函数,传入新函数的参数
注释 5:不满足要求,递归 currying 函数,新的 fn 为 bind 返回的新函数(bind
绑定了 ...args 参数,未执行),新的 length 为 fn 剩余参数的长度
第 70 题:在输入框中如何判断输入的是一个正确的网址。
function isUrl(url) {
const a = document.createElement("a");
a.href = url;
return (
[
/^(http|https):{url}`
].find(x => !x) === undefined
);
}
第 71 题:vue 在 v -for 时给每项元素绑定事件需要用事件代理吗?为什么?
在 vue 中 vue 做了处理
如果我们自己在非 vue 中需要对很多元素添加事件的时候,可以通过将事件添
加到它们的父节点而将事件委托给父节点来触发处理函数
第 72 题:模拟实现一个深拷贝,并考虑对象相互引用以及 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)].red
uce((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)的操作
第 73 题:介绍下前端加密的常见场景和方法
首先,加密的目的,简而言之就是将明文转换为密文、甚至转换为其他的东西,
用来隐藏明文内容本身,防止其他人直接获取到敏感明文信息、或者提高其他
人获取到明文信息的难度。通常我们提到加密会想到密码加密、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
这时,如果你想让用户看到“1
观察者模式中主体和观察者是互相感知的,发布-订阅模式是借助第三方来实现 调度的,发布者和订阅者之间互不感知 第 18题:聊聊 Redux 和 Vuex 的设计思想
不管是 Vue,还是 React,都需要管理状态(state),比如组件之间都有共享 状态的需要。什么是共享状态?比如一个组件需要使用另一个组件的状态,或 者一个组件需要改变另一个组件的状态,都是共享状态。 父子组件之间,兄弟组件之间共享状态,往往需要写很多没有必要的代码,比 如把状态提升到父组件里,或者给兄弟组件写一个父组件,听听就觉得挺啰嗦。 如果不对状态进行有效的管理,状态在什么时候,由于什么原因,如何变化就 会不受控制,就很难跟踪和测试了。如果没有经历过这方面的困扰,可以简单 理解为会搞得很乱就对了。 在软件开发里,有些通用的思想,比如隔离变化,约定优于配置等,隔离变化 就是说做好抽象,把一些容易变化的地方找到共性,隔离出来,不要去影响其 他的代码。约定优于配置就是很多东西我们不一定要写一大堆的配置,比如我 们几个人约定,view 文件夹里只能放视图,不能放过滤器,过滤器必须放到 filter 文件夹里,那这就是一种约定,约定好之后,我们就不用写一大堆配置文 件了,我们要找所有的视图,直接从 view 文件夹里找就行。 根据这些思想,对于状态管理的解决思路就是:把组件之间需要共享的状态抽 取出来,遵循特定的约定,统一来管理,让状态的变化可以预测。根据这个思 路,产生了很多的模式和库,我们来挨个聊聊。 第 19题:说说浏览器和 Node 事件循环的区别
其中一个主要的区别在于浏览器的 event loop 和 nodejs 的 event loop 在处理异 步事件的顺序是不同的,nodejs 中有 micro event;其中 Promise 属于 micro event 该异步事件的处理顺序就和浏览器不同.nodejs V11.0 以上 这两者之间的顺序 就相同了. function test () { console.log('start') setTimeout(() => { console.log('children2') Promise.resolve().then(() => {console.log('children2-1')}) }, 0) setTimeout(() => { console.log('children3') Promise.resolve().then(() => {console.log('children3-1')}) }, 0) Promise.resolve().then(() => {console.log('children1')}) console.log('end') }test()// 以上代码在 node11 以下版本的执行结果(先执行所有的宏任 务,再执行微任务)// start// end// children1// children2// children3// children2-1// children3-1// 以上代码在 node11 及浏览器的执行结果(顺序执 行宏任务和微任务)// start// end// children1// children2// children2-1// children3// children3-1 第 20 题:介绍模块化发展历程
blog.csdn.net/dadadeganhu… 第 21 题:全局作用域中,用 const 和 let 声明的变量不在 window 上,那到底在哪里?如何去获取?
在 ES5 中,顶层对象的属性和全局变量是等价的,var 命令和 function 命令声 明的全局变量,自然也是顶层对象。 var a = 12; function f(){}; console.log(window.a); // 12console.log(window.f); // f(){} 但 ES6 规定,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属 性,但 let 命令、const 命令、class 命令声明的全局变量,不属于顶层对象的属 性。 let aa = 1; const bb = 2; console.log(window.aa); // undefinedconsole.log(window.bb); // undefined 在哪里?怎么获取?通过在设置断点,看看浏览器是怎么处理的: 通过上图也可以看到,在全局作用域中,用 let 和 const 声明的全局变量并没 有在全局对象中,只是一个块级作用域(Script)中 怎么获取?在定义变量的块级作用域中就能获取啊,既然不属于顶层对象,那 就不加 window(global)呗。 let aa = 1; const bb = 2; console.log(aa); // 1console.log(bb); // 2 第 22 题:cookie 和 token 都存放在 header 中,为什么不会劫持 token?
1、攻击者通过 xss 拿到用户的 cookie 然后就可以伪造 cookie 了。 2、或者通过 csrf 在同个浏览器下面通过浏览器会自动带上 cookie 的特性 在通过 用户网站-攻击者网站-攻击者请求用户网站的方式 浏览器会自动带上 cookie 但是 token 1、不会被浏览器带上 问题 2 解决 2、token 是放在 jwt 里面下发给客户端的 而且不一定存储在哪里 不能通过 document.cookie 直接拿到,通过 jwt+ip 的方式 可以防止 被劫持 即使被劫持 也是无效的 jwt 第 23 题:聊聊 Vue 的双向数据绑定,Model 如何改变 View,View 又是如何改变 Model
1、从 M 到 V 的映射(Data Binding),这样可以大量节省你人肉来 update View 的代码 2、从 V 到 M 的事件监听(DOM Listeners),这样你的 Model 会随着 View 触发事件而改变 24 题:两个数组合并成一个数组
方法一 var a=[1,2,3]; var b=[4,5,6]; var c=a.concat(b); alert(c); 方法二 es6 var newarr=[...a,...b] 第 25 题:Virtual DOM 真的比操作原生 DOM 快吗?谈谈你的想法
- 原生 DOM 操作 vs. 通过框架封装操作。 这是一个性能 vs. 可维护性的取舍。框架的意义在于为你掩盖底层的 DOM 操 作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护。 没有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的 DOM 操作层 需要应对任何上层 API 可能产生的操作,它的实现必须是普适的。针对任何一 个 benchmark,我都可以写出比任何框架更快的手动优化,但是那有什么意义 呢?在构建一个实际应用的时候,你难道为每一个地方都去做手动优化吗?出 于可维护性的考虑,这显然不可能。框架给你的保证是,你在不需要手动优化 的情况下,我依然可以给你提供过得去的性能。
- 对 React 的 Virtual DOM 的误解。 React 从来没有说过 “React 比原生操作 DOM 快”。React 的基本思维模式是 每次有变动就整个重新渲染整个应用。如果没有 Virtual DOM,简单来想就是 直接重置 innerHTML。很多人都没有意识到,在一个大型列表所有数据都变了 的情况下,重置 innerHTML 其实是一个还算合理的操作... 真正的问题是在 “全部重新渲染” 的思维模式下,即使只有一行数据变了,它也需要重置整个 innerHTML,这时候显然就有大量的浪费。 我们可以比较一下 innerHTML vs. Virtual DOM 的重绘性能消耗: innerHTML: render html string O(template size) + 重新创建所有 DOM 元 素 O(DOM size) Virtual DOM: render Virtual DOM + diff O(template size) + 必要的 DOM 更新 O(DOM change) Virtual DOM render + diff 显然比渲染 html 字符串要慢,但是!它依然是纯 js 层面的计算,比起后面的 DOM 操作来说,依然便宜了太多。可以看到, innerHTML 的总计算量不管是 js 计算还是 DOM 操作都是和整个界面的大小 相关,但 Virtual DOM 的计算量里面,只有 js 计算和界面大小相关,DOM 操 作是和数据的变动量相关的。前面说了,和 DOM 操作比起来,js 计算是极其 便宜的。这才是为什么要有 Virtual DOM:它保证了 1)不管你的数据变化多 少,每次重绘的性能都可以接受;2) 你依然可以用类似 innerHTML 的思路去 写你的应用。
- MVVM vs. Virtual DOM 相比起 React,其他 MVVM 系框架比如 Angular, Knockout 以及 Vue、Avalon 采用的都是数据绑定:通过 Directive/Binding 对象,观察数据变化并保留对实 际 DOM 元素的引用,当有数据变化时进行对应的操作。MVVM 的变化检查是 数据层面的,而 React 的检查是 DOM 结构层面的。 MVVM 的性能也根据变动检测的实现原理有所不同:Angular 的脏检查使得任 何变动都有固定的 O(watcher count) 的代价;Knockout/Vue/Avalon 都采用了依 赖收集,在 js 和 DOM 层面都是 O(change): 脏检查:scope digest O(watcher count) + 必要 DOM 更新 O(DOM change) 依赖收集:重新收集依赖 O(data change) + 必要 DOM 更新 O(DOM change)可以看到,Angular 最不效率的地方在于任何小变动都有的和 watcher 数量相关的性能代价。但是!当所有数据都变了的时候,Angular 其实并不吃 亏。依赖收集在初始化和数据变化的时候都需要重新收集依赖,这个代价在小 量更新的时候几乎可以忽略,但在数据量庞大的时候也会产生一定的消耗。 MVVM 渲染列表的时候,由于每一行都有自己的数据作用域,所以通常都是每 一行有一个对应的 ViewModel 实例,或者是一个稍微轻量一些的利用原型继 承的 "scope" 对象,但也有一定的代价。所以,MVVM 列表渲染的初始化几乎 一定比 React 慢,因为创建 ViewModel / scope 实例比起 Virtual DOM 来说要 昂贵很多。 这里所有 MVVM 实现的一个共同问题就是在列表渲染的数据源变动时,尤其 是当数据是全新的对象时,如何有效地复用已经创建的 ViewModel 实例和 DOM 元素。 假如没有任何复用方面的优化,由于数据是 “全新” 的,MVVM 实际上需要销 毁之前的所有实例,重新创建所有实例,最后再进行一次渲染!这就是为什么 题目里链接的 angular/knockout 实现都相对比较慢。相比之下,React 的变动 检查由于是 DOM 结构层面的,即使是全新的数据,只要最后渲染结果没变, 那么就不需要做无用功。 Angular 和 Vue 都提供了列表重绘的优化机制,也就是 “提示” 框架如何有效 地复用实例和 DOM 元素。比如数据库里的同一个对象,在两次前端 API 调用 里面会成为不同的对象,但是它们依然有一样的 uid。这时候你就可以提示 track by uid 来让 Angular 知道,这两个对象其实是同一份数据。那么原来这份 数据对应的实例和 DOM 元素都可以复用,只需要更新变动了的部分。或者, 你也可以直接 track by index 的话, 后续重绘是不会比 React 慢多少的。甚至在 dbmonster 测试中,Angular 和 Vue 用了 track by $index 以后都比 React 快: dbmon (注意 Angular 默认版本 无优化,优化过的在下面) 顺道说一句,React 渲染列表的时候也需要提供 key 这个特殊 prop,本质上 和 track-by 是一回事。
- 性能比较也要看场合 在比较性能的时候,要分清楚初始渲染、小量数据更新、大量数据更新这些不 同的场合。Virtual DOM、脏检查 MVVM、数据收集 MVVM 在不同场合各有不 同的表现和不同的优化需求。Virtual DOM 为了提升小量数据更新时的性能, 也需要针对性的优化,比如 shouldComponentUpdate 或是 immutable data。 初始渲染:Virtual DOM > 脏检查 >= 依赖收集 小量数据更新:依赖收集 >> Virtual DOM + 优化 > 脏检查(无法优化) > Virtual DOM 无优化 大量数据更新:脏检查 + 优化 >= 依赖收集 + 优化 > Virtual DOM(无 法/无需优化)>> MVVM 无优化 不要天真地以为 Virtual DOM 就是快,diff 不是免费的,batching 么 MVVM 也 能做,而且最终 patch 的时候还不是要用原生 API。在我看来 Virtual DOM 真 正的价值从来都不是性能,而是它 1) 为函数式的 UI 编程方式打开了大门;2) 可以渲染到 DOM 以外的 backend,比如 ReactNative。
- 总结 以上这些比较,更多的是对于框架开发研究者提供一些参考。主流的框架 + 合 理的优化,足以应对绝大部分应用的性能需求。如果是对性能有极致需求的特 殊情况,其实应该牺牲一些可维护性采取手动优化:比如 Atom 编辑器在文件 渲染的实现上放弃了 React 而采用了自己实现的 tile-based rendering;又比如 在移动端需要 DOM-pooling 的虚拟滚动,不需要考虑顺序变化,可以绕过框架 的内置实现自己搞一个。 第 26 题:浏览器缓存读
可以分成 Service Worker、Memory Cache、Disk Cache 和 Push Cache,那请求 的时候 from memory cache 和 from disk cache 的依据是什么,哪些数据什么 时候存放在 Memory Cache 和 Disk Cache 中? www.jianshu.com/p/54cc04190… 第 27 题:使用迭代的方式实现 flatten 函数。
var arr=[1,2,3,[4,5],[6,[7,[8]]]]/** * 使用递归的方式处理 * wrap 内保 存结果 ret * 返回一个递归函数 * * @returns */function wrap(){ var ret=[]; return function flat(a){ for(var item of a){ if(item.constructor===Array){ ret.concat(flat(item)) }else{ ret.push(item) } } return ret }}console.log(wrap()(arr)); 第 28 题:为什么 Vuex 的 mutation 和 Redux 的 reducer 中不能做异步操作?
Mutation 必须是同步函数一条重要的原则就是要记住 mutation 必须是同步
函数。为什么?请参考下面的例子:
mutations: { someMutation (state) { api.callAsyncMethod(() =>
{ state.count++ }) }}
现在想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。
每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。
然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因
为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时
候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是
不可追踪的。
在组件中提交 Mutation 你可以在组件中使用 this.store.commit('xxx') 提交
mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为
store.commit 调用(需要在根节点注入 store)。
import { mapMutations } from 'vuex'export default {
// ... methods: {
...mapMutations([
'increment', // 将 `this.increment()` 映射为
`this.store.commit('increment')//mapMutations也支持载荷: 'incrementBy' // 将this.incrementBy(amount)映射为this.store.commit('incrementBy',
amount)`
]),
...mapMutations({
add: 'increment' // 将 `this.add()` 映射为
`this.store.commit('increment')`
})
}
}
第 29题:(京东)下面代码中 a 在什么情况下会打印 1?第 39 题:介绍下 BFC 及其应用
var a = ?; if(a == 1 && a == 2 && a == 3){ console.log(1); } 答: var a = { i: 1, toString() { return a.i++; }}if( a == 1 && a == 2 && a == 3 ) { console.log(1); }let a = [1,2,3]; a.toString = a.shift;if( a == 1 && a == 2 && a == 3 ) { console.log(1);} 第 30 题:在 Vue 中,子组件为何不可以修改父组件传递的 Prop
Prop 如果修改了,Vue 是如何监控到属性的修改并给出警告的。 1、子组件为何不可以修改父组件传递的 Prop 单向数据流,易于监测数据的流 动,出现了错误可以更加迅速的定位到错误发生的位置。 2、如果修改了,Vue 是如何监控到属性的修改并给出警告的。 if (process.env.NODE_ENV !== 'production') { var hyphenatedKey = hyphenate(key); if (isReservedAttribute(hyphenatedKey) || config.isReservedAttr(hyphenatedKey)) { warn( (""" + hyphenatedKey + "" is a reserved attribute and cannot be used as component prop."), Vm ); } defineReactive$$1(props, key, value, function () { if (!isRoot && !isUpdatingChildComponent) { warn( "Avoid mutating a prop directly since the value will be " + "overwritten whenever the parent component re-renders. " + "Instead, use a data or computed property based on the prop's " + "value. Prop being mutated: "" + key + """, Vm ); } }); } 在 initProps 的时候,在 defineReactive 时通过判断是否在开发环境,如果是开 发环境,会在触发 set 的时候判断是否此 key 是否处于 updatingChildren 中被修 改,如果不是,说明此修改来自子组件,触发 warning 提示。 需要特别注意的是,当你从子组件修改的 prop 属于基础类型时会触发提示。这 种情况下,你是无法修改父组件的数据源的, 因为基础类型赋值时是值拷贝。 你直接将另一个非基础类型(Object, array)赋值到此 key 时也会触发提示(但实 际上不会影响父组件的数据源), 当你修改 object 的属性时不会触发提示,并 且会修改父组件数据源的数据。 第 31 题:下面代码输出什么
var a = 10;(function () { console.log(a) a = 5 console.log(window.a) var a = 20; console.log(a)})() 分别为 undefined 10 20,原因是作用域问题,在内部声名 var a = 20;相当于 先声明 var a;然后再执行赋值操作,这是在IIFE内形成的独立作用域,如果 把 var a=20 注释掉,那么 a 只有在外部有声明,显示的就是外部的A变量的值 了。结果A会是 10 5 5 第 32 题:实现一个 sleep 函数
比如 sleep(1000) 意味着等待 1000 毫秒,可从 Promise、Generator、Async/Await 等角度实现 const sleep = (time) => { return new Promise(resolve => setTimeout(resolve, time))}sleep(1000).then(() => { // 这里写你的骚操作}) 第 33 题:使用 sort() 对数组[3, 15, 8, 29, 102, 22] 进行排序,输出结果
输出:[102, 15, 22, 29, 3, 8] 解析:根据 MDN 上对 Array.sort()的解释,默认的排序方法会将数组元素转换 为字符串,然后比较字符串中字符的 UTF-16 编码顺序来进行排序。所以'102' 会 排在 '15' 前面。 第 34 题:介绍 HTTPS 握手过程
第 35 题:HTTPS 握手过程中,客户端如何验证证书的合法性
校验证书的颁发机构是否受客户端信任。 2. 通过 CRL 或 OCSP 的方式校验证书是否被吊销。 3. 3 对比系统时间,校验证书是否在有效期内。 4. 通过校验对方是否存在证书的私钥,判断证书的网站域名是否与证书颁 发的域名一致。 第 36 题:输出以下代码执行的结果并解释为什么
var obj = { '2': 3, '3': 4, 'length': 2, 'splice': Array.prototype.splice, 'push': Array.prototype.push}obj.push(1)obj.push(2)console.log(obj) 结果:[,,1,2], length 为 4 伪数组(ArrayLike) 第 37 题:双向绑定和 vuex 是否冲突
在严格模式下直接使用确实会有问题。解决方案: computed: { message: { set (value) { this.store.dispatch('updateMessage', value); }, get () { Return this.store.state.obj.message } }}mutations: { UPDATE_MESSAGE (state, v) { state.obj.message = v; }}actions: { update_message ({ commit }, v) { commit('UPDATE_MESSAGE', v); } } 第 38 题:call 和 apply 的区别是什么,哪个性能更好一些
1.Function.prototype.apply 和Function.prototype.call 的作用是一样的,区
别在于传入参数的不同;
2.第一个参数都是,指定函数体内this 的指向;
3.3 第二个参数开始不同,apply 是传入带下标的集合,数组或者类数组,
apply 把它传给函数作为参数,call 从第二个开始传入的参数是不固定的,都会
传给函数作为参数。
4.call 比apply 的性能要好,平常可以多用call,call 传入参数的格式正是内
部所需要的格式
第 39 题:为什么通常在发送数据埋点请求的时候使用的是 1x1 像素的透明 gif 图片?
-
没有跨域问题,一般这种上报数据,代码要写通用的;(排除 ajax)
-
不会阻塞页面加载,影响用户的体验,只要 new Image 对象就好了;(排 除 JS/CSS 文件资源方式上报)
-
在所有图片中,体积最小;(比较 PNG/JPG) 第 40 题:Vue 的响应式原理中 Object.defineProperty 有什么缺陷
为什么在Vue3.0 采用了Proxy,抛弃了Object.defineProperty?
Object.defineProperty 无法监控到数组下标的变化,导致通过数组下标添
加元素,不能实时响应;
Object.defineProperty 只能劫持对象的属性,从而需要对每个对象,每个
属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy 可以劫持整个对
象,并返回一个新的对象。
Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。 第 41 题:冒泡排序如何实现,时间复杂度是多少, 还可以如何改进?
冒泡算法的原理: 升序冒泡: 两次循环,相邻元素两两比较,如果前面的大于后面的就交换位置 降序冒泡: 两次循环,相邻元素两两比较,如果前面的小于后面的就交换位置 js 实现: // 升序冒泡 function maopao(arr){ const array = [...arr] for(let i = 0, len = array.length; i < len - 1; i++){ for(let j = i + 1; j < len; j++) { if (array[i] > array[j]) { let temp = array[i] array[i] = array[j] array[j] = temp } } }return array } 看起来没问题,不过一般生产环境都不用这个,原因是效率低下,冒泡排序在 平均和最坏情况下的时间复杂度都是 O(n^2),最好情况下都是 O(n),空间复 杂度是 O(1)。因为就算你给一个已经排好序的数组,如[1,2,3,4,5,6] 它也会走 一遍流程,白白浪费资源。所以有没有什么好的解决方法呢? 答案是肯定有的:加个标识,如果已经排好序了就直接跳出循环。 优化版: function maopao(arr){ const array = [...arr] let isOk = true for(let i = 0, len = array.length; i < len - 1; i++){ for(let j = i + 1; j < len; j++) { if (array[i] > array[j]) { let temp = array[i] array[i] = array[j] array[j] = temp isOk = false } } if(isOk){ Break } } return array} 测试: 数组:[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20] 从测试结果来看: 普通冒泡排序 时间:0.044ms 优化后冒泡排序时间:0.018ms 第 42 题:某公司 1 到 12 月份的销售额存在一个对象里面
如下:{1:222, 2:123, 5:888},请把数据处理为如下结构:[222, 123, null, null, 888, null, null, null, null, null, null, null]。 let obj = {1:222, 2:123, 5:888}; const result = Array.from({ length: 12 }).map((_, index) => obj[index + 1] || null); console.log(result) 第 43 题:要求设计 LazyMan 类,实现以下功能。
LazyMan('Tony');
// Hi I am Tony
LazyMan('Tony').sleep(10).eat('lunch');
// Hi I am Tony
// 等待了 10 秒...
// I am eating
lunchLazyMan('Tony').eat('lunch').sleep(10).eat('dinner');
// Hi I am Tony
// I am eating lunch// 等待了 10 秒...
// I am eating
dinerLazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(1
0).eat('junk food');
// Hi I am Tony// 等待了 5 秒...
// I am eating lunch
// I am eating dinner
// 等待了 10 秒...
// I am eating junk food
答:
class LazyManClass {
constructor(name) {
this.name = name
this.queue = []
console.log(Hi I am ${name})
setTimeout(() => {
this.next()
},0)
}
sleepFirst(time) {
const fn = () => {
setTimeout(() => {
console.log(等待了${time}秒...)
this.next()
}, time)
}
this.queue.unshift(fn)
return this
}
sleep(time) {
const fn = () => {
setTimeout(() => {
console.log(等待了${time}秒...)
this.next()
},time)
}
this.queue.push(fn)
return this
}
eat(food) {
const fn = () => {
console.log(I am eating ${food})
this.next()
}
this.queue.push(fn)
return this
}
next() {
const fn = this.queue.shift()
fn && fn() }}function LazyMan(name) {
return new LazyManClass(name)}
第 44 题:分析比较 opacity: 0、visibility: hidden、display: none 优劣和适用场景
display: none (不占空间,不能点击)(场景,显示出原来这里不存在的结 构) visibility: hidden(占据空间,不能点击)(场景:显示不会导致页面结 构发生变动,不会撑开) opacity: 0(占据空间,可以点击)(场景:可以跟 transition 搭配) 第 45 题:箭头函数与普通函数(function)的区别是什么?构造函数(function)可以使用 new 生成实例,那么箭头函数可以吗?为什么?
箭头函数是普通函数的简写,可以更优雅的定义一个函数,和普通函数相比, 有以下几点差异: 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对 象。 不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可 以用 rest 参数代替。 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。 不可以使用 new 命令,因为: o 1.没有自己的 this,无法调用 call,apply。 o 2.没有 prototype 属性 ,而 new 命令在执行时需要将构造函数 的 prototype 赋值给新的对象的 proto new 过程大致是这样的: function newFunc(father, ...rest) { var result = {}; result.proto = father.prototype; var result2 = father.apply(result, rest); if ( (typeof result2 === 'object' || typeof result2 === 'function') && result2 !== null ) { return result2; } return result; } 第 46 题:给定两个数组,写一个方法来计算它们的交集。
例如:给定 nums1 = [1, 2, 2, 1],nums2 = [2, 2],返回 [2, 2]。 var nums1 = [1, 2, 2, 1], nums2 = [2, 2, 3, 4]; // 1. // 有个问题, [NaN].indexOf(NaN) === -1var newArr1 = nums1.filter(function(item) { return nums2.indexOf(item) > -1; }); console.log(newArr1); // 2. var newArr2 = nums1.filter((item) => { return nums2.includes(item); }); console.log(newArr2); 第 47 题:已知如下代码,如何修改才能让图片宽度为 300px ?注意下面代码不可修改。
<img src="1.jpg" style="width:480px!important;”> 答: max-width: 300pxtransform: scale(0.625,0.625) 第 48 题:介绍下如何实现 token 加密
jwt 举例:
- 需要一个 secret(随机数)
- 后端利用 secret 和加密算法(如:HMAC-SHA256)对 payload(如账号密码) 生成一个字符串(token),返回前端
- 前端每次 request 在 header 中带上 token
- 后端用同样的算法解密 第 49 题:redux 为什么要把 reducer 设计成纯函数
redux 的设计思想就是不产生副作用,数据更改的状态可回溯,所以 redux 中处 处都是纯函数 第 50 题:如何设计实现无缝轮播
blog.csdn.net/weixin_3406… 第 51 题:模拟实现一个 Promise.finally
Promise.prototype.finally = function (callback) { let P = this.constructor; return this.then( value => P.resolve(callback()).then(() => value), reason => P.resolve(callback()).then(() => { throw reason }) ); }; 第 52 题: a.b.c.d 和 a['b']['c']['d'],哪个性能更高?
应该是 a.b.c.d 比 a['b']['c']['d'] 性能高点,后者还要考虑 [ ] 中是变量的情况, 再者,从两种形式的结构来看,显然编译器解析前者要比后者容易些,自然也 就快一点。 第 53 题:ES6 代码转成 ES5 代码的实现思路是什么
ES6 转 ES5 目前行业标配是用 Babel,转换的大致流程如下:
-
解析:解析代码字符串,生成 AST;
-
转换:按一定的规则转换、修改 AST;
-
生成:将修改后的 AST 转换成普通代码。 如果不用工具,纯人工的话,就是使用或自己写各种 polyfill 了。 第 54 题: 如何解决移动端 Retina 屏 1px 像素问题
-
伪元素 + transform scaleY(.5)
-
border-image
-
background-image
-
box-shadow 第 55 题: 如何把一个字符串的大小写取反(大写变小写小写变大写),例如 ’AbC' 变成 'aBc' 。
function processString(s) { var arr = s.split(''); var new_arr = arr.map((item) => { return item === item.toUpperCase() ? item.toLowerCase() : item.toUpperCase(); }); Return new_arr.join(''); } console.log(processString('AbC')); function swapString(str) { var result = '' for (var i = 0; i < str.length; i++) { var c = str[i] if (c === c.toUpperCase()) { result += c.toLowerCase() } else { result += c.toUpperCase() } } return result } swapString('ADasfads123!@!@#') // => 'adASFADS123!@!@#' 第 56 题: 介绍下 webpack 热更新原理,是如何做到在不刷新浏览器的前提下更新页面的
- 当修改了一个或多个文件;
- 文件系统接收更改并通知 webpack;
- webpack 重新编译构建一个或多个模块,并通知 HMR 服务器进行更新;
- HMR Server 使用 webSocket 通知 HMR runtime 需要更新,HMR 运行时 通过 HTTP 请求更新 jsonp;
- HMR 运行时替换更新中的模块,如果确定这些模块无法更新,则触发整 个页面刷新。 第 57 题: 实现一个字符串匹配算法,从长度为 n 的字符串 S 中,查找是否存在字符串 T,T 的长度是 m,若存在返回所在位置。
const find = (S, T) => { if (S.length < T.length) return -1 for (let i = 0; i < S.length; i++) { if (S.slice(i, i + T.length) === T) return i } return -1 第 58 题: 为什么普通 for 循环的性能远远高于 forEach 的性能,请解释其中的原因。
for 循环没有任何额外的函数调用栈和上下文; forEach 函数签名实际上是 array.forEach(function(currentValue, index, arr), thisValue) 它不是普通的 for 循环的语法糖,还有诸多参数和上下文需要在执行的时候考 虑进来,这里可能拖慢性能; 第 59 题: 介绍下 BFC、IFC、GFC 和 FFC
BFC(Block formatting contexts): 块级格式上下文页面上的一个隔离的渲染区域,那么他是如何产生的呢?可以 触发 BFC 的元素有 float、position、overflow、display:table-cell/ inline-block/table-caption ;BFC 有什么作用呢?比如说实现多栏布局’ IFC(Inline formatting contexts): 内联格式上下文 IFC 的 line box(线框)高度由其包含行内元素中最高的实际高 度计算而来(不受到竖直方向的 padding/margin 影响)IFC 中的 line box 一般左右 都贴紧整个 IFC,但是会因为 float 元素而扰乱。float 元素会位于 IFC 与与 line box 之间,使得 line box 宽度缩短。 同个 ifc 下的多个 line box 高度会不同 IFC 中时 不可能有块级元素的,当插入块级元素时(如 p 中插入 div)会产生两个匿名块 与 div 分隔开,即产生两个 IFC,每个 IFC 对外表现为块级元素,与 div 垂直排 列。那么 IFC 一般有什么用呢?水平居中:当一个块要在环境中水平居中时, 设置其为 inline-block 则会在外层产生 IFC,通过 text-align 则可以使其水平居中。 垂直居中:创建一个 IFC,用其中一个元素撑开父元素的高度,然后设置其 vertical-align:middle,其他行内元素则可以在此父元素下垂直居中。 GFC(GrideLayout formatting contexts): 网格布局格式化上下文当为一个元素设置 display 值为 grid 的时候,此元素将会 获得一个独立的渲染区域,我们可以通过在网格容器(grid container)上定义 网格定义行(grid definition rows)和网格定义列(grid definition columns)属性 各在网格项目(grid item)上定义网格行(grid row)和网格列(grid columns) 为每一个网格项目(grid item)定义位置和空间。那么 GFC 有什么用呢,和 table 又有什么区别呢?首先同样是一个二维的表格,但 GridLayout 会有更加丰富的 属性来控制行列,控制对齐以及更为精细的渲染语义和控制。 FFC(Flex formatting contexts): 自适应格式上下文 display 值为 flex 或者 inline-flex 的元素将会生成自适应容器 (flex container),可惜这个牛逼的属性只有谷歌和火狐支持,不过在移动端 也足够了,至少 safari 和 chrome 还是 OK 的,毕竟这俩在移动端才是王道。Flex Box 由伸缩容器和伸缩项目组成。通过设置元素的 display 属性为 flex 或 inline-flex 可以得到一个伸缩容器。设置为 flex 的容器被渲染为一个块级元素, 而设置为 inline-flex 的容器则渲染为一个行内元素。伸缩容器中的每一个子元 素都是一个伸缩项目。伸缩项目可以是任意数量的。伸缩容器外和伸缩项目内 的一切元素都不受影响。简单地说,Flexbox 定义了伸缩容器内伸缩项目该如 何布局。 第 60 题: 使用 JavaScript Proxy 实现简单的数据绑定
数组可以直接根据索引取的对应的元素,所以不管取哪个位置的元素的时间复 杂度都是 O(1) 得出结论:消耗时间几乎一致,差异可以忽略不计 第 62 题:输出以下代码运行结果
// example 1 var a={}, b='123', c=123; a[b]='b';a[c]='c'; console.log(a[b]);
// example 2var a={}, b=Symbol('123'), c=Symbol('123'); a[b]='b'; a[c]='c'; console.log(a[b]);
// example 3var a={}, b={key:'123'}, c={key:'456'}; a[b]='b';a[c]='c'; console.log(a[b]); 答:
-
对象的键名只能是字符串和 Symbol 类型。
-
其他类型的键名会被转换成字符串类型。
-
对象转字符串默认会调用 toString 方法。 // example 1 var a={}, b='123', c=123;a[b]='b'; // c 的键名会被转换成字符串'123',这里会把 b 覆盖掉。a[c]='c'; // 输出 cconsole.log(a[b]); // example 2var a={}, b=Symbol('123'), c=Symbol('123'); // b 是 Symbol 类型,不需要转换。a[b]='b'; // c 是 Symbol 类型,不需要转换。任何一个 Symbol 类型的值都是不相等的, 所以不会覆盖掉 b。a[c]='c'; // 输出 bconsole.log(a[b]); // example 3var a={}, b={key:'123'}, c={key:'456'}; // b 不是字符串也不是 Symbol 类型,需要转换成字符串。 // 对象类型会调用 toString 方法转换成字符串 [object Object]。a[b]='b'; // c 不是字符串也不是 Symbol 类型,需要转换成字符串。 // 对象类型会调用 toString 方法转换成字符串 [object Object]。这里会把 b 覆盖掉。a[c]='c'; // 输出 cconsole.log(a[b]); 第 63 题:Vue 的父组件和子组件生命周期钩子执行顺序是什么
-
父组建: beforeCreate -> created -> beforeMount
-
子组件: -> beforeCreate -> created -> beforeMount -> mounted
-
父组件: -> mounted
-
总结:从外到内,再从内到外 第 64 题:input 搜索如何防抖,如何处理中文输入
const p = Promise.all([p1, p2, p3]); Promise.all 方法接受一个数组作为参数,p1、p2、p3 都是 Promise 实例,如果 不是,就会先调用下面讲到的 Promise.resolve 方法,将参数转为 Promise 实例, 再进一步处理。(Promise.all 方法的参数可以不是数组,但必须具有 Iterator 接 口,且返回的每个成员都是 Promise 实例。) 第 66 题:var 、let 和 const 区别的实现原理是什么
var 和 let 用以声明变量,const 用于声明只读的常量; var 和 let 用以声明变量,const 用于声明只读的常量; var 声明的变量,不存在块级作用域,在全局范围内都有效,let 和 const 声明的,只在它所在的代码块内有效; let 和 const 不存在像 var 那样的 “变量提升” 现象,所以 var 定义变 量可以先使用,后声明,而 let 和 const 只可先声明,后使用; let 声明的变量存在暂时性死区,即只要块级作用域中存在 let,那么它 所声明的变量就绑定了这个区域,不再受外部的影响。 let 不允许在相同作用域内,重复声明同一个变量; const 在声明时必须初始化赋值,一旦声明,其声明的值就不允许改变, 更不允许重复声明;如 const 声明了一个复合类型的常量,其存储的是一个引 用地址,不允许改变的是这个地址,而对象本身是可变的。 变量与内存之间的关系,主要由三个部分组成: 变量名 内存地址 内存空间 JS 引擎在读取变量时,先找到变量绑定的内存地址,然后找到地址所指向的内 存空间,最后读取其中的内容。当变量改变时,JS 引擎不会用新值覆盖之前旧 值的内存空间(虽然从写代码的角度来看,确实像是被覆盖掉了),而是重新 分配一个新的内存空间来存储新值,并将新的内存地址与变量进行绑定,JS 引 擎会在合适的时机进行 GC,回收旧的内存空间。 const 定义变量(常量)后,变量名与内存地址之间建立了一种不可变的绑定 关系,阻隔变量地址被改变,当 const 定义的变量进行重新赋值时,根据前面 的论述,JS 引擎会尝试重新分配新的内存空间,所以会被拒绝,便会抛出异常。 第 67 题:请实现一个 add 函数,满足以下功能。
add(1); // 1add(1)(2); // 3add(1)(2)(3); // 6add(1)(2, 3); // 6add(1, 2)(3); // 6add(1, 2, 3); // 6 答: 实现 1: function currying(fn, length) { length = length || fn.length; // 注释 1 return function (...args) { // 注释 2 return args.length >= length // 注释 3 ? fn.apply(this, args) // 注释 4 : currying(fn.bind(this, ...args), length - args.length) // 注释 5 }} 实现 2: const currying = fn => judge = (...args) => args.length >= fn.length ? fn(...args) : (...arg) => judge(...args, ...arg) 其中注释部分 注释 1:第一次调用获取函数 fn 参数的长度,后续调用获取 fn 剩余参数的 长度 注释 2:currying 包裹之后返回一个新函数,接收参数为 ...args 注释 3:新函数接收的参数长度是否大于等于 fn 剩余参数需要接收的长度 注释 4:满足要求,执行 fn 函数,传入新函数的参数 注释 5:不满足要求,递归 currying 函数,新的 fn 为 bind 返回的新函数(bind 绑定了 ...args 参数,未执行),新的 length 为 fn 剩余参数的长度
第 70 题:在输入框中如何判断输入的是一个正确的网址。
function isUrl(url) { const a = document.createElement("a"); a.href = url; return ( [ /^(http|https):{url}` ].find(x => !x) === undefined ); } 第 71 题:vue 在 v -for 时给每项元素绑定事件需要用事件代理吗?为什么?
在 vue 中 vue 做了处理 如果我们自己在非 vue 中需要对很多元素添加事件的时候,可以通过将事件添 加到它们的父节点而将事件委托给父节点来触发处理函数 第 72 题:模拟实现一个深拷贝,并考虑对象相互引用以及 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)].red uce((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)的操作 第 73 题:介绍下前端加密的常见场景和方法
首先,加密的目的,简而言之就是将明文转换为密文、甚至转换为其他的东西, 用来隐藏明文内容本身,防止其他人直接获取到敏感明文信息、或者提高其他 人获取到明文信息的难度。通常我们提到加密会想到密码加密、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 这时,如果你想让用户看到“1