最近朋友内推面试了几家公司(货拉拉虾皮有赞乐信Qtrade苹果树富途涂鸦OPPO微保微众元戎启行),也收获了满意的offer。整理了下面试遇到的问题,作为记录。

JS相关

JS原型及原型链

function Person() {}
Person.prototype.name = 'Zaxlct';
Person.prototype.sayName = function() {
  alert(this.name);
}
var person1 = new Person();
//JS 在创建对象的时候,都有一个__proto__ 的内置属性,用于指向创建它的构造函数的原型对象。
//每个对象都有 __proto__ 属性,但只有函数对象才有 prototype 属性
// 对象 person1 有一个 __proto__属性,创建它的构造函数是 Person,构造函数的原型对象是 Person.prototype
console.log(person1.__proto__ == Person.prototype) //true
//所有函数对象的__proto__都指向Function.prototype
String.__proto__ === Function.prototype  // true
String.constructor == Function //true
prototype.jpg

JS继承的几种方式

详解

  1. 原型继承

function Parent () {
  this.name = 'Parent'
  this.sex = 'boy'
}
function Child () {
  this.name = 'child'
}
// 将子类的原型对象指向父类的实例
Child.prototype = new Parent()
//优:继承了父类的模板,又继承了父类的原型对象
//缺:1.无法实现多继承(因为已经指定了原型对象了)
//   2.创建子类时,无法向父类构造函数传参数
  1. 构造函数继承

在子类构造函数内部使用call或apply来调用父类构造函数,复制父类的实例属性给子类。

function Parent (name) {
  this.name = name
}
function Child () {
  //用.call 来改变 Parent 构造函数内的指向
  Parent.call(this, 'child')
}
//优:解决了原型链继承中子类实例共享父类引用对象的问题,实现多继承,创建子类实例时,可以向父类传递参数
//缺:构造继承只能继承父类的实例属性和方法,不能继承父类原型的属性和方法
  1. 组合继承

    组合继承就是将原型链继承与构造函数继承组合在一起。

  • 使用原型链继承来保证子类能继承到父类原型中的属性和方法

  • 使用构造继承来保证子类能继承到父类的实例属性和方法

  • 寄生组合继承

  • class继承

  • class 中继承主要是依靠两个东西:

    • extends

    • super

    class Parent {
      constructor (name) {
        this.name = name
      }
      getName () {
        console.log(this.name)
      }
    }
    class Child extends Parent {
      constructor (name) {
        super(name)
        this.sex = 'boy'
      }
    }
    

    Event Loop 事件循环

    同步与异步、宏任务和微任务分别是函数两个不同维度的描述。

    同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入任务队列task queue)的任务,只有等主线程任务执行完毕,任务队列开始通知主线程,请求执行任务,该任务才会进入主线程执行。

    当某个宏任务执行完后,会查看是否有微任务队列。如果有,先执行微任务队列中的所有任务;如果没有,在执行环境栈中会读取宏任务队列中排在最前的任务;执行宏任务的过程中,遇到微任务,依次加入微任务队列。栈空后,再次读取微任务队列里的任务,依次类推。

    同步(Promise)>异步(微任务(process.nextTick ,Promises.then, Promise.catch ,resove,reject,MutationObserver)>宏任务(setTimeout,setInterval,setImmediate))

    await阻塞 后面的代码执行,因此跳出async函数执行下一个微任务

    Promise 与 Async/Await  区别

    async/await是基于Promise实现的,看起来更像同步代码,

    • 不需要写匿名函数处理Promise的resolve值

    • 错误处理: Async/Await 让 try/catch 可以同时处理同步和异步错误。

    • 条件语句也跟错误处理一样简洁一点

    • 中间值处理(第一个方法返回值,用作第二个方法参数) 解决嵌套问题

    • 调试方便

    const makeRequest = () => {
        try {
            getJSON().then(result => {
                // JSON.parse可能会出错
                const data = JSON.parse(result)
                console.log(data)
            })
            // 取消注释,处理异步代码的错误
            // .catch((err) => {
            //   console.log(err)
            // })
        } catch (err) {
            console.log(err)
        }
    }
    

    使用aync/await的话,catch能处理JSON.parse错误:

    const makeRequest = async () => {
        try {
            // this parse may fail
            const data = JSON.parse(await getJSON())
            console.log(data)
        } catch (err) {
            console.log(err)
        }
    }
    

    promise怎么实现链式调用跟返回不同的状态

    实现链式调用:使用.then()或者.catch()方法之后会返回一个promise对象,可以继续用.then()方法调用,再次调用所获取的参数是上个then方法return的内容

    1. promise的三种状态是 fulfilled(已成功)/pengding(进行中)/rejected(已拒绝)

    2. 状态只能由 Pending --> Fulfilled 或者 Pending --> Rejected,且一但发生改变便不可二次修改;

    3. Promise 中使用 resolve 和 reject 两个函数来更改状态;

    4. then 方法内部做的事情就是状态判断:

    • 如果状态是成功,调用成功回调函数

    • 如果状态是失败,调用失败回调函数

    函数柯里化

    柯里化(Currying) 是把接收多个参数的原函数变换成接受一个单一参数(原来函数的第一个参数的函数)并返回一个新的函数,新的函数能够接受余下的参数,并返回和原函数相同的结果。

    1. 参数对复用

    2. 提高实用性

    3. 延迟执行 只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。柯里化的函数可以延迟接收参数,就是比如一个函数需要接收的参数是两个,执行的时候必须接收两个参数,否则没法执行。但是柯里化后的函数,可以先接收一个参数

    // 普通的add函数
    function add(x, y) {
        return x + y
    }
    
    // Currying后
    function curryingAdd(x) {
        return function (y) {
            return x + y
        }
    }
    
    add(1, 2)           // 3
    curryingAdd(1)(2)   // 3
    

    JS对象深克隆

    递归遍历对象,解决循环引用问题

    解决循环引用问题,我们需要一个存储容器存放当前对象和拷贝对象的对应关系(适合用key-value的数据结构进行存储,也就是map),当进行拷贝当前对象的时候,我们先查找存储容器是否已经拷贝过当前对象,如果已经拷贝过,那么直接把返回,没有的话则是继续拷贝。

    function deepClone(target) {
        const map = new Map()
        function clone (target) {
            if (isObject(target)) {
                let cloneTarget = isArray(target) ? [] : {};
                if (map.get(target)) {
                    return map.get(target)
                }
                map.set(target,cloneTarget)
                for (const key in target) {
                    cloneTarget[key] = clone(target[key]);
                }
                return cloneTarget;
            } else {
                return target;
            }
        }
        return clone(target)
    };
    

    JS模块化

    nodeJS里面的模块是基于commonJS规范实现的,原理是文件的读写,导出文件要使用exportsmodule.exports,引入文件用require。每个文件就是一个模块;每个文件里面的代码会用默认写在一个闭包函数里面AMD规范则是非同步加载模块,允许指定回调函数,AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。

    AMD推崇依赖前置CMD推崇依赖就近。对于依赖的模块AMD是提前执行,CMD是延迟执行。

    ES6中,我们可以使用 import 关键字引入模块,通过 exprot 关键字导出模块,但是由于ES6目前无法在浏览器中执行,所以,我们只能通过babel将不被支持的import编译为当前受到广泛支持的 require

    CommonJs 和 ES6 模块化的区别:

    1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

    2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

    前端模块化:CommonJS,AMD,CMD,ES6

    import 和 require 导入的区别

    import 的ES6 标准模块;require 是 AMD规范引入方式;

    import是编译时调用,所以必须放在文件开头;是解构过程 require是运行时调用,所以require理论上可以运用在代码的任何地方;是赋值过程。其实require的结果就是对象、数字、字符串、函数等,再把require的结果赋值给某个变量

    异步加载JS方式

    1. 匿名函数自调动态创建script标签加载js

    (function(){
        var scriptEle = document.createElement("script");
        scriptEle.type = "text/javasctipt";
        scriptEle.async = true;
        scriptEle.src = "http://cdn.bootcss/jquery/3.0.0-beta1/jquery.min.js";
        var x = document.getElementsByTagName("head")[0];
        x.insertBefore(scriptEle, x.firstChild);  
     })();
    
    1. async属性

    // async属性规定一旦加载脚本可用,则会异步执行
    <script type="text/javascript" src="xxx.js" async="async"></script>
    
    1. defer属性

    // defer属性规定是否对脚本执行进行延迟,直到页面加载为止
    <script type="text/javascript" src="xxx.js" defer="defer"></script>
    

    Set、Map、WeakSet、WeakMap

    Set对象可以存储任何类型的数据。值是唯一的,没有重复的值。

    Map对象保存键值对,任意值都可以成为它的键或值。

    WeakSet 结构与 Set 类似,也是不重复的值的集合 . WeakMap 对象是一组键值对的集合

    不同:WeakSet 的成员只能是对象,而不能是其他类型的值。WeakSet 不可遍历

    WeakMap只接受对象作为键名null除外),不接受其他类型的值作为键名。

    WeakMap的键名所指向的对象,不计入垃圾回收机制。

    call、apply

    call( this,a,b,c ) 在第一个参数之后的,后续所有参数就是传入该函数的值。apply( this,[a,b,c] ) 只有两个参数,第一个是对象,第二个是数组,这个数组就是该函数的参数。

    共同之处:都可以用来代替另一个对象调用一个方法,将一个函数的对象上下文从初始的上下文改变为由thisObj指定的新对象。

    所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。

    addEventListener的第三个参数干嘛的,为true时捕获,false时冒泡

    Object.prototype.toString.call() 判断对象类型

    // new Set是实现数组去重,
    // Array.from()把去重之后转换成数组
    let arr2 = Array.from(new Set(arr));
    

    词法作用域与作用域链

    作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

    ES5只有全局作用域没和函数作用域,ES6增加块级作用域

    暂时性死区:在代码块内,使用 let 和 const 命令声明变量之前,该变量都是不可用的,语法上被称为暂时性死区。

    JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。

    函数的作用域在函数定义的时候就决定了。

    当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链

    new关键字做了4件事:

    function _new(constructor, ...arg) {
    // 创建一个空对象
      var obj = {};
      // 空对象的`__proto__`指向构造函数的`prototype`, 为这个新对象添加属性 
      obj.__proto__ = constructor.prototype; 
      // 构造函数的作用域赋给新对象
      var res = constructor.apply(obj, arg); 
      // 返回新对象.如果没有显式return语句,则返回this
      return Object.prototype.toString.call(res) === '[object Object]' ? res : obj; 
    }
    

    不应该使用箭头函数一些情况:

    • 当想要函数被提升时(箭头函数是匿名的)

    • 要在函数中使用this/arguments时,由于箭头函数本身不具有this/arguments,因此它们取决于外部上下文

    • 使用命名函数(箭头函数是匿名的)

    • 使用函数作为构造函数时(箭头函数没有构造函数)

    • 当想在对象字面是以将函数作为属性添加并在其中使用对象时,因为咱们无法访问 this 即对象本身。

    判断数组的四种方法

    1. Array.isArray() 判断

    2. instanceof 判断: 检验构造函数的prototype属性是否出现在对象的原型链中,返回一个布尔值。let a = []; a instanceof Array; //true

    3. constructor判断: 实例的构造函数属性constructor指向构造函数let a = [1,3,4]; a.constructor === Array;//true

    4. Object.prototype.toString.call() 判断let a = [1,2,3]; Object.prototype.toString.call(a) === '[object Array]';//true

    TS有什么优势

    1. 静态输入:静态类型化是一种功能,可以在开发人员编写脚本时检测错误。

    2. 大型的开发项目:使用TypeScript工具来进行重构更变的容易、快捷。

    3. 更好的协作:类型安全是在编码期间检测错误,而不是在编译项目时检测错误。

    4. 更强的生产力:干净的 ECMAScript 6 代码,自动完成和动态输入等因素有助于提高开发人员的工作效率。

    interface 和 type的区别

    • interface 只能定义对象类型。type声明可以声明任何类型。

    • interface 能够声明合并,两个相同接口会合并。Type声明合并会报错

    • type可以类型推导

    框架 Vue | React

    Vue3.0 新特性

    双向数据绑定 Proxy

    代理,可以理解为在对象之前设置一个“拦截”,当该对象被访问的时候,都必须经过这层拦截。意味着你可以在这层拦截中进行各种操作。比如你可以在这层拦截中对原对象进行处理,返回你想返回的数据结构。

    ES6 原生提供 Proxy 构造函数,MDN上的解释为:Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。

    const p = new Proxy(target, handler);
    //target: 所要拦截的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
    //handler:一个对象,定义要拦截的行为
    
    const p = new Proxy({}, {
        get(target, propKey) {
            return '哈哈,你被我拦截了';
        }
    });
    
    console.log(p.name);
    

    新增的属性,并不需要重新添加响应式处理,因为 Proxy 是对对象的操作,只要你访问对象,就会走到 Proxy 的逻辑中。

    Vue3 Composition API

    Vue3.x 推出了Composition APIsetup 是组件内使用 Composition API的入口。setup 执行时机是在 beforeCreate 之前执行.

    reactive、ref 与 toRefs、isRef

    Vue3.x 可以使用reactive和ref来进行数据定义。

    // props 传入组件对属性
    // context 一个上下文对象,包含了一些有用的属性:attrs,parent,refs
    setup(props, context) {
      // ref 定义数据
      const year = ref(0);
      // reactive 处理对象的双向绑定
      const user = reactive({ nickname: "xiaofan", age: 26, gender: "女" });
      setInterval(() => {
        year.value++;
        user.age++;
      }, 1000);
      return {
        year,
        // 使用toRefs,结构解构
        ...toRefs(user),
      };
    },
    // 提供isRef,用于检查一个对象是否是ref对象
    

    watchEffect 监听函数

    • watchEffect 不需要手动传入依赖

    • watchEffect 会先执行一次用来自动收集依赖

    • watchEffect 无法获取到变化前的值, 只能获取变化后的值

    computed可传入get和set

    用于定义可更改的计算属性

    const plusOne = computed({
     get: () => count.value + 1,
     set: val => { count.value = val - 1 }
    });
    

    使用TypeScript和JSX

    setup现在支持返回一个渲染函数,这个函数返回一个JSXJSX可以直接使用声明在setup作用域的响应式状态:

    export default {
     setup() {
     const count = ref(0);
     return () => (<div>{count.value}</div>);
     },
    };
    

    Vue 跟React 对比?

    相同点:

    1. 都有虚拟DOM(Virtual DOM 是一个映射真实DOM的JavaScript对象)

    2. 都提供了响应式和组件化的视图组件。

    不同点:Vue 是MVVM框架,双向数据绑定,当ViewModelModel进行更新时,通过数据绑定更新到View

    React是一个单向数据流的库,状态驱动视图。State --> View --> New State --> New View ui = render (data)

    模板渲染方式不同。React是通过JSX来渲染模板,而Vue是通过扩展的HTML来进行模板的渲染。

    组件形式不同,Vue文件里将HTML,JS,CSS组合在一起。react提供class组件和function组

    Vue封装好了一些v-if,v-for,React什么都是自己实现,自由度更高

    Vue 初始化过程,双向数据绑定原理

    vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetterdep.addSub来收集订阅的依赖,watcher监听数据的变化,在数据变动时发布消息给订阅者,触发相应的监听回调。

    监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而调用对应update更新视图。

    v-model 指令,它能轻松实现表单输入和应用状态之间的双向绑定。

    computed: 支持缓存,只有依赖数据结果发生改变,才会重新进行计算,不支持异步操作,如果一个属性依赖其他属性,多对一,一般用computed

    watch: 数据变,直接触发相应操作,支持异步,监听数据必须data中声明过或者父组件传递过来的props中的数据,当数据变化时,触发其他操作,函数有两个参数

    vue-router实现原理

    端路由简介以及vue-router实现原理原理核心就是 更新视图但不重新请求页面。路径之间的切换,也就是组件的切换。vue-router实现单页面路由跳转模式:hash模式、history模式。根据设置mode参数

    hash模式:通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置。history模式:利用 window.history.pushState API 来完成 URL 跳转而无须重新加载页面。

    vuex实现原理:

    Vue.use(vuex)会调用vuex的install方法

    beforeCreate钩子前混入vuexInit方法,vuexInit方法实现了store注入vue组件实例,并注册了vuex store的引用属性$store

    Vuexstate状态是响应式,是借助vuedata是响应式,将state存入vue实例组件的data中;

    Vuexgetters则是借助vue的计算属性computed实现数据实时监听。

    nextTick 的原理以及运行机制?

    nextTick的源码分析

    vue进行DOM更新内部也是调用nextTick来做异步队列控制。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。

    DOM至少会在当前事件循环里面的所有数据变化完成之后,再统一更新视图。而当我们自己调用nextTick的时候,它就在更新DOM的microtask(微任务队列)后追加了我们自己的回调函数

    从而确保我们的代码在DOM更新后执行,同时也避免了setTimeout可能存在的多次执行问题。确保队列中的微任务在一次事件循环前被执行完毕。

    Vue 实现一个高阶组件

    高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。在不改变对象自身的前提下在程序运行期间动态的给对象添加一些额外的属性或行为

    // 高阶组件(HOC)接收到的 props 应该透传给被包装组件即直接将原组件prop传给包装组件
    // 高阶组件完全可以添加、删除、修改 props
    export default function Console(BaseComponent) {
      return {
        props: BaseComponent.props,
        mounted() {
          console.log("高阶组件");
        },
        render(h) {
          console.log(this);
          // 将 this.$slots 格式化为数组,因为 h 函数第三个参数是子节点,是一个数组
          const slots = Object.keys(this.$slots)
            .reduce((arr, key) => arr.concat(this.$slots[key]), [])
            .map((vnode) => {
              vnode.context = this._self; // 绑定到高阶组件上,vm:解决具名插槽被作为默认插槽进行渲染
              return vnode;
            });
     
          // 透传props、透传事件、透传slots
          return h(
            BaseComponent,
            {
              on: this.$listeners,
              attrs: this.$attrs, // attrs 指的是那些没有被声明为 props 的属性
              props: this.$props,
            },
            slots
          );
        },
      };
    }
    

    Vueponent()、Vue.use()、this.$xxx()

    Vueponent()方法注册全局组件。

    • 第一个参数是自定义元素名称,也就是将来在别的组件中使用这个组件的标签名称。

    • 第二个参数是将要注册的Vue组件。

    import Vue from 'vue';
    // 引入loading组件 
    import Loading from './loading.vue';
    // 将loading注册为全局组件,在别的组件中通过<loading>标签使用Loading组件
    Vueponent('loading', Loading);
    

    Vue.use注册插件,这接收一个参数。这个参数必须具有install方法。Vue.use函数内部会调用参数的install方法。

    • 如果插件没有被注册过,那么注册成功之后会给插件添加一个installed的属性值为true。Vue.use方法内部会检测插件的installed属性,从而避免重复注册插件。

    • 插件的install方法将接收两个参数,第一个是参数是Vue,第二个参数是配置项options。

    • 在install方法内部可以添加全局方法或者属性

    import Vue from 'vue';
    
    // 这个插件必须具有install方法
    const plugin = {
      install (Vue, options) {
        // 添加全局方法或者属性
        Vue.myGlobMethod = function () {};
        // 添加全局指令
        Vue.directive();
        // 添加混入
        Vue.mixin();
        // 添加实例方法
        Vue.prototype.$xxx = function () {};
        // 注册全局组件
        Vueponent()
      }
    }
    
    // Vue.use内部会调用plugin的install方法
    Vue.use(plugin);
    

    将Hello方法挂载到Vue的prototype上.

    import Vue from 'vue';
    import Hello from './hello.js';
    Vue.prototype.$hello = Hello;
    

    vue组件中就可以this.$hello('hello world')

    Vue父组件传递props数据,子组件修改参数

    • 父子组件传值时,父组件传递的参数,数组和对象,子组件接受之后可以直接进行修改,并且父组件相应的值也会修改。控制台中发出警告。

    • 如果传递的值是字符串,直接修改会报错。单向数据流,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。

    如果子组件想修改prop中数据:

    1. 定义一个局部变量,使用prop的值赋值

    2. 定义一个计算属性,处理prop的值并返回

    Vue父子组件生命周期执行顺序

    加载渲染过程

    父beforeCreate -> 父created -> 父beforeMount-> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted

    子组件更新过程

    父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated

    父组件更新过程

    父beforeUpdate -> 父updated

    销毁过程

    父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed

    Vue 自定义指令

    自定义指令提供了几个钩子函数:bind:指令第一次绑定到元素时调用inserted:被绑定元素插入父节点时调用update:所在组件的 VNode 更新时调用

    使用slot后可以在子组件内显示插入的新标签

    webpack 及工程化

    webpack的生命周期,及钩子

    compiler(整个生命周期 [kəmˈpaɪlər]) 钩子 https://webpack.docschina/api/compiler-hooks/compilation(编译 [ˌkɑːmpɪˈleɪʃn]) 钩子

    compiler对象包含了Webpack 环境所有的的配置信息。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。

    compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。当运行webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。

    compiler代表了整个webpack从启动到关闭的生命周期,而compilation 只是代表了一次新的编译过程

    webpack 编译过程

    Webpack 的编译流程是一个串行的过程,从启动到结束会依次执行以下流程:

    1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;

    2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run方法开始执行编译;

    3. 确定入口:根据配置中的 entry 找出所有的入口文件;

    4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;

    5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;

    6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;

    7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

    优化项目的webpack打包编译过程

    1.构建打点:构建过程中,每一个Loader 和 Plugin 的执行时长,在编译 JS、CSS 的 Loader 以及对这两类代码执行压缩操作的 Plugin上消耗时长 。一款工具:speed-measure-webpack-plugin

    2.缓存:大部分 Loader 都提供了cache 配置项。cache-loader ,将 loader 的编译结果写入硬盘缓存

    3.多核编译happypack项目接入多核编译,理解为happypack 将编译工作灌满所有线程

    4.抽离webpack-dll-plugin 将这些静态依赖从每一次的构建逻辑中抽离出去,静态依赖单独打包,Externals将不需要打包的静态资源从构建逻辑中剔除出去,使用CDN 引用

    5.tree-shaking,虽然依赖了某个模块,但其实只使用其中的某些功能。通过 tree-shaking,将没有使用的模块剔除,来达到删除无用代码的目的。

    首屏加载优化

    路由懒加载:改为用import引用,以函数的形式动态引入,可以把各自的路由文件分别打包,只有在解析给定的路由时,才会下载路由组件;

    element-ui按需加载:引用实际上用到的组件 ;

    组件重复打包CommonsChunkPlugin配置来拆包,把使用2次及以上的包抽离出来,放进公共依赖文件,首页也有复用的组件,也会下载这个公共依赖文件;

    gzip: 拆完包之后,再用gzip做一下压缩,关闭sourcemap。

    UglifyJsPlugin:  生产环境,压缩混淆代码,移除console代码

    CDN部署静态资源:静态请求打在nginx时,将获取静态资源的地址进行重定向CDN内容分发网络

    移动端首屏加载可以使用骨架屏,自定义loading,首页单独做服务端渲染

    如何进行前端性能优化(21种优化+7种定位方式)

    webpack 热更新机制

    热更新流程总结:

    • 启动本地server,让浏览器可以请求本地的静态资源

    • 页面首次打开后,服务端与客户端通过 websocket建立通信渠道,把下一次的 hash 返回前端

    • 客户端获取到hash,这个hash将作为下一次请求服务端 hot-update.js 和 hot-update.json的hash

    • 修改页面代码后,Webpack 监听到文件修改后,开始编译,编译完成后,发送 build 消息给客户端

    • 客户端获取到hash,成功后客户端构造hot-update.js script链接,然后插入主文档

    • hot-update.js 插入成功后,执行hotAPI 的 createRecord 和 reload方法,获取到 Vue 组件的 render方法,重新 render 组件, 继而实现 UI 无刷新更新。

    webpack的 loader和plugin介绍,css-loader,style-loader的区别

    loader 它就是一个转换器,将A文件进行编译形成B文件,

    plugin ,它就是一个扩展器,来操作的是文件,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,会监听webpack打包过程中的某些节点(run, build-module, program)

    Babel 能把ES6/ES7的代码转化成指定浏览器能支持的代码。

    css-loader 的作用是把 css文件进行转码style-loader: 使用<style>css-loader内部样式注入到我们的HTML页面

    先使用 css-loader转码,然后再使用 style-loader插入到文件

    如何编写一个webpack的plugin?

    https://segmentfault/a/1190000037513682

    webpack 插件的组成:

    • 一个 JS 命名函数或一个类(可以想下我们平时使用插件就是 new XXXPlugin()的方式)

    • 在插件类/函数的 (prototype) 上定义一个 apply 方法。

    • 通过 apply 函数中传入 compiler 并插入指定的事件钩子,在钩子回调中取到 compilation 对象

    • 通过 compilation 处理 webpack 内部特定的实例数据

    • 如果是插件是异步的,在插件的逻辑编写完后调用 webpack 提供的 callback

    为什么 Vite 启动这么快

    Webpack 会先打包,然后启动开发服务器,请求服务器时直接给予打包结果。

    而 Vite 是直接启动开发服务器,请求哪个模块再对该模块进行实时编译

    Vite 将开发环境下的模块文件,就作为浏览器要执行的文件,而不是像 Webpack 那样进行打包合并

    由于 Vite 在启动的时候不需要打包,也就意味着不需要分析模块的依赖不需要编译。因此启动速度非常快。当浏览器请求某个模块时,再根据需要对模块内容进行编译。

    你的脚手架是怎么做的

    使用 download-git-repo 下载仓库代码democommander:完整的 node.js 命令行解决方案。声明program,使用.option() 方法来定义选项Inquirer.js:命令行用户界面的集合。

    前端监控

    前端监控通常包括行为监控(PV/UV,埋点接口统计)、异常监控性能监控

    一个监控系统,大致可以分为四个阶段:日志采集日志存储统计与分析报告和警告

    错误监控

    Vue专门的错误警告的方法 Vue.config.errorHandler,(Vue提供只能捕获其页面生命周期内的函数,比如created,mounted)

    Vue.config.errorHandler = function (err) {
    console.error(‘Vue.error’,err.stack)
    // 逻辑处理
    };
    

    框架:betterjsfundebug(收费) 捕获错误的脚本要放置在最前面,确保可以收集到错误信息 方法:

    1. window.onerror()当有js运行时错误触发时,onerror可以接受多个参数(message, source, lineno, colno, error)。

    2. window.addEventListener('error'), function(e) {}, true 会比window.onerror先触发,不能阻止默认事件处理函数的执行,但可以全局捕获资源加载异常的错误

    前端JS错误捕获--sourceMap

    如何监控网页崩溃?**崩溃和卡顿有何差别?**监控错误

    1. Service Worker 有自己独立的工作线程,与网页区分开,网页崩溃了,Service Worker 一般情况下不会崩溃;

    2. Service Worker 生命周期一般要比网页还要长,可以用来监控网页的状态;

      卡顿:加载中,渲染遇到阻塞

    性能监控 && 性能优化

    性能指标:

    • FP(首次绘制)

    • FCP(首次内容绘制 First contentful paint)

    • LCP(最大内容绘制时间 Largest contentful paint)

    • FPS(每秒传输帧数)

    • TTI(页面可交互时间 Time to Interactive)

    • HTTP 请求响应时间

    • DNS 解析时间

    • TCP 连接时间

    性能数据采集需要使用 window.performance API ,   JS库 web-vitalsimport {getLCP} from 'web-vitals';

        // 重定向耗时
        redirect: timing.redirectEnd - timing.redirectStart,
        // DOM 渲染耗时
        dom: timing.domComplete - timing.domLoading,
        // 页面加载耗时
        load: timing.loadEventEnd - timing.navigationStart,
        // 页面卸载耗时
        unload: timing.unloadEventEnd - timing.unloadEventStart,
        // 请求耗时
        request: timing.responseEnd - timing.requestStart,
        // 获取性能信息时当前时间
        time: new Date().getTime(),
        // DNS查询耗时
        domainLookupEnd - domainLookupStart
     // TCP链接耗时
        connectEnd - connectStart
     // request请求耗时
        responseEnd - responseStart
     // 解析dom树耗时
        domComplete - domInteractive
     // 白屏时间
        domloadng - fetchStart
     // onload时间
        loadEventEnd - fetchStart
    

    性能优化常用手段:缓存技术、   预加载技术、   渲染方案。

    1. 缓存 :主要有 cdn、浏览器缓存、本地缓存以及应用离线包

    2. 预加载 :资源预拉取(prefetch)则是另一种性能优化的技术。通过预拉取可以告诉浏览器用户在未来可能用到哪些资源。

    • prefetch支持预拉取图片、脚本或者任何可以被浏览器缓存的资源。

      在head里 添加 <linkrel="prefetch"href="image.png">

    • prerender是一个重量级的选项,它可以让浏览器提前加载指定页面的所有资源。

    • subresource可以用来指定资源是最高优先级的。当前页面需要,或者马上就会用到时。

    1. 渲染方案

    • 静态渲染(SR)

    • 前端渲染(CSR)

    • 服务端渲染(SSR)

    • 客户端渲染(NSR):NSR 数据请求,首屏数据请求和数据线上与 webview 的一个初始化和框架 JS 初始化并行了起来,大大缩短了首屏时间。

    640.png

    常见的六种设计模式以及应用场景

    https://wwwblogs/whu-2017/p/9471670.html

    观察者模式的概念

    观察者模式模式,属于行为型模式的一种,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主体对象在状态变化时,会通知所有的观察者对象。

    发布订阅者模式的概念

    发布-订阅模式,消息的发送方,叫做发布者(publishers),消息不会直接发送给特定的接收者,叫做订阅者。意思就是发布者和订阅者不知道对方的存在。需要一个第三方组件,叫做信息中介,它将订阅者和发布者串联起来,它过滤和分配所有输入的消息。换句话说,发布-订阅模式用来处理不同系统组件的信息交流,即使这些组件不知道对方的存在。

    需要一个第三方组件,叫做信息中介,它将订阅者和发布者串联起来

    工厂模式  主要是为创建对象提供了接口。场景:在编码时不能预见需要创建哪种类的实例。

    代理模式 命令模式

    单例模式

    保证一个类仅有一个实例,并提供一个访问它的全局访问点。(window)

    Http 及浏览器相关

    七层网络模型

    应用层、表示层、会话层、传输层、网络层、数据链路层、物理层

    TCP:面向连接、传输可靠(保证数据正确性,保证数据顺序)、用于传输大量数据(流模式)、速度慢,建立连接需要开销较多(时间,系统资源) 。(应用场景:HTP,HTTP,邮件)

    UDP:面向非连接、传输不可靠、用于传输少量数据(数据包模式)、速度快 ,可能丢包(应用场景:即时通讯)

    是否连接     面向连接     面向非连接
    传输可靠性   可靠        不可靠
    应用场合    少量数据    传输大量数据
    

    https

    客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。服务器公钥放在数字证书中。

    url到加载渲染全过程

    1. DNS域名解析。

    2. TCP三次握手,建立接连。

    3. 发送HTTP请求报文。

    4. 服务器处理请求返回响应报文。

    5. 浏览器解析渲染页面。

    6. 四次挥手,断开连接。

    DNS 协议提供通过域名查找 IP地址,或逆向从 IP地址反查域名的服务。DNS 是一个网络服务器,我们的域名解析简单来说就是在 DNS 上记录一条信息记录。

    TCP 三次握手,四次挥手:握手挥手都是客户端发起,客户端结束。三次握手与四次挥手详解

    负载均衡:请求在进入到真正的应用服务器前,可能还会先经过负责负载均衡的机器,它的作用是将请求合理地分配到多个服务器上,转发HTTP请求;同时具备具备防攻击等功能。可分为DNS负载均衡,HTTP负载均衡,IP负载均衡,链路层负载均衡等。

    Web Server:请求经过前面的负载均衡后,将进入到对应服务器上的 Web Server,比如 ApacheTomcat

    反向代理是工作在 HTTP 上的,一般都是 Nginx。全国各地访问baidu就肯定要通过代理访问,不可能都访问百度的那台服务器。 (VPN正向代理,代理客户端)

    浏览器解析渲染过程:返回的html传递到浏览器后,如果有gzip会先解压,找出文件编码格式,外链资源的加载 html从上往下解析,遇到js,css停止解析渲染,直到js执行完成。解析HTML,构建DOM树 解析CSS,生成CSS规则树 合并DOM树和CSS规则,生成render树去渲染

    不会引起DOM树变化,页面布局变化,改变元素样式的行为叫重绘

    引起DOM树结构变化,页面布局变化的行为叫回流

    GUI渲染线程负责渲染浏览器界面HTML元素,当界面需要 重绘(Repaint) 或由于某种操作引发 回流(reflow) 时,该线程就会执行。在Javascript引擎运行脚本期间,GUI渲染线程都是处于挂起状态的,也就是说被”冻结”了. 直到JS程序执行完成,才会接着执行。因此如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面,渲染前后元素数据可能不一致

    GPU绘制多进程的浏览器:主控进程,插件进程,GPU,tab页(浏览器内核)多线程的浏览器内核:每一个tab页面可以看作是浏览器内核进程,然后这个进程是多线程的。

    它有几大类子线程:

    • GUI线程

    • JS引擎线程

    • 事件触发线程

    • 定时器线程

    • HTTP请求线程

    http1 跟HTTP2

    http2

    多路复用:相同域名多个请求,共享同一个TCP连接,降低了延迟

    请求优先级:给每个request设置优先级

    二进制传输;之前是用纯文本传输

    数据流:数据包不是按顺序发送,对数据包做标记。每个请求或回应的所有数据包成为一个数据流,

    服务端推送:可以主动向客户端发送消息。

    头部压缩:减少包的大小跟数量

    HTTP/1.1 中的管道( pipeline)传输中如果有一个请求阻塞了,那么队列后请求也统统被阻塞住了 HTTP/2 多请求复用一个TCP连接,一旦发生丢包,就会阻塞住所有的 HTTP 请求。HTTP/3 把 HTTP 下层的 TCP 协议改成了 UDP!http1 keep alive 串行传输

    http 中的 keep-alive 有什么作用

    响应头中设置 keep-alive 可以在一个 TCP 连接上发送多个 http 请求

    浏览器缓存策略

    强缓存:cache-control;no-cache max-age=<10000000>;expires;其中Cache-Conctrol的优先级比Expires高;

    控制强制缓存的字段分别是Expires和Cache-Control,如果客户端的时间小于Expires的值时,直接使用缓存结果。

    协商缓存:Last-Modified / If-Modified-Since和Etag / If-None-Match,其中Etag / If-None-Match的优先级比Last-Modified / 首次请求,服务器会在返回的响应头中加上Last-Modified字段,表示资源最后修改的时间。

    浏览器再次请求时,请求头中会带上If-Modified-Since字段,比较两个字段,一样则证明资源未修改,返回304,否则重新返回资源,状态码为200;

    垃圾回收机制:

    标记清除:进入执行环境的变量都被标记,然后执行完,清除这些标记跟变量。查看变量是否被引用。

    引用计数:会记录每个值被引用的次数,当引用次数变成0后,就会被释放掉。

    前端安全

    同源策略:如果两个 URL 的协议、域名和端口都相同,我们就称这两个 URL 同源。因为浏览器有cookies

    • XSS:跨站脚本攻击(Cross Site Scripting) input, textarea等所有可能输入文本信息的区域,输入<script src="http://恶意网站"></script>等,提交后信息会存在服务器中 。

    • CSRF:跨站请求伪造 。引诱用户打开黑客的网站,在黑客的网站中,利用用户的登录状态发起的跨站请求。

      A站点imgsrc=B站点的请求接口,可以访问;解决:referer携带请求来源

      访问该页面后,表单自动提交, 模拟完成了一次POST操作,发送post请求

      解决:后端注入一个随机串Cookie,前端请求取出随机串添加传给后端。

    • http 劫持:电信运营商劫持

    • SQL注入

    • 点击劫持:诱使用户点击看似无害的按钮(实则点击了透明 iframe中的按钮) ,解决后端请求头加一个字段 X-Frame-Options

    • 文件上传漏洞 :服务器未校验上传的文件

    CSS 及 HTML

    什么是BFC(块级格式化上下文)、IFC(内联格式化上下文 )、FFC(弹性盒模型)

    BFC(Block formatting context),即块级格式化上下文,它作为HTML页面上的一个独立渲染区域,只有区域内元素参与渲染,且不会影响其外部元素。简单来说,可以将 BFC 看做是一个“围城”,外面的元素进不来,里面的元素出不去(互不干扰)。

    一个决定如何渲染元素的容器 ,渲染规则 :

    • 1、内部的块级元素会在垂直方向,一个接一个地放置。

    • 2、块级元素垂直方向的距离由margin决定。属于同一个BFC的两个相邻块级元素的margin会发生重叠。

    • 3、对于从左往右的格式化,每个元素(块级元素与行内元素)的左边缘,与包含块的左边缘相接触,(对于从右往左的格式化则相反)。即使包含块中的元素存在浮动也是如此,除非其中元素再生成一个BFC。

    • 4、BFC的区域不会与浮动元素重叠。

    • 5、BFC是一个隔离的独立容器,容器里面的子元素和外面的元素互不影响。

    • 6、计算BFC容器的高度时,浮动元素也参与计算。

    形成BFC的条件:

    1、浮动元素,float 除 none 以外的值;

    2、定位元素,position(absolute,fixed);

    3、display 为以下其中之一的值 inline-block,table-cell,table-caption;

    4、overflow 除了 visible 以外的值(hidden,auto,scroll);

    BFC 一般用来解决以下几个问题

    • 边距重叠问题

    • 消除浮动问题

    • 自适应布局问题

    flex: 0 1 auto; 是什么意思?

    元素会根据自身宽高设置尺寸。它会缩短自身以适应 flex 容器,但不会伸长并吸收 flex 容器中的额外自由空间来适应 flex 容器 。水平的主轴(main axis)和垂直的交叉轴(cross axis)几个属性决定按哪个轴的排列方向

    • flex-grow0  一个无单位数(): 它会被当作<flex-grow>的值。

    • flex-shrink1  一个有效的**宽度(width)**值: 它会被当作 <flex-basis>的值。

    • flex-basisauto  关键字noneautoinitial.

    放大比例、缩小比例、分配多余空间之前占据的主轴空间。

    避免CSS全局污染

    1. scoped 属性

    2. css in js

    const styles = {
      bar: {
        backgroundColor: '#000'
      }
    }
    const example = (props)=>{
      <div style={styles.bar} />
    }
    
    1. CSS Modules

    2. 使用less,尽量少使用全局对选择器

    // 选择器上>要记得写,免得污染所有ul下面的li
    ul{
      >li{
        color:red;
      }
    }
    

    CSS Modules

    阮一峰 CSS Modules

    CSS Modules是一种构建步骤中的一个进程。通过构建工具来使指定class达到scope的过程。

    CSS Modules 允许使用::global(.className)的语法,声明一个全局规则。凡是这样声明的class,都不会被编译成哈希字符串:local(className): 做 localIdentName 规则处理,编译唯一哈希类名。

    CSS Modules使用特点:

    • 不使用选择器,只使用 class 名来定义样式

    • 不层叠多个 class,只使用一个 class 把所有样式定义好

    • 不嵌套class

    盒子模型和 box-sizing 属性

    width: 160px; padding: 20px; border: 8px solid orange;标准 box-sizing: content-box; 元素的总宽度 = 160 + 202 + 82; IE的 border-box:总宽度160

    margin/padding百分比的值时 ,基于父元素的宽度和高度的。

    css绘制三角形

    1. 通过border 处理

    // border 处理
    .class {
        width: 0;
        height: 0;
        border-left: 50px solid transparent;
        border-right: 50px solid transparent;
        border-bottom: 100px solid red;
    }
    // 宽高+border
    div {
        width: 50px;
        height: 50px;
        border: 2px solid orange;
    }
    
    1. clip-path裁剪获得

    div{
     clip-path: polygon(0 100%, 50% 0, 100% 100%);
    }
    
    1. 渐变linear-gradient 实现

    div {
      width: 200px;
      height: 200px;
      background:linear-gradient(to bottom right, #fff 0%, #fff 49.9%, rgba(148,88,255,1) 50%,rgba(185,88,255,1) 100%);
    }
    

    CSS实现了三角形后如何给三角形添加阴影

    ???

    CSS两列布局的N种实现

    两列布局分为两种,一种是左侧定宽、右侧自适应,另一种是两列都自适应(即左侧宽度由子元素决定,右侧补齐剩余空间)。

    1. 左侧定宽、右侧自适应如何实现

    // 两个元素都设置dislpay:inline-block
    .left {
        display: inline-block;
        width: 100px;
        height: 200px;
        background-color: red;
        vertical-align: top;
    }
    .right {
        display: inline-block;
        width: calc(100% - 100px);
        height: 400px;
        background-color: blue;
        vertical-align: top;
    }
    // 两个元素设置浮动,右侧自适应元素宽度使用calc函数计算
    .left{
        float: left;
        width: 100px;
        height: 200px;
        background-color: red;
    }
    .right{
        float: left;
        width: calc(100% - 100px);
        height: 400px;
        background-color: blue;
    }
    // 父元素设置display:flex,自适应元素设置flex:1
    .box{
        height: 600px;
        width: 100%;
        display: flex;
    }
    .left{
        width: 100px;
        height: 200px;
        background-color: red;
    }
    .right{
        flex: 1;
        height: 400px;
        background-color: blue;
    }
    // 父元素相对定位,左侧元素绝对定位,右侧自适应元素设置margin-left的值大于定宽元素的宽度
    .left{
        position: absolute;
        width: 100px;
        height: 200px;
        background-color: red;
    }
    .right{
        margin-left: 100px;
        height: 400px;
        background-color: blue;
    }
    
    1. 左右两侧元素都自适应

    // flex布局 同上
    // 父元素设置display:grid; grid-template-columns:auto 1fr;(这个属性定义列宽,auto关键字表示由浏览器自己决定长度。fr是一个相对尺寸单位,表示剩余空间做等分)grid-gap:20px(行间距)
    .parent{
        display:grid;
        grid-template-columns:auto 1fr;
        grid-gap:20px
    } 
    .left{
        background-color: red;
        height: 200px;
    }
    .right{
        height:300px;
        background-color: blue;
    }
    // 浮动+BFC   父元素设置overflow:hidden,左侧定宽元素浮动,右侧自适应元素设置overflow:auto创建BFC
    .box{
        height: 600px;
        width: 100%;
        overflow: hidden;
    }
    .left{
        float: left;
        width: 100px;
        height: 200px;
        background-color: red;
    }
    .right{
        overflow: auto;
        height: 400px;
        background-color: blue;
    }
    

    CSS三列布局

    1. float布局:左边左浮动,右边右浮动,中间margin:0 100px;

    2. Position布局: 左边left:0; 右边right:0; 中间left: 100px; right: 100px;

    3. table布局: 父元素 display: table; 左右 width: 100px; 三个元素display: table-cell;

    4. 弹性(flex)布局:父元素 display: flex; 左右 width: 100px;

    5. 网格(gird)布局:

    // gird提供了 gird-template-columns、grid-template-rows属性让我们设置行和列的高、宽
    .div{
        width: 100%;
        display: grid;
        grid-template-rows: 100px;
        grid-template-columns: 300px auto 300px;
    }
    

    常见场景及问题

    app与H5 如何通讯交互的?

    // 兼容IOS和安卓
    callMobile(parameters,messageHandlerName) {
      //handlerInterface由iOS addScriptMessageHandler与andorid addJavascriptInterface 代码注入而来。
      if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) {
        // alert('ios')
        window.webkit.messageHandlers[messageHandlerName].postMessage(JSON.stringify(parameters))
      } else {
        // alert('安卓')
        //安卓传输不了js json对象,只能传输string
        window.webkit[messageHandlerName](JSON.stringify(parameters))
      }
    }
    

    由app将原生方法注入到window上供js调用

    messageHandlerName 约定的通信方法parameters 需要传入的参数

    移动端适配方案

    rem是相对于HTML的根元素em相对于父级元素的字体大小。VW,VH 屏幕宽度高度的高分比

    //按照宽度375图算, 1rem = 100px;
    (function (win, doc) {
       function changeSize() {
         doc.documentElement.style.fontSize = doc.documentElement.clientWidth / 3.75 + 'px';
        console.log(100 * doc.documentElement.clientWidht / 3.75)
       }
       changeSize();
       win.addEventListener('resize', changeSize, false);
    
    })(window, document);
    

    代码编程相关

    实现发布订阅

    /* Pubsub */
    function Pubsub(){
      //存放事件和对应的处理方法
      this.handles = {};
    }
    
    Pubsub.prototype = {
      //传入事件类型type和事件处理handle
      on: function (type, handle) {
        if(!this.handles[type]){
          this.handles[type] = [];
        }
        this.handles[type].push(handle);
      },
      emit: function () {
        //通过传入参数获取事件类型
        //将arguments转为真数组
        var type = Array.prototype.shift.call(arguments);
        if(!this.handles[type]){
          return false;
        }
        for (var i = 0; i < this.handles[type].length; i++) {
          var handle = this.handles[type][i];
          //执行事件
          handle.apply(this, arguments);
        }
      },
      off: function (type, handle) {
        handles = this.handles[type];
        if(handles){
          if(!handle){
            handles.length = 0;//清空数组
          }else{
          for (var i = 0; i < handles.length; i++) {
            var _handle = handles[i];
            if(_handle === handle){
              //从数组中删除
              handles.splice(i,1);
            }
          }
        }
      }  
    }
    

    promise怎么实现链式调用跟返回不同的状态

    // MyPromise.js
    
    // 先定义三个常量表示状态
    const PENDING = 'pending';
    const FULFILLED = 'fulfilled';
    const REJECTED = 'rejected';
    
    // 新建 MyPromise 类
    class MyPromise {
      constructor(executor){
        // executor 是一个执行器,进入会立即执行
        // 并传入resolve和reject方法
        executor(this.resolve, this.reject)
      }
    
      // 储存状态的变量,初始值是 pending
      status = PENDING;
    
      // resolve和reject为什么要用箭头函数?
      // 如果直接调用的话,普通函数this指向的是window或者undefined
      // 用箭头函数就可以让this指向当前实例对象
      // 成功之后的值
      value = null;
      // 失败之后的原因
      reason = null;
    
      // 更改成功后的状态
      resolve = (value) => {
        // 只有状态是等待,才执行状态修改
        if (this.status === PENDING) {
          // 状态修改为成功
          this.status = FULFILLED;
          // 保存成功之后的值
          this.value = value;
        }
      }
    
      // 更改失败后的状态
      reject = (reason) => {
        // 只有状态是等待,才执行状态修改
        if (this.status === PENDING) {
          // 状态成功为失败
          this.status = REJECTED;
          // 保存失败后的原因
          this.reason = reason;
        }
      }
    
        then(onFulfilled, onRejected) {
        // 判断状态
        if (this.status === FULFILLED) {
          // 调用成功回调,并且把值返回
          onFulfilled(this.value);
        } else if (this.status === REJECTED) {
          // 调用失败回调,并且把原因返回
          onRejected(this.reason);
        }
      }
    
    }
    

    实现Promise.all

    // Promise.all
    function all(promises) {
      let len = promises.length, res = []
      if (len) {
        return new Promise(function (resolve, reject) {
            for(let i=0; i < len; i++){
                let promise = promises[i];
                promise.then(response => {
                    res[i] = response
    
                    // 当返回结果为最后一个时
                    if (res.length === len) {
                        resolve(res)
                    }
    
                }, error => {
                    reject(error)
                })
    
            }
        })
    }
    

    对象数组转换成tree数组

    > 将entries 按照 level 转换成 result 数据结构
    
    const entries = [
        {
            "province": "浙江", "city": "杭州", "name": "西湖"
        }, {
            "province": "四川", "city": "成都", "name": "锦里"
        }, {
            "province": "四川", "city": "成都", "name": "方所"
        }, {
            "province": "四川", "city": "阿坝", "name": "九寨沟"
        }
    ];
     
    const level = ["province", "city", "name"];
    
    const  result = [
     {
      value:'浙江',
      children:[
       {
        value:'杭州',
        children:[
         {
          value:'西湖'
         }
        ]
       }
      ]
     },
     {
      value:'四川',
      children:[
       {
        value:'成都',
        children:[
         {
          value:'锦里'
         },
         {
          value:'方所'
         }
        ]
       },
       {
        value:'阿坝',
        children:[
         {
          value:'九寨沟'
         }
        ]
       }
      ]
     },
    ]
    

    思路:涉及到树形数组,采用递归遍历的方式

    function transfrom(list, level) {
      const res = [];
      list.forEach(item => {
        pushItem(res, item, 0);
      });
    
      function pushItem(arr, obj, i) {
        const o = {
          value: obj[level[i]],
          children: [],
        };
        // 判断传入数组里是否有value等于要传入的项
        const hasItem = arr.find(el => el.value === obj[level[i]]);
        let nowArr;
        if(hasItem) {
          // 存在,则下一次遍历传入存在项的children
          nowArr = hasItem.children;
        }else{
          // 不存在 压入arr,下一次遍历传入此项的children
          arr.push(o);
          nowArr = o.children;
        }
        if(i === level.length - 1) delete o.children;
        i++;
        if(i < level.length) {
          // 递归进行层级的遍历
          pushItem(nowArr, obj, i);
        }
      }
    }
    
    transfrom(entries, level);
    

    JS instanceof 方法原生实现

    简单用法

    function Fn () {}
    const fn = new Fn()
    fn instanceof Fn  // true
    

    实现如下:

    // left instanceof right
    function _instanceof(left, right) {
      // 构造函数原型
      const prototype = right.prototype
      // 实列对象属性,指向其构造函数原型
      left = left.__proto__
      // 查实原型链
      while (true) {
        // 如果为null,说明原型链已经查找到最顶层了,真接返回false
        if (left === null) {
          return false
        }
        // 查找到原型
        if (prototype === left){
          return true
        }
        // 继续向上查找
        left = left.__proto__
      }
    }
    
    const str = "abc"
    _instanceof(str, String) // true
    

    编程题

    将有同样元素的数组进行合并

    // 例如:
    const arr = [
        ['a', 'b', 'c'],
        ['a', 'd'],
        ['d', 'e'],
        ['f', 'g'],
        ['h', 'g'],
        ['i']
    ]
    // 运行后的返回结果是:
    [
        ['a', 'b', 'c', 'd', 'e'],
        ['f', 'g', 'h'],
        ['i']
    ]
    // 思路一:
    const arr = [['a', 'b', 'c'], ['a', 'd'], ['d', 'e'], ['f', 'g'], ['h', 'g'], ['i']]
    function transform(arr){
        let res = []
        arr = arr.map(el => el.sort()).sort()
        const item = arr.reduce((pre, cur) => {
          if (cur.some(el => pre && pre.includes(el))) {
            pre = pre.concat(cur)
          } else {
            res.push(pre)
            pre = cur
          }
          return [...new Set(pre)]
        })
        res.push(item)
        return res;
    }
    transform(arr)
    // console.log(transform(arr));
    
    // 思路二: 
    
    function r (arr) {
      const map = new Map()
      arr.forEach((array, index) => {
        const findAlp = array.find((v) => map.get(v))
        if (findAlp) {
          const set = map.get(findAlp)
          array.forEach((alp) => {
            set.add(alp)
            const findAlp2 = map.get(alp)
            if (findAlp2 && findAlp2 !== set) {
              for(const v of findAlp2.values()){
                set.add(v)
                map.set(v, set)
              }
            }
            map.set(alp, set)
          })
        } else {
          const set = new Set(arr[index])
          array.forEach((alp) => map.set(alp, set))
        }
      })
      const set = new Set()
      const ret = []
      for (const [key, value] of map.entries()) {
        if (set.has(value)) continue
        set.add(value)
        ret.push([...value])
      }
      return ret
    }
    

    关于本文

    作者@几米阳光 

    https://juejin/post/6991724298197008421

    推荐阅读

    2021 阿里字节快手面经 & 个人成长经验分享

    记一次高级前端开发工程师面经

    关注下方「前端开发博客」,回复 “加群”

    加入我们一起学习,天天进步

    如果觉得这篇文章还不错,来个【分享、点赞、在看】三连吧,让更多的人也看到~

更多推荐

2021年7月 虾皮、OPPO、富途等十几家公司面经总结