前端常见面试题整理

156 阅读13分钟

1.深拷贝和浅拷贝

相同:浅拷贝和深拷贝都是创建一个新的对象;

不同: 浅拷贝:如果创建的这个对象是基本数据类型,例如:字符串(string)、数值(number)、布尔值(boolean)、undefined、null,那么拷贝的就是基础类型的值,修改这个新对象,旧对象也不会变。如果拷贝的数据类型不是基础数据类型而是引用类型(复杂数据类型),例如:对象(Object)、数组(Array)、函数(Function)。那么拷贝的就是被拷贝对象的内存地址,此时你修改里面的属性,原对象也会被改变。

深拷贝:是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象。此时无论你如何修改新对象的属性,原对象也不会改变。方法:1.通过JSON.parse( )和JSON.stringify( )

image.png 缺点非常明显,无法copy函数,undefined和symbol,同样也无法解决循环引用的对象。

2.以递归方式使用for in 方法来实现深拷贝
   let obj = {name:{name:'1',id:[2,[3,4]]},id:[[1,2],[3,4]],son:undefined,sister:Symbol(1),fun:function(){return 1}}
    const deepClone = (obj)=>{
        let res = typeof obj==='obejct'?{}:[];
        if(obj&&typeof obj ==='object'){
            for(let key in obj){
                if(obj[key]&& typeof obj[key]==='object'){
                    res[key] = deepClone(obj[key])
                }else{
                    res[key] = obj[key];
                }
            }
        }
        return res;
    }
    let obj2 = deepClone(obj);
    obj.name.name='2';
    obj.id[0][0]=10;
    console.log(obj2);  

2.substr和substring的区别

两种方法简单来说都是截取字符串

substr:使用此方法的时候有两个值可以选择,例如:

    var str="Hello world!" 
    console.log(str.substr(n,b))

这个代码片段里面的n代表你想要从第几个字符串开始截取,比如n=1,此时就是从e开始截取,b代表你想要截取字符串的长度,比如b=3,那么此时打印出来的字符串就为ell。如果b不写具体值,那么就是默认截取到最后一个字符串了。

substring:同substr用法相同,但是需要注意的是他的b值是索引值且不包含结尾的,举例如下

    var str="Hello world!" 
             0123456789
    console.log(str.substr(1,3))
    打印出来的是:   el

记住重点,两种方法都是截取字符串,单词短的他就想要的长,所以包含结尾,单词长的要的少,不包含结尾。

3.slice和splice的区别

又是方法的区别,我想很多人和我一样,对这种单词差不多的方法经常会记混,下面我来说一下我的理解。

这两个方法呢是数组的截取方法,但是slice也可以用于字符串哦。

slice:此方法是用来截取部分数组的,并返回一个新数组,并不会改变原数组。语法如下:

    var str=['11','22','33','44','55']
              0    1    2    3    4
    console.log(str.slice(1,3))
    打印出来的是: ['22','33']

记住哦!1在此处表示从索引为1处开始,3表示在索引为3处结束,小伙伴们不好记,可以把此处的3看成停止信号,到了这里就结束,所以当然不包含它了

splice:此方法,插入、删除或替换数组的元素,返回被删除的项目。会改变原数组

语法:array.splice(index,n,b),各个参数的含义如下:

1.index:必须项,规定从何处添加、删除元素;

2.n:可选项,规定应该删除多少元素;如果为0,则不删除元素;若不写,默认删除到结尾的所有元素;

3.b:可选项,要添加到数组的新元素

示例:

var arr = [1,2,3,4,5];
console.log(arr.splice(2,1,"hello"));//[3]  返回的新数组
console.log(arr);//[1, 2, "hello", 4, 5]  改变了原数组

4.事件循环—宏任务和微任务

首先,想要了解这些概念,你要先知道 同步任务和异步任务

我们先从JavaScript的线程说起,JavaScript是单线程的,那为什么它是单线程的呢,这与他的用途有关。作为浏览器脚本语言,JavaScript主要用途是用来与用户交互以及操作DOM的。这决定了他只能是单线程流程,否则会有很多复杂的同步问题。举个例子:一个线程在某个节点里面添加一个元素,另外一个线程想删除这个节点,你咋办。听谁的?你先加还是先删?所以注定了JavaScript是一门单线程的语言。

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

同步任务和异步任务:

单线程,就是指一次只能完成一件任务,如果在同个时间有多个任务的话,这些任务就需要进行排队,前一个任务执行完,才会执行下一个任务。但如果有一个任务的执行时间很长,比如文件的读取、定时器、数据的请求等等,那么后面的任务就要一直等待,这就会影响用户的使用体验。 为了解决这种情况,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。

同步模式: 就是前一个任务执行完成后,再执行下一个任务,程序的执行顺序与任务的排列顺序是一致的、同步的;

异步模式: 则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行队列上的后一个任务,而是执行回调函数;后一个任务则是不等前一个任务的回调函数的执行而执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。

事件循环

上面我们简单的了解了同步异步任务后就可以往下看宏任务和微任务了:

我用比较容易理解的方式说明一下这两个的概念和执行的顺序,当你执行代码的时候,一般来说是从上往下执行代码顺序嘛,我用一下代码来说明下:

setTimeout(function(){
  console.log('1')
  new Promise(function(resolve){
  console.log('2');
     resolve();
}).then(function(){
 console.log('3')
});
  
console.log('4')
//执行结果,2,4,3,1
});

上述代码的执行结果是怎么回事呢? 你可以把浏览器执行任务看成出去旅游,所有同步任务看成做过核酸的人,直接按顺序排队通行即可,异步任务想要排队通行,则需要去做核酸。于是就把他们集中起来放在一个地方(事件列表:Event Table),但是他们也有区别,比如定时器和promise都是异步,那么谁快先执行呢?于是又进一步细分,把他们分成了宏任务和微任务。

宏任务就是由宿主环境发起的异步:setTimeOut、setInterval、特殊的(代码块、script)

image.png

image.png

image.png 微任务:由javascript 自身发起的异步

image.png

image.png 搞清了宏任务和微任务,接下来他们的顺序就简单了,我教你一个记忆方法,就像是搜装备,得先打开包(宏任务),看有没有东西(微任务),没有就下一个包(宏任务),有就深入执行。

  1. 先执行宏任务
  2. 宏任务执行完后看微任务队列是否有微任务
  3. 没有微任务执行下一个宏任务
  4. 有微任务将所有微任务执行
  5. 执行完微任务,执行下一个宏任务

1658738903095.png

5.watch和计算属性

先说下计算属性computed

简单点说根据你给定的数据进行实时计算赋值

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title></title>
    <script src="https://cdn.jsdelivr.net/npm/vue"></script>
  </head>
  <body>
    <div id="app">
      <input type="text" v-model="firstName" />
      <input type="text" v-model="lastName" /> 
      {{fullName}}
    </div>
    <script>
      new Vue({
        el: '#app',
        data: {
          firstName: 'hello',
          lastName: 'dashuaibi'
        },
        computed: {
          fullName: function() {
            return this.firstName + this.lastName;
          }
        }
      });
    </script>
  </body>
</html>

computed用法是有缓存的,要是你data里面有给定的数据,直接就能显示了,想换实时修改就可以, 性能就比较高。定义的时候是一个方法,使用的时候当作属性使用,只要 return 后面的数据发生变化,该计算属性就会重新计算,computed可以监控很多个变量,但是这个变量一定是vue实例里面的。

watch监听的是一个变量(或者是一个常量)的变化,这个变量可能是一个单一的变化也可能是一个数组。 可以监听的数据有(props、data、computed、$route) watch 侦听器如果监听的是一个对象,需要开启深度监听

简单点说watch监听你可以写异步函数,而计算属性,你只能写方法返回值

6. js数据类型和数据类型检测

数据类型

值类型(基本类型) :字符串(String)、数字(Number)、布尔(Boolean)、对空(Null)、未定义(Undefined)、Symbol。

引用数据类型(对象类型) :对象(Object)、数组(Array)、函数(Function),还有两个特殊的对象:正则(RegExp)和日期(Date)。

检测数据类型

  • type of

typeof可以正常检测出:number、boolean、string、object、function、undefined、symbol、bigint

  • 检测基本数据类型,null会检测object,因为null是一个空的引用对象
  • 检测复杂数据类型,除function外,均为object
  • instanceof

  • instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

image.png 注意:只能检测复杂数据类型---对象(Object)、数组(Array)、函数(Function)

  • toString

  • toString() 是 Object 的原型方法(Object.prototype-的方法),调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型

对于 Object 对象,直接调用 toString() 就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。

Object.prototype.toString.call('') ;              // [object String]
Object.prototype.toString.call(1) ;               // [object Number]
Object.prototype.toString.call(true) ;            // [object Boolean]
Object.prototype.toString.call(Symbol());         // [object Symbol]
Object.prototype.toString.call(undefined) ;       // [object Undefined]
Object.prototype.toString.call(null) ;            // [object Null]
Object.prototype.toString.call(new Function()) ;  // [object Function]
Object.prototype.toString.call(new Date()) ;      // [object Date]
Object.prototype.toString.call([]) ;              // [object Array]
Object.prototype.toString.call(new RegExp()) ;    // [object RegExp]
Object.prototype.toString.call(new Error()) ;     // [object Error]
Object.prototype.toString.call(document) ;        // [object HTMLDocument]
Object.prototype.toString.call(window) ;          // [object global] window 是全局对象 global 的引用

还有两种不常用的,面试就答出来上面就够了

constructor

constructor代表获取由哪个构造函数创建而出,可以检测出字面量方式创建的对象类型,因为字面方式创建,实际由对应类创建而出

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    const arr = []
    console.log(arr.constructor === Array)
  </script>
</body>
</html>

不是字面量的方式只会获取到构造函数

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    function Animal(name) {
      this.name = name
    }

    const dog = new Animal('大黄')

    console.log(dog.constructor === Object) // false
    console.log(dog.constructor === Animal) // true
  </script>
</body>
</html>

isArray

isArray可以检测出是否为数组

const arr = []
Array.isArray(arr)

 

7.生命周期,父子组件生命周期

答全面点:先从他是什么开始说起

生命周期:是指一个对象从创建到运行到销毁的整个过程,被称为生命周期

他有四个阶段八个钩子函数,分别是:

1.创建阶段:对应的钩子函数是

 beforeCreate() {
        // 这个生命周函数,代表开始创建实例了
        console.log('beforeCreate',this.num)
      },
 created () {
        // 代表数据和方法已经初始化成功了,此处dom还没有挂载到页面上
        console.log('created',this.num,this.$el)
      },

2.挂载阶段:对应的钩子函数是

 beforeMount () {
        // 挂载之前,创建了虚拟DOM咯
        console.log('beforeMount',this.$el)
      },
 mounted () {
        // dom已经挂载了
        console.log('mounted',this.$el)
      },

3.更新阶段:对应的钩子函数

 // 运行更新阶段
      beforeUpdate () {
        // 数据更新,页面还没有同步
        console.log('beforeUpdated',this.num,document.getElementById('app').innerHTML)
      },
      updated () {
        // 数据更新,页面已经同步
        console.log('updated',this.num,document.getElementById('app').innerHTML)
      },

4.销毁阶段:对应的钩子函数

  // 销毁阶段
      beforeDestroy () {
        // 销毁之前,处理掉定时器啊,解除各种数据引用,移除事件监听
        //删除组件_watcher,删除子实例,删除自身self等。同时将实例属性_isDestroyed置为true
        console.log('beforeDestroy')
      },
      destroyed () {
        // 已经销毁了
        console.log('destroy')
      }

父子组件生命周期

父子组件生命周期说白了就是逻辑关系

你想要用子组件你首先得创建父组件吧,自然而然的父组件就进入了创建阶段,也就是 beforeCreate()和created(),这时候父组件创建好了,但是你子组件没办法用啊。

于是就进入了挂载阶段了吧,父组件挂载的同时,子组件也要挂载啊,由此当父组件完成beforeMount()的时候就子组件此时就要进来了,子组件进来了也要经过创建阶段吧,于是执行beforeCreate()和created(),创建完成后自然也要进行挂载,于是经过beforeMount()和mounted(),然后再回来父组件,此过程你就可以看成包包子,先准备父亲(皮)再完成馅(子),然后父组件执行mounted(),完成闭合。

当都准备好了之后,往下走就是更新阶段了,依然是父组件先开始,beforeUpdate(),因为数据都再子组件里面,你想更新数据,肯定要等子组件数据更新完之后,再反馈给父组件吧,于是子组件进行beforeUpdate()和updated(),然后父组件再执行updated(),更新完毕。

销毁阶段同理更新阶段先从父组件开始beforeDestroy(),由此进入子组件,开始执行数据的销毁 beforeDestroy()和destroyed(),最后回到父组件执行destroyed()完成一个完整的生命周期

8.new的过程

要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4个步骤:

  1. 创建一个新对象;
  2. 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 返回新对象;

new操作符具体干了什么呢? 其实很简单,就干了三件事情。 举例说明一哈:

var obj = new Base();
1.var obj  = {};
2.obj.proto = Base.prototype;
3.Base.call(obj);

第一行,我们创建了一个空对象obj;
第二行,我们将这个空对象的 __proto__ 成员指向了 Base 函数对象 prototype 成员对象;
第三行,我们将 Base 函数对象的 this 指针替换成obj,然后再调用 Base 函数,于是我们就给 obj 对象赋值了一个 id 成员变量,
这个成员变量的值是 ”base” ,call 函数的用法就是改变this指向。
  1. 创建一个空对象,将它的引用赋给 this,继承函数的原型;
  2. 通过 this 将属性和方法添加至这个对象;
  3. 最后返回 this 指向的新对象,也就是实例(如果没有手动返回其他的对象);

9.原型链

对照着这个图看更容易理解

image.png 构造函数通过原型分配的函数是所有对象所共享的。

JavaScript 规定,每一个构造函数都有一个prototype 属性,指向另一个对象。注意这个prototype就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。

我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。

function Star(uname, age) {
    this.uname = uname;
    this.age = age;
}
Star.prototype.sing = function() {
	console.log('我会唱歌');
}
var ldh = new Star('刘德华', 18);
var zxy = new Star('张学友', 19);
ldh.sing();//我会唱歌
zxy.sing();//我会唱歌

对象都会有一个属性__proto__指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有__proto__原型的存在。 __proto__对象原型和原型对象 prototype 是等价的, __proto__对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性,因此实际开发中,不可以使用这个属性,它只是内部指向原型对象 prototype

对象原型__proto__和构造函数(prototype)原型对象里面都有一个属性 constructor 属性 ,constructor 我们称为构造函数,因为它指回构造函数本身。 constructor 主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。 一般情况下,对象的方法都在构造函数的原型对象中设置。如果有多个对象的方法,我们可以给原型对象采取对象形式赋值,但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了。此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。

如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用constructor指回原来的构造函数如:

 function Star(uname, age) {
     this.uname = uname;
     this.age = age;
 }
 // 很多情况下,我们需要手动的利用constructor 这个属性指回 原来的构造函数
 Star.prototype = {
 // 如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用constructor指回原来的构造函数
   constructor: Star, // 手动设置指回原来的构造函数
   sing: function() {
     console.log('我会唱歌');
   },
   movie: function() {
     console.log('我会演电影');
   }
}
var zxy = new Star('张学友', 19);
console.log(zxy)

以上代码运行结果,设置constructor属性如图:

image.png 如果未设置constructor属性,如图:

image.png ​ 每一个实例对象又有一个__proto__属性,指向的构造函数的原型对象,构造函数的原型对象也是一个对象,也有__proto__属性,这样一层一层往上找就形成了原型链。对照着图片更容易看懂

image.png