前端大厂面试题上集

210 阅读27分钟

第 1 题:写 React / Vue 项目时为什么要在列表组件中写 key其作用是什么?

key 是给每一个 vnode 的唯一 id,可以依靠 key,更准确,更快的拿到 oldVnode 中对应的 vnode 节点

第 2 题:['1', '2', '3'].map(parseInt) what & why ?

第一眼看到这个题目的时候,脑海跳出的答案是 [1, 2, 3],但是真正的答案是[1, NaN, NaN]。

首先让我们回顾一下,map 函数的第一个参数 callback。这个 callback 一共可以接收三个参数,其中元素的索引。

arr.map(callback: (value: T, index: number, array: T[]) => U, thisArg?: any); 

parseInt 则是用来解析字符串的,使字符串成为指定基数的整数。接收两个参数 第一个表示被处理的值(字符串),第二个表示为解析时的基数。parseInt(string, radix)了解这两个函数后,我们可以模拟一下运行情况

parseInt('1', 0) //radix 为 0 时,且 string 参数不以“0x”和“0”开头时,按照 10 为基数处理。这个时候返回 1

parseInt('2', 1) //基数为 1(1 进制)表示的数中,最大值小于 2,所以无法解析,返回 NaN

parseInt('3', 2) //基数为 2(2 进制)表示的数中,最大值小于 3,所以无法解析,返回 NaN 

第 3 题:什么是防抖和节流?有什么区别?如何实现?

防抖——触发高频事件后 n 秒后函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间;

function debounce(fn) { 
	let timeout = null 
	// 创建一个标记用来存放定时器的返回值 
	return function() { 
    	// 每当用户输入的时候把前一个 setTimeout clear 掉 
   		clearTimeout(timeout)
    	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() { 
		// 在函数开头判断标记是否为 true,不为 true 则 return
        if (!canRun) 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)) 

第 4 题:介绍下 Set、Map、WeakSet 和 WeakMap 的区别?

Set——对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用

WeakSet——成员都是对象;成员都是弱引用,可以被垃圾回收机制回收,可以用来保存 DOM 节点,不容易造成内存泄漏;

Map——本质上是键值对的集合,类似集合;可以遍历,方法很多,可以跟各种数据格式转换。

WeakMap——只接受对象最为键名(null 除外),不接受其他类型的值作为键名;键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的;不能遍历,方法有 getsethasdelete

第 5 题:介绍下深度优先遍历和广度优先遍历,如何实现?

深度优先遍历——是指从某个顶点出发,首先访问这个顶点,然后找出刚访问这个结点的 第一个未被访问的邻结点,然后再以此邻结点为顶点,继续找它的下一个顶点进行访问。重复此步骤,直至所有结点都被访问完为止。

广度优先遍历——是从某个顶点出发,首先访问这个顶点,然后找出刚访问这个结点所有未被访问的邻结点,访问完后再访问这些结点中第一个邻结点的所有结点,重复此方法,直到所有结点都被访问完为止。

//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() 
            //将现在访问点的节点的子节点存入 stack,供将来访问 
            nodes.push(item) 
            let childrens = item.children 
        	for (let i = childrens.length - 1;i >= 0;i--)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 
} 

第 6 题:请分别用深度优先思想和广度优先思想实现一个拷贝函数?

let _toString = Object.prototype.toString
let 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) 
}

第 7 题:ES5/ES6 的继承除了写法以外还有什么区别?

  1. ES5 的继承实质上是先创建子类的实例对象,然后再将父类的方法添加 到 this 上(Parent.apply(this)).

  2. ES6 的继承机制完全不同,实质上是先创建父类的实例对象 this(所以必须先调用父类的 super()方法),然后再用子类的构造函数修改 this

  3. ES5 的继承时通过原型或构造函数机制来实现。

  4. ES6 通过 class 关键字定义类,里面有构造方法,类之间通过 extends 关键字实现继承。

  5. 子类必须在 constructor 方法中调用 super 方法,否则新建实例报错。因为子类没有自己的 this 对象,而是继承了父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类得不到 this 对象。

  6. 注意 super 关键字指代父类的实例,即父类的 this 对象。注意:在子类构造函数中,调用 super 后,才可使用 this 关键字,否则报错。

    //function 声明会提升,但不会初始化赋值。Foo 进入暂时性死区,类似于 let、 
    
    //const 声明变量。 
    const bar = new Bar(); 
    
    // it's ok 
    function Bar() { 
    	this.bar = 42; 
    }
    const foo = new Foo(); 
    
    // ReferenceError: Foo is not defined
    class Foo{ 
        constructor() { 
        	this.foo = 42; 
        } 
    } 
    
    //class 声明内部会启用严格模式。 
    
    // 引用一个未声明的变量 
    function Bar() { 
    	baz = 42; 
    	// it's ok
    }
    const bar = new Bar(); 
    
    class Foo { 
        constructor() { 
            fol = 42; // ReferenceError: fol is not defined 
        } 
    }
    const foo = new Foo(); //class 的所有方法(包括静态方法和实例方法)都是不可枚举的。 
    
    // 引用一个未声明的变量 
    function Bar() { 
    	this.bar = 42;
    }
    Bar.answer = function() { 
    	return 42; 
    };
    
    Bar.prototype.print = function() { 
    	console.log(this.bar);
    };
    
    const barKeys = Object.keys(Bar); 
    
    // ['answer']
    const barProtoKeys = Object.keys(Bar.prototype); 
    
    // ['print']
    class Foo { 
        constructor() { 
        	this.foo = 42; 
        }
    
        static answer() { 
            return 42; 
        }
    
        print() { 
            console.log(this.foo); 
        }
    }
    const fooKeys = Object.keys(Foo); 
    
    // []
    const fooProtoKeys = Object.keys(Foo.prototype); 
    
    // [] 
    
    //class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]],不能使用 new 来调用。 
    
    function Bar() { 
        this.bar = 42; 
    }
    Bar.prototype.print = function() { 
    	console.log(this.bar); 
    };
    
    const bar = new Bar(); 
    
    const barPrint = new bar.print(); 
    
    // it's ok
    class Foo { 
        constructor() { 
            this.foo = 42; 
        } 
    
        print() { 
        	console.log(this.foo); 
        }
    }
    const foo = new Foo(); 
    
    const fooPrint = new foo.print(); 
    
    // TypeError: foo.print is not a constructor 必须使用 new 调用 class。 
    
    function Bar() { 
    	this.bar = 42; 
    }
    const bar = Bar(); 
    
    // it's ok
    class Foo { 
        constructor() { 
        	this.foo = 42; 
        }
    }
    const foo = Foo(); 
    
    // TypeError: Class constructor Foo cannot be invoked without 'new' 
    //class 内部无法重写类名。 
    
    function Bar() {
        Bar = 'Baz'; 
        // it's ok 
        this.bar = 42; 
    
    }
    const bar = new Bar(); 
    // Bar: 'Baz' 
    
    // bar: Bar {bar: 42} 
    
    class Foo { 
        constructor() { 
            this.foo = 42; 
            Foo = 'Fol'; 
            // TypeError: Assignment to constant variable 
    
        }
    }
    const foo = new Foo(); 
    
    Foo = 'Fol'; 
    
    // it's ok 
    

第 8 题:setTimeout、Promise、Async/Await 的区别

blog.csdn.net/yun_hou/art…

第 9 题:Async/Await 如何通过同步的方式实现异步

async 起什么作用——输出的是一个 Promise 对象

第 10 题:异步笔试题请写出下面代码的运行结果

async function async1() { 
	console.log('async1 start') 
	await async2() 
	console.log('async1 end')
}
async function async2() {
	console.log('async2')
}
console.log('script start')

setTimeout(function(){
	console.log('setTimeout')
}, 0) 
async1()
new Promise(function(resolve)) { 
    console.log('promise1') 
    resolve()
}).then(function(){
	console.log('promise2')
})
console.log('script end') 

//输出 

//script start 

//async1 start 

//async2 

//promise1 

//script end 

//async1 end 

//promise2//setTimeout 

第 11 题:算法手写题

已知如下数组,编写一个程序将数组扁平化去并除其中重复部分数据,最终得到一个升序且不重复的数组

var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10]; 

答:使用 Set 方法去重,flat(Infinity)扁平化

Array.from(new Set(arr.flat(Infinity))).sort((a,b)=>{ return a-b})//[1,2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] 

第 12 题:JS 异步解决方案的发展历程以及优缺点。

1、回调函数(callback)

优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。)

缺点:回调地狱,不能用 try catch 捕获错误,不能 return

2、Promise

优点:解决了回调地狱的问题

缺点:无法取消 Promise ,错误需要通过回调函数来捕获

3、Generator

特点:可以控制函数的执行,可以配合 co 函数库使用

4、Async/await

优点:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题

缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。

第 13 题: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 方法是异步执行的 

第 14 题:情人节福利题,如何实现一个 new

第 15 题:简单讲解一下 http2 的多路复用

HTTP2 采用二进制格式传输,取代了 HTTP1.x 的文本格式,二进制格式解析更 高效。

多路复用代替了 HTTP1.x 的序列和阻塞机制,所有的相同域名请求都通过同一 个 TCP 连接并发完成。

在 HTTP1.x 中,并发多个请求需要多个 TCP 连接,浏览器为了控制资源会有 6-8 个 TCP 连接都限制。HTTP2 中 同域名下所有通信都在单个连接上完成,消除了因多个 TCP 连接而带来的延时和内存消耗。

单个连接上可以并行交错的请求和响应,之间互不干扰

第 16 题:谈谈你对 TCP 三次握手和四次挥手的原理

第 17 题:A、B 机器正常连接后,B 机器突然重启,问 A 此时处于 TCP 什么状态

如果 A 与 B 建立了正常连接后,从未相互发过数据,这个时候 B 突然机器重 启,问 A 此时处于 TCP 什么状态?如何消除服务器程序中的这个状态?(超 纲题,了解即可)

因为 B 会在重启之后进入 tcp 状态机的 listen 状态,只要当 a 重新发送一个数据 包(无论是 syn 包或者是应用数据),b 端应该会主动发送一个带 rst 位的重置 包来进行连接重置,所以 a 应该在 syn_sent 状态

第 18 题:React 中 setState 什么时候是同步的,什么时候是异步的?

  1. 由 React 控制的事件处理程序,以及生命周期函数调用 setState 不会同步更新 state 。

  2. React 控制之外的事件中调用 setState 是同步更新的。比如原生 js 绑定的事件,setTimeout/setInterval 等。

第 19 题:React setState 笔试题,下面的代码输出什么?

class Example extends React.Component { 
    constructor() { 
        super() 
        this.state = { 
            val: 0 
        } 
    }

    componentDidMount() { 

    	this.setState({ val: this.state.val + 1 }) 
        // ### 第 1 次 log 
    	console.log(this.state.val) 
        this.setState({ val: this.state.val + 1 }) 
        
		// ### 第 2 次 log 
        console.log(this.state.val) 
        
        setTimeout(() => { 
        	this.setState({ val: this.state.val + 1 }) 
            
			// ### 第 3 次 log 
        	console.log(this.state.val) 
            this.setState({ val: this.state.val + 1 }) 

        	// ### 第 4 次 log
            console.log(this.state.val) 

        }, 0) 

    }

    render() { 
   		 return null 
    } 
} 

//答:0, 0, 1, 2 

第 20 题:介绍下 npm 模块安装机制,为什么输入 npm install 就可以自动安装对应的模块?

  1. npm 模块安装机制:

    发出 npm install 命令 查询 node_modules 目录之中是否已经存在指定模

    若存在,不再重新安装

    若不存在

    npmregistry 查询模块压缩包的网址

    下载压缩包,存放在根目录下的.npm 目录里

    解压压缩包到当前项目的 node_modules 目录

第 21 题:有以下 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)       // "[objectNull]"
    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); // true
    Object.prototype.toString.call(arr); // true 
    
    // Considered harmful, because doesn't work though iframes
    arr 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]'; 
    	};
    }
    

第 22 题:介绍下重绘和回流(Repaint & Reflow),以及如何进行优化

  1. 浏览器渲染机制浏览器采用流式布局模型(Flow Based Layout) 浏览器会把 HTML 解析成 DOM,把 CSS 解析成 CSSOM,DOM 和 CSSOM 合并就产生了渲染树(Render Tree)。有了 RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大 小和位置,最后把节点绘制到页面上。 由于浏览器使用流式布局,对 Render Tree 的计算通常只需要遍历一次就可以完成,但 table 及其内部元素除外,他们可能需要多次计算,通常要花 3 倍于同等元素的时间,这也是为什么要避免使用 table 布局的原因之一。

  2. 重绘

    由于节点的几何属性发生改变或者由于样式发生改变而不会影响布局的,称为重绘,例如 outline, visibility, color、background-color 等,重绘的代价是高昂的,因为浏览器必须验证 DOM 树上其他节点元素的可见性。

  3. 回流

    回流是布局或者几何属性需要改变就称为回流。回流是影响浏览器性能的关键 因素,因为其变化涉及到部分页面(或是整个页面)的布局更新。一个元素的 回流可能会导致了其所有子元素以及 DOM 中紧随其后的节点、祖先节点元素的随后的回流。

    <body>
        <div class="error"> 
            <h4>我的组件</h4> 
            <p><strong>错误:</strong>错误的描述…</p> 
            <h5>错误纠正</h5> 
            <ol>
                <li>第一步</li> 
                <li>第二步</li> 
            </ol>
        </div>
    </body> 
    

    在上面的 HTML 片段中,对该段落(<p>标签)回流将会引发强烈的回流,因为它是一个子节点。这也导致了祖先的回流(div.errorbody – 视浏览器而定)。 此外,<h5><ol>也会有简单的回流,因为其在 DOM 中在回流元素之后。大部 分的回流将导致页面的重新渲染。 回流必定会发生重绘,重绘不一定会引发回流。4. 浏览器优化 现代浏览器大多都是通过队列机制来批量更新布局,浏览器会把修改操作放在队列中,至少一个浏览器刷新(即 16.6ms)才会清空队列,但当你获取布局信 息的时候,队列中可能有会影响这些属性或方法返回值的操作,即使没有,浏览器也会强制清空队列,触发回流与重绘来确保返回正确的值。

    主要包括以下属性或方法:

    • offsetTop、offsetLeft、offsetWidth、offsetHeight

    • scrollTop、scrollLeft、scrollWidth、scrollHeight

    • clientTop、clientLeft、clientWidth、clientHeight

    • width、height

    • getComputedStyle()

    • getBoundingClientRect()

    所以,我们应该避免频繁的使用上述的属性,他们都会强制渲染刷新队列。

  4. 减少重绘与回流

    CSS

    • 使用 transform 替代 top

    • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局

    • 避免使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局。

    • 尽可能在 DOM 树的最末端改变 class,回流是不可避免的,但可以减少其影响。尽可能在 DOM 树的最末端改变 class,可以限制了回流的范围,使其影响尽可能少的节点。

    • 避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多。

    <div><a><span></span></a></div>
    <style> 
        span { 
            color: red; 
        } 
    
        div > a > span { 
            color: red; 
        }
    </style> 
    

    对于第一种设置样式的方式来说,浏览器只需要找到页面中所有的 span 标签 然后设置颜色,但是对于 第二种设置样式的方式来说,浏览器首先需要找到所有的 span 标签,然后找到 span 标签上的 a 标签,最后再去找到 div 标签, 然后给符合这种条件的 span 标签设置颜色,这样的递归过程就很复杂。所以我们应该尽可能的避免写过于具体的 CSS 选择器,然后对于 HTML 来说也尽量少的添加无意义标签,保证层级扁平。将动画效果应用到 position 属性为 absolutefixed 的元素上,避免影响其他元素的布局,这样只是一个重绘,而不是回流,同时,控制动画速度可以选择requestAnimationFrame,详见探讨 requestAnimationFrame

    避免使用 CSS 表达式,可能会引发回流。将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点,例如 will-change、video、iframe 等标签,浏览器会自动将该节点变为图层。CSS3 硬件加速(GPU 加速),使用 css3 硬件加速,可以让 transformopacityfilters 这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color 这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

    JavaScript

    • 避免频繁操作样式,最好一次性重写 style 属性,或者将样式列表定义为 class并一次性更改 class 属性。

    • 避免频繁操作 DOM,创建一个 documentFragment,在它上面应用所有 DOM操作,最后再把它添加到文档中。

    • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个 变量缓存起来。

    • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

第 23 题:介绍下观察者模式和订阅-发布模式的区别,各自适用于什么场景

观察者模式中主体和观察者是互相感知的,发布-订阅模式是借助 第三方来实现 调度的,发布者和订阅者之间互不感知

第 24 题:聊聊 Redux 和 Vuex 的设计思想

不管是 Vue,还是 React,都需要管理状态(state),比如组件之间都有共享 状态的需要。什么是共享状态?比如一个组件需要使用另一个组件的状态,或者一个组件需要改变另一个组件的状态,都是共享状态。 父子组件之间,兄弟组件之间共享状态,往往需要写很多没有必要的代码,比如把状态提升到父组件里,或者给兄弟组件写一个父组件,听听就觉得挺啰嗦如果不对状态进行有效的管理,状态在什么时候,由于什么原因,如何变化就会不受控制,就很难跟踪和测试了。如果没有经历过这方面的困扰,可以简单理解为会搞得很乱就对了。在软件开发里,有些通用的思想,比如隔离变化,约定优于配置等,隔离变化就是说做好抽象,把一些容易变化的地方找到共性,隔离出来,不要去影响其 他的代码。约定优于配置就是很多东西我们不一定要写一大堆的配置,比如我们几个人约定,view 文件夹里只能放视图,不能放过滤器,过滤器必须放到filter 文件夹里,那这就是一种约定,约定好之后,我们就不用写一大堆配置文件了,我们要找所有的视图,直接从 view 文件夹里找就行。根据这些思想,对于状态管理的解决思路就是:把组件之间需要共享的状态抽取出来,遵循特定的约定,统一来管理,让状态的变化可以预测。根据这个思路,产生了很多的模式和库,我们来挨个聊聊。

第 25 题:说说浏览器和 Node 事件循环的区别

其中一个主要的区别在于浏览器的 event loop nodejsevent 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 

第 26 题:介绍模块化发展历程

可从 IIFEAMDCMDCommonJSUMDwebpack(require.ensure)ES Module

<script type="module"> //这几个角度考虑。

blog.csdn.net/dadadeganhu…

第 27 题:全局作用域中,用 const 和 let 声明的变量不在window 上,那到底在哪里?如何去获取

在 ES5 中,顶层对象的属性和全局变量是等价的,var 命令和 function 命令声明的全局变量,自然也是顶层对象。

var a = 12; 

function f(){}; 

console.log(window.a); 

// 12
console.log(window.f); 

// f(){} 

但 ES6 规定,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性,但 let 命令、const 命令、class 命令声明的全局变量,不属于顶层对象的属性。

let aa = 1; 
const bb = 2; 
console.log(window.aa); // undefined
console.log(window.bb); // undefined 

在哪里?怎么获取?通过在设置断点,看看浏览器是怎么处理的:

通过上图也可以看到,在全局作用域中,用 let 和 const 声明的全局变量并没有在全局对象中,只是一个块级作用域(Script)中怎么获取?在定义变量的块级作用域中就能获取啊,既然不属于顶层对象,那就不加 window(global)呗。

let aa = 1; 
const bb = 2; 

console.log(aa);// 1
console.log(bb);// 2

第 28 题:cookie 和 token 都存放在 header 中,为什么不会劫持 token?

  1. 攻击者通过 xss 拿到用户的 cookie 然后就可以伪造 cookie 了。

  2. 或者通过 csrf 在同个浏览器下面通过浏览器会自动带上 cookie 的特性在通过 用户网站-攻击者网站-攻击者请求用户网站的方式 浏览器会自动带上cookie

但是 token

  1. 不会被浏览器带上 问题 2 解决

  2. token 是放在 jwt 里面下发给客户端的 而且不一定存储在哪里 不能通过document.cookie 直接拿到,通过 jwt+ip 的方式可以防止被劫持即使被劫持也是无效的 jwt

第 29 题:聊聊 Vue 的双向数据绑定,Model 如何改变 View, View 又是如何改变 Model 的

  1. 从 M 到 V 的映射(Data Binding),这样可以大量节省你人肉来 update View的代码

  2. 从 V 到 M 的事件监听(DOM Listeners),这样你的 Model 会随着 View触发事件而改变

第 30 题:两个数组合并成一个数组

请把两个数组 ['A1', 'A2', 'B1', 'B2', 'C1', 'C2', 'D1', 'D2'] 和 ['A', 'B', 'C', 'D'],合并为 ['A1', 'A2', 'A', 'B1', 'B2', 'B', 'C1', 'C2', 'C', 'D1', 'D2', 'D']。

function concatArr (arr1, arr2) { 
    const arr = [...arr1]; 
    let currIndex = 0; 
    for (let i = 0; i < arr2.length; i++) { 
        const RE = new RegExp(arr2[i]) 
        while(currIndex < arr.length) { 
            ++currIndex 
            if (!RE.test(arr[currIndex])) { 
                arr.splice(currIndex, 0, arr2[i]) 
                break; 
            } 
        }
     }
    return arr 
} 
var a1 = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2', 'D1', 'D2'] 
var a2 = ['A', 'B', 'C', 'D'] 
const arr = concatArr(a1, a2) 

console.log(a1) // ['A1', 'A2', 'B1', 'B2', 'C1', 'C2', 'D1', 'D2'] 
console.log(a2) // ['A', 'B', 'C', 'D'] 
console.log(arr)// ['A1', 'A2', 'A', B1', 'B2', 'B', C1', 'C2', 'C', D1', 'D2', 'D'] 

第 31 题:改造下面的代码,使之输出 0 - 9,写出你能想到的所有解法。

for (var i = 0; i< 10; i++){ 
    setTimeout(() => { 
    	console.log(i); 
    }, 1000)
} 

// 解法一:
for (let i = 0; i< 10; i++){ 
    setTimeout(() => { 
    	console.log(i); 
    }, 1000)
} 

// 解法二:
for (var i = 0; i< 10; i++){ 
    ((i) => { 
        setTimeout(() => { 
        	console.log(i); 
        }, 1000) 
    })(i)
} 

第 32 题:Virtual DOM 真的比操作原生 DOM 快吗?谈谈你的想法。

  1. 原生 DOM 操作 vs. 通过框架封装操作。

    这是一个性能 vs. 可维护性的取舍。框架的意义在于为你掩盖底层的 DOM 操 作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护。没有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的DOM 操作层需要应对任何上层 API 可能产生的操作,它的实现必须是普适的。针对任何一 个 benchmark,我都可以写出比任何框架更快的手动优化,但是那有什么意义 呢?在构建一个实际应用的时候,你难道为每一个地方都去做手动优化吗?出于可维护性的考虑,这显然不可能。框架给你的保证是,你在不需要手动优化

    的情况下,我依然可以给你提供过得去的性能。

  2. 对 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 的思路去

    写你的应用。

  3. 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(DOMchange)

    依赖收集:重新收集依赖 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来进行“原地复用”:直接根据在数组里的位置进行复用。在题目给出的例子里,如果angular实现加上trackbyindex 来进行 “原地复用”:直接根据在数组里的位置进行复用。在题目给出的例子里,如果 angular 实现加上 track by index 的话, 后续重绘是不会比 React 慢多少的。甚至在 dbmonster 测试中,Angular 和Vue 用了 track by $index 以后都比 React 快: dbmon (注意 Angular 默认版本无优化,优化过的在下面)

    顺道说一句,React 渲染列表的时候也需要提供 key 这个特殊 prop,本质上和 track-by 是一回事。

  4. 性能比较也要看场合

    在比较性能的时候,要分清楚初始渲染、小量数据更新、大量数据更新这些不同的场合。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。

  5. 总结

    以上这些比较,更多的是对于框架开发研究者提供一些参考。主流的框架 + 合理的优化,足以应对绝大部分应用的性能需求。如果是对性能有极致需求的特殊情况,其实应该牺牲一些可维护性采取手动优化:比如 Atom 编辑器在文件渲染的实现上放弃了 React 而采用了自己实现的 tile-based rendering;又比如在移动端需要 DOM-pooling 的虚拟滚动,不需要考虑顺序变化,可以绕过框架的内置实现自己搞一个。

第 33 题:下面的代码打印什么内容,为什么?

var b = 10; 

(function b(){ 
    b = 20; 
    console.log(b); 
})(); 

//答:

ƒ b(){
    b = 20; 
    console.log(b); 
} 

首先函数声明比变量要高,其次 b = 20 没有 var 获取其他,说明是 window 最外层定义的变量。

js 作用域中,先找最近的 那就是 b fn ,直接打印了,如果 b = 20 有 var 那就是打印 20

第 34 题:简单改造下面的代码,使之分别打印 10 和 20。

var b = 10; 
(function b(){ 
    b = 20; 
    console.log(b); 
})(); 

//答:

var b = 10; 

(function (){ 
    b = 20; 
    console.log(b); // 20
})(); 

var b = 10; 

(function (){ 
    console.log(b);// 10 
    b = 20; 
})(); 

第 35 题:浏览器缓存读取规则

可以分成 Service Worker、Memory Cache、Disk Cache 和 Push Cache,那请求的时候 from memory cache 和 from disk cache 的依据是什么,哪些数据什么时候存放在 Memory Cache 和 Disk Cache 中?

www.jianshu.com/p/54cc04190…

第 36 题:使用迭代的方式实现 flatten 函数。

// * 使用递归的方式处理 * wrap 内保存结果 ret返回一个递归函数 *@returns */
var arr=[1,2,3,[4,5],[6,[7,[8]]]]

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)); 

第 37 题:为什么 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')` 
        }) 
    } 

}

第 38 题:(京东)下面代码中 a 在什么情况下会打印 1?

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);
} 

第 39 题:介绍下 BFC 及其应用。

BFC 就是块级格式上下文,是页面盒模型布局中的一种 CSS 渲染模式,相当于一个独立的容器,里面的元素和外部的元素相互不影响。

创建 BFC 的方式有:

  • html 根元素

  • float 浮动

  • 绝对定位

  • overflow 不为 visiable

  • display 为表格布局或者弹性布局

BFC 主要的作用是: 清除浮动 防止同一 BFC 容器中的相邻元素间的外边距重叠问题

第 40 题:在 Vue 中,子组件为何不可以修改父组件传递的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 的属性时不会触发提示,并且会修改父组件数据源的数据。

第 41 题:下面代码输出什么

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

第 42 题:实现一个 sleep 函数

比如 sleep(1000) 意味着等待 1000 毫秒,可从 Promise、Generator、Async/Await 等角度实现

const sleep = (time) => { return new Promise(resolve => setTimeout(resolve, time))}
sleep(1000).then(() => { 
    // 这里写你的骚操作
}) 

第 43 题:使用 sort() 对数组 [3, 15, 8, 29, 102, 22] 进行排序,输出结果输出:[102, 15, 22, 29, 3, 8]

解析:根据 MDN 上对 Array.sort()的解释,默认的排序方法会将数组元素转换为字符串,然后比较字符串中字符的 UTF-16 编码顺序来进行排序。所以'102' 会排在 '15' 前面。

第 44 题:介绍 HTTPS 握手过程

1、clientHello

2、SeverHello

3、客户端回应

4、服务器的最后回应

第 45 题:HTTPS 握手过程中,客户端如何验证证书的合法性

  1. 校验证书的颁发机构是否受客户端信任。

  2. 通过 CRL 或 OCSP 的方式校验证书是否被吊销。

  3. 对比系统时间,校验证书是否在有效期内。

  4. 通过校验对方是否存在证书的私钥,判断证书的网站域名是否与证书颁发的域名一致。

第 46 题:输出以下代码执行的结果并解释为什么

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) 

第 47 题:双向绑定和 vuex 是否冲突在严格模式下直接使用确实会有问题。

解决方案:

<input v-model="message" />
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); 
    } 
} 

第 48 题:call 和 apply 的区别是什么,哪个性能更好一些

  1. Function.prototype.applyFunction.prototype.call 的作用是一样的,区别在于传入参数的不同;

  2. 第一个参数都是,指定函数体内 this 的指向;

  3. 第二个参数开始不同,apply 是传入带下标的集合,数组或者类数组,apply 把它传给函数作为参数,call 从第二个开始传入的参数是不固定的,都会传给函数作为参数。

  4. callapply 的性能要好,平常可以多用 call, call 传入参数的格式正是内部所需要的格式

第 49 题:为什么通常在发送数据埋点请求的时候使用的是 1x1像素的透明 gif 图片?

  1. 没有跨域问题,一般这种上报数据,代码要写通用的;(排除 ajax)
  2. 不会阻塞页面加载,影响用户的体验,只要 new Image 对象就好了;(排除 JS/CSS 文件资源方式上报)
  3. 在所有图片中,体积最小;(比较 PNG/JPG)

第 50 题:(百度)实现 (5).add(3).minus(2) 功能。

//例: 5 + 3 - 2,结果为 6 

//答:
Number.prototype.add = function(n) { 
    return this.valueOf() + n; 
};
Number.prototype.minus = function(n) { 
	return this.valueOf() - n; 
};