前端面试题集每日一练Day16

325 阅读15分钟

问题先导

  • 水平垂直居中的实现【css布局】
  • 如何根据设计稿进行移动端适配【css布局】
  • 对Flex布局的理解【css布局】
  • escape、encodeURI和encodeURIComponent的区别【js基础】
  • 什么是尾调用【js基础】
  • delete和Vue.delete的区别【Vue】
  • Vue如何监听手动添加的对象或数组的某个属性的变化?【Vue】
  • 什么mixin【Vue】
  • 数组filter的实现【手写代码】
  • 数组map的实现【手写代码】
  • 字符串repeat的实现【手写代码】
  • 字符串翻转的实现【手写代码】
  • 代码输出结果(Promise相关)【代码输出】
  • 螺旋矩阵【算法】

知识梳理

水平垂直居中的实现

水平居中效果图如下:

对于定位类型的题,没太多奇淫技巧,逃不过两种基本方向:

  • 一是使用最基本的定位属性position进行定位。
  • 二是使用结构属性display进行布局。

特殊的一些定位偶尔会使用到float之类的属性,然后盒模型是最基本的知识点,通过调整盒模型数据来调整布局也是常用的手段。

对于水平垂直居中,主要分为两种情况,一是宽高固定,二是宽高不固定。

以下面给出的html代码为例:

<div class="outer">
    <div class="inner">
       I'm the inner box!  
    </div>
</div>

宽高固定

对于宽高固定的元素,最简单的就是使用position: absolute进行绝对定位了,然后设置lefttop属性就能精准定位了。主要有三种不同的实现细节:

position + calc

calc是动态计算函数,利用这个函数就能计算.inner的准确位置:

.outer {
    position: relative;
    margin: 0px auto;
    width: 500px;
    height: 500px;
    background-color: #dedede;
}
.inner {
    width: 200px;
    height: 200px;
    background-color: #9ed0c4;
    margin: 0 auto;
    position: absolute;
    left: calc(50% - 100px);
    top: calc(50% - 100px);
}

position + margin

lefttop50%时,我们还需要向左上各偏移宽高的一半,我们可以将margin设置为负数来实现偏移

.outer {
    position: relative;
    margin: 0px auto;
    width: 500px;
    height: 500px;
    background-color: #dedede;
}
.inner {
    width: 200px;
    height: 200px;
    background-color: #9ed0c4;
    position: absolute;
    left: 50%;
    top: 50%;
    margin-left: -100px;
    margin-top: -100px;
}

宽高不固定

当宽高不固定时,我们就不能准确写死marginleft等属性的值了,对于这种动态变化的图元,我们需要编写一定的规则,也就是计算公式,让css自己去算。一般常见的就是百分比的写法。

position + margin: auto

我们常用margin: 0 auto;来居中元素,对于垂直方向来说同样适用,我们只需要把topbottomleftright均设置为0即可,css会自动计算并垂直居中。

.outer {
    position: relative;
    margin: 0px auto;
    width: 500px;
    height: 500px;
    background-color: #dedede;
}
.inner {
    width: 200px;
    height: 200px;
    background-color: #9ed0c4;
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    margin: auto;
}

margin: auto会自动计算平分上下左右外边距,就达到了垂直居中的效果。

position + transform

上面我们用到了margin来偏移图元,实际上,这不是真正的偏移,只是调整盒模型大小让其位置改变。而transform才是真正的元素偏移属性。

.outer {
    position: relative;
    margin: 0px auto;
    width: 500px;
    height: 500px;
    background-color: #dedede;
}
.inner {
    width: 200px;
    height: 200px;
    background-color: #9ed0c4;
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
}

inline-height + text-align + vertical-align

我们都知道text-align可以让行内元素水平对齐,使用vertical-align可以让自身垂直对齐。但这仅限于行内元素才会生效,而且是根据行高即line-height来对齐。所以将.inner设置为inner-block,把inline-height占满整个外框,然后就能通过text-alignvertical-align进行水平垂直对齐了。

.outer {
    margin: 0px auto;
    width: 500px;
    height: 500px;
    background-color: #dedede;
    line-height: 500px;
    text-align: center;
}
.inner {
    width: 200px;
    height: 200px;
    background-color: #9ed0c4;
    display: inline-block;
    text-align: left;
    vertical-align: middle;
    line-height: 100%;
}

writing-mode

text-alignvertical-align能让文本对齐一样,write-mode能让文本在块级元素中水平或垂直排布

更多细节可参考:writing-mode

需要把html结构稍作修改,利用两个writing-mode属性来控制:.inner-box垂直居中,.inner水平居中。

<div class="outer">
    <div class="inner-box">
        <div class="inner">
            I'm the inner box! 
         </div>
    </div>
</div>
.outer {
    margin: 0px auto;
    width: 500px;
    height: 500px;
    background-color: #dedede;
    text-align: center;
    writing-mode: vertical-lr;
}
.inner-box {
    writing-mode: horizontal-tb;
    display: inline-block;
    text-align: center;
    width: 100%;
}
.inner {
    width: 200px;
    height: 200px;
    background-color: #9ed0c4;
    display: inline-block;
    text-align: left;
}

flex布局

百搭的弹性流式布局,大多数布局都能通过flex实现,而且结构清晰容易控制。

.outer {
    margin: 0px auto;
    width: 500px;
    height: 500px;
    background-color: #dedede;
    display: flex;
    justify-content: center;
    align-items: center;
}
.inner {
    width: 200px;
    height: 200px;
    background-color: #9ed0c4;
}

grid布局

grid布局是二维布局,能够处理更为细致的布局。

.outer {
    margin: 0px auto;
    width: 500px;
    height: 500px;
    background-color: #dedede;
    display: grid;
    justify-content: center;
    align-items: center;
}
.inner {
    width: 200px;
    height: 200px;
    background-color: #9ed0c4;
}

其他布局方案,但不推荐:

  • display: table-cell;
  • <table>元素

参考:

如何根据设计稿进行移动端适配?

移动端适配主要有两个维度:

  • 适配不同像素密度, 针对不同的像素密度,使用 CSS 媒体查询,选择不同精度的图片,以保证图片不会失真;
  • 适配不同屏幕大小, 由于不同的屏幕有着不同的逻辑像素大小,所以如果直接使用 px 作为开发单位,会使得开发的页面在某一款手机上可以准确显示,但是在另一款手机上就会失真。为了适配不同屏幕的大小,应按照比例来还原设计稿的内容。

为了能让页面的尺寸自适应,可以使用 rem,em,vw,vh 等相对单位。也可以使用flexgrid弹性布局来自适应。

对Flex布局的理解及其使用场景

Flex是FlexibleBox的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性。任何一个容器都可以指定为Flex布局。行内元素也可以使用Flex布局。注意,设为Flex布局以后,子元素的float、clear和vertical-align属性将失效。采用Flex布局的元素,称为Flex容器(flex container),简称"容器"。它的所有子元素自动成为容器成员,称为Flex项目(flex item),简称"项目"。容器默认存在两根轴:水平的主轴(main axis)和垂直的交叉轴(cross axis),项目默认沿水平主轴排列。

布局属性分为容器属性和内部项目属性。

以下6个属性设置在容器上

  • flex-direction属性决定主轴的方向(即项目的排列方向)。
  • flex-wrap属性定义,如果一条轴线排不下,如何换行。
  • flex-flow属性是flex-direction属性和flex-wrap属性的简写形式,默认值为row nowrap。
  • justify-content属性定义了项目在主轴上的对齐方式。
  • align-items属性定义项目在交叉轴上如何对齐。
  • align-content属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。

以下6个属性设置在项目上

  • order属性定义项目的排列顺序。数值越小,排列越靠前,默认为0。
  • flex-grow属性定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。
  • flex-shrink属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。
  • flex-basis属性定义了在分配多余空间之前,项目占据的主轴空间。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小。
  • flex属性是flex-grow,flex-shrink和flex-basis的简写,默认值为0 1 auto。
  • align-self属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性。默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch。

简单来说: flex布局是CSS3新增的一种布局方式,可以通过将一个元素的display属性值设置为flex从而使它成为一个flex容器,它的所有子元素都会成为它的项目。一个容器默认有两条轴:一个是水平的主轴,一个是与主轴垂直的交叉轴。可以使用flex-direction来指定主轴的方向。可以使用justify-content来指定元素在主轴上的排列方式,使用align-items来指定元素在交叉轴上的排列方式。还可以使用flex-wrap来规定当一行排列不下时的换行方式。对于容器中的项目,可以使用order属性来指定项目的排列顺序,还可以使用flex-grow来指定当排列空间有剩余的时候,项目的放大比例,还可以使用flex-shrink来指定当排列空间不足时,项目的缩小比例。

参考:

escape、encodeURI、encodeURIComponent 的区别

  • escape:已废弃。废弃的 escape() 方法将指定字符串转义为十六进制的新字符串。 使用encodeURIencodeURIComponent代替。
  • encodeURI :返回 一个新字符串, 表示提供的字符串编码为统一资源标识符 (URI)。
  • encodeURIComponent:原字串作为URI组成部分被被编码后的新字符串。对 URI 的组成部分进行转义,所以一些特殊字符也会得到转义。

总结就是三个方法都会将字符串通过十六进制编码为合法的统一资源标识符,但encodeURI是对整个 URI 进行转义,将 URI 中的非法字符转换为合法字符,所以对于一些在 URI 中有特殊意义的字符不会进行转义,比如&+=等等,而这些字符在XMLHTTPRequest请求中是特殊字符,encodeURI就无法使用了,这时候就可以换用用可以对特殊字符编码的encodeURIComponent

js为什么要进行变量提升?有什么好处?

js变量提升就是指,在同一个上下文作用域中,无论变量在何处声明,都好像被提到了头部,代码运行时就不会报未初定义的错误。

造成变量声明提升的本质原因是 js 引擎在代码执行前有一个解析的过程,创建了执行上下文,初始化了一些代码执行时需要用到的对象。当访问一个变量时,会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。

JS在拿到一个变量或者一个函数的时候,会有两步操作,即解析和执行。

  • 在解析阶段,JS会检查语法,并对函数进行预编译。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。
    • 全局上下文:变量定义,函数声明
    • 函数上下文:变量定义,函数声明,this,arguments
  • 在执行阶段,就是按照代码的顺序依次执行。

这样做的好处就是提高执行性能,代码会进行语法检测和预编译,可以提前找到错误,而且这个解析只会执行一次,下次再调用就不会再重新检查和编译,节约性能。由于变量提升的特性,容错也更高了。

es6提出的两个变量声明关键字letconst都是块作用域变量,都不支持变量提升,变量提升带来了容错,也容易造成变量滥用出错,这也是我们不推荐使用var而改用constlet的原因。

什么是尾调用(tail call)?

尾调用指的是函数的最后一步调用另一个函数。代码执行是基于执行栈的,所以当在一个函数里调用另一个函数时,会保留当前的执行上下文,然后再新建另外一个执行上下文加入栈中。使用尾调用的话,因为已经是函数的最后一步,所以这时可以不必再保留当前的执行上下文,从而节省了内存,这就是尾调用优化。但是 ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

参考:

delete和Vue.delete删除数组的区别

  • delete 只是被删除的元素变成了 empty/undefined 其他的元素的键值还是不变。
  • Vue.delete 直接删除了数组 改变了数组的键值,相当于Array.splice(i, 1)

vue如何监听对象或者数组某个属性的变化

当在项目中直接设置数组的某一项的值,或者直接设置对象的某个属性值,这个时候,你会发现页面并没有更新。这是因为Object.defineProperty()限制,监听不到变化。

解决方式:

  • this.$set

    this.$set(this.arr, 0, "OBKoro1"); // 改变数组this.$set(this.obj, "c", "OBKoro1"); // 改变对象
    
  • 对于数组,可以通过调用一下方法来更新属性:

    splice()
    push()
    pop()
    shift()
    unshift()
    sort()
    reverse()
    

什么是 mixin ?

mixins 选项接收一个混入对象的数组。这些混入对象可以像正常的实例对象一样包含实例选项,这些选项将会被合并到最终的选项中,使用的是和 Vue.extend() 一样的选项合并逻辑。也就是说,如果你的混入包含一个 created 钩子,而创建组件本身也有一个,那么两个函数都会被调用。

var mixin = {
  created: function () { console.log(1) }
}
var vm = new Vue({
  created: function () { console.log(2) },
  mixins: [mixin]

参考:

数组filter的实现?

很简单,就是过滤数组:

Array.prototype.filter = function(call) {
  const res = [];
  this.forEach(item => {
    call && call(item) && res.push(item);
  });
  return res;
}

数组map的实现?

同样很简单,就是将返回值作为新数组的元素:

Array.prototype.filter = function(call) {
  const res = [];
  this.forEach(item => {
    res.push(call ? call(item) : undefined);
  });
  return res;
}

字符串repeat的实现

输入字符串s,以及其重复的次数,输出重复的结果。循环叠加即可。

利用数组转字符串来实现:

function repeat(s, n) {
    return (new Array(n + 1)).join(s);
}

递归调用:

function repeat(s, n) {
    return (n > 0) ? s.concat(repeat(s, --n)) : "";
}

字符串翻转的实现?

可以利用数组的的翻转实现:

String.prototype._reverse = function(a){
    return a.split("").reverse().join("");
}

也可以遍历字符串,然后倒叙输出即可:

String.prototype.revese = function(){
  let s = '';
  for(let i = this.length - 1; i >= 0; i--) {
    s += this[i];
  }
  return s;
}

代码输出结果(Promise相关)

代码片段:

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(resolve => {
    console.log("promise1");
    resolve();
    }).then(function() {
    console.log("promise2");
    });
    console.log('script end')

执行逻辑分析:


async function async1() {
    console.log("async1 start"); // 4.打印async1 start
    await async2(); // 5.执行
    console.log("async1 end"); // 7.进入异步微任务 // 12.打印async1 end
}

async function async2() {
    console.log("async2"); // 6.打印async2
}

console.log("script start"); // 1.打印script start

setTimeout(function() {
    console.log("setTimeout"); // 2.进入异步宏任务队列 // 14.打印setTimeout
}, 0);

async1();   // 3.执行

// 8.执行
new Promise(resolve => {
    console.log("promise1"); // 9.打印promise1
    resolve();
}).then(function() {
    console.log("promise2"); // 10.进入异步微任务 // 13.打印promise2,微任务执行结束,执行队列中的宏任务(2)
});
console.log('script end') // 11.打印script end,宏任务执行结束,开始执行队列中的微任务(7,9,10)

打印结果:

script start
async1 start
async2      
promise1    
script end
async1 end
promise2
setTimeout

代码片段:

async function async1 () {
        await async2();
        console.log('async1');
        return 'async1 success'
    }
async function async2 () {
    return new Promise((resolve, reject) => {
        console.log('async2')
        reject('error')
    })
}
async1().then(res => console.log(res))

执行逻辑分析:

async function async1 () {
    await async2(); // 2.执行
    // 由于4返回已拒绝状态的Promise,后续代码不被执行,由于未捕获拒绝状态,还会打印 Uncaught (in promise) error的信息
    console.log('async1');
    return 'async1 success'
}
async function async2 () {
    return new Promise((resolve, reject) => {
        console.log('async2') // 3.打印 async2
        reject('error') // 4.更新Promise状态
    })
}
// 1.执行
async1().then(res => console.log(res))

打印结果:

async2
Uncaught (in promise) error...

螺旋矩阵

给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

比如:matrix = [[1,2,3],[4,5,6],[7,8,9]]

输出为:[1,2,3,6,9,8,7,4,5]。

我们发现,所谓的螺旋走法就是一层一层往里走,最外面一层走完再用同样的方法走下一层,直到走完为止。我们如何模拟这一层一层的矩形框呢,我们需要用到三个参数(为了直观,也可以使用四个参数):那就是矩形框的四个顶点坐标。我们分别记作:lt(左上),rt(右上)和rb(右下)。或者记录其中一个点,然后记录矩形宽高也可以,目的都是为了算出四个点的坐标范围,然后每遍历一层就更新一下这个矩形的范围,直到矩形宽高均为0即可结束遍历。

var spiralOrder = function(matrix) {
    if (!matrix.length || !matrix[0].length) {
        return [];
    }

    const rows = matrix.length, columns = matrix[0].length;
    const order = [];
    let left = 0, right = columns - 1, top = 0, bottom = rows - 1;
    while (left <= right && top <= bottom) {
        for (let column = left; column <= right; column++) {
            order.push(matrix[top][column]);
        }
        for (let row = top + 1; row <= bottom; row++) {
            order.push(matrix[row][right]);
        }
        if (left < right && top < bottom) {
            for (let column = right - 1; column > left; column--) {
                order.push(matrix[bottom][column]);
            }
            for (let row = bottom; row > top; row--) {
                order.push(matrix[row][left]);
            }
        }
        [left, right, top, bottom] = [left + 1, right - 1, top + 1, bottom - 1];
    }
    return order;
};