手写总结
手写JS
JS 如何实现类?
方法一:使用原型
function Dog(name){ this.name = name this.legsNumber = 4 } Dog.prototype.kind = '狗' Dog.prototype.say = function(){ console.log(`汪汪汪~ 我是${this.name},我有${this.legsNumber}条腿。`) } Dog.prototype.run = function(){ console.log(`${this.legsNumber}条腿跑起来。`) } const d1 = new Dog('啸天') // Dog 函数就是一个类 d1.say()
请试着实现一个 Chicken 类,没 name 会 say 会 fly。
方法二:使用 class
Js的类语法:
- 请使用关键字class创建一个类;
- 请添加一个名为constructor()的方法;
- this指的是对象的所有者,Dog上面的例子创建了一个名为 "Dog" 的类。
class Dog { // 等价于在 constructor 里写 constructor(name) { this.kind = '狗' this.name = name this.legsNumber = 4 // 思考:kind 放在哪,放在哪都无法实现上面的一样的效果 } say(){ console.log(`汪汪汪~ 我是${this.name},我有${this.legsNumber}条腿。`) } run(){ console.log(`${this.legsNumber}条腿跑起来。`) } } const d1 = new Dog('啸天') d1.say()
请试着实现一个 Chicken 类,没 name 会 say 会 fly。
JS 如何实现继承?
方法一:使用原型链
将父类的实例对象 赋值给子类的原型对象
缺点:
- 所有子类实例,共享可修改的原型属性。
- 子类
new
时不能向 父类 构造函数 传参。 - 要手动修改
prototype.constructor
为子类。
function Animal(legsNumber){ this.legsNumber = legsNumber } Animal.prototype.kind = '动物' function Dog(name){ this.name = name Animal.call(this, 4) // 关键代码1 } Dog.prototype.__proto__ = Animal.prototype // 关键代码2,但这句代码被禁用了,怎么办 Dog.prototype.kind = '狗' Dog.prototype.say = function(){ console.log(`汪汪汪~ 我是${this.name},我有${this.legsNumber}条腿。`) } const d1 = new Dog('啸天') // Dog 函数就是一个类 console.dir(d1)
如果面试官问被 ban 的代码如何替换,就说下面三句:
var f = function(){ } f.prototype = Animal.prototype Dog.prototype = new f()
方法二:使用 class
实现:子类 调用 父类构造函数。
- 属性定义在
this
,避免共享。 - 可以向 父类 构造函数 传参。
缺点:
- 父类方法,不能通过
prototype
访问。 - 方法必须在父类构造函数中定义,且每次创建实例都会创建一遍方法。
class Animal{ constructor(legsNumber){ this.legsNumber = legsNumber } run(){} } class Dog extends Animal{ constructor(name) { super(4) this.name = name } say(){ console.log(`汪汪汪~ 我是${this.name},我有${this.legsNumber}条腿。`) } }
手写节流 throttle、防抖 debounce
记忆题,写博客,甩链接。
节流:只执行第一次点击,在第一次点击完成前,后面的点击都会无效
// 节流就是「技能冷却中」 const throttle = (fn, time) => { let cooling = false return (...args) => {// 如果没有开启定时器,开启一个 if(cooling) return fn.call(undefined, ...args) cooling = true setTimeout(()=>{ cooling = false }, time) } }
function throttle(fn, wait) { let timer = null; return function(...args) { // 如果没有开启定时器,开启一个 if (!timer) { timer = setTimeout(()=>{ fn.apply(this,args) // 执行完fn后将定时器timer清空 timer = null; }, wait); } } }
// 还有一个版本是在冷却结束时调用 fn // 简洁版,删掉冷却中变量,直接使用 timer 代替 const throttle = (f, time) => { let timer = null return (...args) => { if(timer) {return} f.call(undefined, ...args) timer = setTimeout(()=>{ timer = null }, time) } }
使用方法:
const f = throttle(()=>{console.log('hi')}, 3000) f() // 打印 hi f() // 技能冷却中
防抖:防抖是在多次点击中,只执行最后一次,前面的点击都会被取消
// 防抖就是「回城被打断」 const debounce = (fn, time) => { let timer = null return (...args)=>{ if(timer !== null) { clearTimeout(timer) } timer = setTimeout(()=>{ fn.call(undefined, ...args) timer = null }, time) } }
function debounce(fun,time){ let timer return function(...args){ clearTimeout(timer) // 打断回城 // 重新回城 timer=setTimeout(()=>{ fun.apply(this,args) // 回城后调用 fn },time) } }
手写发布订阅
记忆题,写博客,甩链接
- 创建一个
EventHub
类 - 在该类上创建一个事件中心(Map)
on
方法用来把函数 fn 都加到事件中心中(订阅者注册事件到调度中心)emit
方法取到 arguments 里第一个当做 event,根据 event 值去执行对应事件中心中的函数(发布者发布事件到调度中心,调度中心处理代码)off
方法可以根据 event 值取消订阅(取消订阅)
const eventHub = { map: { // click: [f1 , f2] }, on: (name, fn)=>{ eventHub.map[name] = eventHub.map[name] || [] eventHub.map[name].push(fn) }, emit: (name, data)=>{ const q = eventHub.map[name] if(!q) return q.map(f => f.call(null, data)) return undefined }, off: (name, fn)=>{ const q = eventHub.map[name] if(!q){ return } const index = q.indexOf(fn) if(index < 0) { return } q.splice(index, 1) } } eventHub.on('click', console.log) eventHub.on('click', console.error) setTimeout(()=>{ eventHub.emit('click', 'frank') },3000) 复制代码
也可以用 class 实现。
class EventHub { map = {} on(name, fn) { this.map[name] = this.map[name] || [] this.map[name].push(fn) } emit(name, data) { const fnList = this.map[name] || [] fnList.forEach(fn => fn.call(undefined, data)) } off(name, fn) { const fnList = this.map[name] || [] const index = fnList.indexOf(fn) if(index < 0) return fnList.splice(index, 1) } } // 使用 const e = new EventHub() e.on('click', (name)=>{ console.log('hi '+ name) }) e.on('click', (name)=>{ console.log('hello '+ name) }) setTimeout(()=>{ e.emit('click', 'frank') },3000)
手写 AJAX
AJAX(Asynchronous JavaScript and XML),指的是通过 JavaScript 的异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。
1.创建XMLHttpRequest
对象,创建一个异步调用对象.
2.创建一个新的HTTP
请求,并指定该HTTP
请求的方法、URL
及验证信息.
3.设置响应HTTP
请求状态变化的函数.
4.发送HTTP
请求
记忆题,写博客吧
const ajax = (method, url, data, success, fail) => { var request = new XMLHttpRequest() request.open(method, url); request.onreadystatechange = function () { if(request.readyState === 4) { if(request.status >= 200 && request.status < 300 || request.status === 304) { success(request) }else{ fail(request) } } }; request.send(); }
快速排序
这里对快排思想不太明白的同学可以看下这个讲解的很清晰的视频:快速排序算法。
function sortArray(nums) { quickSort(0, nums.length - 1, nums); return nums; } function quickSort(start, end, arr) { if (start < end) { const mid = sort(start, end, arr); quickSort(start, mid - 1, arr); quickSort(mid + 1, end, arr); } } function sort(start, end, arr) { const base = arr[start]; let left = start; let right = end; while (left !== right) { while (arr[right] >= base && right > left) { right--; } arr[left] = arr[right]; while (arr[left] <= base && right > left) { left++; } arr[right] = arr[left]; } arr[left] = base; return left; }
instanceof
instanceof
来判断对象的具体类型,其实 instanceof
主要的作用就是判断一个实例是否属于某种类型 这个手写一定要懂原型及原型链。
function myInstanceof(target, origin) { if (typeof target !== "object" || target === null) return false; if (typeof origin !== "function") throw new TypeError("origin must be function"); let proto = Object.getPrototypeOf(target); // 相当于 proto = target.__proto__; while (proto) { if (proto === origin.prototype) return true; proto = Object.getPrototypeOf(proto); } return false; } 复制代码
数组扁平化
重点,不要觉得用不到就不管,这道题就是考察你对 js 语法的熟练程度以及手写代码的基本能力。
function flat(arr, depth = 1) { if (depth > 0) { // 以下代码还可以简化,不过为了可读性,还是.... return arr.reduce((pre, cur) => { return pre.concat(Array.isArray(cur) ? flat(cur, depth - 1) : cur); }, []); } return arr.slice(); }
promise是什么与使用方法?
- 概念:异步编程的一种解决方案,解决了地狱回调的问题
- 使用方法:new Promise((resolve,reject) => {
resolve(); reject();
}) - 里面有多个resovle或者reject只执行第一个。如果第一个是resolve的话后面可以接.then查看成功消息。如果第一个是reject的话,.catch查看错误消息。
手写简化版 Promise
async/await 和 Promise 的关系
async 声明一个函数为异步函数,这个函数返回的是一个 Promise 对象;
await 用于等待一个 async 函数的返回值(注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。
- async/await 是消灭异步回调的终极武器。
- 但和 Promise 并不互斥,反而,两者相辅相成。
- 执行 async 函数,返回的一定是 Promise 对象。
- await 相当于 Promise 的 then。
- tru...catch 可捕获异常,代替了 Promise 的 catch。
class Promise2 { #status = 'pending' constructor(fn){ this.q = [] const resolve = (data)=>{ this.#status = 'fulfilled' const f1f2 = this.q.shift() if(!f1f2 || !f1f2[0]) return const x = f1f2[0].call(undefined, data) if(x instanceof Promise2) { x.then((data)=>{ resolve(data) }, (reason)=>{ reject(reason) }) }else { resolve(x) } } const reject = (reason)=>{ this.#status = 'rejected' const f1f2 = this.q.shift() if(!f1f2 || !f1f2[1]) return const x = f1f2[1].call(undefined, reason) if(x instanceof Promise2){ x.then((data)=>{ resolve(data) }, (reason)=>{ reject(reason) }) }else{ resolve(x) } } fn.call(undefined, resolve, reject) } then(f1, f2){ this.q.push([f1, f2]) } } const p = new Promise2(function(resolve, reject){ setTimeout(function(){ reject('出错') },3000) }) p.then( (data)=>{console.log(data)}, (r)=>{console.error(r)} )
手写 Promise.all
记忆题,写博客吧。
要点:
- 知道要在 Promise 上写而不是在原型上写
- 知道 all 的参数(Promise 数组)和返回值(新 Promise 对象)
- 知道用数组来记录结果
- 知道只要有一个 reject 就整体 reject
Promise.prototype.myAll Promise.myAll = function(list){ const results = [] let count = 0 return new Promise((resolve,reject) =>{ list.map((item, index)=> { item.then(result=>{ results[index] = result count += 1 if (count >= list.length) { resolve(results)} }, reason => reject(reason) ) }) }) }
进一步提问:是否知道 Promise.allSettled():当您有多个彼此不依赖的异步任务成功完成时,或者您总是想知道每个promise
的结果时,通常使用它。
相比之下,Promise.all()
更适合彼此相互依赖或者在其中任何一个reject
时立即结束。
手写深拷贝
这个题一定要会啊!笔者面试过程中疯狂被问到!
- 浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
- 深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
文章推荐:如何写出一个惊艳面试官的深拷贝?
/** * 深拷贝 * @param {Object} obj 要拷贝的对象 * @param {Map} map 用于存储循环引用对象的地址 */ function deepClone(obj = {}, map = new Map()) { if (typeof obj !== "object") { return obj; } if (map.get(obj)) { return map.get(obj); } let result = {}; // 初始化返回结果 if ( obj instanceof Array || // 加 || 的原因是为了防止 Array 的 prototype 被重写,Array.isArray 也是如此 Object.prototype.toString(obj) === "[object Array]" ) { result = []; } // 防止循环引用 map.set(obj, result); for (const key in obj) { // 保证 key 不是原型属性 if (obj.hasOwnProperty(key)) { // 递归调用 result[key] = deepClone(obj[key], map); } } // 返回结果 return result; }
方法一,用 JSON:
const b = JSON.parse(JSON.stringify(a))
答题要点是指出这个方法有如下缺点:
- 不支持 Date、正则、undefined、函数等数据
- 不支持引用(即环状结构)
- 必须说自己还会方法二
方法二,用递归:
要点:
- 递归
- 判断类型
- 检查环
- 不拷贝原型上的属性
const deepClone = (a, cache) => { if(!cache){ cache = new Map() // 缓存不能全局,最好临时创建并递归传递 } if(a instanceof Object) { // 不考虑跨 iframe if(cache.get(a)) { return cache.get(a) } let result if(a instanceof Function) { if(a.prototype) { // 有 prototype 就是普通函数 result = function(){ return a.apply(this, arguments) } } else { result = (...args) => { return a.call(undefined, ...args) } } } else if(a instanceof Array) { result = [] } else if(a instanceof Date) { result = new Date(a - 0) } else if(a instanceof RegExp) { result = new RegExp(a.source, a.flags) } else { result = {} } cache.set(a, result) for(let key in a) { if(a.hasOwnProperty(key)){ result[key] = deepClone(a[key], cache) } } return result } else { return a } } const a = { number:1, bool:false, str: 'hi', empty1: undefined, empty2: null, array: [ {name: 'frank', age: 18}, {name: 'jacky', age: 19} ], date: new Date(2000,0,1,20,30,0), regex: /.(j|t)sx/i, obj: { name:'frank', age: 18}, f1: (a, b) => a + b, f2: function(a, b) { return a + b } } a.self = a const b = deepClone(a) b.self === b // true b.self = 'hi' a.self !== 'hi' //true
手写数组去重
- 使用计数排序的思路,缺点是只支持字符串
function unique(arr) { arr = arr.sort() let array= [arr[0]]; for (let i = 1; i < arr.length; i++) { if (arr[i] !== arr[i-1]) { array.push(arr[i]); } } return array; } let array=[1,2,3,1,2,'true','true',false,false,undefined,undefined,null,null,NaN,NaN,{},{}]; console.log(unique(array));// 1, 2, 3, NaN, NaN, {}, {}, false, null, "true", undefined
- 使用 Set(面试已经禁止这种了,因为太简单)
function unique (arr) { return Array.from(new Set(arr)) } let array=[1,2,3,1,2,'true','true',false,false,undefined,undefined,null,null,NaN,NaN,{},{}] console.log(unique(array));// 1,2,3,'true',false,undefined,null,NaN,{},{}
- 使用 Map,缺点是兼容性差了一点
var uniq = function(a){ var map = new Map() for(let i=0;i<a.length;i++){ let number = a[i] // 1 ~ 3 if(number === undefined){continue} if(map.has(number)){ continue } map.set(number, true) } return [...map.keys()] }
手写事件委托
<ul id="list"> <li>1</li> <li>2</li> <li>3</li> <li>4</li> </ul>
let ul = document.querySelector('#list'); ul.addEventListener('click', function(e){ let target = e.target; while( target.tagName !== 'LI' ){ if ( target.tagName === 'UL' ){ target = null; break; } target = target.parentNode; } if ( target ){ console.log('你点击了ui里的li') } })
手写 一个 div 拖拽
缕清思路
- 要有一个div,要为相对对位
- 鼠标 'mousedown' 时,标志着 可以移动啦,要记录下当前位置
- 鼠标 ' mousemove' 时,真正的会有位置上的移动
- 鼠标 'mouseup ' 时,停止移动。
index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width"> <title>div 拖拽</title> </head> <body> <div id="xxx"></div> </body> </html>
style.css
*{margin:0; padding: 0;} div{ border: 1px solid black; position: absolute; top: 0; left: 0; width: 50px; height: 50px; }
main.js 整体代码
var flag = false//是否进行拖拽的标志 var position = null xxx.addEventListener('mousedown',function(e){ flag = true //存一下当前位置信息,外层声明一个 position position = [e.clientX, e.clientY] }) document.addEventListener('mousemove', function(e){//注意要监听document,否则鼠标移动快了div会掉下去。 if(flag === false){return ;} const x = e.clientX const y = e.clientY // 位移 = 当前位置 - 上次位置,position[0]为上次位置的横坐标,position[1],为上次位置的纵坐标 const deltaX = x - position[0] const deltaY = y - position[1] // xxx.style.left 的值是带像素的字符串,所以用parseInt()转化为number类型, // 当left为空的时候,parseInt('') = NaN, 所以要用 0 作为保底值 const left = parseInt(xxx.style.left || 0) const top = parseInt(xxx.style.top || 0) xxx.style.left = left + deltaX + 'px' xxx.style.top = top + deltaY + 'px' // 保存一下当前位置 position = [x, y] }) document.addEventListener('mouseup', function(e){//注意要监听document, flag = false })
算法题
大数相加
题目
const add = (a, b) => { ... return sum } console.log(add("11111111101234567","77777777707654321")) console.log(add("911111111101234567","77777777707654321"))
答案
function add(a ,b){ const maxLength = Math.max(a.length, b.length) let overflow = false let sum = '' for(let i = 1; i <= maxLength; i++){ const ai = a[a.length-i] || '0' const bi = b[b.length-i] || '0' let ci = parseInt(ai) + parseInt(bi) + (overflow ? 1 : 0) overflow = ci >= 10 ci = overflow ? ci - 10 : ci sum = ci + sum } sum = overflow ? '1' + sum : sum return sum } console.log(add("11111111101234567","77777777707654321")) console.log(add("911111111101234567","77777777707654321"))
15位加速版:
const add = (a, b) => { const maxLength = Math.max(a.length, b.length) let overflow = false let sum = '' for(let i = 0; i < maxLength; i+=15){ const ai = a.substring(a.length-i -15, a.length-i) || '0' const bi = b.substring(b.length-i -15, b.length-i) || '0' let ci = parseInt(ai) + parseInt(bi) overflow = ci > 999999999999999 // 15 个 9 ci = overflow ? ci - (999999999999999+1) : ci sum = ci + sum } sum = overflow ? '1' + sum : sum return sum } console.log(add("11111111101234567","77777777707654321")) console.log(add("911111111101234567","77777777707654321"))
其他思路:
- 转为数组,然后倒序,遍历
- 使用队列,使用 while 循环
可以自行搜索。
两数之和
题目
const numbers = [2,7,11,15] const target = 9 const twoSum = (numbers, target) => { // ... } console.log(twoSum(numbers, target)) // [0, 1] 或 [1, 0] // 出题者保证 // 1. numbers 中的数字不会重复 // 2. 只会存在一个有效答案
答案
const numbers = [2,7,11,15] const target = 9 const twoSum = (numbers, target) => { const map = {} for(let i = 0; i < numbers.length; i++){ const number = numbers[i] const number2 = target - number if(number2 in map){ const number2Index = map[number2] return [i, number2Index] } else { map[number] = i } } return [] } console.log(twoSum(numbers, target))
上面是给菜鸟看的,所以有多余的中间变量,可以删掉。
无重复最长子串的长度
题目
https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/
const lengthOfLongestSubstring = (str) => { //... } console.log(lengthOfLongestSubstring("abcabcbb")) // 3
答案:滑动窗口法
我称之为「两根手指法」。
var lengthOfLongestSubstring = function(s){ if(s.length <= 1) return s.length let max = 0 let p1 = 0 let p2 = 1 while(p2 < s.length) { let sameIndex = -1 for(let i = p1; i < p2; i++){ if(s[i] === s[p2]){ sameIndex = i break } } let tempMax if( sameIndex >= 0){ tempMax = p2 - p1 p1 = sameIndex + 1 }else{ tempMax = p2 - p1 + 1 } if(tempMax > max){ max = tempMax } p2 += 1 } return max }
使用 map 加速:
var lengthOfLongestSubstring = function(s) { if (s.length <= 1) return s.length let max = 0 let p1 = 0 let p2 = 1 const map = {} map[s[p1]] = 0 while (p2 < s.length) { let hasSame = false if(s[p2] in map){ hasSame = true if(map[s[p2]] >= p1){ p1 = map[s[p2]] + 1 } } map[s[p2]] = p2 let tempMax = p2 - p1 + 1 if(tempMax > max) max = tempMax p2 += 1 } return max };
你会发现,加速失败,可能是 JS 的问题。
改用 new Map() 试试:
var lengthOfLongestSubstring = function(s) { if (s.length <= 1) return s.length let max = 0 let p1 = 0 let p2 = 1 const map = new Map() map.set(s[p1], 0) while (p2 < s.length) { let hasSame = false if(map.has(s[p2])){ hasSame = true if(map.get(s[p2]) >= p1){ p1 = map.get(s[p2]) + 1 } } map.set(s[p2],p2) let tempMax = p2 - p1 + 1 if(tempMax > max) max = tempMax p2 += 1 } return max };
你会发现,加速失败,这应该还是 JS 的问题。