前端知识库

JS

数据类型

面试官:JS的数据类型都有哪些⭐⭐⭐⭐⭐

答:
数据类型分为基本数据类型引用数据类型

基本数据类型有:

  • Number
  • String
  • Boolean
  • null
  • Undefined
  • Symbol
  • BigInt

引用数据类型统称为Object类型,

  • 细分的话有:
    • Object
    • Array
    • Function
    • Date
    • RegExp

基本数据类型的数据直接存储在栈中;而引用数据类型的数据存储在堆中,在栈中保存数据的引用地址,这个引用地址指向的是对应的数据,以便快速查找到堆内存中的对象。

顺便提一句,栈内存是自动分配内存的。而堆内存是动态分配内存的,不会自动释放。所以每次使用完对象的时候都要把它设置为null,从而减少无用内存的消耗


为什么0.1+0.2>0.3 ⭐⭐⭐⭐

答:
因为在JS底层中,每个变量是以二进制表示,固定长度为64位,其中第1位是符号位,再往后11位是指数为,最后52表示的是尾数位,而0.1和0.2转为二进制的时候是无限循环小数,所以JS就会进行截取,截取以后0.1和0.2就不是他们本身了,要比原来大那么一丢丢,所以0.1+0.2就>0.3了


如何解决这个问题,使0.1+0.2等于0.3? ⭐⭐⭐⭐⭐

答:
先给他们放大倍数,随后在除以相应倍数

const a = 0.1;
const b = 0.2;
console.log(a + b === 0.3)   // false
console.log((a * 1000 + b * 1000) / 1000 === 0.3)  // true



数据类型的判断方式⭐⭐⭐⭐⭐

答:

1.typeof

  • 缺点:typeof null的值为Object,无法分辨是null还是Object

2.instanceof

  • 缺点:只能判断某对象是否存在于目标对象得的原型链上

3.constructor.name

4.Object.prototype.toString.call()

  • 一种最好的基本类型检测方式 Object.prototype.toString.call() ;它可以区分 null 、 string 、

boolean 、 number 、 undefined 、 array 、 function 、 object 、 date 、 math 数据类型。

  • 缺点:不能细分为谁谁的实例

示例:

// -----------------------------------------typeof
typeof undefined // 'undefined' 
typeof '10' // 'String' 
typeof 10 // 'Number' 
typeof false // 'Boolean' 
typeof Symbol() // 'Symbol' 
typeof Function // ‘function' 
typeof null // ‘Object’ 
typeof [] // 'Object' 
typeof {} // 'Object'


// -----------------------------------------instanceof
function Foo() { }
var f1 = new Foo();
var d = new Number(1)


console.log(f1 instanceof Foo);// true
console.log(d instanceof Number); //true
console.log(123 instanceof Number); //false   -->不能判断字面量的基本数据类型


// -----------------------------------------constructor
var d = new Number(1)
var e = 1
function fn() {
  console.log("ming");
}
var date = new Date();
var arr = [1, 2, 3];
var reg = /[hbc]at/gi;

console.log(e.constructor);//ƒ Number() { [native code] }
console.log(e.constructor.name);//Number
console.log(fn.constructor.name) // Function 
console.log(date.constructor.name)// Date 
console.log(arr.constructor.name) // Array 
console.log(reg.constructor.name) // RegExp


//-----------------------------------------Object.prototype.toString.call()
console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]" 
console.log(Object.prototype.toString.call(null)); // "[object Null]" 
console.log(Object.prototype.toString.call(123)); // "[object Number]" 
console.log(Object.prototype.toString.call("abc")); // "[object String]" 
console.log(Object.prototype.toString.call(true)); // "[object Boolean]" 


function fn() {
  console.log("ming");
}
var date = new Date();
var arr = [1, 2, 3];
var reg = /[hbc]at/gi;
console.log(Object.prototype.toString.call(fn));// "[object Function]" 
console.log(Object.prototype.toString.call(date));// "[object Date]" 
console.log(Object.prototype.toString.call(arr)); // "[object Array]"
console.log(Object.prototype.toString.call(reg));// "[object RegExp]"

为什么要用Object.prototype.toString.call(),为什么不用 Array.prototype.toString.call()?⭐⭐

答:
因为只有Object.prototype.toString.call()返回的是统一格式,而且 Array.prototype.toString.call()的部分类型无法检验。

function fn() {
   console.log("ming");
}
var date = new Date();
var arr = [1, 2, 3];
var reg = /[hbc]at/gi;

console.log(Array.prototype.toString.call(undefined)); // 报错
console.log(Array.prototype.toString.call(null)); // 报错
console.log(Array.prototype.toString.call(123)); // "[object Number]" 
console.log(Array.prototype.toString.call("abc")); // "[object String]" 
console.log(Array.prototype.toString.call(true)); // "[object Boolean]" 
console.log(Array.prototype.toString.call(fn)); // "[object Function]" 
console.log(Array.prototype.toString.call(date)); // "[object Date]" 
console.log(Array.prototype.toString.call(arr)); // "1,2,3"
console.log(Array.prototype.toString.call(reg));// "[object RegExp]"



instanceof原理⭐⭐⭐⭐⭐

答:
instanceof原理实际上就是查找目标对象的原型链

    function myInstance(L, R) {//L代表instanceof左边,R代表右边
      var RP = R.prototype
      var LP = L.__proto__
      while (true) {
        if(LP == null) {
          return false
        }
        if(LP == RP) {
          return true
        }
        LP = LP.__proto__
      }
    }
    console.log(myInstance({},Object)); 

面试官:为什么typeof null 是Object?⭐⭐⭐⭐⭐

答:

因为在JavaScript中,不同的对象都是使用二进制存储的,如果二进制前三位都是0的话,系统会判断为是Object类型,而null的二进制全是0,自然也就判断为Object

这个bug是初版本的JavaScript中留下的,扩展一下其他五种标识位:

  • 000 对象
  • 1 整型
  • 010 双精度类型
  • 100字符串
  • 110布尔类型

面试官:=有什么区别 ⭐⭐⭐⭐⭐

答:

===是严格意义上的相等,会比较两边的数据类型和值大小

  • 数据类型不同返回false
  • 数据类型相同,但值大小不同,返回false

==是非严格意义上的相等,

  • 两边类型相同,比较大小

  • 两边类型不同,根据下方表格,再进一步进行比较。

    • Null == Undefined ->true
    • String == Number ->先将String转为Number,在比较大小
    • Boolean == Number ->现将Boolean转为Number,在进行比较
    • Object == String,Number,Symbol -> Object 转化为原始类型


面试官:NaN === NaN返回什么?⭐⭐⭐⭐⭐

答:

返回 false,NaN<永远不等于NaN,判断是否为NaN用一个函数 isNaN来判断;
isNaN传入的如果是其他数据类型,那么现将它使用Number()转为数字类型在进行判断

call、apply、bind三者的用法和区别

面试官:手写call、apply、bind⭐⭐⭐⭐⭐

答:

  • call和apply实现思路主要是:

    • 判断是否是函数调用,若非函数调用抛异常
    • 通过新对象(context)来调用函数
      • 给context创建一个fn设置为需要调用的函数
      • 结束调用完之后删除fn
  • bind实现思路

    • 判断是否是函数调用,若非函数调用抛异常
    • 返回函数
      • 判断函数的调用方式,是否是被new出来的
        • new出来的话返回空对象,但是实例的proto指向_thisprototype
    • 完成函数柯里化
      • Array.prototype.slice.call()

call:

    Function.prototype.myCall = function (context) {
      // 先判断调用myCall是不是一个函数
      // 这里的this就是调用myCall的
      if (typeof this !== 'function') {
        throw new TypeError("Not a Function")
      }
      // 不传参数默认为window
      context = context || window

      // 保存this
      context.fn = this

      // 保存参数
      let args = Array.from(arguments).slice(1)   //Array.from 把伪数组对象转为数组

      // 调用函数
      let result = context.fn(...args)
      
      delete context.fn
      
      return result
    }

apply:

Function.prototype.myApply = function (context) {
      // 判断this是不是函数
      if (typeof this !== "function") {
        throw new TypeError("Not a Function")
      }

      let result

      // 默认是window
      context = context || window

      // 保存this
      context.fn = this

      // 是否传参
      if (arguments[1]) {
        result = context.fn(...arguments[1])
      } else {
        result = context.fn()
      }
      delete context.fn

      return result
    }

bind:

    Function.prototype.myBind = function(context){
      // 判断是否是一个函数
      if(typeof this !== "function") {
        throw new TypeError("Not a Function")
      }
      // 保存调用bind的函数
      const _this = this 
      // 保存参数
      const args = Array.prototype.slice.call(arguments,1)
      // 返回一个函数
      return function F () {
        // 判断是不是new出来的
        if(this instanceof F) {
          // 如果是new出来的
          // 返回一个空对象,且使创建出来的实例的__proto__指向_this的prototype,
          // 且完成函数柯里化
          return new _this(...args,...arguments)
        }else{
          // 如果不是new出来的改变this指向,且完成函数柯里化
          return _this.apply(context,args.concat(...arguments))
        }
      } 
    }

函数柯里化

详解JS函数柯里化
JS函数柯里化

面试官:字面量创建对象和new创建对象有什么区别,new内部都实现了什么,手写一个new⭐⭐⭐⭐⭐

答:

字面量:

  • 字面量创建对象更简单,方便阅读
  • 不需要作用域解析,速度更快
  • new内部:

创建一个新对象:

  • 使新对象的__proto__指向原函数的prototype
  • 改变this指向(指向新的obj)并执行该函数,执行结果保存起来作为result
  • 判断执行函数的结果是不是null或Undefined,如果是则返回之前的新对象,如果不是则返回result

手写new:

    // 手写一个new
    function myNew(fn, ...args) {
      // 创建一个空对象
      let obj = {}
      // 使空对象的隐式原型指向原函数的显式原型
      obj.__proto__ = fn.prototype
      // this指向obj
      let result = fn.apply(obj, args)
      // 返回
      return result instanceof Object ? result : obj
    }



执行栈和执行上下文

面试官:什么是作用域,什么是作用域链?⭐⭐⭐⭐

答:

  • 规定变量和函数的可使用范围称作作用域
  • 每个函数都有一个作用域链,查找变量或者函数时,需要从局部作用域到全局作用域依次查找,这些作用域的集合称作作用域链

面试官:什么是执行栈,什么是执行上下文?⭐⭐⭐⭐

答:

执行栈:

  • 首先栈特点:先进后出
  • 当进入一个执行环境,就会创建出它的执行上下文,然后进行压栈,当程序执行完成时,它的执行上下文就会被销毁,进行弹栈。
  • 栈底永远是全局环境的执行上下文,栈顶永远是正在执行函数的执行上下文
  • 只有浏览器关闭的时候全局执行上下文才会弹出

执行上下文分为:

  • 全局执行上下文
    • 创建一个全局的window对象,并规定this指向window,执行js的时候就压入栈底,关闭浏览器的时候才弹出
  • 函数执行上下文
    • 每次函数调用时,都会新创建一个函数执行上下文
    • 执行上下文分为创建阶段和执行阶段
      • 创建阶段:函数环境会创建变量对象:arguments对象(并赋值)、函数声明(并赋值)、变量声明 (不赋值),函数表达式声明(不赋值);会确定this指向;会确定作用域
      • 执行阶段:变量赋值、函数表达式赋值,使变量对象编程活跃对象
  • eval执行上下文


闭包

很多人都吃不透js闭包,这里推荐一篇文章:彻底理解js中的闭包

面试官:什么是闭包?闭包的作用?闭包的应用?⭐⭐⭐⭐⭐

答:

函数执行,形成私有的执行上下文,使内部私有变量不受外界干扰,起到保护保存的作用

作用:

  • 保护
    • 避免命名冲突
  • 保存
    • 解决循环绑定引发的索引问题
  • 变量不会销毁
    • 可以使用函数内部的变量,使变量不会被垃圾回收机制回收

应用:

  • 设计模式中的单例模式
  • for循环中的保留i的操作
  • 防抖和节流
  • 函数柯里化

缺点

  • 会出现内存泄漏的问题


显式原型和隐式原型

原型和原型链

面试官:什么是原型?什么是原型链?如何理解⭐⭐⭐⭐⭐

答:

原型: 原型分为隐式原型显式原型,每个对象都有一个隐式原型,它指向自己的构造函数的显式原型。每个构造方法都有一个显式原型。

  • __proto__是隐式原型;prototype是显式原型
  • 所有实例的__proto__都指向他们构造函数的prototype
  • 所有的prototype都是对象,自然它的__proto__指向的是Object()的prototype
  • 所有的构造函数的隐式原型指向的都是Function()的显示原型
  • Object的隐式原型是null

原型链: 多个__proto__组成的集合成为原型链(概念类似于作用域链)

  • instanceof 就是判断某对象是否位于某构造方法的原型链上。

继承

面试官:说一说 JS 中的常用的继承方式有哪些?以及各个继承方式的优缺点。⭐⭐⭐⭐⭐

答:

原型继承、组合继承、寄生组合继承、ES6的extend

原型继承

    // ----------------------方法一:原型继承
    // 原型继承
    // 把父类的实例作为子类的原型
    // 缺点:子类的实例共享了父类构造函数的引用属性   不能传参

    var person = {
      friends: ["a", "b", "c", "d"]
    }

    var p1 = Object.create(person)

    p1.friends.push("aaa")//缺点:子类的实例共享了父类构造函数的引用属性

    console.log(p1);
    console.log(person);//缺点:子类的实例共享了父类构造函数的引用属性

组合继承

    // ----------------------方法二:组合继承
    // 在子函数中运行父函数,但是要利用call把this改变一下,
    // 再在子函数的prototype里面new Father() ,使Father的原型中的方法也得到继承,最后改变Son的原型中的constructor

    // 缺点:调用了两次父类的构造函数,造成了不必要的消耗,父类方法可以复用
    // 优点可传参,不共享父类引用属性
    function Father(name) {
      this.name = name
      this.hobby = ["篮球", "足球", "乒乓球"]
    }

    Father.prototype.getName = function () {
      console.log(this.name);
    }

    function Son(name, age) {
      Father.call(this, name)
      this.age = age
    }

    Son.prototype = new Father()
    Son.prototype.constructor = Son


    var s = new Son("ming", 20)

    console.log(s);

寄生组合继承

    // ----------------------方法三:寄生组合继承
    function Father(name) {
      this.name = name
      this.hobby = ["篮球", "足球", "乒乓球"]
    }

    Father.prototype.getName = function () {
      console.log(this.name);
    }

    function Son(name, age) {
      Father.call(this, name)
      this.age = age
    }

    Son.prototype = Object.create(Father.prototype)
    Son.prototype.constructor = Son

    var s2 = new Son("ming", 18)
    console.log(s2);

extend

**// ----------------------方法四:ES6的extend(寄生组合继承的语法糖)
//     子类只要继承父类,可以不写 constructor ,一旦写了,则在 constructor 中的第一句话
// 必须是 super 。
class Son3 extends Father { // Son.prototype.__proto__ = Father.prototype
  constructor(y) {
    super(200)  // super(200) => Father.call(this,200)
    this.y = y
  }
}**

内存泄露、垃圾回收机制

面试官:什么是内存泄漏⭐⭐⭐⭐⭐

答:

内存泄露是指不再用的内存没有被及时释放出来,导致该段内存无法被使用就是内存泄漏

面试官:为什么会导致的内存泄漏⭐⭐⭐⭐⭐

答:

内存泄漏指我们无法在通过js访问某个对象,而垃圾回收机制却认为该对象还在被引用,因此垃圾回收机制不会释放该对象,导致该块内存永远无法释放,积少成多,系统会越来越卡以至于崩溃

面试官:垃圾回收机制都有哪些策略?⭐⭐⭐⭐⭐

答:

  • 标记清除法
    • 垃圾回收机制获取根并标记他们,然后访问并标记所有来自它们的引用,然后在访问这些对象并标记它们的引用…如此递进结束后若发现有没有标记的(不可达的)进行删除,进入执行环境的不能进行删除
  • 引用计数法
    • 当声明一个变量并给该变量赋值一个引用类型的值时候,该值的计数+1,当该值赋值给另一个变量的时候,该计数+1,当该值被其他值取代的时候,该计数-1,当计数变为0的时候,说明无法访问该值了,垃圾回收机制清除该对象
    • 缺点: 当两个对象循环引用的时候,引用计数无计可施。如果循环引用多次执行的话,会造成崩溃等问题。所以后来被标记清除法取代。

深拷贝和浅拷贝

手写浅拷贝深拷贝⭐⭐⭐⭐⭐

// 只是把对象的属性和属性值拷贝到另一个对象中
// ----------------------------------------------浅拷贝
    var obj1 = {
      a: {
        a1: { a2: 1 },
        a10: { a11: 123, a111: { a1111: 123123 } }
      },
      b: 123,
      c: "123"
    }

方式1:

 // 方式1
    function shallowClone1(o) {
      let obj = {}

      for (let i in o) {
        obj[i] = o[i]
      }
      return obj
    }

方式2:

 var shallowObj2 = { ...obj1 }

    // 方式3
    var shallowObj3 = Object.assign({}, obj1)

    let shallowObj = shallowClone1(obj1);

    shallowObj.a.a1 = 999
    shallowObj.b = true

    console.log(obj1);  //第一层的没有被改变,一层以下就被改变了
    
// ----------------------------------------------深拷贝

简易版

    function deepClone(o) {
      let obj = {}
      for (var i in o) {
        // if(o.hasOwnProperty(i)){
        if (typeof o[i] === "object") {
          obj[i] = deepClone(o[i])
        } else {
          obj[i] = o[i]
        }
        // }
      }
      return obj
    }


    var myObj = {
      a: {
        a1: { a2: 1 },
        a10: { a11: 123, a111: { a1111: 123123 } }
      },
      b: 123,
      c: "123"
    }

    var deepObj1 = deepClone(myObj)
    deepObj1.a.a1 = 999
    deepObj1.b = false
    console.log(myObj);
    

简易版存在的问题:参数没有做检验,传入的可能是 Array、null、regExp、Date

function deepClone2(o) {
      if (Object.prototype.toString.call(o) === "[object Object]") {  //检测是否为对象
        let obj = {}
        for (var i in o) {
          if (o.hasOwnProperty(i)) {
            if (typeof o[i] === "object") {
              obj[i] = deepClone(o[i])
            } else {
              obj[i] = o[i]
            }
          }
        }
        return obj
      } else {
        return o
      }
    }

  function isObject(o) {
      return Object.prototype.toString.call(o) === "[object Object]" || Object.prototype.toString.call(o) === "[object Array]"
   }

// 继续升级,没有考虑到数组,以及ES6中的map、set、weakset、weakmap

function deepClone3(o) {
      if (isObject(o)) {//检测是否为对象或者数组
        let obj = Array.isArray(o) ? [] : {}
        for (let i in o) {
          if (isObject(o[i])) {
            obj[i] = deepClone(o[i])
          } else {
            obj[i] = o[i]
          }
        }
        return obj
      } else {
        return o
      }
    }

//有可能碰到循环引用问题 var a = {}; a.a = a; clone(a);//会造成一个死循环
// 循环检测
// 继续升级

function deepClone4(o, hash = new map()) {
  if (!isObject(o)) return o//检测是否为对象或者数组
  if (hash.has(o)) return hash.get(o)
  let obj = Array.isArray(o) ? [] : {}

  hash.set(o, obj)
  for (let i in o) {
    if (isObject(o[i])) {
      obj[i] = deepClone4(o[i], hash)
    } else {
      obj[i] = o[i]
    }
  }
  return obj
}

递归易出现爆栈问题
将递归改为循环,就不会出现爆栈问题了

var a1 = { a: 1, b: 2, c: { c1: 3, c2: { c21: 4, c22: 5 } }, d: 'asd' };
var b1 = { b: { c: { d: 1 } } }
function cloneLoop(x) {
  const root = {};
  // 栈 
  const loopList = [  //->[]->[{parent:{a:1,b:2},key:c,data:{ c1: 3, c2: { c21: 4, c22: 5 } }}]
    {
      parent: root,
      key: undefined,
      data: x,
    }
  ];
  while (loopList.length) {
    // 深度优先
    const node = loopList.pop();
    const parent = node.parent; //{} //{a:1,b:2}
    const key = node.key; //undefined //c
    const data = node.data; //{ a: 1, b: 2, c: { c1: 3, c2: { c21: 4, c22: 5 } }, d: 'asd' }  //{ c1: 3, c2: { c21: 4, c22: 5 } }}
    // 初始化赋值目标,key 为 undefined 则拷贝到父元素,否则拷贝到子元素
    let res = parent; //{}->{a:1,b:2,d:'asd'} //{a:1,b:2}->{}
    if (typeof key !== 'undefined') {
      res = parent[key] = {};
    }
    for (let k in data) {
      if (data.hasOwnProperty(k)) {
        if (typeof data[k] === 'object') {
          // 下一次循环 
          loopList.push({
            parent: res,
            key: k,
            data: data[k],
          })
        } else {
          res[k] = data[k];
        }
      }
    }
  }
  return root
}

function deepClone5(o) {
      let result = {}
      let loopList = [
        {
          parent: result,
          key: undefined,
          data: o
        }
      ]

      while (loopList.length) {
        let node = loopList.pop()
        let { parent, key, data } = node
        let anoPar = parent
        if (typeof key !== 'undefined') {
          anoPar = parent[key] = {}
        }

        for (let i in data) {
          if (typeof data[i] === 'object') {
            loopList.push({
              parent: anoPar,
              key: i,
              data: data[i]
            })
          } else {
            anoPar[i] = data[i]
          }
        }
      }
      return result
    }


    let cloneA1 = deepClone5(a1)
    cloneA1.c.c2.c22 = 5555555
    console.log(a1);
    console.log(cloneA1);

// ------------------------------------------JSON.stringify()实现深拷贝

function cloneJson(o) {
  return JSON.parse(JSON.stringify(o))
}

深拷贝能使用hash递归的方式写出来就可以了 不过技多不压身,
推荐还是看一看使用while实现深拷贝方法

单线程,同步异步

面试官:为什么JS是单线程的?⭐⭐⭐⭐⭐

答: 因为JS里面有可视的Dom,如果是多线程的话,这个线程正在删除DOM节点,另一个线程正在编辑Dom节点,导致浏览器不知道该听谁的

面试官:说说 Promise 的原理?你是如何理解 Promise 的?⭐⭐⭐⭐⭐

做到简易版的promise理解,以及会写race和all函数就可以

答:

class MyPromise2 {
      constructor(executor) {
        // 规定状态
        this.state = "pending"
        // 保存 `resolve(res)` 的res值
        this.value = undefined
        // 保存 `reject(err)` 的err值
        this.reason = undefined
        // 成功存放的数组
        this.successCB = []
        // 失败存放的数组
        this.failCB = []


        let resolve = (value) => {
          if (this.state === "pending") {
            this.state = "fulfilled"
            this.value = value
            this.successCB.forEach(f => f())
          }
        }
        let reject = (reason) => {
          if (this.state === "pending") {
            this.state = "rejected"
            this.value = value
            this.failCB.forEach(f => f())
          }
        }

        try {
          // 执行
          executor(resolve, reject)
        } catch (error) {
          // 若出错,直接调用reject
          reject(error)
        }
      }
      then(onFulfilled, onRejected) {
        if (this.state === "fulfilled") {
          onFulfilled(this.value)
        }
        if (this.state === "rejected") {
          onRejected(this.value)
        }
        if (this.state === "pending") {
          this.successCB.push(() => { onFulfilled(this.value) })
          this.failCB.push(() => { onRejected(this.reason) })
        }
      }
    }


    Promise.all = function (promises) {
      let list = []
      let count = 0
      function handle(i, data) {
        list[i] = data
        count++
        if (count == promises.length) {
          resolve(list)
        }
      }
      return Promise((resolve, reject) => {
        for (let i = 0; i < promises.length; i++) {
          promises[i].then(res => {
            handle(i, res)
          }, err => reject(err))
        }
      })
    }

     Promise.race = function (promises) {
          return Promise((resolve, reject) => {
              for (let i = 0; i < promises.length; i++) {
                  promises[i].then(res => {
                      resolve(res)
                  }, err => {
                      reject(err)
                  })
              }

          })
      }
  }

面试官:以下代码的执行顺序是什么⭐⭐⭐⭐⭐

答:

  async function async1() {
   console.log('async1 start')
   await async2()
   console.log('async1 end')
  }
  async function async2() {
   console.log('async2')
  }
  async1()
  console.log('script start')
//执行到await时,如果返回的不是一个promise对象,await会阻塞下面代码(当前async代码块的代码),会先执行async外的同步代码(在这之前先看看await中函数的同步代码,先把同步代码执行完),等待同步代码执行完之后,再回到async内部继续执行
//执行到await时,如果返回的是一个promise对象,await会阻塞下面代码(当前async代码块的代码),会先执行async外的同步代码(在这之前先看看await中函数的同步代码,先把同步代码执行完),等待同步代码执行完之后,再回到async内部等promise状态达到fulfill的时候再继续执行下面的代码
//所以结果为
//async1 start
//async2
//script start
//async1 end

面试官:宏任务和微任务都是怎样执行的⭐⭐⭐⭐⭐

答:

  1. 执行宏任务script,
  2. 进入script后,所有的同步任务主线程执行
  3. 所有宏任务放入宏任务执行队列
  4. 所有微任务放入微任务执行队列
  5. 先清空微任务队列,
  6. 再取一个宏任务,执行,再清空微任务队列
  7. 依次循环

例题1

setTimeout(function(){
    console.log('1')
});
new Promise(function(resolve){
    console.log('2');
    resolve();
}).then(function(){
    console.log('3')
});
console.log('4');
new Promise(function(resolve){
    console.log('5');
    resolve();
}).then(function(){
    console.log('6')
});
setTimeout(function(){
    console.log('7')
});
function bar(){
    console.log('8')
    foo()
}
function foo(){
    console.log('9')
}
console.log('10')
bar()

解析

  1. 首先浏览器执行Js代码由上至下顺序,遇到setTimeout,把setTimeout分发到宏任务Event Queue中
  2. new Promise属于主线程任务直接执行打印2
  3. Promis下的then方法属于微任务,把then分到微任务 Event Queue中
  4. console.log(‘4’)属于主线程任务,直接执行打印4
  5. 又遇到new Promise也是直接执行打印5,Promise 下到then分发到微任务Event Queue中
  6. 又遇到setTimouse也是直接分发到宏任务Event Queue中,等待执行
  7. console.log(‘10’)属于主线程任务直接执行
  8. 遇到bar()函数调用,执行构造函数内到代码,打印8,在bar函数中调用foo函数,执行foo函数到中代码,打印9
  9. 主线程中任务执行完后,就要执行分发到微任务Event Queue中代码,实行先进先出,所以依次打印3,6
  10. 微任务Event Queue中代码执行完,就执行宏任务Event Queue中代码,也是先进先出,依次打印1,7。

例题2

setTimeout(() => {
  console.log('1');
  new Promise(function (resolve, reject) {
    console.log('2');
    setTimeout(() => {
      console.log('3');
    }, 0);
    resolve();
  }).then(function () {
    console.log('4')
  })
}, 0);
console.log('5'); //5 7 10 8 1 2 4 6 3
setTimeout(() => {
  console.log('6');
}, 0);
new Promise(function (resolve, reject) {
  console.log('7');
  // reject();
  resolve();
}).then(function () {
  console.log('8')
}).catch(function () {
  console.log('9')
})
console.log('10');

运行结果: 5 7 10 8 1 2 4 6 3

变量提升

面试官:变量和函数怎么进行提升的?优先级是怎么样的?⭐⭐⭐

答:

  • 对所有函数声明进行提升(除了函数表达式和箭头函数),引用类型的赋值
    • 开辟堆空间
    • 存储内容
    • 将地址赋给变量
    • 对变量进行提升,只声明,不赋值,值为undefined

面试官:var let const 有什么区别⭐⭐⭐⭐⭐

答:

  • var
    • var声明的变量可进行变量提升,let和const不会
    • var可以重复声明
    • var在非函数作用域中定义是挂在到window上的
  • let
    • let声明的变量只在局部起作用
    • let防止变量污染
    • 不可在声明
  • const
    • 具有let的所有特征
    • 不可被改变
    • 不可改变只适用于直接地址。如果使用const声明的是对象的话,是可以修改对象内部的值的。

模块化

面试官:为什么要使用模块化?都有哪几种方式可以实现模块化,各有什么特点?⭐⭐⭐

  • 为什么要使用模块化
    • 防止命名冲突
    • 更好的分离,按需加载
    • 更好的复用性
    • 更高的维护性

面试官:exports和module.exports有什么区别?⭐⭐⭐

  • 导出方式不一样
    • exports.xxx=‘xxx’
    • module.export = {}
  • exports是module.exports的引用,两个指向的是用一个地址,而require能看到的只有module.exports

面试官:JS模块包装格式有哪些?⭐⭐⭐

  • commonjs
    • 同步运行,不适合前端
  • AMD
    • 异步运行
    • 异步模块定义,主要采用异步的方式加载模块,模块的加载不影响后面代码的执行。所有依赖这个模块的语句都写在一个回调函数中,模块加载完毕,再执行回调函数
  • CMD
    • 异步运行
    • seajs 规范

面试官:ES6和commonjs的区别⭐⭐⭐

Commonjs、AMD、CMD、UMD、ESM 都有什么区别

  • Commonjs

    • 是同步执行的,不适合前端,后端 nodejs 可以使用 commonjs。

    • 使用方式

module.exports = xxx
require('xxx')

AMD/CMD/UMD 适用前端 异步执行

  • AMD
define(["a","b","c","d","e"],function(a,b,c,d,e){
	// 相当于在前面声明并初始化了要用到的所有模块
	a.dosomething()
  
  if(false) {
    // 即使没有用到模块 b,也会提前执行
    b.dosomething()
  }
	
})
  • CMD
define(function(require, exports, module){
	var a = require("./a") //需要的时候声明
  a.dosomething()
  if(false) {
    var b = require("./b")
    b.dosomething()
  }
})
  • AMD 和 CMD 的差别是

    • AMD 是依赖前置(把依赖放在前面)、提前执行(即使没有用到某个模块,也会提前执行)
    • CMD依赖就近、延时执行(用到的时候在声明依赖)

  • ESM

    • 使用 export 、 export default 来导出模块,使用 import 来引入模块

  • ESM 和 commonjs 的区别主要在于

    • commonjs 是运行时加载 ;ESM 是编译时加载
    • commonjs 是同步加载模块;ESM 是异步加载模块
    • commonjs 是对值的浅拷贝;ESM 是对值的引用,而且不可修改(直接地址不可修改,类似于 const)。



面试官问:require 和 import的区别?⭐⭐⭐

  • 调用时机

    • require 是运行时调用,所以其实是可以放在任何地方的
    • Import 是编译时调用,所以必须放在文件的开头
  • 使用时,

    • require 需要使用 module.exports = fs 或者exports.fs = xxx
    • import 用 export default 或 export const xx
  • 解构赋值

    • require 是赋值的过程
    • import 是解构的过程

JS其他

面试官:箭头函数和普通函数的区别?箭头函数可以当做构造函数 new 吗?⭐⭐⭐⭐⭐

  • 箭头函数是普通函数的简写,但是它不具备很多普通函数的特性
  • 第一点,this指向问题,箭头函数的this指向它定义时所在的对象,而不是调用它的对象
  • 不会进行函数提升
  • 没有arguments对象,不能使用arguments,如果要获取参数的话可以使用rest运算符
  • 没有yield属性,不能作为生成器Generator使用
  • 不能new
    • 没有自己的this,不能调用call和apply
    • 没有prototype,new关键字内部需要把新对象的_proto_指向函数的prototype

ajax

手写 ajax⭐⭐⭐⭐

var Ajax = {
      get: function (url, callback) {
        let xhr = XMLHttpRequest();
        xhr.open("get", url, false)
        xhr.onreadystatechange = function () {
          if (xhr.readyState == 4) {
            if (xhr.status == 200 || xhr.status == 304) {
              console.log(xhr.responseText);
              callback(xhr.responseText)
            }
          }
        }
        xhr.send()
      },



      post: function (url, data, callback) {
        let xhr = new XMLHttpRequest()
        // 第三个参数为是否异步执行
        xhr.open('post', url, true)
        // 添加http头,设置编码类型
        xhr.setRequestHeader("Content-type","x-www-form-urlencoded")
        xhr.onreadystatechange = function () {
          if(xhr.readyState == 4) {
            if(xhr.status == 200 || xhr.status == 304) {
              console.log(xhr.responseText);
              callback(xhr.responseText)
            }
          }
        }
        xhr.setRequestHeader('Content-type', "application/x-www-urlencoded")
        xhr.send(data)
      }
    }

防抖节流

什么是防抖?什么是节流?手写一个⭐⭐⭐⭐⭐

  • 防抖
    • n秒后在执行该事件,若在n秒内被重复触发,则重新计时
  • 节流
    • n秒内只运行一次,若在n秒内重复触发,只有一次生效

防抖:

 // ---------------------------------------------------------防抖函数
    function debounce(func, delay) {
      let timeout
      return function () {
        let arg = arguments
        if (timeout) clearTimeout(timeout)
        timeout = setTimeout(() => {
          func(arg)
        }, delay);
      }
    }

    // ---------------------------------------------------------立即执行防抖函数
    function debounce2(fn, delay) {
      let timer

      return function () {
        let args = arguments
        if (timer) clearTimeout(timer)


        let callNow = !timer
        timer = setTimeout(() => {
          timer = null
        }, delay);
        if (callNow) { fn(args) }
      }
    }
    // ---------------------------------------------------------立即执行防抖函数+普通防抖
    function debounce3(fn, delay, immediate) {
      let timer

      return function () {
        let args = arguments
        let _this = this
        if (timer) clearTimeout(timer)

        if (immediate) {
          let callNow = !timer
          timer = setTimeout(() => {
            timer = null
          }, delay);

          if (callNow) { fn.apply(_this, args) }
        } else {
          timeout = setTimeout(() => {
            func.apply(_this, arguments)
          }, delay);
        }
      }
    }

节流

  // ---------------------------------------------------------节流 ,时间戳版

    function throttle(fn, wait) {

      let previous = 0
      return function () {
        let now = Date.now()
        let _this = this
        let args = arguments
        if (now - previous > wait) {
          fn.apply(_this, arguments)
          previous = now
        }
      }
    }

    // ---------------------------------------------------------节流 ,定时器版
    function throttle2(fn, wait) {
      let timer
      return function () {
        let _this = this
        let args = arguments
        if (!timer) {
          timer = setTimeout(() => {
            timer = null
            fn.apply(_this, arguments)
          }, wait);
        }
      }
    }

函数柯里化原理⭐⭐⭐⭐⭐

function add() {
  var args = Array.prototype.slice.call(arguments)

  var adder = function () {
    args.push(...arguments)
    return adder
  }

  adder.toString = function () {
    return args.reduce((prev, curr) => {
      return prev + curr
    }, 0)
  }

  return adder
}

let a = add(1, 2, 3)
let b = add(1)(2)(3)
console.log(a)
console.log(b)
console.log(add(1, 2)(3));
console.log(Function.toString)


// --------普通函数转为柯里化函数------
function createCurry(fn, args = []) {
    return function () {
        let _args = args.concat(...arguments)
        if (_args.length < fn.length) {
            return createCurry.call(this, fn, _args)
        }
        return fn.apply(this, _args)
    }
}

function add(a, b, c) {
    return a + b + c;
}

var _add = createCurry(add);

console.log(_add(1, 2, 3));
console.log(_add(1)(2, 3));
console.log(_add(1)(2)(3));

什么是requestAnimationFrame?⭐⭐⭐⭐

  • requestAnimationFrame请求数据帧可以用做动画执行

  • 可以自己决定什么时机调用该回调函数

  • 能保证每次频幕刷新的时候只被执行一次

  • 页面被隐藏或者最小化的时候暂停执行,返回窗口继续执行,有效节省CPU

var s = 0
function f() {
  s++
  console.log(s);
  if (s < 999) {
    window.requestAnimationFrame(f)
  }
}
window.requestAnimationFrame(f)

设计模式

js常见的设计模式⭐⭐⭐⭐

单例模式、工厂模式、构造函数模式、发布订阅者模式、迭代器模式、代理模式

  • 单例模式

  • 不管创建多少个对象都只有一个实例

var Single = (function () {
  var instance = null
  function Single(name) {
    this.name = name
  }
  return function (name) {
    if (!instance) {
      instance = new Single(name)
    }
    return instance
  }
})()

var oA = new Single('hi')
var oB = new Single('hello')
console.log(oA);
console.log(oB);
console.log(oB === oA);
  • 工厂模式

  • 代替new创建一个对象,且这个对象想工厂制作一样,批量制作属性相同的实例对象(指向不同)

function Animal(o) {
  var instance = new Object()
  instance.name = o.name
  instance.age = o.age
  instance.getAnimal = function () {
    return "name:" + instance.name + " age:" + instance.age
  }
  return instance
}

var cat = Animal({name:"cat", age:3})
console.log(cat);
  • 构造函数模式

  • 发布订阅者模式


```javascript
  class Watcher {
      // name模拟使用属性的地方
      constructor(name, cb) {
        this.name = name
        this.cb = cb
      }
      update() {//更新
        console.log(this.name + "更新了");
        this.cb() //做出更新回调
      }
    }

    class Dep {//依赖收集器
      constructor() {
        this.subs = []
      }
      addSubs(watcher) {
        this.subs.push(watcher)
      }
      notify() {//通知每一个观察者做出更新
        this.subs.forEach(w => {
          w.update()
        });
      }
    }

    // 假如现在用到age的有三个地方
    var w1 = new Watcher("我{{age}}了", () => { console.log("更新age"); })
    var w2 = new Watcher("v-model:age", () => { console.log("更新age"); })
    var w3 = new Watcher("I am {{age}} years old", () => { console.log("更新age"); })

    var dep = new Dep()
    dep.addSubs(w1)
    dep.addSubs(w2)
    dep.addSubs(w3)


    // 在Object.defineProperty 中的 set中运行
    dep.notify()
  • 代理模式

  • 迭代器模式

点击300ms 延迟问题⭐⭐⭐⭐⭐

  • 设置一个 meta 标签
  • fastClick 插件
  • 自己实现
    • 实现大概思路:封装一个函数,接受两个参数,一个是目标对象,一个是点击后的回调函数; 在 ontouchstart 开始计时,在 ontouchend 中计时结束,如果超出150ms 就
    • //封装函数,处理延迟300ms问题
 function tap(obj, callback) {
        var isMove = false;
        var startTime = 0;
        obj.addEventListener('touchstart', function () {
            startTime += Date.now();
        })
        obj.addEventListener('touchmove', function () {
            isMove = true;
        })
        obj.addEventListener('touchend', function () {
            if (!isMove && (Date.now() - startTime) < 150) {
                callback && callback();
            }
            isMove = false;
            startTime = 0;
        })
    }

如何实现上传视频?⭐⭐⭐⭐

  • input type = 'file’去接收

  • 用 window.URL.createObjectURL(file)把 file 文件转换为 URL(现场的/前端转为 URL)
    或者用 FormData 去接收, 把 file 文件append 进去,然后传给后端,使用返回的 URL

setTimeOut第三个参数是什么?⭐⭐⭐⭐⭐

可以作为参数传给函数,一般用于 for 循环赋值

什么是暂时性死区?⭐⭐⭐⭐⭐

暂时性死区是指,当进入一个作用域,我去使用一个变量名,而这个变量名已经存在了,但是是不可获取的,就会报错,造成暂时性死区问题;比如一个作用域下面使用了 let 定义了 x,但是在定义之前就使用了 x,就会报错;暂时性死区意味着 typeof 也不是绝对安全的操作

x = '123'; // 报错

let x = 1

typeof y; // 报错

let y = 123

js 遍历数组的方法⭐⭐⭐⭐⭐

reduce、map、filter、every、some、foreach.

数组可以改变原数组的方法⭐⭐⭐⭐⭐

Push、pop、shift、unshift、splice、sort、reverse

不改变的

join 变成字符

Slice,截取

concat 合并数组

forEach 和 map 有什么区别⭐⭐⭐⭐

  • foreach 没有返回值,一般如果用来遍历修改原数组的话可以用 foreach 方法

如何捕获浏览器关闭事件?⭐⭐

  • 创建一个canvas对象,把图片保存在 canvas 中,然后 canvas 对象 toDataUrl,在把 dataurl 数据存储在 localstorage 中。

或者使用 blob 二进制流存储,canvas 对象toBlob

如何实现 localstorage 定时清除⭐⭐⭐⭐

  • 自己重写一个 set 方法,内部逻辑就是添加一个现在的时间以及有效时长
  • 再重写一个 get 方法,每当 get 的时候先进行判断是否过期,如果过期就删除,并返回 null,没过期的话正常返回

web worker是干什么的?⭐⭐⭐

js是单线程的,而web worker可以多创建一个子线程,多出来的这个子线程执行代码时不会阻塞主线程。它有几个限制,

同源限制,子线程资源必须和主线程资源是同源

dom限制,子线程不能操作dom

文件限制,不能打开本机(file://)文件,只能来源于网络

通信限制,只能使用postmessage来传输信息

脚本限制,不能使用alert、confirm方法

jquery 如何实现链式调用⭐⭐⭐

let fun = {
    fun1: function() {
        console.log("fun1");
        return this;
    },
 
    fun2: function() {
        console.log("fun2");
        return this;
    },
 
    fun3: function() {
        console.log("fun3");
        return this;
    }
}
fun.fun1().fun2().fun3();

node 事件循环机制和浏览器事件循环机制有什么区别⭐⭐

  • 浏览器和 Node 环境下,microtask 任务队列的执行时机不同

    • Node 端,microtask 在事件循环的各个阶段之间执行
    • 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行

https://zhuanlan.zhihu.com/p/54882306

讲一讲Reflect⭐⭐

顾名思义,reflect反射的意思。可以反射对象

Reflect可以提供一些方法去拦截js的操作,Reflect不是一个函数对象,所以它不可构造,Reflect内部的方法和属性都是静态的。

比如创建一个没有原型的对象,也就是说他自己不能调用任何基于Object原型链上的方法

var myObject = Object.create(null) 

// 如果想列举它的key值,只需使用Reflect的静态方法,拦截该对象,然后做出处理
Reflect.ownKeys(myObject)

Object.keys和Object.getOwnPropertyNames有什么区别?⭐⭐⭐

Object.keys只列出非原型上可枚举的key值,而Object.getOwnPropertyNames列出非原型上的所有key值(Symbol除外)

如何配置rem⭐⭐⭐

 //rem适配
 (function () {
    const styleEle = document.createElement('style');
    const docWidth = document.documentElement.clientWidth;
    const rootFontSize = docWidth / 16;

    styleEle.innerHTML = 'html{font-size:' + rootFontSize + 'px!important}';
    document.head.appendChild(styleEle);

})()

clientHeight、offsetHeight、scrollHeight有什么区别⭐⭐⭐

  • clientHeight

    • 用户可见内部高度+padding
  • offsetHeight

    • 总高度,算上滚动条
  • scrollHeight

    • 可滚动高度的+padding
  • scrollTop

    • 当前元素距离顶部的距
  • 触底加载

    • scrollTop + clientHeight >= scrollHeight - 50px

bom和dom的区别⭐⭐⭐

bom就是window,包含windows(窗口)、navigator(浏览器)、screen(浏览器屏幕)、history(访问历史)、location(地址)等,浏览器相关的东西。bom是包含dom的。

dom是document, html相关的都在里面


倒计时用setimeout来实现还是setInterval⭐⭐⭐⭐
  • setTimeout
    • 因为假如用setInterval的话,该程序执行需要105ms,而设置的间隔为100ms,则还没运行完最后的那5毫秒就会运行下一次的函数

promise相对于async…await的优缺点⭐⭐⭐
  • promise

    • 无法取消
    • 错误无法被try…catch捕获,但是可以被catch方法捕获
  • async传染力比较强


fetch优缺点 ⭐⭐⭐⭐
  • fetch脱离了XHR,基于promise实现

  • 对某些错误不会reject,比如状态码400、500

  • fetch不支持超时timeout处理

  • fetch默认不携带cookie,需要手动配置

  • fetch没有办法监测请求进度,而xhr可以


秒传、分片传输、断点传输⭐⭐⭐⭐
  • 秒传
    • 文件上传前,服务器先对文件做MD5校验,如果服务器上有同样的文件,则返回一个新地址,如果不想秒传也可以,修改文件中的内容就可以了(改名字不行)
  • 分片传输
    • 利用Blob提供的slice方法把大文件分割为一个个小文件分别传输。全部上传完成时候由服务端进行归总整合
  • 断点传输
    • 在分片上传的基础上,分成一个个小文件之后,每个小文件上传完毕之后对其进行状态的存储(localStorage),如果中间发生网络断线或者刷新,下次可以接着上次的进度上传

e.target和e.currentTarget的区别⭐⭐⭐

e.target是点击的那个对象,e.currentTarget是绑定该事件的对象


JS性能优化的方式⭐⭐⭐⭐⭐
  • 垃圾回收
  • 闭包中的对象清楚
  • 防抖节流
  • 分批加载(setInterval,加载10000个节点)
  • 事件委托
  • 少用with
  • requestAnimationFrame的使用
  • script标签中的defer和async
  • CDN

JS章节的结束了,下面迎接下一个boss——计算机网络

在这里插入图片描述

计算机网络

面试官:讲一讲三次握手四次挥手,为什么是三次握手四而不是两次握手?⭐⭐⭐⭐⭐

  • 客户端和服务端之间通过三次握手建立连接,四次挥手释放连接

  • 三次握手,客户端先向服务端发起一个SYN包,进入SYN_SENT状态,服务端收到SYN后,给客户端返回一个ACK+SYN包,表示已收到SYN,并进入SYN_RECEIVE状态,最后客户端再向服务端发送一个ACK包表示确认,双方进入establish状态。

    • 之所以是三次握手而不是两次,是因为如果只有两次,在服务端收到SYN后,向客户端返回一个ACK确认就进入establish状态,万一这个请求中间遇到网络情况而没有传给客户端,客户端一直是等待状态,后面服务端发送的信息客户端也接受不到了。
  • 四次挥手,首先客户端向服务端发送一个FIN包,进入FIN_WAIT1状态,服务端收到后,向客户端发送ACK确认包,进入CLOSE_WAIT状态,然后客户端收到ACK包后进入FIN_WAIT2状态,然后服务端再把自己剩余没传完的数据发送给客户端,发送完毕后在发送一个FIN+ACK包,进入LAST_ACK(最后确认)状态,客户端收到FIN+ACK包后,再向服务端发送ACK包,在等待两个周期后在关闭连接

    • 之所以等待两个周期是因为最后客户端发送的ACK包可能会丢失,如果不等待2个周期的话,服务端在没收收到ACK包之前,会不停的重复发送FIN包而不关闭,所以得等待两个周期

面试官:HTTP的结构⭐⭐⭐⭐

  • 请求行 请求头 空行 请求体
    • 请求行包括 http版本号,url,请求方式
    • 响应行包括版本号,状态码,原因

HTTP头都有哪些字段⭐⭐⭐⭐

  • 请求头
    • cache-control 是否使用缓存
    • Connection:keep-alive 与服务器的连接状态
    • Host 主机域
  • 返回头
    • cache-control
    • etag 唯一标识,缓存用的
    • last-modified最后修改时间

面试官:说说你知道的状态码⭐⭐⭐⭐⭐

  • 2开头的表示成功
    • 一般见到的就是200
  • 3开头的表示重定向
    • 301永久重定向
    • 302临时重定向
    • 304表示可以在缓存中取数据(协商缓存)
  • 4开头表示客户端错误
    • 403跨域
    • 404请求资源不存在
  • 5开头表示服务端错误
    • 500
    • 504网关错误

网络OSI七层模型都有哪些?TCP是哪一层的⭐⭐⭐⭐

  • 七层模型
    • 应用层
    • 表示层
    • 会话层
    • 传输层
    • 网络层
    • 数据链路层
    • 物理层
  • TCP属于传输层
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值