8月更文挑战 | 前端Javascript面试题汇总(概念篇)

549 阅读28分钟

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战

最近在准备秋招面试,关注了不少前辈们和大佬们的公众号,也拜读了掘金里许多大佬的前端方面的文章,对我而言十分有用,我在下面把他们汇总下来,让自己能够再深入学习。

吃水不忘挖井人,再次感谢各位大佬,各位大佬的文章我会在下面一一做汇总

1.JS的数据类型以及存储方式

答: Js一共有7种原始数据类型以及1种引用数据类型

    //原始数据类型
    null
    undefined
    Boolean
    Number
    String
    Symbol(ES6新增)
    BigInt
    //引用数据类型
    Object
    /*
    Object包含:
    普通对象Object
    数组对象Array
    正则对象RegExp
    日期对象Date
    数学函数Math
    函数对象Function
    */

存储方式:原始数据类型存储在栈内存中,引用数据类型同时存储在栈内存堆内存中(引用类型指针存在栈中,所存入的指针指向堆内存中的数据的起始地址)

2.JS数据类型的判断

1.使用typeof判断

不足:原始数据类型null无法正确判断、引用数据类型除Function均检测为Object

    typeof 1 //'number'
    typeof '1' //'string'
    typeof undefined //'undefined'
    typeof true //'boolean'
    typeof Symbol() //'symbol'
    
    typeof [] //'object'
    typeof {} //'object'
    typeof console.log() //'function'

2.使用instanceof判断

不足:无法精确判断原始数据类型,只能够判断引用数据类型

原理:instanceof通过判断对象的原型链上是否能够查找到被判断类型的prototype

    1 instanceof Number //false
    true instanceof Boolean //false 
    'string' instanceof String //false  
    
    [] instanceof Array //true
    console.log() instanceof Function //true
    {} instanceof Object//true    

3.使用Object.prototype.toString.call()判断

    Object.prototype.toString.call(1) //Number
    Object.prototype.toString.call(true) //Boolean
    Object.prototype.toString.call('string') //String
    Object.prototype.toString.call(Symbol()) //Symbol
    Object.prototype.toString.call(null) //null
    Object.prototype.toString.call(undefined) //undefined
    Object.prototype.toString.call([]) //Array
    Object.prototype.toString.call({}) //Object
    Object.prototype.toString.call(console.log()) //Function

3.null与undefined的区别

nullundefined均为Javascript的基本数据类型,undefined表示为该变量应有值但并未定义,而null则表示不应该有值(阮一峰博客中的部分解答)

4.==与===的区别

===为严格运算符,==为相等运算符

1. 严格运算符比较规则

  • 不同类型相比较:类型不相同直接返回false
  • 相同类型(原始数据类型):值相等则返回true,不想等则返回false
  • 相同类型(引用数据类型):指向的是同一个对象则返回true,否则返回false

2.相等运算符比较规则

  • 原始类型比较:均将数据转换为数值类型后进行比较

  • 对象与原始类型比较:对象转换为原始数据类型的值后进行比较

  • 相等运算符中存在隐式转换

    其中值得一提的是nullundefined之间的比较(也可能出笔试题)

    null === undefined //false
    null == undefined //true

5.原型链

这里推荐一篇文章,简单易懂:《面不面试的,你都得懂原型和原型链》 ——尼克陈 ,顺便贴一张尼克陈大佬上述文章的一张图和一张经典的原型链图。顺带写下我个人的记忆方法(可能有误,我后面再仔细学习):对象的__proto__均指向上一级对象的.prototype,直到为null,对象的.prototype的构造函数constructor指向同级对象

image.png


image.png

6.Js的继承

ECMAScript只支持实现继承,主要依靠原型链来实现。Js的继承有原型链继承借用构造函数继承组合继承原型式继承寄生式继承寄生式组合继承六种。本文写出两种继承方式:组合继承以及寄生式组合继承

组合继承 (原型链继承+构造函数继承)

    //定义构造函数
    function Parent(age){
        this.age = age
    }
    //设置Parent方法
    Parent.prototype.sayAge = function(){
        console.log(this.age)
    }
    //定义Child并使用构造函数继承Parent的age属性
    function Child(age){
        Parent.call(this,age)
    }
    //通过原型链继承Parent的sayAge方法
    Child.prototype = new Parent()//此处调用了一次构造函数
    //创建Child实例
    const childInstance = new Child(18)
    console.log(childInstance.age)//18
    console.log(childInstance instanceof Parent)//true
  • 优点:父类方法可复用、构造函数可传参、继承的属性不共享
  • 缺点:多出了父类中不必要的属性,造成内存浪费,调用了两次构造函数

寄生组合继承

  • 寄生式组合继承对组合继承进行了优化,弥补了继承父类时调用了父类的构造函数这一缺点,避免了不必要的属性
  • 原理:在继承时,并没有继承父类的实例对象,从而没有调用父类的构造函数,而是继承了父类对象原型的实例对象,避免了继承时产生了不必要的属性
  • 方法:在修改原型
    //定义构造函数
    function Parent(age){
        this.age = age
    }
    //设置Parent方法
    Parent.prototype.sayAge = function(){
        console.log(this.age)
    }
    //定义Child并使用构造函数继承Parent的age属性
    function Child(age){
        Parent.call(this,age)
    }
    //修改原型链
    const prototype = object(Parent.prototype)
    prototype.constructor = Child
    Child.prototype = prototype
    //创建Child实例
    const childInstance = new Child(18)
    console.log(childInstance.age)//18
    console.log(childInstance instanceof Parent)//true

7.事件传播

当某个事件发生在DOM元素上时,该事件并不仅仅在那个元素上发生,而是由window开始一层一层往内递进触发,直到指定的元素。事件传播有以下三个阶段:

  • 捕获阶段:事件从window开始层层向内递进触发直到指定元素
  • 目标阶段:事件已到达目标元素
  • 冒泡阶段:事件从目标元素向外层冒泡,逐层递进直到window

8.addEventListener的useCapture参数

addEventListener方法第三个可选参数为useCapture,其值默认为false,当值为false时,监听事件将在事件冒泡阶段触发,值为true时,监听事件将在捕获阶段触发

9.事件捕获

当某个事件发生在DOM元素上时,该事件并不仅仅在那个元素上发生,而是由window开始一层一层往内递进触发,直到指定的元素。事件捕获阶段,发生事件有:windowdocumenthtmlbody指定元素

10.事件委托

事件委托本质上利用了事件冒泡的机制。事件在冒泡的过程中,会上传到父节点,同时父节点也可以通过事件对象定位到目标子节点,因此可以将子节点的监听事件定义在父节点之上,这样就可以由一个父节点监听父节点下所有子节点的事件而不需要为每一个子节点绑定监听事件,也称事件代理。事件代理减少了内存的消耗,还可以实现事件的动态绑定。

11.ES6的新特性

  • 块作用域
  • 模板字符串
  • 对象解构
  • 箭头函数
  • 模块
  • 加强的字面量对象
  • 函数默认参数
  • Promise
  • Symbol
  • set
  • proxy
  • rest

12.箭头函数特点

  • 没有自己的定义上下文
  • this永远指向外层的this而非箭头函数内
  • 无法作为构造函数(不能使用new
  • 没有arguments

13.Set、WeakSet、Map、WeakMap

1.Set

特点:
元素顺序元素重复性可存储类型
无序不可重复原始数据类型+引用数据类型

Set常用方法:

    let set = new Set() //创建Set
    set.add(0)
    set.add(1)
    set.add(2) //添加元素
    set.delete(2) //删除元素
    set.forEach(item=>{
        console.log(item) //1,2
    }) //遍历
    set.has(1) //检查集合是否包含某元素,true
    set.clear() //清空集合

此外,Set对象中的引用对象都已强类型化,且不能够自动回收无用的引用对象的,这样会造成内存浪费,且回收代价高,WeakSet解决了这一个问题

2.WeakSet

特点:
元素顺序元素重复性可存储类型
无序不可重复引用数据类型
  1. WeakSet只能够存储引用对象,否则会抛出TypeError错误
  2. WeakSet不能够包含无引用的对象(null),否则会将该对象自动进行回收(移出集合)
  3. WeakSet中存储的对象不可枚举,即无法获取WeakSet大小和其中的元素

相关代码:

    let weakset = new WeakSet()
    let foo = {}
    weakset.add(a)
    console.log(...weakset) //Exception is thrown
    console.log(weakset.size) //undefined
    weakset.clear() //没有这个方法
    //移出对象以及检查对象中元素是否存在的方法
    console.log(weakset.has(a)) //true
    foo = null //自动回收无引用的对象
    console.log(weakset.has(a)) //false

3.Map

特点:

Map类似于对象,也是键值对的集合,但不同的是,传统对象仅可以NumberStringSymbol类型值作为键名,而Map可以任何类型值作为键名

使用Map的优点:

  • 相同大小内存,Map存储量更大
  • 可通过size获取Map的长度

相关方法:

    //基础
    let map = new Map()
    map.set("tony",21)
    map.set("steve",22) //键值对添加
    console.log(map.get("tony")) //通过key查找value,21
    console.log(map.has("tony")) //通过key检查是否包含元素,true
    console.log(map.delete("steve")) //通过key删除元素,true
    console.log(map.has("steve")) //false
    //遍历方法
    let m = new Map([
        ["tony",21],
        ["steve",22]
    ])
    for(let key of m.keys()){
        //keys()获取Map所有key的遍历器
        console.log(key) 
        // "tony"
        // "steve"
    }
    for(let value of m.values()){
        //values()获取Map所有value的遍历器
        console.log(value)
        // 21
        // 22
    }
    //entries()获取Map所有键值对遍历器(两种写法:普通写法+解构写法)
    for(let item of m.entries()){
        //普通写法
        console.log(item)
        // ["tony",21]
        // ["steve",22]
    }
    for(let [key,value] of m.entries()){
        //解构写法
        console.log(key,value)
        // "tony" 21
        // "steve" 22
    }

4.WeakMap

特点:

WeakMap的特点

  • 只接收对象作为key(null除外)
  • key是弱引用,所指的value可被回收,回收后key是无效的,成员随时会消失
  • 不支持Map中获取遍历器的方法

14.Proxy

概念:Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

15.this指向

这里借用一张Jake大佬博文《由浅入深,66条JavaScript面试知识点》中的一张图

image.png

图片中表示地十分详尽、清晰:

  • 在全局作用域中,this指向window
  • 在函数中,this永远指向最后调用该函数的对象
  • 构造函数中,this指向被new出来的实例
  • call、apply、bind中,this指向被绑定的对象上
  • 箭头函数中的this永远指向父作用域中的this

16.new是怎么实现的

个人理解:new就是创建了一个空对象,将这一空对象的__proto__指向了构造函数的prototype即其原型构成一条原型链,之后借用构造函数继承对象的属性再赋值给指定的变量

new使用后,会进行以下几个步骤:

  1. 创建一个空对象Object{}
  2. 将该空对象链接到另一个对象(需要new的对象)(构造函数的链接)
  3. 将新创建的空对象作为this上下文
  4. 返回

这里贴上一段Jake大佬博文《由浅入深,66条JavaScript面试知识点》中的代码

    function Dog(name, color, age) {
      this.name = name;
      this.color = color;
      this.age = age;
    }

    Dog.prototype={
      getName: function() {
        return this.name
      }
    }

    var dog = new Dog('大黄', 'yellow', 3)

一部分一部分分析代码如下:

    //第一步:创建一个简单空对象
    var obj = {}
    //第二步:链接该对象到另一个对象(原型链)
    obj.__proto__ = Dog.prototype
    //第三步:将步骤1新创建的对象作为 `this` 的上下文,this指向obj对象
    Dog.apply(obj, ['大黄', 'yellow', 3])
    //第四步:如果该函数没有返回对象,则返回this
    
    // 因为 Dog() 没有返回值,所以返回obj
    var dog = obj
    dog.getName() // '大黄'

    //如果 Dog() 有 return 则返回 return的值
    var rtnObj = {}
    function Dog(name, color, age) {
      // ...
      //返回一个对象
      return rtnObj
    }

    var dog = new Dog('大黄', 'yellow', 3)
    console.log(dog === rtnObj) // true

17.作用域与作用域链

  • 作用域:定义变量的区域,它有一套访问变量的规则,这套规则管理着浏览器引擎在当前作用域或嵌套作用域内寻找变量
  • 作用域链:保证浏览器引擎在作用域内的有序访问,简单点说,就是内层的作用域找不到的变量,就向外寻找,就像一个链条
  • 作用域本质:实际上是一个指向变量的指针列表,变量对象包含了所执行环境中的所有变量以及函数,也就是说这个指针列表里所含的是每一个上下文、由内到外的变量对象,全局作用域的变量对象就是指针列表中的最后一个对象 当查找一个变量时,若在当前执行环境中没有查找到,则会沿着作用域链向后查找

18.var、let、const的区别

  • var存在变量提升而letconst不存在
  • letconst声明形成块作用域,作用域外找不到变量
  • 同一作用域下var能够重复声明变量,而letconst不能
  • const必须在声明时就进行赋值,若赋值的是基本数据类型,则不能修改,否则会报错,若复制的是引用数据类型对象),则不能修改const对象的指针,但可以修改对象中的属性

19.暂时性死区

在一个块作用域中,若一个变量使用letconst声明的情况下,则在只会在该块作用域内查找该变量,即便块作用域外有相同的变量,因此有以下代码:

var a = 100;
if(1){
    a = 10;
    //在当前块作用域中存在a使用let/const声明的情况下,给a赋值10时,只会在当前作用域找变量a,
    // 而这时,还未到声明时候,所以控制台Error:a is not defined
    let a = 1;
}

20.闭包

简单的一句话:闭包就是拥有权限能够读取其他函数作用域内部变量的函数

常用用途:

  • 创建私有变量
  • 将运行结束的上下文中变量保存在内存中

示例代码:

    //普通用法
    function a(){
        var b = 0;
        function c(){
            console.log(b)
        }
        return c
    }
    const testc = a()
    testc()
    //私有变量
    function person(name,age){
        var name = name
        this.age = age
        this.getName = function(){
            return name
        }
    }
    const tony = new person('tony',21)
    console.log(tony.name) //undefined
    console.log(tony.age) //21
    console.log(tony.getName())//tony

可以认为,闭包是在一个函数声明创建的时候就生成了的,你可以理解成游戏角色创建以后就有了一个背包,里面装的就是这个函数执行上下文所有变量的词法作用域。更详细的内容,可以参考 《medium 五万赞好文-《我永远不懂 JS 闭包》》——掘金安东尼

21.JS运行机制

Javascript单线程的,与其用途有关。Javacript主要用途是与用户进行互动和操作DOM,因此只能是单线程。例如,如果Javascript有两个线程,一个线程进行了DOM添加操作,另一个线程进行了DOM删除操作,则会造成矛盾。

22.宏任务与微任务

  • 宏任务:整体scriptsetTimeoutsetIntervalsetImmediate
  • 微任务:Promiseprocess.nextTickMutationObserver

23.事件循环

先说一下个人对事件循环的理解:在程序开始执行时,先进行宏任务(一般先是script)的执行,在该宏任务中,若发现同步任务,则立即执行,若发现异步任务,则进行挂载(宏任务则挂载到宏任务Event Queue上,微任务则挂载到微任务Event Queue上),完成这些步骤(宏任务的完成后)后,就算是进行了一轮事件循环,进行了这一轮事件循环后,就进入Event Queue查看是否有挂载的微任务,有则执行,没有则直接进入下一轮的事件循环,在Event Queue寻找宏任务继续进行执行代码、挂载宏任务微任务的工作,不断循环往复。

继续捋一下:宏任务→上一个宏任务挂载的微任务→上一个宏任务挂载的宏任务→不断循环

个人浅见,学习得可能不是很深刻,如果有错误,十分感激各位的斧正

下面贴一些代码:

    /*
    输出结果:
    1.script start
    2.script end
    3.promise1
    4.promise2
    5.setTimeout
    */
    console.log('script start');

    setTimeout(function() {
      console.log('setTimeout');
    }, 0);

    Promise.resolve().then(function() {
      console.log('promise1');
    }).then(function() {
      console.log('promise2');
    });

    console.log('script end');

24.Promise

  • 三种状态:pendingfulfilledrejected
  • 特点:状态一旦从pending转变为其他状态,则不可改变
  • 作用:解决回调地狱问题、以同步操作方式表达异步操作
  • 缺点:无法取消、无法获取进度

故事:Promise相当于一个承诺,这个承诺本身是一个异步的任务(比如两天后我请你吃饭),这就进入了pending状态,到了两天后,我们都有时间,就一起去吃了顿饭,吃完后,你说下周请我,进入了fulfilled状态,另外一种情况:领导临时说有新项目,我必须加班,没法请你了,根据这一个状态,我们又有了新的计划,打算下周再聚,进入rejected状态。

我的理解:在上一个故事中,这个约定就是一个Promise,我的承诺让这个Promise进入了pending状态,在履行承诺的时候,请客和加班分别让这个承诺进入了不同的状态(fulfilledrejected),一旦状态进入了fulfilledrejected,则这个Promise就定型了(resolve),无法改变了。根据resolve,我们就有了新的计划,就相当于使用了回调函数得到结果并进行了相应操作

再捋一遍:Promisependingfulfilled or rejected→返回fulfilled or rejected的回调函数→进行下一步操作

25.Promise.all与Promise.race

Promise.all

  • 应用:合并请求
  • 参数:Promise对象组成的数组
  • 特点:传入数组,多个Promise,全部执行成功则成功,返回成功回调函数,若其中有执行失败则算失败
  • 作用:并行执行多个异步操作,且在一个回调函数中处理所有结果
  • 理解:完成传入的所有Promise,全部成功返回成功的回调函数,若有失败则返回失败的回调函数
  • 以执行慢的Promise为主,最后一个Promise结束后才进行回调
    let Promise1 = new Promise(function(resolve, reject){})
    let Promise2 = new Promise(function(resolve, reject){})
    let Promise3 = new Promise(function(resolve, reject){})

    let p = Promise.all([Promise1, Promise2, Promise3])

    p.then(funciton(){
      // 三个都成功则成功  
    }, function(){
      // 只要有失败,则失败 
    })

Promise.race

  • 应用:请求超时处理
  • 参数:Promise对象组成的数组
  • 特点:传入数组,多个Promise,若其中某一个Promise率先改变状态,则Promise.race返回该Promise回调函数
  • 作用:解决超时问题
  • 以执行快的Promise为主,返回该Prmise的操作回调
    const p = Promise.race([
      fetch('/resource-that-may-take-a-while'),
      new Promise(function (resolve, reject) {
        setTimeout(() => reject(new Error('request timeout')), 5000)
      })
    ]);

    p
    .then(console.log)
    .catch(console.error);

Promise.race执行参数内两个异步操作,当超过5s后,定时器异步操作触发,p返回reject回调函数,提出请求超时错误,否则正常请求到数据

26.async与await

  • 优点:可读性更高,代码更加简洁
  • 特点:一个函数如果在前面加上async,则会返回一个Promise,若在async函数内写下return,则该函数直接返回Promise.resolve()
  • 注意:await只能在async函数下使用

下面贴出一段代码,分别用Promiseasync/await实现,来源:《由浅入深,66条JavaScript面试知识点》——JakeZhang

    //初始代码
    function takeLongTime(n) {
        return new Promise(resolve => {
            setTimeout(() => resolve(n + 200), n);
        });
    }

    function step1(n) {
        console.log(`step1 with ${n}`);
        return takeLongTime(n);
    }

    function step2(n) {
        console.log(`step2 with ${n}`);
        return takeLongTime(n);
    }

    function step3(n) {
        console.log(`step3 with ${n}`);
        return takeLongTime(n);
    }
    //Promise实现
    // step1 with 300
    // step2 with 500
    // step3 with 700
    // result is 900
    function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
        });
    }
    doIt();
    //async/await实现
    async function doIt() {
        console.time("doIt");
        const time1 = 300;
        const time2 = await step1(time1);
        const time3 = await step2(time2);
        const result = await step3(time3);
        console.log(`result is ${result}`);
    }
    doIt();

27.script标签中的async与defer

常见使用:

    <script src='xxx'></script>
    <script src='xxx' async></script>
    <script src='xxx' defer></script>
  • async:异步请求脚本,若请求到,则暂停HTML的解析,执行完获取到的Javascript代码后才继续解析HTML
  • defer: 异步请求脚本,若请求到,则先等待HTML解析完成,再执行获取到的Javascript代码

async的缺点:获取到脚本的时机是不确定的,获取到脚本时,部分DOM元素还未被解析,可能无法被脚本获取到。而且若有多个async,那么脚本的获取也是不可控的

更加详细的可以看:《图解 script 标签中的 async 和 defer 属性》——乔珂力

28.JS预编译

顺序:全局预编译函数体预编译

  1. 函数体内预编译
  • 创建一个AO对象(activation object
  • 寻找形参以及变量声明,将形参以及变量声明作为新建的AO对象的属性
  • 形参实参统一
  • 寻找函数声明,将函数名作为AO对象的属性,这一属性的值为函数体
  • 函数逐行执行(若某行代码存在变量提升则忽略,赋值不忽略)

步骤总结:创建空AO对象→添加变量声明形参AO对象→形参实参统一→函数声明添加至AO对象→逐行执行函数

    function fn(a) { 
      console.log(a); //function a() {}
      var a = 123; //赋值
      console.log(a); //123
      function a() {} //变量已提升,忽略
      console.log(a); //123
      var b = function() {} 
      console.log(b); //function() {} 
      function d() {} 
      var d = a //赋值
      console.log(d); //123
    }
    fn(1)
    /*
    解析:
    1.创建空AO对象
    AO:{
    }
    
    2.寻找函数内形参和变量声明
    AO:{
        a:undefined
        b:undefined
        d:undefined
    }
    
    3.形参实参相统一
    AO:{
        a:1
        b:undefined
        d:undefined
    }
    
    4.寻找函数声明,函数名作为AO属性名,函数体为AO属性值
    AO:{
        a:function(){}
        b:function(){}
        d:function(){}
    }
    
    5.使用AO对象,根据函数内代码一步步执行(变量提升部分忽略,赋值部分不能忽略)
    */
  1. 全局预编译
  • 创建GO对象(global object
  • 变量声明,将变量声明作为GO对象的属性名,值赋予undefined
  • 全局里函数声明,将函数名作为GO对象的属性名函数体作为该属性

步骤总结:创建空GO对象→添加变量声明``GO对象→函数声明添加至GO对象→逐行执行函数(先全局函数体

    1:global = 100 //赋值操作,不是声明
    2:function fn() {
    3:   console.log(global); // undefined
    4:   global = 200
    5:   console.log(global); // 200
    6:   var global = 300
    7:}
    8:fn()
    /*
    解析:
    一、全局预编译
    
    1.创建空GO对象
    GO:{}
    
    2.寻找变量声明,而全局内没有变量声明,仅有函数声明及一个global变量赋值,故GO为空
    GO:{}
    
    3.寻找函数声明,有fn函数声明,写入GO对象中
    GO:{ 
       fn:function fn() {
              console.log(global); // undefined
              global = 200
              console.log(global); // 200
              var global = 300
          }
    }
    到此处全局预编译完成,开始函数体内预编译
    
    二、函数体内预编译
    1.创建空AO对象
    AO:{}
    
    2.寻找变量声明及形参,写入AO对象
    AO{
        global:undefined
    }
    
    3.形参与实参相统一,此处无形参,AO不变
    AO{
        global:undefined
    }
    
    4.寻找函数体内的函数声明,此内无函数声明,AO不变
    AO{
        global:undefined
    }
    
    三、代码执行:
    
    1.第一行,global变量赋值,而GO对象中没有global,因此会声明一个global变量赋值为100并写入GO对象中
    2.第八行,执行函数fn,进入函数体内执行
    3.第三行,global在AO对象中为undefined,故输出undefined
    4.第四行,global赋值为200
    5.第五行,输出global为200
    6.第六行,赋值global为300(第3、4、5、6步均是在AO对象中的处理)
    */

29.AMD、CMD、Module与CommonJs的区别

AMDCMD的区别:

  • AMD推崇依赖前置,提前执行,即define方法中传入的依赖模块会在一开始就下载并执行(定义模块依赖时就进行声明)
  • CMD推崇依赖就近,延迟执行,即在require时依赖模块才执行(按需)
  • 常见AMD规范:RequireJs
  • 常见CMD规范:SeaJs

下面贴出一段代码,来源:《由浅入深,66条JavaScript面试知识点》——JakeZhang

    // CMD
    define(function(require, exports, module) {
      var a = require("./a");
      a.doSomething();
      // 此处略去 100 行
      var b = require("./b"); // 依赖可以就近书写
      b.doSomething();
      // ...
    });

    // AMD 默认推荐
    define(["./a", "./b"], function(a, b) {
      // 依赖必须一开始就写好
      a.doSomething();
      // 此处略去 100 行
      b.doSomething();
      // ...
    });

CommonJs和ES6Module的区别:

与依赖的关系:

  • CommonJs模块与依赖关系的建立发生在代码运行阶段(动态)
  • Module模块与依赖关系的建立发生在代码编译阶段(静态)

本质与特点:

  • CommonJs输出的是一个值的拷贝(输出则不再受影响),而ES6中Module输出的是值的引用(即变量的绑定)
  • CommonJs仅能导出单个模块,而Module能导出多个

运行机制:

  • ES6中ModuleCommonJs不同,Js引擎在对脚本进行静态分析时,若有import,则生成一个只读引用,到了脚本真正执行时才根据只读引用加载模块并取得值(import仅能写在顶层,而CommonJs无限制)
  • CommonJs机制是运行时加载CommonJs模块就是对象,即在加载模块后,生成一个对象,随后从这个对象上读取方法,这种机制就成为运行时加载

30.Js垃圾回收机制

Js垃圾回收两个策略:

  • 标记回收
  • 引用计数

标记回收

  • 标记回收是浏览器最常见的垃圾回收机制。当变量进入了执行环境,则该变量就标记为进入环境,离开了执行环节则标记为离开环境,被标记为离开环境的变量会被内存释放
  • 垃圾收集器(GC)在运行时会给存储在内存中的所有变量加上标记。随后,它会去掉正在执行环境中的变量的标记,随后将有标记的变量进行回收,垃圾收集器完成内存清理工作,销毁带标记的变量并回收它们所占用的内存

引用计数

  • 引用计数就是记录每个值被引用的次数,当声明了一个变量并将一个引用类型值赋值给该变量时,则该值引用次数1。相反,若该变量被赋值为另一个值,则该值引用次数1,当该值引用次数0时,说明这个值已经没有使用价值,在回收时,这个值所占的内存就会被释放
  • 存在的问题:可能会造成循环引用。例如:如下代码中,obj1obj2通过属性相互引用,当执行完成,obj1obj2离开了作用域,但是obj1obj2引用次数并不为0,因此不会被自动回收,导致循环引用(每运行一次函数则加1),内存需要手动回收。
    //每调用一次,引用次数加1,永远不为0
    function fun() {
        let obj1 = {};
        let obj2 = {};
        obj1.a = obj2; // obj1 引用 obj2
        obj2.a = obj1; // obj2 引用 obj1
    }
    //手动回收内存
    obj1.a =  null
    obj2.a =  null

减少垃圾回收的方法

当代码比较复杂时,垃圾回收代价就会比较大,因此应尽量减少垃圾回收,方法如下:

  • 数组进行优化:手动设置数组长度为0
  • object进行优化:object尽量复用,若不再使用,则赋值为null,使其尽快回收
  • 函数进行优化:若循环体内的函数可复用,则尽量放在循环体外

31.内存泄漏

造成内存泄漏的原因:

  • 意外的全局变量:使用了未声明的变量,意外地创建了一个全局变量
  • 被遗忘的计时器(忘记取消setInterval)或回调函数(对外部变量有引用)无法被回收
  • 脱离了DOM的引用:获取一个DOM元素的引用,而后面这个元素被删除,但一直保留了对这个元素的引用,导致无法被回收
  • 不合理的闭包

32.IIFE(立即调用函数表达式)

用法:函数被创建后立即执行

    (function IIFE(){
     console.log( "Hello!" );
    })();    

    // "Hello!"

33. for..in 和 for...of 的区别

对象遍历:

  • for...of获取对象键值,for...in获取对象键名 对数组遍历:
  • for...of获取数组元素,for...in获取数组索引 区别:
  • for...in循环主要是为了遍历对象,不适用于遍历数组for...of循环可以用来遍历数组类数组对象字符串SetMap 以及Generator对象。

34.XSS和CSRF

XSS(Cross Site Scripting),也称跨站脚本攻击,是一种代码注入攻击

  • 原理:攻击者将恶意脚本代码植入到页面中,使用该页面的用户运行了该脚本,攻击者利用该脚本获取用户的Cookie等敏感信息,危害数据安全
  • 分类:存储型(持久型,恶意代码在数据库中)、反射型(非持久型,恶意代码存在URL中)、DOM型

对XSS的防范:

  • httpOnly:在Cookie中设置httpOnly属性,让Javascript脚本无法获取Cookie信息
  • 输入过滤:检查输入的格式,避免Javascript相关脚本代码的输入与上传
  • 对输入、输出结果进行必要的转义

CSRF(Cross-site request forgery),也称跨站点请求伪造

这里先贴出一张图,来源:《前端面试查漏补缺--(七) XSS攻击与CSRF攻击》——shotCat

image.png

  • 原理:攻击者利用危险网站使用被攻击者的Cookie伪造成被用户进行危险操作
  • 类型:GET类型、POST类型、链接类型

下面贴出一段代码,来源同上:

    //GET类型CSRF
    <img src="http://bank.example/withdraw?amount=10000&for=hacker" > 
    //在受害者访问含有这个img的页面后,浏览器会自动向`http://bank.example/withdraw?account=xiaoming&amount=10000&for=hacker`发出一次HTTP请求。bank.example就会收到包含受害者登录信息的一次跨域请求。
    
    //POST类型CSRF
    <form action="http://bank.example/withdraw" method=POST>
        <input type="hidden" name="account" value="xiaoming" />
        <input type="hidden" name="amount" value="10000" />
        <input type="hidden" name="for" value="hacker" />
    </form>
    <script> document.forms[0].submit(); </script> 
    //访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST操作。POST类型的攻击通常比GET要求更加严格一点,但仍并不复杂。任何个人网站、博客,被黑客上传页面的网站都有可能是发起攻击的来源,后端接口不能将安全寄托在仅允许POST上面。
    
    //链接类型CSRF
    <a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">
    重磅消息!!
    <a/>
    //用户点击后触发请求

特点:

  • 攻击一般在第三方网站发起
  • 攻击者利用用户的登录凭证冒充用户进行操作,而不是直接窃取数据
  • 整个过程中攻击者无法获取用户的Cookie数据,仅仅是冒用
  • 攻击通常是跨域的

对CSRF的防范:

  • 使用验证码:防止请求直接执行
  • Referer检查:请求来源不符合要求则不执行请求
  • 使用token令牌:在请求时验证token是否正确

XSS与CSRF的区别

  • XSS是注入代码实现攻击、CSRF是利用用户Cookie非法请求Api实现非法操作
  • XSS攻击不需要用户登录、CSRF需要用户登录后才能攻击

35.DOM节点的增删改查

    //查找
    document.getElementById(id)
    document.getElementByName(name)
    document.getElementByClassName(className)
    document.getElementByTagName(tagName)
    //创建
    document.createElement("h1")
    document.createTextNode(String)//创建文本节点
    document.createAttribute("class")//创建属性节点
    //删除
    element.removeChild(node)

36.如何删除一个DOM

看看代码,来源:CSDN

<body>
    <div id="main">
        <div id="box">111</div>
        <div class="box2">222</div>
    </div>
    <script type="text/javascript">
        var box = document.getElementById("box");
        var main = document.getElementById("main");
        var newMask = document.createElement("div");
        newMask.id ="newMask";
        main.appendChild(newMask); 
        if(box){
            box.parentNode.removeChild(box); //在这
        }
        else{
            alert("没有这个div");
        }
    </script>
</body>

37.forEach、map、find与filter

  • forEach没有实质性的返回,一般是直接操作原数组,而mapfilter会分配内存空间存储新的数组并返回,对原数组没有影响,find对原数组同样没有影响,返回一个回调函数
  • forEach用法:常用于展示(只能用于遍历数组,不返回值)
  • map用法:让数组按照某一个函数对数组中每个元素进行相应操作重新建立数组返回(必须要有返回值)
  • filter用法:让数组按照某一个规则对数组中每个元素进行筛选重新建立数组返回

回调函数:callback(arg1arg2arg3)

  • arg1:遍历得到的元素
  • arg2:遍历得到的元素索引
  • arg3:遍历的数组

代码如下,来源《JavaScript数组中一些实用的方法(forEach,map,filter,find)》——itclanCoder

//forEach
var obj = {
    "data":{
        "members": [
        {"id":111,"name":"小高"},
        {"id":222,"name":"小凡"},
        {"id":333,"name":"小王"}
        ]
    }
}
var newArrs= [];
obj.data.members.forEach(function(member,index,originArrs){
newArrs.push(member.name);
})
console.log(newArrs); //["小高", "小凡", "小王"]

//map
var numbersA = [1,2,3,4,5,6];
var numbersB = []
var numbersC = numbersA.map(function(numberA,index,originArrs){
   return numbersB.push(numberA*2);
}
console.log(numbersA); // [1,2,3,4,5,6]
console.log(numbersB);// [2, 4, 6, 8, 10, 12]
console.log(numbersC);// [1, 2, 3, 4, 5, 6]
console.log(numbersC==numbersA)  // false

//filter
var persons = [
{name:"小王",type:"boy",city:"广西",age:15,height:170},
{name:"小美",type:"girl",city:"北京",age:16,height:180},
{name:"小高",type:"girl",city:"湖南",age:18,height:175},
{name:"小刘",type:"boy",city:"河北",age:20,height:177}
]
var filterPersons = persons.filter(function(person,index,arrs){
    return person.type === "boy";
})
console.log(filterPersons) // 会过滤筛选出类型type为boy的整个对象,然后塞到一个新的数组当中去

参考来源(部分已在上文提到,不分先后)

  1. 《「2021」高频前端面试题汇总之JavaScript篇(上)》——CUGGZ
  2. 《「2021」高频前端面试题汇总之JavaScript篇(下)》——CUGGZ
  3. 《javascript篇--1.6万字带你回忆那些遗忘的JS知识点》——别催我码得慢
  4. 《由浅入深,66条JavaScript面试知识点》—JakeZhang
  5. 《# 26个精选的JavaScript面试问题》——Fundebug
  6. 后续可能会继续进行补充

最后:

概念篇我就暂时写到这里,这些题目都是我在这段时间内收集了当前部分公司提前批、秋招的一些前端Js面试题,往后如果遇到了没有写到的题目,我会继续在这里更新,如果各位大佬发现我写的有错误,敬请斧正,万分感激!最后我贴出这段时间里所参考的文章来源,十分感谢在整理过程中各位大佬对我的帮助,我也十分有收获,感谢大家的阅读o(^▽^)o,共勉。

——(完)