前端面试题集每日一练Day10

446 阅读6分钟

问题先导

  • iframe有哪写优缺点?【html】
  • line-height的理解及其赋值方式【css基础】
  • :before::after中的双冒号和单冒号的区别是什么?【css基础】
  • display:inline-block什么时候会显示间隙?【css基础】
  • 说一说Proxy的应用场景【es6】
  • 说一说解构赋值语法【es6】
  • $nextTick 原理及作用【Vue基础】
  • 给Vue实例中的data添加新属性时会发生什么?如何解决出现的问题?【Vue基础】
  • 手写节流函数【手写代码】
  • 手写防抖函数【手写代码】
  • 代码输出结果(Promise相关)【输出结果】
  • 爬楼梯【算法】

知识梳理

iframe有哪写优缺点?

**HTML内联框架元素 **<iframe> 表示嵌套的浏览上下文,有效地将另一个HTML页面嵌入到当前页面中。

内联的框架,就像 <iframe> 元素一样,会加入 window.frames 伪数组(类数组的对象)中。

通过contentWindow属性,脚本可以访问iframe元素所包含的HTML页面的window对象。contentDocument属性则引用了iframe中的文档元素(等同于使用contentWindow.document),但IE8-不支持。

通过访问window.parent,脚本可以从框架中引用它的父框架的window。

除了上述这种通过DOM API的方式来实现通信,还可以使用HTML5提供的安全跨域通信API:window.postMessage

脚本试图访问的框架内容必须遵守同源策略,并且无法访问非同源的window对象的几乎所有属性。同源策略同样适用于子窗体访问父窗体的window对象。跨域通信可以通过window.postMessage来实现。

优点:

  • 可以引入和展示一个独立于当前页面的网页,方便统一管理
  • 可以用于加载缓慢的第三方网页如广告
  • 可以创建一个独立的宿主环境,方便数据与主页面的隔离

缺点:

  • iframe的加载会阻塞主页面的onload事件,iframe的创建比其它包括sscriptscss等 DOM 元素慢了1-2个数量级。
  • ifrmae与主页面共享连接池,会影响主页面的并行加载。
  • 不利于搜索引擎SEO
  • 容易造成页面结构混乱

参考:

line-height的理解及其赋值方式

line-height属性设置行间的距离(行高)。line-heightfont-size的计算值之差(在 CSS 中成为“行间距”)分为两半,分别加到一个文本行内容的顶部和底部。

除了不能设置负数,可能的值有:

描述
normal默认。设置合理的行间距,一般为字体尺寸的110%。
number设置纯数字,此数字会与当前的字体尺寸相乘来设置行间距。
length指定<长度>用于计算 line box 的高度。参考<长度>了解可使用的单位。以 em 为单位的值可能会产生不确定的结果
%基于当前字体尺寸的百分比行间距。
inherit规定应该从父元素继承 line-height 属性的值。

使用纯数字是推荐的做法,和设置百分比是一个逻辑,这样子元素继承时更稳定。

:before::after中的双冒号和单冒号的区别是什么?

css3中,双冒号::用于表示伪元素,单冒号:用于表示伪类。

伪类和伪元素都是css选择器的一种类型之一。

伪类,关键词是类,就像我们手动增加了类名一样,去筛选过滤元素,伪类就可以理解为浏览器默认添加的元素类,用于标识特殊状态的元素。比如一些用户行为::hover(鼠标指针悬浮到元素上)、:focus(键盘选定元素时激活),还有一些不像状态标记的伪类描述::first-child(一组兄弟元素中的第一个)等等,但是为什么不使用伪元素来描述呢?我们继续观察伪元素的特点。

伪元素,关键词是元素,所以已经和状态描述无关了,伪元素也是一种元素,但是这种元素虽然真实存在于页面,但不存在与DOM树中,这些元素可能来自源文档,也可能是CSS附加生成的。比如::first-line可以匹配元素的第一行,在DOM中,这是无法表示的,因为这不是一个完整的节点,只是一部分。比如::after可以创建一个选中元素的最后一个虚拟行内子元素,这种由css创建的元素也是不存在与DOM节点中的。上面说到的:frist-child是存在DOM中的,因此不适合用伪元素来表示。

这两个概念常常容易混淆,简单来说,伪类就是区别于手动增加的类名,当符合某种状态描述时,浏览器默认为元素增加的类,由于看不见,因此称为伪类,但伪类选择到的元素却是真实存在于DOM中的,这是区分伪类和伪元素的关键。通过简单的抽象类比记忆,类名我们使用.来匹配,那么伪类也用一个符号:来匹配。

而伪元素与元素状态无关,只和元素之间的关系有关,最重要的特点就是伪元素不存在与DOM中

注意:beforecss2的写法,现在已经不使用这种写法了,改为::before

参考:

display:inline-block什么时候会显示间隙?

  • 有空白字符时,浏览器会把元素之间的空白字符渲染为一个空格。
  • margin为正时,外边距的空白看起来像是间隙
  • letter-spacingword-spcing可能让文本之间的空白看起像像间隙

说一说Proxy的应用场景

Proxy是ES6新增的对象,用于创建一个对象的代理,从而实现被代理对象基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

在这个对象出现之前,我们可以使用Object.defineProperty来定义对象,通过设置该属性的getter/setter属性,就能实现属性的调用、修改拦截。但是,这些捕获对某些内部封装的原生方法或操作符是无法捕获的,比如数组的pushpop等方法,也不能监听delete这种常用的属性操作符。

针对上述Object.defineProperty存在的弊病,Proxy完全承担起了创建对象代理的任务,并新增了很多捕获器来实现对原生函数、操作符的拦截,此外,Proxy作为新标准收到浏览器性能优化的重点关注对象,也称之为新标准性能红利。而目前看来唯一的不足之处可能就是兼容性问题,低版本的浏览器无法被polyfill完整实现。

关于Proxy的具体用法和相关捕获器请参考:Proxy - MDN

说一说解构赋值语法

解构赋值语法是一种 Javascript 表达式。通过解构赋值, 可以将属性/值从对象/数组中取出,赋值给其他变量。

有点类似于展开语法,展开是逐一迭代复制,而解构是适应性迭代赋值,两者能结合使用:

var a, b, rest;
[a, b] = [10, 20];
console.log(a); // 10
console.log(b); // 20

[a, b, ...rest] = [10, 20, 30, 40, 50];
console.log(a); // 10
console.log(b); // 20
console.log(rest); // [30, 40, 50]

({ a, b } = { a: 10, b: 20 });
console.log(a); // 10
console.log(b); // 20

// Stage 4(已完成)提案中的特性
({a, b, ...rest} = {a: 10, b: 20, c: 30, d: 40});
console.log(a); // 10
console.log(b); // 20
console.log(rest); // {c: 30, d: 40}

$nextTick 原理及作用

用法:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
  // DOM 更新了
})

// 作为一个 Promise 使用 (2.1.0 起新增)
Vue.nextTick()
  .then(function () {
    // DOM 更新了
  });

虽然Vue采用数据驱动视图的思想,但有些时候我们还是需要获得一些DOM数据,但Vue的视图更新是异步的,如果某部分逻辑的数据需要等待视图更新之后的DOM,那我们就需要知道DOM什么时候更新完成,然后再这个时间点之后进行数据的读取,nextTick就是这样一个钩子API,当DOM更新之后触发调用。

nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout的原生 JavaScript 方法来模拟对应的微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列。

nextTick 是典型的将底层 JavaScript 执行原理应用到具体案例中的示例,引入异步更新队列机制的原因∶

  • 如果是同步更新,则多次对一个或多个属性赋值,会频繁触发 UI/DOM 的渲染,可以减少一些无用渲染
  • 同时由于 VirtualDOM 的引入,每一次状态发生变化后,状态变化的信号会发送给组件,组件内部使用 VirtualDOM 进行计算得出需要更新的具体的 DOM 节点,然后对 DOM 进行更新操作,每次更新状态后的渲染过程需要更多的计算,而这种无用功也将浪费更多的性能,所以异步渲染变得更加至关重要

给Vue实例中的data添加新属性时会发生什么?如何解决出现的问题?

我们知道data属性能响应式更新视图,但对于Vue实例创建之后或者delete属性之后再新增的属性,数据变化时并不会触发响应式更新逻辑,这个时候需要我们手动调用Vue.set( target, propertyName/index, valu…这个api来设置新属性的响应式逻辑。

addObjB () (
   this.$set(this.obj, 'b', 'obj.b')
}

手写节流函数

函数节流是指在一定之间内,函数只会被触发一次。在这个设定的单位时间内多次调用函数,只会执行一次,特别是处理频繁变化的操作比如监听scroll时间时很有必要。

函数节流关键点就在于函数执行时需要记录当前时间,当再次被执行时比较时间间隔是否已经超过了设定的单位时间。

/**
 * 函数节流
 * @param {Function} fn 
 * @param {number} delay 延迟执行时间间隔(ms)
 */
function throttle(fn, delay) {
    let lastApplyTime = 0;
    return function(...args) {
        const self = this;
        const nowTime = Date.now();
        if(nowTime - lastApplyTime >= delay) {
            lastApplyTime = nowTime;
            return fn.apply(self, args);
        };
    }
}

除了计算时间戳来比较执行间隔的实现方式,还有一种实现方式是利用setTimeOut来达到延迟执行的效果。

/**
 * 函数节流
 * @param {Function} fn 
 * @param {number} delay 延迟执行时间间隔(ms)
 */
function throttle(fn, delay) {
    let waiting = false; // 是否处于执行状态
    let res = undefined; // 记录结果
    return function(...args) {
        const self = this;
        if(!waiting) {
            waiting = true;
        } else {
            return;
        }
        setTimeout(function(){
            res = fn.apply(self, args);
            waiting = false;
        }, delay);
        return res;
    }
}

手写防抖函数

函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。节流和防抖都是预防高频调用的一种手段,因为调用频率很高可能带来的效果是一样的,让代码在合理频率内调用有助于减轻CPU负担。

节流和防抖的区别在于:节流是先调用的优先执行,单位时间内后续会被调用忽略,而防抖正好相反,后调用的会刷新单位时间内的前置调用,优先执行后调用。此外,节流可以不使用异步,但防抖一定是异步。

/**
 * 函数防抖
 * @param {Function} fn 
 * @param {number} delay 延迟执行时间间隔(ms)
 */
function debounce(fn, delay) {
    let timer = undefined; // 当前执行timer
    let res = undefined; // 记录执行结果
    return function(...args) {
        const self = this;
        // 如果延迟还未执行结束,重新计时执行
        if(timer != undefined) {
            clearTimeout(timer);
        };
        timer = setTimeout(function(){
            res = fn.apply(self, args);
            timer = undefined;
        }, delay);
        return res;
    }
}

代码输出结果(Promise相关)

代码片段:

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

本题考察Promise.then的回调处理逻辑,我们知道Promise.then一定会返回一个新的Promise对象,但状态和状态值是由回调函数的返回值决定的,如果返回的是一个Promise对象那么直接当做新的Promise对象返回,否则返回值将作为新对象的状态值,而新对象的状态默认为fulfilled,除非捕获到执行错误。值得注意的是,如果回调参数传入的不是一个函数,则状态值继承调用者的状态值,但状态为fulfilled

  1. 成功状态值为1的Promise调用了then
  2. 执行then,回调为2,不是一个函数,相当于跳过这句代码
  3. 执行then,回调为一个Promise对象,同样不是一个函数,跳过
  4. 所以最终打印1,结束
1

代码片段:

Promise.reject('err!!!')
  .then((res) => {
    console.log('success', res)
  }, (err) => {
    console.log('error', err)
  }).catch(err => {
    console.log('catch', err)
  })

本题考查的是catch函数的用法,catch(call)函数实际上等同于then(undefined, call)

因此当第一个已拒绝状态的Promise调用then之后,得到的是一个已成功状态的Promise,然后用catch接受,是捕获不到错误的。

因此输出为:

error err!!!

爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1.  1 阶 + 1 阶 + 1 阶
2.  1 阶 + 2 阶
3.  2 阶 + 1 阶

因为每次只能爬1阶或2阶,那么上到第3阶就只有两种情况,从1阶或2阶上来,同理,上到第4阶,就只能从第2阶或第3阶上来,依此类推,上到第n阶,只能从第n-1阶或n-2阶上来,这就是一个简单的排列组合,组合使用加法,那么爬到n阶就是两种组合的加法:爬到n-1阶 + 爬到n-2阶。

我们用f(x)表示爬到第x阶的种数,那么就有:f(x) = f(x-1) + f(x-2)。有了这么一个公式,我们就可以使用动态规划来解题了:

var climbStairs = function(n) {
    let p = 0, q = 0, r = 1;
    for (let i = 1; i <= n; ++i) {
        p = q;
        q = r;
        r = p + q;
    }
    return r;
};