问题先导
-
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的创建比其它包括sscripts和css等 DOM 元素慢了1-2个数量级。ifrmae与主页面共享连接池,会影响主页面的并行加载。- 不利于搜索引擎SEO
- 容易造成页面结构混乱
参考:
对line-height的理解及其赋值方式
line-height属性设置行间的距离(行高)。line-height与 font-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中。
注意::before是css2的写法,现在已经不使用这种写法了,改为::before。
参考:
display:inline-block什么时候会显示间隙?
- 有空白字符时,浏览器会把元素之间的空白字符渲染为一个空格。
margin为正时,外边距的空白看起来像是间隙letter-spacing和word-spcing可能让文本之间的空白看起像像间隙
说一说Proxy的应用场景
Proxy是ES6新增的对象,用于创建一个对象的代理,从而实现被代理对象基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
在这个对象出现之前,我们可以使用Object.defineProperty来定义对象,通过设置该属性的getter/setter属性,就能实现属性的调用、修改拦截。但是,这些捕获对某些内部封装的原生方法或操作符是无法捕获的,比如数组的push、pop等方法,也不能监听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的Promise调用了
then - 执行
then,回调为2,不是一个函数,相当于跳过这句代码 - 执行
then,回调为一个Promise对象,同样不是一个函数,跳过 - 所以最终打印
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;
};