前端面试题集每日一练Day12

534 阅读12分钟

问题先导

  • head标签有什么用?其中有哪个标签必不可少?【html】
  • 对媒体查询的理解【css】
  • 对css工程化的理解【css】
  • 说一说Reflect对象【js基础】
  • 说一说Generator对象【js基础】
  • 说一说单页应用和多页应用的区别【Vue】
  • Vue template到render的过程【Vue】
  • Vue实例中data某一属性值更新后,视图会立即同步执行渲染吗【Vue】
  • 手写浅拷贝【手写代码】
  • 手写深拷贝【手写代码】
  • 输出结果(Promise相关)【输出结果】
  • 环形列表【算法】

知识梳理

head标签有什么用,其中什么标签必不可少?

HTML head 元素 规定文档相关的配置信息(元数据),包括文档的标题,引用的文档样式和脚本等。

对媒体查询的理解?

媒体查询Media queries)是css3引入的一种方法,仅在设备环境与指定的媒体规则匹配时css才会生效,以此针对不同类型的设备来编写不同的css样式,达到响应式的效果。

最简单的媒体查询看来是这样的:

@media media-type and (media-feature-rule) {
  /* CSS rules go here */
}

除了@media的写法,还支持使用media= 属性为stylelinksource等元素指定特定的媒体类型。

<link rel="stylesheet" media="(max-width: 800px)" href="example.css" /> 

它由以下部分组成:

  • 一个媒体类型,告诉浏览器这段代码是用在什么类型的媒体上的(例如印刷品或者屏幕);目前可选的媒体类型有:
    • all(所有设备)
    • print(打印预览模式下在屏幕上查看的分页材料和文档)
    • screen(主要用于屏幕)
    • speech(主要用于语音合成器)
  • 一个媒体表达式,是一个被包含的CSS生效所需的规则或者测试。比如width为视窗(viewport)的宽度,包括纵向滚动条的宽度。
  • 一组CSS规则,会在测试通过且媒体类型正确的时候应用。

注:弹性盒、网格和多栏布局都给了你建立可伸缩的甚至是响应式组件的方式,而不需要媒体查询。这些布局方式能否在不加入媒体查询的时候实现你想要的设计,总是值得考虑的一件事。

参考:

对css工程化的理解?

和js工程化一样,当css规模达到一定程度后,css工程化也是很有必要的,css的组织、设计以及优化等问题就需要更专业的库来帮助我们。

一般来说,CSS 工程化是为了解决以下问题:

  1. 宏观设计:css代码如何组织、如何拆分、模块结构怎样设计?
  2. 编程优化:让css规则更优
  3. 自动化构建:自动打包
  4. 可维护性

一下三个方向都是比较流行的css工程化实践:

  • 预处理器:less、sass等等。利用css预处理器自己独特设计的语法,让css的编辑更有逻辑性、更易读,这解决了css工程化宏观设计的问题。

    预处理器普遍会具备这样的特性:

    • 嵌套代码的能力,通过嵌套来反映不同 css 属性之间的层级关系 ;
    • 支持定义 css 变量;
    • 提供计算函数;
    • 允许对代码片段进行 extend 和 mixin;
    • 支持循环语句的使用;
    • 支持将 CSS 文件模块化,实现复用。
  • 后处理器:PostCss等,根据css规则对生成是css进行再编译整理,让其更优,目前最常做的是给css属性添加浏览器私有前缀,实现跨浏览器兼容性的问题,有点使用babel.js来转换js的那种感觉。

  • webpack loader等加载器:webpack 的css-loader和style-loader能将css像插件一样动态加载到页面,解决了自动打包的问题

说一说Reflect对象

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers的方法相同。Reflect不是一个函数对象,因此它是不可构造的。

reflect是反射的意思,我们知道Proxy可以实现对象的代理,也能拦截对象的基本操作。实际上,这个对象没什么特别之处,我们完全可以把它看作是一个静态工具类,一个收集了对象拦截操作的工具类。

比如:

  • Reflect.apply等同于Function.proptype.apply
  • Reflect.constuctor等同于new操作符

等等。

说一说Generator对象

生成器对象是由一个 generator function 返回的,并且它符合可迭代协议迭代器协议

生成器对象是生成器函数的返回值,使用带*的函数就是生成器函数,搭配yield操作符就可以生成固定次序的数据:

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

let g = gen(); // "Generator { }"

比如一个无限迭代器的示例:

function* idMaker(){
    let index = 0;
    while(true)
        yield index++;
}

let gen = idMaker(); // "Generator { }"

console.log(gen.next().value);
// 0
console.log(gen.next().value);
// 1
console.log(gen.next().value);
// 2
// ...

生成器对象是async/await语法糖的基础。更多相关说明可阅读:async和await:让异步编程更简单

Vue单页应用和多页应用的区别

  • SPA单页面应用(SinglePage Web Application),指只有一个主页面的应用,一开始只需要加载一次js、css等相关资源。所有内容都包含在主页面,对每一个功能模块组件化。单页应用跳转,就是切换相关组件,仅仅刷新局部资源。

  • MPA多页面应用 (MultiPage Application),指有多个独立页面的应用,每个页面必须重复加载js、css等相关资源。多页应用跳转,需要整页资源刷新。

对比项多页应用模式MPA单页应用模式SPA
应用构成由多个完整页面构成一个外壳页面和多个页面片段构成
跳转方式页面之间的跳转是从一个页面跳转到另一个页面页面片段之间的跳转是把一个页面片段删除或隐藏,加载另一个页面片段并显示出来。这是片段之间的模拟跳转,并没有开壳页面
跳转后公共资源是否重新加载
URL模式http://xxx/page1.html http://xxx/page1.htmlhttp://xxx/shell.html#page1 http://xxx/shell.html#page2
用户体验页面间切换加载慢,不流畅,用户体验差,特别是在移动设备上页面片段间的切换快,用户体验好,包括在移动设备上
能否实现转场动画无法实现容易实现(手机app动效)
页面间传递数据依赖URL、cookie或者localstorage,实现麻烦因为在一个页面内,页面间传递数据很容易实现
搜索引擎优化(SEO)可以直接做需要单独方案做,有点麻烦
特别适用的范围需要对搜索引擎友好的网站对体验要求高的应用,特别是移动应用
搜索引擎优化(SEO)可以直接做需要单独方案做,有点麻烦
开发难度低一些,框架选择容易高一些,需要专门的框架来降低这种模式的开发难度

更多细节可参考:

Vue tempalte到render的过程

简单来说,Vue最核心的三部分,即:compiler、reactivity、runtime。

  • compiler(编译):表示template编译成有规律的数据结构,即AST抽象语法树。

  • reactivity(数据响应):表示data数据可以被监控,通过Object.defineProperty或proxy语法来实现。

  • runtime(运行时)表示运行时相关功能,虚拟DOM(即:VNode)、diff算法、真实DOM操作等。

其中template说的就是编译模板的逻辑,render就是渲染视图的逻辑,渲染一般使用render function,我们可以手动编写,就可以引用runtime运行时部分的代码,节省了template编译为render function的过程,而这部分代码也占据很大一部分内存。

编译器compiler主要编译过程如下:

  1. 将template模板转换为ast(抽象语法树):ast可以更有逻辑地存储模板信息,便于后续整理操作
  2. 通过optimize函数对静态节点做优化,给其打一个标记,为后续更新渲染可以直接跳过静态节点做优化。静态节点的意思就是这部分DOM不是响应式的,不会更新,因此不需要监听和检测,以此来优化更新效率。
  3. 生成render字符串:通过generate函数生成render字符串并将静态部分放到 staticRenderFns 中,最后通过 new Function(render) 生成render函数。

备注:render函数就是生成虚拟节点的函数,而虚拟节点是对频繁更新DOM有很好优化的一种结构,而AST是compiler中把模板编译成有规律的数据结构,方便转换成render函数所存在的;虚拟节点VNode是优化DOM操作的,减少频繁DOM操作的,提升DOM性能的。

Vue实例data中的某一个属性值发生改变后,视图会立即同步执行重新渲染吗?

不会立即同步执行重新渲染。Vue实现响应式更新但并不是立即更新DOM,而是异步更新的,且按照一定策略进行更新,已达到最优渲染:**Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。**这对某些频繁操作比如watcher的数据监听是有益的,当短时间内多次触发同一个watcher,这些变化会在缓冲队列中去重,避免不必要的计算,和js里常用的防抖和节流是一样的优化思路。

实现浅拷贝

一些现成的API就可以实现浅拷贝:

  • Object.assign()
  • Object.create()也是浅拷贝,原理同new关键字一致
  • 扩展运算符,原理同Object.assign
  • 数组的slice等方法也是浅拷贝

手写浅拷贝:

function shallowCopy(obj) {
    if(!obj || typeof obj != 'object') {
        return {};
    }
    let newObject = Array.isArray(obj) ? [] : {};
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObject[key] = obj[key];
        }
    }
    return newObject;
}

浅拷贝就是直接复用原引用类型的引用,而不是新建一份数据,因此原数据改变,会导致所有引用的地方发生变化,这就是浅拷贝。

实现深拷贝

  • 使用JSON.parse(JSON.stringify(obj))是最常见的深拷贝方法之一,原理就是利用json的序列化和反序列化来创建一个新的对象。但是这个方法存在一些限制,就是json不支持序列化的对象就会消失,比如函数、自定义的一些对象等等。
  • 使用lodash库的cloneDeep方法

下面是手写实现深拷贝的实现:

function deepCopy(obj) {
    if(!obj || typeof obj != 'object') {
        return {};
    }
    let newObject = Array.isArray(obj) ? [] : {};
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObject[key] = obj[key] && typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
        }
    }
    return newObject;
}

实际上,和浅拷贝的区别就在于遇到对象时再展开拷贝,而不是直接赋值引用,这样就能实现深拷贝了。

这里函数我们也是直接赋值的,应该对于函数这种类型来说一般我们不会去修改它。

输出结果(Promise相关)

代码片段:

function runAsync (x) {
    const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
    return p
}

Promise.all([runAsync(1), runAsync(2), runAsync(3)]).then(res => console.log(res))

本题考查Promise.all的用法。Promise.all函数会并行执行所以异步函数,如果都成功则输出同参数迭代顺序一致的包含所有Promise的状态值的数组,如果有一个失败就直接返回这个失败的Promise

所以输出结果很明显:

1
2
3
[1, 2, 3]

代码片段:

function runAsync (x) {
  const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
  return p
}
function runReject (x) {
  const p = new Promise((res, rej) => setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x))
  return p
}
Promise.all([runAsync(1), runReject(4), runAsync(3), runReject(2)])
       .then(res => console.log(res))
       .catch(err => console.log(err))

由于是并行执行,所以异步执行时间决定了哪个异步先执行完毕。

  1. runAsync(1)执行,1s后触发settimeout异步回调,打印1,状态为成功
  2. runReject(4)执行,4s后触发settimeout异步回调,打印4,状态失败
  3. runAsync(31)执行,1s后触发settimeout异步回调,打印3,状态为成功
  4. runReject(2)执行,2s后触发settimeout异步回调,打印2,状态失败,Promise.all捕获到错误,异步执行catch回调

注意回调触发的先后顺序:

// 1s
1
3
// 2s
2
Error: 2
// 4s
4

环形链表

给定一个链表,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

本题十分简单,就是遍历链表,判断有没有链表指向了遍历过的节点即可。

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
    const node_caches = [];
    let next = head;
    while(next) {
        if(node_caches.includes(next)) {
            return true;
        }
        node_caches.push(next);
        next = next.next;
    }
    return false;
};

这种思路对于环靠前的比较有优势,遇到就可以结束循环,但如果换位置比较靠后,节点的缓存空间无疑越来越大。

本题还有一个特别的思路,就是设置快慢指针,我们让慢指针每次前进一步,而快指针前进两步甚至更多步,这样,如果遇到环,快慢指针都会开始“绕圈”,由于快指针比慢指针快,总会追上慢指针,如果遍历结束快指针都没有与慢指针相遇,说明没有环。

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
    let slow = head, quick = head && head.next;
    while(quick) {
        if(slow == quick || slow == quick.next) {
            return true;
        }
        slow = slow.next;
        quick = quick.next && quick.next.next;
    }
    return false;
};