1. 实现一个函数 add(1)(2, 3)(4).getValue()
function add() {
let arr = [...arguments];
function getValue() {
const result = arr.reduce((a, b) => a + b);
arr = [];
return result;
}
function temp() {
arr = [...arr, ...arguments];
return temp;
}
temp.getValue = getValue;
return temp;
}
// 第二种
function add() {
const result = Array.prototype.slice.call(arguments).reduce((pre, cur) => (pre += cur), 0);
const fn = function() {
return add.apply(null, [result, ...Array.prototype.slice.call(arguments)]);
};
fn.getValue = function() {
return result;
};
return fn;
}
add(1).getValue();
add(1)(2, 3).getValue();
add(1)(2, 3)(4).getValue();
var tmpl = `
name is {obj.a}
age is {obj.b.c}
address is {obj.c.d}
phone is {obj2.a}
`;
var data = {
obj: {
a: 1,
b: {c: {d:2}},
c: false
}
}
根据上面给出的数据写出一个函数 render 返回的结果为:
"
name is 1 // 基本类型返回,,没有找到就返回字符串
age is {"d":2} // 对象类型直接JSON.stringify处理
address is obj.c.d // 没有找到就直接拼接字符串
phone is obj2.a
"
function render(tmpl, data){
// TODO...
}
function render(tmpl, data) {
const A = tmpl.match(/(?<={).*(?=})/g); // ["obj.a", "obj.b.c", "obj.c.d", "obj2.a"]
const B = tmpl.match(/{.*}/g); // ["{obj.a}", "{obj.b.c}", "{obj.c.d}", "{obj2.a}"]
A.forEach((v, i) => {
let res;
try {
res = eval(`data.${v}`);
} catch (e) {
res = v;
}
if (res instanceof Object) {
res = JSON.stringify(res);
}
if (res === undefined) {
res = v;
}
tmpl = tmpl.replace(`${B[i]}`, res);
});
return tmpl;
}
2. 如何加快页面渲染速度 / 网站性能提升
静态资源优化
- 减少 http 请求数:合并 css、js,制作雪碧图,以及使用 http 缓存
- 减少资源大小:压缩文件、图片,小图可以用 base64 编码
- 使用 CDN 加速和缓存
- 异步组件和图片懒加载
接口访问优化
- 使用 http 长连接 connection: keep-alive
- 冷数据接口用 localStorage 缓存,减少请求
- 后端优化合并接口
页面渲染速度优化
- 由于浏览器的 GUI 线程与 JS 引擎线程是互斥的,在执行 JS 的时候会阻塞它的渲染。会阻塞 DOM 渲染,所以一般会把 css 放在顶部有限渲染,js 放在底部
- 减少页面的重绘和回流
- 使用虚拟 DOM 渲染方案,做到最小化操作真实的 dom
- 减少操作 DOM
- 事件代理:利用事件冒泡原理,将函数注册到父级元素上
3. 判断以下的值
0 == '' // true
0 == '0' // true
1 == '1' // true
'0' == '' // false
false == 0 // true
false == '0' // true
false == 'false' // false
false == null // false
true == null // false
false == undefined // false
true == undefined // false
null == undefined // true
NaN == NaN // false
// ----- 分割线 -------------------------
0 === '' // false
0 === '0' // false
1 === '1' // false
false === '0' // false
null === undefined // false
// --------- 分割线 -------------------
[] == false // true
![] == false // true
![] == [] // true
![] === [] // false
null === document.getElementById('ID') // true 'ID' 不存在
解释名词
- == 叫做相等运算符
- === 叫做严格运算符
== 相等运算符时转化规则
一般转化规则: 对象 => 字符串 => 数字值 或 布尔值 => 数字值
- 字符串与数字比较时,字符串转化成数字值,再进行比较;
- 布尔值与数字比较时,布尔值转化成数字值,再进行比较;
- 字符串与布尔值比较时,两者都转化成数字值,再进行比较;
- 对象与数字比较时,对象先转化成字符串,再转化成数字值,再进行比较;
- 对象与字符串比较时,对象转化成字符串,再进行比较;
- 对象与布尔值比较时,对象先转化成字符串,再转化成数字值,字符串转化成数字,再进行比较;
- null 与 undefined 二者相等,不能把二者转化成其他值,二者与其他值比较均为 false;
- ![] == [],根据运算符的优先级,! 先执行,直接转化成布尔值(空字符串、NaN、0、null、undefined 都是 false, 其他的都为 true)再取反,故![]转化为 false;
=== 严格运算符
- 两个值类型不同,就返回 false;
- 两个值都是数值,且为同一个值,那么为 true。另外:如果其中一个为 NaN,那么肯定为 false;(判断一个值是否为 NaN,只能用 Number.isNaN()来判断)
- 两个值都是字符串,且每个位置的字符都一样,则为 true;
- 两个值都引用同一个对象或函数,则为 true;
- 两个值都是 true 或 false, 则为 true;
- 两个值都是 null 或 undefined,则为 true;
4.实现函数接受任意二叉树,求二叉树所有根到叶子路径组成的数字之和
// 1
// 2 3
// 4 5 6 7
//结果:(1+2)+(1+3)+(2+4)+(2+5)+(3+6)+(3+7) // 39
class TreeNode {
constructor() {
this.value = '';
this.left = null;
this.right = null;
}
}
function getPathSum(root) {
// TODO CODE
}
// test
const node = new TreeNode();
node.value = 1;
node.left = new TreeNode();
node.left.value = 2;
node.right = new TreeNode();
node.right.value = 3;
node.left.left = new TreeNode();
node.left.left.value = 4;
node.left.right = new TreeNode();
node.left.right.value = 5;
node.right.left = new TreeNode();
node.right.left.value = 6;
node.right.right = new TreeNode();
node.right.right.value = 7;
const result = getPathSum(node);
console.log(result); // 39
题解
function getPathSum(root) {
let num = 0;
const addNum = node => {
if (node.left) {
num += node.value + node.left.value;
if (node.left.left) {
addNum(node.left);
} else if (node.left.right) {
addNum(node.left);
}
}
if (node.right) {
num += node.value + node.right.value;
if (node.right.left) {
addNum(node.right);
} else if (node.right.right) {
addNum(node.right);
}
}
};
addNum(root);
return num;
}
5. 要求⽤不同⽅式对 A 进⾏改造实现 A.name 发⽣变化时⽴即执⾏ A.getName
/*
已知对象A = {name: 'sfd', getName: function(){console.log(this.name)}},
现要求⽤不同⽅式对A进⾏改造实现A.name发⽣变化时⽴即执⾏A.getName
*/
题解
// 第一种 用get set
let _name = 'sfd';
let A = {
get name() {
return _name;
},
set name(value) {
_name = value;
this.getName();
return _name;
},
getName: function() {
console.log(`名字:${this.name}`);
}
};
A.name = 'kkkkkkkk';
// 第二种 用Object.defineProperty
const B = {
getName: function() {
console.log(`名字:${this.name}`);
}
};
Object.defineProperty(B, 'name', {
get() {
return _name;
},
set(value) {
_name = value;
this.getName();
return _name;
}
});
B.name = 'dddddddd';
// 第三种 Proxy
let _C = {
name: 'sfd',
getName: function() {
console.log(`名字:${this.name}`);
}
};
let C = new Proxy(_C, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
},
set(target, key, receiver) {
const result = Reflect.set(target, key, receiver);
target.getName();
return result;
}
});
C.name = 'bbbbbbbb';
6. 实现输出一个十六进制的随机颜色(#bcf7f0)
function getColor() {
// substr 不推荐使用
// return `#${Math.random().toString(16).substr(-6)}`
// 推荐使用substring
const str = Math.random().toString(16);
return `#${str.substring(str.length - 6)}`;
}
function getColor1() {
const colorStr = '1234567890abcdef';
const colorArray = colorStr.split('');
let result = '#';
for (let i = 0; i < 6; i++) {
const idx = Math.floor(Math.random() * colorStr.length);
result += colorArray[idx];
}
return result;
}
console.log(getColor());
console.log(getColor1());
公式
生成随机数的公式:【 Math.random() * (m - n + 1) 】+ n;
- n 是开始的数字
e.g
- 取随机数 0-50,则为
(Math.random() * (50 - 0 + 1)) + 0=>Math.random() * 51 - 取随机数 1-50,则为
(Math.random() * (50 - 1 + 1)) + 1=>(Math.random() * 50) + 1
7. 重复打印
如题, 完成 repeat 函数,每隔 3000 秒打印输出一次 Hello world
const repeatFn = repeat(console.log, 4, 3000);
repeatFn('Hello world');
function repeat(fn, times, delay) {
// TODO
}
// 用setInter
function repeat(fn, times, delay) {
const self = this;
return function(msg) {
let num = 0;
timer = setInterval(() => {
fn.call(self, msg);
num++;
if (num === times) clearInterval(timer);
}, delay);
};
}
// 用Promise
function repeat(fn, times, delay) {
const self = this;
let timer = null;
const print = msg => {
return new Promise(resolve => {
timer = setTimeout(() => {
fn.call(self, msg);
resolve();
}, delay);
});
};
return async msg => {
while (times > 0) {
await print(msg);
times--;
}
timer && clearTimeout(timer);
};
}
8. 手写代码实现 kuai-shou-front-end=>KuaiShouFrontEnd
function firstUp(str) {
return str
.split('-')
.map(key => key[0].toUpperCase() + key.slice(1))
.join('');
}
console.log(firstUp1(string));
9. 写出打印结果,并解释为什么
var a = { k1: 1 };
var b = a;
a.k3 = a = { k2: 2 };
console.log(a); // ?
console.log(b); // ?
提示
- 点的优先级比等于的优先级高
- 对象以指针的形式进行存储,每个新对象都是一份新的存储地址
- var b = a, 此时都指向同一个内存地址 -> {k1: 1}
- 由于点的优先级比等号高,所以 a.k3 此时 a 和 b 的内存地址变成 {k1: 1, k3: undefined}
- 接着执行等号运算,从右往左执行,所以 a = {k2 : 2}, 这时候已经指向新的内存地址了。
- a.k3 = a, 这时候 a 已经不是原来的 a ({k1: 1}) 了,而是刚刚新的内存地址即 {k2: 2}, 所以 a.k3 = {k2: 2}。则 a 为{k1: 1, k3: {k2: 2}}
- 由于 a 和 b 指向同一个内存地址,所以 b 也为{k1: 1, k3: {k2: 2}}
10. 在数组中插入值
// 给两个数组 [A1,A2,B1,B2,C1,C2,D1,D2] [A,B,C,D]
// 输出 [A1,A2,A,B1,B2,B,C1,C2,C,D1,D2,D]
function insertArr(arrA = [], arrB = []) {
if (!arrA || !arrA.length) return arrB;
if (!arrB || !arrB.length) return arrB;
let result = [];
for (const item of arrB) {
const arr = arrA.filter(value => value.includes(item));
result = [...result, ...arr, item];
}
return result;
}
const A = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2', 'D1', 'D2'];
const B = ['A', 'B', 'C', 'D'];
insertArr(A, B);
11. 请写一个函数,输出出多级嵌套结构的 Object 的所有 key 值
var obj = {
a: '12',
b: '23',
first: {
c: '34',
d: '45',
second: { e: '56', f: '67', three: { g: '78', h: '89', i: '90' } }
}
};
// => [a,b,c,d,e,f,g,h,i]
function getAllKey(obj) {
if (typeof obj !== 'object') return obj;
let result = [];
for (const key in obj) {
if (obj[key] instanceof Object && !Array.isArray(obj[key])) {
result = result.concat(getAllKey(obj[key]));
} else {
result.push(key);
}
}
return result;
}
getAllKey(obj);
12. 给定一个数组,按找到每个元素右侧第一个比它大的数字,没有的话返回-1 规则返回一个数组
/*
*示例:
*给定数组:[2,6,3,8,10,9]
*返回数组:[6,8,8,10,-1,-1]
*/
// 暴力双重循环法
function findMaxRight(arr) {
let result = [];
for (let i = 0; i < arr.length; i++) {
for (let j = i; j < arr.length; j++) {
if (arr[i] < arr[j]) {
result[i] = arr[j];
break;
} else {
result[i] = -1;
}
}
}
result[arr.length - 1] = -1;
return result;
}
13. 平时在项目开发中都做过哪些前端性能优化
一、 体验优化
从用户角度而言,优化能够让页面加载得更快、对用户的操作响应得更及时,能够给用户提供更友好的用户体验。
- 首屏渲染优化,请求少、加载体积小、善用缓存
- 动画优化,避免某些动画造成页面的卡顿
- 优化用户的操作感官,提升视觉反馈,比如 hover 销售、让用户一眼就知道是否可操作
- 长列表服用 dom,优化滚动效果及页面卡断现象,减少页面一次性渲染的数量
- 骨架屏的使用
- 组件的预加载、懒加载
二、提升页面性能
- 减少 http 请求和冗余数据
- 组件,路由懒加载
- 配置 nginx 优化
- 优化 webpack 打包机制
- 使用 CDN
- 预渲染
- ssr
- 图片转 base64
- 后台分布式部署,负载均衡
三、首页加载优化(减少白屏时间)
- cdn 分发:通过在多台服务器部署相同的副本,当用户访问时,服务器根据用户跟哪台服务器地理距离小或者那台服务器此时的压力小,来决定哪台服务器去响应这个请求。
- 后端在业务层的缓存:数据库查询缓存是可以设置缓存的,这个对于高频率的请求很有用。值得注意的是,接口也是可以设置缓存的,比如获取一定时间内不会变的资源,设置缓存会很有用。
- 静态文件缓存方案:这个最常看到。现在流行的方式为文件 hash+强缓存的一个方案。比如 hash+cacheControl:max-age=一年
- 前端的资源动态加载:
- a. 路由动态加载,最常见的做法,以页面为单位,进行动态加载
- b. 组件动态加载(offScreen Component),对于不在当前视口的组件,先不加载
- c. 图片懒加载(offScreen Image),同上。值得庆幸的是,越来越多的浏览器支持原生的懒加载,通过给 img 标签加上 loading="lazy"来开启懒加载模式
- 利用好 async 和 defer 这两个属性:如果是独立功能的 js 文件,可以加入 async 属性。如果是优先级低且没有依赖的 js,我们可以加入 defer 属性。
- 渲染的优先级:浏览器有一套资源的加载优先级策略。也可以通过 js 来自己控制请求的顺序和渲染的顺序。一般我们不需要那么细粒度的控制,而且控制的代码也很不好写。
- 前端做的一些接口缓存:前端也可以做接口缓存,缓存的位置有两个,一个是内存,即保存给变量,一个是 localStorage。比如用户的签到日历(展示用户是否签到),我们可以缓存这样的接口到 localStorage,有效期是当天。或者有个列表页,我们总是缓存上次的列表内容到本地,下次加载时,我们先从本地读取缓存,并同事发起请求到服务器获取最新列表。
- 页面使用骨架屏:意思是在首屏加载完成之前,通过渲染一些简单元素进行占位。骨架屏虽然不能提高首屏加载速度,但是可以减少用户在首屏等待的急躁情绪。这点很有效,在很多成熟的网站都有大量应用。
- 使用 SSR 渲染:服务器性能一般都很好,那么可以先在服务器先把 vdom 计算完成后,再输出给前端。这样可以节约的时间为:计算量 / (服务器计算速度 - 客户端计算速度)。 第二个是服务器可以把首屏的 ajax 请求在服务器端阶段就完成,这样可以省去和客户端通过 tcp 传输的时间。
- 引入 http2.0:http2.0 对比 http1.1,最主要的提升是传输性能,特别在接口小而多的时候。
- 选择先进的图片格式:使用 JPEG 2000、JPEG XR,和 WebP 的图片格式来代替的 jpeg 和 png。当页面图片较多时,这点作用非常明显。把部分大容量的图片从 BaseLine JPEG 切换成 Progressive JPEG 也能缩小体积。
- 利用好的 http 压缩:使用 http 压缩的效果也非常明显。
14. versions 是一个项目的版本号列表,因多人维护,不规则,动手实现一个版本号处理函数
var versions = ['1.45.0', '1.5', '6', '3.3.3.3.3.3.3'];
// 要求从小到大排序,注意'1.45'比'1.5'大
function sortVersion(versions) {
// TODO
}
// => ['1.5','1.45.0','3.3.3.3.3.3','6']
15. 给定起止日期,返回中间的所有月份
// 输入两个字符串 2022-01 2022-05
// 输出他们中间的月份 [ '2022-02', '2022-03', '2022-04' ]
/**
* 格式化时间字符串,转化成date
* @param {string} dataStr YYYY-MM
* @returns date 标准时间类型
*/
function strToDate(dateStr) {
const [year, month] = dateStr.split('-');
return new Date(year, month - 1);
}
/**
* 将标准时间格式转换成YYYY-MM
* @param {date string} date 标准时间格式
* @returns YYYY-MM
*/
function dateToStr(date) {
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}`;
}
/**
* 获取中间值
* @param {date string} startDateStr 开始日期 YYYY-MM
* @param {date string} endDateStr 结束日期 YYYY-MM
* @returns [string]
*/
function getMonths(startDateStr, endDateStr) {
// 转成时间戳,比较好比较
let startTime = strToDate(startDateStr).getTime();
const endTime = strToDate(endDateStr).getTime();
let result = [];
while (startTime < endTime) {
// 将时间戳转成标准时间格式后塞入数组
const curDate = new Date(startTime);
result.push(dateToStr(curDate));
curDate.setMonth(curDate.getMonth() + 1);
startTime = curDate.getTime();
}
// 把首次的也塞进来了,需要剔除掉
return result.slice(1);
}
getMonths('2022-01', '2022-05');