Skip to content

面试 -- js ​

  1. let var const?

    • var会进行声明提升,没有局部作用域,并且可以重复声明
    • let 不会进行声明提升,没有局部作用域,不能重复声明
    • let const class声明的变量不属于顶层对象的属性(浏览器环境的window,Node环境的global),而是放在了script块作用域中,var,function声明的全局变量依旧是顶层对象的属性
    • const 对于基本类型,不能修改值,对于引用类型,可以修改其内部属性值
    • 可以使用Object.freeze()与递归来冻结对象中的每一个值
      js
          var constantize = (obj) => {
              Object.freeze(obj);
              Object.keys(obj).forEach( (key, i) => {
                  if ( typeof obj[key] === 'object' ) {
                  constantize( obj[key] );
                  }
              });
              };
    • 暂时性死区
      js
           if (true) {
              // TDZ开始
              tmp = 'abc'; // ReferenceError
              console.log(tmp); // ReferenceError
              let tmp; // TDZ结束
              console.log(tmp); // undefined
              tmp = 123;
              console.log(tmp); // 123
              }               
          //typeOf也不是一个百分百安全的操作了
              typeof x; // ReferenceError
              let x;
  2. new

    1. 创建一个空对象
      • let obj1 = Object.create(null) 没有__proto__,如果不用方法,效率更高
      • let obj2 = {}
    2. 设置原型链
      • obj.__proto__ = XXX.prototype
    3. 改变this指向
      • let result = xxx.call(obj)
    4. 判断返回值
  3. this(执行主体)指向问题

    1. 函数执行或者方法调用的时候,this指向指向他的调用者
      js
          test() //严格模式下 this-->undefined  非严格模式下this-->window  
          //node环境下指向gloabl 
          let obj1 ={
              name:'111',
              f1:function(){},
              obj2:{
                  name:'222',
                  f2:function(){}
              }
          }  
          obj1.f1() //this-->obj1 
          obj1.obj2.f2()//this-->obj2 多层对象嵌套,指向离调用函数最近的对象
    2. 给当前元素的某个元素行为绑定方法,当事件行为触发,方法中的this就是当前元素本身
    3. 构造函数中的this指向实例对象
    4. 箭头函数的this
      • 箭头函数没有this和arguments
        1. 不要作为对象内置函数,不能读取对象内其他值
        2. 不能作为构造函数
        3. 不能在动态上下文中绑定(dom监听)
        4. 无法通过 call、bind、apply 改变 this 指向
      • 箭头函数的this在时就决定了,箭头函数的this与所在作用域的this指向一致
      • js中只有全局作用域,函数作用域,块级作用域,箭头函数要看他所处的作用域
    5. 可以通过call/apply/bind改变this指向
      js
          //1.手撕call
          //原理:利用.调用改变this
          Function.prototype.call = function call(context, ...params) {
              let self = this,
                  key = Symbol('key'),
                  result
              context == null ? context = window : null;
              !/^(object|function)$/i.test(typeof context) ? context = Object(
                  context) : null //包装对象,防止context是基本属性
              context[key] = self //在context上加一个唯一属性,就是待调用的函数
              result = context[key](...params)
              delete context[key] //删除此属性
              return result //返回值
          }           
      
          //2.手撕bind        
          Function.prototype.bind = function bind(context, ...params) {
              let self = this
              return function proxy(...args) { //执行proxy时可以再传的参数,如$event
                  self.apply(context, [...params, ...args])
              }
          }       
      
          //鸭子类型      类数组-->数组      
          Array.prototype.slice.call(arguments)
  4. JS几大循环对比以及性能分析

    1. for循环和while循环
      • for用var声明的时候,for和while性能差不多,(不确定循环次数用while,确定用for)
      • for用let声明的时候,for的性能更好,因为没有创造全局不释放的变量
    2. for和forEach
      • for属于命令式语句,可以自己灵活管控循环过程break,continue,return
      • forEach属于封装好的函数语句,不好管控循环过程,return只能终止本次循环,thorw new Error()才能跳出整个循环,相对来说,for循环性能更好
      • 重写forEach
        js
            Array.prototype.forEach = function forEach(callBack, context) {
                let self = this,
                    i = 0,
                    len = self.length
                context = context == null ? window : context
                for (; i < len; i++) {
                    typeof callBack === 'function' ? callBack.call(context, self[i], i) : null
                }
            }
    3. for...in和for...of
      1. for...in性能很差,迭代当前对象中所有可枚举的属性,大部分私有属性,还有部分公有属性(出现在所属类的原型上的)都是可枚举的,所以他会查找到原型链上去。遍历的是key值。我们可以通过Object.getOwnPropertyDescriptors(obj)来查看属性的enumerable属性是不是true来判断是否可枚举(数组、字符串、对象)
        • 遍历的结果属性名为整数数字升序排序
        • 无法遍历symbol属性
        • 会遍历公有属性(添加到原型链上的)
      2. for...of只能遍历可迭代的数据,遍历的是value值,我们可以通过[Symbol.iterator]来看是否有next函数判断是否可迭代(数组、部分类数组、字符串、set、map)
        • 代码实现
          js
              arr[Symbol.iterator] = function () {
                  let self = this,
                      index = 0
                  return {
                      next() { //必须具备next方法,执行一次next方法,拿到结构中的某一项的值
                      if (index > self.length - 1) {
                          return {
                          done: true,
                          value: undefined
                          }
                      }
                      return {
                          done: false,
                          value: self[index++]
                      }
                      }
                  }
              }
        • 让类数组对象可以用for..of:obj[Symbol.iterator] = Array.prototype[Symbol.iterator]
  5. 异步遍历器for await(let x of arr){}

    js
            let b = Promise.resolve('11')
            let b = Promise.resolve('33')
            let c = Promise.resolve('6666')
            let arr = [a, b, c];
            (async function () {
                for await (const x of arr) {
                console.log(x);
                }
            })();      //11 33 6666
    • 可以用来遍历一组promise,promise.all也有类似效果
  6. 防抖和节流

    1. debounce只执行最后一次,防止频繁操作
      • 应用场景:input输入 resize屏幕变化
      js
          function debounce(fn,delay){
              let timer = null    
              return function(){
                  if(timer) clearTimeOut(timer)
                  timer = setTimeOut(()=>{
                      fn.call(this,arguments)
                      timer = null
                  },delay)
              }
          }
    2. throttle是有规律的执行
      • 应用场景:拖拽监听 scroll
      js
          function throttle(fn,delay){
              let timer = null    
              return function(){
                  if(timer) return
                  timer = setTimeOut(()=>{
                      fn.call(this,arguments)
                      timer = null
                  },delay)
              }
          }
  7. event Loop事件机制

    1. 同步代码,一行一行放在Call Stack中执行
    2. 遇到异步,会先“记录”下代码,等待执行时机(setTimeOut,Ajax),时机到了,将之前记录的代码放入到Callback Queue
    3. 当Call Stack为空(同步代码执行完毕),event loop开始工作
    4. event loop轮询查找Callback Queue中是否有可执行的代码,如果有,将代码移动到Call Stack中执行
    5. event loop如果没有找到可以执行代码,则会继续轮询查找
    6. 延时队列:用于存放计时器到达后的回调任务,优先级「中」
    7. 交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」
    8. 微队列:用户存放需要最快执行的任务,优先级「最高」
  8. 手写一个promise加载图片

    js
        const op = new Promise((resolve, reject) => {
        const myImg = new Image()
        myImg.src = 'https://robohash.org/1'
        myImg.onload = () => resolve(myImg)
        myImg.onerror = () => reject('加载失败')
        })
        op.then((value) => {
        document.body.appendChild(value)
        }).catch((e) => {
        console.log(e)
        })
  9. 数据类型检测

    1. typeof
      • 直接在计算机底层基于数据类型的值(二进制的前缀以及特点)进行检测
      • typeof null === 'object' 对象存储在计算机中,都是以000开始的二进制存储,null也是,所以检测出来是对象
      • typeof 普通对象/数组/正则/日期 === 'object' 无法细致区分 只能区分除了null以外的基本类型和function
    2. A instanceof B 检测当前A实例是否属于这个B类
      • 底层机制,只要当前B类出现在A实例的原型链上,结果都是true
      • 原型的指向是可以修改的,所以检测结果是不准确的
      • 不能检测基本数据类型
      • 手写instanceof
        js
            function insrance_of(example, classFunc) {
                let classFuncPrototype = classFunc.prototype,
                    proto = Object.getPrototypeOf(example)
                while (true) {
                    if (proto === null) return false
                    if (proto === classFuncPrototype) return true
                    proto = Object.getPrototypeOf(proto)
                }
            }
    3. constructor
      • 比instanceof好用,支持基本类型,而且可以区分引用类型
      • 但是constructor也是可以修改的,所以也不准确 arr.constructor === Array //true
    4. Object.prototype.toString.call([value])
      • 标准检测数据类型的方法,返回当前实例所属类的信息
      • [object Null/Number/String/Boolean/Undefined/Symbol/Object/Array/RegExp/Date/Function]
      • JQ源码封装toType方法
        js
            (function () {
                let class2Type = {}
                let toString = class2Type.toString; //Object.prototype.toString 注意这个分号,防止foreach报错
                //设定数据类型的映射表
                ['Boolean', 'Number', 'String', 'Function', 'Array', 'RegExp', 'Date', 'Object', 'Error', 'Symbol'].forEach(
                    name => {
                    class2Type[`[object ${name}]`] = name.toLowerCase()
                    });
                // console.log(class2Type)
        
                function toType(obj) {
                    if (obj == null) return obj + '' //判断null 和 undefined
                    return typeof obj === 'object' || typeof obj === 'function' ? class2Type[toString.call(obj)] || 'object' :
                    typeof obj
                }
                window.toType = toType
            })()
  10. 深浅拷贝

    1. 浅拷贝:只能拷贝第一层的值,地址值也是拷贝
      js
          //数组      
          newArr = [...arr]       
      
          newArr = arr.concat([])    
      
          newArr = arr.slice()  
          //对象      
          newObj = Object.assign({},obj)      
      
          newObj = {...obj}       
      
          let newObj = {} 
          for(let key in obj){
              newObj[key] = obj[key]  //自己forin遍历不支持Symbol属性的处理
          }       
      
          let newObj = {}
          let keys = [...Object.keys(),...Object.getOwnPropertySymbols()]
          for(let key in obj){
              newObj[key] = obj[key]  
          }
      js
          function deepClone(obj){
              if(typeof obj !== 'object' || obj == null) return obj
              let result  
              if(obj instanceof Array){
                  result = []
              }else{
                  result ={}
              }
              for(let key in obj){
                  if(obj.hasOwnProperty(key)){
                      result[key] = deepClone(obj[key])
                  }
              }
              return result
          }
  11. 对象合并

    1. 基于浅比较实现的对象合并
      js
          Object.assign(options,params)
    2. merge实现
      js
          //A-->options中的key值 B-->target中的key值
          //1. A & B都是原始值,B替换A即可      
          //2. A是对象 & B是原始值,抛出异常信息        
          //3. A是原始值 & B是对象,B替换A即可        
          //4. A & B都是对象,依次遍历B中每一项,重复merge判断过程        
          function merge(options, params = {}) {
              function isObj(value) { //判断是否为对象
                  return Object.prototype.toString.call() === '[object object]'
              }
              for (const key in params) {
                  let isA = isObj(options[key])
                  let isB = isObj(params[key])
                  if (isA && !isB) throw new TypeError(`${key} in params must be object`)
                  if (isA && isB) {
                  options[key] = merge(options[key], params[key])
                  return
                  }
                  options[key] = params[key]
                  return options
              }
          }
  12. aop切面编程(埋点)

    js
        //事件执行前定义        
        Function.prototype.before = function(beforefn){
            if(typeof beforefn !== 'function') throw new TypeError('callback must be function')
            let _self = this
            return function proxy(...params){
                beforefn.call(this,...params)
                return _self.call(this,...params)
            }
        }   
        //事件执行后定义        
        Function.prototype.after = function(afterfn){
            if(typeof afterfn !== 'function') throw new TypeError('callback must be function')
            let _self = this
            return function proxy(...params){
                let res = _self.call(this,...params)
                afterfn.call(this,...params)
                return res
            }
        }  
        //事件环绕(前+后)定义        
        Function.prototype.around = function(beforefn,afterfn){
            if(typeof afterfn !== 'function') throw new TypeError('callback must be function')
            let _self = this
            return function proxy(...params){
                beforefn.call(this,...params)
                let res = _self.call(this,...params)
                afterfn.call(this,...params)
                return res
            }
        }       
        let func = ()=>{
            //主要业务逻辑
        }
        func.before(()=>{
            console.log('====before====')
        }).after(()=>{
            console.log('====after====')
        })
  13. js设计模式
    设计模式就是一种思想,是用来规范编程的代码,使得代码更加整洁、清晰、维护性高、可拓展性高

    1. 单例设计模式 && 命令模式:基于单独的实例,来管理某一个模块中的内容,实现模块之间的独立划分,也可以实现模块之间方法的相互调用
      js
          var aModule = (function(){
              var data = []
              function bindHTML(){}
              function afunc(){}
              //window.afunc = afunc  1. 挂载到window上来实现暴露 
              return {afunc}  //2. 通过闭包暴露
          })()    
          var bModule = (function(){
              var data = []
              function bindHTML(){}
              function bfunc(){}
              aModule.afunc()   //单例模式  缺点:如果修改,修改的是闭包下的同一容器,这样会产生关联和影响
              //window.afunc()  污染了全局变量
          })()          
          // AMD  CMD  ES6 Module     
      
          //业务来讲:按照一定的顺序依次执行的方法,从而实现整个板块功能的开发        
          let searchModule = (function(){
              function queryData(){}
              function bindHTML(){}
              function handler(){}
              return {
                  init:function(){ //init相当于大脑,可以控制谁先执行,谁后执行
                      queryData()
                      bindHTML()
                      handler()
                  }
              }
          })()       
          searchModule.init() //命令模式
    2. 构造器模式(插件组件封装、类库封装)
      js
          class Amodule{
              constructor(){
                  this.arr = []  //this-->每个类的实例
              }
              change(val){ //原型上的公共属性和方法
                  this.arr.push(val)
              }
          }
    3. 工厂模式(Factory):帮助我们实现调用的切换,或者实现一些中转的处理
      js
          function factory(options){
              options = options || {}
              let {type,payload} = options    
              if(type == 'array'){
                   //执行A逻辑
                   return
              }
              //执行B逻辑
          }
    4. 观察者模式(Observer--Subject)vue2.0响应式原理
      js
          class Dep {// 发布者 - 目标
              constructor () {
                  this.subs = []// 记录所有的订阅者
              }
              addSubs (sub) {// 把订阅者添加到 subs 中
                  if (sub && sub.update) {
                      this.subs.push(sub)
                  } 
              }
              notify () {// 当事件发生,通知所有的订阅者,调用所有观察者的 update() 方法
                  this.subs.forEach(sub => {
                      sub.update()
                  }) 
              }
          }
          class Watcher {// 订阅者 - 观察者
              update () { // 当事件发生,由发布者调用所有观察者的 update() 方法 // 实现更新视图等功能
                  console.log('update');
              }
          }
          // 测试
          let dep = new Dep()
          let watcher = new Watcher()
          dep.addSubs(watcher)
          dep.notify()
    5. 中介者模式(Mediator)
    6. 发布/订阅模式(publish/subscribe)
      • 发布订阅模式中统一由调度中心进行处理,订阅者和发布者互不干扰。这样一方面实现了解耦,还有就是可以实现更细粒度的一些控制。比如发布者发布了很多消息,但是不想所有的订阅者都接收到,就可以在调度中心做一些处理,类似于权限控制之类的。还可以做一些节流操作
      js
          //定义一家猎人工会
          //主要功能包括任务发布大厅(topics),以及订阅任务(subscribe),发布任务(publish)
          let HunterUnion = {
              type: 'hunt',
              topics: Object.create(null),
              subscribe: function (topic, fn){
                  if(!this.topics[topic]){
                      this.topics[topic] = [];  
                  }
                  this.topics[topic].push(fn);
              },
              publish: function (topic, money){
                  if(!this.topics[topic])
                      return;
                  for(let fn of this.topics[topic]){
                      fn(money)
                  }
              }
          }
          //定义一个猎人类
          //包括姓名,级别
          function Hunter(name, level){
              this.name = name
              this.level = level
          }
          //猎人可在猎人工会发布订阅任务
          Hunter.prototype.subscribe = function (topic, fn){
              console.log(this.level + '猎人' + this.name + '订阅了狩猎' + topic + '的任务')
              HunterUnion.subscribe(topic, fn)
          }
          Hunter.prototype.publish = function (topic, money){
              console.log(this.level + '猎人' + this.name + '发布了狩猎' + topic + '的任务')
              HunterUnion.publish(topic, money)
          }
          //猎人工会走来了几个猎人
          let hunterMing = new Hunter('小明', '黄金')
          let hunterJin = new Hunter('小金', '白银')
          let hunterZhang = new Hunter('小张', '黄金')
          let hunterPeter = new Hunter('Peter', '青铜')
          //小明,小金,小张分别订阅了狩猎tiger的任务
          hunterMing.subscribe('tiger', function(money){
              console.log('小明表示:' + (money > 200 ? '' : '不') + '接取任务')
          })
          hunterJin.subscribe('tiger', function(money){
              console.log('小金表示:接取任务')
          })
          hunterZhang.subscribe('tiger', function(money){
              console.log('小张表示:接取任务')
          })
          //Peter订阅了狩猎sheep的任务
          hunterPeter.subscribe('sheep', function(money){
              console.log('Peter表示:接取任务')
          })  
          //Peter发布了狩猎tiger的任务
          hunterPeter.publish('tiger', 198)
          //猎人们发布(发布者)或订阅(观察者/订阅者)任务都是通过猎
  14. 基于JS实现ajax并发请求的控制
    1. 串行:请求是异步的,需要等待上一个请求成功,才能执行下一个请求
    2. 并行:同时发多个请求,HTTP请求可以同时进行,但是JS是单线程,操作都是一步一步来,等待所有请求都成功,我们再去做什么事情

    js
        const delay = function (interval) {
        return new Promise((resolve, reject) => { //睡眠函数
            setTimeout(() => {
            resolve(interval)
            }, interval)
        })
        }
        let tasks = [() => {
        return delay(1000)
        }, () => {
        return delay(1003)
        }, () => {
        return delay(1005)
        }, () => {
        return delay(1002)
        }, () => {
        return delay(1006)
        }]
        //1.利用库函数asyncPool
        let results = []
        asyncPool(2,tasks,(tasks,next)=>{
            task().then(result =>{
                results.push(result)
                next()
            })
        },()=>{
            console.log(results)
        })      
        //2. js实现     
            //tasks:数组,数组中包含很多方法,每一个方法相当于发一个请求    
            //pool:并发限制数量
            function createRequest(tasks, pool) {
            pool = pool || 5 //工作区限制
            let results = [],
                together = new Array(pool).fill(null),
                index = 0
            together = together.map(() => {
                return new Promise((resolve, reject) => {
                const run = function () {
                    if (index >= tasks.length) {
                    resolve()
                    return
                    }
                    let old_index = index,
                    task = tasks[index++] //获取任务
                    task().then(result => {
                    results[old_index] = result
                    run() //递归获取任务
                    }).catch(reason => {
                    reject(reason)
                    })
                }
                run()
                })
            })
            return Promise.all(together).then(() => results) //并发工作区内请求
            }
            createRequest(tasks, 2).then(results => {
            console.log('====成功', results) //都成功才算成功,按顺序存储结果
            }).catch((reason) => {
            console.log('====失败', reason) //只要有一个失败,整体失败
            })
    
            function createRequest2(tasks, pool, callback) {
            class TaskQueue {
                running = 0
                queue = []
                results = []
                pushTack(task) {
                let self = this
                self.queue.push(task)
                self.next()
                }
                next() {
                let self = this
                while (self.running < pool && self.queue.length) {
                    self.running++
                    let task = self.queue.shift()
                    task().then(result => {
                    self.results.push(result)
                    }).finally(() => {
                    self.running--
                    self.next()
                    })
                }
                if (self.running === 0) {
                    callback(self.results)
                }
                }
            }
            }
            createRequest2(tasks, pool, results => {
            console.log(results)
            })