请实现一个 add 函数,满足以下功能
add(1); // 1add(1)(2); // 3add(1)(2)(3);// 6add(1)(2, 3); // 6add(1, 2)(3); // 6add(1, 2, 3); // 6
复制代码
function add(...args) { // 在内部声明一个函数,利用闭包的特性保存并收集所有的参数值 let fn = function(...newArgs) { return add.apply(null, args.concat(newArgs)) }
// 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回 fn.toString = function() { return args.reduce((total,curr)=> total + curr) }
return fn}
复制代码
考点:
// 测试,调用toString方法触发求值
add(1).toString(); // 1add(1)(2).toString(); // 3add(1)(2)(3).toString();// 6add(1)(2, 3).toString(); // 6add(1, 2)(3).toString(); // 6add(1, 2, 3).toString(); // 6
复制代码
数组去重方法汇总
首先:我知道多少种去重方式
1. 双层 for 循环
function distinct(arr) { for (let i=0, len=arr.length; i<len; i++) { for (let j=i+1; j<len; j++) { if (arr[i] == arr[j]) { arr.splice(j, 1); // splice 会改变数组长度,所以要将数组长度 len 和下标 j 减一 len--; j--; } } } return arr;}
复制代码
思想: 双重 for 循环是比较笨拙的方法,它实现的原理很简单:先定义一个包含原始数组第一个元素的数组,然后遍历原始数组,将原始数组中的每个元素与新数组中的每个元素进行比对,如果不重复则添加到新数组中,最后返回新数组;因为它的时间复杂度是O(n^2),如果数组长度很大,效率会很低
2. Array.filter() 加 indexOf/includes
function distinct(a, b) { let arr = a.concat(b); return arr.filter((item, index)=> { //return arr.indexOf(item) === index return arr.includes(item) })}
复制代码
思想: 利用indexOf检测元素在数组中第一次出现的位置是否和元素现在的位置相等,如果不等则说明该元素是重复元素
3. ES6 中的 Set 去重
function distinct(array) { return Array.from(new Set(array));}
复制代码
思想: ES6 提供了新的数据结构 Set,Set 结构的一个特性就是成员值都是唯一的,没有重复的值。
4. reduce 实现对象数组去重复
var resources = [ { name: "张三", age: "18" }, { name: "张三", age: "19" }, { name: "张三", age: "20" }, { name: "李四", age: "19" }, { name: "王五", age: "20" }, { name: "赵六", age: "21" }]var temp = {};resources = resources.reduce((prev, curv) => { // 如果临时对象中有这个名字,什么都不做 if (temp[curv.name]) {
}else { // 如果临时对象没有就把这个名字加进去,同时把当前的这个对象加入到prev中 temp[curv.name] = true; prev.push(curv); } return prev}, []);console.log("结果", resources);
复制代码
这种方法是利用高阶函数 reduce 进行去重, 这里只需要注意initialValue得放一个空数组[],不然没法push
实现观察者模式
观察者模式(基于发布订阅模式) 有观察者,也有被观察者
观察者需要放到被观察者中,被观察者的状态变化需要通知观察者 我变化了 内部也是基于发布订阅模式,收集观察者,状态变化后要主动通知观察者
class Subject { // 被观察者 学生 constructor(name) { this.state = 'happy' this.observers = []; // 存储所有的观察者 } // 收集所有的观察者 attach(o){ // Subject. prototype. attch this.observers.push(o) } // 更新被观察者 状态的方法 setState(newState) { this.state = newState; // 更新状态 // this 指被观察者 学生 this.observers.forEach(o => o.update(this)) // 通知观察者 更新它们的状态 }}
class Observer{ // 观察者 父母和老师 constructor(name) { this.name = name } update(student) { console.log('当前' + this.name + '被通知了', '当前学生的状态是' + student.state) }}
let student = new Subject('学生');
let parent = new Observer('父母'); let teacher = new Observer('老师');
// 被观察者存储观察者的前提,需要先接纳观察者student. attach(parent); student. attach(teacher); student. setState('被欺负了');
复制代码
实现一个链表结构
链表结构
看图理解 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
复制代码
参考前端手写面试题详细解答
怎么在制定数据源里面生成一个长度为 n 的不重复随机数组 能有几种方法 时间复杂度多少(字节)
第一版 时间复杂度为 O(n^2)
function getTenNum(testArray, n) { let result = []; for (let i = 0; i < n; ++i) { const random = Math.floor(Math.random() * testArray.length); const cur = testArray[random]; if (result.includes(cur)) { i--; break; } result.push(cur); } return result;}const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];const resArr = getTenNum(testArray, 10);
复制代码
第二版 标记法 / 自定义属性法 时间复杂度为 O(n)
function getTenNum(testArray, n) { let hash = {}; let result = []; let ranNum = n; while (ranNum > 0) { const ran = Math.floor(Math.random() * testArray.length); if (!hash[ran]) { hash[ran] = true; result.push(ran); ranNum--; } } return result;}const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];const resArr = getTenNum(testArray, 10);
复制代码
第三版 交换法 时间复杂度为 O(n)
function getTenNum(testArray, n) { const cloneArr = [...testArray]; let result = []; for (let i = 0; i < n; i++) { debugger; const ran = Math.floor(Math.random() * (cloneArr.length - i)); result.push(cloneArr[ran]); cloneArr[ran] = cloneArr[cloneArr.length - i - 1]; } return result;}const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];const resArr = getTenNum(testArray, 14);
复制代码
值得一提的是操作数组的时候使用交换法 这种思路在算法里面很常见
最终版 边遍历边删除 时间复杂度为 O(n)
function getTenNum(testArray, n) { const cloneArr = [...testArray]; let result = []; for (let i = 0; i < n; ++i) { const random = Math.floor(Math.random() * cloneArr.length); const cur = cloneArr[random]; result.push(cur); cloneArr.splice(random, 1); } return result;}const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];const resArr = getTenNum(testArray, 14);
复制代码
实现一下 hash 路由
基础的html代码:
<html> <style> html, body { margin: 0; height: 100%; } ul { list-style: none; margin: 0; padding: 0; display: flex; justify-content: center; } .box { width: 100%; height: 100%; background-color: red; } </style> <body> <ul> <li> <a href="#red">红色</a> </li> <li> <a href="#green">绿色</a> </li> <li> <a href="#purple">紫色</a> </li> </ul> </body></html>
复制代码
简单实现:
<script> const box = document.getElementsByClassName('box')[0]; const hash = location.hash window.onhashchange = function (e) { const color = hash.slice(1) box.style.background = color }</script>
复制代码
封装成一个 class:
<script> const box = document.getElementsByClassName('box')[0]; const hash = location.hash class HashRouter { constructor (hashStr, cb) { this.hashStr = hashStr this.cb = cb this.watchHash() this.watch = this.watchHash.bind(this) window.addEventListener('hashchange', this.watch) } watchHash () { let hash = window.location.hash.slice(1) this.hashStr = hash this.cb(hash) } } new HashRouter('red', (color) => { box.style.background = color })</script>
复制代码
实现 redux 中间件
简单实现
function createStore(reducer) { let currentState let listeners = []
function getState() { return currentState }
function dispatch(action) { currentState = reducer(currentState, action) listeners.map(listener => { listener() }) return action }
function subscribe(cb) { listeners.push(cb) return () => {} }
dispatch({type: 'ZZZZZZZZZZ'})
return { getState, dispatch, subscribe }}
// 应用实例如下:function reducer(state = 0, action) { switch (action.type) { case 'ADD': return state + 1 case 'MINUS': return state - 1 default: return state }}
const store = createStore(reducer)
console.log(store);store.subscribe(() => { console.log('change');})console.log(store.getState());console.log(store.dispatch({type: 'ADD'}));console.log(store.getState());
复制代码
2. 迷你版
export const createStore = (reducer,enhancer)=>{ if(enhancer) { return enhancer(createStore)(reducer) } let currentState = {} let currentListeners = []
const getState = ()=>currentState const subscribe = (listener)=>{ currentListeners.push(listener) } const dispatch = action=>{ currentState = reducer(currentState, action) currentListeners.forEach(v=>v()) return action } dispatch({type:'@@INIT'}) return {getState,subscribe,dispatch}}
//中间件实现export applyMiddleWare(...middlewares){ return createStore=>...args=>{ const store = createStore(...args) let dispatch = store.dispatch
const midApi = { getState:store.getState, dispatch:...args=>dispatch(...args) } const middlewaresChain = middlewares.map(middleware=>middleware(midApi)) dispatch = compose(...middlewaresChain)(store.dispatch) return { ...store, dispatch } }
// fn1(fn2(fn3())) 把函数嵌套依次调用export function compose(...funcs){ if(funcs.length===0){ return arg=>arg } if(funs.length===1){ return funs[0] } return funcs.reduce((ret,item)=>(...args)=>ret(item(...args)))}
//bindActionCreator实现
function bindActionCreator(creator,dispatch){ return ...args=>dispatch(creator(...args))}function bindActionCreators(creators,didpatch){ //let bound = {} //Object.keys(creators).forEach(v=>{ // let creator = creator[v] // bound[v] = bindActionCreator(creator,dispatch) //}) //return bound
return Object.keys(creators).reduce((ret,item)=>{ ret[item] = bindActionCreator(creators[item],dispatch) return ret },{})}
复制代码
实现一个 JS 函数柯里化
预先处理的思想,利用闭包的机制
柯里化把多次传入的参数合并,柯里化是一个高阶函数
每次都返回一个新函数
每次入参都是一个
当柯里化函数接收到足够参数后,就会执行原函数,如何去确定何时达到足够的参数呢?
有两种思路:
将这两点结合一下,实现一个简单 curry 函数
通用版
// 写法1function curry(fn, args) { var length = fn.length; var args = args || []; return function(){ newArgs = args.concat(Array.prototype.slice.call(arguments)); if (newArgs.length < length) { return curry.call(this,fn,newArgs); }else{ return fn.apply(this,newArgs); } }}
复制代码
// 写法2// 分批传入参数// redux 源码的compose也是用了类似柯里化的操作const curry = (fn, arr = []) => {// arr就是我们要收集每次调用时传入的参数 let len = fn.length; // 函数的长度,就是参数的个数
return function(...args) { let newArgs = [...arr, ...args] // 收集每次传入的参数
// 如果传入的参数个数等于我们指定的函数参数个数,就执行指定的真正函数 if(newArgs.length === len) { return fn(...newArgs) } else { // 递归收集参数 return curry(fn, newArgs) } }}
复制代码
// 测试function multiFn(a, b, c) { return a * b * c;}
var multi = curry(multiFn);
multi(2)(3)(4);multi(2,3,4);multi(2)(3,4);multi(2,3)(4)
复制代码
ES6 写法
const curry = (fn, arr = []) => (...args) => ( arg => arg.length === fn.length ? fn(...arg) : curry(fn, arg))([...arr, ...args])
复制代码
// 测试let curryTest=curry((a,b,c,d)=>a+b+c+d)curryTest(1,2,3)(4) //返回10curryTest(1,2)(4)(3) //返回10curryTest(1,2)(3,4) //返回10
复制代码
// 柯里化求值// 指定的函数function sum(a,b,c,d,e) { return a + b + c + d + e}
// 传入指定的函数,执行一次let newSum = curry(sum)
// 柯里化 每次入参都是一个参数newSum(1)(2)(3)(4)(5)
// 偏函数newSum(1)(2)(3,4,5)
复制代码
// 柯里化简单应用// 判断类型,参数多少个,就执行多少次收集function isType(type, val) { return Object.prototype.toString.call(val) === `[object ${type}]`}
let newType = curry(isType)
// 相当于把函数参数一个个传了,把第一次先缓存起来let isString = newType('String')let isNumber = newType('Number')
isString('hello world')isNumber(999)
复制代码
实现千位分隔符
// 保留三位小数parseToMoney(1234.56); // return '1,234.56'parseToMoney(123456789); // return '123,456,789'parseToMoney(1087654.321); // return '1,087,654.321'
复制代码
function parseToMoney(num) { num = parseFloat(num.toFixed(3)); let [integer, decimal] = String.prototype.split.call(num, '.'); integer = integer.replace(/\d(?=(\d{3})+$)/g, '$&,'); return integer + '.' + (decimal ? decimal : '');}
复制代码
正则表达式(运用了正则的前向声明和反前向声明):
function parseToMoney(str){ // 仅仅对位置进行匹配 let re = /(?=(?!\b)(\d{3})+$)/g; return str.replace(re,','); }
复制代码
实现非负大整数相加
JavaScript 对数值有范围的限制,限制如下:
Number.MAX_VALUE // 1.7976931348623157e+308Number.MAX_SAFE_INTEGER // 9007199254740991Number.MIN_VALUE // 5e-324Number.MIN_SAFE_INTEGER // -9007199254740991
复制代码
如果想要对一个超大的整数(> Number.MAX_SAFE_INTEGER)进行加法运算,但是又想输出一般形式,那么使用 + 是无法达到的,一旦数字超过 Number.MAX_SAFE_INTEGER 数字会被立即转换为科学计数法,并且数字精度相比以前将会有误差。
实现一个算法进行大数的相加:
function sumBigNumber(a, b) { let res = ''; let temp = 0;
a = a.split(''); b = b.split('');
while (a.length || b.length || temp) { temp += ~~a.pop() + ~~b.pop(); res = (temp % 10) + res; temp = temp > 9 } return res.replace(/^0+/, '');}
复制代码
其主要的思路如下:
首先用字符串的方式来保存大数,这样数字在数学表示上就不会发生变化
初始化 res,temp 来保存中间的计算结果,并将两个字符串转化为数组,以便进行每一位的加法运算
将两个数组的对应的位进行相加,两个数相加的结果可能大于 10,所以可能要仅为,对 10 进行取余操作,将结果保存在当前位
判断当前位是否大于 9,也就是是否会进位,若是则将 temp 赋值为 true,因为在加法运算中,true 会自动隐式转化为 1,以便于下一次相加
重复上述操作,直至计算结束
手写常见排序
冒泡排序
冒泡排序的原理如下,从第一个元素开始,把当前元素和下一个索引元素进行比较。如果当前元素大,那么就交换位置,重复操作直到比较到最后一个元素,那么此时最后一个元素就是该数组中最大的数。下一轮重复以上操作,但是此时最后一个元素已经是最大数了,所以不需要再比较最后一个元素,只需要比较到 length - 1 的位置。
function bubbleSort(list) { var n = list.length; if (!n) return [];
for (var i = 0; i < n; i++) { // 注意这里需要 n - i - 1 for (var j = 0; j < n - i - 1; j++) { if (list[j] > list[j + 1]) { var temp = list[j + 1]; list[j + 1] = list[j]; list[j] = temp; } } } return list;}
复制代码
快速排序
快排的原理如下。随机选取一个数组中的值作为基准值,从左至右取值与基准值对比大小。比基准值小的放数组左边,大的放右边,对比完成后将基准值和第一个比基准值大的值交换位置。然后将数组以基准值的位置分为两部分,继续递归以上操作
ffunction quickSort(arr) { if (arr.length<=1){ return arr; } var baseIndex = Math.floor(arr.length/2);//向下取整,选取基准点 var base = arr.splice(baseIndex,1)[0];//取出基准点的值, // splice 通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。 // slice方法返回一个新的数组对象,不会更改原数组 //这里不能直接base=arr[baseIndex],因为base代表的每次都删除的那个数 var left=[]; var right=[]; for (var i = 0; i<arr.length; i++){ // 这里的length是变化的,因为splice会改变原数组。 if (arr[i] < base){ left.push(arr[i]);//比基准点小的放在左边数组, } }else{ right.push(arr[i]);//比基准点大的放在右边数组, } return quickSort(left).concat([base],quickSort(right));}
复制代码
选择排序
function selectSort(arr) { // 缓存数组长度 const len = arr.length; // 定义 minIndex,缓存当前区间最小值的索引,注意是索引 let minIndex; // i 是当前排序区间的起点 for (let i = 0; i < len - 1; i++) { // 初始化 minIndex 为当前区间第一个元素 minIndex = i; // i、j分别定义当前区间的上下界,i是左边界,j是右边界 for (let j = i; j < len; j++) { // 若 j 处的数据项比当前最小值还要小,则更新最小值索引为 j if (arr[j] < arr[minIndex]) { minIndex = j; } } // 如果 minIndex 对应元素不是目前的头部元素,则交换两者 if (minIndex !== i) { [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]; } } return arr;}// console.log(selectSort([3, 6, 2, 4, 1]));
复制代码
插入排序
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]));
复制代码
实现 every 方法
Array.prototype.myEvery=function(callback, context = window){ var len=this.length, flag=true, i = 0;
for(;i < len; i++){ if(!callback.apply(context,[this[i], i , this])){ flag=false; break; } } return flag; }
// var obj = {num: 1} // var aa=arr.myEvery(function(v,index,arr){ // return v.num>=12; // },obj) // console.log(aa)
复制代码
查找字符串中出现最多的字符和个数
例: abbcccddddd -> 字符最多的是 d,出现了 5 次
let str = "abcabcabcbbccccc";let num = 0;let char = '';
// 使其按照一定的次序排列str = str.split('').sort().join('');// "aaabbbbbcccccccc"
// 定义正则表达式let re = /(\w)\1+/g;str.replace(re,($0,$1) => { if(num < $0.length){ num = $0.length; char = $1; }});console.log(`字符最多的是${char},出现了${num}次`);
复制代码
实现节流函数(throttle)
节流函数原理:指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的时间才会执行回调函数。总结起来就是: 事件,按照一段时间的间隔来进行触发 。
像 dom 的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多
手写简版
使用时间戳的节流函数会在第一次触发事件时立即执行,以后每过 wait 秒之后才执行一次,并且最后一次触发事件不会被执行
时间戳方式:
// func是用户传入需要防抖的函数// wait是等待时间const throttle = (func, wait = 50) => { // 上一次执行该函数的时间 let lastTime = 0 return function(...args) { // 当前时间 let now = +new Date() // 将当前时间和上一次执行函数时间对比 // 如果差值大于设置的等待时间就执行函数 if (now - lastTime > wait) { lastTime = now func.apply(this, args) } }}
setInterval( throttle(() => { console.log(1) }, 500), 1)
复制代码
定时器方式:
使用定时器的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数
function throttle(func, delay){ var timer = null; returnfunction(){ var context = this; var args = arguments; if(!timer){ timer = setTimeout(function(){ func.apply(context, args); timer = null; },delay); } }}
复制代码
适用场景:
总结
Object.is
Object.is解决的主要是这两个问题:
+0 === -0 // trueNaN === NaN // false
复制代码
const is= (x, y) => { if (x === y) { // +0和-0应该不相等 return x !== 0 || y !== 0 || 1/x === 1/y; } else { return x !== x && y !== y; }}
复制代码
debounce(防抖)
触发高频时间后 n 秒内函数只会执行一次,如果 n 秒内高频时间再次触发,则重新计算时间。
const debounce = (fn, time) => { let timeout = null; return function() { clearTimeout(timeout) timeout = setTimeout(() => { fn.apply(this, arguments); }, time); }};
复制代码
防抖常应用于用户进行搜索输入节约请求资源,window触发resize事件时进行防抖只触发一次。
实现 add(1)(2)(3)
函数柯里化概念: 柯里化(Currying)是把接受多个参数的函数转变为接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。
1)粗暴版
function add (a) {return function (b) { return function (c) { return a + b + c; }}}console.log(add(1)(2)(3)); // 6
复制代码
2)柯里化解决方案
var add = function (m) { var temp = function (n) { return add(m + n); } temp.toString = function () { return m; } return temp;};console.log(add(3)(4)(5)); // 12console.log(add(3)(6)(9)(25)); // 43
复制代码
对于 add(3)(4)(5),其执行过程如下:
先执行 add(3),此时 m=3,并且返回 temp 函数;
执行 temp(4),这个函数内执行 add(m+n),n 是此次传进来的数值 4,m 值还是上一步中的 3,所以 add(m+n)=add(3+4)=add(7),此时 m=7,并且返回 temp 函数
执行 temp(5),这个函数内执行 add(m+n),n 是此次传进来的数值 5,m 值还是上一步中的 7,所以 add(m+n)=add(7+5)=add(12),此时 m=12,并且返回 temp 函数
由于后面没有传入参数,等于返回的 temp 函数不被执行而是打印,了解 JS 的朋友都知道对象的 toString 是修改对象转换字符串的方法,因此代码中 temp 函数的 toString 函数 return m 值,而 m 值是最后一步执行函数时的值 m=12,所以返回值是 12。
function add (...args) { //求和 return args.reduce((a, b) => a + b)}function currying (fn) { let args = [] return function temp (...newArgs) { if (newArgs.length) { args = [ ...args, ...newArgs ] return temp } else { let val = fn.apply(this, args) args = [] //保证再次调用时清空 return val } }}let addCurry = currying(add)console.log(addCurry(1)(2)(3)(4, 5)()) //15console.log(addCurry(1)(2)(3, 4, 5)()) //15console.log(addCurry(1)(2, 3, 4, 5)()) //15
复制代码
图片懒加载
可以给 img 标签统一自定义属性data-src='default.png',当检测到图片出现在窗口之后再补充 src 属性,此时才会进行图片资源加载。
function lazyload() { const imgs = document.getElementsByTagName('img'); const len = imgs.length; // 视口的高度 const viewHeight = document.documentElement.clientHeight; // 滚动条高度 const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop; for (let i = 0; i < len; i++) { const offsetHeight = imgs[i].offsetTop; if (offsetHeight < viewHeight + scrollHeight) { const src = imgs[i].dataset.src; imgs[i].src = src; } }}
// 可以使用节流优化一下window.addEventListener('scroll', lazyload);
复制代码
转化为驼峰命名
var s1 = "get-element-by-id"
// 转化为 getElementById
复制代码
var f = function(s) { return s.replace(/-\w/g, function(x) { return x.slice(1).toUpperCase(); })}
复制代码
手写节流函数
函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。
// 函数节流的实现;function throttle(fn, delay) { let curTime = Date.now();
return function() { let context = this, args = arguments, nowTime = Date.now();
// 如果两次时间间隔超过了指定时间,则执行函数。 if (nowTime - curTime >= delay) { curTime = Date.now(); return fn.apply(context, args); } };}
复制代码
// 题目let a = "9007199254740991";let b = "1234567899999999999";
function add(a ,b){ //...}
复制代码
实现代码如下:
function add(a ,b){ //取两个数字的最大长度 let maxLength = Math.max(a.length, b.length); //用0去补齐长度 a = a.padStart(maxLength , 0);//"0009007199254740991" b = b.padStart(maxLength , 0);//"1234567899999999999" //定义加法过程中需要用到的变量 let t = 0; let f = 0; //"进位" let sum = ""; for(let i=maxLength-1 ; i>=0 ; i--){ t = parseInt(a[i]) + parseInt(b[i]) + f; f = Math.floor(t/10); sum = t%10 + sum; } if(f!==0){ sum = '' + f + sum; } return sum;}
复制代码
原生实现
function ajax() { let xhr = new XMLHttpRequest() //实例化,以调用方法 xhr.open('get', 'https://www.google.com') //参数2,url。参数三:异步 xhr.onreadystatechange = () => { //每当 readyState 属性改变时,就会调用该函数。 if (xhr.readyState === 4) { //XMLHttpRequest 代理当前所处状态。 if (xhr.status >= 200 && xhr.status < 300) { //200-300请求成功 let string = request.responseText //JSON.parse() 方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象 let object = JSON.parse(string) } } } request.send() //用于实际发出 HTTP 请求。不带参数为GET请求}
复制代码
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))
复制代码
实现一个简易的 MVVM
实现一个简易的MVVM我会分为这么几步来:
首先我会定义一个类Vue,这个类接收的是一个options,那么其中可能有需要挂载的根元素的id,也就是el属性;然后应该还有一个data属性,表示需要双向绑定的数据
其次我会定义一个Dep类,这个类产生的实例对象中会定义一个subs数组用来存放所依赖这个属性的依赖,已经添加依赖的方法addSub,删除方法removeSub,还有一个notify方法用来遍历更新它subs中的所有依赖,同时 Dep 类有一个静态属性target它用来表示当前的观察者,当后续进行依赖收集的时候可以将它添加到dep.subs中。
然后设计一个observe方法,这个方法接收的是传进来的data,也就是options.data,里面会遍历data中的每一个属性,并使用Object.defineProperty()来重写它的get和set,那么这里面呢可以使用new Dep()实例化一个dep对象,在get的时候调用其addSub方法添加当前的观察者Dep.target完成依赖收集,并且在set的时候调用dep.notify方法来通知每一个依赖它的观察者进行更新
完成这些之后,我们还需要一个compile方法来将 HTML 模版和数据结合起来。在这个方法中首先传入的是一个node节点,然后遍历它的所有子级,判断是否有firstElmentChild,有的话则进行递归调用 compile 方法,没有firstElementChild的话且该child.innderHTML用正则匹配满足有/\{\{(.*)\}\}/项的话则表示有需要双向绑定的数据,那么就将用正则new Reg('\\{\\{\\s*' + key + '\\s*\\}\\}', 'gm')替换掉是其为msg变量。
完成变量替换的同时,还需要将Dep.target指向当前的这个child,且调用一下this.opt.data[key],也就是为了触发这个数据的get来对当前的child进行依赖收集,这样下次数据变化的时候就能通知child进行视图更新了,不过在最后要记得将Dep.target指为null哦(其实在Vue中是有一个targetStack栈用来存放target的指向的)
那么最后我们只需要监听document的DOMContentLoaded然后在回调函数中实例化这个Vue对象就可以了
coding :
需要注意的点:
完整代码如下:
<!DOCTYPE html><html lang="en"> <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>MVVM</title> </head> <body> <div id="app"> <h3>姓名</h3> <p>{{name}}</p> <h3>年龄</h3> <p>{{age}}</p> </div> </body></html><script> document.addEventListener( "DOMContentLoaded", function () { let opt = { el: "#app", data: { name: "等待修改...", age: 20 } }; let vm = new Vue(opt); setTimeout(() => { opt.data.name = "jing"; }, 2000); }, false ); class Vue { constructor(opt) { this.opt = opt; this.observer(opt.data); let root = document.querySelector(opt.el); this.compile(root); } observer(data) { Object.keys(data).forEach((key) => { let obv = new Dep(); data["_" + key] = data[key];
Object.defineProperty(data, key, { get() { Dep.target && obv.addSubNode(Dep.target); return data["_" + key]; }, set(newVal) { obv.update(newVal); data["_" + key] = newVal; }, }); }); } compile(node) { [].forEach.call(node.childNodes, (child) => { if (!child.firstElementChild && /\{\{(.*)\}\}/.test(child.innerHTML)) { let key = RegExp.$1.trim(); child.innerHTML = child.innerHTML.replace( new RegExp("\\{\\{\\s*" + key + "\\s*\\}\\}", "gm"), this.opt.data[key] ); Dep.target = child; this.opt.data[key]; Dep.target = null; } else if (child.firstElementChild) this.compile(child); }); } }
class Dep { constructor() { this.subNode = []; } addSubNode(node) { this.subNode.push(node); } update(newVal) { this.subNode.forEach((node) => { node.innerHTML = newVal; }); } }</script>
复制代码
简化版 2
function update(){ console.log('数据变化~~~ mock update view')}let obj = [1,2,3]// 变异方法 push shift unshfit reverse sort splice pop// Object.definePropertylet oldProto = Array.prototype;let proto = Object.create(oldProto); // 克隆了一分['push','shift'].forEach(item=>{ proto[item] = function(){ update(); oldProto[item].apply(this,arguments); }})function observer(value){ // proxy reflect if(Array.isArray(value)){ // AOP return value.__proto__ = proto; // 重写 这个数组里的push shift unshfit reverse sort splice pop } if(typeof value !== 'object'){ return value; } for(let key in value){ defineReactive(value,key,value[key]); }}function defineReactive(obj,key,value){ observer(value); // 如果是对象 继续增加getter和setter Object.defineProperty(obj,key,{ get(){ return value; }, set(newValue){ if(newValue !== value){ observer(newValue); value = newValue; update(); } } })}observer(obj); // AOP// obj.name = {n:200}; // 数据变了 需要更新视图 深度监控// obj.name.n = 100;obj.push(123);obj.push(456);console.log(obj);
复制代码
参考前端手写面试题详细解答
实现一个链表结构
链表结构
看图理解 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
复制代码
实现 Object.create
Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__
// 模拟 Object.create
function create(proto) { function F() {} F.prototype = proto;
return new F();}
复制代码
实现防抖函数(debounce)
防抖函数原理:在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时。
那么与节流函数的区别直接看这个动画实现即可。
手写简化版:
// 防抖函数const debounce = (fn, delay) => { let timer = null; return (...args) => { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); };};
复制代码
适用场景:
生存环境请用 lodash.debounce
对象数组如何去重
根据每个对象的某一个具体属性来进行去重
const responseList = [ { id: 1, a: 1 }, { id: 2, a: 2 }, { id: 3, a: 3 }, { id: 1, a: 4 },];const result = responseList.reduce((acc, cur) => { const ids = acc.map(item => item.id); return ids.includes(cur.id) ? acc : [...acc, cur];}, []);console.log(result); // -> [ { id: 1, a: 1}, {id: 2, a: 2}, {id: 3, a: 3} ]
复制代码
实现字符串的 repeat 方法
输入字符串 s,以及其重复的次数,输出重复的结果,例如输入 abc,2,输出 abcabc。
function repeat(s, n) { return (new Array(n + 1)).join(s);}
复制代码
递归:
function repeat(s, n) { return (n > 0) ? s.concat(repeat(s, --n)) : "";}
复制代码
手写深度比较 isEqual
思路:深度比较两个对象,就是要深度比较对象的每一个元素。=> 递归
function isEqual(obj1, obj2){ //其中一个为值类型或null if(!isObject(obj1) || !isObject(obj2)){ return obj1 === obj2; }
//判断是否两个参数是同一个变量 if(obj1 === obj2){ return true; }
//判断keys数是否相等 const obj1Keys = Object.keys(obj1); const obj2Keys = Object.keys(obj2); if(obj1Keys.length !== obj2Keys.length){ return false; }
//深度比较每一个key for(let key in obj1){ if(!isEqual(obj1[key], obj2[key])){ return false; } }
return true;}
复制代码
实现一个 sleep 函数,比如 sleep(1000) 意味着等待 1000 毫秒
// 使用 promise来实现 sleepconst sleep = (time) => { return new Promise(resolve => setTimeout(resolve, time))}
sleep(1000).then(() => { // 这里写你的骚操作})
复制代码
实现 Promise
var PromisePolyfill = (function () { // 和reject不同的是resolve需要尝试展开thenable对象 function tryToResolve (value) { if (this === value) { // 主要是防止下面这种情况 // let y = new Promise(res => setTimeout(res(y))) throw TypeError('Chaining cycle detected for promise!') }
// 根据规范2.32以及2.33 对对象或者函数尝试展开 // 保证S6之前的 polyfill 也能和ES6的原生promise混用 if (value !== null && (typeof value === 'object' || typeof value === 'function')) { try { // 这里记录这次then的值同时要被try包裹 // 主要原因是 then 可能是一个getter, 也也就是说 // 1. value.then可能报错 // 2. value.then可能产生副作用(例如多次执行可能结果不同) var then = value.then
// 另一方面, 由于无法保证 then 确实会像预期的那样只调用一个onFullfilled / onRejected // 所以增加了一个flag来防止resolveOrReject被多次调用 var thenAlreadyCalledOrThrow = false if (typeof then === 'function') { // 是thenable 那么尝试展开 // 并且在该thenable状态改变之前this对象的状态不变 then.bind(value)( // onFullfilled function (value2) { if (thenAlreadyCalledOrThrow) return thenAlreadyCalledOrThrow = true tryToResolve.bind(this, value2)() }.bind(this),
// onRejected function (reason2) { if (thenAlreadyCalledOrThrow) return thenAlreadyCalledOrThrow = true resolveOrReject.bind(this, 'rejected', reason2)() }.bind(this) ) } else { // 拥有then 但是then不是一个函数 所以也不是thenable resolveOrReject.bind(this, 'resolved', value)() } } catch (e) { if (thenAlreadyCalledOrThrow) return thenAlreadyCalledOrThrow = true resolveOrReject.bind(this, 'rejected', e)() } } else { // 基本类型 直接返回 resolveOrReject.bind(this, 'resolved', value)() } }
function resolveOrReject (status, data) { if (this.status !== 'pending') return this.status = status this.data = data if (status === 'resolved') { for (var i = 0; i < this.resolveList.length; ++i) { this.resolveList[i]() } } else { for (i = 0; i < this.rejectList.length; ++i) { this.rejectList[i]() } } }
function Promise (executor) { if (!(this instanceof Promise)) { throw Error('Promise can not be called without new !') }
if (typeof executor !== 'function') { // 非标准 但与Chrome谷歌保持一致 throw TypeError('Promise resolver ' + executor + ' is not a function') }
this.status = 'pending' this.resolveList = [] this.rejectList = []
try { executor(tryToResolve.bind(this), resolveOrReject.bind(this, 'rejected')) } catch (e) { resolveOrReject.bind(this, 'rejected', e)() } }
Promise.prototype.then = function (onFullfilled, onRejected) { // 返回值穿透以及错误穿透, 注意错误穿透用的是throw而不是return,否则的话 // 这个then返回的promise状态将变成resolved即接下来的then中的onFullfilled // 会被调用, 然而我们想要调用的是onRejected if (typeof onFullfilled !== 'function') { onFullfilled = function (data) { return data } } if (typeof onRejected !== 'function') { onRejected = function (reason) { throw reason } }
var executor = function (resolve, reject) { setTimeout(function () { try { // 拿到对应的handle函数处理this.data // 并以此为依据解析这个新的Promise var value = this.status === 'resolved' ? onFullfilled(this.data) : onRejected(this.data) resolve(value) } catch (e) { reject(e) } }.bind(this)) }
// then 接受两个函数返回一个新的Promise // then 自身的执行永远异步与onFullfilled/onRejected的执行 if (this.status !== 'pending') { return new Promise(executor.bind(this)) } else { // pending return new Promise(function (resolve, reject) { this.resolveList.push(executor.bind(this, resolve, reject)) this.rejectList.push(executor.bind(this, resolve, reject)) }.bind(this)) } }
// for prmise A+ test Promise.deferred = Promise.defer = function () { var dfd = {} dfd.promise = new Promise(function (resolve, reject) { dfd.resolve = resolve dfd.reject = reject }) return dfd }
// for prmise A+ test if (typeof module !== 'undefined') { module.exports = Promise }
return Promise})()
PromisePolyfill.all = function (promises) { return new Promise((resolve, reject) => { const result = [] let cnt = 0 for (let i = 0; i < promises.length; ++i) { promises[i].then(value => { cnt++ result[i] = value if (cnt === promises.length) resolve(result) }, reject) } })}
PromisePolyfill.race = function (promises) { return new Promise((resolve, reject) => { for (let i = 0; i < promises.length; ++i) { promises[i].then(resolve, reject) } })}
复制代码
实现 some 方法
Array.prototype.mySome=function(callback, context = window){ var len = this.length, flag=false, i = 0;
for(;i < len; i++){ if(callback.apply(context, [this[i], i , this])){ flag=true; break; } } return flag; }
// var flag=arr.mySome((v,index,arr)=>v.num>=10,obj) // console.log(flag);
复制代码
实现 bind
实现 bind 要做什么
// mdn的实现if (!Function.prototype.bind) { Function.prototype.bind = function(oThis) { if (typeof this !== 'function') { // closest thing possible to the ECMAScript 5 // internal IsCallable function throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); }
var aArgs = Array.prototype.slice.call(arguments, 1), fToBind = this, fNOP = function() {}, fBound = function() { // this instanceof fBound === true时,说明返回的fBound被当做new的构造函数调用 return fToBind.apply(this instanceof fBound ? this : oThis, // 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的 aArgs.concat(Array.prototype.slice.call(arguments))); };
// 维护原型关系 if (this.prototype) { // Function.prototype doesn't have a prototype property fNOP.prototype = this.prototype; } // 下行的代码使fBound.prototype是fNOP的实例,因此 // 返回的fBound若作为new的构造函数,new生成的新对象作为this传入fBound,新对象的__proto__就是fNOP的实例 fBound.prototype = new fNOP();
return fBound; };}
复制代码
实现一个迭代器生成函数
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 中的实现有更深的理解。
实现类的继承
类的继承在几年前是重点内容,有 n 种继承方式各有优劣,es6 普及后越来越不重要,那么多种写法有点『回字有四样写法』的意思,如果还想深入理解的去看红宝书即可,我们目前只实现一种最理想的继承方式。
function Parent(name) { this.parent = name}Parent.prototype.say = function() { console.log(`${this.parent}: 你打篮球的样子像kunkun`)}function Child(name, parent) { // 将父类的构造函数绑定在子类上 Parent.call(this, parent) this.child = name}
/** 1. 这一步不用Child.prototype =Parent.prototype的原因是怕共享内存,修改父类原型对象就会影响子类 2. 不用Child.prototype = new Parent()的原因是会调用2次父类的构造方法(另一次是call),会存在一份多余的父类实例属性3. Object.create是创建了父类原型的副本,与父类原型完全隔离*/Child.prototype = Object.create(Parent.prototype);Child.prototype.say = function() { console.log(`${this.parent}好,我是练习时长两年半的${this.child}`);}
// 注意记得把子类的构造指向子类本身Child.prototype.constructor = Child;
var parent = new Parent('father');parent.say() // father: 你打篮球的样子像kunkun
var child = new Child('cxk', 'father');child.say() // father好,我是练习时长两年半的cxk
复制代码
instanceof
instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。
const myInstanceof = (left, right) => { // 基本数据类型都返回false if (typeof left !== 'object' || left === null) return false; let proto = Object.getPrototypeOf(left); while (true) { if (proto === null) return false; if (proto === right.prototype) return true; proto = Object.getPrototypeOf(proto); }}
复制代码
实现双向数据绑定
let obj = {}let input = document.getElementById('input')let span = document.getElementById('span')// 数据劫持Object.defineProperty(obj, 'text', { configurable: true, enumerable: true, get() { console.log('获取数据了') }, set(newVal) { console.log('数据更新了') input.value = newVal span.innerHTML = newVal }})// 输入监听input.addEventListener('keyup', function(e) { obj.text = e.target.value})
复制代码
实现 Array.of 方法
Array.of()方法用于将一组值,转换为数组
Array.of(3, 11, 8) // [3,11,8]Array.of(3) // [3]Array.of(3).length // 1
复制代码
实现
function ArrayOf(){ return [].slice.call(arguments);}
复制代码
验证是否是身份证
function isCardNo(number) { var regx = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/; return regx.test(number);}
复制代码
评论