博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
前端面试题 HTML5 CSS3(盒子模型、盒子水平垂直居中、经典布局) JS(闭包、深浅克隆、数据劫持和拦截) 算法(排序、去重、数组扁平化) Vue(双向数据绑定原理、通信方式)
阅读量:3968 次
发布时间:2019-05-24

本文共 24490 字,大约阅读时间需要 81 分钟。

前端面试题


本人是个新手,写下博客用于自我复习、自我总结。

如有错误之处,请各位大佬指出。
学习资料来源于:尚硅谷 和 珠峰培训


HTML5 相关面试题

相信大家对于HTML已经有自己的理解了,那么HTML5和HTML到底有什么区别?

这类的文章网上有很多,我也就不赘述了。
HTML5主要的新特性:
在这里插入图片描述
什么是标签语义化: 合理的标签干合理的事情

都有哪些标签:

  • 块状标签:<div> <p> <nav> <h1~h6> <ul> <ol> <li> <dl> <dt> <dd> <table> <tr> <td> <footer> <header> <main> <section> <artical>
  • 行内标签:<a> <i> <span> <em> <u> <br> <strong>
  • 行内块状标签:<input> <img> <label> <textarea> <select> <option>

块标签和行内标签有什么区别:

块状标签: 能独自占据一行,可以对其设置宽度、高度、对齐等属性。它可以容纳行内标签和其他块状标签。但是 p,h1,h2,h3,h4,h5,h6,dt,他们都是文字类块级标签,里面不能放其他块级元素。

行内状标签: 不能独自占据一行,默认在一行中排列,不能对其设置宽度、高度、对齐等属性,仅仅靠自身的字体大小和图像尺寸来支撑结构。行内元素只能容纳文本或其他行内元素。(a特殊: a标签可以放div块级标签,同时a标签里不能再放a标签)

行内块标签: 和相邻行内元素(行内块)在一行上,但是之间会有空白缝隙。默认宽度就是它本身内容的宽度。高度,行高、外边距以及内边距都可以控制。

上述三类标签怎么转换:

(1)display:inline;转换为行内元素
(2)display:block;转换为块状元素
(3)display:inline-block;转换为行内块状元素

display中还有哪些值:

display:none
display:flex
display:table

让元素隐藏,你可以怎么做?display:none和visibility:hidden的区别:

推荐文章:

项目中,你什么时候用到了display:flex:

主要用flex来代替浮动,完成页面的布局

除了这种方式能居中,还有哪些:(在下方)

都有哪些盒子模型:(在下方)


CSS3 相关面试题

面试的时候我们应该怎么回答问题,某位大佬的讲解如下:

比如面试官提问:需要盒子水平垂直居中,你有几种方案?
这种情况下,虽然你心里可能知道有几种方案,但是我们不能一开口就说:一共有五种方案,方案如下:… 这样显得是背下来的。
在回答这一类的问题时,可以这么说:这种需求在我之前的项目中是非常常见的,最开始的时候,我只使用了(xxx,xxx)这些方式…随着之后的不断学习和使用,发现(xxx)这种方式是有(xxx)这样的缺陷的,因此经过查阅资料 / 阅读文档 发现, 从(xxx)方面上来看,这个方式(xxx)是比较好的。之后有一段时间,当我自己去看博客、GitHub时,发现这个方式(xxx)虽然不常用,但也能实现,我觉得还是挺有意思的,所以就记下来了。

对于其他的问题也是同样的答题思路。总结来说,就是:

(1)在之前的学习生涯 / 项目开发 中,遇到过这样的问题 / 曾实现过这样的功能,我当时是(xxx)这么做的。
(2)随着某新型语言的兴起 / 随着之后我不断的学习,发现了一个更好的(xxx)方案,虽然这方案有(xxx)这样的缺陷,但是它实现起来,从目前来看是最好的。
(3)同时,我平常喜欢去看博客(或者其他的xxx),在查阅时突然发现有这样的一个(xxx)方式也能实现,发现也还不错,所以我也记录了下来。


不考虑其他因素,下面哪种的渲染性能比较高

在这里插入图片描述
答案是第二种,因为css的浏览器渲染机制是选择器从右向左查询。
即:第二种方式就只找所有的a,而对于第一种它会先找所有的a,再找box下所有的a,它进行了二次筛选。


盒子模型

推荐文章:

请你谈一谈盒子模型:

它可以分为标准盒子模型、怪异盒模型(IE盒子模型)、flex弹性伸缩盒模型、columns多列布局盒模型。(当然最基础的还是前两种盒模型,后面两种并不是一定要在回答中提到)

标准盒模型box-sizing: content-box;

在这里插入图片描述
在标准盒模型下,width和height是内容区域即content的width和height。
而盒子总宽度为 width + padding(左右) + border(左右) + margin(左右)

可想而知,如果我们使用标准盒模型设置width和height,调整好了一切参数后,一旦需要我们修改其中的参数,比如调整border,那么盒子模型的位置也就必然出现偏移,即:每次修改border、padding后,你都需要手动修改width和height的值。

在css3中就允许我们使用怪异盒模型,这次我们设置width和height,那么盒子大小就是这个值,不管我们怎么调border、padding,它会通过自动缩放内容,来帮我们保证这个width和height值不变。

因此推荐在项目中使用怪异盒模型。不仅如此,其实各大UI组件、公共样式的源码中,使用的也都是怪异盒模型。

怪异盒模型box-sizing: border-box;

在这里插入图片描述
而IE盒模型或怪异盒模型,width和height除了content区域外,还包含padding和border。
盒子的总宽度为 width + margin(左右)(即width已经包含了padding和border值)

flex盒模型display: flex;

推荐文章:
在这里插入图片描述
容器默认存在两根轴:水平的主轴(main axis)和垂直的交叉轴(cross axis)。
主轴的开始位置(与边框的交叉点)叫做main start,结束位置叫做main end;
交叉轴的开始位置叫做cross start,结束位置叫做cross end。
项目默认沿主轴排列。单个项目占据的主轴空间叫做main size,占据的交叉轴空间叫做cross size。

多列布局:(基本上不用)

在这里插入图片描述


盒子水平垂直居中的方案

  
盒子水平垂直居中
zzz

经典布局方案

圣杯布局 、 双飞翼布局 => 左右固定,中间自适应

(但实际上flex和定位更简单,因此我更偏向于使用定位方式,如果不考虑兼容就用flex)

圣杯布局

  
圣杯布局

双飞翼布局

  
双飞翼布局

flex布局

  
flex布局

定位方式布局

  
定位

css实现三角形

    
Title

JS 相关面试题


8种数据类型及区别

(在我的认知中,从最开始的学习中,也一直以为只有6种数据类型,直到真的有人问到8种数据类型,才傻眼了)

基本数据类型:Number、String、Boolean、Undefined、Null

引用数据类型:Object、Function
ES6 中新增了一种 Symbol 。这种类型的对象永不相等,即始创建的时候传入相同的值,可以解决属性名冲突的问题,做为标记。

(但是对于 bigInt 到底算不算数据类型,暂持观望态度)

推荐文章:


关于堆栈内存和闭包作用域的题

堆:存储引用类型值的空间

栈:存储基本类型值和执行代码的环境

//example 1    let a = {
}, b = '0', c = 0; a[b] = '你'; a[c] = '好'; console.log(a[b]);

在这里插入图片描述

因此输出结果是:好

数组和对象的区别

数组:以数字作为索引,这个索引就是它的属性名。即:数组的数据没有”名称”
对象:当然也可以用数字作为索引,这个索引也是它的属性名,但它还可以设置其他的属性名。

且数组表示有序数据的集合,而对象表示无序数据的集合。如果数据的顺序很重要,就用数组,否则就用对象。


//example 2    let a = {
}, b = Symbol('1'), c = Symbol('1'); a[b] = '你'; a[c] = '好'; console.log(a[b]);

因为Symbol是创建唯一值的,因此会出现下面的效果:

在这里插入图片描述

因此输出结果是:你


//example 3    let a = {
}, b = {
n: '1' }, c = {
m: '2' }; a[b] = '你'; a[c] = '好'; console.log(a[b]);

看下图就会发现,只要在obj中,即将要存放一个对象,无论这个对象里面存放的是什么,存放的效果都会是[object Object]: xxx。(即属性名相同,均为Object)

(这是因为存放的时候调用了toString()方法)
在这里插入图片描述

因此输出结果是:好

Object.prototype.toString()的用法

之前,我们总会看到直接输出一个对象,会得到[object Object]这样的输出结果,为什么会输出这样的结果,其实是因为,输出的是toString()方法的返回值。它默认输出的就会是这样的结果。现在,通过调用Object.prototype.toString,可以为其改变输出结果。
在这里插入图片描述
当然也可以直接对 构造函数的实例 修改。
在这里插入图片描述


var test = (function (i) {
return function(){
alert(i*=2); } })(2); test(5);

给var test = ? 赋值的步骤是:(1)创建一个变量 (2)准备值 (3)关联

这题的值是执行一个函数得来的,紧接着就把函数执行了,传进来的值就是2。这个叫:立即执行的自定义函数。

这里还涉及到了栈内存,这个过程的标准说法叫:浏览器一加载页面就形成栈内存,这个栈内存是用来执行代码的。每当这个函数被执行一次,都会形成一个全新的执行上下文(Execution Context Stack :执行栈)。页面就会将这个函数压缩到这个栈里去执行。如下图

需要注意,因为alert弹出的结果都会转化为字符串,

因此答案是:‘4’ (字符串4)
在这里插入图片描述
在这里插入图片描述
闭包形成的条件

  1. 函数嵌套
  2. 内部函数引用外部函数的局部变量

闭包的优点:延长外部函数局部变量的生命周期

闭包的缺点:容易造成内存泄漏

需要注意的是:合理的使用闭包,用完闭包要及时清除(销毁)


var a = 0,        b = 0;    function A(a){
A = function(b){
alert(a + b++); }; alert(a++); } A(1); A(2);

在这里插入图片描述

因此输出结果是:‘1’ 和 ‘4’

你会发现全局的a和b根本就没变,因此可知闭包的作用:保存和保护


作用域

			

值类型和引用类型的传递

在这里插入图片描述

在这里插入图片描述

			

过程:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


对象(数组)的深克隆和浅克隆

// 二维及二维以上都叫做多维对象    // 比如obj是一维的,一维的属性名里的属性值,不一定仅仅是基本属性值,还有可能是数组,对象,正则    // 其中b是数组,c是对象,还要再深一个层级,变成二维。如果再深就三维,以此类推。    let obj = {
a: 100, b: [10,20,30], c: {
x: 10 }, d: /^\d+$/ }; // =>浅克隆:只把第一层克隆下来 let obj2 = {
}; for(let key in obj){
//运行到if里,也就证明遍历到原型了,需要break if(!obj.hasOwnProperty(key)) break; obj2[key] = obj[key]; } console.log(obj, obj2); //ES6浅克隆方式: let obj2 = {
...obj};

在这里插入图片描述

因为obj===obj2,返回的是false,也就证明obj2是被克隆出来的。
但是当你修改obj2.c.x的值:
在这里插入图片描述
你会发现,obj.c.x的值也被修改了。按理说obj和obj2应该没有联系了?
在这里插入图片描述
这就是因为浅克隆只把第一级克隆了,第二级没克隆,这样一来你操作第二级就也会对第一级造成影响。
在这里插入图片描述
在项目中假如用到的数据有很多层,你只是想修改一部分,而不想修改原有的数据结构,因此就肯定需要使用深克隆。


// =>深克隆方式1(项目中使用这种方案就可以了)    let obj2 = JSON.parse(JSON.stringify(obj));

虽然这种方式经常使用,但是需要说明的是,经过JSON.stringify处理后会变成字符串。但你可以发现,这个过程中,如果存放的有正则、函数就会变成空字符串。对于日期而言,我们想让它变成标准日期格式对象,现在它变成了字符串。这样显然是不好的。因此这种方式是有问题的,但实际上我们很少碰到存放正则、函数、日期的情况,所以暂且只要注意这三个情况就可以了。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最完善的方案:

// 深克隆    function deepClone(obj){
// 过滤特殊情况 if (obj === null) return null; if (typeof obj !== "object") return obj; if (obj instanceof RegExp) return new RegExp(obj); if (obj instanceof Date) return new Date(obj); // 不直接创建空对象,目的:克隆的结果和之前保持相同的所属类 let cloneObj = new obj.constructor; for (let key in obj){
if (obj.hasOwnProperty(key)){
cloneObj[key] = deepClone(obj[key]); } } return cloneObj; } let obj2 = deepClone(obj); console.log(obj, obj2); console.log(obj === obj2); //false console.log(obj.c === obj2.c); //false

阿里的一道关于面向对象的面试题

function Foo(){
getName = function(){
console.log(1); }; return this; } Foo.getName = function(){
console.log(2); }; Foo.prototype.getName = function(){
console.log(3); }; var getName = function(){
console.log(4); } function getName(){
console.log(5); } Foo.getName(); getName(); Foo().getName(); getName(); new Foo.getName(); new Foo().getName(); new new Foo().getName();

输出结果为:

在这里插入图片描述
所谓变量提升就是:在当前作用域下,所有js代码执行之前,将所有function函数提前声明定义、将所有var变量提前声明。
(即:函数声明和变量声明总是会被解释器悄悄地被"提升"到方法体的最顶部)

在本题中,首先是变量提升,涉及到下面三个部分:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
因此Foo函数被提前声明。然后是var getName被提前声明。(此时没有赋值)
最后getName函数被提前声明,但是发现getName已经被声明了,所以它会去给这个getName赋值。所以这一步的结果如下:
(Foo和getName各占据一段堆内存,为了便于看到输出结果,这里忽略堆内存地址号,直接用输出结果替代)
在这里插入图片描述
变量提升之后,代码执行:
对于以下部分:
在这里插入图片描述
运行Foo.getName,给Foo里getName声明赋值。
然后Foo.prototype.getName,就是给Foo的原型中添加getName。
因为原型也是对象,所以它也占据一段堆内存。

所以在这里会形成这样的结构:

在这里插入图片描述
然后是以下代码部分:
在这里插入图片描述
因为之前只是声明而没有赋值,所以在这里就又会去找那个getName然后赋值。
在这里插入图片描述
因此,输出结果中肯定不会出现5了。

最后分析输出结果:

根据之前的分析,画出图就很好理解了:

在这里插入图片描述
在这里插入图片描述
对于Foo().getName(),它是两步操作,先把Foo执行,再让它的返回结果执行getName()。需要注意的是,Foo执行时,它里面的getName并不是私有的,因此会去上级作用域寻找,即到了全局中寻找。因此全局下的getName再次发生了变化:
在这里插入图片描述
对于普通函数来说,return this的时候,就相当于return了 window 。
所以Foo()的返回结果是window。
因此Foo().getName() => window.getName()。此时getName结果为1。所以:
在这里插入图片描述
从以上可以发现,Foo加()和不加()是两个概念。
对于接下来的题,就需要涉及到JS运算符的优先执行顺序。
如下图,不加 () 就叫 无参数new ,加 () 就叫 有参数new。
.的方式叫成员访问。
那么这三个到底是谁先运行呢?
(因为它可能是先 new Foo 再 getName(),也有可能是先 Foo.getName() 再 new )
在这里插入图片描述
查阅文档发现:
(优先级高的优先执行,相同优先级从左到右执行)
在这里插入图片描述
因此对于 new Foo.getName(),
在这里插入图片描述
先执行Foo.getName(),再new。
Foo.getName()返回的结果就是输出2的那个函数,
对它new还是把它当普通函数执行,输出结果为2.

同理对于 new Foo().getName(),

在这里插入图片描述
就是 从左到右,先执行 new Foo() ,再getName()。
new Foo()虽然也是函数执行,但它创建了一个当前函数的实例,实例调getName显然要去原型里面找了(因为最开始构建函数时,没给实例本身添加任何属性)
所以输出结果是3.

最后对于 new new Foo().getName(),有人如果对这个过程还是不了解的话,就可能会说,这优先级不都是19吗,那从左向右执行,怎么还出现new 一个 new 的操作?

实则不然。这其中 new Foo() 执行出来是个实例xxx,那么这里就相当于变成了:
new xxx.getName(),又回到了new Foo.getName()这个形式上。

因此,首先创建了一个当前函数的实例,实例调getName依然去原型里面找,返回的是一个输出3的函数,对它new还是把它当普通函数执行,输出结果为3.

在这里插入图片描述


头条的一道关于EventLoop的面试题

推荐文章:

eventloop解析:
理解async和await:
理解promise中的resolve:

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');

输出结果:

在这里插入图片描述
首先需要说明的是,浏览器是多线程的,而JS是单线程的,也就导致浏览器只给一个线程让它来渲染。

代码一行一行从上至下执行叫“同步”,反之就是“异步”。JS中大部分代码都是同步的。

而本题中就涉及到异步。

浏览器提供了一个:事件队列(EventQueue),里面存放的就是一些延后执行的任务,比如定时器。而在事件队列中,又分为微任务队列和宏任务队列。

在这里插入图片描述
整体的执行顺序就是:主线程代码先执行,执行过后,到事件队列中执行。在事件队列中,先去微任务队列中执行,没有微任务再去执行宏任务。等到宏任务都执行完了,浏览器也就空闲下来了。

那么现在回到本题中,从上到下的执行代码也就是主线程:

首先创建函数async1和async2,然后输出’script start’。

接下来碰到了一个定时器,需要说明的是,定时器属于宏任务,因此把这个定时器任务存放到事件队列的宏任务队列中。设置好之后,主线程代码继续执行,碰到了async1(),async1函数执行。因此接下来输出’async1 start’。

输出后,碰到了await async2()。它做的操作是:执行async2,等待返回的结果。

但它本身并不是同步操作,而是异步操作,属于微任务。因此async1函数的执行就停在了这个地方,等待主线程执行结束去执行微任务,返回结果后才会继续执行。
此时async2已经执行,所以输出’async2’。
但是到底返回了一个怎样的结果,可以参考一下上面的推荐文章。
因此接下来执行new Promise部分。

new Promise时,会立即把EC函数执行(即 new的时候 才是同步的)。

因此输出’promise1’。然后碰到了resolve(resolve和reject都属于微任务)。
所以Promise中的执行就停在了这个地方,等待主线程执行结束去执行微任务。
resolve / reject 执行的时候,把then / catch 中的方法执行。

主线程最后输出’script end’。(主栈第一阶段完成)

在这里插入图片描述
然后就是去事件队列中寻找了(先微任务后宏任务),找到任务后就拿回到主线程中执行。这个查找、运行、查找、运行…的操作,就叫做EventLoop。

在这里需要说明的是,在从微任务队列中查找任务时,不同的v8引擎会导致结果不同,即不一定是先获取到先存放的任务,所以在这里输出结果有可能不同。

那么假设先查找到了上图的B任务,那么接下来输出’async1 end’。

然后查找到了上图的C任务,那么接下来输出’promise2’。
最后执行到了宏任务,输出’setTimeout’,结束。

具体哪些是宏任务?哪些是微任务?以及它们的细节,感兴趣的大家可以查阅一下官方文档。

宏任务:定时器、事件绑定…

微任务:promise、 async、 await…


数据劫持和拦截

问,a等于什么值,才会console.log(1)?

var a = ?;  if (a == 1 && a == 2 && a == 3) {
console.log(1); }

首先需要说明 ===== 的差异:

在这里插入图片描述在这里插入图片描述
三个等于号不仅要比较值,也要比较数据类型。
两个等于号只比较值,不比较数据类型,但是它到底是如何实现这个比较的?

如果两个等于号左右两侧的数据类型不一样,它的转换规则为:

  1. 对象 == 字符串 :会使用 对象.toString() 把对象变成字符串
  2. null == undefined :相等,但是和其他值比较就不再相等了
  3. NaN == NaN: 不相等,NaN和任何值都不相等
  4. 剩下的所有情况比较时,都转换为数字
    比如:"1"==true,等号左右两侧全都会变成数字1
    对于对象也是一样会转成数字
    在这里插入图片描述
    当你知道这上面的规则时,第一种方案就出现了:因为对象和数字比较需要先调用toString(),如果我们自己没设置toString()就会去调用原型上的toString(),那我们可以自己设置一个toString()效果,让它满足题目等式要求。
var a = {
i: 0, toString() {
return ++this.i; } }; if (a == 1 && a == 2 && a == 3) {
console.log(1); }

第二种方案:数据劫持和拦截(这也是之后Vue里要涉及到的知识点)

推荐文章:

数据劫持:

数据劫持和拦截的简例:

let obj = {
name: '1' }; Object.defineProperty(obj, 'name', {
get() {
console.log('获取'); }, set() {
console.log('设置'); } });

通过Object.defineProperties的设置,当我们获取obj.name时就会触发get,当我们设置obj.name时就会触发set,如下图:

(获取时出现的undefined,是因为get没有返回值)
在这里插入图片描述
那么数据劫持和拦截就是:监听某个对象里的某个属性,当它在获取和设置时做一系列操作。

因此根据这个特点,第二种方案为:

var i = 0;  Object.defineProperty(window, 'a', {
get() {
return ++i; } }); if (a == 1 && a == 2 && a == 3) {
console.log(1); }

第一种方案toString的另一种实现方式

var a = [1, 2, 3];  a.toString = a.shift;  if (a == 1 && a == 2 && a == 3) {
console.log(1); }

练习题

function A(){
alert(1); } function Fn(){
A=function(){
alert(2); }; return this; } Fn.A=A; Fn.prototype={
A:()=>{
alert(3); } }; A(); Fn.A(); Fn().A(); new Fn.A(); new Fn().A(); new new Fn().A();

看起来和之前的题很像,唯一的区别就是这其中出现了箭头函数。

需要注意 原型对象上的A函数,是个箭头函数,箭头函数不能被new。
因此 new new Fn().A() 会报错。

箭头函数和普通函数的区别

箭头函数没有自己的执行主体(this),它使用的this都是继承自己上下文的this。
普通函数有它自己的this。
箭头函数之所以不能被new,是因为它没有原型链prototype也就没有constructor构造器函数,所以它不能被new。

推荐文章:

箭头函数和普通函数:

因此输出结果:1、1、2、1、3、报错


var x = 2;  var y = {
x: 3, z: (function (x) {
this.x *= x; x += 2; return function (n) {
this.x *= n; x += 3; console.log(x); } })(x) }; var m = y.z; m(4); y.z(5); console.log(x, y.x);

输出结果为:

在这里插入图片描述


var x = 0,      y = 1;  function fn() {
x += 2; fn = function (y) {
console.log(y + (--x)); }; console.log(x, y); } fn(3); fn(4); console.log(x, y);

输出结果为:

在这里插入图片描述


算法相关面试题


将字符串进行驼峰命名

			

冒泡排序

如果有兴趣看其他的排序方式及思路,大家可以去我之前的文章查阅一下:

虽然是后台Java语法的,但是整体思路和实现方式没有差异。

			

反转数组

  

去掉数组中重复性的数据

推荐文章:

思路1:只用到原数组。遍历原数组,每次都拿原数组的中的一个值,和它后面的所有值比较,一旦出现重复,就把当前值从原数组中去掉,然后遍历到下一个值。

let ary = [8, 11, 20, 5, 20, 8, 0, 2, 4, 0, 8];  for (let i = 0; i < ary.length - 1; i++) {
let item = ary[i], args = ary.slice(i + 1); if (args.indexOf(item) > -1) {
ary.splice(i, 1); i--; } } console.log(ary);

但是对于思路1会出现一些问题:使用splice删除了原数组中的当前重复数据项,后面所有项的索引都要发生改变,一旦原数组中存放的数据很多,在性能方面一定很差。同时因为原数组的改变,一定需要i--,否则会产生数组塌陷问题。

为了避免对原数组进行操作,因此从思路1的思路继续出发,思路2:我们可以再建一个新数组,一旦判断出当前项不重复,就把当前项放入新数组。

let ary = [8, 11, 20, 5, 20, 8, 0, 2, 4, 0, 8];  let arr = [];  for (let i = 0; i < ary.length - 1; i++) {
let item = ary[i], args = ary.slice(i + 1); if (i + 1 == ary.length - 1) {
arr.push(ary[i + 1]); } if (args.indexOf(item) > -1) {
} else {
arr.push(item); } } console.log(arr);

对于思路2,需要注意的是,在思路1代码的基础上,一定要把最后一项加入到新数组

当然也有办法,让我们只对原数组进行操作,且避免数组塌陷问题的。

思路3:在遍历时,将重复的元素设为null,遍历结束后,把这些null从原数组中去除。

let ary = [8, 11, 20, 5, 20, 8, 0, 2, 4, 0, 8];  for (let i = 0; i < ary.length - 1; i++) {
let item = ary[i], args = ary.slice(i + 1); if (args.indexOf(item) > -1) {
ary[i] = null; } } ary = ary.filter(item => item !== null); console.log(ary);

但是这么做其实有一个二次处理的过程,性能上也不太好。当然还有其他办法.

思路4:当我们发现重复项,我们不去删除它,而是把数组最后一项赋值到第一项,这样一来也就实现了删除的效果,同时没有数组塌陷问题。然后把最后一项删除。
需要注意的是,我们把最后一项放到了当前项上,所以需要i--,继续从当前索引位置开始进行判断。

let ary = [8, 11, 20, 5, 20, 8, 0, 2, 4, 0, 8];  for (let i = 0; i < ary.length - 1; i++) {
let item = ary[i], args = ary.slice(i + 1); if (args.indexOf(item) > -1) {
ary[i] = ary[ary.length-1]; ary.length--; i--; } } console.log(ary);

在前面的代码中,都用到了indexOf,但是indexOf有兼容性问题。

那么接下来就不使用indexOf来实现。
思路5:模拟indexOf实现

let ary = [8, 11, 20, 5, 20, 8, 0, 2, 4, 0, 8];  let obj = {
}; for (let i = 0; i < ary.length; i++) {
let item = ary[i]; if (typeof obj[item]!=='undefined') {
ary[i] = ary[ary.length-1]; ary.length--; i--; continue; } obj[item] = item; } console.log(ary);

思路6

1.创建一个新数组,把原数组中的第一个元素插入到新数组中
2.遍历原数组中的每一个元素分别和新数组中的每一个元素进行比较
3.一旦原数组的某个元素和新数组的值都没有重复,那么就把它加入到新数组中

//原数组  var arr = [8, 11, 20, 5, 20, 8, 0, 2, 4, 0, 8];  //新数组  var t = [];  t[0] = arr[0];  //arr中的每个元素  for (var i = 0; i < arr.length; i++) {
//t中的每个元素 for (var k = 0; k < t.length; k++) {
//当原数组中的值和新数组中的值相同的时候,就没有必要再继续比较了,跳出内循环 if (t[k] == arr[i]) {
break; } //拿原数组中的某个元素比较到新数组中的最后一个元素还没有重复 if (k == t.length - 1) {
//将数据插入新数组 t.push(arr[i]); } } } console.log(t);

思路7:先排序,再相邻比较

let ary = [8, 11, 20, 5, 20, 8, 0, 2, 4, 0, 8];  ary.sort((a, b) => a - b);  for (let i = 0; i < ary.length; i++) {
if(ary[i] == ary[i+1]){
ary[i] = null; } } ary = ary.filter(item => item !== null); console.log(ary);

思路8:先排序,再相邻比较(基于正则)

let ary = [8, 11, 20, 5, 20, 8, 0, 2, 4, 0, 8];  ary.sort((a, b) => a - b);  ary = ary.join('@');  console.log(ary);  let reg = /(\d+@)\1*/g,      arr = [];  ary.replace(reg, (n, m) => {
arr.push(Number(m.slice(0, m.length - 1))); //arr.push(parseFloat(m)); }); console.log(arr);

思路9:利用Set就简单的多了,但是需要注意Set是ES6中才出现的。

let ary = [8, 11, 20, 5, 20, 8, 0, 2, 4, 0, 8];  let arr = [...new Set(ary)];  console.log(arr);
let ary = [8, 11, 20, 5, 20, 8, 0, 2, 4, 0, 8];  let arr = Array.from(new Set(ary));  console.log(arr);

更多方法可以参考推荐文章。


数组扁平化的实现方案

数组扁平化:把多维数组变成一维数组。

方案一:ES6 方法直接实现

let arr = [    [1, 2, 2],    [3, 4, 5, 5],    [6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10  ];  arr = arr.flat(Infinity);  console.log(arr);

方案二:

首先arr.toString,arr就会变成一串数字,并用逗号分隔,而且它不是数组。
那我们就可以用split(',')让它变成数组,并根据逗号区分出每个元素。
之后会发现,数组中存放的每个数字变成了字符串。
然后用map,把每个元素变成数字,即可。

let arr = [    [1, 2, 2],    [3, 4, 5, 5],    [6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10  ];  arr = arr.toString().split(',').map(item => parseFloat(item));  console.log(arr);

方案三:

let arr = [    [1, 2, 2],    [3, 4, 5, 5],    [6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10  ];  arr = JSON.stringify(arr).replace(/(\[|\])/g,'').split(',').map(item => parseFloat(item));  console.log(arr);

方案四:

let arr = [    [1, 2, 2],    [3, 4, 5, 5],    [6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10  ];  while(arr.some(item => Array.isArray(item))){
arr = [].concat(...arr); } console.log(arr);

方案五:

let arr = [    [1, 2, 2],    [3, 4, 5, 5],    [6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10  ];  (function () {
function myFlat() {
let result = [], _this = this; //循环arr中每一项,把不是数组的存储到新数组中 let fn = (arr) => {
for (let i = 0; i < arr.length; i++) {
let item = arr[i]; if (Array.isArray(item)) {
fn(item); continue; } result.push(item); } }; fn(_this); return result; } Array.prototype.myFlat = myFlat; })(); arr = arr.myFlat(); console.log(arr);

斐波那契数列

方法一:

function fibonacci(count) {
if (count <= 1) return 1; let arr = [1, 1]; //即将要创建多少个 let i = count + 1 - 2; while (i > 0) {
let a = arr[arr.length - 2], b = arr[arr.length - 1]; arr.push(a+b); i--; } return arr[arr.length-1]; } console.log(fibonacci(5));

方法二:

function fibonacci(count) {
function fn(count,curr=1,next=1){
if(count==0){
return curr; }else{
return fn(count-1,next,curr+next); } }; return fn(count); } console.log(fibonacci(5));

React和Vue比较

相同点

  1. 都有组件化开发和Virtual DOM
  2. 都支持props进行父子组件间数据通信
  3. 都支持数据驱动视图, 不直接操作真实DOM, 更新状态数据界面就自动更新
  4. 都支持服务器端渲染

不同点

  1. 数据绑定: vue实现了数据的双向绑定,react数据流动是单向的
  2. 组件写法不一样, React推荐的做法是 JSX , 也就是把HTML和CSS全都写进JavaScript了,即’all in js’; Vue推荐的做法是webpack+vue-loader的单文件组件格式,即html,css,js写在同一个文件
  3. state对象在react应用中不可变的,需要使用setState方法更新状态;在vue中,state对象不是必须的,数据由data属性在vue对象中管理
  4. virtual DOM不一样,vue会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树.而对于React而言,每当应用的状态被改变时,全部组件都会重新渲染,所以react中会需要shouldComponentUpdate这个生命周期函数方法来进行控制
  5. React严格上只针对MVC的view层,Vue则是MVVM模式

Vue 相关面试题

(因为我只使用Vue,因此React我也就无法提供相关内容了)

推荐文章:

vue3.0相关内容:

首先vue3.0版本目前还有很长的路要走,还没有开始正式使用,所以在项目中肯定还是用2.0,但是我们准备面试肯定多多少少理解了一些3.0的特性。因此在回答这一类问题时,可以这么回答:

在Vue2.0中,这个需求在我平常的项目中曾经遇到过,可以这么做: xxxx …

当我Vue2.0使用的熟练了后,我就开始研究它的底层实现原理,这时候才知道原来双向数据绑定的底层实现原理是ES5中新加的Object.defineProperty来进行数据劫持和拦截的(说一些和问题相关的底层原理),它的实现思路是…
虽然3.0还没有投入到实战,但我经常关注博客文章(或者去GitHub下载源码),看了大家对3.0的评价(或者对代码的分析),发现在Vue3.0中,双向数据绑定的底层实现原理是ES6的Proxy(说一些和问题相关的一些特性…),它相对于2.0有…这样的好处。


Vue2.0/3.0双向数据绑定的实现原理

推荐文章:

Vue3.0双向数据绑定原理:

双向数据绑定就是:当数据改变的时候,让视图重新渲染。

Vue2.0:ES5:Object.defineProperty

姓名:

可以发现,defineProperty方式会有以下问题:

首先遇到defineProperty就一定要克隆原始数据,避免发生死循环,
因为如果是直接对原数据劫持和拦截get() {return obj.name;}
当运行到return obj.name时,就相当于又去obj中获取name,又会去执行get…
这样就不断执行get,从而陷入死循环。

其次,get和set里,我们只能一个一个地给每个属性赋新值,

即:需要分别给对象中每个属性设置监听。
同时正因如此,如果在运行过程中,给对象添加了新的属性,(显然在最开始没有给它设置监听),就算是$set,将新属性添加其中,也会因为最开始没给它设置监听,从而实现不出效果。

因为2.0的实现方式会有上述问题,所以在Vue3.0,就对它进行了改进,使用了ES6的Proxy。首先先来看看Proxy的简单用法和实现效果:

let obj = {
}; obj = new Proxy(obj, {
/*target:监听对象,prop:监听对象属性*/ get(target, prop){
console.log('A'); return target[prop]; }, /*value:监听对象属性值*/ set(target, prop, value){
console.log('B'); target[prop] = value; } });

在这里插入图片描述

通过输出结果就可以看到:Proxy方式不用克隆原始数据,而且可以实时向对象中添加属性,还能为它设置监听,从而完美解决了问题。
因此现在来看一看到底如何去做:
Vue3.0:ES6:Proxy

姓名:

MVC和MVVM的区别

React:MVC,Vue:MVVM。

推荐文章:

MVC和MVVM区别:

MVVM:

在这里插入图片描述
首先有一个监听,会监听这个输入框内容,当输入框中的内容发生了改变,监听器就触发。这个监听器的回调函数就会把输入框的内容保存到data里,同时将这个数据输出到页面里去显示。而这个过程就会用到DOM监听和数据绑定。实现原理已经在上面说过了。

MVVM:

model:模型,数据对象(data)
view:视图,模板页面
viewModel:视图模型(Vue的实例)

在代码中的体现:

在这里插入图片描述

因为我对React不熟,所以对于MVC没有去太深的理解。

简单来说,
MVC少了一个视图更改数据,因此它叫单向数据更改。即单向指的是:数据的更改控制视图。而MVVM不仅实现了数据的更改控制视图,也实现了视图的更改控制数据。

在编码方面上来说,Vue中,通过原理,我们可以发现,Vue帮我们做好了oninput和onchange事件,而React没有实现它们,需要我们自己去实现,但是实现方式还是很简单的。


跨域问题

推荐文章:

跨域问题:

跨域问题的产生及其价值意义

当我们基于ajax向服务器发送请求,在浏览器处理的时候,它有一个特点:

只要协议、域名、端口有任何一个不同,都被当作是不同的域,而服务器是不允许跨域访问的。
比如:
在这里插入图片描述
(http默认端口号80,https默认端口号443,ftp默认端口号21)

在前后端没分离的时候,肯定就不会遇到这样的问题,因为当时后台程序和客户端程序都会部署在同一台服务器上,同一个域下,同一个端口号上。

随后在前后端分离的初期,也是部署在同一台服务器上的。
但之后随着前后端的不断发展,也就发现,前后端貌似没必要一定在一个服务器上。
因此区分出了 数据服务器和Web服务器
在这里插入图片描述
这么做的好处在于:
数据服务器专门用于处理 业务逻辑和数据接口,而Web服务器主要用来 请求一些资源文件。这样一来做服务器集群部署的时候,就可以把Web服务器在全国各地建立站点,然后当你访问服务器的时候,就可以总是访问最近的服务器。
(从成本等方面上来看,肯定不能把数据服务器也部署到全国各地)
这样之后,从Web服务器上拿接口,就不在一个服务器上了,所以跨域问题就出现了


解决方案

(因为我对这部分也只是简单认识,也许有的地方说的不对,详细的部分大家可以百度查找,写下这部分也是因为听闻有些公司的前端需要仔细研究跨域问题)

阶段一:JSONP跨域解决方案
在这里插入图片描述
最开始的跨域解决方案都是使用JSONP。JSONP利用了一个特点:ajax发请求会存在域的限制,但有一些东西发请求不需要限制,
比如<script>标签、<link>标签、<img>标签。
平常在项目中,我们就经常这么用:
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
script请求资源一定是get请求,它不存在域的限制。

原理

即:动态创建一个script标签,把要向服务器发送请求的地址,赋值给script里的src。然后是怎么做的呢?
首先通过问号传参的方式,我们传一个函数func给服务器(这个方法是我们自己写的)(这种方式只能是Get请求)
在这里插入图片描述
当服务器拿到这个请求之后,就会去准备数据,然后把数据返回到客户端。
那么服务器如何把数据传回来呢?服务器会拿到函数func,并把它拼成字符串:
"func({...})"(把我们想要的数据拼接成这样的结果)
在这里插入图片描述
然后客户端就相当于拿到了"func({...})",里面有我们想要的数据。(浏览器会帮我们执行这个函数)
在这里插入图片描述
但是Get会有一系列问题:不安全、有缓存、传递的信息有大小限制…
同时服务器还得能帮我们把数据进行一系列拼接,来帮我们实现功能。

因此目前JSONP已经很少用了。

阶段二:基于iframe的跨域解决方案

(详情可见推荐文章,这也是前几年的解决方案了)
在这里插入图片描述

阶段三:CORS跨域资源共享

以前不能发请求,是因为服务器不允许,所以现在的做法就是让服务器允许这么做。

客户端:

import axios from 'axios';import qs from 'qs';/*向服务器发送请求的地址*/axios.defaults.baseURL = "http://127.0.0.1:3000";/*设置超时时间*/axios.defaults.timeout = 10000;/*服务器跨域发请求时,允许携带cookie资源凭证*/axios.defaults.withCredentials = true;/** 设置请求传递数据的格式(看服务器要求什么格式)* x-www-form-urlencoded* */axios.defaults.headers['Content-Type'] = 'application/x-www-form-urlencoded';axios.defaults.transfromRequest = data =>qs.stringify(data);/** 设置请求拦截器* TOKEN校验(JWT):接收服务器返回的token,存储到vuex/本地存储中,每一次向服务器发请求,我们应该把token带上* */axios.interceptors.request.use(config => {
let token = localStorage.getItem('token'); token && (config.headers.Authorization = token); return config;}, error =>{
return Promise.reject(error);});/** 响应拦截器* */axios.interceptors.response.use(response =>{
return response.data;}, error =>{
});export default axios;

服务器端:

在这里插入图片描述

除此以外还有 基于http proxy实现跨域请求、nginx反向代理…感兴趣的可以查阅百度


Vue组件间通信方式

(面试基本必问,然后回答到vuex方式时,面试官就会问vuex的工作原理)

通信方式相关内容,可以参考我之前的文章:

通信种类

  1. 父组件向子组件通信
  2. 子组件向父组件通信
  3. 隔代组件间通信
  4. 兄弟组件间通信

方式1: props(属性传递)

组件之间联系需要import,想要映射组件标签需要components。最基本的传递就可以通过这个组件标签直接传递<xxx :yyy="yyy"/>

(xxx:组件标签名,yyy:传递的对象属性或者方法)
如:<Add :comments="comments"/>

传递过去后,接收的组件用props就可以接收到传递过来的数据。

props一共有三种使用方式:

props:{ //指定属性名和属性值的类型      comment: Object}
props:{      addComment:{ //最完整写法:指定属性名/属性值的类型/必要性         type:Function,         required: true      }}
props: ['comments']

方式1:只能父向子通信,不能子向父通信。隔代组件和兄弟组件间通信比较麻烦。


方式2: vue自定义事件($on / $emit)

绑定事件监听:

// 方式一: 通过 v-on 绑定@delete_todo="deleteTodo"// 方式二: 通过$on()this.$refs.xxx.$on('delete_todo', function (todo) {
this.deleteTodo(todo)})

触发事件:

// 触发事件(只能在父组件中接收)this.$emit(eventName, data)

比如:(方式一)

(方式二)

Add.vue

方式2: 只适合于子向父通信。隔代组件或兄弟组件间通信此种方式不合适。


方式3: 消息订阅与发布

想使用这种方式,需要安装:

npm i --save pubsub-js

订阅消息

PubSub.subscribe('msg', function(msg, data){})
发布消息
PubSub.publish('msg', data)

比如:

Add.vue

方式3: 此方式可实现任意关系组件间通信(数据)


方式4: vuex

vuex是vue官方提供的集中式管理vue多组件共享状态数据的vue插件

优点: 对组件间关系没有限制, 且相比于pubsub库管理更集中, 更方便


方式5: slot(插槽)

子组件: Child.vue

父组件: Parent.vue

xxx 对应的标签结构
yyyy 对应的标签结构

方式5: 专门用来实现父向子传递带数据的标签

(注意: 通信的标签模板是在父组件中解析好后再传递给子组件的)

用了这种方式以后,就相当于子组件用到的所有属性或方法全都需要从子组件移动到父组件中,除了样式。平常肯定不推荐这么使用,它用在什么地方呢?这样设计之后,比如可能会多次用到这个子组件(即:很多类似的组件,就没必要建好多个了,直接用这种模板就行了,只要在父组件中设置好slot,就能使用了),这样一来就能满足重用性。


vuex管理状态的机制

详细vuex可以参考我之前的文章:

对Vuex基本理解

  1. 是什么: Vuex 是一个专为 Vue.js 应用程序开发的状态管理的vue插件
  2. 作用: 集中式管理vue多个组件共享的状态和从后台获取的数据

工作原理

在这里插入图片描述

转载地址:http://clyki.baihongyu.com/

你可能感兴趣的文章
POSIX定时器timer_create()以及线程中的gettid() 和pthread_self()
查看>>
c /c++中日期和时间的获取:strftime()函数
查看>>
C语言 回调函数
查看>>
c语言swap(a,b)值交换的4种实现方法
查看>>
c 排序 汇总
查看>>
C 二维数组的动态申请与释放
查看>>
C/C++中产生随机数(rand和srand的用法)
查看>>
c/c++ 中的 struct和typedef struct
查看>>
C++中class类 的 构造函数、析构函数
查看>>
C++小知识点
查看>>
【转载】zedboard中PL_GPIO控制(8个sw、8个leds)
查看>>
zedboard烧写程序到FLASH,用于QSPI Flash启动
查看>>
软件工程师,你必须知道的20个常识
查看>>
常用STL算法2_查找
查看>>
常用STL算法3_排序
查看>>
常用STL算法4_拷贝和替换
查看>>
常用STL算法5_算术和生成
查看>>
常用STL算法6_集合
查看>>
STL综合案例
查看>>
数据结构 的可视化
查看>>