前端面试题集每日一练Day17

问题先导

  • grid布局的理解和使用【CSS】
  • for infor of的区别【js基础】
  • ajaxaxiosfetch的区别【js基础】
  • 数组遍历有哪些方法?有什么区别?【js基础】
  • template和jsx有什么区别【Vue】
  • Vue初始化页面闪烁的问题如何解决【Vue】
  • Vue.extend有什么作用【Vue】
  • 将数字每千分位使用逗号隔开【手写代码】
  • 实现非负大整数的相加【手写代码】
  • 实现add(1)(2)(3)与add(1, 2)(3)的结果相等【手写代码】
  • 代码输出(Promise相关)【输出结果】

知识梳理

grid布局的理解和使用

grid布局又称网格布局,是一种二维布局,使用网格布局的元素就像像表格一样,网格布局让我们能够按行或列来对齐元素。 然而在布局上,网格比表格更可能做到或更简单。 例如,网格容器的子元素可以自己定位,以便它们像CSS定位的元素一样,真正的有重叠和层次。

使用网格布局,我们需要将元素的display设置为gridinline-grid

网格布局的一些基本的概念:

轨道(行列)

网格就像一个表格,有行和列划分出多个网格单元格,一行或者一列就可以称为网格的一个轨道。使用grid-template-columnsgrid-template-rows来分别设置行列的布局情况。值有多种形式:

  • 像素或百分比:如grid-template-columns: 100px 20% 20% auto;表示划分为四列的网格布局。
  • 按比例布局:使用特殊单位fr,可以用于表示单元格的比例,如100px 1fr 2fr表示该网格划分为三列,第一列占100px宽度,剩下列两列按照1:2的比例平分。
  • 重复函数repeatrepeat(3, 1fr 2fr)表示重复1fr 2fr三次。
  • 长度边界:有时候我们希望宽度有最小或最大值,就可以使用minmax来设置,比如minmax(100px, auto)表示该行或列最小长度为100px,而最大值会根据内容自动扩展。
  • 弹性布局:有时候我们不能确定列数,就可以使用auto-fill来设置动态盒模型:repeat(auto-fill, minmax(100px, auto)),这样,盒子就能根据整体长度自适应变化,来调整列数。

网格线

当我们设计好了网格的行列值,就可以再具体划分网格区域,这就需要用到网格线。

应该注意的是,当我们定义网格时,我们定义的是网格轨道,而不是网格线。Grid 会为我们创建编号的网格线来让我们来定位每一个网格元素. 例如下面这个三列两行的网格中,就拥有四条纵向的网格线。

1_diagram_numbered_grid_lines.png

默认每个子元素占据一个网格单元,如果需要某个子元素占据多个单元,就可以用grid-column-startgrid-column-endgrid-row-startgrid-row-end四个属性来控制该单元格被哪些网格线所包围。

值得注意的是,我们还能让两个单元格重叠,共用一部分网络单元格,这时候会出现遮挡叠加效果,我们可以使用z-index来控制单元格的层级。

网格间距

使用grid-column-gapgrid-row-gap来设置行列间距。

其他

到这里,网格布局的基本使用已经差不多了,还有一些细节可通过其他属性来控制。常用的有:

  • justify-content:设置单元格水平对齐方式,类似flex布局的justify-content,属性名一样,但可选值不一样。
  • align-content:设置单元格的垂直对齐方式,类似flex布局的align-items,同样可选值也有差异,但值得注意的是,flex也有align-content属性,定义的是多根轴线的对齐方式。
  • place-content:上面两个属性的总和就是place-content
  • grid-auto-flow:设置网格流动方向,默认是先行后列,也就是从左往右再从上往下,我们也可以改为column让流动方向变成先列后行,这和flex中的flex-direction类似,此外flex还多了一个flex-wrap来控制换行的方式,两个属性联合就是flex-flow属性了。

还有一些别的属性说明可以参考:CSS Grid 网格布局教程

参考:

for in 和 for of 的区别

for…of 是ES6新增的遍历方式,允许遍历一个含有iterator可迭代接口的数据结构(数组、对象等)并且返回各项的值,和ES3中的for…in的区别如下:

  • for…of 遍历获取的是对象的键值,for…in 获取的是对象的键名
  • for… in 会遍历对象的整个原型链,性能非常差不推荐使用,而 for … of 只遍历当前对象不会遍历原型链。

ajax、axios、fetch的区别

老生常谈的ajax,05年之前本身并不是一种新技术,而是作为一种新术语出现用来描述一种使用现有技术集合的‘新’方法,包括: HTMLXHTML, CSS, JavaScript, DOM, XML, XSLT, 以及最重要的 XMLHttpRequest。AJAX模型以后, 网页应用能够快速地将增量更新呈现在用户界面上,而不需要重载(刷新)整个页面。这使得程序能够更快地回应用户的操作。

ajax一直没有规范的封装体,类似jquery ajax封装地已经十分便捷好用了,但还是有一些缺陷:

  • 本身是针对MVC编程,不符合前端MVVM的浪潮
  • 基于原生XHR开发,XHR本身的架构不清晰
  • 不符合关注分离(Separation of Concerns)的原则
  • 配置和调用方式非常混乱,而且基于事件的异步模型不友好。

而ea6提出的Fetch的问世正是为了提供一个获取资源的接口(包括跨域请求),提供了更强大和灵活的功能集。但目前大多数浏览器仍处于实验阶段,应用程度不高。

fetch带来了以下好处:

  • 语法简洁,更加语义化
  • 基于标准 Promise 实现,支持 async/await
  • 更加底层,提供的API丰富(request, response)
  • 脱离了XHR,是ES规范里新的实现方式

Axios是一个js库,是基于Promise封装的HTTP客户端,同时支持浏览器端和Node端使用,符合最新的ES规范,封装了一些常规的请求和响应API,是jquery ajax的替代品,底层仍然是XMLHttpRequest对象。官网地址:www.axios-js.com/

总结来说就是,ajax这种技术合集,由于XMLHttpRequest本身不太友好的设计,让网络请求的发起和数据处理比较繁琐,之后出现的Juqery Ajax封装了较为实用的版本,但由于jquery逐渐退出历史舞台和Promise的出现,出现了类似Axios这样的HTTP请求库,让异步请求的发起更加方便,而Fetch是ES6官方提出的一种ajax封装API,同样基于Promise,但摒弃了XMLHttpRequest对象,实用更新的规范,语法也更加简洁,目前的缺点就是大多数浏览器还处于试验阶段,大都还不支持Fetch API

参考:

数组有哪些遍历方法,有什么区别

方法 改变原数组 特点
forEach() 不改变原数组,没有返回值
map() 不改变原数组,通过遍历返回值生成新的数组
filter() 过滤数组,返回包含符合条件的元素的数组
for…of for…of遍历具有Iterator迭代器的对象的属性,返回的是数组的元素、对象的属性值,不能遍历普通的obj对象,将异步循环变成同步循环
every() 和 some() 数组元素检测方法,some()只要有一个是true,便返回true;而every()只要有一个是false,便返回false.
find() 和 findIndex() 数组元素查找方法,find()返回的是第一个符合条件的值;findIndex()返回的是第一个返回条件的值的索引值
reduce() 和 reduceRight() 接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。reduce()对数组正序操作;reduceRight()对数组逆序操作

template和jsx的有什么分别

对于 runtime 来说,只需要保证组件存在 render 函数即可,而有了预编译之后,只需要保证构建过程中生成 render 函数就可以。在 webpack 中,使用vue-loader编译.vue文件,内部依赖的vue-template-compiler模块,在 webpack 构建过程中,将template预编译成 render 函数。与 react 类似,在添加了jsx的语法糖解析器babel-plugin-transform-vue-jsx之后,就可以直接手写render函数。

所以,template和jsx的都是render的一种表现形式,不同的是:JSX相对于template而言,具有更高的灵活性,在复杂的组件中,更具有优势,而 template 虽然显得有些呆滞。但是 template 在代码结构上更符合视图与逻辑分离的习惯,更简单、更直观、更好维护。

vue初始化页面闪动现象如何解决

使用vue开发时,在vue初始化之前,由于div是不归vue管的,所以我们写的代码在还没有解析的情况下会容易出现花屏现象,看到类似于{{message}}的字样,虽然一般情况下这个时间很短暂,但是还是有必要让解决这个问题的。

我们可以在css中加入隐藏元素的代码,让其在初始化之前处于隐藏状态:

[v-cloak] {    display: none;}
复制代码

如果没有彻底解决问题,则在根元素加上style="display: none;" :style="{display: 'block'}"

extend有什么作用

这个 API 很少用到,作用是扩展组件生成一个构造器,通常会与 $mount 一起使用。使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。data 选项是特例,需要注意 – 在 Vue.extend() 中它必须是函数。

// 创建构造器
var Profile = Vue.extend({
  template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
  data: function () {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'Heisenberg'
    }
  }
})
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#mount-point')
复制代码

将数字每千分位用逗号隔开

很简单的一题,有多种实现思路,这里提供其中一种:

/**
 * 将数字以某个分隔符分割成若干片段
 * @param {number} number 数字
 * @param {number} sliceLen 片段长度
 * @param {string} split 分割符
 * @returns string 
 */
function numberFormat(number, sliceLen, split) {
    if(sliceLen <= 0 || isNaN(sliceLen))  {
        sliceLen = 1;
    }
    const numberStr = number.toString();
    const len = numberStr.length;
    if(len <= sliceLen) {
        return numberStr;
    }
    if(typeof split !== 'string') {
        split = ',';
    }
    let str = '', count = 0;
    for(let i = 0; i < len; i++) {
        str += numberStr[i];
        if(count === sliceLen - 1 && i != len - 1) {
            str += split;
            count = 0;
        } else {
            count++;
        }
    }
    return str;
}
复制代码

实现非负大整数相加

我们知道由于存储位置的限制,整数和小数都是有安全范围的,当整数超过最大安全值Number.MAX_SAFE_INTEGER(2^52 – 1),或小数小于最小值Number.EPSILON(2 ^ -52)时,数字之间的计算就不准确了。

因此ES6提出了大整数BigInt用于存储这些超过范围的整数,进而实现大整数的计算。当然本题为了实现大整数相加,是不借用BigInt对象的。方法就是模拟加法的计算规则,从个位开始相加,不断进位再相加,最终得到答案。

/**
 * 非负大整数相加
 * @param {string} n1Str 
 * @param {string} n2Str 
 */
function addBignumber(n1Str, n2Str) {
  const len1 = n1Str.length;
  const len2 = n2Str.length;
  let i = 1; // 1表示个位
  let temp = 0; // 进位值
  let res = '';
  while(!((i > len1 || i > len2) && temp == 0)) {
    const a = +(n1Str[len1 - i] || 0);
    const b = +(n2Str[len2 - i] || 0);
    const sum = (a + b + temp).toString();
    res = sum.slice(-1) + res;
    temp = parseInt(sum.slice(0, sum.length - 1) || '0');
    i++;
  }
  if(i <= len1) {
    res = n1Str.slice(0, len1 - i + 1) + res;
  } else if(i <= len2) {
    res = n2Str.slice(0, len2 - i + 1) + res;
  }
  return res;
}
复制代码

实现add(1)(2)(3)相加结果等于add(1, 2)(3)

本题的目的就是实现一个柯里化函数。这在之前的练习我们已经写过。

/**
 * 柯里化函数
 * @param {Function} fn 
 * @param  {...any} args 
 */
function curry(fn, ...args) {
  if(fn.length <= args.length) {
    return fn.apply(this, args);
  }
  return curry.bind(this, fn, ...args);
}

/**
 * 三数求和
 */
function add(a, b, c) {
  debugger
  return a + b + c;
}

const curryAdd = curry(add);

console.log(curryAdd(1)(2)(3) === curryAdd(1, 2)(3))
复制代码

代码输出(Promise相关)

代码片段:

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
复制代码

本题主要考察process.nextTick的使用,这是node.js中的api。在node中,同浏览器端一样代码处于轮询执行的过程,比如浏览器端就是:宏任务和微任务不断交替执行的过程,期间可能不断会有异步让宏任务和微任务进入任务队列。

node端也是一样,它的事件循环机制分为以下几个阶段

  • 定时器:本阶段执行已经被 setTimeout()setInterval() 的调度回调函数。
  • 待定回调:执行延迟到下一个循环迭代的 I/O 回调。
  • idle, prepare:仅系统内部使用。
  • 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
  • 检测setImmediate() 回调函数在这里执行。
  • 关闭的回调函数:一些关闭的回调函数,如:socket.on('close', ...)

感到费解的请参考nexttick – node.js中的说明,实际上和浏览器端的宏任务微任务无差异。

setImmediate()setTimeout() 很类似,但是基于被调用的时机,他们也有不同表现。

  • setImmediate() 设计为一旦在当前 轮询 阶段完成, 就执行脚本。
  • setTimeout() 在最小阈值(ms 单位)过后运行脚本。

就用户而言,我们有两个类似的调用,但它们的名称令人费解。

  • process.nextTick() 在同一个阶段立即执行。
  • setImmediate() 在事件循环的接下来的迭代或 ‘tick’ 上触发。

实质上,这两个名称应该交换,因为 process.nextTick()setImmediate() 触发得更快,但这是过去遗留问题,因此不太可能改变。如果贸然进行名称交换,将破坏 npm 上的大部分软件包。每天都有更多新的模块在增加,这意味着我们要多等待每一天,则更多潜在破坏会发生。尽管这些名称使人感到困惑,但它们本身名字不会改变。

基于上面API的分析,我们来看具体代码片段的执行流程分析:

console.log('1'); // 1.打印1

// 2.定时器(可以理解为异步宏任务进入队列)
setTimeout(function() {
    // 10.打印2
    console.log('2');
    // 11.加入立即阶段执行回调
    process.nextTick(function() {
        // 15.打印3,执行下一个阶段:微任务
        console.log('3');
    })
    // 12.执行Promise
    new Promise(function(resolve) {
        // 13.打印4
        console.log('4');
        // 14.异步微任务进入队列,执行阶段 nextTick
        resolve();
    }).then(function() {
        // 16.打印5,执行下一个宏任务
        console.log('5')
    })
})
// 3.加入立即阶段执行回调
process.nextTick(function() {
    // 8.打印6。一个轮询执行结束,执行 setImmediate 并开启下一个事件轮询:执行微任务队列
    console.log('6');
})
// 4.执行异步Promise
new Promise(function(resolve) {
    // 5.打印7
    console.log('7');
    // 6.异步微任务进入队列
    resolve();
}).then(function() {
    // 9.打印8,执行宏任务队列
    console.log('8')
})
// 7.定时器(异步宏任务进入队列),一个阶段执行结束,执行 nextTick
setTimeout(function() {
    // 17.打印9
    console.log('9');
    // 18.加入立即阶段执行回调
    process.nextTick(function() {
        // 22.打印10,nextTick执行完毕,开启下一个阶段:执行微任务队列
        console.log('10');
    })
    // 19.执行Promise
    new Promise(function(resolve) {
        // 20.打印11
        console.log('11');
        // 21.异步微任务进入队列,宏任务执行结束,执行阶段回调函数 nextTick
        resolve();
    }).then(function() {
        // 23.打印12
        console.log('12')
    })
})
复制代码

收集打印信息和执行序号,就可以得到输出结果(对于这种很长的代码,执行过的代码可以注释掉以清晰视野):

1
7 
6 
8 
2 
4 
3 
5 
9 
11
10
12
复制代码

参考:

代码片段:


console.log(1)

setTimeout(() => {
  console.log(2)
})

new Promise(resolve =>  {
  console.log(3)
  resolve(4)
}).then(d => console.log(d))

setTimeout(() => {
  console.log(5)
  new Promise(resolve =>  {
    resolve(6)
  }).then(d => console.log(d))
})

setTimeout(() => {
  console.log(7)
})

console.log(8)
复制代码

执行流程分析:

// 1.打印1
console.log(1)

// 2.加入异步宏任务队列01
setTimeout(() => {
  // 10.打印2,执行微任务队列(无),继续执行宏任务队列02
  console.log(2)
})

// 3.执行Promise
new Promise(resolve =>  {
  // 4.打印3
  console.log(3)
  // 5.加入异步微任务队列
  resolve(4)
}).then(d => console.log(d)) // 9.打印4,微任务队列执行结束,执行宏任务队列01

// 6.加入异步宏任务队列02
setTimeout(() => {
  // 11.打印5
  console.log(5)
  // 12.执行Promise
  new Promise(resolve =>  {
    // 13.加入异步微任务队列,宏任务执行结束,执行下一个微任务队列
    resolve(6)
  }).then(d => console.log(d)) // 14.打印6,执行下一个宏任务队列03
})

// 7.加入异步宏任务队列03
setTimeout(() => {
  // 15.打印7
  console.log(7)
})

// 8.打印8,宏任务执行结束,执行微任务队列
console.log(8)
复制代码

打印结果:

1
3
8
4
2
5
6
7
复制代码

代码片段:

setTimeout(() => {
  console.log(1);
}, 0);

new Promise((resolve, reject) => {
  console.log(2);
  resolve(3);
  new Promise(resolve2 => {
    console.log(4);
    resolve2(5);
  }).then((r) => {
    console.log(r);
  });
}).then(r => {
  console.log(r);
});

new Promise(resolve2 => {
  console.log(6);
  resolve2(7);
}).then((r) => {
  console.log(r);
});
复制代码

本题有个小细节,就是Promise内部的Promise回调会先于父Promie进入异步微任务队列,所以最终的输出结果为:

2
4
6
5
3
7
1
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享