zoukankan      html  css  js  c++  java
  • 面试十题(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

    模块化

  • 相关阅读:
    jni 内存泄露 local reference table overflow (max=512)
    解决Android BitmapOutOfMemory 内存泄露
    c/c++ library编译
    如何查Android native crash BUG
    Shallow Size Retained Size
    android adb 命令发送 keyevent
    Android Dialog背景全透明无边框 Theme Style
    Android 销毁ImageView的bitmap
    Android EditText show softKeyBoard automatically
    jQuery 参考手册 事件
  • 原文地址:https://www.cnblogs.com/jialuchun/p/15167997.html
Copyright © 2011-2022 走看看