前端常见面试-js篇

javascript/jquery

浏览数:13

2020-5-18

AD:资源代下载服务

        前面我们已经梳理了前端面试中css的相关内容,同时也对面试中常问的本地存储 、缓存做了一个详细的介绍,有需要的可自行查看之前的文章,文章链接如下:
前端常见面试-css篇
前端常见面试-存储/缓存篇
        当然,对于前端开发来说,js那就是每一个前端小可爱都需要必备掌握的技能,无论现在多火爆的mv*框架,都是基于基础的js来进行的。好了,话不多说,直接进入主题

1. 请说出js中的数据类型

        答:js中的数据类型主要分为两种:基础类型和引用类型,其中基础类型包括:字符串(String)、数字(Number)、布尔(Boolean)、对空(Null)、未定义(Undefined)、Symbol(es6中引入的);引用类型包括:对象(Object)、数组(Array)、函数(Function)

2. 请说明一下js中定义变量的方式

        答:js中申明变量的方式主要有三种,分别为:var、let、const
1)var:定义的变量可以修改,如果不初始化会输出undefined,不会报错
2)let:是块级作用域,函数内部使用let定义后,对函数外部无影响
3)const:定义变量不可以修改,而且必须初始化,但是如果定义的是对象,则对象的属性可以修改(原因是引用的是对象的地址,地址不可更改,但是地址对应的内容可以修改)
        不过对于非严格模式下来说,没有使用这三者方式定义的变量会自动的挂载到window对象下,成为一个全局变量。

3. js的变量提升和函数提升

        答:在ES6之前,JavaScript没有块级作用域(一对花括号{}即为一个块级作用域),只有全局作用域和函数作用域。变量提升即将变量声明提升到它所在作用域的最开始的部分。js中创建函数有两种方式:函数声明式和函数表达式申明。只有函数声明才存在函数提升

4. js的事件冒泡机制

        答:事件冒泡(event bubbling),事件最开始时由触发的那个元素身上发生,然后沿着DOM树向上传播,直到document对象。

js事件冒泡的应用:

        1)事件冒泡允许多个操作被集中处理(把事件处理器添加到一个父级元素上,避免把事件处理器添加到多个子级元素上),它还可以让你在对象层的不同级别捕获事件
        2)让不同的对象同时捕获同一事件,并调用自己的专属处理程序做自己的事情

阻止js冒泡

        1)使用e.stopPropagation()
        2)根据当前触发的元素来进行判断阻止:e.target==e.currentTarget

事件冒泡的注意事项

●事件捕获其实有三种方式,事件冒泡只是其中的一种:(1)IE从里到外(inside→outside)的冒泡型事件。(2)Netscape4.0从外到里(outside→inside)的捕获型事件。(3)DOM事件流,先从外到里,再从里到外回到原点(outside→inside→outside)的事件捕获方法(似乎对象将触发两次事件处理,这有什么作用?鄙人不懂!)。

●不是所有的事件都能冒泡。以下事件不冒泡:blur、focus、load、unload。

●事件捕获方式在不同浏览器,甚至同种浏览器的不同版本中是有所区别的。如Netscape4.0采用捕获型事件解决方案,其它多数浏览器则支持冒泡型事件解决方案,另外DOM事件流还支持文本节点事件冒泡。

●事件捕获到达顶层的目标在不同浏览器或不同浏览器版本也是有区别的。在IE6中HTML是接收事件冒泡的,另外大部分浏览器将冒泡延续到window对象,即……body→documen→window。

●阻止冒泡并不能阻止对象默认行为。比如submit按钮被点击后会提交表单数据,这种行为无须我们写程序定制。

5. js的严格模式和非严格模式

        答:js默认情况是非严格模式,如果需要使用严格模式,可以使用关键字:’use strict’,对于严格模式和非严格模式有如下区别:
        1) 严格模式下, delete运算符后跟随非法标识符(即delete 不存在的标识符),会抛出语法错误; 非严格模式下,会静默失败并返回false
        2)严格模式中,对象中定义同名属性会抛出语法错误; 非严格模式不会报错
        3)严格模式中,函数形参存在同名的,抛出错误; 非严格模式不会
        4)严格模式不允许八进制整数(如:023)
        5)严格模式中,arguments对象是传入函数内实参列表的静态副本;非严格模式下,arguments对象里的元素和对应的实参是指向同一个值的引用
        6)严格模式中 eval和arguments当做关键字,它们不能被赋值和用作变量声明
        7)严格模式会限制对调用栈的检测能力,访问arguments.callee.caller会抛出异常
        8)严格模式下变量必须先声明,直接给变量赋值,不会隐式创建全局变量,不能用with
        9)严格模式中 call、apply传入null、undefined保持原样不被转换为window

6. call,apply,bind的用法及区别
共同点和区别:

        1)apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;
        2)apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文(函数的每次调用都会拥有一个特殊值——本次调用的上下文(context)——这就是this关键字的值。);
        3)apply 、 call 、bind 三者都可以利用后续参数传参;
        4)bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用
        5)apply也可以有多个参数,但是不同的是,第二个参数必须是一个数组
        6)call的的多个参数需要依次罗列传入

7.js中的this对象理解

        答:一般情况下:this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁实际上this的最终指向的是那个调用它的对象。但是,在几个特殊的情况下this的指向会有所变化,具体如下:
        1)情况1:如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是window(非严格模式下)
        2)情况2:如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象。
        3)情况3:如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象

知识点补充:

        1)在严格版中的默认的this不再是window,而是undefined。
        2)new操作符会改变函数this的指向问题,虽然我们上面讲解过了,但是并没有深入的讨论这个问题,网上也很少说,所以在这里有必要说一下。

function fn(){
    this.num = 1;
}
var a = new fn();
console.log(a.num); //1

参考:js中this的指向的原理详解

8. js中arguments的理解

        答:其实Javascript并没有重载函数的功能,但是Arguments对象能够模拟重载。Javascrip中每个函数都会有一个Arguments对象实例arguments,它引用着函数的实参,可以用数组下标的方式”[]”引用arguments的元素。其中arguments.length为函数实参个数,arguments.callee引用函数自身。
        当然,对于arguments来说,其只是一个类数组对象,不能直接使用数组对象封装的内置方法,其转换为数组的方法为: Array.prototype.slice.call(arguments)

9. es6的箭头函数与function函数的区别

        答:两者的具体区别如下:
        1)箭头函数与function定义函数的写法

  function demo(){console.log('demo')}
  var demo=()=>{consoloe.log('demo')}

        2) this指向的区别:使用function定义的函数,this的指向随着调用环境的变化而变化的,而箭头函数中的this指向是固定不变的,一直指向的是定义函数的环境。(箭头函数本身没有this对象)
        3) 构造函数的区别:function是可以定义构造函数的,而箭头函数是不行的(箭头函数本身没有this对象)
        4)变量提升的区别:由于js的内存机制,function的级别最高,而用箭头函数定义函数的时候,需要var(let const定义的时候更不必说)关键词,而var所定义的变量不能得到变量提升,故箭头函数一定要定义于调用之前!

10. es6解构赋值

        对某种结构进行解析,然后将解析出来的值赋值给相应的变量,常见的有数组、对象、字符串、函数的解构赋值等
        1.数组解构赋值:1)模式匹配解构赋值,2)省略解构赋值,3)含剩余参数的解构赋值(省略号模式),4)set解构赋值:先执行new Set()进行数据去重 然后进行解构赋值,5)迭代器解构赋值
        2. 对象解构赋值:1)解构的属性名称必须保持一致,2)解构的属性名称可以直接使用冒号进行设置新的别名,设置为新的别名后,原属性名称不存在 只能进行别名调用
        3.字符串解构赋值
️:解构赋值时 只有当变量严格等于(===)的时候才会进行赋值为默认值 其他情况下均直接赋值为变量的值
        总结:解构成对象,只要等号右边的值不是对象或者数组,就先将其转换为对象。由于undefined和null无法转换为对象,因此直接对它们解构赋值会直接报错;解构成数组,等号右边必须为了迭代的对象

11. 列举几个es6新特性及用途

        1. let定义变量,const定义常量(如果常量为对象 对象的属性可以修改),作用域为块级作用域,无变量提升 必须先定义才可以使用
        2. 函数扩展:增加函数参数的默认值定义,箭头函数,箭头函数无this和arguments,但是其有refuse,其this指向箭头函数最近的一个函数,如果没有最近的函数则寻找到window对象
        3. 对象扩展:属性名为变量名时直接可以写属性名,Object.keys():获取对象的所有属性名和方法名组成一个数组,Object.assign():合并对象 有对象属性名相同时,后者覆盖前者;
        4. for of循环;
        5. import和export:export可以对外暴露一个module,import引入export暴露的module
        6. promise:异步解决方案,其包含三个状态,分别为:rejected:失败、resolveed:已完成、pending:进行中。promise构造函数中包括一个参数和一个带有resolve和reject的回调,对于实例化的promise可以通过promise.then来接受resolve和reject
        7. 解构赋值
        8. Set数据结构:一个类数组格式的数据结构,无重复值。size为长度,add()添加数据 返回Set本身,delete()删除一个数据 返还boolean值表示是否删除成功,has()查找某个元素 返回boolean是否找到,clear()清除所有的元素 无返回值
        9. 模板字符串,使用反引号进行模板字符串 内部可以使用${}来书写变量

12. setTimeOut和setInterval的用法和区别

        答:对于两者来说,其都是定义在一定的时间后进行相应函数的触发操作,但是二者也有明显的区别,具体如下:
        1)setTimeout(fn,t),超时调用,超过时间t,就执行fn;setInterval(fn,t),间歇调用,调用周期t,执行fn。
        2) 二者调用后,均返回一个数值ID,此ID是计划执行代码的唯一标识符,可以通过它来取消尚未执行的调用clearTimeout(id)是取消setTimeout的调用,clearInterval(id)是取消setInterval的调用。取消间歇调用的重要性要远远高于取消超时调用,因为在不加干涉的情况下,间歇调用将会一直执行到页面卸载。
        3) setTimeout(fn,t)中t告诉JS再过多久把当前任务添加到队列中。并不是执行的到setTimeout就添加任务。如果队列是空,那么添加的代码会立即执行;如果队列不空,那么它就要等前面的代码执行完了以后在执行。二对于setInterval(fn,t),在间歇调用时,会出现一些意外:

setInterval(function () {
    func(i++);
}, 100)

        a、如果函数的执行时间小于100毫秒的时候,那么下一个循环调用将在100毫秒前完成;
        b、当func的执行时间多于100毫秒,在触发下一个func函数时(200ms处),上一个func还没有执行完,那么此时第二个func会在队列(event loop)中等待,直到第一个函数执行完,第二个func立即执行
        c、当第一个函数的执行时间特别长,以致于在执行过程中出发了多个func时,就会导致第3个func及以后的函数被忽视。因为任务队列中不会有两个及以上同一个定时器的回调函数
        因此,如果需要每次函数在相同的间隔下完成指定的内容,则需要使用setTimeout,如果只是为了每隔一定范围的时间运行指定的内容,则使用setInterval
详情了解:setTimeout和setInterval的详解

13. 深度拷贝的用途及实现

        1、首先要理解js的数据类型,js基本类型主要包括String、Number、Boolean、Null、Undefined、Symbol:直接将值存在堆内存中,直接进行值赋值;引用类型包括:object、function、array,引用类型分为引用地址和具体值,引用地址存在堆内存中,具体值存在队内存中
        2、深拷贝和浅拷贝:常用的浅拷贝引用的方法:Object.assign()、Array.prototype.slice()、Array.prototype.concat();常用深拷贝:JSON.stringify()、JSON.parse()

具体实现js深度拷贝:
function deepCopy(obj) {
 if (obj instanceof Object) { 
            //当拷贝的是对象时
  let tmp =  {}
  for(let key in obj) {
   tmp[i] = deepCopy(key)
  }
  return tmp
 } else if (obj instanceof Array) { 
            //当拷贝的是数组时
  let tmp = []
  for (let i = 0;i < obj.length;i++) {
    tmp.push(deepCopy(obj[i]))
   }
   return tmp
 } else if (obj instanceof Function) {
          //当拷贝的是函数时
  return new Function('return '+ obj.toString())
 } else {
  return obj   //当拷贝的是基本数据类型
      }

参考地址:深度拷贝详解

14. 递归的用法及尾递归

        答:递归就是在函数体内重复的调用函数本身,达到循环计算的逻辑。但是,由于递归不断的调用函数本身,引用函数的时候会给函数引用开辟一个独立的内存空间直到因函数引用结束才会释放,因此待一定量后的递归就容易出现堆栈溢出。因此,为了优化递归的堆栈溢出,就提出了尾递归的概念:即:在函数的最后一步进行函数的引用,而不是在代码的最后一行进行函数的引用。具体原理如下:
        1. 堆栈:后进先出的原则。想象一下桌子上放一堆书,先放的在最底下,后放的在最顶部,拿书的时候,后放的被先拿走。即后进入堆栈的先出栈。
        2. 函数的堆栈概念:js中,每次函数调用会在内存形成一个“调用记录”, 保存着调用位置和内部变量等信息。如果函数中调用了其他函数,则js引擎将其运行过程暂停,去执行新调用的函数,新调用函数成功后继续返回当前函数的执行位置,继续往后执行。执行新调用的函数过程,也是将其推入到堆栈的最顶部并为其开辟一块内容。新函数执行完毕后将其推出堆栈,并回收内存。由此便形成了函数的堆栈。

15. js闭包的用法及注意事项

        答:闭包是实现可重用的局部变量,且保护其不受污染的机制。具体包括:
        1. 外层函数包裹受保护的变量和内层函数。
        2. 内层函数专门负责操作外层函数的局部变量。
        3. 将内层函数返回到外层函数外部,反复调用。

作用域

        子函数会一级一级地向上寻找所有父函数的变量。所以,父函数的所有变量,对子函数都是可见的,反之则不成立。

函数调用

        1. 外层函数调用了几次,就有几个受保护的局部变量副本。
        2. 同一次外层函数调用返回的多个内层函数,共同用一个局部变量。
        3. 闭包的局部变量不能释放。
        4. 尽量不要在定时器中使用闭包。因为闭包的外层函数定义的变量不能释放,但是定时器使用中需要释放

闭包的危害

        1)引用的变量可能发生变化

function outer() {
      var result = [];
      for (var i = 0; i<10; i++){
        result.[i] = function () {
            console.info(i)
        }
     }
     return result
}
function outer() {
      var result = [];
      for (var i = 0; i<10; i++){
        result.[i] = function (num) {
             return function() {
                   console.info(num);    // 此时访问的num,是上层函数执行环境的num,数组有10个函数对象,每个对象的执行环境下的number都不一样
             }
        }(i)
     }
     return result
}

        2)this的指向问题

var object = {
     name: ''object",
     getName: function() {
        return function() {
             console.info(this.name)
        }
    }
}
object.getName()()    // underfined
// 因为里面的闭包函数是在window作用域下执行的,也就是说,this指向windows

        3)内存泄漏

function  showId() {
    var el = document.getElementById("app")
    el.onclick = function(){
      aler(el.id)   // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
    }
}

// 改成下面
function  showId() {
    var el = document.getElementById("app")
    var id  = el.id
    el.onclick = function(){
      aler(id)   // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
    }
    el = null    // 主动释放el
}
16. js原型链

        1、所有的引用对象都有__proto__属性,其指向它的构造函数的prototype属性,所有的函数都有prototype属性
        2、当试图得到一个对象的属性时,如果对象本身不存在这个属性,那么就会去它的__proto__属性中寻找

17. js面向对象编程

        js的面向对象编程和大多数其他语言如Java、C#的面向对象编程都不太一样。面向对象的两个基本概念:
        1. 类:类是对象的类型模板,例如,定义Student类来表示学生,类本身是一种类型,Student表示学生类型,但不表示任何具体的某个学生;
        2. 实例:实例是根据类创建的对象,例如,根据Student类可以创建出多个实例,每个实例表示一个具体的学生,他们全都属于Student类型。
        JavaScript不区分类和实例的概念,而是通过原型(prototype)来实现面向对象编程

// 原型对象:
var Student = {
    name: 'Robot',
    height: 1.2,
    run: function () {
        console.log(this.name + ' is running...');
    }
};

function createStudent(name) {
    // 基于Student原型创建一个新对象:
    var s = Object.create(Student);
    // 初始化新对象:
    s.name = name;
    return s;
}

var xiaoming = createStudent('小明');
xiaoming.run(); // 小明 is running...
xiaoming.__proto__ === Student; // true
18. 操作数组的方法及用途

        答:操作数组的常用方法具体分为改变原数组、不改变原数组、遍历原数组等:

改变原数组的方法:

        push():向数组的尾部增加一个元素,并返回新的长度;
        pop():删除一个数组中的最后的一个元素,并且返回这个元素;
        shift():删除数组的第一个元素,并返回这个元素;
        unshift():向数组的开头添加一个或更多元素,并返回新的长度;
        reverse():颠倒数组中元素的顺序;
        splice():向/从数组中添加/删除项目,然后返回被删除的项目,第二个参数为0的时候表示不删除;
        sort():对数组元素进行排序,并返回这个数组;
        conpyWithin():在当前数组内部,将指定位置的成员复制到其他位置,并返回这个数组;
        fill():使用给定值,填充一个数组;

不改变原数组的方法:

        join():把数组中的所有元素通过指定的分隔符进行分隔放入一个字符串,返回生成的字符串;
        slice():返回一个从开始到结束(不包括结束)选择的数组的一部分浅拷贝到一个新数组对象,且原数组不会被修改;
        concat():方法用于合并两个或多个数组,返回一个新数组;
        indexOf():在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1;
        lastIndexOf():返回指定元素,在数组中的最后一个的索引,如果不存在则返回 -1。(从数组后面往前查找);
        includes():返回一个布尔值,表示某个数组是否包含给定的值;

遍历数组的方法:

        forEach():按升序为数组中含有效值的每一项执行一次回调函数(无法中途退出循环,只能用return退出本次回调,进行下一次回调。它总是返回 undefined值,即使你return了一个值。)
        every():方法用于检测数组所有元素是否都符合函数定义的条件(如果数组中检测到有一个元素不满足,则整个表达式返回 false,且剩余的元素不会再进行检测。如果所有元素都满足条件,则返回 true)
        some():数组中的是否有满足判断条件的元素(如果有一个元素满足条件,则表达式返回true, 剩余的元素不会再执行检测。如果没有满足条件的元素,则返回false)
        filter():返回一个新数组, 其包含通过所提供函数实现的测试的所有元素
        map():创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果
        reduce():对累加器和数组中的每个元素(从左到右)应用一个函数,最终合并为一个值(reduceRight()是从右到左的执行)
        find()& findIndex() 根据条件找到数组成员:find()定义:用于找出第一个符合条件的数组成员,并返回该成员,如果没有符合条件的成员,则返回undefined。findIndex()定义:返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。
        keys()&values()&entries() 遍历键名、遍历键值、遍历键名+键值:三个方法都返回一个新的 Array Iterator 对象,对象根据方法不同包含不同的值

详细介绍:js数组中常见的操作方法

19. 操作字符串的方法及用途

        split():按照指定的格式将字符串进行拆分返回一个新的数组,第二个参数可选,表示返回的数组的最大长度
        indexOf():返回字符串中一个子串第一处出现的索引(从左到右搜索)。如果没有匹配项,返回 -1
        lastIndexOf():返回字符串中一个子串最后一处出现的索引(从右到左搜索),如果没有匹配项,返回 -1
        match():直接通过字符串进行匹配,也可以通过正则进行匹配
        slice():可以为负数,如果起始位置为负数,则从字符串最后一位向前找对应位数并且向后取结束位置,如果为正整数则从前往后取起始位置到结束位置
        substring():只能非负整数,截取起始结束位置同slice()函数一致
        substr():从起始位置开始截取,结束位置为第二个参数截取的字符串最大长度
        trim():删除字符串前后的空格
        charAt()/charCodeAt():返回指定位置的字符或其字符编码值
        replace():替换对应的内容,可直接替换也可使用正则表达式替换

20. 双等于和三等于的区别

        答:双等于是非严格等于,只需要在值上相等即可为真;三等于是严格等于,必须在值和值的类型都一致的情况下才返回真

21. js的事件委托是什么

        答:事件委托就是利用了js的事件冒泡,通过事件冒泡来对多个事件进行委托触发。

事件委托的作用
  1. 支持为同一个DOM元素注册多个同类型事件
  2. 可将事件分成事件捕获和事件冒泡机制
事件委托的优点
  1. 提高性能:每一个函数都会占用内存空间,只需添加一个事件处理程序代理所有事件,所占用的内存空间更少。
  2. 动态监听:使用事件委托可以自动绑定动态添加的元素,即新增的节点不需要主动添加也可以一样具有和其他元素一样的事件。
22. javascript的事件流模型都有什么

        1、“事件冒泡”:事件开始由最具体的元素接受,然后逐级向上传播

        2、“事件捕捉”:事件由最不具体的节点先接收,然后逐级向下,一直到最具体的

        3、“DOM事件流”:三个阶段:事件捕捉,目标阶段,事件冒泡

23. null和undefined的区别

        1)null是一个表示”无”的对象,转为数值时为0;undefined是一个表示”无”的原始值,转为数值时为NaN。

        2)当声明的变量还未被初始化时,变量的默认值为undefined。 null用来表示尚未存在的对象

        3)undefined表示”缺少值”,就是此处应该有一个值,但是还没有定义。典型用法是:

        (1)变量被声明了,但没有赋值时,就等于undefined。

        (2)调用函数时,应该提供的参数没有提供,该参数等于undefined。

        (3)对象没有赋值的属性,该属性的值为undefined。

        (4)函数没有返回值时,默认返回undefined。

        4)null表示”没有对象”,即该处不应该有值。典型用法是:

        (1) 作为函数的参数,表示该函数的参数不是对象。

        (2) 作为对象原型链的终点。

24. new操作符具体干了什么

        1.创建一个空对象
        2.由this变量引用该对象
        3.该对象继承该函数的原型
        4.把属性和方法加入到this引用的对象中
        5.新创建的对象由this引用,最后隐式地返回this。
过程如下:

var obj={};
obj.__proto__=Base.prototype;
Base.call(obj);
25. js放在html中的位置

        html文件是自上而下的执行方式,但引入的css和javascript的顺序有所不同,css引入执行加载时,程序仍然往下执行,而执行到<script>脚本是则中断线程,待该script脚本执行结束之后程序才继续往下执行。
        所以,大部分网上讨论是将script脚本放在<body>之后,那样dom的生成就不会因为长时间执行script脚本而延迟阻塞,加快了页面的加载速度。

        但又不能将所有的script放在body之后,因为有一些页面的效果的实现,是需要预先动态的加载一些js脚本。所以这些脚本应该放在<body>之前。

        其次,不能将需要访问dom元素的js放在body之前,因为此时还没有开始生成dom,所以在body之前的访问dom元素的js会出错,或者无效

        script放置位置的原则“页面效果实现类的js应该放在body之前,动作,交互,事件驱动,需要访问dom属性的js都可以放在body之后

26. 同源策略

        答:同源策略限制了一个源(origin)中加载文本或脚本与来自其它源(origin)中资源的交互方式。同源指的是协议、域名、端口相同,同源策略是一种安全协议。

27. 函数声明和函数表达式申明

        答:在Javscript中,解析器在向执行环境中加载数据时,对函数声明和函数表达式并非是一视同仁的,解析器会率先读取函数声明,并使其在执行任何代码之前可用(可以访问),至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解析执行
        下一篇将进一步介绍面试过程中的深入内容:前端常见面试-进阶篇

作者:bilibili