手撕代码

1.Object.create

1
2
3
4
5
6
7
8
9
10
11
//实现Object.create方法
function create(proto) {
function Fn() {};
Fn.prototype = proto;
Fn.prototype.constructor = Fn;
return new Fn();
}
let demo = {
c : '123'
}
let cc = Object.create(demo)

2.instanceof

1
2
3
4
5
6
7
8
9
10
11
function myInstanceof(left, right) {
while (true) {
if (left === null) {
return false;
}
if (left.__proto__ === right.prototype) {
return true;
}
left = left.__proto__;
}
}

3.new

1
2
3
4
5
6
7
8
9
10
function myNew(fn, ...args) {
let obj = Object.create(fn.prototype);
let res = fn.call(obj, ...args);
//不需要考虑fn函数没有返回值的情况,因为函数没有return默认返回undefind,undefind不是对象,没有原型,res instanceof
// Object返回false
if (res instanceof Object) {
return res;
}
return obj;
}

4.实现sleep

某个时间后就去执行某个函数,使用Promise封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function sleep(fn, time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(fn);
}, time);
});
}
async function autoPlay() {
console.log(" 我在sleep 之前");
// 等待多少秒
await sleep(time);
console.log(" 我在sleep 之后");
}
autoPlay()

5.Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

function Promise(excutor) {
let that = this; // 缓存当前promise实例对象
that.status = PENDING; // 初始状态
that.value = undefined; // fulfilled状态时 返回的信息
that.reason = undefined; // rejected状态时 拒绝的原因
that.onFulfilledCallbacks = []; // 存储fulfilled状态对应的onFulfilled函数
that.onRejectedCallbacks = []; // 存储rejected状态对应的onRejected函数

function resolve(value) { // value成功态时接收的终值
if(value instanceof Promise) {
return value.then(resolve, reject);
}
// 实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。
setTimeout(() => {
// 调用resolve 回调对应onFulfilled函数
if (that.status === PENDING) {
// 只能由pending状态 => fulfilled状态 (避免调用多次resolve reject)
that.status = FULFILLED;
that.value = value;
that.onFulfilledCallbacks.forEach(cb => cb(that.value));
}
});
}
function reject(reason) { // reason失败态时接收的拒因
setTimeout(() => {
// 调用reject 回调对应onRejected函数
if (that.status === PENDING) {
// 只能由pending状态 => rejected状态 (避免调用多次resolve reject)
that.status = REJECTED;
that.reason = reason;
that.onRejectedCallbacks.forEach(cb => cb(that.reason));
}
});
}

// 捕获在excutor执行器中抛出的异常
// new Promise((resolve, reject) => {
// throw new Error('error in excutor')
// })
try {
excutor(resolve, reject);
} catch (e) {
reject(e);
}
}

Promise.prototype.then = function(onFulfilled, onRejected) {
const that = this;
let newPromise;
// 处理参数默认值 保证参数后续能够继续执行
onFulfilled =
typeof onFulfilled === "function" ? onFulfilled : value => value;
onRejected =
typeof onRejected === "function" ? onRejected : reason => {
throw reason;
};
if (that.status === FULFILLED) { // 成功态
return newPromise = new Promise((resolve, reject) => {
try{
let x = onFulfilled(that.value);
x instanceof Promise ? x.then(resolve, reject) : resolve(x);
} catch(e) {
reject(e); // 捕获前面onFulfilled中抛出的异常 then(onFulfilled, onRejected);
}
})
}

if (that.status === REJECTED) { // 失败态
return newPromise = new Promise((resolve, reject) => {
try {
let x = onRejected(that.reason);
x instanceof Promise ? x.then(resolve, reject) : resolve(x);
} catch(e) {
reject(e);
}
});
}

if (that.status === PENDING) { // 等待态
// 当异步调用resolve/rejected时 将onFulfilled/onRejected收集暂存到集合中
return newPromise = new Promise((resolve, reject) => {
that.onFulfilledCallbacks.push((value) => {
try {
let x = onFulfilled(value);
x instanceof Promise ? x.then(resolve, reject) : resolve(x);
} catch(e) {
reject(e);
}
});
that.onRejectedCallbacks.push((reason) => {
try {
let x = onRejected(reason);
x instanceof Promise ? x.then(resolve, reject) : resolve(x);
} catch(e) {
reject(e);
}
});
});
}
};

6.Promise.all

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Promise.all=function (promises) {
// 检查是否是迭代对象
if(typeof promises[Symbol.iterator] !== 'function') {
throw(`传入的参数不是一个可迭代对象`)
}
return new Promise(function(resolve, reject) {
if(!Array.isArray(promises)){
throw new TypeError(`argument must be a array`)
}
var resolvedCounter = 0;
var promiseNum = promises.length;
var resolvedResult = [];
for (let i = 0; i < promiseNum; i++) {
Promise.resolve(promises[i]).then(value=>{
resolvedCounter++;
resolvedResult[i] = value;
if (resolvedCounter == promiseNum) {
resolve(resolvedResult);
}
},error=>{
reject(error);
})
}
})
}

7.Promise.race

1
2
3
4
5
6
7
8
9
10
11
Promise.race=function(proms) {
// 检查是否是迭代对象
if(typeof promises[Symbol.iterator] !== 'function') {
throw(`传入的参数不是一个可迭代对象`)
}
return new Promise((resolve, reject) => {
for (const p of proms) {
Promise.resolve(p).then(resolve, reject);
}
});
}

8.Promise.prototype.finally

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 无论成功还是失败都会执行回调
* @param {Function} onSettled
*/
Promise.prototype.finally = function (onSettled) {
return this.then(
(data) => {
onSettled(); // 实现了收不到参数了
return data;
},
(reason) => {
onSettled();
throw reason;
}
);
// finally函数 返回结果应该是无效的
}

/******test finally*******/
// 无论什么结果,都会运行
const pro = new Promise((resolve, reject) => {
resolve(1);
});
const pro2 = pro.finally((d) => {
console.log("finally", d); // 收不到d参数
// 本身不改变状态,但是抛出一个错误,数据就会变成它的错误
// throw 123;
return 123; //不起作用
});
setTimeout(() => {
console.log(pro2);
});

9.Promise.allSettled

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 等待所有的Promise有结果后
* 该方法返回的Promise完成
* 并且按照顺序将所有结果汇总
* @param {Iterable} proms
*/

Promise.allSettled=function(proms) {
const ps = [];
for (const p of proms) {
ps.push(
Promise.resolve(p).then(
(value) => ({
status: FULFILLED,
value,
}),
(reason) => ({
status: REJECTED,
reason,
})
)
);
}
return Promise.all(ps);
}

10.Promise.prototype.catch

1
2
3
4
5
6
7
8
 /**
* 本质就是then,只是少传了一个onFulfilled
* 所以仅处理失败的场景
* @param {*} onRejected
*/
Promise.prototype.catch = function(onRejected) {
return this.then(null, onRejected);
}

11.Promise.resolve

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Promise.resolve=function (value) {
// 如果 value 已经是 Promise 对象,则直接返回该 Promise 对象
if (value instanceof Promise) {
return value;
}
// 如果 value 是 thenable 对象,则包装成 Promise 对象并返回
if (value && typeof value.then === 'function') {
return new Promise(function(resolve, reject) {
value.then(resolve, reject);
});
}
// 将传入的值作为 Promise 的成功值,并返回 Promise 对象
return new Promise(function(resolve) {
resolve(value);
});
}

12.Promise.reject

1
2
3
4
5
6
7
8
9
/**
* 得到一个被拒绝的Promise
* @param {*} reason
*/
Promise.reject=function(reason) {
return new MyPromise((resolve, reject) => {
reject(reason);
});
}

13.防抖和节流

setTimeout函数中的第一个参数使用箭头函数时,apply中可直接传入this,因为箭头函数的this指向定义时外部代码块的this,如果不是箭头函数,则需要在外层用一个变量保留this再传入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function debounce(callback, time) {
let timer = null;
return (...args) => {
if (timer) {
clearTimeout(timer)
timer = null
}
timer = setTimeout(() => {
callback.apply(this, args);
}, time);
}
}

// 第一次执行,中间是节流,最后一次也要执行版本
function throttle(fn, delay) {
let timer = null;
let lastTime = 0;
return function() {
const args = arguments;
const nowTime = Date.now();
if (nowTime - lastTime >= delay) {
fn.apply(this, args);
lastTime = nowTime;
} else {
clearTimeout(timer);
timer = setTimeout(()=>{
fn.apply(this, args);
lastTime = nowTime;
}, delay - (nowTime - lastTime));
}
};
}

14.类型判断函数

1
2
3
function getType(value) {
return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
}

15.call apply bind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Function.prototype.call = function(context, ...args) {
context = (context === undefined || context === null) ? window : context
context.__fn = this
let result = context.__fn(...args)
delete context.__fn
return result
}

Function.prototype.apply = function(context, args) {
context = (context === undefined || context === null) ? window : context
context.__fn = this
let result = context.__fn(...args)
delete context.__fn
return result
}

Function.prototype.bind = function(context, ...args1) {
context = (context === undefined || context === null) ? window : context
let _this = this
return function(...args2) {
context.__fn = _this
let result = context.__fn(...[...args1, ...args2])
delete context.__fn
return result
}
}

16.curry柯里化

1
2
3
4
5
6
7
8
9
10
11
function myCurrying(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function (...args2) {
return curried.apply(this, args.concat(args2));
};
}
}
}

17.AJAX请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", SERVER_URL, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功时
if ((this.status >= 200&&this.status<300)|| this.status === 304) {
handle(this.response);
} else {
console.error(this.statusText);
}
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);

18.Promise封装AJAX请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function getJSON(url) {
// 创建一个 promise 对象
let promise = new Promise(function(resolve, reject) {
let xhr = new XMLHttpRequest();
// 新建一个 http 请求
xhr.open("GET", url, true);
// 设置状态的监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功或失败时,改变 promise 的状态
if ((this.status >= 200&&this.status<300)|| this.status === 304) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
// 设置错误监听函数
xhr.onerror = function() {
reject(new Error(this.statusText));
};
// 设置响应的数据类型
xhr.responseType = "json";
// 设置请求头信息
xhr.setRequestHeader("Accept", "application/json");
// 发送 http 请求
xhr.send(null);
});
return promise;
}

19.实现浅拷贝

浅拷贝是指,一个新的对象对原始对象的属性值进行精确地拷贝,如果拷贝的是基本数据类型,拷贝的就是基本数据类型的值,如果是引用数据类型,拷贝的就是内存地址。如果其中一个对象的引用内存地址发生改变,另一个对象也会发生变化。

(1)Object.assign()

Object.assign()是ES6中对象的拷贝方法,接受的第一个参数是目标对象,其余参数是源对象,用法:Object.assign(target, source_1, ···),该方法可以实现浅拷贝,也可以实现一维对象的深拷贝。

注意:

  • 如果目标对象和源对象有同名属性,或者多个源对象有同名属性,则后面的属性会覆盖前面的属性。
  • 如果该函数只有一个参数,当参数为对象时,直接返回该对象;当参数不是对象时,会先将参数转为对象然后返回。
  • 因为nullundefined 不能转化为对象,所以第一个参数不能为nullundefined,会报错。
1
2
3
4
5
6
let target = {a: 1};
let object2 = {b: 2};
let object3 = {c: 3};
Object.assign(target,object2,object3);
console.log(target); // {a: 1, b: 2, c: 3}
复制代码

(2)扩展运算符

使用扩展运算符可以在构造字面量对象的时候,进行属性的拷贝。语法:let cloneObj = { ...obj };

1
2
3
4
5
6
7
8
9
let obj1 = {a:1,b:{c:1}}
let obj2 = {...obj1};
obj1.a = 2;
console.log(obj1); //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}
obj1.b.c = 2;
console.log(obj1); //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}
复制代码

(3)数组方法实现数组浅拷贝

1)Array.prototype.slice
  • slice()方法是JavaScript数组的一个方法,这个方法可以从已有数组中返回选定的元素:用法:array.slice(start, end),该方法不会改变原始数组。
  • 该方法有两个参数,两个参数都可选,如果两个参数都不写,就可以实现一个数组的浅拷贝。
1
2
3
4
let arr = [1,2,3,4];
console.log(arr.slice()); // [1,2,3,4]
console.log(arr.slice() === arr); //false
复制代码
2)Array.prototype.concat
  • concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
  • 该方法有两个参数,两个参数都可选,如果两个参数都不写,就可以实现一个数组的浅拷贝。
1
2
3
4
let arr = [1,2,3,4];
console.log(arr.concat()); // [1,2,3,4]
console.log(arr.concat() === arr); //false
复制代码

(4)手写实现浅拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 浅拷贝的实现;

function shallowCopy(object) {
// 只拷贝对象
if (!object || typeof object !== "object") return;

// 根据 object 的类型判断是新建一个数组还是对象
let newObject = Array.isArray(object) ? [] : {};

// 遍历 object,并且判断是 object 的属性才拷贝
for (let key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] = object[key];
}
}

return newObject;
}// 浅拷贝的实现;

20. 实现深拷贝

  • 浅拷贝: 浅拷贝指的是将一个对象的属性值复制到另一个对象,如果有的属性的值为引用类型的话,那么会将这个引用的地址复制给对象,因此两个对象会有同一个引用类型的引用。浅拷贝可以使用 Object.assign 和展开运算符来实现。
  • 深拷贝: 深拷贝相对浅拷贝而言,如果遇到属性值为引用类型的时候,它新建一个引用类型并将对应的值复制给它,因此对象获得的一个新的引用类型而不是一个原有类型的引用。深拷贝对于一些对象可以使用 JSON 的两个函数来实现,但是由于 JSON 的对象格式比 js 的对象格式更加严格,所以如果属性值里边出现函数或者 Symbol 类型的值时,会转换失败

(1)JSON.stringify()

  • JSON.parse(JSON.stringify(obj))是目前比较常用的深拷贝方法之一,它的原理就是利用JSON.stringifyjs对象序列化(JSON字符串),再使用JSON.parse来反序列化(还原)js对象。
  • 这个方法可以简单粗暴的实现深拷贝,但是还存在问题,拷贝的对象中如果有函数,undefined,symbol,当使用过JSON.stringify()进行处理之后,都会消失。
1
2
3
4
5
6
7
8
9
10
11
let obj1 = {  a: 0,
b: {
c: 0
}
};
let obj2 = JSON.parse(JSON.stringify(obj1));
obj1.a = 1;
obj1.b.c = 1;
console.log(obj1); // {a: 1, b: {c: 1}}
console.log(obj2); // {a: 0, b: {c: 0}}
复制代码

(2)函数库lodash的_.cloneDeep方法

该函数库也有提供_.cloneDeep用来做 Deep Copy

1
2
3
4
5
6
7
8
9
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false
复制代码

(3)手写实现深拷贝函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 深拷贝的实现
function deepCopy(object) {
if (!object || typeof object !== "object") return;

let newObject = Array.isArray(object) ? [] : {};

for (let key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] =
typeof object[key] === "object" ? deepCopy(object[key]) : object[key];
}
}

return newObject;
}

21.日期格式化函数

最简易版本的,注意 没有加0的版本
格式:年-月-日 时:分:秒

1
2
3
4
5
6
7
8
9
10
11
function formatTime() {
const time = new Date();
const year = time.getFullYear();
const month = time.getMonth() + 1;
const day = time.getDate();
const hour = time.getHours();
const minute = time.getMinutes();
const seconds = time.getSeconds();
return `${year}-${month}-${day} ${hour}:${minute}:${seconds}`;
}
console.log(formatTime());

22.数组的乱序输出

主要的实现思路就是:

  • 取出数组的第一个元素,随机产生一个索引值,将该第一个元素和这个索引对应的元素进行交换。
  • 第二次取出数据数组第二个元素,随机产生一个除了索引为1的之外的索引值,并将第二个元素与该索引值对应的元素进行交换
  • 按照上面的规律执行,直到遍历完成
1
2
3
4
5
6
var arr = [1,2,3,4,5,6,7,8,9,10];
for (var i = 0; i < arr.length; i++) {
const randomIndex = Math.round(Math.random() * (arr.length - 1 - i)) + i;
[arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];
}
console.log(arr)

23.数组的扁平化

(1)递归实现

普通的递归思路很容易理解,就是通过循环递归的方式,一项一项地去遍历,如果每一项还是一个数组,那么就继续往下遍历,利用递归程序的方法,来实现数组的每一项的连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let arr = [1, [2, [3, 4, 5]]];
function flatten(arr) {
let result = [];

for(let i = 0; i < arr.length; i++) {
if(Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i]));
} else {
result.push(arr[i]);
}
}
return result;
}
flatten(arr); // [1, 2, 3, 4,5]
复制代码

(2)reduce 函数迭代

从上面普通的递归函数中可以看出,其实就是对数组的每一项进行处理,那么其实也可以用reduce 来实现数组的拼接,从而简化第一种方法的代码,改造后的代码如下所示:

1
2
3
4
5
6
7
8
let arr = [1, [2, [3, 4]]];
function flatten(arr) {
return arr.reduce(function(prev, next){
return prev.concat(Array.isArray(next) ? flatten(next) : next)
}, [])
}
console.log(flatten(arr));// [1, 2, 3, 4,5]
复制代码

(3)扩展运算符实现

这个方法的实现,采用了扩展运算符和 some 的方法,两者共同使用,达到数组扁平化的目的:

1
2
3
4
5
6
7
8
9
let arr = [1, [2, [3, 4]]];
function flatten(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
复制代码

(4)split 和 toString

可以通过 split 和 toString 两个方法来共同实现数组扁平化,由于数组会默认带一个 toString 的方法,所以可以把数组直接转换成逗号分隔的字符串,然后再用 split 方法把字符串重新转换为数组,如下面的代码所示:

1
2
3
4
5
6
let arr = [1, [2, [3, 4]]];
function flatten(arr) {
return arr.toString().split(',');
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
复制代码

通过这两个方法可以将多维数组直接转换成逗号连接的字符串,然后再重新分隔成数组。

(5)ES6 中的 flat

我们还可以直接调用 ES6 中的 flat 方法来实现数组扁平化。flat 方法的语法:arr.flat([depth])

其中 depth 是 flat 的参数,depth 是可以传递数组的展开深度(默认不填、数值是 1),即展开一层数组。如果层数不确定,参数可以传进 Infinity,代表不论多少层都要展开:

1
2
3
4
5
6
let arr = [1, [2, [3, 4]]];
function flatten(arr) {
return arr.flat(Infinity);
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
复制代码

可以看出,一个嵌套了两层的数组,通过将 flat 方法的参数设置为 Infinity,达到了我们预期的效果。其实同样也可以设置成 2,也能实现这样的效果。在编程过程中,如果数组的嵌套层数不确定,最好直接使用 Infinity,可以达到扁平化。 (6)正则和 JSON 方法 在第4种方法中已经使用 toString 方法,其中仍然采用了将 JSON.stringify 的方法先转换为字符串,然后通过正则表达式过滤掉字符串中的数组的方括号,最后再利用 JSON.parse 把它转换成数组:

1
2
3
4
5
6
7
8
let arr = [1, [2, [3, [4, 5]]], 6];
function flatten(arr) {
let str = JSON.stringify(arr);
str = str.replace(/(\[|\])/g, '');
str = '[' + str + ']';
return JSON.parse(str);
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]

24.数组去重

给定某无序数组,要求去除数组中的重复数字并且返回新的无重复数组。

ES6方法(使用数据结构集合):

1
2
3
4
const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];

Array.from(new Set(array)); // [1, 2, 3, 5, 9, 8]
复制代码

ES5方法:使用map存储不重复的数字(可以使用reduce代替for循环)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];

uniqueArray(array); // [1, 2, 3, 5, 9, 8]

function uniqueArray(array) {
let map = {};
let res = [];
for(var i = 0; i < array.length; i++) {
if(!map.hasOwnProperty([array[i]])) {
map[array[i]] = 1;
res.push(array[i]);
}
}
return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 方法三 : filter+indexOf
const newArr2 = [];
arr.filter((item, index) =>{
if(arr.indexOf(item) === index) {
newArr2.push(item)
}
})
console.log(newArr2);


// 方法四 : includes
let newArr3 = [];
for (let i = 0; i<arr.length; i++) {
if( !newArr3.includes(arr[i]) ) {
newArr3.push(arr[i]);
}
}
console.log(newArr3);

25.Array.prototype.flat

1
2
3
4
5
6
7
8
9
10
11
12
Array.prototype.flat = function (deep = 1) {
let res = []
deep--
for (const p of this) {
if (Array.isArray(p) && deep >= 0) {
res = res.concat(p.flat(deep))
} else {
res.push(p)
}
}
return res
}

26.Array.prototype.push

1
2
3
4
5
6
Array.prototype.myPush = function (...arg) {
for (let i = 0; i < arg.length; i++) {
this[this.length] = arg[i];
}
return this.length;
};

27.Array.prototype.filter

1
2
3
4
5
6
7
Array.prototype.myFilter = function (callback) {
const res = [];
for (let i = 0; i < this.length; i++) {
callback(this[i], i, this) && res.push(this[i]);
}
return res;
};

28.Array.prototype.map

1
2
3
4
5
6
7
Array.prototype.map = function (callback) {
const res = [];
for (let i = 0; i < this.length; i++) {
res.push(callback(this[i], i, this))
}
return res;
}

29.String.prototype.repeat

1
2
3
4
5
6
7
8
9
10
11
12
13
 String.prototype.repeat= function (n) {
let str = this;
let res = ''
while (n) {
res += str;
n--
}
return res
}
//补充
function repeat(s, n) {
return (n > 0) ? s.concat(repeat(s, --n)) : "";
}

30.类数组转化为数组

  • 通过 call 调用数组的 slice 方法来实现转换
1
Array.prototype.slice.call(arrayLike);
  • 通过 call 调用数组的 splice 方法来实现转换
1
Array.prototype.splice.call(arrayLike, 0);
  • 通过 apply 调用数组的 concat 方法来实现转换
1
Array.prototype.concat.apply([], arrayLike);
  • 通过 Array.from 方法来实现转换
1
Array.from(arrayLike);

31.解析 URL Params 为对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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); // 解码
if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则添加一个值
paramsObj[key] = [].concat(paramsObj[key], val);
} else { // 如果对象没有这个 key,创建 key 并设置值
paramsObj[key] = val;
}
} else { // 处理没有 value 的参数
paramsObj[param] = true;
}
})
return paramsObj;
}

32.循环打印红黄绿

下面来看一道比较典型的问题,通过这个问题来对比几种异步编程方法:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯不断交替重复亮灯?

三个亮灯函数:

1
2
3
4
5
6
7
8
9
10
function red() {
console.log('red');
}
function green() {
console.log('green');
}
function yellow() {
console.log('yellow');
}
复制代码

这道题复杂的地方在于需要“交替重复”亮灯,而不是“亮完一次”就结束了。

(1)用 callback 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const task = (timer, light, callback) => {
setTimeout(() => {
if (light === 'red') {
red()
}
else if (light === 'green') {
green()
}
else if (light === 'yellow') {
yellow()
}
callback()
}, timer)
}
task(3000, 'red', () => {
task(2000, 'green', () => {
task(1000, 'yellow', Function.prototype)
})
})
复制代码

这里存在一个 bug:代码只是完成了一次流程,执行后红黄绿灯分别只亮一次。该如何让它交替重复进行呢?

上面提到过递归,可以递归亮灯的一个周期:

1
2
3
4
5
6
7
8
9
const step = () => {
task(3000, 'red', () => {
task(2000, 'green', () => {
task(1000, 'yellow', step)
})
})
}
step()
复制代码

注意看黄灯亮的回调里又再次调用了 step 方法 以完成循环亮灯。

(2)用 promise 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const task = (timer, light) => 
new Promise((resolve, reject) => {
setTimeout(() => {
if (light === 'red') {
red()
}
else if (light === 'green') {
green()
}
else if (light === 'yellow') {
yellow()
}
resolve()
}, timer)
})
const step = () => {
task(3000, 'red')
.then(() => task(2000, 'green'))
.then(() => task(2100, 'yellow'))
.then(step)
}
step()
复制代码

这里将回调移除,在一次亮灯结束后,resolve 当前 promise,并依然使用递归进行。

(3)用 async/await 实现

1
2
3
4
5
6
7
const taskRunner =  async () => {
await task(3000, 'red')
await task(2000, 'green')
await task(2100, 'yellow')
taskRunner()
}
taskRunner()

33.每隔一秒打印 1,2,3,4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用闭包实现
for (var i = 0; i < 5; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, i * 1000);
})(i);
}
// 使用 let 块级作用域
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}

34.Promise实现图片的异步加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let imageAsync=(url)=>{
return new Promise((resolve,reject)=>{
let img = new Image();//img变量是一个img标签
img.src = url;
img.οnlοad=()=>{
console.log(`图片请求成功,此处进行通用操作`);
resolve(image);
}
img.οnerrοr=(err)=>{
console.log(`失败,此处进行失败的通用操作`);
reject(err);
}
})
}

imageAsync("url").then(()=>{
console.log("加载成功");
}).catch((error)=>{
console.log("加载失败");
})

35.发布-订阅模式

题目描述:实现一个发布订阅模式拥有 on emit once off 方法

实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class EventEmitter {
constructor() {
this.events = {};
}
// 实现订阅
on(type, callBack) {
if (!this.events[type]) {
this.events[type] = [callBack];
} else {
this.events[type].push(callBack);
}
}
// 删除订阅
off(type, callBack) {
if (!this.events[type]) return;
this.events[type] = this.events[type].filter((item) => {
return item !== callBack;
});
}
// 只执行一次订阅事件
once(type, callBack) {
function fn() {
callBack();
this.off(type, fn);
}
this.on(type, fn);
}
// 触发事件
emit(type, ...rest) {
this.events[type] &&
this.events[type].forEach((fn) => fn.apply(this, rest));
}
}

36.封装异步的fetch,使用async await方式来使用

1
2
3
4
5
6
7
8
9
10
11
12
13
async function myFetchAsync(url, options) {
try {
//等 获取到数据
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`);
}
return response;
} catch (error) {
console.error(error);
throw error;
}
}

37.实现简单路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function Router(){
this.routes={}
this.curUrl=''

//添加回调函数
this.route=function(path,callback){
this.routes[path]=callback||function(){}
}
//执行回调函数
this.refresh=function(){
//获取url
this.curUrl=location.hash.slice(1)||'/'
this.routes[this.curUrl]()
}
//监听load和hashchange
this.init=function(){
window.addEventListener('load',this.refresh.bind(this),false)
window.addEventListener('hashchange',this.refresh.bind(this),false)
}
}

let res=document.getElementById('div')
let R=new Router()
//触发监听
R.init()
R.route('/',function(){
res.style.backgroundColor='pink'
res.innerHTML='11111'
})

38.斐波那契数列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 尾递归
function Fibonacci(n,num=1,sum=1){

if (n<=2){
return sum;
}
return Fibonacci(n-1,sum,num+sum);
}

function feibo3(n) {
let pre = 1;
let cur = 1;
for (let i = 2; i <= n; i++) {
[pre, cur] = [cur, cur + pre];
}
return cur;
}

39.使用 setTimeout 实现 setInterval

使用 setTimeout 递归调用来模拟 setInterval,确保了只有一个事件结束了,才会触发下一个定时器事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function mySetInterval(fn, timeout) {
// 控制器,控制定时器是否继续执行
var timer = {
flag: true
};
// 设置递归函数,模拟定时器执行。
function interval() {
if (timer.flag) {
fn();
setTimeout(interval, timeout);
}
}
// 启动定时器
setTimeout(interval, timeout);
// 返回控制器
return timer;
}

40.jsonp

JSONP 原理

  1. 定义好回调函数,比方说命名为 callback ,并将函数名作为 url 的参数;
  2. 添加 script 标签,指定的资源为目标域的方法,也就是上面的 url ;
  3. 后端接收 GET 请求,返回 callback(responseData) 格式数据,把要返回的数据 responseData 传到 callback() 中;
  4. 前端接收 javaScript 内容,执行了后端返回的 callback(responseData) ,这样就完成了一次前后端交互了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 动态的加载js文件
function addScript(src) {
const script = document.createElement('script');
script.src = src;
script.type = "text/javascript";
document.body.appendChild(script);
}
addScript("http://xxx.xxx.com/xxx.js?callback=handleRes");
// 设置一个全局的callback函数来接收回调结果
function handleRes(res) {
console.log(res);
}
// 接口返回的数据格式
handleRes({a: 1, b: 2});

41.判断对象是否存在循环引用

对象循环引用会导致垃圾回收机制无法正确回收这些对象,从而导致内存泄漏,使得程序消耗更多的内存,直到最终内存耗尽,导致程序崩溃。

具体来说,当出现循环引用的情况时,垃圾回收机制会出现一些问题。因为垃圾回收机制是通过遍历对象的引用关系来确定哪些对象不再被引用,从而将它们标记为垃圾并回收。但是,当对象之间存在循环引用时,垃圾回收机制无法确定哪些对象是垃圾,哪些对象还在使用中,从而导致无法正确回收垃圾对象,造成内存泄漏。

此外,对象循环引用也会影响程序的性能和稳定性。当内存不足时,程序会变得非常缓慢,甚至无法响应用户的操作,最终导致程序崩溃。

JSON.stringify将对象转换为字符串时,会抛出异常。

这是因为在进行对象字符串化时,字符串化工具会尝试遍历整个对象,并将对象中的所有属性都转换为字符串。如果对象中存在循环引用,字符串化工具会陷入死循环,无法正常完成操作。为了防止这种情况发生,字符串化工具会检测对象中是否存在循环引用,如果存在,就会抛出异常,防止程序陷入死循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function hasCircularReference(obj) {
let hasCircular = false;
const seenObjects = new WeakSet(); // 使用WeakSet来存储已经遍历过的对象

function detectCircularReference(obj) {
if (typeof obj === 'object' && obj !== null) {
if (seenObjects.has(obj)) {
hasCircular = true;
return;
}
seenObjects.add(obj); // 标记该对象已经被遍历过
for (const key in obj) {
detectCircularReference(obj[key]); // 遍历该对象的属性
}
}
}

detectCircularReference(obj);
return hasCircular;
}

42.继承

寄生组合式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function clone (parent, child) {
// 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
child.prototype = Object.create(parent.prototype);//*****
child.prototype.constructor = child;//*****
}

function Parent6() {
this.name = 'parent6';
this.play = [1, 2, 3];
}
Parent6.prototype.getName = function () {
return this.name;
}
function Child6() {
Parent6.call(this); //*****
this.friends = 'child5';
}

clone(Parent6, Child6);

Child6.prototype.getFriends = function () {
return this.friends;
}

let person6 = new Child6();
console.log(person6); //{friends:"child5",name:"child5",play:[1,2,3],__proto__:Parent6}
console.log(person6.getName()); // parent6
console.log(person6.getFriends()); // child5

原型链继承

原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针

举个例子

1
2
3
4
5
6
7
8
9
function Parent() {
this.name = 'parent1';
this.play = [1, 2, 3]
}
function Child() {
this.type = 'child2';
}
Child.prototype = new Parent();
console.log(new Child())

上面代码看似没问题,实际存在潜在问题

1
2
3
4
var s1 = new Child();
var s2 = new Child();
s1.play.push(4);
console.log(s1.play, s2.play); // [1,2,3,4]

改变s1play属性,会发现s2也跟着发生变化了,这是因为两个实例使用的是同一个原型对象,内存空间是共享的

构造函数继承

借助 call调用Parent函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Parent(){
this.name = 'parent1';
}

Parent.prototype.getName = function () {
return this.name;
}

function Child(){
Parent1.call(this);
this.type = 'child'
}

let child = new Child();
console.log(child); // 没问题
console.log(child.getName()); // 会报错

可以看到,父类原型对象中一旦存在父类之前自己定义的方法,那么子类将无法继承这些方法

相比第一种原型链继承方式,父类的引用属性不会被共享,优化了第一种继承方式的弊端,但是只能继承父类的实例属性和方法,不能继承原型属性或者方法

组合继承

前面我们讲到两种继承方式,各有优缺点。组合继承则将前两种方式继承起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Parent3 () {
this.name = 'parent3';
this.play = [1, 2, 3];
}

Parent3.prototype.getName = function () {
return this.name;
}
function Child3() {
// 第二次调用 Parent3()
Parent3.call(this);
this.type = 'child3';
}

// 第一次调用 Parent3()
Child3.prototype = new Parent3();
// 手动挂上构造器,指向自己的构造函数
Child3.prototype.constructor = Child3;
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play); // 不互相影响
console.log(s3.getName()); // 正常输出'parent3'
console.log(s4.getName()); // 正常输出'parent3'

这种方式看起来就没什么问题,方式一和方式二的问题都解决了,但是从上面代码我们也可以看到Parent3 执行了两次,造成了多构造一次的性能开销

原型式继承

这里主要借助Object.create方法实现普通对象的继承

同样举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let parent4 = {
name: "parent4",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};

let person4 = Object.create(parent4);
person4.name = "tom";
person4.friends.push("jerry");

let person5 = Object.create(parent4);
person5.friends.push("lucy");

console.log(person4.name); // tom
console.log(person4.name === person4.getName()); // true
console.log(person5.name); // parent4
console.log(person4.friends); // ["p1", "p2", "p3","jerry","lucy"]
console.log(person5.friends); // ["p1", "p2", "p3","jerry","lucy"]

这种继承方式的缺点也很明显,因为Object.create方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能

寄生式继承

寄生式继承在上面继承基础上进行优化,利用这个浅拷贝的能力再进行增强,添加一些方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let parent5 = {
name: "parent5",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};

function clone(original) {
let clone = Object.create(original);
clone.getFriends = function() {
return this.friends;
};
return clone;
}

let person5 = clone(parent5);

console.log(person5.getName()); // parent5
console.log(person5.getFriends()); // ["p1", "p2", "p3"]

其优缺点也很明显,跟上面讲的原型式继承一样

43.异步操作串并行执行

串行执行

reduce 方法的第一个参数是一个异步函数,它将前一个异步操作的 Promise 对象作为参数传递给下一个异步操作。reduce 方法的第二个参数是一个 Promise 对象,它的值为 undefined。在每次调用 reduce 方法时,都会等待前一个异步操作执行完毕之后再执行下一个异步操作。

1
2
3
4
5
6
7
8
async function sequentialAsync() {
const asyncFunctions = [asyncFunction1, asyncFunction2, asyncFunction3];

return asyncFunctions.reduce(async (previousPromise, asyncFunction) => {
await previousPromise;
return asyncFunction();
}, Promise.resolve());
}

并行执行

1
2
3
4
5
6
7
8
9
async function parallelAsync() {
const [result1, result2, result3] = await Promise.all([
asyncFunction1(),
asyncFunction2(),
asyncFunction3(),
]);

// 处理异步操作的结果
}

并行与串行的结合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Scheduler {
constructor(max) {
// 最大可并发任务数
this.max = max;
// 当前并发任务数
this.count = 0;
// 任务队列
this.queue = [];
}

add(fn) {
this.queue.push(fn)
this.run()
}
run() {
if (this.count >= this.max || this.queue.length === 0) return
this.count++
Promise.resolve(this.queue.shift()()).finally(() => {
this.count--
this.run()
})
}
}
// ------------test-------------------
// 延迟函数
const sleep = time => new Promise(resolve => setTimeout(resolve, time));

// 同时进行的任务最多2个
const scheduler = new Scheduler(2);

// 添加异步任务
// time: 任务执行的时间
// val: 参数
const addTask = (time, val) => {
scheduler.add(() => {
return sleep(time).then(() => console.log(val));
});
};

addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
// 2
// 3
// 1
// 4

44.对象的扁平化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function flattenObject(obj, prefix = '') {
let result = {};
for (let key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const propName = prefix ? `${prefix}.${key}` : key;
if (typeof obj[key] === 'object' && obj[key] !== null) {
Object.assign(result, flattenObject(obj[key], propName));
} else {
result[propName] = obj[key];
}
}
}
return result;
}

//
const obj = {
a: 1,
b: {
c: 2,
d: {
e: 3,
f: 4
}
}
};
//
{
"a": 1,
"b.c": 2,
"b.d.e": 3,
"b.d.f": 4
}

45.版本号排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
arr.sort((a, b) => {
let i = 0;
const arr1 = a.split(".");
const arr2 = b.split(".");

while (true) {
const s1 = arr1[i];
const s2 = arr2[i];
i++;
if (s1 === undefined || s2 === undefined) {
return arr1.length - arr2.length;
}

if (s1 === s2) continue;

return s1 - s2;
}
});

46.DOM 节点输出为 JSON 格式

要将一个 DOM 节点输出为 JSON 格式,需要考虑节点的以下属性:

  1. tagName - 节点的标签名。
  2. attributes - 节点的属性,需要遍历所有属性并以键值对的形式存储。
  3. childNodes - 节点的子节点,需要递归地对每个子节点进行相同的操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function nodeToJson(node) {
var obj = {};
obj['tagName'] = node.tagName.toLowerCase();

if (node.attributes) {
var attrs = {};
for (var i = 0; i < node.attributes.length; i++) {
var attr = node.attributes[i];
attrs[attr.nodeName] = attr.nodeValue;
}
obj['attributes'] = attrs;
}

if (node.childNodes && node.childNodes.length > 0) {
var children = [];
for (var j = 0; j < node.childNodes.length; j++) {
var childNode = node.childNodes[j];
if (childNode.nodeType === Node.ELEMENT_NODE) {
children.push(nodeToJson(childNode));
}
}
obj['childNodes'] = children;
}

return obj;
}

//
var node = document.getElementById('my-element');
var json = nodeToJson(node);

47.JSON 格式输出为DOM 节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 真正的渲染函数
function _render(vnode) {
// 如果是数字类型转化为字符串
if (typeof vnode === "number") {
vnode = String(vnode);
}
// 字符串类型直接就是文本节点
if (typeof vnode === "string") {
return document.createTextNode(vnode);
}
// 普通DOM
const dom = document.createElement(vnode.tag);
if (vnode.attributes) {
// 遍历属性
Object.keys(vnode.attributes).forEach((key) => {
const value = vnode.attributes[key];
dom.setAttribute(key, value);
});
}
// 子数组进行递归操作
vnode.children.forEach((child) => dom.appendChild(_render(child)));
return dom;
}

48.Array.prototype.reduce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
使用方法
array.reduce(function(accumulator, currentValue, index, array) {
// return updated accumulator
}, initialValue)
其中,参数 function(accumulator, currentValue, index, array) 是一个回调函数,用于处理每个数组元素并更新累加器的值。回调函数接受四个参数:

accumulator: 累加器的值,初始值为 initialValue
currentValue: 当前数组元素的值
index: 当前数组元素的索引
array: 原始数组
实现方法
Array.prototype.reduce = function(callback, initialValue) {
var accumulator = (initialValue !== undefined) ? initialValue : this[0];
for (var i = (initialValue !== undefined) ? 0 : 1; i < this.length; i++) {
accumulator = callback.call(undefined, accumulator, this[i], i, this);
}
return accumulator;
}

49.实现一个事件委托(易错)

事件委托这里就不阐述了,比如给li绑定点击事件

看错误版,(容易过的,看「面试官水平了」)👇

1
2
3
4
5
6
7
ul.addEventListener('click', function (e) {
console.log(e,e.target)
if (e.target.tagName.toLowerCase() === 'li') {
console.log('打印') // 模拟fn
}
})
复制代码

「有个小bug,如果用户点击的是 li 里面的 span,就没法触发 fn,这显然不对」👇

1
2
3
4
5
6
7
8
<ul id="xxx">下面的内容是子元素1
<li>li内容>>> <span> 这是span内容123</span></li>
下面的内容是子元素2
<li>li内容>>> <span> 这是span内容123</span></li>
下面的内容是子元素3
<li>li内容>>> <span> 这是span内容123</span></li>
</ul>
复制代码

这样子的场景就是不对的,那我们看看高级版本👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function delegate(element, eventType, selector, fn) {
element.addEventListener(eventType, e => {
let el = e.target
while (!el.matches(selector)) {
if (element === el) {
el = null
break
}
el = el.parentNode
}
el && fn.call(el, e, el)
},true)
return element
}

50.事件的代理

由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理。

事件委托的优点

  • 只需要将同类元素的事件委托给父级或者更外级的元素,不需要给所有的元素都绑定事件,减少内存占用空间,提升性能。
  • 动态新增的元素无需重新绑定事件

错误版:

1
2
3
4
5
ul.addEventListener('click', function(e){
if(e.target.tagName.toLowerCase() === 'li'){
fn() // 执行某个函数
}
})

正确版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function delegate(element, eventType, selector, fn) {
element.addEventListener(eventType, e => {
let el = e.target
while (!el.matches(selector)) {
if (element === el) {
el = null
break
}
el = el.parentNode
}
el && fn.call(el, e, el)
})
return element
}

51.大数相加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 = Number(a[i]) + Number(b[i]) + f;
f = Math.floor(t/10);
sum = t%10 + sum;
}
if(f!==0){
sum = '' + f + sum;
}
return sum;
}

52.模板字符串解析功能

1
2
3
4
5
6
7
8
9
10
11
12
13
let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let data = {
name: '姓名',
age: 18
}
render(template, data); // 我是姓名,年龄18,性别undefined
function render(template, data) {
let computed = template.replace(/\{\{(\w+)\}\}/g, function (match, key) {
return data[key];
});
return computed;
}

53.列表转成树形结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function listToTree(list) {
const map = {};
const roots = [];
for (const item of list) {
const id = item.id;
const parentId = item.parentId;
map[id] = { ...item, children: map[id]?.children||[] };
if (parentId === 0) {
roots.push(map[id]);
} else {
if(!map[parentId]){
map[parentId]={children:[]};
}
map[parentId].children.push(map[id]);
}
}
return roots;
}

54.树形结构转成列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function treeToList(data) {
let res = [];
const dfs = (tree) => {
tree.forEach((item) => {
if (item.children) {
dfs(item.children);
delete item.children;
}
res.push(item);
});
};
dfs(data);
return res;
}

55.lodash.get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let obj = {
foo: {
bar: {
baz: 'Hello World'
}
}
};
function get(source, path, defaultValue = undefined) {
const paths = path.split(".");
let result = source;
for (const p of paths) {
if(result[p]===undefined){
return undefined;
}
result = result[p];
}
return result === undefined ? defaultValue : result;
}
console.log(get(obj, 'foo.bar.baz'));

56.将数字每千分位用逗号隔开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let format = n => {
let num = n.toString() // 转成字符串
let decimals = ''
// 判断是否有小数
num.indexOf('.') > -1 ? decimals = '.'+num.split('.')[1] : decimals
let len = num.length
if (len <= 3) {
return num
} else {
let remainder = len % 3
if (remainder > 0) { // 不是3的整数倍
return num.slice(0, remainder) + ',' + num.slice(remainder, len).match(/\d{3}/g).join(',') + decimals
} else { // 是3的整数倍
return num.slice(0, len).match(/\d{3}/g).join(',') + temp
}
}
}
console.log(format(12323.33)); // '12,323.33'

57.双向数据绑定

响应式也可以这这个模式写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
双向数据绑定
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
})

58.设计模式

发布订阅模式

单例模式

保证一个类只能被实例一次,每次获取的时候,如果该类已经创建过实例则直接返回该实例,否则创建一个实例保存并返回。

1
2
3
4
5
6
7
8
9
10
11
12
class Singleton {
constructor(name, age) {
if (!Singleton.instance) {
this.name = name
this.age = age
Singleton.instance = this
}
return Singleton.instance
}
}

console.log(new Singleton("Taobao", 18) === new Singleton("Baidu", 15)) // true

命令模式

命令模式中的命令指的是一个执行某些特定的事情的指令。

命令模式最常见的应用场景如:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时可以通过一种松耦合的方式来设计程序,使得请求发送者和请求接收者消除彼此之间的耦合关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 接收者类
class Receiver {
execute() {
console.log('接收者执行请求')
}
}

// 命令者
class Command {
constructor(receiver) {
this.receiver = receiver
}
execute () {
console.log('命令');
this.receiver.execute()
}
}
// 触发者
class Invoker {
constructor(command) {
this.command = command
}
invoke() {
console.log('开始')
this.command.execute()
}
}

// 仓库
const warehouse = new Receiver();
// 订单
const order = new Command(warehouse);
// 客户
const client = new Invoker(order);
client.invoke()

策略模式

策略模式指的是定义一系列算法,把他们一个个封装起来,目的就是将算法的使用和算法的实现分离开来。同时它还可以用来封装一系列的规则,可以被替换使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
let strategies = {
add: function (num) {
return num + num;
},
multiply: function (num) {
return num * num;
},
};
let calculateBonus = function (strategy, num) {
return strategies[strategy](num);
};
console.log(calculateBonus("add", 3));
console.log(calculateBonus("multiply", 3));

观察者模式

定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使它们能够自动更新自己

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Subject {
constructor() {
this.observers = [];
}

// 添加观察者
addObserver(observer) {
this.observers.push(observer);
}

// 移除观察者
removeObserver(observer) {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}

// 通知观察者
notify(data) {
this.observers.forEach((observer) => {
observer.update(data);
});
}
}

class Observer {
update(data) {
console.log(`Received data: ${data}`);
}
}

// 测试代码
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();

subject.addObserver(observer1);
subject.addObserver(observer2);

subject.notify('Hello world!'); // Output: Received data: Hello world!

subject.removeObserver(observer1);

subject.notify('Goodbye world!'); // Output: Received data: Goodbye world!

工厂模式

工厂模式是用来创建对象的常见设计模式,在不暴露创建对象的具体逻辑,而是将逻辑进行封装,那么它就可以被称为工厂。工厂模式又叫做静态工厂模式,由一个工厂对象决定创建某一个类的实例。

优点

  1. 调用者创建对象时只要知道其名称即可
  2. 扩展性高,如果要新增一个产品,直接扩展一个工厂类即可。
  3. 隐藏产品的具体实现,只关心产品的接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class DownJacket {
production(){
console.log('生产羽绒服')
}
}
class Underwear{
production(){
console.log('生产内衣')
}
}
class TShirt{
production(){
console.log('生产t恤')
}
}
// 工厂类
class clothingFactory {
constructor(){
this.downJacket = DownJacket
this.underwear = Underwear
this.t_shirt = TShirt
}
getFactory(clothingType){
const _production = new this[clothingType]
return _production.production()
}
}
const clothing = new clothingFactory()
clothing.getFactory('t_shirt')// 生产t恤

代理模式

例如:Proxy

代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。

装饰者模式

动态地给某个对象添加一些额外的职责。

在不改变原对象的基础上,通过对其进行包装扩展,使原有对象可以满足用户的更复杂需求,而不会影响从这个类中派生的其他对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Cellphone {
create() {
console.log('生成一个手机')
}
}
class Decorator {
constructor(cellphone) {
this.cellphone = cellphone
}
create() {
this.cellphone.create()
this.createShell(cellphone)
}
createShell() {
console.log('生成手机壳')
}
}
// 测试代码
let cellphone = new Cellphone()
cellphone.create()

console.log('------------')
let dec = new Decorator(cellphone)
dec.create()