前端面试题集每日一练Day8

873 阅读19分钟

问题先导

  • html5有哪些特性?【html】
  • <title><h1>的区别?<b><strong>的区别?<i><em>的区别?【html】
  • <li>元素之间看不见的空白是什么?如何解决?【css】
  • css3有哪些新特性?【css】
  • 为什么会有新增BigInt这种数据类型,使用的时候有什么需要注意的?【js数据类型】
  • Object.assign和展开语法有什么区别?【js基础】
  • letconstvar的区别?【js基础】
  • v-ifv-show的区别?【Vue】
  • v-model如何实现?【Vue】
  • 手写Promise.then【手写代码】
  • 手写Promise.all【手写代码】
  • 手写Promise.race【手写代码】
  • 输出结果(Promise相关)【输出结果】
  • 复原IP地址【算法】

知识梳理

hml5有哪些新特性?

  • 语义化标签:header、footer、nav、article、section、aside等

  • 媒体标签:audio、video、source(提供多份媒体源,以供浏览器选择性播放)

    <audio controls>
        <source src="/i/horse.ogg" type="audio/ogg">
        <source src="/i/horse.mp3" type="audio/mpeg">
        Your browser does not support the audio element.
    </audio>
    
  • 表单标签

    • 新增type类型:email、url、number、search、range、color、time、data、datatime、datatime-local、week、month
    • 表单属性:placeholder(输入提示)、autofocus(自动聚焦)、autocomplete(自动完成属性有两个可选值:on和off,使用前提是表单必须要指定name属性,表单提交后才会生效。效果就是提交后回退页面表单会自动填充历史输入值,且输入时会显示历史提交值)、required(必填项)、pattern(定义匹配的正则规则)、multiple(表单多可存储多个文件或邮箱)、form(指定所属form的ID)
    • 表单事件:oninput、oninvalid(验证不通过时触发)
  • 进度条、度量器

    • <progress>:用来表示任务的进度(IE、Safari不支持),max用来表示任务的总量,value表示已完成量,如果不设置value值,进度条还会自动滚动,出现动画效果。和input[type="range"]除了样式上的差异,input当然是方便用户可修改的。

    • <meter>:用来度量给定范围内的数据。既然是范围度量器,就需要需要使用minmax来指定度量范围,默认为0-1,然后可以使用lowhigh来指定度量器的低值区域和高值区域:[min-low)之间的值就是低值区域,并有低值的视觉效果(Chrome中度量颜色变橘色),那么[low-high]之间的值就是正常区域,(high, max]就是高值区域,也会有视觉上的变化(Chrome中度量颜色变红)。

      比如,我们知道PE(市盈率)常用来作为指数估值指标,其中等权历史百分位处于30%-70%区间时为正常区域,而小于30%就认为该指数处于低谷区域,具有很好的投资价值,相反,高于70%则处于高估区,不建议买入。

      <p>PE指标(百分位)</p>
      <div>14%<meter min="0" max="100" low="30" high="70" value="14">14%</meter>属于低估区</div>
      <div>45%<meter min="0" max="100" low="30" high="70" value="45">45%</meter>属于正常区</div>
      <div>88%<meter min="0" max="100" low="69" high="70" value="88">88%</meter>属于高估区</div>
      

      效果图:

    此外,还可以设置optimun属性来标记最优值所处的区间,比如案例中设置optinum="10",则说明低估区属于优选区间,这个属性只是逻辑上的指标,没有特殊效果,即最佳值处于哪个区间,哪个区间就是逻辑上的优选区间(期待区间)。 。

  • DOM查询操作:document.querySelector、document.querySelectorAll

  • Web存储:localStorage、sessionStorage

  • 其他:拖放属性draggable、画布canvas、矢量图svg、地理定位geolocation、通信协议websocket、历史立即history API等。

<title><h1>的区别?<b><strong>的区别?<i><em>的区别?

<title>元素可定义文档的标题,而<h1>则表示层次明确的标题,对页面信息的抓取有很大的影响。

<b><strong>的页面都是加粗显示,但<strong>还有强调的意义。同样的,<i><em>的物理效果都是斜体,但<em>还有强调的意义。

总结就是,h1strongem之类的标签具有逻辑状态,而titlebi只具有物理状态,具有逻辑状态的标签和语义化标签的效果是一致的,这种逻辑强调效果也是搜索引擎所针对性捕获的重点,而物理状态只是这些逻辑状态标签自带的效果,修改其默认样式并不影响其逻辑效果。

<li>元素之间看不见的空白是什么?如何解决?

<li>元素的默认display属性值为list-item,也就是以列表显示,有时候我们需要把<li>设置为行内显示:

<ul class="hero">
  <li>Jinx</li>
  <li>Yasuo</li>
  <li>Riven</li>
  <li>Teemo</li>
</ul>
.hero li {
  display: inline;
}
.hero li:nth-child(odd)  {
  background: #21c96e;
}
.hero li:nth-child(even) {
  background: #ff0000;
}

显示效果就会变成这样: image.png 我们发现li标签之间多了一个空格,实际上,这个不是li标签本身的问题,而是浏览器会把内联标签中间的空白字符(空格、换行、Tab等)渲染为一个空格,因此,上面把<li>单独放在一行,元素之间的空白字符就被渲染为了空格。

解决方法:

  • <li>写到一行。但这样不美观,也可能被html格式化插件格式化后换行,不推荐。
  • 设置li浮动样式float: left;。缺点:某些场景下不利于控制。
  • 设置父元素的字符尺寸为0,即font-size: 0;,然后再单独设置lifont-size。这样做的目的是将空白字符的font-size的字符尺寸设置为0,也就看不到了,但如果父元素除了li元素还有别的文本元素,也还需要单独设置font-size属性,会比较麻烦。
  • 设置父元素的字符间隔属性letter-spacing: -5px;,由于空格大概占据5px,设置字符间隔为负数就可以将这些空格“挤到”一起,但是同样的,还需要单独设置子元素<li>letter-spacing:normal;

CSS3有哪些新特性?

  • 新增各种CSS选择器::not()

  • 圆角属性:border-radius

    <div style="
      width: 50px;
      height: 50px;
      background: #29a2b1;
      border-radius: 10px;
    "></div>
    
  • 多列布局:multi-column layout

  • 阴影(shadow)和反射(reflect)

    <div style="
      width: 50px;
      height: 50px;
      background: #29a2b1;
      border-radius: 10px;
      box-shadow: 10px 10px 5px #cca;
    "></div>
    

    语法:

    box-shadow: h-shadow v-shadow blur spread color inset;

    1. h-shadow:水平阴影偏移(必须)
    2. v-shadow:垂直阴影偏移(必须)
    3. blur:模糊距离
    4. spread:阴影大小
    5. color:阴影颜色
    6. inset:如果没有指定inset,默认阴影在边框外,即阴影向外扩散。 使用 inset 关键字会使得阴影落在盒子内部,这样看起来就像是内容被压低了。 此时阴影会在边框之内 (即使是透明边框)、背景之上、内容之下。

    此外,box-sahdow可以接受多个值,用逗号分隔,也就是设置多个阴影值重叠的效果。

    而反射box-reflect是实验中的属性,可以做出倒影的效果,更多说明可参考-webkit-box-reflect - MDN

  • 文字特效 (text-shadow)

  • 文字渲染 (Text-decoration)

  • 线性渐变 (gradient)

  • 偏移(transform)

  • 增加了旋转,缩放,定位,倾斜,动画,多背景等等

为什么会新增BigInt这种数据类型?有哪些使用细节?

js的最大安全数Math.MAX_SAFE_INTEGER表示整数2^53 - 1,当超过这个数值时,数字运算就会丢失精度。

这是因为js采用双精度浮点数,即64位固定长度来存储一个数字,在二进制科学表示法中,分为三部分:符号(1位)+ 指数(11位)+ 有效数字(52位),也就是说,除去符号位和指数位,只剩52位能够存储有效数字,对于整数来说,2^0相当于00(*11)0(*51)1,2^1相当于00(*11)0(*50)10,也就是说,指数每增加1,二进制小数高位的位置也要增加往上增加,那么25位有效位就只能保存最多2^52的指数,但是0的地方还能替换为1,也就是2^25 + n都可以保存,换一种说法就是最大安全整数为:2^53 - 1,因为一旦指数超过53就需要53位有效位数,但现在只有52位,因此在这个极限上减1即可。同样的,对于小数来说,指数部分其实就是变成了负数,整数还能减1,小数就不能了,所以小数也有最小精度:2^-52,这个数字我们可以使用Math.EPSILON获得。

总结来说,js采用了双精度浮点数计数法,也就是64位固定长度存储数字,符号占1位,指数部分占11位,那么剩下的52位就是有效数字部分,对于整数来说,最大安全数为Math.MAX_SAFE_INTEGER (2^53 - 1),最小精度为Math.EPSILON(2^-52)。

因此,对于超过最大安全数的整数来说,计算就有可能丢失进度,正如0.1 + 0.2 != 0.3那样,整数超出了存储范围也会丢失精度,因此**BigInt** 作为一种内置对象,它提供了一种方法来表示大于 2^53 - 1 的整数。这原本是 Javascript中可以用 Number 表示的最大数字。BigInt 可以表示任意大的整数。可以用在一个整数字面量后面加 n 的方式定义一个 BigInt ,如:10n,或者调用函数BigInt()

需要注意的是:

  • 除了一般的数字运算符可以直接使用,由于BigInt是有符号整数,因此不支持无符号位移操作符>>>
  • 不支持单目+运算,如+2n会抛出错误,这是为了兼容 asm.js
  • 当使用 BigInt 时,带小数的运算会被取整。如5n / 2n == 2n
  • BigIntNumber不是严格相等的,但是是宽松相等的。比如5n == 5true,但5n === 5就是false
  • BigIntNumber之间不可以进行数字运算,但可以进行比较运算。

Object.assign和展开语法有什么区别?有哪些值得注意的地方?

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象,并返回目标对象。

展开语法(Spread syntax), 可以在函数调用/数组构造时, 将数组表达式或者string在语法层面展开;还可以在构造字面量对象时, 将对象表达式按key-value的方式展开。

  • 函数调用:myFunction(...iterableObj);
  • 字面量数组构造或字符串:[...[1, 2, 3], '4', ...'hello', 6];返回值为数组 [1, 2, 3, "4", "h", "e", "l", "l", "o", 6]
  • 构造字面量对象时,进行克隆或者属性拷贝(ECMAScript 2018规范新增特性):let objClone = { ...obj };

实际上, 展开语法和 Object.assign()历一层)。

两者的区别很小,都可以展开一个可迭代对象并拷贝到目标对象上,相较而言,展开语法更灵活,可以在函数参数中表达剩余变量,但对于非迭代对象比如nullundefined展开会报错,但Object.assign()不会抛出错误,使用起来显得更稳妥

letconstvar的区别?

letconstes6新增的块级作用域变量声明关键字,由于是块级作用域,letconst不存在变量提升,在使用前必须提前声明,而且在同一个作用域块级作用域不能重复声明,这使得变量在使用上相较于var更加具体、严谨,而letconst的区别在于,const用于表示常量,只能在声明时赋值,一旦赋值便不能再重新赋值。

为了对变量声明关键字的使用,推荐优先使用const,需要变更值的变量使用let,尽量不使用var

v-ifv-show的区别

Vue的两个条件渲染指令

  • v-if会根据指令值,生成或删除vnode节点,需要删除或重新生成dom节点,开销较大,因此对于频繁切换元素显示隐藏的,不要使用这个指令
  • v-show会直接生成vnode和对应的dom节点,然后再根据指令值切换元素的display是显示还是隐藏。

v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做,直到条件第一次变为真时,才会开始渲染条件块。相比之下,v-show 就简单得多,不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。

此外,v-if还能搭配v-else一起使用。

v-model是如何实现的?

v-model是表单控件常常使用的一个双向数据绑定指令,这个指令绑定的数据将会和表单的值value进行绑定,当这个值发生变化时,表单值也会发生变化,反过来,当用户修改了表单值时,绑定的数据也会变化,这就是实现了双向绑定的一个简单指令。

实际上,v-model就是v-bind:valuev-on:input的结合体。

v-model一般只在表单元素中使用才能生效:

  • <input>
  • <select>
  • <textarea>
  • components(组件中)

我们知道了v-model的原理,也可以自己写一个相同功能的自定义指令v-my-model

关于自定义指令的定义请参考官方说明文档:自定义指令

Vue.directive('my-model', {
    inserted: function(el, binding, vnode) {
        el.value = binding.value;
        // 监听事件,更新绑定的表达式
        el.addEventListener('input', function(evt){
            vnode.context[binding.expression] = evt.target.value;
        });
    },
    update: function(el, binding, vnode) {
        // 表达式更新时更新表单value
        el.value = binding.value;
    },
});

注意:虚拟节点的上下文对象context有指令表达式的引用,我们直接赋值就能触发Vue实例的表达式更新。

手写Promise.then

Promise.thenPromise链式调用的关键,该方法返回一个新的Promise对象。

该方法接受两个参数,一个是Promise成功状态下的回调函数,一个是失败状态下的回调函数。

Promise.then的关键在于回调函数的返回值,返回值决定了返回的新Promise对象的状态和状态结果,具体分为以下几种返回值和处理逻辑:

  • 返回了一个值,那么 then 返回的 Promise 将会成为接受状态,并且将返回的值作为接受状态的回调函数的参数值。
  • 没有返回任何值,那么 then 返回的 Promise 将会成为接受状态,并且该接受状态的回调函数的参数值为 undefined
  • 抛出一个错误,那么 then 返回的 Promise 将会成为拒绝状态,并且将抛出的错误作为拒绝状态的回调函数的参数值。
  • 返回一个已经是接受状态的 Promise,那么 then 返回的 Promise 也会成为接受状态,并且将那个 Promise 的接受状态的回调函数的参数值作为该被返回的Promise的接受状态回调函数的参数值。
  • 返回一个已经是拒绝状态的 Promise,那么 then 返回的 Promise 也会成为拒绝状态,并且将那个 Promise 的拒绝状态的回调函数的参数值作为该被返回的Promise的拒绝状态回调函数的参数值。
  • 返回一个未定状态(pending)的 Promise,那么 then 返回 Promise 的状态也是未定的,并且它的终态与那个 Promise 的终态相同;同时,它变为终态时调用的回调函数参数与那个 Promise 变为终态时的回调函数的参数是相同的。

其实总结就两句话,分为两种情况,如果返回值是一个Promise对象,那么直接返回该对象(但不能then返回值本身,否则造成调用闭环);如果不是,那么then将返回一个fulfilled状态的Promise,并将返回值设置为新Promise对象的状态值,如果回调返回执行报错,返回的新的Promise对象状态将变为rejected

在实现Promise.then之前,我们还需要先实现自己的Promise对象,具体可参考前一天的练习笔记:手写Promise

这里我们直接编写MyPromise.then部分代码:

/**
 * Promise.then
 * @param {Function} onFulfilled 
 * @param {Function} onRejected 
 */
MyPromise.prototype.then = function(onFulfilled, onRejected) {
    const THIS = this;
    const SELF = new MyPromise((resolve, reject) => {
        // 成功回调
        function fulfilledCall(res) {
            try {
                const callRes = typeof onFulfilled === 'function' ? onFulfilled(res) : undefined;
                checkBackSelf(callRes);
                callRes instanceof MyPromise ? callRes.then(resolve, reject) : resolve(callRes);
            } catch (err) {
                reject(err);
            }
        }
        // 失败回调
        function rejectedCall(res) {
            try {
                const callRes = typeof onRejected === 'function' ? onRejected(res) : undefined;
                checkBackSelf(callRes);
                callRes instanceof MyPromise ? callRes.then(resolve, reject) : reject(callRes);
            } catch (err) {
                reject(err);
            }
        }

        // 返回自己检测
        function checkBackSelf(newPro) {
            if(newPro === SELF) {
                throw new TypeError('Chaining cycle detected for promise #<Promise>');
            }
            return false;
        }

        if(THIS.state === PromiseState.FULFILLED) {
            fulfilledCall(THIS.data);
        } else if(THIS.state === PromiseState.REJECTED) {
            rejectedCall(THIS.err);
        } else {
            // 等待执行
            this.fulfilledCalls.push(fulfilledCall);
            this.rejectedCalls.push(rejectedCall);
        }
    });
    return SELF;
};

参考:

手写 Promise.all

Promise.all接受一个Promise的可迭代对象(iterable),并返回一个Promise实例,如果可迭代对象均返回了fulfilled状态,那么返回的Promise状态也为成功,并且返回值为包含所有可迭代对象Promise成功值的数组,如果可迭代对象中有一个Promise状态为已拒绝,那么返回的Promise实例将返回第一个失败的Promise的失败状态。

值得注意的一点是,成功时,收集到的成功状态值与可迭代对象顺序一致。

比如:

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values);
}, (err) => {
   console.log(err);
});
// expected output: Array [3, 42, "foo"]

值得注意的是,这里传入的42并非是Promise对象,其实也是支持的,内部可理解为自动转换成了Promise.resolve(2)

如果理解还是有疑惑,我们直接看一下手写源码就很清晰了:

const promiseAll = function(iterable) {
    return new Promise((resolve,reject) => {
        const reses = [];
        const keys = Object.keys(iterable);
        const len  = keys.length;
        let resolveCount = 0;
        for(let i = 0; i < len; i++) {
            Promise.resolve(iterable[keys[i]]).then(res => {
                reses[i] = res;
                if(len == (++resolveCount)) {
                    resolve(reses);
                }
            }, (err) => {
                reject(err);
            });
        }
    });
}

注意这里我们使用reses[i]进行复制而不是直接push,这样就能保证收集的成功状态与迭代顺序保持一致。

手写 Promise.race

Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。

race有“比赛、竞赛”的含义,这个api的用法也正是如此,让可迭代对象里的Promise并行执行,谁先返回结果就直接返回该对象的状态值。

const promiseRace = function(iterable) {
    return new Promise((resolve,reject) => {
        for(let promise of iterable) {
            Promise.resolve(promise).then((res) => {
                resolve(res);
            }, (err) => {
                reject(err);
            });
        };
    });
}

输出结果(Promise相关)

代码片段:

const promise = new Promise((resolve, reject) => {
    resolve('success1');
    reject('error');
    resolve('success2');
});
promise.then((res) => {
    console.log('then:', res);
}).catch((err) => {
    console.log('catch:', err);
})

本题考察的是Promise的状态变化,一旦Promise的状态切换未fulfilledrejected,状态就不会再发生变化了,所以多次执行状态切换函数只会触发第一次,因此打印结果为:

then: success1

代码片段:

Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log)

本题考察Promise.then函数的执行逻辑,为了达到链式调用的效果,Promise.then的回调返回值将作为一个新的Promise返回,如果回调函数没有返回值,那么将返回一个padding状态的Promise

此外,如果Promise.then的参数不是一个函数,那么直接将当前Promise作为返回值。

  1. 首先Promise.resolve返回一个fulfilled状态的Promise
  2. 由于2不是一个函数,因此直接返回当前Promise,也就是第一句的Promise.resolve(1),相当于这句then调用无效
  3. 注意第三行代码是一个Promise对象,同样不是回调函数,跳过
  4. console.log是可执行函数,打印当前Promise的状态值1

所以输出结果为:

1

复原 IP 地址

给定一个只包含数字的字符串,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 IP 地址 。你可以按任何顺序返回答案。

有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。
例如:"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效 IP 地址。
示例1:
输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]
示例2:
输入:s = "010010"
输出:["0.10.0.10","0.100.1.0"]
示例3:
输入:s = "0000"
输出:["0.0.0.0"]
示例4:
输入:s = "101023"
输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]

提示:s仅由数字组成。

本题需要列举出所有的可能值,也就是枚举出满足条件的所有答案,常用的枚举搜索算法是回溯算法。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。

题目要求我们查找四段IP地址,每段由一个整数组成,值必须处于[0, 255]之间,且不能含有前导0。正常的思路就是一段一段寻找,找到第一段后寻找第二段,一次类推,如果找到第四段数字仍然满足条件且字符串刚好被遍历完,那么就可以得到一个解。从这个思路分析,对于四段IP地址,使用暴力解法我们只需要列出三层for循环就能遍历出所有情况,但这只是对于段数比较少的情况,如果段数过多,暴力解法会比较吃力,因此我们使用回朔算法。

回朔步骤的思路和暴力解法是一样的:我们需要一段一段寻找,所以我们需要的参数有:段下标segId、查找起始位置segStart。我们用递归函数dfs(s, segId, segStart) 表示我们正在从字符串s中的s[segStart]位置查找第segId + 1段(segId从0开始计算)。

/**
 * @param {string} s
 * @return {string[]}
 */
var restoreIpAddresses = function(s) {
    const totalSeg = 4; // 查找总段数
    const res = [];
    const segment = [];
    dfs(s, 0, 0);
    return res;

    /**
     * 字符串回朔查找段
     * @param {string} s 
     * @param {number} segId 
     * @param {number} segStart 
     */
    function dfs(s, segId, segStart) {
        // 如果段数查询完毕,且字符串也刚好搜索结束,说明满足条件,记录
        if(segId === totalSeg && segStart === s.length) {
            res.push(segment.join('.'));
            return;
        }
        // 搜索段已经超过了查找段数或者字符串搜索结束,结束回朔
        if(segId >= totalSeg || segStart === s.length) {
            return;
        }
        // 特殊情况,当查找起点为0时,该段只能为0
        if(s[segStart] === '0') {
            segment[segId] = 0; // 记录段值
            dfs(s, segId+1, segStart + 1);
        }
        // 正常情况,遍历当前段每一种可能性,并递归查找下一段
        let segValue = '';
        for(let i = segStart; i < s.length; i++) {
            segValue += s[i];
            const val = +segValue;
            if(val > 0 && val <= 255) {
                segment[segId] = val;
                dfs(s, segId + 1, i + 1);
            } else {
                // 由于地址的连续性,一旦有不满足条件的段,后续都不会再满足
                break;
            }
        }
    }
};