四、数据结构和算法
JavaScript编码能力
-
多种方式实现数组去重、扁平化、对比优缺点
数组去重
1.遍历数组:新建一个数组,遍历需去重的数组,当值不再新数组时indexOf === -1 就加入新数组
2.排序后相邻去除法:给传入的数组排序,排序后相同的值会相邻,然后遍历排序后数组时,新数组只加入不与前一值重复的值。
3.优化遍历数组法:双层循环
4.ES6中的Set数据结构
数组扁平化
1.ES6中的flat(),可带参数,表示拉平层数,默认1,未知Infinity
2.循环数组+递归调用
3.apply+some:
function steamroller2(arr){
while(arr.some(item=> Array.isArray(item))){
arr=[].concat.apply([],arr)
}
return arr
}
console.log(steamroller2(arr))
4.reduce方法
function steamroller3(arr){
return arr.reduce((prev,next)=>{
return prev.concat(Array.isArray(next)?steamroller3(next):next)
},[])
}
console.log(steamroller3(arr))
5.es6展开运算符 :
function steamroller4(arr){
while(arr.some(item=> Array.isArray(item))){
arr=[].concat(...arr)
}
return arr
}
-
多种方式实现深拷贝、对比优缺点
1.递归实现:
function deep(obj) {
//判断拷贝的要进行深拷贝的是数组还是对象,是数组的话进行数组拷贝,对象的话进行对象拷贝
var objClone = Array.isArray(obj) ? [] : {};
//进行深拷贝的不能为空,并且是对象或者是
if (obj && typeof obj === "object") {
for (key in obj) {
if (obj.hasOwnProperty(key)) {
if (obj[key] && typeof obj[key] === "object") {
objClone[key] = deep(obj[key]);
} else {
objClone[key] = obj[key];
}
}
}
}
return objClone;
}
2. 通过JSON对象实现:无法实现对对象中方法的深拷贝
//通过js的内置对象JSON来进行数组对象的深拷贝
function deepClone2(obj) {
var _obj = JSON.stringify(obj),
objClone = JSON.parse(_obj);
return objClone;
}
3.lodash函数库实现: lodash.cloneDeep()
4.Jquery的extend()方法
注意:Object.assign(),slice(),concat()方法进行拷贝只能实现一级属性的拷贝成功,二级以下仍脱离不了,算不上真正的深拷贝
-
手写函数柯里化工具,并理解其应用场景和优势
柯里化,是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
优势:1.参数复用 2.提前确认 3.延迟运行
通用封装方法:
// 初步封装
var currying = function(fn) {
// args获取第一个方法内的全部参数
var args = Array.prototype.slice.call(arguments, 1);
return function() {
// 将后面方法里的全部参数和args进行合并
var newArgs = args.concat(Array.prototype.slice.call(arguments));
// 把合并后的参数通过apply作为fn的参数并执行
return fn.apply(this, newArgs);
}
}
// 支持多参数传递
function progressCurrying(fn, args) {
var _this = this
var len = fn.length;
var args = args || [];
return function() {
var _args = Array.prototype.slice.call(arguments);
Array.prototype.push.apply(args, _args);
// 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
if (_args.length < len) {
return progressCurrying.call(_this, fn, _args);
}
// 参数收集完毕,则执行fn
return fn.apply(this, _args);
}
}
-
手写防抖和节流工具函数,并理解其内部原理和应用场景
防抖:触发事件后再n秒内函数只执行一次,如果再n秒内又触发了事件,则重新计算函数执行时间。
应用场景:
1. 给按钮加防抖防止表单多次提交;
2. 输入栏连续输入进行AJAX验证,防抖减少请求次数;
3. 判断scroll是否滑到底部;
适合多次事件一次响应的情况。
/**
* @desc 函数防抖
* @param func 函数
* @param wait 延迟执行毫秒数
*/
function debounce(func,wait) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout);
let callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
}
}
节流:指连续触发事件但是再n秒内只执行一次函数。
适用场景:游戏中的刷新率;DOM元素拖拽,Cancas画笔功能
总体来说,适合大量事件按时间做平均分配触发
function throttle(fn, gapTime) {
let _lastTime = null;
return function () {
let _nowTime = + new Date()
if (_nowTime - _lastTime > gapTime || !_lastTime) {
fn();
_lastTime = _nowTime
}
}
}
-
实现一个sleep函数
sleep函数:让线程休眠等到值定时间再重新唤起
// 第一种实现方法
function sleep(numberMillis) {
var now = new Date();
var exitTime = now.getTime() + numberMillis;
while (true) {
now = new Date();
if (now.getTime() > exitTime){
break;
}
}
}
// 第二种实现方法
function sleep(numberMillis) {
var start = new Date().getTime();
while (true) {
if (new Date().getTime() - start > numberMillis) {
break;
}
}
}
// 方法三
function sleep(ms, callback) {
setTimeout(callback, ms);
}
// 方法四
function sleep(ms) {
return new Promise(
function(resolve, reject){
setTimeout(resolve, ms);
}
)
}
手动实现前端轮子
-
手动实现call、apply、bind
实现call
将目标函数的this指向传入的第一个对象,参数不定长,且立即执行
function.prototype.mycall = function(obj){
var args = Array.prototype.slice.apply(arguments, [1]);
obj.fn = this; // 在obj上添加fn属性,值是this
obj.fn(...args); // 在obj上调用函数,那函数的this值就是obj
delete obj.fn; // 伤处obj的fn属性,去除影响
}
使用eval方法,回对传入的字符串,当作JS代码进行解析执行
function.prototype.mycall = function(obj){
obj = obj||window;
var args = [];
for(var i = 1 ; i < arguments.length; i++) {
args.push('arguments[' + i + ']');
// 不能直接push值,因为会导致参数为数组时eval调用回将其转换成字符串
}
obj.fn = this;
eval('obj.fn('+args+'));
delete obj.fn;
}
实现apply
apply和call区别只有一个,apply第二个参数为数组
function.prototype.myApply = function(obj, arr) {
obj.fn = this;
if (!arr) {
obj.fn();
}else{
var args = []
for(let i = 0;i < arr.length;i++) {
args.push('arr[' + i + ']')
}
eval('obj.fn('+args+')');
}
delete obj.fn;
}
实现bind
返回一个被调用函数具有相同函数体的新函数,且之歌新函数也能使用new操作符。
Function.prototype.myFind = function(obj){
if(obj === null || obj === undefined){
obj = window;
} else {
obj = Object(obj);
}
let _this = this;
let argArr = [];
let arg1 = [];
for(let i = 1 ; i<arguments.length ; i++){
arg1.push( arguments[i] );
argArr.push( 'arg1[' + (i - 1) + ']' ) ;
}
// 下面可用apply
return function(){
let val ;
for(let i = 0 ; i<arguments.length ; i++){
argArr.push( 'arguments[' + i + ']' ) ;
}
obj._fn_ = _this;
console.log(argArr);
val = eval( 'obj._fn_(' + argArr + ')' ) ;
delete obj._fn_;
return val
};
}
-
手动实现符合Promise/A+规范的Promise、手动实现async await
Promise内部维护着三种状态,即pending,resolved和rejected。初始状态是pending,状态可以有pending--->relolved,或者pending--->rejected.不能从resolve转换为rejected 或者从rejected转换成resolved.
即 只要Promise由pending状态转换为其他状态后,状态就不可变更。
-
手写一个EventEmitter实现事件发布、订阅
-
可以手动实现两种实现双向绑定的方案
-
手写JSON.stringify、JSON.parse
原生js实现JSON.parse()和JSON.stringify()
-
手写一个模板引擎,并能解释其中原理
正则匹配并替换字符串中 {{}} 中的值
function template(str, data) {
var reg = /{{([a-zA-Z0-9_$][a-zA-Z0-9\.]+)}}/g;
return str.replace(reg, function(raw, key, offset, string) {
var paths = data,
ary = key.split('.');
while(ary.length > 0) {
paths = paths[ary.shift()];
}
return paths || raw;
});
}
-
手写懒加载、下拉刷新、上拉加载、预加载等效果
懒加载的目的是作为服务器前端的优化,减少请求数或延迟请求数
实现方式:
1. 纯粹的延迟加载,使用setTimeOut进行加载延迟
2. 条件加载,符合条件或触发事件再开始异步加载
3. 可视区加载,监控滚动条来实现
var num = document.getElementsByTagName('img').length;
var img = document.getElementsByTagName("img");
// 存储图片加载到的位置,避免每次都从第一张图片开始遍历
var n = 0;
// 页面载入完毕加载可视区域内的图片
lazyLoad();
window.onscroll = lazyLoad;
// 监听页面滚动事件
function lazyLoad() {
// 可见区域高度
var seeHeight = document.documentElement.clientHeight;
// 滚动条距离顶部高度
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
for (var i = n; i < num; i++) {
// 图片距离上方的距离小于可视区高度加滚动条距上方高度
if (img[i].offsetTop < seeHeight + scrollTop) {
if (img[i].getAttribute("src") == "default.jpg") {
img[i].src = img[i].getAttribute("data-src");
}
n = i + 1;
}
}
}
预加载是牺牲服务器前端性能,换取更好的用户体验
实现方式:
1.用CSS和JS实现预加载
2.仅使用JS实现预加载
3.使用Ajax实现预加载
function preLoadImg(url, callback) {
var img = new Image(); //创建一个Image对象,实现图片的预下载
img.src = url;
if (img.complete) { // 如果图片已经存在于浏览器缓存,直接调用回调函数
callback.call(img);
return; // 直接返回,不用再处理onload事件
}
img.onload = function () { //图片下载完毕时异步调用callback函数。
callback.call(img);//将回调函数的this替换为Image对象
};
};
数据结构
-
理解常见数据结构的特点,以及他们在不同场景下使用的优缺点
数据结构是以某种形式将数据组织在一起的集合,它不仅存储数据,还支持访问和处理数据的操作。
常见的数据结构:
1.线性表(数组):
数组是基础,为数组封装好一个List构造函数,增加长度、插入、删除、索引等工具结构。
数组的索引下标需要在js语言内部转换为js对象的属性名,因此效率打了折扣
2.栈:具有后进先出的特点,是一种高效的列表,只对栈顶的数据进行添加和删除
3.队列:具有先进先出的特点,是只能在队首取出或删除元素,在队尾插入元素的列表。
4.链表:链表是由一组节点组成的集合。
5.字典及散列(object):散列也叫做散列表,在散列表上插入、删除和取用数据非常快,但对查找操作来说效率低下。
6.集合(set):是一种包含不同元素的数据结构。特点是无序且各不相同。
7.树:树是非线性,分层存储的数据结构,可用来存储文件系统或有序列表。
-
理解数组、字符串的存储原理,并熟练应用他们解决问题
数组是一个连续的内存分配,但在JS中不是连续分配的,类似哈希映射的方式存在的。
字符串是存储在栈中的
-
理解二叉树、栈、队列、哈希表的基本结构和特点
二叉树(Binary Tree)是一种树形结构,它的特点是每个节点最多只有两个分支节点,一棵二叉树通常由根节点,分支节点,叶子节点组成。而每个分支节点也常常被称作为一棵子树。js数据结构-二叉树(二叉搜索树)
哈希表也称为散列表。是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
-
了解图、堆的基本结构和使用场景
图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V(vertex)是图G中顶点的集合,E(edge)是图G中边的集合。
算法
-
可计算一个算法的时间复杂度和空间复杂度,可估计业务逻辑代码的耗时和内存消耗
算法是指用来操作数据、解决程序问题的一组方法。
-
至少理解五种排序算法的实现原理、应用场景、优缺点,可快速说出时间、空间复杂度
-
了解递归和循环的优缺点、应用场景、并可在开发中熟练应用
递归:
函数内部调用这个函数本身。代码简洁,但时间空间消耗大。会有重复计算,栈溢出的问题。
循环:
通过设置初始值和终止条件,在一个范围内重复运算。代码可读性差,但效率高。
-
可应用回溯算法、贪心算法、分治算法、动态规划等解决复杂问题
回溯算法:是一种选优搜索法,又称试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法。而满足回溯条件的某个状态的点称为“回溯点”。
贪心算法:对问题进行求解时,在每一步选择中采取最好或最优的选择,从而希望能够导致结果是最好或最优的算法。
分治算法:就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
动态规划:可以把原始问题分解为相关联的子解问题,并通过求取和保存子问题的解,获得原问题的解。找到问题的状态描述和状态转移方程。
-
前端处理海量数据的算法方案