手写题:Promise 原理
class MyPromise { constructor(fn) { this.callbacks = []; this.state = "PENDING"; this.value = null;
fn(this._resolve.bind(this), this._reject.bind(this)); }
then(onFulfilled, onRejected) { return new MyPromise((resolve, reject) => this._handle({ onFulfilled: onFulfilled || null, onRejected: onRejected || null, resolve, reject, }) ); }
catch(onRejected) { return this.then(null, onRejected); }
_handle(callback) { if (this.state === "PENDING") { this.callbacks.push(callback);
return; }
let cb = this.state === "FULFILLED" ? callback.onFulfilled : callback.onRejected; if (!cb) { cb = this.state === "FULFILLED" ? callback.resolve : callback.reject; cb(this.value);
return; }
let ret;
try { ret = cb(this.value); cb = this.state === "FULFILLED" ? callback.resolve : callback.reject; } catch (error) { ret = error; cb = callback.reject; } finally { cb(ret); } }
_resolve(value) { if (value && (typeof value === "object" || typeof value === "function")) { let then = value.then;
if (typeof then === "function") { then.call(value, this._resolve.bind(this), this._reject.bind(this));
return; } }
this.state === "FULFILLED"; this.value = value; this.callbacks.forEach((fn) => this._handle(fn)); }
_reject(error) { this.state === "REJECTED"; this.value = error; this.callbacks.forEach((fn) => this._handle(fn)); }}
const p1 = new Promise(function (resolve, reject) { setTimeout(() => reject(new Error("fail")), 3000);});
const p2 = new Promise(function (resolve, reject) { setTimeout(() => resolve(p1), 1000);});
p2.then((result) => console.log(result)).catch((error) => console.log(error));
复制代码
原型链
原型链实际上在上面原型的问题中就有涉及到,在原型的继承中,我们继承来多个原型,这里再提一下实现完美继承的方案,通过借助寄生组合继承,PersonB.prototype = Object.create(PersonA.prototype)这是当我们实例化PersonB得到实例化对象,访问实例化对象的属性时会触发get方法,它会先在自身属性上查找,如果没有这个属性,就会去__proto__中查找,一层层向上直到查找到顶层对象Object,这个查找的过程就是原型链来。
复制代码
基于 Localstorage 设计一个 1M 的缓存系统,需要实现缓存淘汰机制
设计思路如下:
存储的每个对象需要添加两个属性:分别是过期时间和存储时间。
利用一个属性保存系统中目前所占空间大小,每次存储都增加该属性。当该属性值大于 1M 时,需要按照时间排序系统中的数据,删除一定量的数据保证能够存储下目前需要存储的数据。
每次取数据时,需要判断该缓存数据是否过期,如果过期就删除。
以下是代码实现,实现了思路,但是可能会存在 Bug,但是这种设计题一般是给出设计思路和部分代码,不会需要写出一个无问题的代码
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; }}
复制代码
事件委托的使用场景
场景:给页面的所有的 a 标签添加 click 事件,代码如下:
document.addEventListener("click", function(e) { if (e.target.nodeName == "A") console.log("a");}, false);
复制代码
但是这些 a 标签可能包含一些像 span、img 等元素,如果点击到了这些 a 标签中的元素,就不会触发 click 事件,因为事件绑定上在 a 标签元素上,而触发这些内部的元素时,e.target 指向的是触发 click 事件的元素(span、img 等其他元素)。
这种情况下就可以使用事件委托来处理,将事件绑定在 a 标签的内部元素上,当点击它的时候,就会逐级向上查找,知道找到 a 标签为止,代码如下:
document.addEventListener("click", function(e) { var node = e.target; while (node.parentNode.nodeName != "BODY") { if (node.nodeName == "A") { console.log("a"); break; } node = node.parentNode; }}, false);
复制代码
代码输出结果
function Dog() { this.name = 'puppy'}Dog.prototype.bark = () => { console.log('woof!woof!')}const dog = new Dog()console.log(Dog.prototype.constructor === Dog && dog.constructor === Dog && dog instanceof Dog)
复制代码
输出结果:true
解析: 因为 constructor 是 prototype 上的属性,所以 dog.constructor 实际上就是指向 Dog.prototype.constructor;constructor 属性指向构造函数。instanceof 而实际检测的是类型是否在实例的原型链上。
constructor 是 prototype 上的属性,这一点很容易被忽略掉。constructor 和 instanceof 的作用是不同的,感性地来说,constructor 的限制比较严格,它只能严格对比对象的构造函数是不是指定的值;而 instanceof 比较松散,只要检测的类型在原型链上,就会返回 true。
代码输出结果
const promise = Promise.resolve().then(() => { return promise;})promise.catch(console.err)
复制代码
输出结果如下:
Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>
复制代码
这里其实是一个坑,.then 或 .catch 返回的值不能是 promise 本身,否则会造成死循环。
参考 前端进阶面试题详细解答
数组去重
使用 indexOf/includes 实现
function unique(arr) { var res = []; for(var i = 0; i < arr.length; i++) { if(res.indexOf(arr[i]) === -1) res.push(arr[i]); // if(!res.includes(arr[i])) res.push(arr[i]); } return res;}
复制代码
使用 filter(forEach) + indexOf/includes 实现
// filterfunction unique(arr) { var res = arr.filter((value, index) => { // 只存第一个出现的元素 return arr.indexOf(value) === index; }); return res;}// forEachfunction unique(arr) { var res = []; arr.forEach((value) => { if(!res.includes(value)) res.push(value); }); return res;}
复制代码
非 API 版本(原生)实现
function unique(arr) { var res = []; for(var i = 0; i < arr.length; i++) { var flag = false; for(var j = 0; j < res.length; j++) { if(arr[i] === res[j]) { flag = true; break; } } if(flag === false) res.push(arr[i]); } return res;}
复制代码
ES6 使用 Set + 扩展运算符(...)/Array.from() 实现
function unique(arr) { // return [...new Set(arr)]; return Array.from(new Set(arr));}
复制代码
了解 this 嘛,bind,call,apply 具体指什么
它们都是函数的方法
call: Array.prototype.call(this, args1, args2]) apply: Array.prototype.apply(this, [args1, args2]) :ES6 之前用来展开数组调用, foo.appy(null, []),ES6 之后使用 ... 操作符
四条规则:
function foo() { console.log(this.a); }
var a = 2;foo();
复制代码
function foo() { console.log(this.a);}
var obj = { a: 2, foo: foo,}
obj.foo(); // 2
复制代码
function foo() { console.log(this.a);}
var obj = { a: 2};
foo.call(obj);
复制代码
显示绑定之硬绑定
function foo(something) { console.log(this.a, something);
return this.a + something;}
function bind(fn, obj) { return function() { return fn.apply(obj, arguments); };}
var obj = { a: 2}
var bar = bind(foo, obj);
复制代码
New 绑定,new 调用函数会创建一个全新的对象,并将这个对象绑定到函数调用的 this。
function foo(a) { this.a = a;}
var bar = new foo(2);console.log(bar.a)
复制代码
Promise.allSettled
描述:等到所有promise都返回结果,就返回一个promise实例。
实现:
Promise.allSettled = function(promises) { return new Promise((resolve, reject) => { if(Array.isArray(promises)) { if(promises.length === 0) return resolve(promises); let result = []; let count = 0; promises.forEach((item, index) => { Promise.resolve(item).then( value => { count++; result[index] = { status: 'fulfilled', value: value }; if(count === promises.length) resolve(result); }, reason => { count++; result[index] = { status: 'rejected'. reason: reason }; if(count === promises.length) resolve(result); } ); }); } else return reject(new TypeError("Argument is not iterable")); });}
复制代码
分片思想解决大数据量渲染问题
题目描述:渲染百万条结构简单的大数据时 怎么使用分片思想优化渲染
实现代码如下:
let ul = document.getElementById("container");// 插入十万条数据let total = 100000;// 一次插入 20 条let once = 20;//总页数let page = total / once;//每条记录的索引let index = 0;//循环加载数据function loop(curTotal, curIndex) { if (curTotal <= 0) { return false; } //每页多少条 let pageCount = Math.min(curTotal, once); window.requestAnimationFrame(function () { for (let i = 0; i < pageCount; i++) { let li = document.createElement("li"); li.innerText = curIndex + i + " : " + ~~(Math.random() * total); ul.appendChild(li); } loop(curTotal - pageCount, curIndex + pageCount); });}loop(total, index);
复制代码
扩展思考:对于大数据量的简单 dom 结构渲染可以用分片思想解决 如果是复杂的 dom 结构渲染如何处理?
这时候就需要使用虚拟列表了 大家自行百度哈 虚拟列表和虚拟表格在日常项目使用还是很频繁的
异步任务调度器
描述:实现一个带并发限制的异步调度器 Scheduler,保证同时运行的任务最多有 limit 个。
实现:
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
复制代码
什么是同源策略
跨域问题其实就是浏览器的同源策略造成的。
同源策略限制了从同一个源加载的文档或脚本如何与另一个源的资源进行交互。这是浏览器的一个用于隔离潜在恶意文件的重要的安全机制。同源指的是:协议、端口号、域名必须一致。
同源策略:protocol(协议)、domain(域名)、port(端口)三者必须一致。
同源政策主要限制了三个方面:
同源政策的目的主要是为了保证用户的信息安全,它只是对 js 脚本的一种限制,并不是对浏览器的限制,对于一般的 img、或者 script 脚本请求都不会有跨域的限制,这是因为这些操作都不会通过响应结果来进行可能出现安全问题的操作。
什么是作用域链?
首先要了解作用域链,当访问一个变量时,编译器在执行这段代码时,会首先从当前的作用域中查找是否有这个标识符,如果没有找到,就会去父作用域查找,如果父作用域还没找到继续向上查找,直到全局作用域为止,,而作用域链,就是有当前作用域与上层作用域的一系列变量对象组成,它保证了当前执行的作用域对符合访问权限的变量和函数的有序访问。
vue-router
vue-router是vuex.js官方的路由管理器,它和vue.js的核心深度集成,让构建但页面应用变得易如反掌
<router-link> 组件支持用户在具有路由功能的应用中 (点击) 导航。 通过 to 属性指定目标地址
<router-view> 组件是一个 functional 组件,渲染路径匹配到的视图组件。
<keep-alive> 组件是一个用来缓存组件
router.beforeEach
router.afterEach
to: Route: 即将要进入的目标 路由对象
from: Route: 当前导航正要离开的路由
next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。
介绍了路由守卫及用法,在项目中路由守卫起到的作用等等
复制代码
代码输出结果
async function async1 () { console.log('async1 start'); await new Promise(resolve => { console.log('promise1') }) console.log('async1 success'); return 'async1 end'}console.log('srcipt start')async1().then(res => console.log(res))console.log('srcipt end')
复制代码
输出结果如下:
script startasync1 startpromise1script end
复制代码
这里需要注意的是在async1中await后面的 Promise 是没有返回值的,也就是它的状态始终是pending状态,所以在await之后的内容是不会执行的,包括async1后面的 .then。
行内元素有哪些?块级元素有哪些? 空(void)元素有那些?
空元素,即没有内容的 HTML 元素。空元素是在开始标签中关闭的,也就是空元素没有闭合标签:
二分查找--时间复杂度 log2(n)
题目描述:如何确定一个数在一个有序数组中的位置
实现代码如下:
function search(arr, target, start, end) { let targetIndex = -1;
let mid = Math.floor((start + end) / 2);
if (arr[mid] === target) { targetIndex = mid; return targetIndex; }
if (start >= end) { return targetIndex; }
if (arr[mid] < target) { return search(arr, target, mid + 1, end); } else { return search(arr, target, start, mid - 1); }}// const dataArr = [1, 2, 3, 4, 5, 6, 7, 8, 9];// const position = search(dataArr, 6, 0, dataArr.length - 1);// if (position !== -1) {// console.log(`目标元素在数组中的位置:${position}`);// } else {// console.log("目标元素不在数组中");// }
复制代码
代码输出结果
var x = 3;var y = 4;var obj = { x: 1, y: 6, getX: function() { var x = 5; return function() { return this.x; }(); }, getY: function() { var y = 7; return this.y; }}console.log(obj.getX()) // 3console.log(obj.getY()) // 6
复制代码
输出结果:3 6
解析:
我们知道,匿名函数的 this 是指向全局对象的,所以 this 指向 window,会打印出 3;
getY 是由 obj 调用的,所以其 this 指向的是 obj 对象,会打印出 6。
节流
**节流(throttle)**:触发高频事件,且 N 秒内只执行一次。这就好比公交车,10 分钟一趟,10 分钟内有多少人在公交站等我不管,10 分钟一到我就要发车走人!类似 qq 飞车的复位按钮。
核心思想:使用时间戳或标志来实现,立即执行一次,然后每 N 秒执行一次。如果 N 秒内触发则直接返回。
应用:节流常应用于鼠标不断点击触发、监听滚动事件。
实现:
// 版本一:标志实现function throttle(fn, wait){ let flag = true; // 设置一个标志 return function(...args){ if(!flag) return; flag = false; setTimeout(() => { fn.call(this, ...args); flag = true; }, wait); }}
// 版本二:时间戳实现function throttle(fn, wait) { let pre = 0; return function(...args) { let now = new Date(); if(now - pre < wait) return; pre = now; fn.call(this, ...args); }}
复制代码
HTML5 有哪些更新
1. 语义化标签
2. 媒体标签
(1) audio:音频
<audio src='' controls autoplay loop='true'></audio>
复制代码
属性:
controls 控制面板
autoplay 自动播放
loop=‘true’ 循环播放
(2)video 视频
<video src='' poster='imgs/aa.jpg' controls></video>
复制代码
属性:
(3)source 标签因为浏览器对视频格式支持程度不一样,为了能够兼容不同的浏览器,可以通过 source 来指定视频源。
<video> <source src='aa.flv' type='video/flv'></source> <source src='aa.mp4' type='video/mp4'></source></video>
复制代码
3. 表单
表单类型:
email :能够验证当前输入的邮箱地址是否合法
url : 验证 URL
number : 只能输入数字,其他输入不了,而且自带上下增大减小箭头,max 属性可以设置为最大值,min 可以设置为最小值,value 为默认值。
search : 输入框后面会给提供一个小叉,可以删除输入的内容,更加人性化。
range : 可以提供给一个范围,其中可以设置 max 和 min 以及 value,其中 value 属性可以设置为默认值
color : 提供了一个颜色拾取器
time : 时分秒
data : 日期选择年月日
datatime : 时间和日期(目前只有 Safari 支持)
datatime-local :日期时间控件
week :周控件
month:月控件
表单属性:
placeholder :提示信息
autofocus :自动获取焦点
autocomplete=“on” 或者 autocomplete=“off” 使用这个属性需要有两个前提:
表单必须提交过
必须有 name 属性。
required:要求输入框不能为空,必须有值才能够提交。
pattern=" " 里面写入想要的正则模式,例如手机号 patte="^(+86)?\d{10}$"
multiple:可以选择多个文件或者多个邮箱
form=" form 表单的 ID"
表单事件:
4. 进度条、度量器
设置规则:min < low < high < max
5.DOM 查询操作
它们选择的对象可以是标签,可以是类(需要加点),可以是 ID(需要加 #)
6. Web 存储
HTML5 提供了两种在客户端存储数据的新方法:
7. 其他
<canvas id="myCanvas" width="200" height="100"></canvas>
复制代码
总结: (1)新增语义化标签:nav、header、footer、aside、section、article(2)音频、视频标签:audio、video(3)数据存储:localStorage、sessionStorage(4)canvas(画布)、Geolocation(地理定位)、websocket(通信协议)(5)input 标签新增属性:placeholder、autocomplete、autofocus、required(6)history API:go、forward、back、pushstate
移除的元素有:
评论