前端面试题集每日一练Day14

285 阅读11分钟

问题先导

  • 渐进增强和优雅降级的区别【html】
  • transform有哪些属性?【css】
  • css常用的布局单位有哪些?【css】
  • 类数组对象如何转为数组?【js基础】
  • 数组有哪些原生方法?【js基础】
  • Unicode好UTF-8、UTF-16这些字符编码有什么区别?【js基础】
  • 子组件可以直接修改父组件的数据吗?【Vue】
  • Vue是如何收集依赖的?【Vue】
  • 实现数组的乱序输出【手写代码】
  • 实现数组的扁平化【手写代码】
  • 代码输出结果(Promise相关)【输出结果】
  • N叉树的层次遍历【算法】

知识梳理

渐进增强和优雅降级的区别

渐进增强(Progressive enhancement)是一种设计理念,其核心是为尽可能多的用户提供基本内容和功能,同时进一步为现代化浏览器用户提供最佳体验,运行所有需要的代码。核心是保障基本功能在大多数浏览器都能使用,然后再考虑现代浏览器的用户体验问题。

特性检测通常用于确定浏览器是否可以处理高级内容,而polyfill通常用于使用JavaScript构建缺少的功能。

而**优雅降级(Graceful degradation)**是一种与渐进增强箱单的设计理念,其核心是尝试构建可在最新浏览器中运行的现代网站/应用程序,而作为降级体验,在低版本浏览器中仍然提供必要的内容和功能。也就是说核心是以现代浏览器体验为准,然后再考虑低版本浏览器的体验问题。

同样的,Polyfill可用于使用JavaScript构建缺少的功能,但应尽可能提供样式和布局等功能的可接受替代方案,例如使用CSS级联或HTML回退行为。在处理常见的HTML和CSS问题中可以找到一些很好的例子。

transform有哪些属性?

CSS**transform**属性允许你旋转,缩放,倾斜或平移给定元素。这是通过修改CSS视觉格式化模型的坐标空间来实现的。和普通定位不同,transform使用GPU进行硬件加速,也是专门用于做动画效果的属性,而且不会触发文档回流。

transform变换函数来控制,要应用的一个或多个CSS变换函数。 变换函数按从左到右的顺序相乘,这意味着复合变换按从右到左的顺序有效地应用

常用的交换函数有:

  • matrix:矩阵函数。CSS 函数 matrix() 用六个指定的值来指定一个均匀的二维(2D)变换矩阵。这个矩形中的常量值是不作为参数进行传递的,其他的参数则在主要列的顺序中描述。matrix(a, b, c, d, tx, ty)matrix3d(a, b, 0, 0, c, d, 0, 0, 0, 0, 1, 0, tx, ty, 0, 1) 的简写
  • matrix3d:CSS 函数 matrix3d() 用一个 4 × 4 的齐次矩阵来描述一个三维(3D)变换。16个参数都在主要列的顺序中描述。
  • rotate:将元素在不变形的情况下旋转到不动点周围(如 transform-origin如果为负值,则为逆时针 。
  • rotateX:定义了将元素在横坐标上旋转而不使其变形的方法,相当于将元素绕x轴旋转。
  • rotateY:定义了将元素在纵坐标上旋转而不使其变形的方法,相当于将元素绕y轴旋转。
  • rotateZ:定义了将元素在z坐标上旋转而不使其变形的方法,相当于将元素绕z轴旋转。
  • rotate3D:上面四个函数的结合:rotate3D(x, y, z, a)。
  • scale:缩放。scale(sx, sy):相对于x轴和y轴进行缩放。
  • scale3D
  • scaleX
  • scaleY
  • scaleZ
  • skew:拉伸角度。scale(ax, ay):相对于x轴和y轴进行旋转
  • skewX
  • skewY
  • translate:平移。translate(tx, ty)
  • translate3D
  • translateX
  • translateY
  • translateZ

参考:

css常用的布局单位有哪些?

  • 像素(px):图像切割的最小单位,一般有css像素和物理像素之分。CSS像素:为web开发者提供,在CSS中使用的一个抽象单位;物理像素:只与设备的硬件密度有关,任何设备的物理像素都是固定的。
  • 百分比(%)
  • 长度比例:emremem是相对父元素父长度比例,rem是相对根元素的长度比例。
  • 相对于视窗宽高的百分比比例:vwvh,也就是现对于视窗宽高的比例再乘以100。

类数组对象如何转为数组?

一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。常见的类数组对象有 arguments 和 DOM 方法的返回结果,还有一个函数也可以被看作是类数组对象,因为它含有 length 属性值,代表可接收的参数个数。

  • 使用slice函数:

    Array.prototype.slice.call(arrayLike);
    
  • 使用splice函数:

    Array.prototype.splice.call(arrayLike, 0);
    
  • 使用concate函数:

    Array.prototype.concat.apply([], arrayLike);
    
  • 使用Array.from方法来转换:

    Array.from(arrayLike);
    

数组有哪些原生方法?

const arr = [1, 2, 3];

// 数组转字符串
arr.toString();
arr.toLocaleString();
arr.join(',');

// 数组的元素增删
arr.push(); // 尾部添加
arr.pop();  // 尾部删除
arr.unshift(); // 头部添加
arr.shift();   // 头部删除

arr.concat([1, 3, 5]) // 数组拼接
arr.slice(0, 3); // 数组子序列截取[start, end)
arr.splice(0, 1, 2, 3); // 【修改原数组】删除元素并在删除的位置插入元素[start, deleteCount, ...inserts] 

// 数组的遍历
arr.forEach(n => console.log(n));
arr.includes(1);
arr.indexOf(1);
arr.lastIndexOf(1);
arr.filter(n => n % 2 == 0);
arr.map(n => n * 2)

// 其他
arr.every(n => n % 2 == 0); // 用于检测数组所有元素是否都符合指定条件
arr.some(n => n > 0); // 用于检测数组中的元素是否满足指定条件
// 接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。
arr.reduce((total, n) => {
    return total + n
})
// reduceRight() 方法的功能和 reduce() 功能是一样的,不同的是 reduceRight() 从数组的末尾向前将数组中的数组项做累加。

Unicode和UTF-8、UTF-16等字符编码的区别?

在说Unicode之前需要先了解一下ASCII码:ASCII 码(American Standard Code for Information Interchange)称为美国标准信息交换码。

  • 它是基于拉丁字母的一套电脑编码系统。
  • 它定义了一个用于代表常见字符的字典。
  • 它包含了"A-Z"(包含大小写),数据"0-9" 以及一些常见的符号。
  • 它是专门为英语而设计的,有128个编码,对其他语言无能为力

ASCII码可以表示的编码有限,要想表示其他语言的编码,还是要使用Unicode来表示,可以说UnicodeASCII 的超集。

Unicode全称 Unicode Translation Format,又叫做统一码、万国码、单一码。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。

Unicode的实现方式(也就是编码方式)有很多种,常见的是UTF-8UTF-16UTF-32USC-2

UTF-8是使用最广泛的Unicode编码方式,它是一种可变长的编码方式,可以是1—4个字节不等,它可以完全兼容ASCII码的128个字符。

UTF-16也是Unicode编码集的一种编码形式,把Unicode字符集的抽象码位映射为16位长的整数(即码元)的序列,用于数据存储或传递。

如果字符内容全部英文或英文与其他文字混合,但英文占绝大部分,那么用UTF-8就比UTF-16节省了很多空间;而如果字符内容全部是中文这样类似的字符或者混合字符中中文占绝大多数,那么UTF-16就占优势了,可以节省很多空间。

子组件可以直接改变父组件的数据吗?

子组件不可以直接改变父组件的数据。这样做主要是为了维护父子组件的单向数据流。每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。如果子组件修改了父组件传过来的数据,Vue 会在浏览器的控制台中发出警告。

Vue提倡单向数据流,即父级 props 的更新会流向子组件,但是反过来则不行。这是为了防止意外的改变父组件状态,使得应用的数据流变得难以理解,导致数据流混乱。如果破坏了单向数据流,当应用复杂时,debug 的成本会非常高。

如果需要子组件修改父组件的数据,需要父组件向外暴露接口,也就是提供触发事件:通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。

Vue如何收集依赖?

在初始化 Vue 的每个组件时,会对组件的 data 进行初始化,就会将由普通对象变成响应式对象,在这个过程中便会进行依赖收集的相关逻辑:

function defieneReactive (obj, key, val){
  const dep = new Dep();
  ...
  Object.defineProperty(obj, key, {
    ...
    get: function reactiveGetter () {
      if(Dep.target){
        dep.depend();
        ...
      }
      return val
    }
    ...
  })
}

Vue实例化一个对象的具体过程如下:

  1. 使用Object.definePropertyProxy实现数据劫持。也就是拦截数据的变化,以实现响应式更新。
  2. 在数据劫持过程中,就会不断收集依赖关系,并将数据通过Observer观察者添加到Dep对象中
  3. 然后事件监听机制通过Watcher对象将订阅者添加到Dep对象中。
  4. 这样,Dep就有了观察者和订阅者的依赖关系,当数据发生变化,数据就会被劫持,观察者检测到数据变化,通知Dep做出反应,Dep再发送信息通知所有的订阅者,订阅者就会去更新视图,也就达到了数据响应式更新视图的效果。

参考:

实现数组的乱序输出

乱序输出也就是随机生成一个索引,然后取出,以此类推,直到数组取出完毕,具体思路就是:

  1. 取出数组的第一个元素,随机产生一个索引值,将该第一个元素和这个索引对应的元素进行交换。
  2. 第二次取出数据数组第二个元素,随机产生一个除了索引为1的之外的索引值,并将第二个元素与该索引值对应的元素进行交换
  3. 按照上面的规律执行,直到遍历完成
var arr = [1,2,3,4,5,6,7,8,9,10];
for (var i = 0; i < arr.length; i++) {
  const randomIndex = Math.round(Math.random() * (arr.length - 1 - i)) + i;
  [arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];
}
console.log(arr)

**Math.random()** 函数返回一个浮点数, 伪随机数在范围从0到小于1,也就是说,从0(包括0)往上,但是不包括1(排除1),然后您可以缩放到所需的范围。

实现数组扁平化

扁平化的意思是将数组内部的数组展开到一级数组中,比如[1,2, [3, 4], 5]扁平化就变成:[1, 2, 3, 4, 5]

思路很简单,就是遍历数组,遇到数组就再进行扁平化操作,然后插入数组当前位置。

/**
 * @param {any[]} arr 
 * @returns any[]
 */
function flatten(arr) {
    const res = [];
    arr.forEach(item => {
        if(Array.isArray(item)) {
            res.push(...flatten(item));
        }else {
            res.push(item);
        }
    });
    return res;
}

代码输出结果(Promise相关)

代码片段:

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2");
}
async1();
console.log('start')

本题考查asyncawait关键字,比较简单:

async1 start
async2
start
async1 end

代码片段:

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
  setTimeout(() => {
    console.log('timer1')
  }, 0)
}
async function async2() {
  setTimeout(() => {
    console.log('timer2')
  }, 0)
  console.log("async2");
}
async1();
setTimeout(() => {
  console.log('timer3')
}, 0)
console.log("start")

本题考查宏任务和微任务的执行逻辑,其中,还穿插着await操作符,实际上,await之后的代码我们看做是Promise.then()即可。

  1. 执行async1()

    console.log("async1 start"); // 打印async1 start
    async2(); // console.log(timer2)进入异步宏任务队列; 打印async2;后续代码进入异步微任务
    
  2. console.log('timer3')进入异步宏任务队列

  3. 执行console.log("start"),打印start

  4. 宏任务执行结束,开始执行队列中的微任务:

    // async1()中的微任务开始执行
    console.log("async1 end"); // 打印async1 end
    setTimeout(() => {
        console.log('timer1') // 进入异步宏任务队列
    }, 0)
    
  5. 微任务执行完毕,开始执行队列中的宏任务:

    console.log(timer2); // 打印time2
    console.log(time3); // 打印time3
    
  6. 宏任务执行结束,开始执行队列中的微任务,无微任务,又开始执行队列中的宏任务:

    console.log('timer1') // 打印time1
    
  7. 代码执行完毕

梳理上面的打印结果:

async1 start
async2
start
async1 end
timer2
timer3
timer1

N 叉树的层序遍历

给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。
树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。
输入:root = [1,null,3,2,4,null,5,6]
输出:[[1],[3,2,4],[5,6]]
/**
 * // Definition for a Node.
 * function Node(val,children) {
 *    this.val = val;
 *    this.children = children;
 * };
 */

本题要求对数进行层次遍历,也就是数的广度优先遍历(BFS),广度优先是基于队列的遍历方式:

var levelOrder = function(root) {
    const result = [];
    if (root == null) return result;
    const queue = [];
    queue.push(root);
    while (queue.length) {
        const level = [];
        const len = queue.length;
        for (let i = 0; i < len; i++) {
            const node = queue.shift();
            level.push(node.val);
            queue.push(...node.children);
        }
        result.push(level);
    }
    return result;
};