前端常见面试题

145 阅读14分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 13 天

一、JS中几种常见的继承方法

1、原型链继承

优点:父类方法可以复用

缺点:1.父类所有的引用类型数据(对象,数组)会被子类共享,更改一个子类的数据,其他数据会受到影响,一直变化

2.子类实例不能给父类构造函数传参

例:

2、构造函数的继承

优点:父类的引用类型数据不会被子类贡献,不会互相影响

缺点:子类不能访问父类原型属性上的方法和参数(.prototype出来的东西都不能访问)

 function Person(){
  this.name = '小明'
  this.eats = ['苹果']
  this.getName = function(){
  console.log(this.name)}
}
Person.prototype.get = ()=>{
console.log("Person.prototype上的方法")
}


 function Student(){
  person.call(this)
}

3、组合继承

优点:

1.父类可以复用

2.父类构造函数中的引用属性数据不会被共享

缺点:

会调用两次父类的构造函数,会有两份一样的属性和方法,会影响性能

 function Person(){
  this.name = '小明'
  this.eats = ['苹果']
  this.getName = function(){
  console.log(this.name)}
}
Person.prototype.get = ()=>{
console.log("Person.prototype上的方法")
}

 function Student(){
  person.call(this)
}

student.prototye = new Person();

4、寄生组合继承

目前最优的方案,解决了组合继承有两份一样的属性和方法的问题

 function Person(){
  this.name = '小明'
  this.eats = ['苹果']
  this.getName = function(){
  console.log(this.name);
  }
}
Person.prototype.get = ()=>{
console.log("Person.prototype上的方法");
}

 function Student(){
  person.call(this)
}
const Fn = function(){};
Fn.prototye = new Person();
student.prototye = new Fn();

5、class继承

最优方案,代码简介且性能满足所有要求

class Person{
  constructor(){
    this.name = '小明'
    this.eats =["苹果"] 
    this.getName = function(){
      console.log(this.name);
  }
  }
 get = ()=>{
  console.log("Person.prototype上的方法");
}
}
class Student extends Person{}

二、二叉树

构造二叉树

class Node {
  constructor(key) {
    this.key = key;
    this.left = null;
    this.right = null;

  }
}
export class BinarySearchTree {
  constructor() {
    this.root = null;
  }

插入节点

  //插入节点
  insert(key) {
    const newNode = new Node(key);
    if (this.root === null)//如果插入的是根节点
    {
      this.root = newNode;
    } else {
      this.inserNode(this.root, newNode)
    }

  }
  //使用递归插入非根节点
  inserNode(Node, newNode) {
    if (Node.key > newNode.key) {//key大插右边,key小插左边
      if (Node.left === null) {
        Node.left = newNode
      } else {
        this.inserNode(Node.left, newNode)
      }
    } else {
      if (Node.right === null) {
        Node.right = newNode
      } else {
        this.inserNode(Node.right, newNode)
      }
    }
  }

先序遍历

先访问根节点,再先序遍历其左子树,然后先序遍历其右子树

  preOrderTravel() {
    this.preOrderTravelNode(this.root)
  }

  preOrderTravelNode(node) {
    if (node === null) return;
    console.log(node.key);//直接访问
    this.preOrderTravelNode(node.left)
    this.preOrderTravelNode(node.right)

  }

中序遍历

ES5版本代码(一般用ES6不用ES5)

  //中序遍历:先访问左子树,再访问跟节点,最后访问右子树
  inOrderTravel() {
    this.inOrderTravelNode(this.root)
  }

  inOrderTravelNode(node) {
    if (node === null) return;
    this.inOrderTravelNode(node.left)
    console.log(node.key);//中间访问
    this.inOrderTravelNode(node.right)

  }

和先序遍历代码类似,换个顺序而已

PS:中序遍历的特点是可以将按key值由大到小进行遍历

后序遍历

  //后序遍历:先访问左子树,再访问右子树,最后再访问根节点
  postOrderTravel() {
    this.postOrderTravelNode(this.root)
  }

  postOrderTravelNode(node) {
    if (node === null) return;
    this.postOrderTravelNode(node.left)
    this.postOrderTravelNode(node.right)
    console.log(node.key);

  }

浏览器的同源策略

同源策略:要求一个服务器上的网页,只能请求这个服务器上的资源;同源政策是为了保证用户信息的安全,防止恶意的网站窃取数据。最初的同源政策是指A网站在客户端设置的Cookie,B网站是不能访问的。

同源策略就是指必须在同一个协议,域名,端口号下,而且三者必须一致的。

解决跨域问题的方案

1、CORS

服务器设置允许跨域访问的请求头

Access-Control-Allow-Origin: http://a.com

后面也可以写成*,代表允许所有源访问

2、使用jsonp方案

JSONP就是利用script标签的跨域能力来发送请求的。

举个例子:比如引入axios的script标签,引入的是一个远程资源

2.1jsonp的使用

1、动态的创建一个script标签

var script=document. createElement("script");

2、设置script的src,设置回调函数

script.src="http://localhost:3000/testAJAX?calback=abc"; 

ps:使用jsonp返回代码给html文件需要返回完整的html代码,而不是简单的字符串

 response.send('hello jsonp-server');//会报错
 response.send('console.log("hello")');//不会报错

原因是浏览器引擎只能解析HTML的内容,所以服务端发送过来的内容必须是HTML语句

script.onload = function(){
//将body中的script标签删除
  document.body.removeChild(script)
}

2.2jsonp封装

缺点:麻烦,并且只能使用get请求

    //封装jsonp函数
        function jsonp(option){
             //动态创建javascript标签
            var script = document.createElement('script');
            var fnName = 'myjsonp'+Math.random().toString().replace('.','');
            var params = '';
            for(var attr in option.data){
                params += '&'+attr + '=' + option.data[attr];
            }
            //success函数已经不是全局函数了,要想办法把它变成全局函数
            //window对象新建一个fn属性,这个属性是一个函数
            window[fnName] = option.success; //变量不能用点来引用
            //添加src属性
            script.src = option.url + '?callback='+fnName + params;
            //将script标签追加到页面中
            document.body.appendChild(script);
            script.onload = function(){
             document.body.removeChild(script);
           }
        }
var btn1 = document.getElementById('btn1')
btn1.onclick = function(){
 jsonp({
         url:"http://localhost:8000/better",
         success:function(data){
          console.log(123);
            },
          data:{
               name:'hgc',
               age:20
             }
        })
}

3、使用vue.cli的反向代理

在vue.config.js文件中进行配置

4、使用vue.vite进行反向代理

在vite.config.js中进行配置

设置好api接口地址后可以在vue模块中通过axios直接访问

当请求以/api开头的地址时,vue会转发给http://127.0.0.1:8020这个服务器

PS:无论是vue.cli还是vue.vite都只能在开发过程中使用,还有其他方式暂时不做深入了解

浏览器中的缓存

简介

缓存的原理是在首次请求后保存一份请求资源的响应副本,当用户再次发起相同请求后,如果判断缓存命中则拦截请求,将之前存储的响应副本返回给用户,从而避免重新向服务器发起资源请求。

缓存的技术种类有很多,比如代理缓存、浏览器缓存、网关缓存、负载均衡器及内容分发网络等,它们大致可以分为两类:共享缓存和私有缓存。共享缓存指的是缓存内容可被多个用户使用,如公司内部架设的Web代理;私有缓存指的是只能单独被用户使用的缓存,如浏览器缓存。

HTTP缓存应该算是前端开发中最常接触的缓存机制之一,它又可细分为强制缓存和协商缓存,二者最大的区别在于判断缓存命中时,浏览器是否需要向服务器端进行询问以协商缓存的相关信息,进而判断是否需要就响应内容进行重新请求。下面就来具体看HTTP缓存的具体机制及缓存的决策策略。

浏览器缓存的优点

1.减少了冗余的数据传输,节省了网费

2.减少了服务器的负担,大大提升了网站的性能

3.加快了客户端加载网页的速度

浏览器缓存的分类

缓存协商和彻底缓存,也有称之为协商缓存和强缓存。

1.强制缓存:不会向服务器发送请求,直接从缓存中读取资源,在chrome控制台的network选项中可以看到该请求返回200的状态码;

2.协商缓存在使用本地缓存之前,需要向服务器发送请求,服务器会根据这个请求的request header的一些参数来判断是否命中协商缓存,如果命中,则返回304状态码并带上新的response header通知浏览器从缓存中读取资源;协商缓存可以解决强制缓存的情况下,资源不更新的问题

两者的共同点是,都是从客户端缓存中读取资源;区别是强缓存不会发请求,协商缓存会发请求。

强制缓存中header的参数(响应头)

Expires:response header里的过期时间,浏览器再次加载资源时,如果在这个过期时间内,则命中强缓存。

Cache-Control:当值设为max-age=300时,则代表在这个请求正确返回时间(浏览器也会记录下来)的5分钟内再次加载资源,就会命中强缓存。

cache-contro除了该字段外,还有下面几个比较常用的设置值:

-no-cache:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。

-no-store:直接禁止浏览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。

public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。

private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。

ps:

Expires:设置以分钟为单位的绝对过期时间,设置相对过期时间,max-age指明以秒为单位的缓存时间。Expires优先级比Cache-Control低,同时设置Expires和Cache-Control则后者生效.

协商缓存中的header参数

设置协商缓存:cache-control:-no-cache(不强制缓存)

Last-Modify/If-Modify-Since:浏览器第一次请求一个资源的时候,服务器返回的header中会加上Last-Modify,Last-modify是一个时间标识该资源的最后修改时间;当浏览器再次请求该资源时,request的请求头中会包含If-Modify-Since,该值为缓存之前返回的Last-Modify。服务器收到If-Modify-Since后,根据资源的最后修改时间判断是否命中缓存

Last-Modify的值是资源最后修改时间

PS:这种方式会有小bug,因为Last-Modify的值是以秒为单位的所以如果资源在一秒内修改了那么服务器是查不出来的

Etag/If-None-Match:web服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器决定)。If-None-Match:发现资源具有Etage声明,则再次向web服务器请求时带上头If-None-Match(Etag的值)。web服务器收到请求后发现有头If-None-Match则与被请求资源的相应校验串进行比对,决定是否命中协商缓存;

Etag的值是文件对应的hash值(但是会加服务器大开销,这个hash是需要生成的)

ETag和Last-Modified的作用和用法,他们的区别:

1.Etag要优于Last-Modified。Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;

2.在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值;

3.在优先级上,服务器校验优先考虑Etag。

浏览器缓存的过程

1.浏览器第一次加载资源,服务器返回200,浏览器将资源文件从服务器上请求下载下来,并把response header及该请求的返回时间一并缓存;

2.下一次加载资源时,先比较当前时间和上一次返回200时的时间差,如果没有超过cache-control设置的max-age,则没有过期,命中强缓存,不发请求直接从本地缓存读取该文件(如果浏览器不支HTTP1.1,则用expires判断是否过期);如果时间过期,则向服务器发送header带有If-None-Match和IfModified-Since的请求

3.服务器收到请求后,优先根据Etag的值判断被请求的文件有没有做修改,Etag值一致则没有修改,命中协商缓存,返回304;如果不一致则有改动,直接返回新的资源文件带上新的Etag值并返回200;

Cookie和Storage(localStorage和sessionStorage)

1.什么是cookie

是浏览器存储数据的一种方式,因为存储在用户本地,而不是存储在服务器上,是本地存储

一般会自动随着浏览器每次请求发送到服务器端

2.Cookie 有什么用

利用Cookie 跟踪统计用户访问该网站的习惯,比如什么时间访问,访问了哪些页面,在每个网页的停留时间等

3.Cookie的大小

Cookie一般不超过4kb,但大部分情况会更小

cookie的基本用法

document.cookie = 'username=zs'; 
document.cookie = 'age=18';
//2.读取 Cookie 
console.log(document.cookie);

读取的是一个由键值对构成的字符串,每个键值对之间由“;”(一个分号和一个空格)隔开

PS:虽然读取Cookie是所有键值对同时读取,但是设置的时候是一个个设置的,不能同时设置

cookie的属性

一般都会使用编码和解码处理再存储

如果max-age的值为0或者负数,cookie会被删除

localStorage是什么

localStorage 也是一种浏览器存储数据的方式(本地存储),它只是存储在本地,不会发送到服务器端

单个域名下的localStorage总大小有限制,一般是5M,有的浏览器只有2~3M

localStorage的基本用法

//setItem
localStorage. setltem('username', 'alex');
localStorage. setltem('username', 'zs');
//后面设置的同名属性会覆盖前面设置的
localStorage. setltem('age', 18); 
localStorage. setltem('sex', 'male');

//length
console.log(localStorage.length);

//getltem()
console.log(localStorage.getltem('username')); 
console.log(localStorage.getltem('age'));
//获取不存在的会返回null

//removeItem
localStorage.removeltem('username');
localStorage.removeltem('age');
//删除不存在的 key,不报错
localStorage.removeltem('name');


//clear
localStorage.clear();

localStorage的注意事项

1、localStorage是持久化的本地存储,除非手动清除(比如通过js删除,或者清除浏览器缓存),否则数据是永远不会过期的

2、localStorage 存储的键和值只能是字符串类型不是字符串类型,也会先转化成字符串类型再存进去

3、不同的域名是不能共用localStorage的

4、IE7及以下版本不支持 localStorage,IE8 开始支持

sessionStorage

基本上和localStorage一致,只是sessionStorage是会话结束的时候清空数据

宏任务和微任务

js把异步任务分为宏任务和微任务

每次执行宏任务之前都要检查一下微任务队列是否为空,执行完微任务之后才去执行第二个宏任务

PS:DOM 元素的点击事件是宏任务

宏任务是由宿主(Node/浏览器)发起

微任务由js引擎发起

!!!await关键字后面所有代码都会放到微任务队列里面

事件循环

执行顺序:
同步代码->微任务队列按先进先出原则执行->宏任务队列按先进先出原则执行

CSS权重问题

PS:虽然同级叠加可以增加权重,但是最大叠加值不会超过上一级,也就是说11个类选择器叠加的权重也不会大于一个ID选择器

img标签中alt和title的区别

区别一:

title是鼠标悬停时显示的值

alt是图片无法正常加载时显示的值

区别二:

在seo的层面上,蜘蛛抓取不到图片的内容,所以前端在写img标签的时候为了增加seo效果要加入alt属性来描述这张图是什么内容或者关键词。

清除浮动以及为什么要清除浮动

为什么要清除浮动

对元素设置浮动后元素会脱离文档流,如果此时包含块没有设置高度或者高度为自适应,此时浮动元素的高度就不会被计算进去,相当于没有可以展开的内容,包含块高度无法撑起,形成塌陷,虽然这种情况最简单的解决办法就是设置包含块高度,但是由于不知道以后内容是否会增加因此后期不好维护

清除浮动的方法

1、在末尾添加空标签如p标签

在末尾的空标签中设置clear:both,这样浏览器执行到这行代码的时候就知道不需要进行浮动,并且空元素是常规流,可以让父元素高度扩展过来

缺点:会产生大量空元素浪费资源

2、给父元素设置overflow:hidden/auto

加了overflow之后会把浮动流的高度计算以后再把后面多余的部分处理掉

3、父元素也设置为浮动

缺点:会影响布局

4、伪元素

类似第一种,但是创建的是虚拟元素,具体做法:

父元素类名::after{

content:'';

display:block;

clear:both;

}