前端面试总结-Javascript

262 阅读16分钟

预解析

function fn(a,c){
            console.log(a) //f a(){}
            var a = 123
            console.log(a) //123
            console.log(c) //f c(){}
            function a(){}
            if(false){
                var d=678
            }
            console.log(d)//undefined
            console.log(b)//undefined
            var b =function() {}
            console.log(b)//f(){}
            function c(){}
            console.log(c)//f c(){}
        }
        fn(1,2)
        //预编译
        //作用域的创建阶段 预编译阶段
        //预编译的时候做了哪些事情
        //js的变量对象 AO对象 供js引擎自己去访问的
        //1 创建了ao对象 2 找形参和变量的声明 作为ao对象的属性名 值为undefined 3 实参和形参相统一 4找函数        声明 会覆盖变量的声明
        AO:{
        a:undefined 1 function a(){}
        c:undefined 2 function c(){}
        d:undefined
        b:undefined
        }
  • 词法作用域:变量的作用域是在定义时决定而不是执行时决定,也就是说词法作用域取决于源码,通过静态分析就能确定,因此词法作用域也叫做静态作用域。 with和eval除外,所以只能说JS的作用域机制非常接近词法作用域(Lexical scope)

JS为什么是单线程

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript就是单线程。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

JS的事件循环机制

  • js中的异步操作比如fetch setTimeout setInterval压入到调用栈中的时候里面的回调函数会进入到消息队列中去,消息队列会等到调用栈清空之后再执行

  • promise async await的异步操作的时候会加入到微任务中去,会在调用栈清空的时候立即执行,调用栈中加入的微任务会立马执行(微任务:Promise process.nextTick)

image.png

下列代码执行

setTimeout(()=>{
    console.log(1)
},0)
setInterval(() => {
    console.log('interval')
}, 1000);
new Promise(resolve=>{
    console.log(2)
    resolve()
}).then(res=>{
    console.log(3)
})
new Promise(resolve=>{
    console.log(5)
    resolve()
}).then(resolve=>{
    console.log(6)
})
console.log(4)
​
/*
2
5
4
3
6
1
interval
*/

执行优先级 调用栈>微任务队列>消息队列

哪些是异步任务

  • setTimeout 和 setInterval
  • DOM事件
  • Promise
  • 网络请求
  • I/O

JS语言特性

  • 运行在客户端浏览器上
  • 不用预编译,直接解析执行代码(有预解析)
  • 是弱类型语言,较为灵活
  • 与操作系统无关,跨平台的语言
  • 脚本语言,解释性语言

JS数据类型

基本数据类型:

字符串(String)、数字(Number)、布尔(Boolean)、对空(Null)、未定义(Undefined)、Symbol

引用数据类型:

对象(Object)、数组(Array)、函数(Function)。

前端事件流

事件捕获阶段

处于目标阶段

事件冒泡阶段

arguments

function get(){
    console.log(arguments)
}
get(1,2,3)
//arguments是一个类数组对象
//可以通过Array.prototype.slice.call(arguments)来将其转化为数组
//也可以通过es6中的展开运算符来转化为数组
//for

为什么在调用这个函数的时候,代码中的b会变成全局变量

 function func(){
            let a=b=3
        }
        func()
        console.log(b)//3
        console.log(a)//error

因为let a=b=3等价于let a=(b=3) b并没有声明,所以在创建的时候会变成全局变量,在func()执行之后,因为b为全局变量,所以可以访问,但是a在func()的作用域内,外部并不能访问

this指向问题

this指向有哪几种

  • 默认绑定:全局环境中this默认绑定到window

  • 隐式绑定:被直接对象所包含的函数调用时,即方法调用时,this隐式绑定到该对象上

  • 隐式丢失:被隐式绑定的函数丢失绑定对象时,会默认绑定到window

    var name='window'
    var person={
        name:'zs',
        getName:function(){
            return this.name
        }
    }
    console.log(person.getName())//zs
    let getName2=person.getName
    console.log(getName2())//window
    
  • 显示绑定:通过call()、apply()、bind()方法把对象绑定到this上

  • new绑定:函数或方法调用前带有关键字new,就构成构造函数调用,对于this绑定来说,称为new绑定

在函数中直接使用

   function get(content){
         console.log(content)
     }
     get('您好')//可以看作下列语句的语法糖
     get.call(window,'您好')

函数作为对象的方法被调用(谁调用 就指向谁)

  var person = {
        name:'张三',
        run:function(time){
            console.log(`${this.name}在跑步 最多${time}min就不行了`)
        }
    }
    person.run(30)//可以看作下列语句的语法糖
    person.run.call(person,30)

例题

    var name=222
       var a ={
           name:111,
           say:function(){
               console.log(this.name)
           }
       }
       var fun = a.say
       fun() //fun,call(window) 222
       a.say()//a.say.call(a) 111
​
       var b={
           name:333,
           say:function(fun){
               fun()
           }
       }
       b.say(a.say)//传入之后等价于  fun()=a.say 等价于第一种 222
       b.say = a.say
       b.say()//b调用 等于console.log(this.name)复制到b的say方法中 即333

箭头函数中的this

  • 箭头函数中的this是在定义函数的时候绑定的,而不是在执行函数的时候绑定
  • 箭头函数中,this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为他没有this,所以也就不能用作构造函数
     var x =11
        var obj={
            x:22,
            say:()=>{
                console.log(this.x)
            }
        }
        obj.say()   //输出是11
     var obj={
               birth:1990,
               getAge:function(){
                   var b=this.birth
                   var fn=()=>new Date().getFullYear()-this.birth
                   return fn()
               }
           }
           console.log(obj.getAge())//输出是2021-1990=31

因为箭头函数在getAge()中定义的,其父级为obj内部作用域,所以其箭头函数中的this指向的是父级obj对象,this.birth即1990

改变this指向的三种方法

  • call
  • apply
  • bind
let obj = {
    name:'胡图图'
}
function Child(name){
    this.name = name
}
Child.prototype = {
    constructor:Child,
    showName:function(){
        console.log(this.name)
    }
}
var child = new Child('小美')
child.showName()//小美

child.showName.call(obj)//胡图图
child.showName.apply(obj)//胡图图
let bind = child.showName.bind(obj)//胡图图
bind()

call和apply会立即执行,而bind不会立即执行,bind除了返回是函数以外,它的参数和call是一样的。call传参是以逗号分割开,而apply传参则是一个数组

let arr = [1,2,3,4,5,6]
console.log(Math.max.call(null,1,2,3,4,5,6))//6
console.log(Math.max.call(null,arr))//NaN
console.log(Math.max.apply(null,arr))//6

JS中的深浅拷贝

赋值:当我们把一个对象赋值给一个新的变量时候,赋的其实是该对象在栈中的地址,而不是堆中的数据,也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此两个对象是联动的

浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响

深拷贝:从堆中开辟一块新的区域存放对象,原始对象和新对象之前互不影响 \

操作和原数据是否指向同一对象第一层数据为一般数据类型第一层数据不是一般数据类型
赋值改变会使得原始数据改变改变会改变原始数据
浅拷贝改变不会改变原始数据改变会改变原始数据
深拷贝改变不会改变原始数据改变不会改变原始数据
  • 赋值代码

    var person = {
               name:"张三",
               hobby:['学习','敲代码','吃瓜']
           }
    var person1 = person
       person1.name = "李四"
       person1.hobby[0]='玩耍'
       console.log(person)//都是name:"李四" hobby:['玩耍','敲代码','吃瓜']
       console.log(person1)
    
  • 浅拷贝代码(基本数据类型不受影响,引用数据类型被同时改变)

    var person = {
               name:"张三",
               hobby:['学习','敲代码','吃瓜']
           }
    function shallowCopy(obj){
               var target = {}
               for(var i in obj){
                   if(obj.hasOwnProperty(i)){
                       target[i]=obj[i]
                   }
               }
               return target
           }
    ​
    var person1 = shallowCopy(person)
    person1.name='王二'
    person1.hobby[0]="玩耍"
    console.log(person)//张三       玩耍 敲代码  吃瓜
    console.log(person1)//王二      玩耍 敲代码  吃瓜
    
  • 深拷贝代码(拷贝前后数据互不影响)

    var person = {
               name:"张三",
               hobby:['学习','敲代码','吃瓜']
           }
    function deepClone(obj){
            var cloneObj = new obj.constructor()
            if(obj===null) return obj
            if(obj instanceof Date) return new Datr(obj)
            if(obj instanceof RegExp) return new RegExp(obj)
            if(typeof obj !== 'object') return obj
            for(var i in obj){
                if(obj.hasOwnProperty(i)){
                    cloneObj[i]=deepClone(obj[i])
                }
            }
            return cloneObj
        }
        // var person1 = JSON.parse(JSON.stringify(person))//大多数时候可以,但是如果对象中有日期、正   则表达式、函数就不行
        var person1 = deepClone(person)
        person1.name="胡图图"
        person1.hobby[0]="打麻将"
        console.log(person1)//二者完全不影响
        console.log(person)
    

浅拷贝的实现方式:

  • lodash里面的_.cloneObj
  • ...展开运算符
  • Array.prototype.concat()
  • Array.prototype.slice()

深拷贝的实现方式:

  • JSON.parse(JSON.stringify()) 在对象中有正则表达式、Date对象、正则对象、Promise时会发生异常
  • 递归实现(如上图中代码)
  • loadsh里面的cloneDeep()
  • jquery.extend()

闭包

什么是闭包?闭包有什么用?

闭包:能够访问其他函数作用域中的变量的函数

应用:模仿块级作用域;保存外部函数的变量;封装私有变量(单例模式)

闭包与堆内存:闭包中的变量并不保存在栈内存中,而是保存在堆内存中,所以函数调用之后闭包还能引用到函数内的变量

防抖

可以采用闭包的方式

 var input = document.querySelector('input')
      function debounce(delay){
          let timer
          return function(value){
              clearTimeout(timer)
              timer=setTimeout(function(){
                  console.log(value)
                  
              },delay)
          }
      }
      var debounceFunc = debounce(1000)
      input.addEventListener('keyup',function(e){
          debounceFunc(e.target.value)
      })

采用闭包的方式,return的函数中既可以使用timer,timer只会在调用debounce的时候生成一次

节流

一段时间内只做一件事情,无论点击多少次,等到执行完了之后再执行第二个事情,闭包解决节流

document.querySelector('button').addEventListener('click',thro(handle,2000))
        function thro(func,wait){
            let timeout
            return function(){
                if(!timeout){
                    timeout = setTimeout(function(){
                        func()
                        timeout=null
                    },wait)
                }
            }
        }
        function handle(){
            console.log(Math.random())
        }

闭包的底层原理

image.png

 function a(){
         var aa=123
         function b(){
             var bb = 234
             console.log(aa)
         }
         return b
     }
     var res = a()
     res()//能够输出123

因为a()执行完毕之后a对应的作用域链会断开,但是b在定义的时候就可以访问到a作用域内的作用域链,这个作用域链并不会断开,所以res()依然可以访问到aa=123,即可以输出123,闭包可以让变量一直保存在内存中。

闭包实现单例模式

var createLogin = function () {
        var div = document.createElement('div')
        div.innerHTML = "我是弹出的div"
        div.style.display = 'none'
        document.body.appendChild(div)
        return div
    }
​
    var getSingle = function(fn){
        var result
        return function(){
            return result || (result=fn.apply(this,arguments))
        }
    }
    
    var create = getSingle(createLogin)
    document.querySelector('button').onclick=function(){
        var loginLay = create()
        loginLay.style.display = "block"
    }

无论点击多少次,只创建一个div

哪些操作会造成内存泄漏

  • 闭包

  • 意外的全局变量

    function fn(){
        a=1//a就是由于失误未进行var或者let声明,会被自动var提升到全局变量
    }
    function fn(){
        this.a=1//this会指向window,就变成了window下的a变量,这个只有在关闭页面的时候才会销毁
    }
    
  • 被遗忘的定时器

  • 脱离dom的引用(比如var div=document.querySelector('div')之后将其删除,但是并未对消除div这个引用,在内存中依然保留了对于div的引用)

      var elements = {
                txt: document.getElementById("test")
            }
            function fn() {
                elements.txt.innerHTML = "1111"
            }
            function removeTxt() {
                document.body.removeChild(document.getElementById('test'));
            }
            fn();
            removeTxt()
            console.log(elements.txt)
    ​
    //依然会输出<div id="test">1111</div>
    

高阶函数

将函数作为参数或者返回值的函数

function highOrder(params,callback){
    return callback(params)
}

数组操作

数组扁平化

  • 数组自带的扁平化方法

    const arr = [1,[2,[3,[4,5]]],6]
    console.log(arr.flat(Infinity))
    
  • 正则加JSON.parse(JSON.stringify(arr))

    let arr=[1,[2,[3,[4,[5]]]]]
    let newarr=JSON.parse('['+JSON.stringify(arr).replace(/[|]/g,'')+']')
    console.log(newarr)
    
  • 递归

    const array=[]
    const fn=(arr)=>{
        for(let i=0;i<arr.length;i++){
            if(Array.isArray(arr[i])){
                fn(arr[i])
            }
            else{
                array.push(arr[i])
            }
        }
    }
    
  • reduce

arr.reduce(function(prev,cur,index,arr){})

arr 表示将要使用的原数组

prev 表示上一次调用的返回值或者初始值init

cur 表示当前正在处理的数组元素

index 表示当前正在处理的数组的索引,若提供init值,则索引为0,否则索引为1

init表示初始值

let arr=[1,[2,[3,[4,[5]]]]]
const fn=(arr)=>{
 return   arr.reduce((pre,cur)=>{
        return pre.concat(Array.isArray(cur)?fn(cur):cur)
    },[])
}
let newarr=fn(arr)
console.log(newarr)

数组去重

Set去重

let arr=[1,1,2,3,4,4,5,6]
let newarr=[...new Set(arr)]
console.log(newarr)

数组的IndexOf去重

let arr=[1,1,2,3,4,4,5,6]
function unique(arr){
    let newarr=[]
    for(var item of arr){
        if(newarr.indexOf(item)==-1){
            newarr.push(item)
        }
    }
    return newarr
}
console.log(unique(arr))

reduce使用

计算数组中每个元素出现的个数

let person=['胡图图','胡图图','牛爷爷','壮壮']
let nameNum = person.reduce((pre,cur)=>{
    if(cur in pre){
        pre[cur]++
    }else{
        pre[cur]=1
    }
    return pre
},{})
console.log(nameNum)

数组去重

var arr = [1,2,3,4,5,1,2,3,4,5]
let newArr = arr.reduce((pre,cur)=>{
     if(pre.includes(cur)){return pre}
     else{return pre.concat(cur)}//此处如果用push pre会变成1
 },[])
console.log(newArr)
​

数组遍历

for(let i=0;i<arr.length;i++){
   
}
for(var i in arr){//i是索引
    arr[i]
}
for(var value of arr){//value是遍历的每一项的值
    value
}
arr.map((item)=>{})//返回一个新数组
arr.forEach((item)=>{})//没有返回值

构造函数创建对象

ES5创建对象的三种方式

利用new Object()创建对象

var obj = new Object()

利用对象字面量创建对象

var obj = {}

利用构造函数创建对象

function Star(name,age){
    this.name = name
    this.age = age
    this.sing = function(){console.log('我会唱歌')}
}
var ldh = new Star('刘德华',12)//构造函数不同于普通函数,要使用new

采用new之后this才会指向实例对象,构造函数里面不需要return

通过this创建的叫做实例成员,只有实例才能访问

不通过this创建的叫做静态成员,可以通过Star直接访问,不能通过实例去访问

function Star(name,age){
    this.name = name
    this.age = age
    this.sing = function(){console.log('我会唱歌')}
}
Star.sex='男'
Star.play=function(){
    console.log('打麻将')
}
console.log(Star.sex)
Star.play()
let ldh = new Star('刘德华',12)
console.log(ldh.sex)//undefined
ldh.play()//报错

原型链

先来看看以下代码

function Star(name,age){
    this.name = name
    this.age = age
    this.sing = function(){
        console.log('我会唱歌')
    }
}
var ldh = new Star('刘德华',12)
var zxy = new Star('张学友',34)
console.log(ldh.sing===zxy.sing)//结果是false

说明创建两个对象,开辟了两个不同的内存空间,相同的部分仍然占用了不同的内存空间,造成了内存浪费

采用console.dir(Star)

image.png

Star有一个prototype属性,指向的是一个对象,即原型对象。所以可以考虑将公共的部分加到构造函数的原型对象上去,改写如下

function Star(name,age){
    this.name = name
    this.age = age
    
}
Star.prototype.sing = function(){console.log('我会唱歌')}
var ldh = new Star('刘德华',12)
var zxy = new Star('张学友',34)
console.log(ldh.sing===zxy.sing)//结果是true//可以通过以下方式使用这个sing函数
ldh.sing()
Star.prototype.sing()

对象原型

但是sing()是加在Star.prototype上面的,ldh为啥可以访问呢,这就引出了 __ proto __ 了,我们打印ldh看一下

image.png ldh有一个 __ proto __ ,对象都会有一个 __ proto __ 属性,它指向的是构造函数的prototype原型对象,即ldh的 __ proto __ 指向了Star的prototype,因此就可以使用挂载到Star的prototype上面的sing()方法了

可以通过以下代码来测试以下两者是否相等,结果为true,也验证了以上的说法

console.log(ldh.__proto__===Star.prototype)//true

用图说话直观一点(推荐b站pink老师,这一块讲的特别清晰,图片也是截取于他的视频中)

image.png Star中prototype指向中的construtor

console.log(Star.prototype)
console.log(ldh.__proto__)

image.png constructor顾名思义,就是构造函数,用以记录Star本身,constructor只要用于记录该对象引用与哪个构造函数,它可以让原型对象重新指向原来的构造函数

再考虑以上情况,假如我们要在prototype上挂载很多公共属性,采用Star.prototype.xxxx=xxxx的形式来说明显就非常的繁琐,于是考虑将我们要添加的封装成一个对象

Star.protoype={
    sing:function(){},
    movie:function(){},
    play:function(){}
    
}

但是采用这种方法添加的话,我们的对象会直接覆盖prototype原有的一些属性,也无法知道它是引自哪个地方,所以这个时候constructor的作用就体现出来了

Star.protoype={
    constructor:Star,//手动添加constructor将其指向重新指回Star
    sing:function(){},
    movie:function(){},
    play:function(){}
    
}

构造函数实例和原型对象三角关系

image.png

原型链

console.log(Star.prototype.__proto__===Object.prototype)//true
console.log(Object.prototype.__proto__)//null

image.png 组合继承(普通属性用call改变this 方法用prototype加new)(构造函数继承加原型对象继承)

function Parent(name){
    this.name = name
}
Parent.prototype.getName = function(){
    return this.name
}
function Child(name){
    Parent.call(this,name)
}
Child.prototype = new Parent()
Child.prototype.construcor = Child
const parent = new Parent('胡英俊')
const child = new Child('胡图图')
console.log(parent.name)//胡英俊
console.log(child.name)//胡图图
console.log(parent.getName())//胡英俊
console.log(child.getName())//胡图图

组合式继承的缺点(每一次创建Child实例对象的时候都要new一个Parent的实例对象)

寄生组合(child.prototype上加东西会影响parent.prototype)

//Child.prototype = new Parent()
Child.prototype = Parent.prototype

解决方案,浅拷贝

Child.prototype = Object.create(Parent.prototype)

对象字面量代替switch方法

const fruitsColor={
    red:['apple'],
    yellow:['banana']
}
function printFruits(color){
    return fruitsColor[color] || []
}
console.log(printFruits('y'))

JS类型判断

  • typeof

    typeof可以识别出基本类型boolean,number,undefined,string,symbol。但是不能识别null,null会返回object类型。对于引用类型,会把array、object统一归为object类型,但是可以识别出function。

  • instanceof

    从结果中看出instanceof不能识别出基本的数据类型 number、boolean、string、undefined、null、symbol。但是可以检测出引用类型,如array、object、function。

  • Object.prototype.toString.call

    此方法可以相对较全的判断js的数据类型。

什么是立即执行函数?有什么特点?有什么用处?

(function(){
    //执行语句
})()

特点:立即执行函数中的代码,又不会在内存中留下对该函数的引用,函数内部的所有变量都会被立即销毁(除非这些变量赋值给了包含作用域中的变量)

作用:实现块级作用域

JS垃圾回收机制

JS垃圾回收机制主要是由一个叫垃圾收集器(garbage collector)GC的后台进程负责监控、清理对象、并及时回收空闲内存。

GC的最主要职责是监控数据的可达性,“可达性”的值就是哪些以某种方式可以访问或可用的值。比如直接访问或者链式访问等,当没有入口去访问它的时候,这块数据就变成了垃圾会被清除掉(一般来说没有被引用的对象就是垃圾,就是要被清除的,有个例外,几个对象相互引用形成一个环,但是其实根本没有访问入口,这几个对象也是垃圾)

如何清除垃圾

  • 标记清除

    function func3 () {
          const a = 1
          const b = 2
          // 函数执行时,a b 分别被标记 进入环境
    }
    ​
    func3() // 函数执行结束,a b 被标记 离开环境,被回收
    
  • 引用计数

    function func4 () {
          const c = {} // 引用类型变量 c的引用计数为 0
          let d = c // c 被 d 引用 c的引用计数为 1
          let e = c // c 被 e 引用 c的引用计数为 2
          d = {} // d 不再引用c c的引用计数减为 1
          e = null // e 不再引用 c c的引用计数减为 0 将被回收
    }
    ​
    

    引用计数的缺点(循环引用)

    function func5 () {
          let f = {}
          let g = {}
          f.prop = g
          g.prop = f
          // 由于 f 和 g 互相引用,计数永远不可能为 0
    }
    

    像这种情况我们需要手动将变量的内存释放

    f.prop = null
    g.prop = null
    

    在现代浏览器中,JavaScript使用的方式是标记清除,所以我们无需担心循环引用

    ==与===,Object.is的区别

console.log(""==0)//true
console.log("0"==0)//true
console.log(123=="123")//true
console.log(null==undefined)//true
console.log(NaN==NaN)//false
      console.log(""===0)//false
      console.log("0"===0)//false
      console.log(123==="123")//false
      console.log(null===undefined)//false
      console.log(NaN===NaN)//false

NaN感觉六亲不认。。。。

==两边值得类型不一样会强制转换成number再进行比较

Object.is():与===基本一致,不同(Object.is(+0,-0) false;Object.is(NaN,NaN) true)

eval是什么

它的功能是将对应的字符串解析成js并执行,应该避免使用js,因为非常消耗性能(2次,一次解析成js,一次执行)