类数组转化为数组
类数组是具备length属性,但不具备数组原型上的办法。常见的类数组有arguments、DOM操作方法返回的后果。
办法一:Array.from
Array.from(document.querySelectorAll('div'))
办法二:Array.prototype.slice.call()
Array.prototype.slice.call(document.querySelectorAll('div'))
办法三:扩大运算符
[...document.querySelectorAll('div')]
办法四:利用concat
Array.prototype.concat.apply([], document.querySelectorAll('div'));
解析 URL Params 为对象
let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';parseParam(url)/* 后果{ user: 'anonymous', id: [ 123, 456 ], // 反复呈现的 key 要组装成数组,能被转成数字的就转成数字类型 city: '北京', // 中文需解码 enabled: true, // 未指定值得 key 约定为 true}*/
function parseParam(url) { const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 前面的字符串取出来 const paramsArr = paramsStr.split('&'); // 将字符串以 & 宰割后存到数组中 let paramsObj = {}; // 将 params 存到对象中 paramsArr.forEach(param => { if (/=/.test(param)) { // 解决有 value 的参数 let [key, val] = param.split('='); // 宰割 key 和 value val = decodeURIComponent(val); // 解码 val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字 if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则增加一个值 paramsObj[key] = [].concat(paramsObj[key], val); } else { // 如果对象没有这个 key,创立 key 并设置值 paramsObj[key] = val; } } else { // 解决没有 value 的参数 paramsObj[param] = true; } }) return paramsObj;}
滚动加载
原理就是监听页面滚动事件,剖析clientHeight、scrollTop、scrollHeight三者的属性关系。
window.addEventListener('scroll', function() { const clientHeight = document.documentElement.clientHeight; const scrollTop = document.documentElement.scrollTop; const scrollHeight = document.documentElement.scrollHeight; if (clientHeight + scrollTop >= scrollHeight) { // 检测到滚动至页面底部,进行后续操作 // ... }}, false);
Array.prototype.reduce()
Array.prototype.reduce = function(callback, initialValue) { if (this == undefined) { throw new TypeError('this is null or not defined'); } if (typeof callback !== 'function') { throw new TypeError(callbackfn + ' is not a function'); } const O = Object(this); const len = this.length >>> 0; let accumulator = initialValue; let k = 0; // 如果第二个参数为undefined的状况下 // 则数组的第一个有效值作为累加器的初始值 if (accumulator === undefined) { while (k < len && !(k in O)) { k++; } // 如果超出数组界线还没有找到累加器的初始值,则TypeError if (k >= len) { throw new TypeError('Reduce of empty array with no initial value'); } accumulator = O[k++]; } while (k < len) { if (k in O) { accumulator = callback.call(undefined, accumulator, O[k], k, O); } k++; } return accumulator;}
实现简略路由
// hash路由class Route{ constructor(){ // 路由存储对象 this.routes = {} // 以后hash this.currentHash = '' // 绑定this,防止监听时this指向扭转 this.freshRoute = this.freshRoute.bind(this) // 监听 window.addEventListener('load', this.freshRoute, false) window.addEventListener('hashchange', this.freshRoute, false) } // 存储 storeRoute (path, cb) { this.routes[path] = cb || function () {} } // 更新 freshRoute () { this.currentHash = location.hash.slice(1) || '/' this.routes[this.currentHash]() }}
分片思维解决大数据量渲染问题
题目形容: 渲染百万条构造简略的大数据时 怎么应用分片思维优化渲染
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
构造渲染如何解决?
这时候就须要应用虚构列表了,虚构列表和虚构表格在日常我的项目应用还是很多的
参考 前端进阶面试题具体解答
应用ES5和ES6求函数参数的和
ES5:
function sum() { let sum = 0 Array.prototype.forEach.call(arguments, function(item) { sum += item * 1 }) return sum}
ES6:
function sum(...nums) { let sum = 0 nums.forEach(function(item) { sum += item * 1 }) return sum}
Promise实现
基于Promise
封装Ajax
- 返回一个新的
Promise
实例 - 创立
HMLHttpRequest
异步对象 - 调用
open
办法,关上url
,与服务器建设链接(发送前的一些解决) - 监听
Ajax
状态信息 如果
xhr.readyState == 4
(示意服务器响应实现,能够获取应用服务器的响应了)xhr.status == 200
,返回resolve
状态xhr.status == 404
,返回reject
状态
xhr.readyState !== 4
,把申请主体的信息基于send
发送给服务器
function ajax(url) { return new Promise((resolve, reject) => { let xhr = new XMLHttpRequest() xhr.open('get', url) xhr.onreadystatechange = () => { if (xhr.readyState == 4) { if (xhr.status >= 200 && xhr.status <= 300) { resolve(JSON.parse(xhr.responseText)) } else { reject('申请出错') } } } xhr.send() //发送hppt申请 })}let url = '/data.json'ajax(url).then(res => console.log(res)) .catch(reason => console.log(reason))
实现apply办法
思路: 利用this
的上下文个性。apply
其实就是改一下参数的问题
Function.prototype.myApply = function(context = window, args) { // this-->func context--> obj args--> 传递过去的参数 // 在context上加一个惟一值不影响context上的属性 let key = Symbol('key') context[key] = this; // context为调用的上下文,this此处为函数,将这个函数作为context的办法 // let args = [...arguments].slice(1) //第一个参数为obj所以删除,伪数组转为数组 let result = context[key](...args); // 这里和call传参不一样 // 革除定义的this 不删除会导致context属性越来越多 delete context[key]; // 返回后果 return result;}
// 应用function f(a,b){ console.log(a,b) console.log(this.name)}let obj={ name:'张三'}f.myApply(obj,[1,2]) //arguments[1]
setTimeout与setInterval实现
setTimeout 模仿实现 setInterval
题目形容: setInterval
用来实现循环定时调用 可能会存在肯定的问题 能用 setTimeout
解决吗
实现代码如下:
function mySetInterval(fn, t) { let timerId = null; function interval() { fn(); timerId = setTimeout(interval, t); // 递归调用 } timerId = setTimeout(interval, t); // 首次调用 return { // 利用闭包的个性 保留timerId cancel:() => { clearTimeout(timerId) } }}
// 测试var a = mySetInterval(()=>{ console.log(111);},1000)var b = mySetInterval(() => { console.log(222)}, 1000)// 终止定时器a.cancel()b.cancel()
为什么要用setTimeout
模仿实现setInterval
?setInterval
的缺点是什么?
setInterval(fn(), N);
下面这句代码的意思其实是fn()
将会在N
秒之后被推入工作队列。在setInterval
被推入工作队列时,如果在它后面有很多工作或者某个工作等待时间较长比方网络申请等,那么这个定时器的执行工夫和咱们预约它执行的工夫可能并不统一
// 最常见的呈现的就是,当咱们须要应用 ajax 轮询服务器是否有新数据时,必定会有一些人会应用 setInterval,然而无论网络情况如何,它都会去一遍又一遍的发送申请,最初的间隔时间可能和原定的工夫有很大的出入// 做一个网络轮询,每一秒查问一次数据。let startTime = new Date().getTime();let count = 0;setInterval(() => { let i = 0; while (i++ < 10000000); // 假如的网络提早 count++; console.log( "与原设定的距离时差了:", new Date().getTime() - (startTime + count * 1000), "毫秒" );}, 1000)// 输入:// 与原设定的距离时差了: 567 毫秒// 与原设定的距离时差了: 552 毫秒// 与原设定的距离时差了: 563 毫秒// 与原设定的距离时差了: 554 毫秒(2次)// 与原设定的距离时差了: 564 毫秒// 与原设定的距离时差了: 602 毫秒// 与原设定的距离时差了: 573 毫秒// 与原设定的距离时差了: 633 毫秒
再次强调 ,定时器指定的工夫距离,示意的是何时将定时器的代码增加到音讯队列,而不是何时执行代码。所以真正何时执行代码的工夫是不能保障的,取决于何时被主线程的事件循环取到,并执行。
setInterval(function, N)//即:每隔N秒把function事件推到音讯队列中
上图可见,setInterval
每隔100ms
往队列中增加一个事件;100ms
后,增加T1
定时器代码至队列中,主线程中还有工作在执行,所以期待,some event
执行完结后执行T1
定时器代码;又过了100ms
,T2
定时器被增加到队列中,主线程还在执行T1
代码,所以期待;又过了100ms
,实践上又要往队列里推一个定时器代码,但因为此时T2
还在队列中,所以T3
不会被增加(T3
被跳过),后果就是此时被跳过;这里咱们能够看到,T1
定时器执行完结后马上执行了 T2 代码,所以并没有达到定时器的成果
setInterval有两个毛病
- 应用
setInterval
时,某些距离会被跳过 - 可能多个定时器会间断执行
能够这么了解 :每个setTimeout
产生的工作会间接push
到工作队列中;而setInterval
在每次把工作push
到工作队列前,都要进行一下判断(看上次的工作是否仍在队列中)。因此咱们个别用setTimeout
模仿setInterval
,来躲避掉下面的毛病
setInterval 模仿实现 setTimeout
const mySetTimeout = (fn, t) => { const timer = setInterval(() => { clearInterval(timer); fn(); }, t);};
// 测试// mySetTimeout(()=>{// console.log(1);// },1000)
实现Object.create
Object.create()
办法创立一个新对象,应用现有的对象来提供新创建的对象的__proto__
// 模仿 Object.createfunction create(proto) { function F() {} F.prototype = proto; return new F();}
实现一个迷你版的vue
入口
// js/vue.jsclass Vue { constructor (options) { // 1. 通过属性保留选项的数据 this.$options = options || {} this.$data = options.data || {} this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el // 2. 把data中的成员转换成getter和setter,注入到vue实例中 this._proxyData(this.$data) // 3. 调用observer对象,监听数据的变动 new Observer(this.$data) // 4. 调用compiler对象,解析指令和差值表达式 new Compiler(this) } _proxyData (data) { // 遍历data中的所有属性 Object.keys(data).forEach(key => { // 把data的属性注入到vue实例中 Object.defineProperty(this, key, { enumerable: true, configurable: true, get () { return data[key] }, set (newValue) { if (newValue === data[key]) { return } data[key] = newValue } }) }) }}
实现Dep
class Dep { constructor () { // 存储所有的观察者 this.subs = [] } // 增加观察者 addSub (sub) { if (sub && sub.update) { this.subs.push(sub) } } // 发送告诉 notify () { this.subs.forEach(sub => { sub.update() }) }}
实现watcher
class Watcher { constructor (vm, key, cb) { this.vm = vm // data中的属性名称 this.key = key // 回调函数负责更新视图 this.cb = cb // 把watcher对象记录到Dep类的动态属性target Dep.target = this // 触发get办法,在get办法中会调用addSub this.oldValue = vm[key] Dep.target = null } // 当数据发生变化的时候更新视图 update () { let newValue = this.vm[this.key] if (this.oldValue === newValue) { return } this.cb(newValue) }}
实现compiler
class Compiler { constructor (vm) { this.el = vm.$el this.vm = vm this.compile(this.el) } // 编译模板,解决文本节点和元素节点 compile (el) { let childNodes = el.childNodes Array.from(childNodes).forEach(node => { // 解决文本节点 if (this.isTextNode(node)) { this.compileText(node) } else if (this.isElementNode(node)) { // 解决元素节点 this.compileElement(node) } // 判断node节点,是否有子节点,如果有子节点,要递归调用compile if (node.childNodes && node.childNodes.length) { this.compile(node) } }) } // 编译元素节点,解决指令 compileElement (node) { // console.log(node.attributes) // 遍历所有的属性节点 Array.from(node.attributes).forEach(attr => { // 判断是否是指令 let attrName = attr.name if (this.isDirective(attrName)) { // v-text --> text attrName = attrName.substr(2) let key = attr.value this.update(node, key, attrName) } }) } update (node, key, attrName) { let updateFn = this[attrName + 'Updater'] updateFn && updateFn.call(this, node, this.vm[key], key) } // 解决 v-text 指令 textUpdater (node, value, key) { node.textContent = value new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } // v-model modelUpdater (node, value, key) { node.value = value new Watcher(this.vm, key, (newValue) => { node.value = newValue }) // 双向绑定 node.addEventListener('input', () => { this.vm[key] = node.value }) } // 编译文本节点,解决差值表达式 compileText (node) { // console.dir(node) // {{ msg }} let reg = /\{\{(.+?)\}\}/ let value = node.textContent if (reg.test(value)) { let key = RegExp.$1.trim() node.textContent = value.replace(reg, this.vm[key]) // 创立watcher对象,当数据扭转更新视图 new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } } // 判断元素属性是否是指令 isDirective (attrName) { return attrName.startsWith('v-') } // 判断节点是否是文本节点 isTextNode (node) { return node.nodeType === 3 } // 判断节点是否是元素节点 isElementNode (node) { return node.nodeType === 1 }}
实现Observer
class Observer { constructor (data) { this.walk(data) } walk (data) { // 1. 判断data是否是对象 if (!data || typeof data !== 'object') { return } // 2. 遍历data对象的所有属性 Object.keys(data).forEach(key => { this.defineReactive(data, key, data[key]) }) } defineReactive (obj, key, val) { let that = this // 负责收集依赖,并发送告诉 let dep = new Dep() // 如果val是对象,把val外部的属性转换成响应式数据 this.walk(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get () { // 收集依赖 Dep.target && dep.addSub(Dep.target) return val }, set (newValue) { if (newValue === val) { return } val = newValue that.walk(newValue) // 发送告诉 dep.notify() } }) }}
应用
<!DOCTYPE html><html lang="cn"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Mini Vue</title></head><body> <div id="app"> <h1>差值表达式</h1> <h3>{{ msg }}</h3> <h3>{{ count }}</h3> <h1>v-text</h1> <div v-text="msg"></div> <h1>v-model</h1> <input type="text" v-model="msg"> <input type="text" v-model="count"> </div> <script src="./js/dep.js"></script> <script src="./js/watcher.js"></script> <script src="./js/compiler.js"></script> <script src="./js/observer.js"></script> <script src="./js/vue.js"></script> <script> let vm = new Vue({ el: '#app', data: { msg: 'Hello Vue', count: 100, person: { name: 'zs' } } }) console.log(vm.msg) // vm.msg = { test: 'Hello' } vm.test = 'abc' </script></body></html>
实现一个迭代器生成函数
ES6对迭代器的实现
JS原生的汇合类型数据结构,只有Array
(数组)和Object
(对象);而ES6
中,又新增了Map
和Set
。四种数据结构各自有着本人特地的外部实现,但咱们仍期待以同样的一套规定去遍历它们,所以ES6
在推出新数据结构的同时也推出了一套 对立的接口机制 ——迭代器(Iterator
)。
ES6
约定,任何数据结构只有具备Symbol.iterator
属性(这个属性就是Iterator
的具体实现,它实质上是以后数据结构默认的迭代器生成函数),就能够被遍历——精确地说,是被for...of...
循环和迭代器的next办法遍历。 事实上,for...of...
的背地正是对next
办法的重复调用。
在ES6中,针对Array
、Map
、Set
、String
、TypedArray
、函数的 arguments
对象、NodeList
对象这些原生的数据结构都能够通过for...of...
进行遍历。原理都是一样的,此处咱们拿最简略的数组进行举例,当咱们用for...of...
遍历数组时:
const arr = [1, 2, 3]const len = arr.lengthfor(item of arr) { console.log(`以后元素是${item}`)}
之所以可能按程序一次一次地拿到数组里的每一个成员,是因为咱们借助数组的Symbol.iterator
生成了它对应的迭代器对象,通过重复调用迭代器对象的next
办法拜访了数组成员,像这样:
const arr = [1, 2, 3]// 通过调用iterator,拿到迭代器对象const iterator = arr[Symbol.iterator]()// 对迭代器对象执行next,就能一一拜访汇合的成员iterator.next()iterator.next()iterator.next()
丢进控制台,咱们能够看到next
每次会按程序帮咱们拜访一个汇合成员:
而for...of...
做的事件,根本等价于上面这通操作:
// 通过调用iterator,拿到迭代器对象const iterator = arr[Symbol.iterator]()// 初始化一个迭代后果let now = { done: false }// 循环往外迭代成员while(!now.done) { now = iterator.next() if(!now.done) { console.log(`当初遍历到了${now.value}`) }}
能够看出,for...of...
其实就是iterator
循环调用换了种写法。在ES6中咱们之所以可能开心地用for...of...
遍历各种各种的汇合,全靠迭代器模式在背地给力。
ps:此处举荐浏览迭代协定 (opens new window),置信大家读过后会对迭代器在ES6中的实现有更深的了解。
Promise.race
Promise.race = function(promiseArr) { return new Promise((resolve, reject) => { promiseArr.forEach(p => { // 如果不是Promise实例须要转化为Promise实例 Promise.resolve(p).then( val => resolve(val), err => reject(err), ) }) })}
实现Array.isArray办法
Array.myIsArray = function(o) { return Object.prototype.toString.call(Object(o)) === '[object Array]';};console.log(Array.myIsArray([])); // true
实现一个compose函数
组合多个函数,从右到左,比方:compose(f, g, h)
最终失去这个后果(...args) => f(g(h(...args))).
题目形容:实现一个 compose
函数
// 用法如下:function fn1(x) { return x + 1;}function fn2(x) { return x + 2;}function fn3(x) { return x + 3;}function fn4(x) { return x + 4;}const a = compose(fn1, fn2, fn3, fn4);console.log(a(1)); // 1+4+3+2+1=11
实现代码如下
function compose(...funcs) { if (!funcs.length) return (v) => v; if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => { return (...args) => a(b(...args))) }}
compose
创立了一个从右向左执行的数据流。如果要实现从左到右的数据流,能够间接更改compose
的局部代码即可实现
- 更换
Api
接口:把reduce
改为reduceRight
- 交互包裹地位:把
a(b(...args))
改为b(a(...args))
二分查找
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("指标元素不在数组中");// }
实现一个链表构造
链表构造
看图了解next层级
// 链表 从头尾删除、减少 性能比拟好// 分为很多类 罕用单向链表、双向链表// js模仿链表构造:增删改查// node节点class Node { constructor(element,next) { this.element = element this.next = next } }class LinkedList { constructor() { this.head = null // 默认应该指向第一个节点 this.size = 0 // 通过这个长度能够遍历这个链表 } // 减少O(n) add(index,element) { if(arguments.length === 1) { // 向开端增加 element = index // 以后元素等于传递的第一项 index = this.size // 索引指向最初一个元素 } if(index < 0 || index > this.size) { throw new Error('增加的索引不失常') } if(index === 0) { // 间接找到头部 把头部改掉 性能更好 let head = this.head this.head = new Node(element,head) } else { // 获取以后头指针 let current = this.head // 不停遍历 直到找到最初一项 增加的索引是1就找到第0个的next赋值 for (let i = 0; i < index-1; i++) { // 找到它的前一个 current = current.next } // 让创立的元素指向上一个元素的下一个 // 看图了解next层级 current.next = new Node(element,current.next) // 让以后元素指向下一个元素的next } this.size++; } // 删除O(n) remove(index) { if(index < 0 || index >= this.size) { throw new Error('删除的索引不失常') } this.size-- if(index === 0) { let head = this.head this.head = this.head.next // 挪动指针地位 return head // 返回删除的元素 }else { let current = this.head for (let i = 0; i < index-1; i++) { // index-1找到它的前一个 current = current.next } let returnVal = current.next // 返回删除的元素 // 找到待删除的指针的上一个 current.next.next // 如删除200, 100=>200=>300 找到200的上一个100的next的next为300,把300赋值给100的next即可 current.next = current.next.next return returnVal } } // 查找O(n) get(index) { if(index < 0 || index >= this.size) { throw new Error('查找的索引不失常') } let current = this.head for (let i = 0; i < index; i++) { current = current.next } return current }}var ll = new LinkedList()ll.add(0,100) // Node { ellement: 100, next: null }ll.add(0,200) // Node { element: 200, next: Node { element: 100, next: null } }ll.add(1,500) // Node {element: 200,next: Node { element: 100, next: Node { element: 500, next: null } } }ll.add(300)ll.remove(0)console.log(ll.get(2),'get')console.log(ll.head)module.exports = LinkedList
模仿new操作
3个步骤:
- 以
ctor.prototype
为原型创立一个对象。 - 执行构造函数并将this绑定到新创建的对象上。
- 判断构造函数执行返回的后果是否是援用数据类型,若是则返回构造函数执行的后果,否则返回创立的对象。
function newOperator(ctor, ...args) { if (typeof ctor !== 'function') { throw new TypeError('Type Error'); } const obj = Object.create(ctor.prototype); const res = ctor.apply(obj, args); const isObject = typeof res === 'object' && res !== null; const isFunction = typeof res === 'function'; return isObject || isFunction ? res : obj;}
实现lodash的chunk办法--数组按指定长度拆分
题目
/** * @param input * @param size * @returns {Array} */_.chunk(['a', 'b', 'c', 'd'], 2)// => [['a', 'b'], ['c', 'd']]_.chunk(['a', 'b', 'c', 'd'], 3)// => [['a', 'b', 'c'], ['d']]_.chunk(['a', 'b', 'c', 'd'], 5)// => [['a', 'b', 'c', 'd']]_.chunk(['a', 'b', 'c', 'd'], 0)// => []
实现
function chunk(arr, length) { let newArr = []; for (let i = 0; i < arr.length; i += length) { newArr.push(arr.slice(i, i + length)); } return newArr;}