实现深拷贝
function deepCopy(src){
if (!src | !(src instanceof Object) | (typeof src === "function")) {
return src || undefined
}
var constructor = src.constructor
var dst = new constructor()
for (var key in src) {
if (src.hasOwnProperty(key)) {
dst[key] = deepCopy(src[key])
}
}
return dst
}
实现一个ajax
ajax实现局部刷新的原理是通过XmlHttpRequest对象来向服务器发送异步请求,通过js操作相应的DOM来更新页面
ajax实现过程:
- 创建XmlHttpRequest对象
- 初始化参数
- 发送信息
- 接收信息
function getData(url) {
return new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) { // 4表示响应信息已经全部接收
return;
}
if (this.status === 200) { // status 从服务器返回的状态码
resolve(this.reposeText); // reposeText 从服务器返回数据的字符串格式
} else {
reject(new Error(this.statusText));// statusText 伴随状态码返回的信息,如status=200时,statusText='OK'
}
};
const xhr = new XMLHttpRequest();// 创建XMLHttpRequest对象
xhr.open("GET", url);// 初始化http的请求参数,但是不发送请求
xhr.onreadystatechange = handler;// 状态改变触发的回调函数 接收请求
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");// 给一个打开但是未发送的请求设置参数
xhr.send();// 发送http请求
});
};
数组去重
set去重:
缺点:对象无法去重
[...new Set(arr)]
indexOf去重:
缺点:NaN和对象不能去重
Array.prototype.unique = function() {
var result = [];
this.forEach((val) => {
if(result.indexOf(val) < 0) {
result.push(val)
}
})
return result
}
// [1,{},{},NaN,NaN,null,null,undefined,undefined,'ss','ss'].unique();
includes去重:
缺点:对象不能去重
Array.prototype.unique = function () {
let resultArr = [];
this.forEach((item) => {
if (!resultArr.includes(item)) {
resultArr.push(item);
}
})
return resultArr;
}
splice去重:
缺点:NaN和对象不能去重,null直接消失了
function unique(arr){
for(var i=0; i<arr.length; i++){
for(var j=i+1; j<arr.length; j++){
if(arr[i]==arr[j]){ //第一个等同于第二个,splice方法删除第二个
arr.splice(j,1);
j--;
}
}
}
return arr;
}
hasOwnProperty去重:
缺点:无,全部去重了
Array.prototype.unique1 = function () {
let obj = {};
return this.filter((item) => obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true));
}
filter去重:
function unique(arr) {
return arr.filter(function(item, index, arr) {
//当前元素,在原始数组中的第一个索引===当前索引值,否则返回当前元素
return arr.indexOf(item) === index;
});
}
map去重:
Array.prototype.unique = function () {
let map = new Map();
let resultArr = [];
this.forEach((item) => {
if (!map.has(item)) {
resultArr.push(item);
map.set(item,true)
}
})
return resultArr;
}
reduce+includes去重:
Array.prototype.unique3 = function () {
return this.reduce((prev, cur) => prev.includes(cur) ? prev : [...prev,cur], []);
}
扁平化数组
方法一:Array.prototype.flat()
Array.prototype.flat()特性总结:Array.prototype.flat()用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。- 不传参数时,默认“拉平”一层,可以传入一个整数,表示想要“拉平”的层数。
- 传入
<=0的整数将返回原数组,不“拉平” Infinity关键字作为参数时,无论多少层嵌套,都会转为一维数组- 如果原数组有空位,
Array.prototype.flat()会跳过空位。
let arr = [[1, 2, 2],[3, 4, 5, 5],[6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10];
console.log(arr.flat(Infinity));
/* [1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10] */
方法二:转换为字符串,再把字符串对象用“,”转换成数组
-
思路:可以先把多维数组先转换为字符串,再基于","分隔符将字符串对象分割成字符串数组
-
toString()扁平化数组:let arr = [[1, 2, 2],[3, 4, 5, 5],[6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10]; arr = arr.toString(); // "1,2,2,3,4,5,5,6,7,8,9,11,12,12,13,14,10" arr = arr.toString().split(','); // ["1", "2", "2", "3", "4", "5", "5", "6", "7", "8", "9", "11", "12", "12", "13", "14", "10"] arr = arr.toString().split(',').map(item => parseFloat(item)); // [1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10] -
JSON.stringify()扁平化数组:let arr = [[1, 2, 2],[3, 4, 5, 5],[6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10]; arr = JSON.stringify(arr); // "[[1,2,2],[3,4,5,5],[6,7,8,9,[11,12,[12,13,[14]]]],10]" arr = JSON.stringify(arr).replace(/(\[|\])/g, ''); // "1,2,2,3,4,5,5,6,7,8,9,11,12,12,13,14,10" arr = JSON.stringify(arr).replace(/(\[|\])/g, '').split(',').map(item=>parseFloat(item)); // [1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10]
方法三:循环验证是否为数组
- 基于数组的
some方法,只要数组里面有一项元素是数组就继续循环,扁平数组 - 核心:
[].concat(...arr)
let arr = [[1, 2, 2],[3, 4, 5, 5],[6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10];
while(arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
console.log(arr); // [1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10]
方法四:forEach+isArray+push+recursivity
- 相当于:自己实现一个
flat扁平化 - 实现思路:
- 循环数组里的每一个元素
- 判断该元素是否为数组
- 是数组的话,继续循环遍历这个元素——数组
- 不是数组的话,把元素添加到新的数组中
- 实现流程:
- 创建一个空数组,用来保存遍历到的非数组元素
- 创建一个循环遍历数组的函数,cycleArray
- 取得数组中的每一项,验证
Array.isArray()- 数组的话,继续循环
- 非数组的话,添加到新数组中
- 返回新数组对象
let arr = [[1, 2, 2],[3, 4, 5, 5],[6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10];
// 方式一:
// forEach 遍历数组会自动跳过空元素
const eachFlat = (arr = [], depth = 1) => {
const result = []; // 缓存递归结果
// 开始递归
(function flat(arr, depth) {
// forEach 会自动去除数组空位
arr.forEach((item) => {
// 控制递归深度
if (Array.isArray(item) && depth > 0) {
// 递归数组
flat(item, depth - 1)
} else {
// 缓存元素
result.push(item)
}
})
})(arr, depth)
// 返回递归结果
return result;
}
console.log(eachFlat(arr)); // [1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10]
// 方式二:
// for of 循环不能去除数组空位,需要手动去除
const forFlat = (arr = [], depth = 1) => {
const result = [];
(function flat(arr, depth) {
for (let item of arr) {
if (Array.isArray(item) && depth > 0) {
flat(item, depth - 1)
} else {
// 去除空元素,添加非undefined元素
item !== void 0 && result.push(item);
}
}
})(arr, depth)
return result;
}
console.log(forFlat(arr)); // [1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10]
方法五:reduce+ concat+ isArray+ recursivity
- 相当于:自己实现一个
flat扁平化 - 思路与方法四相同
// 只能展开一层(可忽略不看):
let arr = [12, 23, [34, 56, [78, 90, 100, [110, 120, 130, 140]]]];
const myFlat = arr => {
return arr.reduce((pre, cur) => {
return pre.concat(cur);
}, []);
};
console.log(myFlat(arr));// [ 12, 23, 34, 56, [ 78, 90, 100, [ 110, 120, 130, 140 ] ] ]
// 不可控制深度的直接全员扁平化(也就那样):
const myFlat = arr => {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? myFlat(cur) : cur);
}, []);
};
console.log(myFlat(arr));// [12, 23, 34, 56, 78, 90, 100, 110, 120, 130, 140]
// 可控制展开深度的扁平化(最优):
// 使用 reduce、concat 和递归展开无限多层嵌套的数组
function flatDeep(arr, d = 1) {
return d > 0 ? arr.reduce((acc, val) => acc.concat(Array.isArray(val) ? flatDeep(val, d - 1) : val), [])
: arr.slice();
};
flatDeep(arr, Infinity);// [12, 23, 34, 56, 78, 90, 100, 110, 120, 130, 140]
方法六:使用栈的思想实现flat函数
- 栈思想: 后进先出的数据结构
- 实现思路:
- 不断获取并删除栈中最后一个元素A,判断A是否为数组元素,直到栈内元素为空,全部添加到
newArr- 是数组,则
push到栈中,继续循环栈内元素,直到栈为空 - 不是数组,则
unshift添加到newArr中
- 是数组,则
- 不断获取并删除栈中最后一个元素A,判断A是否为数组元素,直到栈内元素为空,全部添加到
// 无递归数组扁平化,使用堆栈
// 注意:深度的控制比较低效,因为需要检查每一个值的深度
// 也可能在 shift / unshift 上进行 w/o 反转,但是末端的数组 OPs 更快
function flatten(input) {
const stack = [...input]; // 将数组元素拷贝至栈,直接赋值会改变原数组
const res = [];
//如果栈不为空,则循环遍历
while (stack.length) {
const next = stack.pop(); // 删除数组最后一个元素,并获取它
if (Array.isArray(next)) {
stack.push(...next); // 如果是数组再次入栈,并且展开了一层
} else {
res.unshift(next); // 如果不是数组就将其取出来放入结果数组中
}
}
return res;
}
let arr = [12, 23, [34, 56, [78, 90, 100, [110, 120, 130, 140]]]];
console.log(flatten(arr));
// [12, 23, 34, 56, 78, 90, 100, 110, 120, 130, 140]
方法七:Use Generator function
function* flatten(array) {
for (const item of array) {
if (Array.isArray(item)) {
yield* flatten(item);
} else {
yield item;
}
}
}
let arr = [1, 2, [3, 4, [5, 6]]];
const flattened = [...flatten(arr)]; // [1, 2, 3, 4, 5, 6]
休眠函数
function sleep (time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
// 用法
sleep(500).then(() => {
// 这里写sleep之后需要去做的事情
})
// 用法
(async function() {
console.log('Do some thing, ' + new Date());
await sleep(500);
console.log('Do other things, ' + new Date());
})();
斐波那契数列
普通递归:
会出现浏览器假死现象,毕竟递归需要堆栈,数字过大内存不够。
function fibonacci(n) {
if (n == 1 || n == 2) {
return 1
};
return fibonacci(n - 2) + fibonacci(n - 1);
}
优化递归:
把前两位数字做成参数避免重复计算
function fibonacci(n) {
function fib(n, v1, v2) {
if (n == 1)
return v1;
if (n == 2)
return v2;
else
return fib(n - 1, v2, v1 + v2)
}
return fib(n, 1, 1)
}
优化递归:
利用闭包特性把运算结果存储在数组里,避免重复计算
var fibonacci = function () {
let memo = [0, 1];
return function fib (n) {
if (memo[n] == undefined) {
memo[n] = fib(n - 2) + fib(n - 1)
}
return memo[n]
}
}()
for循环:
function fibonacci(n) {
let n1 = 1,
n2 = 1,
sum = 1
for(let i = 3; i <= n; i += 1) {
sum = n1 + n2
n1 = n2
n2 = sum
}
return sum
}
for循环+解构赋值:
function fibonacci(n) {
let n1 = 1; n2 = 1;
for (let i = 2; i < n; i++) {
[n1, n2] = [n2, n1 + n2]
}
return n2
}
发布订阅模式
快排
function quickSort(arr) {
if(arr.length <= 1) {
return arr
}
let middle = arr.shift() || 0
let left = [], right = []
for(let i=0; i < arr.length; i++) {
if (arr[i] < middle) {
left.push(arr[i])
}
if (arr[i] > middle) {
right.push(arr[i])
}
}
return quickSort(left).concat(middle, quickSort(right))
}
冒泡
function bubbleSort(arr: Array<number>) {
for(let i=0, len=arr.length; i<len-1; i++) {
for(let j=0; j < len-i-1; j++) {
if(arr[j] > arr[j+1]) {
[arr[j], arr[j+1]] = [arr[j+1], arr[j]]
}
}
}
return arr
}
手动控制并发请求
// ……
debounce防抖
防抖:
在事件被触发后不马上执行回调,n秒后再执行回调,如果在这n秒内又被触发,则重新计时
如果再次执行,就清空之前的定时器,重新加定时器
强调执行最后一次
function debounce(fn, delay) {
let timer = null;
return function () {
let args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn(...args);
}, delay);
};
}
throttle节流
节流:强调一段固定的时间内只触发一次回调函数
定时器版本:
function throttle(fn,delay) {
let timer;
return function () {
let args = arguments;
if (timer) {
return;
}
setTimeout(() => {
fn(...args);
timer = null; // 只有触发了函数,timer为null,下一次才能再次执行这个定时器
},delay)
}
}
时间戳版本:
function throttle(fn, delay) {
// if (typeof fn !== 'function') {
// throw new TypeError('need a function!')
// }
let previous = 0;
return function () {
let now = Date.now();
let args = arguments;
if (now - previous > delay) {
fn(...args);
previous = now;
}
}
}
call
详细原理可以看this指向与call,apply,bind
xFn.call(xObj,x1,x2,……)
上面调用相当于给xObj添加一个xFn方法并把参数x1,x2……传进去执行,以下实现也是按照此逻辑
Function.prototype.myCall = function(context) {
// context就是call的第一个参数 xObj
// 如果没有传或传的值为空对象 context指向window
context = context || window;
context.fn = this //给context添加一个方法 指向this this就是我们调用call的那个函数xFn
// 处理参数 将content后面的参数取出来
let arg = [...arguments].slice(1) //[...xxx]把类数组变成数组,arguments为啥不是数组自行搜索 slice返回一个新数组
let result = context.fn(...arg) //执行fn
delete context.fn //删除方法
return result;
}
apply
与call的区别是 参数是数组或类数组的对象
Function.prototype.myApply = function (context) {
let context = context || window
context.fn = this
let result;
// 需要判断是否存储第二个参数
// 如果存在,就将第二个参数展开
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
bind
与call和apply的区别是 bind返回的是新函数 不会立即执行
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
let _this = this
let args = [...arguments].slice(1)
// 返回一个函数
return function() {
let context = context || window;
//同样因为支持柯里化形式传参我们需要再次获取存储参数
let newArgs = args.concat([...arguments]);
context.fn = _this;
let result;
if (newArgs.length) {
result = context.fn(...newArgs)
} else {
result = context.fn()
}
delete context.fn
return result
}
}
instanceof
a instanceof Object
判断Object的prototype是否在a的原型链上。
原型链图解可参考:原型相关
function myInstanceof(target, origin) {
const proto = target.__proto__;
if (proto) {
if (origin.prototype === proto) {
return true;
} else {
return myInstanceof(proto, origin);//继续沿着原型链网上找
}
} else { // Object的_proto_指向null(原型链的尽头)
return false;
}
}
实现柯里化函数
柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c)转换为可调用的 f(a)(b)(c)。柯里化不会调用函数。它只是对函数进行转换。
具体原理可见:函数柯里化
function myCurry(func) {
// func 是要转换的函数
return function curried(...args) {
// func.length是原函数的形参数量
if (args.length >= func.length) { // 实际传进来的参数数量>=原函数形参数量,才直接执行
return func.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2)); // 传的参数不够时,就连着之前传的参数一起继续柯里化
}
}
};
}
或者:
function myCurry(fn, ...args) {
if (args.length >= fn.length) {
return fn(...args);
} else {
return (...args2) => currying(fn, ...args, ...args2);
}
}
扩展题:
实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6
add(1, 2, 3)(4) = 10
add(1)(2)(3)(4)(5) = 15
function add() {
var _args = [...arguments]
return function() {
if (arguments.length === 0) {
return _args.reduce(function (a, b) {
return a + b
})
}
[].push.apply(_args, [...arguments])
return arguments.callee
}
}
add(1, 2)(1)(2)(5)()
compose
compose函数可以将需要嵌套执行的函数平铺,嵌套执行就是一个函数的返回值将作为另一个函数的参数。右边的方法最开始执行,然后往左边返回(从右往左)
让compose(multiply, add)(10)等价于:multiply(add(10));
const myCompose = (...args) => x => args.reduceRight((res, cb) => cb(res), x);
pipe
跟compose函数的作用是一样的,也是将参数平铺,只不过他的顺序是从左往右
让pipe(add, multiply)(10)等价于:multiply(add(10));
const myPipe = (...args) => x => args.reduce((res, cb) => cb(res), x)
实现new
new干了什么:
- 帮我们创建一个空对象;
- 将新对象的原型(prototype)指向构造函数的原型(prototype);
- 执行构造函数,把构造函数的属性添加到新对象;
- 返回创建的新对象;
function myNew(func, ...rest) {
// 创建空对象,并将新对象的__proto__属性指向构造函数的prototype,func是构造函数
const obj = Object.create(func.prototype)
// 执行构造函数,改变构造函数的this指针,指向新创建的对象(新对象也就有了构造函数的所有属性)
func.apply(obj, rest)
return obj;
}
或者:
function myNew () {
let obj = new Object()
let constructor = [].shift.apply(arguments)
obj.__proto__ = constructor.prototype
let result = constructor.apply(obj, arguments)
return typeof result === "object" ? result : obj
}
promise
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'
function MyPromise(fn) {
const that = this
that.state = PENDING //一开始 Promise 的状态应该是 pending
that.value = null //保存 resolve 或者 reject 中传入的值
//resolvedCallbacks 和 rejectedCallbacks 用于保存 then 中的回调,
//因为当执行完 Promise 时状态可能还是等待中,
//这时候应该把 then 中的回调保存起来用于状态改变时使用
that.resolvedCallbacks = []
that.rejectedCallbacks = []
function resolve(value) {
console.log('999')
if (that.state === PENDING) { //判断当前状态是否为等待中,因为规范规定只有等待态才可以改变状态
that.state = RESOLVED //将当前状态更改为对应状态
that.value = value // 并且将传入的值赋值给 value
that.resolvedCallbacks.map(cb => cb(that.value)) // 遍历回调数组并执行
}
}
function reject(value) {
if (that.state === PENDING) {
that.state = REJECTED
that.value = value
that.rejectedCallbacks.map(cb => cb(that.value))
}
}
try {
console.log('000');
fn(resolve, reject)
} catch (e) {
reject(e)
}
}
MyPromise.prototype.then = function (onFulfilled, onRejected) { // 较为复杂的 then 函数
const that = this
// 判断两个参数是否为函数类型,因为这两个参数是可选参数
// 当参数不是函数类型时,需要创建一个函数赋值给对应的参数
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
onRejected =
typeof onRejected === 'function'
? onRejected
: r => {
throw r
}
// 状态是等待态的话,就往回调函数中 push 函数
if (that.state === PENDING) {
console.log('222')
that.resolvedCallbacks.push(onFulfilled)
that.rejectedCallbacks.push(onRejected)
}
// 当状态不是等待态时,就去执行相对应的函数
if (that.state === RESOLVED) {
console.log('333')
onFulfilled(that.value)
}
if (that.state === REJECTED) {
console.log('444')
onRejected(that.value)
}
}
// 调用 进入等待态的逻辑
new MyPromise((resolve, reject) => {
setTimeout(() => {//pending状态
resolve(1)
}, 0)
// resolve(1)
}).then(value => {
console.log(value)
})