问题先导
-
canvas和svg的区别?【html】 - css有哪些优化可以提高性能?【css】
- css预处理器/后处理器是什么?为什么要使用它们?【css】
-
new操作符的创建原理?【js基础】 -
Map和Object的区别?【js基础】 -
WeakMap和Map又有什么区别?【js基础】 - Vue是如何实现原生方法的监听的?【Vue】
- 柯里化函数实现【手写代码】
- Ajax请求实现【手写代码】
- 输出结果(Promise相关)【输出结果】
- 二叉搜索树中第K小的元素【算法】
知识梳理
canvas和svg的区别?
Canvas API 提供了一个通过JavaScript 和 HTML的canvas元素来绘制图形的方式。它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。Canvas API主要聚焦于2D图形。而同样使用<canvas>元素的 WebGL API 则用于绘制硬件加速的2D和3D图形。
SVG,Scalable Vector Graphics的缩写,即可缩放矢量图形,是W3C XML的分支语言之一,用于标记可缩放的矢量图形。目前SVG在Firefox、Opera、Webkit浏览器、IE等浏览器中已经部分实现。
两者都可用于绘制矢量图形,区别在于绘制方式不同,Canvas是基于脚本在canvas画布上进行绘制,但svg是XML标准语言,也就是用标签来绘制图形,而且存在于BOM中。
也就是说svg基于XML元素进行图形描述,图形之间比较独立,支持事件处理,由于基于XML,DOM可能会过度使用,渲染也就较慢。
而Canvas是基于像素来绘制图形的,而且使用WebGL启用硬件加速,渲染效率较高,但整个画布融为一体,如果需要修改某处显示,就需要整个画布重新渲染。
两者各有优缺点,svg适合小型图像绘制,兼容性也更好,而canvas适合频繁变更图像的绘制。
参考:
css有哪些优化可以提高性能?
- 加载性能
- css压缩:减少css体积,减少传输带宽。
- 尽量使用css单一样式:比如只需要修改上下边距时,只使用
margin-bottom和margin-top的效率会比统一使用margin要快一些。 - 减少使用
@import而改用link,因为前者会等待页面加载结束后再加载,而后者是并行加载。
- 选择器性能
- 避免使用通配符
- 优先考虑类选择器代替元素选择器
- 尽量少地使用后代选择器,最高不要超过三层,可以用类来组合这些元素
- 对于可继承的属性,没必要再重复指定规则,除非不希望使用继承值
- 渲染性能
- 谨慎使用高性能属性:浮动、定位。因为页面回流一般就需要重新计算这些属性的准确位置
- 尽量减少页面的回流、重绘
- 属性值为0时,不加单位,可以减少运算次数
- 小图标使用图片精灵或者base64编码加载
- 不滥用web字体,因为中文网站对webFonts可能很陌生,需要下载体积庞大的字体数据,从而阻塞页面渲染
- 可维护性
- 相同属性的样式抽离出来,使用
class来统一控制 - 样式与html分离,便于管理
- 相同属性的样式抽离出来,使用
css预处理器/后处理器是什么?为什么要使用它们?
预处理器, 如:less,sass,stylus,用来预编译sass或者less,增加了css代码的复用性。层级,mixin, 变量,循环, 函数等对编写以及开发UI组件都极为方便。
后处理器, 如: postCss,通常是在完成的样式表中根据css规范处理css,让其更加有效。目前最常做的是给css属性添加浏览器私有前缀,实现跨浏览器兼容性的问题。
css预处理器为css增加一些编程特性,无需考虑浏览器的兼容问题,可以在CSS中使用变量,简单的逻辑程序,函数等在编程语言中的一些基本的性能,可以让css更加的简洁,增加适应性以及可读性,可维护性等。其它css预处理器语言:Sass(Scss), Less, Stylus, Turbine, Swithch css, CSS Cacheer, DT Css。
使用原因:
- 结构清晰, 便于扩展
- 可以很方便的屏蔽浏览器私有语法的差异
- 可以轻松实现多重继承
- 完美的兼容了
CSS代码,可以应用到老项目中
new操作符的创建原理?
new运算符创建具有构造函数的对象实例。
new关键字会执行如下操作:
-
创建一个空对象
-
链接该对象的
constructor到构造函数本身,这个值也等于构造函数原型上的constructor。比如({}).constructor === Object.prototype.constructor结果为true,同时Object.prototype.constructor === Objec也等于本身。这一步我们也可以理解为让该对象继承构造函数原型对象的属性
-
调用构造函数,并让新对象作为
this上下文 -
如果函数没有返回值,则返回这个新对象,有返回值则直接返回该值
我们可以自己实现一下:
/**
* new操作符实现
* @param {Function} constructor 构造函数
* @param {...any} args 参数
*/
function instanceFactory(constructor, ...args) {
// #使用构造函数原型来创建一个新对象
const instance = Object.create(constructor.prototype);
// #让新对象作为 this 上下文
const res = constructor.apply(instance, args);
return (typeof res === 'function' || typeof res === 'object') ? res : instance;
}
Map和Object的区别?
Map 对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者原始值) 都可以作为一个键或一个值。
Objects Maps 类似的是,它们都允许你按键存取一个值、删除键、检测一个键是否绑定了值。因此(并且也没有其他内建的替代方式了)过去我们一直都把对象当成 Maps 使用。不过 Maps 和 Objects 有一些重要的区别,在下列情况里使用 Map 会是更好的选择:
| Map | Object | |
|---|---|---|
| 意外的键 | Map 默认情况不包含任何键。只包含显式插入的键。 | 一个 Object 有一个原型, 原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。注意: 虽然 ES5 开始可以用 Object.create(null) 来创建一个没有原型的对象,但是这种用法不太常见。 |
| 键的类型 | 一个 Map的键可以是任意值,包括函数、对象或任意基本类型。 | 一个Object 的键必须是一个 String 或是Symbol。 |
| 键的顺序 | Map 中的 key 是有序的。因此,当迭代的时候,一个 Map 对象以插入的顺序返回键值。 | 一个 Object 的键是无序的注意:自ECMAScript 2015规范以来,对象确实保留了字符串和Symbol键的创建顺序; 因此,在只有字符串键的对象上进行迭代将按插入顺序产生键。 |
| Size | Map 的键值对个数可以轻易地通过size 属性获取 | Object 的键值对个数只能手动计算 |
| 迭代 | Map 是 iterable 的,所以可以直接被迭代。 | 迭代一个Object需要以某种方式获取它的键然后才能迭代。 |
| 性能 | 在频繁增删键值对的场景下表现更好。 | 在频繁添加和删除键值对的场景下未作出优化。 |
相比之下,map比object更像一个真正的映射表,支持的功能也更多。
参考:
Map和WeakMap的区别?
WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。WeakMap 的 key 只能是 Object 类型。
new WeakMap([iterable])
Iterable 是一个数组(二元数组)或者其他可迭代的且其元素是键值对的对象。每个键值对会被加到新的 WeakMap 里。null 会被当做 undefined。
总的来说,WeekMap和Map除了键必须为对象(也就是引用类型数据)外,没有太大区别。
为什么要叫弱引用,我们先来看一个现象:
let obj = {
name: 'Jinx',
age: 23,
};
const map = new Map();
map.set(obj, true);
obj = null;
console.log(obj)
最开始obj的赋值引用就是强引用,当我们设置obj=null时,先前被设置的强引用被移除,正常情况下这个obj对象在没有被引用的时候就会被垃圾回收(GC),但由于map中的键引用了这个对象,因此不能被收回。但是如果我们把这个对象换成WeakMap,这里的键引用就是弱类型,当垃圾回收时这个对象就会被回收,而这个弱引用对象中的键值也会消失。
相比之下,原生的 WeakMap 持有的是每个键对象的“弱引用”,这意味着在没有其他引用存在时垃圾回收能正确进行。原生 WeakMap 的结构是特殊且有效的,其用于映射的 key 只有在其没有被回收时才是有效的。
正由于这样的弱引用,WeakMap 的 key 是不可枚举的 (没有方法能给出所有的 key)。如果key 是可枚举的话,其列表将会受垃圾回收机制的影响,从而得到不确定的结果。因此,如果你想要这种类型对象的 key 值的列表,你应该使用 Map)。
为什么要引入WeakMap可以看这篇官方说明:Why WeakMap?。
参考:
Vue中如何实现对数组原生方法的监听?
在Vue中,对响应式处理利用的是Object.defineProperty对数据进行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以需要对这些操作进行hack,也就是重写了原生方法,增加了响应式处理逻辑,让Vue能监听到其中的变化。
// 缓存数组原型
const arrayProto = Array.prototype;
// 实现 arrayMethods.__proto__ === Array.prototype
export const arrayMethods = Object.create(arrayProto);
// 需要进行功能拓展的方法
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse"
];
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function(method) {
// 缓存原生数组方法
const original = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
// 执行并缓存原生数组功能
const result = original.apply(this, args);
// 响应式处理
const ob = this.__ob__;
let inserted;
switch (method) {
// push、unshift会新增索引,所以要手动observer
case "push":
case "unshift":
inserted = args;
break;
// splice方法,如果传入了第三个参数,也会有索引加入,也要手动observer。
case "splice":
inserted = args.slice(2);
break;
}
//
if (inserted) ob.observeArray(inserted);// 获取插入的值,并设置响应式监听
// notify change
ob.dep.notify();// 通知依赖更新
// 返回原生数组方法的执行结果
return result;
});
});
柯里化函数实现
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。函数柯里化是函数式编程的基础概念。
科里化的关键在于判断传入的参数数量,如果大于等于函数参数数量,则直接调用函数执行,否则就将传入的参数缓存起来,然后返回一个接收剩余参数的函数。
/**
* 函数柯里化
* @param {Function} fn
* @param {...any} arg
* @returns *
*/
function curry(fn, ...arg) {
return arg.length >= fn.length ? fn.apply(this, arg) : curry.bind(this, fn, ...arg);
}
Ajax请求的实现
AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。
Ajax不是一种技术,而是一种术语,也就是一些现有技术的合集:异步、JavaScript、XML和XMLHTTPRequest,核心就是XMLHTTPRequest对象。XMLHTTPRequest对象用于和服务器通信,关键点在于通信过程是异步的,也就是页面无需等待通信结果,而通信内容就是我们需要的XML等内容,实际上,JSON是比较通用也是最常用的ajax交互数据,当异步拿到这些数据,再来更新页面,用户无需等待这个过程交互体验就很良好。
总结一下就是:
- XMLHTTPRequest像服务器发起异步请求:使用
open函数初始化请求,设置完相关请求参数后,使用send发起请求。 - 浏览器主线程继续执行别的任务
- 服务器返回请求数据,
onreadystatechange函数接收到状态变更信息,执行请求回调 - 如果请求失败,使用
onerror函数可以监听到对应异常信息
根据这个逻辑我们就能写一段Ajax请求的逻辑:
const url = '/server';
const xhr = new XMLHTTPRequest();
// 初始化请求信息:设置请求方法、请求路径和是否异步
xhr.open('GET', url, true);
// 设置响应类型、请求头信息
xhr.responseType = 'json';
xhr.setRequestHeader("Accept", "application/json");
// 监听状态变化
xhr.onreadystatechange = function(){
if(this.readSate !== 4){
return;
}
if (this.status === 200) {
console.log('responseData:', this.response);
} else {
console.error(this.statusText);
}
}
// 设置请求失败回调
xhr.onerror = function() {
console.error(this.statusText);
};
// 发送 Http 请求
xhr.send(null);
提取上述变量,就可以用Promise对象封装出更方便的ajax函数。
参考:
输出结果(Promise相关)
代码片段:
Promise.resolve('1') // Promise1
.then(res => {
console.log(res)
})
.finally(() => {
console.log('finally')
})
Promise.resolve('2') // Promise2
.finally(() => {
console.log('finally2')
return '我是finally2返回的值'
})
.then(res => {
console.log('finally2后面的then函数', res)
})
本题考查Promise.finally的执行逻辑,也就是无论Promise的状态成功与否都会调用,返回的新Promise对象继承自上一个Promise的状态和值。
- Promise1的
then回调加入微任务队列 - Promise2的
finally回调加入微任务队列 - 执行微任务:打印1,2步骤的回调
- promise1.then的
finally回调加入微任务队列 - promise2.finally的
then回调加入微任务队列 - 执行微任务:打印4,5的回调
1
finally2
finally
finally2后面的then函数 2
代码片段:
Promise.resolve('1')
.finally(() => {
console.log('finally1')
throw new Error('我是finally中抛出的异常')
})
.then(res => {
console.log('finally后面的then函数', res)
})
.catch(err => {
console.log('捕获错误', err)
})
本题考查Promise.finally回调抛出异常的处理逻辑:不再继承上一个Promise的状态和值,直接返回一个已拒绝状态的Promise。
finally1
捕获错误 Error: 我是finally中抛出的错误
二叉搜索树中第K小的元素
给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。
输入:root = [3,1,4,null,2], k = 1
输出:1
输入:root = [5,3,6,2,4,null,null,1], k = 3
输出:3
寻找第k大的元素,一般的做法是排完序,然后就能直接得到答案了。本题我们可以使用有优化的排序算法,可以不完全排序结束,就能得到答案,比如:快排(二分法)、堆排序(利用大小根堆)。
树的遍历有深度优先和广度优先两种,但本题的树不是一般的树,是二叉搜索树(BST),BST是满足这样性质的树:
- 左子树所有节点均小于根节点
- 由子树所有节点均大于根节点
- 左右子树也是BST树
注:和堆结构不同在于,堆结构是完全二叉树,且堆的节点是和左右孩子比较,不是和左右子树所有节点比较。
这种树结构有一个特点就是中序遍历就是升序排序,所以我们中序遍历后就能得到一个升序排列的数组。
递归中序遍历:
let kthSmallest = function(root, k) {
let res = null
let inOrderTraverseNode = function(node) {
if(node !== null && k > 0) {
// 先遍历左子树
inOrderTraverseNode(node.left)
// 然后根节点
if(--k === 0) {
res = node.val
return
}
// 再遍历右子树
inOrderTraverseNode(node.right)
}
}
inOrderTraverseNode(root)
return res
}
递归需要遍历完整颗树,实际上,遍历是升序的过程,当存储升序的数组长度变为k时,这个数其实就是我们需要的数,后面的数都比这个数大,就没必要继续遍历了。
迭代中序遍历:
let kthSmallest = function(root, k) {
let stack = []
let node = root
while(node || stack.length) {
// 遍历左子树
while(node) {
stack.push(node)
node = node.left
}
node = stack.pop()
if(--k === 0) {
return node.val
}
node = node.right
}
return null
}