原函数形参不定长(此时 fn.length
为0)
代码语言:javascript复制function curry(fn) {
// 保存参数,除去第一个函数参数
let args = [].slice.call(arguments, 1);
// 返回一个新函数
let curried = function () {
// 新函数调用时会继续传参
let allArgs = [...args, ...arguments];
return curry(fn, ...allArgs);
};
// 利用toString隐式转换的特性,当最后执行函数时,会隐式转换
curried.toString = function () {
return fn(...args);
};
return curried;
}
// 测试
function add(...args) {
return args.reduce((pre, cur) => pre cur, 0);
}
console.log(add(1, 2, 3, 4));
let addCurry = curry(add);
console.log(addCurry(1)(2)(3) == 6); // true
console.log(addCurry(1, 2, 3)(4) == 10); // true
console.log(addCurry(2, 6)(1).toString()); // 9
console.log(addCurry(2, 6)(1, 8)); // 打印 curried 函数
手写题:数组扁平化
代码语言:javascript复制function flatten(arr) {
let result = [];
for (let i = 0; i < arr.length; i ) {
if (Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i]));
} else {
result = result.concat(arr[i]);
}
}
return result;
}
const a = [1, [2, [3, 4]]];
console.log(flatten(a));
基于 Localstorage 设计一个 1M 的缓存系统,需要实现缓存淘汰机制
设计思路如下:
- 存储的每个对象需要添加两个属性:分别是过期时间和存储时间。
- 利用一个属性保存系统中目前所占空间大小,每次存储都增加该属性。当该属性值大于 1M 时,需要按照时间排序系统中的数据,删除一定量的数据保证能够存储下目前需要存储的数据。
- 每次取数据时,需要判断该缓存数据是否过期,如果过期就删除。
以下是代码实现,实现了思路,但是可能会存在 Bug,但是这种设计题一般是给出设计思路和部分代码,不会需要写出一个无问题的代码
代码语言:text复制class Store {
constructor() {
let store = localStorage.getItem('cache')
if (!store) {
store = {
maxSize: 1024 * 1024,
size: 0
}
this.store = store
} else {
this.store = JSON.parse(store)
}
}
set(key, value, expire) {
this.store[key] = {
date: Date.now(),
expire,
value
}
let size = this.sizeOf(JSON.stringify(this.store[key]))
if (this.store.maxSize < size this.store.size) {
console.log('超了-----------');
var keys = Object.keys(this.store);
// 时间排序
keys = keys.sort((a, b) => {
let item1 = this.store[a], item2 = this.store[b];
return item2.date - item1.date;
});
while (size this.store.size > this.store.maxSize) {
let index = keys[keys.length - 1]
this.store.size -= this.sizeOf(JSON.stringify(this.store[index]))
delete this.store[index]
}
}
this.store.size = size
localStorage.setItem('cache', JSON.stringify(this.store))
}
get(key) {
let d = this.store[key]
if (!d) {
console.log('找不到该属性');
return
}
if (d.expire > Date.now) {
console.log('过期删除');
delete this.store[key]
localStorage.setItem('cache', JSON.stringify(this.store))
} else {
return d.value
}
}
sizeOf(str, charset) {
var total = 0,
charCode,
i,
len;
charset = charset ? charset.toLowerCase() : '';
if (charset === 'utf-16' || charset === 'utf16') {
for (i = 0, len = str.length; i < len; i ) {
charCode = str.charCodeAt(i);
if (charCode <= 0xffff) {
total = 2;
} else {
total = 4;
}
}
} else {
for (i = 0, len = str.length; i < len; i ) {
charCode = str.charCodeAt(i);
if (charCode <= 0x007f) {
total = 1;
} else if (charCode <= 0x07ff) {
total = 2;
} else if (charCode <= 0xffff) {
total = 3;
} else {
total = 4;
}
}
}
return total;
}
}
ES6中模板语法与字符串处理
ES6 提出了“模板语法”的概念。在 ES6 以前,拼接字符串是很麻烦的事情:
代码语言:javascript复制var name = 'css'
var career = 'coder'
var hobby = ['coding', 'writing']
var finalString = 'my name is ' name ', I work as a ' career ', I love ' hobby[0] ' and ' hobby[1]
仅仅几个变量,写了这么多加号,还要时刻小心里面的空格和标点符号有没有跟错地方。但是有了模板字符串,拼接难度直线下降:
代码语言:javascript复制var name = 'css'
var career = 'coder'
var hobby = ['coding', 'writing']
var finalString = `my name is ${name}, I work as a ${career} I love ${hobby[0]} and ${hobby[1]}`
字符串不仅更容易拼了,也更易读了,代码整体的质量都变高了。这就是模板字符串的第一个优势——允许用${}的方式嵌入变量。但这还不是问题的关键,模板字符串的关键优势有两个:
- 在模板字符串中,空格、缩进、换行都会被保留
- 模板字符串完全支持“运算”式的表达式,可以在${}里完成一些计算
基于第一点,可以在模板字符串里无障碍地直接写 html 代码:
代码语言:javascript复制let list = ` <ul> <li>列表项1</li> <li>列表项2</li> </ul>`;
console.log(message); // 正确输出,不存在报错
基于第二点,可以把一些简单的计算和调用丢进 ${} 来做:
代码语言:javascript复制function add(a, b) {
const finalString = `${a} ${b} = ${a b}`
console.log(finalString)
}
add(1, 2) // 输出 '1 2 = 3'
除了模板语法外, ES6中还新增了一系列的字符串方法用于提升开发效率:
(1)存在性判定:在过去,当判断一个字符/字符串是否在某字符串中时,只能用 indexOf > -1 来做。现在 ES6 提供了三个方法:includes、startsWith、endsWith,它们都会返回一个布尔值来告诉你是否存在。
- includes:判断字符串与子串的包含关系:
const son = 'haha'
const father = 'xixi haha hehe'
father.includes(son) // true
- startsWith:判断字符串是否以某个/某串字符开头:
const father = 'xixi haha hehe'
father.startsWith('haha') // false
father.startsWith('xixi') // true
- endsWith:判断字符串是否以某个/某串字符结尾:
const father = 'xixi haha hehe'
father.endsWith('hehe') // true
(2)自动重复:可以使用 repeat 方法来使同一个字符串输出多次(被连续复制多次):
代码语言:javascript复制const sourceCode = 'repeat for 3 times;'
const repeated = sourceCode.repeat(3)
console.log(repeated) // repeat for 3 times;repeat for 3 times;repeat for 3 times;
箭头函数和普通函数有啥区别?箭头函数能当构造函数吗?
- 普通函数通过 function 关键字定义, this 无法结合词法作用域使用,在运行时绑定,只取决于函数的调用方式,在哪里被调用,调用位置。(取决于调用者,和是否独立运行)
- 箭头函数使用被称为 “胖箭头” 的操作
=>
定义,箭头函数不应用普通函数 this 绑定的四种规则,而是根据外层(函数或全局)的作用域来决定 this,且箭头函数的绑定无法被修改(new 也不行)。- 箭头函数常用于回调函数中,包括事件处理器或定时器
- 箭头函数和 var self = this,都试图取代传统的 this 运行机制,将 this 的绑定拉回到词法作用域
- 没有原型、没有 this、没有 super,没有 arguments,没有 new.target
- 不能通过 new 关键字调用
- 一个函数内部有两个方法:[Call] 和 [Construct],在通过 new 进行函数调用时,会执行 [construct] 方法,创建一个实例对象,然后再执行这个函数体,将函数的 this 绑定在这个实例对象上
- 当直接调用时,执行 [Call] 方法,直接执行函数体
- 箭头函数没有 [Construct] 方法,不能被用作构造函数调用,当使用 new 进行函数调用时会报错。
function foo() {
return (a) => {
console.log(this.a);
}
}
var obj1 = {
a: 2
}
var obj2 = {
a: 3
}
var bar = foo.call(obj1);
bar.call(obj2);
字符串模板
代码语言:javascript复制function render(template, data) {
const reg = /{{(w )}}/; // 模板字符串正则
if (reg.test(template)) { // 判断模板里是否有模板字符串
const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段
template = template.replace(reg, data[name]); // 将第一个模板字符串渲染
return render(template, data); // 递归的渲染并返回渲染后的结构
}
return template; // 如果模板没有模板字符串直接返回
}
测试:
代码语言:javascript复制let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let person = {
name: '布兰',
age: 12
}
render(template, person); // 我是布兰,年龄12,性别undefined
参考 前端进阶面试题详细解答
事件循环机制 (Event Loop)
事件循环机制从整体上告诉了我们 JavaScript 代码的执行顺序 Event Loop
即事件循环,是指浏览器或Node
的一种解决javaScript
单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。
先执行 Script 脚本,然后清空微任务队列,然后开始下一轮事件循环,继续先执行宏任务,再清空微任务队列,如此往复。
- 宏任务:Script/setTimeout/setInterval/setImmediate/ I/O / UI Rendering
- 微任务:process.nextTick()/Promise
上诉的 setTimeout 和 setInterval 等都是任务源,真正进入任务队列的是他们分发的任务。
优先级
- setTimeout = setInterval 一个队列
- setTimeout > setImmediate
- process.nextTick > Promise
for (const macroTask of macroTaskQueue) {
handleMacroTask();
for (const microTask of microTaskQueue) {
handleMicroTask(microTask);
}
}
说一下前端登录的流程?
初次登录的时候,前端调后调的登录接口,发送用户名和密码,后端收到请求,验证用户名和密码,验证成功,就给前端返回一个token,和一个用户信息的值,前端拿到token,将token储存到Vuex中,然后从Vuex中把token的值存入浏览器Cookies中。把用户信息存到Vuex然后再存储到LocalStroage中,然后跳转到下一个页面,根据后端接口的要求,只要不登录就不能访问的页面需要在前端每次跳转页面师判断Cookies中是否有token,没有就跳转到登录页,有就跳转到相应的页面,我们应该再每次发送post/get请求的时候应该加入token,常用方法再项目utils/service.js中添加全局拦截器,将token的值放入请求头中 后端判断请求头中有无token,有token,就拿到token并验证token是否过期,在这里过期会返回无效的token然后有个跳回登录页面重新登录并且清除本地用户的信息
异步任务调度器
描述:实现一个带并发限制的异步调度器 Scheduler,保证同时运行的任务最多有 limit
个。
实现:
代码语言:javascript复制class Scheduler {
queue = []; // 用队列保存正在执行的任务
runCount = 0; // 计数正在执行的任务个数
constructor(limit) {
this.maxCount = limit; // 允许并发的最大个数
}
add(time, data){
const promiseCreator = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(data);
resolve();
}, time);
});
}
this.queue.push(promiseCreator);
// 每次添加的时候都会尝试去执行任务
this.request();
}
request() {
// 队列中还有任务才会被执行
if(this.queue.length && this.runCount < this.maxCount) {
this.runCount ;
// 执行先加入队列的函数
this.queue.shift()().then(() => {
this.runCount--;
// 尝试进行下一次任务
this.request();
});
}
}
}
// 测试
const scheduler = new Scheduler(2);
const addTask = (time, data) => {
scheduler.add(time, data);
}
addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
// 输出结果 2 3 1 4
10 个 Ajax 同时发起请求,全部返回展示结果,并且至多允许三次失败,说出设计思路
这个问题相信很多人会第一时间想到 Promise.all
,但是这个函数有一个局限在于如果失败一次就返回了,直接这样实现会有点问题,需要变通下。以下是两种实现思路
// 以下是不完整代码,着重于思路 非 Promise 写法
let successCount = 0
let errorCount = 0
let datas = []
ajax(url, (res) => {
if (success) {
success
if (success errorCount === 10) {
console.log(datas)
} else {
datas.push(res.data)
}
} else {
errorCount
if (errorCount > 3) {
// 失败次数大于3次就应该报错了
throw Error('失败三次')
}
}
})
// Promise 写法
let errorCount = 0
let p = new Promise((resolve, reject) => {
if (success) {
resolve(res.data)
} else {
errorCount
if (errorCount > 3) {
// 失败次数大于3次就应该报错了
reject(error)
} else {
resolve(error)
}
}
})
Promise.all([p]).then(v => {
console.log(v);
});
插入排序--时间复杂度 n^2
题目描述:实现一个插入排序
实现代码如下:
代码语言:javascript复制function insertSort(arr) {
for (let i = 1; i < arr.length; i ) {
let j = i;
let target = arr[j];
while (j > 0 && arr[j - 1] > target) {
arr[j] = arr[j - 1];
j--;
}
arr[j] = target;
}
return arr;
}
// console.log(insertSort([3, 6, 2, 4, 1]));
生命周期
init
initLifecycle/Event
,往vm上挂载各种属性callHook: beforeCreated
: 实例刚创建initInjection/initState
: 初始化注入和data
响应性created: 创建完成,属性已经绑定, 但还未生成真实
dom`- 进行元素的挂载:
$el / vm.$mount()
- 是否有
template
: 解析成render function
*.vue
文件:vue-loader
会将<template>
编译成render function
beforeMount
: 模板编译/挂载之前- 执行
render function
,生成真实的dom
,并替换到dom tree
中 mounted
: 组件已挂载
update
- 执行
diff
算法,比对改变是否需要触发UI
更新 flushScheduleQueue
watcher.before
: 触发beforeUpdate
钩子 -watcher.run()
: 执行watcher
中的notify
,通知所有依赖项更新UI- 触发
updated
钩子: 组件已更新 actived / deactivated(keep-alive)
: 不销毁,缓存,组件激活与失活destroy
beforeDestroy
: 销毁开始- 销毁自身且递归销毁子组件以及事件监听
remove()
: 删除节点watcher.teardown()
: 清空依赖vm.$off()
: 解绑监听
destroyed
: 完成后触发钩子
Vue2 | Vue3 |
---|---|
| ❌ |
| ❌ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|