前端面试题集每日一练Day9

247 阅读14分钟

问题先导

  • <label>标签有什么作用?【html】
  • CSS Sprites的理解?【css基础】
  • 什么是物理像素、逻辑像素和像素密度?移动端如何处理图片适配问题?【css基础】
  • 箭头函数与普通函数的区别?【js基础】
  • 如果new一个箭头函数会发生什么?为什么?【es6】
  • 如何理解箭头函数的this?【es6】
  • v-model可以在自定义组件上使用吗?如果可以,如何使用?【vue基础】
  • Vue组件模板的data为什么是一个函数而不是一个对象?【vue基础】
  • keep-alive组件的理解,它是如何实现的,具体缓存的是什么?【vue基础】
  • 打印结果(Promise相关)【代码输出】
  • 手写Function.prototype.call【手写代码】
  • 手写Function.prototype,apply【手写代码】
  • 手写Function.prototype.bind【手写代码】
  • 岛屿数量【算法】

知识梳理

<label>标签有什么作用?

<label> 标签为 input 元素定义标注(标记),当用户点击改标签时能自动聚焦到预与其绑定的<input>元素上。绑定方法有两种:

  • input元素作为label的子元素。
  • labelfor属性设置为input标签的id值。

CSS Sprites的理解?

CSS Sprites称为css精灵,是利用background-imagebackground-positionbackground-repeat属性进行图片精准定位来显示图标的一种方式。

由于网页加载图片会阻塞页面渲染,对于图片很多的网页,用户体验较差,如果把这些图片都放到一张图片中排列好,那么页面加载时只会请求一次图片,大大减少了带宽。

然后在使用的时候,我们就可以利用图片定位属性background-position以及设置图片宽高来精准显示需要的图片,同时,一般我们会把backgroud-repeat设置为no-repeat

css精灵的目的是减少浏览器过多的图片请求次数,对于几百k大小的图片,请求效率大致是相同的,把小图片(一般为图标)都集中到一张图上,就能减少不必要的请求次数。

但缺点也很明显:

  • 精灵图的制作与维护困难:为了精准定位,精灵图必须借助专业的工具实现图片拼接,并需要记录好坐标位置。其次,如果需要增加或替换图片,就显得十分麻烦。
  • 使用不便:需要记录内部图片的位置来定位图片,不是很方便。

个人不是很推荐这种做法,精灵图一般也只能压缩小图标,且制作和维护十分麻烦,如果只是为了减少请求带宽,可以将图标转换为base64编码,这样图片就能随着css一起加载,但会增加css的解析时间。相比之下,使用和维护都变得十分简单。

此外,对于图标文件,有时候为了兼容高分辨率的图标,精灵图就无法胜任,使用矢量的字体图标是我们常用的方法。

什么是物理像素、逻辑像素和像素密度?移动端如何处理图片适配问题?

先熟悉几个关键的概念:

下面的几个概念在不同场景可能有不同的使用概念,请自行区分对待。

  • 像素(pixel):图像是由一个个小方格组成的,每个小方格都有自己的位置和色彩值,图像的最小切割单位我们就称之为像素,一般使用px来表示。
  • 分辨率:我们常说的分辨率就是指横纵向像素点数,比如1920x1200分辨率的屏幕就是说这个屏幕横向能显示1920个像素点,纵向能显示1200个像素点,像素越多,同样大小的图片也就能划分得越密集,也就越清晰。
  • 物理像素(point磅):如果说像素理解为图像的最小切割单位,同样大小的屏幕我们可以切割得大一些,也可以切割得小一些,比如同样是27英寸的显示屏,我们可以设置不同的调整屏幕的分辨率,这样,单位像素的实际尺寸也会发生变化,但是屏幕的物理尺寸是不会变化的。pt是实际物理长度,在不同是使用场景中有不同的单位转换关系:比如作为印刷单位时,1 pt = 1/72 inch,但在某些电子设备中比如iso中就是1 pt = 1/163 inch
  • css像素:由于不同的物理设备的物理像素的大小是不一样的,所以css认为浏览器应该对css中的像素进行调节,使得浏览器中 1css像素的大小在不同物理设备上看上去大小总是差不多 ,目的是为了保证阅读体验一致。为了达到这一点浏览器可以直接按照设备的物理像素大小进行换算,而css规范中使用参考像素来进行换算。
  • 像素密度PPI:像素密度是pixels per inch的缩写,意为每英寸的像素数量,可以表示每单位物理像素可以显示多少个像素,和分辨率有类似的逻辑描述,像素密度越高,图像越清晰。在屏幕中我们一般使用对角线长度来计算像密度:1ppi = √ ̄(x^2 + y^2) / S,其中x * y为屏幕分辨率宽高,S为屏幕尺寸。
  • 设备像素比:表示一个设备的物理像素与逻辑像素的比。在很久以前,的确没有区别,CSS里写1px,屏幕就给你渲染成1个实际的像素点,所以DPR=1。但是后来苹果公司为其产品mac、iPhone等的屏幕配置了Retina高清屏,也就是说这种屏幕拥有的物理像素点比非高清屏多4被甚至更多。如果还按照DPR=1进行展示,那么同一张图片在高清屏上显示的区域面积会是非高清屏的1/4,这样的话图片在屏幕上的展示面积大大缩小,也就会导致看不清的问题。

对于如何创建自适应图片请参考:响应式图片 - MDN

参考:

箭头函数与普通函数的区别?

箭头函数表达式的语法比函数表达式更简洁,并且没有自己的thisargumentssupernew.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。

箭头函数相当于一个匿名函数,使用起来更方便简洁。

如果new一个箭头函数会发生什么?为什么?

会抛出xxx is not a constructortypeError错误。

箭头函数不能用作构造函数,因为new一个构造函数需要进行以下操作:

  1. 创建一个新对象。
  2. 将这个新对象链接到构造函数的prototype属性,即继承构造函数的protoytype属性。
  3. 将构造函数的this上下文重新指向新生成的对象。
  4. 如果构造函数没有返回对象,则直接返回新对象。

由于箭头函数不能修改this的绑定,因此不能作为构造函数使用。

如何理解箭头函数的this

箭头函数的this总是指向词法作用域,由上下文决定。箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。因此,在下面的代码中,传递给setInterval的函数内的this与封闭函数中的this值相同:

function Person(){
  this.age = 0;

  setInterval(() => {
    this.age++; // |this| 正确地指向 p 实例
  }, 1000);
}

var p = new Person();

由于箭头函数的this在声明时就定义好了,且不能重新绑定,因此Object.callObject.apply等方法对箭头函数来说,第一个参数都是无效的。

v-model可以在自定义组件上使用吗?如果可以,如何使用?

v-model可以在组件上使用,实际上相当于一个语法糖,等同于v-bind:valuev-on:input两个指令的结合。

但是v-model在自定义组件中使用的时候还需要特殊处理。在原生表单中,只有一个表单元素,也不存在父作用域和子作用域的问题,因此v-model可以正常工作,但是在自定义组件中,表单元素可能不止一个,事件监听也存在作用域问题:组件模板作为父作用域无法执行修改子作用域的数据。

因此,我们需要在模板中利用prop来解决作用域问题,并需要手动指定需要绑定的表单。

Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})

注意:对于checkbox这样的表单,不像input那样需要绑定value值和监听input属性,我们可以使用pmodel属性来明确指定绑定的属性名和侦听的事件名。

Vue组件模板的data为什么是一个函数而不是一个对象?

这是为了组件能正常复用,在js中对象是引用类型,重复调用并不能真正复制对象,同样的,为了让组件的实例能维护完全属于自己的对象,就必须是一份独立的数据拷贝,因此需要使用函数来创建一份独立拷贝的对象。

keep-alive的理解,它是如何实现的,具体缓存的是什么?

<keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 <transition> 相似,<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。

当组件在 <keep-alive> 内被切换,它的 activateddeactivated 这两个生命周期钩子函数将会被对应执行。主要用于保留组件状态或避免重新渲染。

keep-alive组件有三个特殊功能的属性:

  • include:缓存白名单。字符串、数组或正则表达式。只有名称匹配匹配的组件才会被缓存。
  • exclude:缓存黑名单。字符串、数组或正则表达式。名字匹配的组件不会被缓存。
  • max:缓存数量极限。数字。设置最多可以缓存多少组件实例。一旦这个数字达到了,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉。

includeexclude prop 允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示。如果不配置默认都缓存。

keep-alive组件会根据黑白名单设置和缓存数量缓存组件,在组件切换时缓存需要缓存的组件虚拟节点VNode,当激活当前组件时再从缓存中取出缓存的虚拟节点进行渲染。

具体实现细节可参考官方源码,或博文:keep-alive实现原理

参考:

打印结果(Promise相关)

代码片段:

Promise.resolve().then(() => {
  return new Error('error!!!')
}).then(res => {
  console.log("then: ", res)
}).catch(err => {
  console.log("catch: ", err)
})

本题考查,Promise返回值为一个错误对象时,状态如何切换。实际上,返回错误不代表异步请求就是错误的,因为成功的回调也可以是Error对象,而这里Promise.resolve函数返回的是一个fulfiiled状态的Promise和返回值无关。

值得注意的是,如果把return换成throw关键字,那么就能被catch捕获到,这是因为Promise内部使用了try catch用于捕获错误,一旦执行抛出错误,那么Promise的状态就会被切换为rejected

因此本题打印结果为:

then:  Error: error!!!

代码片段:

const promise = Promise.resolve().then(() => {
  return promise;
});
promise.catch(console.error)

这里其实是Promise本身的一个问题,如果返回Promise本身,就会抛出错误:

Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>

手写Function.prototype.call函数

call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

function.call(thisArg, arg1, arg2, ...)

我们知道,函数中的this指向当前调用对象,要更新this的指向,我们只需要使用指定的对象来调用该函数即可。

Function.prototype.my_call = function(obj, ...args) {
    if(!obj || typeof obj != 'object') {
        obj = window; // 指定的对象不存在或不属于对象,则设置为windows对象
    }
    const key = Symbol();
    obj[key] = this;
    const res = obj[key](...args);
    delete obj[key];
    return res;
}

注意这里使用Symbol()来创建一个唯一属性,避免和Obj对象里出现冲突,调用结束需要再删除这个属性。

手写Function.prototype.apply函数

apply() 方法调用一个具有给定this值的函数,以及以一个数组(或类数组对象)的形式提供的参数。

apply函数和call除了参数接受方式不同,其他没什么区别,所以改一下参数的接受方式即可。

Function.prototype.my_apply = function(obj, args) {
    if(!obj || typeof obj != 'object') {
        obj = window; // 指定的对象不存在或不属于对象,则设置为windows对象
    }
    const key = Symbol();
    obj[key] = this;
    const res = obj[key](...args);
    delete obj[key];
    return res;
}

手写Function.prototype.bind函数

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

注意bind函数和call的区别,bind是返回一个新的函数,而不会直接像call那样执行后返回结果,有点函数式编程的那种韵味。

Function.prototype.my_bind = function(obj, ...args1) {
    if(!obj || typeof obj != 'object') {
        obj = window; // 指定的对象不存在或不属于对象,则设置为windows对象
    }
    const fn = this;
    return function(...args2) {
        return fn.apply(obj, args1.concat(args2));
    };
}

岛屿数量

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。

输入:grid = [
  ["1","1","1","1","0"],
  ["1","1","0","1","0"],
  ["1","1","0","0","0"],
  ["0","0","0","0","0"]
]
输出:1
---
输入:grid = [
  ["1","1","0","0","0"],
  ["1","1","0","0","0"],
  ["0","0","1","0","0"],
  ["0","0","0","1","1"]
]
输出:3

本题其实就是在找可以连成一片的陆地(形成岛屿)的数量。有两个关键点:

  • 识别是否可以连成片(上下左右)
  • 属于已存在的哪座岛(如何存储)

对于这种二维矩阵,我们一般看出无向图,对于无向图有两种搜索方式:

  • 深度优先搜索(DFS)
  • 广度优先搜索(BFS)

深度优先搜索,就是当遇到数字1时,开始做深度搜索,将该位置及其四周均进行深度搜索,并将遍历到的1

更新为0,表示已被识别过。当无向图完全遍历结束,做深度搜索的次数就是岛屿的数量。

广度优先搜索,同样遇到数字1时,开始做广度优先搜索,存储与当前岛屿连成片的陆地,更新为0。当无向图完全遍历结束,做广度优先搜索的次数就是岛屿的数量。

深度优先搜索一般会用栈这种数据结构,由于需要深度搜索,一般可使用迭代的方法实现,优点时不需要存储信息,搜索的过程中就可以处理信息,由于需要回朔,操作次数也一般比广度优先更多。

而广度优先搜索一般会使用队里这种数据结构,由于是层次遍历,数据之间的相关性不如深度搜索,就需要存储信息,不断入队再出队,直到队列为空。广度优先搜索不需要回朔,但占用的空间一般比广度优先更大。

下面是深度优先搜索的的实现代码:

/**
 * @param {string[][]} grid
 * @return {number}
 */
var numIslands = function(grid) {
    const row = grid && grid.length;
    if(!row) {
        return 0;
    }
    const col = grid[0].length;
    let count = 0;
    for(let i = 0; i < row; i++) {
        for(let j = 0; j < col; j++) {
            if(grid[i][j] === '1') {
                count++;
                dfs(grid, i, j);
            }
        }
    }
    return count;

    /**
     * 广度优先搜索
     * @param {string[][]} grid 
     * @param {number} i 
     * @param {number} j 
     */
    function dfs(grid, i, j) {
        const row = grid.length;
        const col = grid[0].length;
        if(i >= 0 && i < row && j >= 0 && j < col && grid[i][j] === '1') {
            grid[i][j] = '2'; // 标记为已遍历
            // 继续深度遍历上下左右节点
            dfs(grid, i, j+1);
            dfs(grid, i, j-1);
            dfs(grid, i-1, j);
            dfs(grid, i+1, j);
        }
    }
};