问题先导
- head标签有什么用?其中有哪个标签必不可少?【html】
- 对媒体查询的理解【css】
- 对css工程化的理解【css】
- 说一说Reflect对象【js基础】
- 说一说Generator对象【js基础】
- 说一说单页应用和多页应用的区别【Vue】
- Vue template到render的过程【Vue】
- Vue实例中data某一属性值更新后,视图会立即同步执行渲染吗【Vue】
- 手写浅拷贝【手写代码】
- 手写深拷贝【手写代码】
- 输出结果(Promise相关)【输出结果】
- 环形列表【算法】
知识梳理
head标签有什么用,其中什么标签必不可少?
HTML head 元素 规定文档相关的配置信息(元数据),包括文档的标题,引用的文档样式和脚本等。
对媒体查询的理解?
媒体查询(Media queries)是css3引入的一种方法,仅在设备环境与指定的媒体规则匹配时css才会生效,以此针对不同类型的设备来编写不同的css样式,达到响应式的效果。
最简单的媒体查询看来是这样的:
@media media-type and (media-feature-rule) {
/* CSS rules go here */
}
除了@media的写法,还支持使用media= 属性为style、link、source等元素指定特定的媒体类型。
<link rel="stylesheet" media="(max-width: 800px)" href="example.css" />
它由以下部分组成:
- 一个媒体类型,告诉浏览器这段代码是用在什么类型的媒体上的(例如印刷品或者屏幕);目前可选的媒体类型有:
- all(所有设备)
- print(打印预览模式下在屏幕上查看的分页材料和文档)
- screen(主要用于屏幕)
- speech(主要用于语音合成器)
- 一个媒体表达式,是一个被包含的CSS生效所需的规则或者测试。比如
width为视窗(viewport)的宽度,包括纵向滚动条的宽度。 - 一组CSS规则,会在测试通过且媒体类型正确的时候应用。
注:弹性盒、网格和多栏布局都给了你建立可伸缩的甚至是响应式组件的方式,而不需要媒体查询。这些布局方式能否在不加入媒体查询的时候实现你想要的设计,总是值得考虑的一件事。
参考:
对css工程化的理解?
和js工程化一样,当css规模达到一定程度后,css工程化也是很有必要的,css的组织、设计以及优化等问题就需要更专业的库来帮助我们。
一般来说,CSS 工程化是为了解决以下问题:
- 宏观设计:css代码如何组织、如何拆分、模块结构怎样设计?
- 编程优化:让css规则更优
- 自动化构建:自动打包
- 可维护性
一下三个方向都是比较流行的css工程化实践:
-
预处理器:less、sass等等。利用css预处理器自己独特设计的语法,让css的编辑更有逻辑性、更易读,这解决了css工程化宏观设计的问题。
预处理器普遍会具备这样的特性:
- 嵌套代码的能力,通过嵌套来反映不同 css 属性之间的层级关系 ;
- 支持定义 css 变量;
- 提供计算函数;
- 允许对代码片段进行 extend 和 mixin;
- 支持循环语句的使用;
- 支持将 CSS 文件模块化,实现复用。
-
后处理器:PostCss等,根据css规则对生成是css进行再编译整理,让其更优,目前最常做的是给
css属性添加浏览器私有前缀,实现跨浏览器兼容性的问题,有点使用babel.js来转换js的那种感觉。 -
webpack loader等加载器:webpack 的css-loader和style-loader能将css像插件一样动态加载到页面,解决了自动打包的问题
说一说Reflect对象
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers的方法相同。Reflect不是一个函数对象,因此它是不可构造的。
reflect是反射的意思,我们知道Proxy可以实现对象的代理,也能拦截对象的基本操作。实际上,这个对象没什么特别之处,我们完全可以把它看作是一个静态工具类,一个收集了对象拦截操作的工具类。
比如:
Reflect.apply等同于Function.proptype.applyReflect.constuctor等同于new操作符
等等。
说一说Generator对象
生成器对象是由一个 generator function 返回的,并且它符合可迭代协议和迭代器协议。
生成器对象是生成器函数的返回值,使用带*的函数就是生成器函数,搭配yield操作符就可以生成固定次序的数据:
function* gen() {
yield 1;
yield 2;
yield 3;
}
let g = gen(); // "Generator { }"
- Generator.prototype.next():返回一个由
yield表达式生成的值
比如一个无限迭代器的示例:
function* idMaker(){
let index = 0;
while(true)
yield index++;
}
let gen = idMaker(); // "Generator { }"
console.log(gen.next().value);
// 0
console.log(gen.next().value);
// 1
console.log(gen.next().value);
// 2
// ...
生成器对象是async/await语法糖的基础。更多相关说明可阅读:async和await:让异步编程更简单。
Vue单页应用和多页应用的区别
-
SPA单页面应用(SinglePage Web Application),指只有一个主页面的应用,一开始只需要加载一次js、css等相关资源。所有内容都包含在主页面,对每一个功能模块组件化。单页应用跳转,就是切换相关组件,仅仅刷新局部资源。
-
MPA多页面应用 (MultiPage Application),指有多个独立页面的应用,每个页面必须重复加载js、css等相关资源。多页应用跳转,需要整页资源刷新。
| 对比项 | 多页应用模式MPA | 单页应用模式SPA |
|---|---|---|
| 应用构成 | 由多个完整页面构成 | 一个外壳页面和多个页面片段构成 |
| 跳转方式 | 页面之间的跳转是从一个页面跳转到另一个页面 | 页面片段之间的跳转是把一个页面片段删除或隐藏,加载另一个页面片段并显示出来。这是片段之间的模拟跳转,并没有开壳页面 |
| 跳转后公共资源是否重新加载 | 是 | 否 |
| URL模式 | http://xxx/page1.html http://xxx/page1.html | http://xxx/shell.html#page1 http://xxx/shell.html#page2 |
| 用户体验 | 页面间切换加载慢,不流畅,用户体验差,特别是在移动设备上 | 页面片段间的切换快,用户体验好,包括在移动设备上 |
| 能否实现转场动画 | 无法实现 | 容易实现(手机app动效) |
| 页面间传递数据 | 依赖URL、cookie或者localstorage,实现麻烦 | 因为在一个页面内,页面间传递数据很容易实现 |
| 搜索引擎优化(SEO) | 可以直接做 | 需要单独方案做,有点麻烦 |
| 特别适用的范围 | 需要对搜索引擎友好的网站 | 对体验要求高的应用,特别是移动应用 |
| 搜索引擎优化(SEO) | 可以直接做 | 需要单独方案做,有点麻烦 |
| 开发难度 | 低一些,框架选择容易 | 高一些,需要专门的框架来降低这种模式的开发难度 |
更多细节可参考:
Vue tempalte到render的过程
简单来说,Vue最核心的三部分,即:compiler、reactivity、runtime。
-
compiler(编译):表示template编译成有规律的数据结构,即AST抽象语法树。
-
reactivity(数据响应):表示data数据可以被监控,通过Object.defineProperty或proxy语法来实现。
-
runtime(运行时)表示运行时相关功能,虚拟DOM(即:VNode)、diff算法、真实DOM操作等。
其中template说的就是编译模板的逻辑,render就是渲染视图的逻辑,渲染一般使用render function,我们可以手动编写,就可以引用runtime运行时部分的代码,节省了template编译为render function的过程,而这部分代码也占据很大一部分内存。
编译器compiler主要编译过程如下:
- 将template模板转换为ast(抽象语法树):ast可以更有逻辑地存储模板信息,便于后续整理操作
- 通过
optimize函数对静态节点做优化,给其打一个标记,为后续更新渲染可以直接跳过静态节点做优化。静态节点的意思就是这部分DOM不是响应式的,不会更新,因此不需要监听和检测,以此来优化更新效率。 - 生成render字符串:通过
generate函数生成render字符串并将静态部分放到 staticRenderFns 中,最后通过new Function(render)生成render函数。
备注:render函数就是生成虚拟节点的函数,而虚拟节点是对频繁更新DOM有很好优化的一种结构,而AST是compiler中把模板编译成有规律的数据结构,方便转换成render函数所存在的;虚拟节点VNode是优化DOM操作的,减少频繁DOM操作的,提升DOM性能的。
Vue实例data中的某一个属性值发生改变后,视图会立即同步执行重新渲染吗?
不会立即同步执行重新渲染。Vue实现响应式更新但并不是立即更新DOM,而是异步更新的,且按照一定策略进行更新,已达到最优渲染:**Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。**这对某些频繁操作比如watcher的数据监听是有益的,当短时间内多次触发同一个watcher,这些变化会在缓冲队列中去重,避免不必要的计算,和js里常用的防抖和节流是一样的优化思路。
实现浅拷贝
一些现成的API就可以实现浅拷贝:
- Object.assign()
- Object.create()也是浅拷贝,原理同
new关键字一致 - 扩展运算符,原理同
Object.assign - 数组的
slice等方法也是浅拷贝
手写浅拷贝:
function shallowCopy(obj) {
if(!obj || typeof obj != 'object') {
return {};
}
let newObject = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObject[key] = obj[key];
}
}
return newObject;
}
浅拷贝就是直接复用原引用类型的引用,而不是新建一份数据,因此原数据改变,会导致所有引用的地方发生变化,这就是浅拷贝。
实现深拷贝
- 使用
JSON.parse(JSON.stringify(obj))是最常见的深拷贝方法之一,原理就是利用json的序列化和反序列化来创建一个新的对象。但是这个方法存在一些限制,就是json不支持序列化的对象就会消失,比如函数、自定义的一些对象等等。 - 使用
lodash库的cloneDeep方法
下面是手写实现深拷贝的实现:
function deepCopy(obj) {
if(!obj || typeof obj != 'object') {
return {};
}
let newObject = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObject[key] = obj[key] && typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
}
}
return newObject;
}
实际上,和浅拷贝的区别就在于遇到对象时再展开拷贝,而不是直接赋值引用,这样就能实现深拷贝了。
这里函数我们也是直接赋值的,应该对于函数这种类型来说一般我们不会去修改它。
输出结果(Promise相关)
代码片段:
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
Promise.all([runAsync(1), runAsync(2), runAsync(3)]).then(res => console.log(res))
本题考查Promise.all的用法。Promise.all函数会并行执行所以异步函数,如果都成功则输出同参数迭代顺序一致的包含所有Promise的状态值的数组,如果有一个失败就直接返回这个失败的Promise。
所以输出结果很明显:
1
2
3
[1, 2, 3]
代码片段:
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
function runReject (x) {
const p = new Promise((res, rej) => setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x))
return p
}
Promise.all([runAsync(1), runReject(4), runAsync(3), runReject(2)])
.then(res => console.log(res))
.catch(err => console.log(err))
由于是并行执行,所以异步执行时间决定了哪个异步先执行完毕。
- runAsync(1)执行,1s后触发settimeout异步回调,打印1,状态为成功
- runReject(4)执行,4s后触发settimeout异步回调,打印4,状态失败
- runAsync(31)执行,1s后触发settimeout异步回调,打印3,状态为成功
- runReject(2)执行,2s后触发settimeout异步回调,打印2,状态失败,Promise.all捕获到错误,异步执行catch回调
注意回调触发的先后顺序:
// 1s
1
3
// 2s
2
Error: 2
// 4s
4
环形链表
给定一个链表,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
本题十分简单,就是遍历链表,判断有没有链表指向了遍历过的节点即可。
/**
* @param {ListNode} head
* @return {boolean}
*/
var hasCycle = function(head) {
const node_caches = [];
let next = head;
while(next) {
if(node_caches.includes(next)) {
return true;
}
node_caches.push(next);
next = next.next;
}
return false;
};
这种思路对于环靠前的比较有优势,遇到就可以结束循环,但如果换位置比较靠后,节点的缓存空间无疑越来越大。
本题还有一个特别的思路,就是设置快慢指针,我们让慢指针每次前进一步,而快指针前进两步甚至更多步,这样,如果遇到环,快慢指针都会开始“绕圈”,由于快指针比慢指针快,总会追上慢指针,如果遍历结束快指针都没有与慢指针相遇,说明没有环。
/**
* @param {ListNode} head
* @return {boolean}
*/
var hasCycle = function(head) {
let slow = head, quick = head && head.next;
while(quick) {
if(slow == quick || slow == quick.next) {
return true;
}
slow = slow.next;
quick = quick.next && quick.next.next;
}
return false;
};