手写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 的问题。