面试十题(2)
1. JS 异步解决方案的发展历程以及优缺点。
- 回调函数(callback)
setTimeout(() => {
// callback 函数体
}, 1000)
- 优点:解决了同步的问题(同步问题,只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行)
- 缺点:回调地狱,不能用 try catch 捕获错误,不能 return
ajax('XXX1', () => {
// callback 函数体
ajax('XXX2', () => {
// callback 函数体
ajax('XXX3', () => {
// callback 函数体
})
})
})
- Promise
- Promise就是为了解决callback的问题而产生的。
- Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装
- 优点:解决了回调地狱的问题
ajax('XXX1')
.then(res => {
// 操作逻辑
return ajax('XXX2')
}).then(res => {
// 操作逻辑
return ajax('XXX3')
}).then(res => {
// 操作逻辑
})
- 缺点:无法取消 Promise ,错误需要通过回调函数来捕获
- Generator
- 特点:可以控制函数的执行,可以配合 co 函数库使用
function *fetch() {
yield ajax('XXX1', () => {})
yield ajax('XXX2', () => {})
yield ajax('XXX3', () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()
- async/await
- async、await 是异步的终极解决方案
- await 内部实现了 generator,其实 await 就是 generator 加上 Promise的语法糖,且内部实现了自动执行 generator。
- 优点:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题
- 缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。可以使用Promise.all
async function test() {
// 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式
// 如果有依赖性的话,其实就是解决回调地狱的例子了
await fetch('XXX1')
await fetch('XXX2')
await fetch('XXX3')
}
let a = 0
let b = async () => {
a = a + await 10
console.log('2', a)
}
b()
a++
console.log('1', a)
// 结果
1 1
2 10
2. new操作符做了什么?如何实现一个 new?
- 做了什么
- 创建了一个空的js对象(即{})
- 将空对象的原型prototype指向构造函数的原型
- 将空对象作为构造函数的上下文(改变this指向)
- 对构造函数有返回值的判断
- 怎么实现
function _new(constructor, ...args){
// 1、创建一个空的对象
// let obj = Object.create({})
let obj = {}
// 2、将空对象的原型prototype指向构造函数的原型
// Object.setPrototypeOf(obj, constructor.prototype)
obj.__proto__ = constructor.prototype
//3、改变构造函数的上下文(this),并将剩余的参数传入
let result = constructor.apply(obj, args)
//4、在构造函数有返回值的情况进行判断
// return Object.prototype.toString.call(result) === '[object Object]' ? result : obj;
return result instanceof Object ? result : obj
}
3. 谈谈你对 TCP 三次握手和四次挥手的理解
- 三次握手: 检测双方都有 发送和接收 数据的能力
A:喂喂喂,我是A,你听的到吗?
B:在在在,我能听到,我是B,你能听到我吗?
A:听到了。我们今天去钓鱼吧。。balabala
- 四次挥手: A等待2MSL后(大概4分钟),无回复,关闭
- 为什么A进入TIME_WAIT需要等待最大报文段生存的时间后,才能关闭?
原因是,担心网络不可靠而导致的丢包,最后一个回应B的ACK万一丢了怎么办,在这个时间内,A是可以重新发包的,但是超过了最大等待时间的话,就算收不到也没用了,所以就可以关闭了。
4. React 中 setState 什么时候是同步的,什么时候是 异步的?
- 由 React 控制的事件处理程序,以及生命周期函数调用 setState 不会同步更 新 state 。
- React 控制之外的事件中调用 setState 是同步更新的。比如原生 js 绑定的事 件,setTimeout/setInterval 等。
- 注意: setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
- this.setState()接收 两种参数:对象或函数
对象:即想要修改的state
函数:接收两个函数,第一个函数接受两个参数,第一个是当前state,第二个是当前props,该函数返回一个对象,和直接传递对象参数是一样的,就是要修改的state;第二个函数参数是state改变后触发的回调。
constructor() {
this.state = {
count: 10
}
}
handleClick() {
this.setState({
count: this.state.count + 1
})
this.setState({
count: this.state.count + 1
})
this.setState({
count: this.state.count + 1
})
}
// 最终的结果只加了1
// setState提供了函数式用法,接收两个函数参数,第一个函数调用更新state,第二个函数是更新完之后的回调。
increment(state, props) {
return {
count: state.count + 1
}
}
handleClick() {
this.setState(this.increment)
this.setState(this.increment)
this.setState(this.increment)
}
// 结果: 13
// 对于多次调用函数式setState的情况,React会保证调用每次increment时,state都已经合并了之前的状态修改结果。
// 也就是说,第一次调用this.setState(increment),传给increment的state参数的count是10,第二次调用是11,第三次调用是12,最终handleClick执行完成后的结果就是this.state.count变成了13。
// 值得注意的是:在increment函数被调用时,this.state并没有被改变,依然要等到render函数被重新执行时(或者shouldComponentUpdate函数返回false之后)才被改变,因为render只执行一次。
5. 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;
}
};
// 0012
6. 有以下 3 个判断数组的方法,请分别介绍它们之间的区别和优劣Object.prototype.toString.call() 、 instanceof 以及 Array.isArray()
- Object.prototype.toString.call()
每一个继承 Object 的对象都有 toString 方法,如果 toString 方法没有重写的话,会返回 [Object type],其中 type 为对象的类型。但当除了 Object 类型的对象外,其他类型直接使用 toString 方法时,会直接返回都是内容的字符串,所以我们需要使用call或者apply方法来改变toString方法的执行上下文。
Object.prototype.toString.call() 常用于判断浏览器内置对象时。
const a = ['1','2'];
a.toString(); // "1,2"
Object.prototype.toString.call(a); // "[object Array]"
这种方法对于所有基本的数据类型都能进行判断,即使是 null 和 undefined 。
Object.prototype.toString.call('a') // "[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: 'a'}) // "[object Object]"
- instanceof
instanceof 的内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。
使用 instanceof判断一个对象是否为数组,instanceof 会判断这个对象的原型链上是否会找到对应的 Array 的原型,找到返回 true,否则返回 false。
但 instanceof 只能用来判断对象类型,原始类型不可以。并且所有对象类型 instanceof Object 都是 true。
[] instanceof Array; // true
[] instanceof Object; // true
延伸:instanceof的算法机制,js原型链
// 取左边的__proto__和右边的prototype进行比较
function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
var O = R.prototype;// 取 R 的显示原型
L = L.__proto__;// 取 L 的隐式原型
while (true) {
if (L === null)
return false;
if (O === L)// 这里重点:当 O 严格等于 L 时,返回 true
return true;
L = L.__proto__;
}
}
- Array.isArray()
用来判断对象是否为数组
instanceof 与 isArray:当检测Array实例时,Array.isArray 优于 instanceof ,因为 Array.isArray 可以检测出 iframes
Array.isArray()与 Object.prototype.toString.call():Array.isArray()是ES6新增的方法,当不存在 Array.isArray() ,可以用 Object.prototype.toString.call() 实现。Array.isArray的polyfill通常这样:
if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}
- 延伸:typeof 大多用于基础数据类型,基于机器码判断,typeof null === 'object'
7. 介绍下观察者模式和订阅-发布模式的区别,各自适用于什么场景
-
观察者模式中主体和观察者是互相感知的,发布-订阅模式是借助第三方来实现调度的,发布者和订阅者之间互不感知
-
发布-订阅模式是观察者模式的一种变体。发布-订阅只是把一部分功能抽象成一个独立的ChangeManager。
-
都是某个对象改变,使依赖于它的多个对象得到通知。
-
发布-订阅模式适合更复杂的场景。
观察者模式中观察者和目标直接进行交互,而发布订阅模式中统一由调度中心进行处理,订阅者和发布者互不干扰。这样一方面实现了解耦,还有就是可以实现更细粒度的一些控制。比如发布者发布了很多消息,但是不想所有的订阅者都接收到,就可以在调度中心做一些处理,类似于权限控制之类的。还可以做一些节流操作。
// 观察者模式
// ES6 写法:
// 观察者
class Observer {
constructor() {
}
update(val) {
}
}
// 观察者列表
class ObserverList {
constructor() {
this.observerList = []
}
add(observer) {
return this.observerList.push(observer);
}
remove(observer) {
this.observerList = this.observerList.filter(ob => ob !== observer);
}
count() {
return this.observerList.length;
}
get(index) {
return this.observerList[index];
}
}
// 目标
class Subject {
constructor() {
this.observers = new ObserverList();
}
addObserver(observer) {
this.observers.add(observer);
}
removeObserver(observer) {
this.observers.remove(observer);
}
notify(...args) {
let obCount = this.observers.count();
for (let index = 0; index < obCount; index++) {
this.observers.get(i).update(...args);
}
}
}
// ES5 写法:
function Subject(){
this.observers = [];
}
Subject.prototype = {
add:function(observer){ // 添加
this.observers.push(observer);
},
remove:function(observer){ // 删除
var observers = this.observers;
for(var i = 0;i < observers.length;i++){
if(observers[i] === observer){
observers.splice(i,1);
}
}
},
notify:function(){ // 通知
var observers = this.observers;
for(var i = 0;i < observers.length;i++){
observers[i].update();
}
}
}
function Observer(name){
this.name = name;
}
Observer.prototype = {
update:function(){ // 更新
console.log('my name is '+this.name);
}
}
var sub = new Subject();
var obs1 = new Observer('ttsy1');
var obs2 = new Observer('ttsy2');
sub.add(obs1);
sub.add(obs2);
sub.notify(); //my name is ttsy1、my name is ttsy2
// 发布订阅模式
// ES6 写法
class PubSub {
constructor() {
this.subscribers = {}
}
subscribe(type, fn) {
if (!Object.prototype.hasOwnProperty.call(this.subscribers, type)) {
this.subscribers[type] = [];
}
this.subscribers[type].push(fn);
}
unsubscribe(type, fn) {
let listeners = this.subscribers[type];
if (!listeners || !listeners.length) return;
this.subscribers[type] = listeners.filter(v => v !== fn);
}
publish(type, ...args) {
let listeners = this.subscribers[type];
if (!listeners || !listeners.length) return;
listeners.forEach(fn => fn(...args));
}
}
let ob = new PubSub();
ob.subscribe('add', (val) => console.log(val));
ob.publish('add', 1);
// ES5 写法
let pubSub = {
list:{},
subscribe:function(key,fn){ // 订阅
if (!this.list[key]) {
this.list[key] = [];
}
this.list[key].push(fn);
},
publish:function(){ // 发布
let arg = arguments;
let key = [].shift.call(arg);
let fns = this.list[key];
if(!fns || fns.length<=0) return false;
for(var i=0,len=fns.length;i<len;i++){
fns[i].apply(this, arg);
}
},
unSubscribe(key) { // 取消订阅
delete this.list[key];
}
};
pubSub.subscribe('name', (name) => {
console.log('your name is ' + name);
});
pubSub.subscribe('sex', (sex) => {
console.log('your sex is ' + sex);
});
pubSub.publish('name', 'ttsy1'); // your name is ttsy1
pubSub.publish('sex', 'male'); // your sex is male
var dom= document.getElementById('dom');
dom.onclick = function(){};
<!--Dom一级事件 相当于观察者模式-->
dom.addEventListener('click',function(){});
<!--Dom二级事件,相当于发布订阅,会有一个事件池,存放注册的回调-->
8. 聊聊 Redux 和 Vuex 的设计思想
Redux
- 整个应用只有一个Store:单一数据源
- Store.state不能直接修改(只读),必须调用dispatch(action) => store.reducer => return newState
- Action是一个对象,且必须具有type属性,用来告诉reducer执行哪个操作
- reducer必须是一个纯函数,以此来保证相同的输入必定是相同的输出,确保返回的newState可预测可控
- 大型应用中可以有多个reducer,通过combineReducer 来整合成一个根reducer
Vuex
- 由 State + Mutations(commit) + Actions(dispatch) 组成
- 全局只有一个Store实例(单一数据源)
- Mutations必须是同步事务,Why?:不同步修改的话,会很难调试,不知道改变什么时候发生,也很难确定先后顺序,A、B两个 mutation,调用顺序可能是 A -> B,但是最终改变 State 的结果可能是 B -> A
- Actions 负责处理异步事务,然后在异步回调中触发一个或多个mutations,当然,也可以在业务代码中处理异步事务,然后在回调中同样操作
Redux VS Vuex
Redux:
// view——>actions——>reducer——>state变化——>view变化(同步异步一样)
Vuex:
// view——>commit——>mutations——>state变化——>view变化(同步操作)
// view——>dispatch——>actions——>mutations——>state变化——>view变化(异步操作)
- 总的来说都是让 View 通过某种方式触发 Store 的事件(mutation)或方法(reducer),Store 的事件或方法对 State 进行修改(state.xxx = xxx)或返回一个新的 State(return newState),State 改变之后,View 发生响应式改变(setState vs 数据劫持响应式)。
9. 说说浏览器和 Node 事件循环(event loop)的区别
浏览器
- 微任务和宏任务在浏览器的执行顺序是这样的: 执行一个宏任务=> 执行完微任务。 如此循环往复下去
- 宏任务:setTimeout、setInterval、script(整体代码)、 I/O 操作、UI 渲染
- 微任务:new Promise().then(回调)、MutationObserver(html5新特性)
node
- 大体宏任务执行顺序:【timer定时器】=> 本阶段执行已经安排的setTimeout()和setInterval() 的回调函数。【i/o callbacks】=> 处理一些上一轮循环中的少数未执行的 I/O 回调【idle, prepare】=> 闲置阶段,仅系统内部使用。【poll 轮询】=> 获取新的I/O事件, 适当的条件下node将阻塞在这里。【check 阶段】=> 执行 setImmediate() 的回调。【close callbacks】=> 一些准备关闭的回调函数,如:socket.on('close', ...)。
- 微任务和宏任务在Node的执行顺序:
Node10以前: 执行完一个阶段的所有任务=>执行完nextTick队列里面的内容=>然后执行完微任务队列的内容
Node 11以后: 和浏览器的行为统一了,都是每执行一个宏任务就执行完微任务队列。
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
10. 介绍模块化发展历程
模块化要解决的三个问题:
- 可维护性:降低编程人员对代码的维护成本,抽离公共代码
- 命名空间:解决全局变量污染和变量重名等问题
- 依赖管理:更好地管理依赖,并保证一定的加载顺序
①IIFE
使用自执行函数来编写模块化,特点:在一个单独的函数作用域中执行代码,避免变量冲突。 缺点:只提供逻辑划分,但是不解决代码本身的划分,也就是说还是做不到“分文件”
(function(){
return {
data:[]
}
})()
CommonJS
2009年发布,Node 应用由模块组成,采用 CommonJS 模块规范。commonJS规范加载是同步的,也就是说,加载完成才执行后面的操作。 Node.js主要用于服务端编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快。分成三部分:1.模块标识。module:变量在每个模块内部,代表当前模块。2.模块定义。require:用来加载外部模块,读取并执行JS文件,返回该模块的exports对象。3.模块引用。exports:属性是对外的接口,用于导出当前模块的方法或变量
AMD和require.js
特点:依赖必须提前声明好。 "异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
// 规定采用require语句加载模块,但是不同于CommonJS,它要求两个参数(模块名数组格式和回调函数)
require(['math'], function (math) {
math.add(2, 3);
});
// 在定义模块的时候需要使用define函数定义:
define(id?, dependencies? factory)
RequireJS是js模块化的工具框架,是AMD规范的具体实现。 有以下优点:
- 动态并行加载js,依赖前置,一个模块的回调函数必须得等到所有依赖都加载完毕之后,才可执行。无需再考虑js加载顺序问题。
- 规范化输入输出,使用起来方便。
- 对于不满足AMD规范的文件可以很好地兼容。
CMD和SeaJS
同样是受到Commonjs的启发,国内(阿里)诞生了一个CMD(Common Module Definition)规范。该规范借鉴了Commonjs的规范与AMD规范,在两者基础上做了改进。
与AMD相比非常类似,CMD规范(2011)具有以下特点:
- define定义模块,require加载模块,exports暴露变量。
- 与AMD规范的主要区别在于定义模块和依赖引入的部分,与AMD模块规范相比,CMD模块更接近于Node对CommonJS规范的定义:不同于AMD的依赖前置,CMD推崇依赖就近(需要的时候再加载)
- 推崇api功能单一,一个模块干一件事。
// CMD支持动态引入
define(function(require,exports,module){
....
})
SeaJS是CMD规范的具体实现,跟RequireJs类似,CMD也是SeaJs推广过程中诞生的规范。CMD借鉴了很多AMD和Commonjs优点,同样SeaJs也对AMD和Commonjs做出了很多兼容。
ES6 module
2015年,ES6规范中,终于将模块化纳入JavaScript标准,从此js模块化被官方扶正,也是未来js的标准