面试十题(2)

1. JS 异步解决方案的发展历程以及优缺点。

  1. 回调函数(callback)
setTimeout(() => {
    // callback 函数体
}, 1000)
  • 优点:解决了同步的问题(同步问题,只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行)
  • 缺点:回调地狱,不能用 try catch 捕获错误,不能 return
ajax('XXX1', () => {
    // callback 函数体
    ajax('XXX2', () => {
        // callback 函数体
        ajax('XXX3', () => {
            // callback 函数体
        })
    })
})
  1. 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 ,错误需要通过回调函数来捕获
  1. 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()
  1. 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?

  • 做了什么
    1. 创建了一个空的js对象(即{})
    2. 将空对象的原型prototype指向构造函数的原型
    3. 将空对象作为构造函数的上下文(改变this指向)
    4. 对构造函数有返回值的判断
  • 怎么实现
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 三次握手和四次挥手的理解

  • 三次握手: 检测双方都有 发送和接收 数据的能力
AB你好,我是A收到,我是B好的,开始连接AB

A:喂喂喂,我是A,你听的到吗?

B:在在在,我能听到,我是B,你能听到我吗?

A:听到了。我们今天去钓鱼吧。。balabala

  • 四次挥手: A等待2MSL后(大概4分钟),无回复,关闭
AB你好,我要关了。稍等,还有最后一个包。我已经好了,随时关闭。你关闭吧,不用回复。AB
  • 为什么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),传给incrementstate参数的count10,第二次调用是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()

  1. 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]"
  1. 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__;
  }
 }
  1. 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. 介绍模块化发展历程

模块化要解决的三个问题:

  1. 可维护性:降低编程人员对代码的维护成本,抽离公共代码
  2. 命名空间:解决全局变量污染和变量重名等问题
  3. 依赖管理:更好地管理依赖,并保证一定的加载顺序

①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规范的具体实现。 有以下优点:

  1. 动态并行加载js,依赖前置,一个模块的回调函数必须得等到所有依赖都加载完毕之后,才可执行。无需再考虑js加载顺序问题。
  2. 规范化输入输出,使用起来方便。
  3. 对于不满足AMD规范的文件可以很好地兼容。

CMD和SeaJS

同样是受到Commonjs的启发,国内(阿里)诞生了一个CMD(Common Module Definition)规范。该规范借鉴了Commonjs的规范与AMD规范,在两者基础上做了改进。

与AMD相比非常类似,CMD规范(2011)具有以下特点:

  1. define定义模块,require加载模块,exports暴露变量。
  2. 与AMD规范的主要区别在于定义模块和依赖引入的部分,与AMD模块规范相比,CMD模块更接近于Node对CommonJS规范的定义:不同于AMD的依赖前置,CMD推崇依赖就近(需要的时候再加载)
  3. 推崇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的标准

es6 module

模块化

posted @ 2021-08-20 19:50  jialuchun  阅读(104)  评论(0编辑  收藏  举报