前端面试题集每日一练Day3

问题先导

  • script标签中defer和async属性的区别?【html

  • 单行/多行文本溢出可以怎么处理?【css

  • undefined和null的区别?typeof null的结果为什么是object?【js

  • Vue双向绑定的原理【vue

  • 数组中第K个最大元素

    在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
    

知识梳理

script标签中的defer和async属性的区别?

script标签一般用于加载js脚本,我们知道js脚本是阻塞式加载和执行的,即页面解析到script标签时会暂停页面的解析,先加载脚本并执行脚本后再继续页面的解析。

而H5新增的两个属性:deferasync,可以让脚本异步加载,但脚本的执行方式有所不同,defer是延迟的意思,所以脚本会在页面加载结束再执行,而async是异步的意思,脚本只会异步加载,并立即执行。

页面的加载、脚本加载和脚本执行示意图如下所示:

前端面试题集每日一练Day3_第1张图片

总结来说就是,deferasync属性让script标签能异步加载,但async立即执行,而defer是延迟到页面加载结束再执行。

值得注意的是,当两个属性同时存在时,async的优先级更高。

关键字:html、页面脚本的加载与执行

单行和多行文本溢出如何处理?

文本溢出最常见的方式就是替换为省略号。文本溢出属性为text-overflow,有三个可选值:

  • clip:默认值,裁剪溢出文本,即溢出文本会被隐藏起来
  • ellipsis:省略号的意思,溢出部分替换为省略号,这也是最常用的文本溢出处理方式
  • string:实验中的属性,可用指定字符特换溢出文本

除此之外,溢出文本一般还需要搭配两个属性才能正常工作,

  • overflow:溢出处理,可选值有visiblehiddenscrollauto。一般来说,为了保证溢出文本正确被替换为省略号,需要隐藏起来才能称之为溢出文本。
  • white-space:空白处理,同样的道理,为了保证溢出文本不显示出来,需要设置为不换行,即nowrap值才行。

多行文本有时候也需要溢出显示为省略号,但这个时候whire-space对于多行来说就不起作用了,为了达到这个效果,我们需要另外使用几个属性:

text-overflow: ellpsis;
overflow: hidden;

/** 显示方式设置为box,子元素垂直排列*/
display: -webkit-box;    
-webkit-box-orient: vertical;
/** 需要显示到的行数*/
-webkit-line-clamp: 3;

由于display: boxbox-orientline-clamp都是实验中的属性,一些浏览器并未支持,所以存在兼容性问题。

实例:

这是一行文字,这是一行文字,这是一行文字,这是一行文字,这是一行文字,这是一行文字,

这是一行文字,这是一行文字,这是一行文字,这是一行文字,这是一行文字,这是一行文字,

这是一行文字,这是一行文字,这是一行文字,这是一行文字,这是一行文字,这是一行文字,

总结来说就是,文本溢出处理需要使用text-ellipsis属性,常见的是设置为ellipsis,即省略号,但文本溢出属性需要“文本发生溢出”时才会生效,对应单行文本,通过overflow:hiddenwhite-space:nowarp来让单行文本达到溢出状态。

而对于多行文本,需要将父元素设置为box布局,且子元素排列方式box-orient设置为垂直排列,最后,再设置显示的行数line-clamp,这样后面为显示的行就会被替换为省略号了。

css基础、文本溢出

undefined和null的区别?typeof null的结果为什么是object

首先从定义来说,undefined是指未初始化的变量,而null是指空对象,虽然都是基本数据类型,但本质上是不一样的数据类型。

这一点从typeof nullobject也可以看出。本质上,也就是从存储方式上来说,null的存储方式和undefined也是不同的。

在第一版的js设计中,使用32位作为存储单元,并使用低三位(1-3位)表示值的类型:

  • 000:Object类型,后续位数用于存储指向对象的引用,而null的后31位全是0,用于表示无引用,也就是空对象。

  • 1:int类型,后续位数存储一个31位的有符号整数。

  • 010:double类型。后续位数存储一个双精度浮点数。

  • 100:string类型。

  • 110:布尔值。

    而undefined使用整数-2^30表示,也就是说需要32位才能表示这个数字,这超出了int类型的范围。(尽管如此,我还是不太清楚这里具体是怎么区别undefined和null的,因为-2^30用32位二进制表示为11000000000000000000000000000000,同样的低三位为000,如果只按照低三位作为判断标准,那么undefined同样判断为object类型才对,没找到相关说明,难受。目前的猜测是当进行类型判断时首先判断这个数字是否与-2^30相等,相等就直接返回undefined,不相等再进一步根据低三位数值来判断数据类型,不过这种设计思路取决于开最初的设计者,无需太过关注。)

更多细节参考:《The history of “typeof null”》

关键字:js数据类型

Vue双向绑定的实现原理

Vue的双向绑定原理简单来说就是当数据发生变化时能检测到数据变化,然后做出响应。而js中的Object.definePropertygettersetter正是用于监听数据的读取操作的,Vue也是基于这两个api来实现数据的监听,进而实现即时响应。

双向绑定的实现有两个过程:

  • 数据劫持(Observer):也就是数据监听的定义,即使用Object.defineProperty的getter和setter来实现数据劫持(Vue3.0已使用Proxy代理对象来实现数据劫持)。
  • 视图更新逻辑:当数据发生变化,就会被Observer作为观察者监听到,然后发送消息给Dep,Dep作为经纪人再将信息发送给所有订阅者,订阅者就会触发视图的更新:re-render,所以数据变化能触发视图更新。
  • 双向绑定:也就是反过来视图变化也能更新数据,视图是Dom,所以视图变化我们可以通过原生Dom的事件来实现监听,然后触发数据变化,数据变化的变化又引起视图层的变化,也就实现了双向绑定效果。

关键字:Vue基础

数组中第K个最大元素

在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

题目很清晰,需要找到第K大的元素,最简单的思路就是排序,然后就能根据下标定位到第K大的数。这样做是可行的,但我们需要思考是否有优化空间。

题目要求的是查找第K大的数,实际上,如果我们不需要完全排好序就可以确认第K大位置的元素,就不需要再继续排序浪费操作次数。

不用完全排序,也就是排序是递进的过程,主要有两种排序算法:快排和堆排序。

快排,也就是快速排序算法,使用的是分而治之的思想,

  • 从序列中选择一个基数
  • 把数字较小的放到左边,较大的放到右边
  • 对左右区间重复以上步骤,直到区间数只有一个时,排序结束
/**
 * 查找序列中第K大的数字
 * @param {number[]} nums 
 * @param {number} k 
 * @returns {number}
 */
function findKthLargest(nums, k) {
    return quickSort(nums)[nums.length - k];
};

/**
 * 快速排序
 * @param {number[]} nums 待排序数组
 * @param {number?} left 区间左指针
 * @param {number?} right 区间右指针
 * @returns {number[]}
 */
function quickSort(nums, left, right) {
    if(Object.is(left, undefined)) {
        left = 0;
    }
    if(Object.is(right, undefined)) {
        right = nums.length - 1;
    }
    if(left >= right) {
        return nums;
    }
    let baseIndex = left; // 基数指针
    const base = nums[baseIndex];
    for(let i = left;i < right + 1; i++) {
        // 小于基数,放到基数左边,基数被往右“挤”一位
        if(nums[i] < base) {
            // 交换
            nums[baseIndex] = nums[i]; // less
            baseIndex++;
            nums[i] = nums[baseIndex]; // more
            nums[baseIndex] = base; // base
        }
        // 大于基数本身就在右侧,无需移动
    }
    quickSort(nums, left, baseIndex-1);
    quickSort(nums, baseIndex+1, right);
    return nums;
}

排有多种不同的位置交换方案,上面使用的是一次遍历法,从左扫到右,遇到比基数小的放到左边即可,值得注意的是,由于是往前放,需要把基数和右区间的数后移,右区间后移只需要把基数移到右区间最前端(基数后边那个数)移到右区间最后端(遍历指针的地方),基数后移一位即可。

此外,还有一种碰撞双指针法,左指针指向左区间最右侧,右指针指向右区间最左侧。所以初始时左右指针在数组区间的左右两侧。首先从左侧开始遍历,需要找较大值,找到需要移到右区间,也就是移到右指针的位置,同时,调整基数位置到左指针处。然后开始遍历右侧,找较小值,找到需要放到左区间,也就是i指针的位置,同时,调整基数的位置到右指针处。重复,直到左右指针碰撞,说明左右侧均找完。

function quickSort(nums, left, right) {
    if(Object.is(left, undefined)) {
        left = 0;
    }
    if(Object.is(right, undefined)) {
        right = nums.length - 1;
    }
    if(left >= right) {
        return nums;
    }
    let i = left,
        j = right;
    const base = nums[j];
    while(i < j) {
        // 寻找左侧比基数大的值
        while(i < j && nums[i] <= base) {
            i++;
        }
        nums[j] = nums[i];
        nums[i] = base;
        // 寻找右侧比基数小的值
        while(j > i && nums[j] >= base) {
            j--;
        }
        nums[i] = nums[j];
        nums[j] = base;
    }
    quickSort(nums, left, j-1);
    quickSort(nums, j+1, right);
    return nums;
}

基于快速排序的快速选择

我们知道,快速排序是分治思想,一步一步进行排序的,其中有个数据是明确的,那就是基数的位置。每进行一次的快排,我们就可以得到基数的位置,如果要找的数在K左侧,那我们就只需要快排左区间,如果在右侧,就只需要快排右区间,直到基数就是要找的数字为止。

实际上,我们只需要把快排函数稍微修改即可:

/**
 * 基于快速排序的快速查找
 * @param {number[]} nums 待排序数组
 * @param {number} k
 * @param {number?} left 区间左指针
 * @param {number?} right 区间右指针
 * @returns {number}
 */
function findKthLargest(nums, k, left, right) {
    if(Object.is(left, undefined)) {
        left = 0;
    }
    if(Object.is(right, undefined)) {
        right = nums.length - 1;
    }
    if(left >= right) {
        return nums[right];
    }
    let i = left,
        j = right;
    const base = nums[j];
    while(i < j) {
        // 寻找左侧比基数大的值
        while(i < j && nums[i] <= base) {
            i++;
        }
        nums[j] = nums[i];
        nums[i] = base;
        // 寻找右侧比基数小的值
        while(j > i && nums[j] >= base) {
            j--;
        }
        nums[i] = nums[j];
        nums[j] = base;
    }
    const d = j - (nums.length - k);
    if(d == 0) {
        return base;
    } else if(d > 0) {
        return findKthLargest(nums, k, left, j-1);
    } else {
        return findKthLargest(nums, k, j+1, right);
    }
}

由于利用到了K值信息以及快排的特点,我们只需要对左区间或右区间进行快排就能找到答案,而无需整个数组完全排序结束。

堆排序

堆排序是利用了堆这种数据结构:

堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子的值,称为大顶堆(大根堆)。或者每个结点的值都小于或等于其左右孩子的值,称为小顶堆(小根堆)。

根据堆的特点,我们知道,大根堆能保证根元素为最大值,小根堆能保证根元素为最小值,这样,我们通过不断构建堆结构,同时不断缩小堆的规模,当堆的规模为1时,排序结束。这就是堆排序的逻辑。

  • 把一个无序序列构建成一个大根堆(升序)或小根堆(降序)
  • 将堆顶元素放到序列末尾
  • 序列长度缩小1,重复以上步骤,直到序列长度为1,结束排序过程。

看到堆排序有点冒泡排序的韵味,都是找最大值,然后存起来,同时不断缩小查找序列的范围。然而堆排序和快排一样,时间复杂度仅为`O(nlogn)。这是因为堆这种结构带来的优化效果:当第一次构建堆之后,后续只是调整首位交换带来的变化,而无需像第一步那样重建堆。重建堆和调整堆是有很大区别的:重建是对无序序列,需要从最后一个非叶子节点开始调整,是从下往上调整,但之后的调整堆由于只有根元素发生了变化,而其他非叶子节点都已经是堆结构了,所以只需要从上往下调整,直到某个非叶子节点也变成堆结构。

堆排序实际上也可以原地排序,由于是完全二叉树,非叶子节点与左右孩子的对应关系十分明确,无需借助多余的堆结构。

/**
 * 查找序列中第K大的数字
 * @param {number[]} nums 
 * @param {number} k 
 * @returns {number}
 */
function findKthLargest(nums, k) {
    // 1.构建大根堆
    let level = nums.length;
    buildHeap(nums, level);
    // 2.交换首尾元素, 缩小堆级别并维护堆,重复步骤2直至堆级别为1
    while(level > 1) {
        // 交换首尾
        level--;
        const root = nums[0];
        nums[0] = nums[level];
        nums[level] = root;
        // 重新维护堆
        adjustHeap(nums, level, 0);
    }
    // 返回第K大的数
    return nums[nums.length - k];
};

/**
 * 构建大根堆
 * @param {number[]} nums 序列
 * @param {number} level 构建级别|范围|长度:[0 ~ level)
 */
function buildHeap(nums, level) {
    // 节点(i) => (左孩子)2*i + 1, (右孩子)2*i + 2
    // 最后一个非叶子节点,也就是至少存在左孩子 => 2*i + 1 <= len - 1 => i <= len/2 - 1
    const lastNodeIndex = Math.ceil(level/2 - 1);
    for(let i = lastNodeIndex; i >= 0; i--) {
        adjustHeap(nums, level, i);
    }
}

/**
 * 调整大根堆
 * @param {number[]} nums 序列
 * @param {number} level 构建级别|范围|长度:[0 ~ level)
 * @param {number} i 当前结点下标
 */
function adjustHeap(nums, level, i) {
    const lastNodeIndex = Math.ceil(level/2 - 1);
    if(i <= lastNodeIndex) {
        let nodeVal = nums[i];
        // 交换孩子结点中的最大值
        const left = 2 * i + 1;
        const right = 2 * i + 2;
        let sweapIndex = i; // 交换结点的坐标
        if(left < level && nums[left] > nodeVal) {
            sweapIndex = left;
            nodeVal = nums[left];
        }
        if(right < level && nums[right] > nodeVal) {
            sweapIndex = right;
            nodeVal = nums[right];
        }
        // 交换
        if(sweapIndex != i) {
            nums[sweapIndex] = nums[i];
            nums[i] = nodeVal;
            adjustHeap(nums, level, sweapIndex);
        }
    }
}

你可能感兴趣的:(前端面试题集每日一练Day3)