「2021」高频前端面试题汇总笔记(其一)

339 阅读8分钟

近期整理了一下高频的前端面试题,分享给大家一起来学习。如有问题,欢迎指正!

事件循环

微任务: process.nextTickpromiseMutation Observer
宏任务scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering
队列顺序:自上而下执行→微任务→宏任务

自测题

async function async1() {
    console.log('async1 start');
    await async2();                
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}
console.log('script start');
setTimeout(function() {
    console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');
▶点击查看答案
/**
 * scripr start  
 * async1 start  
 * async2  
 * promise1  
 * scroptend  
 * async1end  
 * promise2  
 * setimeout  
 */  

事件捕获&冒泡

直接上图:

1.jpg
1-5是捕获过程,5-6是目标阶段,6-10是冒泡阶段

e.stopPropagation()//取消事件冒泡
e.preventDefault()//取消默认事件

事件委托

e.g:一个宿舍的同学同时快递到了,一种方法就是他们都傻傻地一个个去领取,还有一种方法就是把这件事情委托给宿舍长,让一个人出去拿好所有快递,然后再根据收件人一一分发给每个宿舍同学。 封装代码

function eventDelegate (parentSelector, targetSelector, events, foo) {
                            //父级            后代     事件类型  事件
  // 触发执行的函数
  function triFunction (e) {
    // 兼容性处理
    var event = e || window.event;
    var target = event.target || event.srcElement;
    // 处理 matches 的兼容性
    if (!Element.prototype.matches) {
      Element.prototype.matches =
        Element.prototype.matchesSelector ||
        Element.prototype.mozMatchesSelector ||
        Element.prototype.msMatchesSelector ||
        Element.prototype.oMatchesSelector ||
        Element.prototype.webkitMatchesSelector ||
        function(s) {
          var matches = (this.document || this.ownerDocument).querySelectorAll(s),
            i = matches.length;
          while (--i >= 0 && matches.item(i) !== this) {}
          return i > -1;            
        };
    }
    // 判断是否匹配到我们所需要的元素上
    if (target.matches(targetSelector)) {
      // 执行绑定的函数,注意 this
      foo.call(target, Array.prototype.slice.call(arguments));
    }
  }
  // 如果有多个事件的话需要全部一一绑定事件
  events.split('.').forEach(function (evt) {
    // 多个父层元素的话也需要一一绑定
    Array.prototype.slice.call(document.querySelectorAll(parentSelector)).forEach(function ($p) {
      $p.addEventListener(evt, triFunction);
    });
  });
}

强引用&弱引用

强引用 就是一个小孩A牵着一条狗,他们之间通过狗链儿连着。
弱引用 就是,旁边有个小孩B指着A牵的狗,说:嘿,那有条狗,B指向那条狗,但他们之间没有是指绑在一起的东西。
当A放开狗链,狗就会跑掉(被垃圾回收),无论B是不是还指着。
但是,当B不再指着那条狗,狗还被A牵着,不会影响它是否跑掉。
强引用:在引用结束后必须解除引用关系,否则会导致内存泄露!!!

    let e1 = document.getElementById('foo')
    let e2 = document.getElementById('bar')
    const arr = [
      [e1,'foo']
      [e2,'bar']
    ]
    //手动删除引用
    arr[0] = null
    arr[1] = null

弱引用:一旦被垃圾回收器检测到,就会被回收。

const myMap = new WeakMap()
let my = {
    name: "ljc",
    sex: "male"
}
myMap.set(my, 'info');

内存泄露

js的垃圾回收机制是根据引用计数来实现的。会自动把一个函数运行之后内部的变量给销毁掉,而产生闭包之后其外部函数中的局部变量的引用就变成了循环引用,则不会被垃圾回收机制捕获并销毁,所以这就造成了内存泄漏,除非人工把该闭包函数=null将其引用解除并内存释放,此部分将一直留存在runtime的内存中。

深拷贝&浅拷贝

深拷贝:拷贝了目标对象全属性并在堆栈中申请新的空间来存储。
浅拷贝:只是拷贝了目标对象一层属性并在堆栈中申请新的空间来存储。
直接赋值:只是拷贝了对象的指针,和原对象指向同一储存空间。 浅拷贝

  1. Object.assign({},Obj)
  2. slice()
  3. concat()
  4. {...Obj}

深拷贝

  1. JSON.parse(JSON.stringify(Obj))
  2. jquery$.extend(true, {}, obj1)
  3. lodash.cloneDeep()实现深拷贝:
let ash = require('lodash');
let obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
let obj2 = ash.cloneDeep(obj1);
  1. 封装函数实现
module.exports = function clone(target) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        for (const key in target) {
            cloneTarget[key] = clone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

JavaScript 数组遍历方法的对比

1.png 效率for>for of>filter==forEach>map>for in
事实上,whilefor执行效率更高

Ajax

封装函数

    // 封装自己的ajax函数
    /* 参数1:{string} method 请求方法
       参数2:{string} url 请求地址
       参数2:{Object} params 请求参数
       参数3:{function} done 请求完成后执行的回调函数
    */
   function ajax(method,url,params,done){
        // 创建xhr对象,兼容写法
        var xhr = window.XMLHttpRequest 
        ? new XMLHttpRequest()
        : new ActiveXObject("Microsoft.XMLHTTP");

        // 将method转换成大写
        method = method.toUpperCase();
        // 参数拼接
        var pair = [];
        for(var k in params){
            pair.push(k + "=" + params[k]);
        }
        var str = pair.join("&");
        // 判断请求方法
        if(method === "GET"){
            // 字符串拼接 或者 模板字符串
            url += "?" + str;
        }
        xhr.open(method,url);

        var data = null;
        if(method === "POST"){
            // 需要请求头
            xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
            data = str;
        }
        xhr.send(data);

        // 指定xhr状态变化事件处理函数
        // 执行回调函数
        xhr.onreadystatechange = function (){
            if(this.readyState === 4){
                // 返回的应该是一个对象,这样客户端更好渲染
                done(JSON.parse(xhr.responseText));
            }
        }
   }

//    调用自己写的ajax函数
ajax("get","http://localhost:3000/users",{
    name:"zs",
    age:45
},function (a){
    console.log(a);
});

DOM&BOM

DOM

文件对象模型,浏览器把拿到的html代码,结构化一个浏览器能识别并且js可操作的一个模型。 BOM 浏览器对象模型,为了控制浏览器的行为而出现的接口。 参考文章: juejin.cn/post/692429…
juejin.cn/post/699133…

Promise&async的区别

  • 使用了async的函数始终隐式返回一个promise对象

  • await需要搭配async函数使用

  • await需要跟在promise前面使用,将会使promise返回resolve的参数

  • 使用async,await包裹Promise后,异步的过程就变成同步了

  • 使用场景:在几个Promise之间有前后的依赖关系的情况下可以使用async,await

  • 被await的Promise可以在包裹其的async函数后.catch()来处理错误

async/await 是 Promises的语法糖

语法糖

通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。

JSONP

跨域的一种民间解决方案
参考文章:juejin.cn/post/684490…

数据类型检测

基础数据类型UndefinedNullBooleanNumberStringObjectSymbolBigInt
引用数据类型:对象、数组和函数。 (1)typeof:数组、对象、null都会被判断为object,其他判断都正确。
(2)instanceofinstanceof只能正确判断引用数据类型
(3) constructorconstructor有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor 对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了:

function Fn(){};
 
Fn.prototype = new Array();
 
var f = new Fn();
 
console.log(f.constructor===Fn);    // false
console.log(f.constructor===Array); // true

(4)Object.prototype.toString.call()Object.prototype.toString.call() 使用 Object 对象的原型方法 toString 来判断数据类型。

Vue 和 React的区别

  • 原理上讲:

Vue的数据绑定依赖数据劫持Object.defineProperty()中的gettersetter以及发布订阅模式(eventEmitter)来监听值的变化,从而让virtual DOM驱动Model层View层的更新,利用v-model这一语法糖能够轻易地实现双向的数据绑定。这种模式被称为MVVM: M <=> VM <=> V。但其实质还是State->View->Actions的单向数据流,只是使用了v-model就不需要显式地编写从视图层Model层更新罢了。

React则需要依赖onChange()/setState()模式来实现数据的双向绑定,因为它在诞生之初就是被设计成单向的数据流

  • 组件通信的区别:

父子之间 两者都可以通过props绑定datastate来进行传值,又或者通过绑定回调函数来传值。

兄弟组件之间 都可以通过使用发布订阅模式写一个EventBus来监听值的变化。

跨层级:React中可以使用React.context来进行跨层级的通信。Vue中可以使用V2.2.0新增的provide/inject来实现跨层级注入数据。

  • 模板渲染的方式区别:

React在JSX中使用原生的JS语法来实现插值,条件渲染,循环渲染等等。

而Vue则需要依赖指令来进行,更加容易上手但是封装的程度更高,调试成本更大,难以定位Bug。

  • 性能差异

React中组件的更新渲染是从数据发生变化的根组件开始往子组件逐层重新渲染,而组件的生命周期有shouldComponentUpdate()函数供给开发者优化组件在不需要更新的时候返回false

而在Vue中是通过watcher监听到数据的变化之后通过自己的diff算法,在virtualDom中直接找到以最低成本更新视图的方式。

  • 所以总的来说,Vue更适合开发周期更短的相对小型的项目,React更适合构建稳定大型的应用,可定制化的能力更强。

跨域

  • JSONP

  • CORS

  • 利用NGINX反向代理转发请求

  • document.domain + iframe 跨域

  • location.hash + iframe 跨域

  • window.name + iframe 跨域

  • postMessage

  • nodejs作为中间件代理跨域

  • WebSocket协议跨域

Link 和 @import 的区别

  • link是从html引入的,import是从css引入的
  • link会在浏览器加载页面时同步加载css;页面加载完成后再加载import的css
  • 优先级link > import
  • import是CSS2.1加入的语法,只有IE5+才可识别,Link无兼容性问题
  • 权重:!important > 行内样式 > ID > 类、伪类、属性 > 标签名 > 继承 > 通配符

闭包

js的变量作用域有全局变量和局部变量。
函数内部可以直接读取全局变量。
函数外部无法读取到函数内部的局部变量,因为函数在执行完之后,函数内部的环境就被销毁了。
如果函数内部没有使用var声明一个变量则实际上是声明为全局变量。
而闭包就是能够读取外部函数内部变量的函数,并且最终外部函数return这个内部函数,这时就生成了闭包。

原型链

访问一个对象的属性,如果对象的内部不存在这个属性则会访问其__proto__的属性,如果还是找不到就再继续访问它的__proto__的属性,知道null为止。 613542483e83cdd3fd267de74556ba1a.jpeg 613542483e83cdd3fd267de74556ba1a.jpeg

箭头函数

this指向在定义时已被确定,指向函数定义上下文。 事实上,这是函数特性所决定的,函数没有的属性会向其上层查找

i = 1
let a = {
  i: 2,
  b: function() {
    this.i = 3
    let c = ()=>{
      i = 4
      console.log(this.i);
    }
    return c
  }
}
a.b()()
▶点击查看答案
 3  
i = 1
let a = {
  i: 2,
  b: function() {
    let i = 3
    let c = ()=>{
      i = 4
      console.log(this.i);
    }
    return c
  }
}
a.b()()
▶点击查看答案
 2