lizheng0515 / ignore

🔥个人博客
http://jimilee.tech/
MIT License
28 stars 2 forks source link

面经整理 #37

Open lizheng0515 opened 3 years ago

lizheng0515 commented 3 years ago

2021.7 货拉拉、虾皮、有赞。。。高级前端岗面试整理

JS相关

JS原型及原型链

function Person() {}
Person.prototype.name = 'Zaxlct';
Person.prototype.sayName = function() {
  alert(this.name);
}
var person1 = new Person();
//JS 在创建对象的时候,都有一个__proto__ 的内置属性,用于指向创建它的构造函数的原型对象。
// 对象 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.创建子类时,无法向父类构造函数传参数
  2. 构造函数继承

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

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

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

    • 使用原型链继承来保证子类能继承到父类原型中的属性和方法
    • 使用构造继承来保证子类能继承到父类的实例属性和方法
  4. 寄生组合继承

  5. 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实现的,看起来更像同步代码,

   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 中使用 resolvereject 两个函数来更改状态;

  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规范则是非同步加载模块,允许指定回调函数,AMDRequireJS 在推广过程中对模块定义的规范化产出。

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.com/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增加块级作用域

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

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

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

判断数组的四种方法

  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的区别

框架 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 监听函数

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更新视图。 img

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传给包装组件// 高阶组件完全可以添加、删除、修改 propsexport 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      );    },  };}

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

Vue.component()方法注册全局组件。

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

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

import Vue from 'vue';// 这个插件必须具有install方法const plugin = {  install (Vue, options) {    // 添加全局方法或者属性    Vue.myGlobMethod = function () {};    // 添加全局指令    Vue.directive();    // 添加混入    Vue.mixin();    // 添加实例方法    Vue.prototype.$xxx = function () {};    // 注册全局组件    Vue.component()  }}// 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中数据:

  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.org/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.构建打点:构建过程中,每一个LoaderPlugin 的执行时长,在编译 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 热更新机制

热更新流程总结:

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: 使用