前端面试知识点

作者: 夜幕小草 | 来源:发表于2019-01-27 23:52 被阅读368次
    一、作用域(全局作用域、函数作用域)
    • 、全局作用域
      顾名思义:也就是挂在window上下文中的属性,或者function外部的变量,成为全局变量
      JS中声明全局变量主要分为显式声明或者隐式声明
      • 显示声明
        1、var(关键字)+变量名(标识符)的方式在function外部声明,即为全局变量,否则在function声明的是局部变量。如:
         var test = 5;  //全局变量
         function a(){
            var cc=3; //函数变量
            alert(test); // 3
         }
         function b(){alert(test);}
        
      • 隐式声明
        2、没有使用var,直接给标识符test赋值,这样会隐式的声明了全局变量test。即使该语句是在一个function内,当该function被执行后test变成了全局变量。
        test = 5;//全局变量 
        function a(){
          aa=3; //全局变量
          alert(test);  // 3
        }
        
        3、使用window全局对象来声明,全局对象的属性对应也是全局变量
          window.test
          window.test = 80
        
      本身就是在window全局作用域下边的,所以有些是省略了

    • 、函数作用域
      在fuction内部声明的变量就是局部变量,这个范围的上下文就是函数作用域
        function(){
          //函数作用域
           var a = 90  
        }
      
    二、变量提升(变量、函数)
    三、事件循环
    四、面向对象
    五、继承
    六、类型(值类型、引用类型)
    七、几个常见的api(foreach、every、some、filter、sort、map)
    八、浏览器
    九、性能优化
    十、webpack处理
    十一、vue实现原理
    十一、vue知识点一
    • 生命周期钩子函数

      • beforeCreate 钩子函数调用的时候,是获取不到 props或者 data 中的数据的,因为这些数据的初始化都在 initState中。
        initState主要是对 props, methods, data, computedwatch 进行初始化
      • 然后会执行 created 钩子函数,在这一步的时候已经可以访问到之前不能访问到的数据,但是这时候组件还没被挂载,所以是看不到的。
      • 接下来会先执行beforeMount钩子函数,开始创建 VDOM,最后执行 mounted 钩子,并将 VDOM渲染为真实 DOM 并且渲染数据。组件中如果有子组件的话,会递归挂载子组件,只有当所有子组件全部挂载完毕,才会执行根组件的挂载钩子。
      • 接下来是数据更新时会调用的钩子函数 beforeUpdateupdated,就是分别在数据更新前和更新后会调用。
      • 另外还有 keep-alive 独有的生命周期,分别为 activateddeactivated。用 keep-alive包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated钩子函数,命中缓存渲染后会执行 actived钩子函数。
      • 最后就是销毁组件的钩子函数 beforeDestroydestroyed。前者适合移除事件、定时器等等,否则可能会引起内存泄露的问题。然后进行一系列的销毁操作,如果有子组件的话,也会递归销毁子组件,所有子组件都销毁完毕后才会执行根组件的 destroyed钩子函数。 附生命周期示意图:
        image.png
    • 组件通信
      组件通信一般分为以下几种情况:

      • 父子组件通信
      • 兄弟组件通信
      • 跨多层级组件通信
      • 任意组件

      对于以上每种情况都有多种方式去实现,接下来就来学习下如何实现。

      • 父子组件通信
        父组件通过 props 传递数据给子组件,子组件通过 emit 发送事件传递数据给父组件,这两种方式是最常用的父子通信实现办法。是典型的单向数据流,父组件通过 props传递数据,子组件不能直接修改 props, 而是必须通过发送事件的方式告知父组件修改数据。
        父组件:
      <template>
        <div>
          <child :total="total" v-on:reduce="reduce"></child>
          <button @click="increse">增加5</button>
        </div>
      </template>
      
      <script>
       import Child from "./child.vue"
       export default {
       components: {
           Child
        },
        data: function () {
          return {
              total: 0
          };
      },
      methods: {
          increse: function () {
              this.total += 5;
          },
          reduce: function (v) {
              this.total -= v;
          }
        }
      }
      </script>
      

      子组件child.vue

       <template>
        <div>
            <span>{{total}}</span>
            <button @click="reduce">减少5</button>
        </div>
       </template>
      
       <script>
      export default {
        props: {
            total: Number
        },
        methods: {
            reduce: function(){
                this.$emit("reduce", 5)
            }
        }
      }
      </script>
          这种方式需要在父组件中添加一个reduce方法,并且需要在<child>标签上添加v-on:reduce监听事件,方式有点繁琐
      

      另外这两种方式还可以使用语法糖 v-model来直接实现,因为 v-model默认会解析成名为 valueprop 和名为 input 的事件。这种语法糖的方式是典型的双向绑定,常用于 UI控件上,但是究其根本,还是通过事件的方法让父组件修改数据。
      如何使用v-model来实现父子组件间的通信(其实原理还是和上面一样)

      <template>
       <div>
           <child v-model="total"></child>
           <button @click="increse">增加5</button>
       </div>
      </template>
      
      <script>
      import Child from "./child.vue"
      export default {
       components: {
           Child
       },
       data: function () {
           return {
               total: 0
           };
       },
       methods: {
           increse: function () {
               this.total += 5;
           }
        }
      }
      </script>
      

      子组件child.vue:

      <template>
        <div>
            <span>{{value}}</span>
            <button @click="reduce">减少5</button>
        </div>
      </template>
      
      <script>
      export default {
        props: {
            value: Number  // 注意这里是value
        },
        methods: {
            reduce: function(){
                this.$emit("input", this.value - 5)
            }
          }
      }
      </script>
        这样是不是简单许多?
        如果把<input v-model="total">理解成<input v-on:input="total=arguments[0]" :value="total">就容易懂了
      

      当然我们还可以通过访问$parent或者 $children对象来访问组件实例中的方法和数据。
      另外如果你使用 Vue 2.3及以上版本的话还可以使用 $listeners.sync这两个属性。
      $listeners 属性会将父组件中的 (不含 .native 修饰器的) v-on 事件监听器传递给子组件,子组件可以通过访问$listeners来自定义监听器。

        示例代码待贴
      

      .sync 属性是个语法糖,可以很简单的实现子组件与父组件通信

        示例代码待贴
      
      • 兄弟组件通信
        对于这种情况可以通过查找父组件中的子组件实现,也就是this.$parent.$children,在 $children 中可以通过组件 name 查询到需要的组件实例,然后进行通信。
       代码待贴
      
      • 跨多层次组件通信
        对于这种情况可以使用Vue 2.2 新增的 API provide / inject,虽然文档中不推荐直接使用在业务中,但是如果用得好的话还是很有用的。
        假设有父组件 A,然后有一个跨多层级的子组件 B
       // 父组件 A
      export default {
        provide: {
          data: 1
        }
      }
      // 子组件 B
      export default {
        inject: ['data'],
        mounted() {
          // 无论跨几层都能获得父组件的 data 属性
          console.log(this.data) // => 1
        }
      }
      
      • 任意组件
        这种方式可以通过 Vuex 或者 Event Bus 解决,另外如果你不怕麻烦的话,可以使用这种方式解决上述所有的通信情况

    • extend 能做什么
      这个 API 很少用到,作用是扩展组件生成一个构造器,通常会与 $mount 一起使用。
        // 创建组件构造器
       let Component = Vue.extend({
         template: '<div>test</div>'
      })
       // 挂载到 #app 上
       new Component().$mount('#app')
       // 除了上面的方式,还可以用来扩展已有的组件
      let SuperComponent = Vue.extend(Component)
      new SuperComponent({
         created() {
            console.log(1)
         }
      })
      new SuperComponent().$mount('#app')
      

    • mixin 和 mixins 区别
      mixin 用于全局混入,会影响到每个组件实例,通常插件都是这样做初始化的。
      Vue.mixin({
         beforeCreate() {
            // ...逻辑
            // 这种方式会影响到每个组件的 beforeCreate 钩子函数
           }
        })
      
      虽然文档不建议我们在应用中直接使用 mixin,但是如果不滥用的话也是很有帮助的,比如可以全局混入封装好的 ajax 或者一些工具函数等等。
      mixins应该是我们最常使用的扩展组件的方式了。如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过 mixins混入代码,比如上拉下拉加载数据这种逻辑等等。
      另外需要注意的是 mixins 混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并。

    • computed 和 watch 区别
      computed 是计算属性,依赖其他属性计算值,并且 computed的值有缓存,只有当计算值变化才会返回内容。
      watch监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作
      所以一般来说需要依赖别的属性来动态获得值的时候可以使用 computed,对于监听到值的变化需要做一些复杂业务逻辑的情况可以使用 watch
      另外 computedwatch 还都支持对象的写法,这种方式知道的人并不多。
      vm.$watch('obj', {
        // 深度遍历
        deep: true,
        // 立即触发
        immediate: true,
        // 执行的函数
        handler: function(val, oldVal) {}
      })
      var vm = new Vue({
        data: { a: 1 },
        computed: {
          aPlus: {
            // this.aPlus 时触发
            get: function () {
              return this.a + 1
            },
            // this.aPlus = 1 时触发
            set: function (v) {
              this.a = v - 1
            }
          }
        }
      })
      

    • keep-alive 组件有什么作用
      如果你需要在组件切换的时候,保存一些组件的状态防止多次渲染,就可以使用keep-alive 组件包裹需要保存的组件。
      对于 keep-alive组件来说,它拥有两个独有的生命周期钩子函数,分别为 activateddeactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行actived钩子函数。

    • v-show 与 v-if 区别
      v-show只是在display: nonedisplay: block之间切换。无论初始条件是什么都会被渲染出来,后面只需要切换 CSSDOM 还是一直保留着的。所以总的来说 v-show在初始渲染时有更高的开销,但是切换开销很小,更适合于频繁切换的场景。
      v-if 的话就得说到 Vue底层的编译了。当属性初始为 false时,组件就不会被渲染,直到条件为 true,并且切换条件时会触发销毁/挂载组件,所以总的来说在切换时开销更高,更适合不经常切换的场景。
      并且基于 v-if的这种惰性渲染机制,可以在必要的时候才去渲染组件,减少整个页面的初始渲染开销。

    • 组件中 data 什么时候可以使用对象
      组件复用时所有组件实例都会共享data,如果 data是对象的话,就会造成一个组件修改 data 以后会影响到其他所有组件,所以需要将 data写成函数,每次用到就调用一次函数获得新的数据。
      当我们使用 new Vue()的方式的时候,无论我们将 data 设置为对象还是函数都是可以的,因为 new Vue() 的方式是生成一个根组件,该组件不会复用,也就不存在共享 data 的情况了
    十二、vue知识点二
    • 响应原理
      Vue内部使用了 Object.defineProperty() 来实现数据响应式,通过这个函数可以监听到 setget的事件。
       var data = { name: 'Dashu' }
       observe(data)
       let name = data.name // -> get value
       data.name = 'jjj' // -> change value
       
      function observe(obj) {
      // 判断类型
        if (!obj || typeof obj !== 'object') {
          return
        }
        Object.keys(obj).forEach(key => {
           defineReactive(obj, key, obj[key])
         })
      }
      
      function defineReactive(obj, key, val) {
        // 递归子属性
        observe(val)
        Object.defineProperty(obj, key, {
          // 可枚举
          enumerable: true,
          // 可配置
          configurable: true,
          // 自定义函数
          get: function reactiveGetter() {
            console.log('get value')
            return val
          },
          set: function reactiveSetter(newVal) {
            console.log('change value')
            val = newVal
          }
        })
      }
      
      以上代码简单的实现了如何监听数据的 setget 的事件,但是仅仅如此是不够的,因为自定义的函数一开始是不会执行的。只有先执行了依赖收集,从能在属性更新的时候派发更新,所以接下来我们需要先触发依赖收集。
      <div>
       {{name}}
      </div>
      
      在解析如上模板代码时,遇到{{name}}就会进行依赖收集。
      接下来我们先来实现一个 Dep 类,用于解耦属性的依赖收集和派发更新操作。
      // 通过 Dep 解耦属性的依赖和更新操作
      class Dep {
         constructor() {
         this.subs = []
       }
      // 添加依赖
        addSub(sub) {
          this.subs.push(sub)
        }
        // 更新
        notify() {
          this.subs.forEach(sub => {
            sub.update()
          })
        }
      }
      // 全局属性,通过该属性配置 Watcher
       Dep.target = null
      
      以上的代码实现很简单,当需要依赖收集的时候调用 addSub,当需要派发更新的时候调用 notify
      接下来我们先来简单的了解下 Vue组件挂载时添加响应式的过程。在组件挂载时,会先对所有需要的属性调用 Object.defineProperty(),然后实例化 Watcher,传入组件更新的回调。在实例化过程中,会对模板中的属性进行求值,触发依赖收集。
      触发依赖收集时的操作:
      class Watcher {
       constructor(obj, key, cb) {
         // 将 Dep.target 指向自己
         // 然后触发属性的 getter 添加监听
         // 最后将 Dep.target 置空
         Dep.target = this
         this.cb = cb
         this.obj = obj
         this.key = key
         this.value = obj[key]
         Dep.target = null
      }
      update() {
         // 获得新值
         this.value = this.obj[this.key]
         // 调用 update 方法更新 Dom
         this.cb(this.value)
         }
      }
      
      以上就是 Watcher的简单实现,在执行构造函数的时候将 Dep.target 指向自身,从而使得收集到了对应的 Watcher,在派发更新的时候取出对应的 Watcher 然后执行 update函数。
      接下来,需要对 defineReactive 函数进行改造,在自定义函数中添加依赖收集和派发更新相关的代码
      function defineReactive(obj, key, val) {
        // 递归子属性
        observe(val)
        let dp = new Dep()
        Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get: function reactiveGetter() {
            console.log('get value')
            // 将 Watcher 添加到订阅
            if (Dep.target) {
              dp.addSub(Dep.target)
            }
            return val
          },
          set: function reactiveSetter(newVal) {
            console.log('change value')
            val = newVal
            // 执行 watcher 的 update 方法
            dp.notify()
          }
        })
      }
      
      以上所有代码实现了一个简易的数据响应式,核心思路就是手动触发一次属性的getter来实现依赖收集。
       var data = { name: 'yck' }
       observe(data)
       function update(value) {
         document.querySelector('div').innerText = value
       }
      // 模拟解析到 `{{name}}` 触发的操作
       new Watcher(data, 'name', update)
       // update Dom innerText
       data.name = 'yyy'
      
    • Object.defineProperty 的缺陷
      以上已经分析完了 Vue 的响应式原理,接下来说一点 Object.defineProperty 中的缺陷。
      如果通过下标方式修改数组数据或者给对象新增属性并不会触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作,更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。
      对于第一个问题,Vue 提供了一个 API 解决
       export function set (target: Array<any> | Object, key: any, val: any): any {
        // 判断是否为数组且下标是否有效
         if (Array.isArray(target) && isValidArrayIndex(key)) {
          // 调用 splice 函数触发派发更新
          // 该函数已被重写
          target.length = Math.max(target.length, key)
          target.splice(key, 1, val)
          return val
        }
        // 判断 key 是否已经存在
        if (key in target && !(key in Object.prototype)) {
          target[key] = val
            return val
        }
        const ob = (target: any).__ob__
        // 如果对象不是响应式对象,就赋值返回
        if (!ob) {
          target[key] = val
          return val
        }
        // 进行双向绑定
        defineReactive(ob.value, key, val)
        // 手动派发更新
        ob.dep.notify()
          return val
        }
      
      对于数组而言,Vue 内部重写了以下函数实现派发更新
        // 获得数组原型
       const arrayProto = Array.prototype
       export const arrayMethods = Object.create(arrayProto)
       // 重写以下函数
       const methodsToPatch = [
         'push',
         'pop',
         'shift',
         'unshift',
         'splice',
         'sort',
         'reverse'
       ]
       methodsToPatch.forEach(function (method) {
         // 缓存原生函数
         const original = arrayProto[method]
         // 重写函数
         def(arrayMethods, method, function mutator (...args) {
         // 先调用原生函数获得结果
           const result = original.apply(this, args)
           const ob = this.__ob__
           let inserted
           // 调用以下几个函数时,监听新数据
          switch (method) {
             case 'push':
             case 'unshift':
            inserted = args
             break
          case 'splice':
            inserted = args.slice(2)
            break
        }
        if (inserted) ob.observeArray(inserted)
        // 手动派发更新
        ob.dep.notify()
        return result
        })
      })
      
    • 编译过程
      想必大家在使用 Vue 开发的过程中,基本都是使用模板的方式。那么你有过「模板是怎么在浏览器中运行的」这种疑虑嘛?
      首先直接把模板丢到浏览器中肯定是不能运行的,模板只是为了方便开发者进行开发。Vue会通过编译器将模板通过几个阶段最终编译为 render函数,然后通过执行 render函数生成 Virtual DOM 最终映射为真实DOM
      接下来我们就来学习这个编译的过程,了解这个过程中大概发生了什么事情。这个过程其中又分为三个阶段,分别为: AST(抽象语法树)
      1.将模板解析为AST
      2.转化为AST
      3.将 AST转换为 render函数
      在第一个阶段中,最主要的事情还是通过各种各样的正则表达式去匹配模板中的内容,然后将内容提取出来做各种逻辑操作,接下来会生成一个最基本的AST对象
       {
         // 类型
         type: 1,
         // 标签
         tag,
         // 属性列表
         attrsList: attrs,
         // 属性映射
         attrsMap: makeAttrsMap(attrs),
         // 父节点
         parent,
         // 子节点
         children: []
        }
      
      然后会根据这个最基本的 AST对象中的属性,进一步扩展 AST。
      当然在这一阶段中,还会进行其他的一些判断逻辑。比如说对比前后开闭标签是否一致,判断根组件是否只存在一个,判断是否符合 HTML5 Content Model 规范等等问题。
      接下来就是优化 AST的阶段。在当前版本下,Vue 进行的优化内容其实还是不多的。只是对节点进行了静态内容提取,也就是将永远不会变动的节点提取了出来,实现复用 Virtual DOM,跳过对比算法的功能。在下一个大版本中,Vue会在优化 AST的阶段继续发力,实现更多的优化功能,尽可能的在编译阶段压榨更多的性能,比如说提取静态的属性等等优化行为。
      最后一个阶段就是通过 AST生成 render函数了。其实这一阶段虽然分支有很多,但是最主要的目的就是遍历整个 AST,根据不同的条件生成不同的代码罢了。
    • NextTick 原理分析
      nextTick可以让我们在下次DOM更新循环结束之后执行延迟回调,用于获得更新后的 DOM
      Vue 2.4之前都是使用的 microtasks,但是 microtasks 的优先级过高,在某些情况下可能会出现比事件冒泡更快的情况,但如果都使用 macrotasks又可能会出现渲染的性能问题。所以在新版本中,会默认使用 microtasks,但在特殊情况下会使用 macrotasks,比如v-on
      对于实现 macrotasks,会先判断是否能使用setImmediate,不能的话降级为 MessageChannel ,以上都不行的话就使用 setTimeout
      if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
         macroTimerFunc = () => {
          setImmediate(flushCallbacks)
        }
      } else if (
        typeof MessageChannel !== 'undefined' &&
        (isNative(MessageChannel) ||
         // PhantomJS
          MessageChannel.toString() === '[object MessageChannelConstructor]')
      ) {
        const channel = new MessageChannel()
        const port = channel.port2
        channel.port1.onmessage = flushCallbacks
        macroTimerFunc = () => {
          port.postMessage(1)
        }
      } else {
        macroTimerFunc = () => {
          setTimeout(flushCallbacks, 0)
        }
      }
      

    详细了解 Vue 技术揭秘

    十二、react实现原理
    十三、闭包
    十四、混合开发兼容问题
    十五、混合开发难点
    十六、首页白屏处理(service worker)

    作者不易,失业中。。。


    前端面试知识点

    相关文章

      网友评论

        本文标题:前端面试知识点

        本文链接:https://www.haomeiwen.com/subject/htnnjqtx.html