banner
前端部分手写实现,看完之后,没一个能打的~💪

前端部分方法的手写实现

Scroll down

手写实现一个new

  1. 创建一个空的简单JavaScript对象(即{});
  2. 为步骤1新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
  3. 将步骤1新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this。

new关键词执行后总会返回一个对象, 要么是实例对象, 要么是return语句指定的对象.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function _new(fn,...args){
// let obj = new Object()
// obj.__proto__ = fn.prototype
// 基于fn构造函数原型创建一个新对象
let obj = Object.create(fn.prototype)
// 执行构造函数,并获取fn执行的结果
let res = fn.call(obj,...args)
// 如果执行结果有返回值并且是一个对象,返回执行结果,否则,返回新创建的对象
let isObject = typeof res === 'object' && res !== null
let isFunction = typeof res === 'function'
return isObject || isFunction ? res : obj
}

// 更好理解
function myNew(fn, ...args) {
let obj = Object.create(fn.prototype)
let res = fn.call(obj,...args)

if (res && (typeof res === 'object' || typeof res === 'function')) retrun res
return obj
}

Object.create

1
2
3
4
5
6
function create (o) {
function F() {}
F.prototype = o
return new F()
}
// 创建一个对象,以o为新创建对象的原型对象

instanceof

  • Instanceof: 用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
  • 通俗一点就是: 判断new出的实例对象是否是当前构造函数生成的对象
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 my_instanceof(left, right) {
// 这里先用typeof来判断基础数据类型,如果是,直接返回false
if(typeof left !== 'object' || left === null) return false;
// getProtypeOf是Object对象自带的API
// 返回指定对象的原型(内部[[Prototype]]属性的值)隐式原型
let proto = Object.getPrototypeOf(left);
let prototype = right.prototype
while(true) { //循环往下寻找,直到找到相同的原型对象
if(proto === null) return false;
if(proto === prototype) return true;//找到相同原型对象,返回true
proto = Object.getPrototypeof(proto);
}
}

// 更好理解版本
function my_instanceof(left, right) {
if(typeof left !== 'object' || left === null) return false;

let proto = left.__proto__
let prototype = right.prototype // 右边的原型
while (true) {
if (proto === null) return false
if (proto === prototype) return true
proto = proto.__proto__ // 向上查找,直到proto为null
}
}

🔥手写Promise.all

  1. 参数可迭代
  2. 返回值是promise
  3. 如果全部成功,状态变为resolve
  4. 但凡有一个失败,状态变为reject
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function PromiseAll(arr) {
if(!Array.isArray(arr)) {
return new TypeError(`${arr} is not iterable`)
}
return new Promise((resolve,reject) => {
let result = [] // 存放结果
let count = 0 // 进入fullfilled的promise个数

for(let i = 0; i < arr.length; i++) {
// 这里默认把所有入参都包装成promise返回了
// 因为如果是普通值,在Promise内部实现 2.3.4时, 有返回值的操作
Promise.resolve(arr[i]).then(value => {
// counter++;
result[i] = value;
if ( ++count === arr.length) resolve(result) //判断已经完成
// 只要有一个被rejected时, 就reject
}).catch(e => reject(e))
}

})
}

手写call, apply, bind

call

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Function.prototype._call = function(context, ...args) {
// 判断上下文对象
context = context ?Object(context) : window
// 创造唯一key值,作为我们构造的context内部方法名
let fn = Symbol('thisFn')
// 1. 将fn作为属性添加到context上
context[fn] = this // 隐式绑定,改变构造函数的调用者间接改变 this 指向
// 2. 将挂载以后的方法调用
let res = context[fn](...args) // 这里..args将args数组,变为参数列表,数组的扩展运算符
// 3. 删除新添加的对象属性
delete context[fn]
// 4. 返回调用结果
return res
}

apply

1
2
3
4
5
6
7
8
// func.apply(thisArg, [argsArray])
Function.prototype.myApply = function (thisArg, args) {
let fn = Symbol('fn') // 创建个独一无二的方法名
thisArg[fn] = this // 方法变换执行对象
let res = thisArg[fn](...args)
delete thisArg[fn] // 删除对象属性
return res
}

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
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
// // ES2015 arguments是一个对象,类数组;
// const args = Array.from(arguments);
// const args = [...arguments];
// let new_array = old_array.concat(value1,value2,...)
// concat后面参数可以是数组,也可以是参数本身

Function.prototype.myBind = function (context) {
if(typeof this != 'function'){
throw new TypeError('this is not a function')
}
const self = this
const args = [...arguments].slice(1) // 第一个参数是this,截取掉
const fNOP = function () {}

const fBound = function () { // 返回一个绑定了this指向的方法,闭包
const newArgs = args.concat(...arguments)
// _self.call(context, ...args.concat(...arguments)) 参数为数组,用apply简单
return self.apply(this instance of fNOP ? this : context, newArgs)
}
// 1.方法1 原型式继承
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP(); //绑定原型 原型式继承
// 2. 方法2: fBound.prototype = Object.create(this.prototype)

return fBound;
}

// 去注释 double
Function.prototype.bind2 = function (context) {
// 1. 判断调用bind的是否是函数
if (typeof this !== 'function') {
throw new TypeError('this is not a function')
}
// 2. 保存当前环境上下文,取截取this后的传入参数
const self = this
const args = [...arguments].slice(1)
const fNOP = function () {}

const fBound = function () {
const newArgs = args.concat(...arguments)
// 1.当作为构造函数 new操作的时候,this指向实例,将绑定函数的this指向该实例,可以让实例获得来自绑定函数的值
// 2.当作为普通函数时,将绑定函数的this指向context
return self.apply(this instanceof fNOP ? this : context, newArgs)
}

// 3.原型式继承
fNOP.prototype = this.prototype
fBound.prototype = new fNOP()
// 复制原函数的prototype给fBound, 一些情况下函数没有prototype,如箭头函数
// fBound.prototype.__proto__ = (fNOP.prototype=this.prototype)

return fBound
}

// fBound.prototype = this.prototype;
// 直接修改 fBound.prototype 的时候,也会直接修改绑定函数的 prototype

bind2

1
2
3
4
5
6
7
8
9
10
11
12
13
// https://jsgodroad.com/interview/js/#%E6%89%8B%E5%86%99%E9%A2%98
Function.prototype.myBind = function (context) {
var _this = this
var args = [...arguments].slice(1)
// 返回一个函数
return function F() {
// 因为返回了一个函数,我们可以 new F(),所以需要判断
if (this instanceof F) {
return new _this(...args, ...arguments)
}
return _this.apply(context, args.concat(...arguments))
}
}

🔥bind3 new 2021-12-13

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Function.prototype.myBind = function(objThis, ...args) {
if (typeof this !== 'function') {
return new TypeError('what is trying to be bound is not callable')
}
const self = this // 保存源函数,以及参数
const fBound = function () {
const newArgs = args.concat(...arguments)
return self.apply(this instanceof fBound ? this : objThis, newArgs)
}
// 修复函数没有prototype的情况 // 维护原型关系
if (this.prototype) {
fBound.prototype = Object.create(this.prototype)
}
return fBound
}
// https://juejin.cn/post/6844903906279964686#heading-14

bind-polyfill-core-js

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
//  Yes, it does work with `new (funcA.bind(thisArg, args))`
if (!Function.prototype.bind) (function(){
var ArrayPrototypeSlice = Array.prototype.slice;
Function.prototype.bind = function(otherThis) {
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 baseArgs= ArrayPrototypeSlice.call(arguments, 1),
baseArgsLength = baseArgs.length,
fToBind = this,
fNOP = function() {},
fBound = function() {
baseArgs.length = baseArgsLength; // reset to default base arguments
baseArgs.push.apply(baseArgs, arguments);
return fToBind.apply(
// 是否被new操作符调用,是的话就用新创建的this替换bind的this
fNOP.prototype.isPrototypeOf(this) ? this : otherThis, baseArgs
);
};

if (this.prototype) {
// Function.prototype doesn't have a prototype property
fNOP.prototype = this.prototype;
}
// 防止new了函数后改变原型导致原函数的原型被修改
fBound.prototype = new fNOP();

return fBound;
};
})();

防抖和节流

防抖 debounce

  • 事件响应函数在一段时间后才执行,如果这段时间内再次调用,则重新计算。 在一定的时间间隔内,将多次触发变成一次触发
应用场景
  1. 限制鼠标连续点击(按钮提交等)
  2. Scroll事件滚动防抖
  3. 搜索框输入查询
  4. 浏览器窗口缩放,resize事件
1
2
3
4
5
6
7
8
9
10
// https://www.30secondsofcode.org/js/s/debounce 目前看见最简写法,best!
const debounce = (fn, ms = 0) => {
let timeoutId
return function(...args) {
// function(...args) rest参数 ,将args转为数组
// 对比数组的扩展运算法 fn.call(obj,...args) 含义不一样, 将args转为参数列表
clearTimeout(timerId) // 每次点击的时候清除上一个定时器,重新计时
timeoutId = setTimeout(() => fn.apply(this,args), ms)
}
}

节流 throttle

  • 持续的触发事件,每隔一段时间, 只执行一次 ,减少一段时间的触发频率
时间戳版本
1
2
3
4
5
6
7
8
9
10
11
12
13
const throttle = (fn, wait) => {
let pre = 0
return function () {
// Date.now || + new Date() || new Date().getTime() || new Date().valueOf()
let now = Date.now()
if (now - pre > = wait) {
// apply第二个参数可以是数组,也可以是类数组对象
// 所以写arguments没有问题!
fn.apply(this,arguments)
pre = Date.now() // 将当前时间记录, 作为下一个计时起点
}
}
}

定时器版本

1
2
3
4
5
6
7
8
9
10
11
12
13
const throttle = (fn, wait) => {
let timerId
return function () {
if (!timerId) {
// 这里箭头函数,this本身指向上层
timerId = setTimeout(() => {
fn.apply(this, arguments)
timerId = null
}, wait)
}
}
}
//冴羽 https://github.com/mqyqingfeng/Blog/issues/26

deepClone深拷贝 !!!

浅拷贝

  1. Object.assign()
  2. ...扩展运算符 let cloneObj = {...obj} let newArr = [...arr]
  3. 数组的Slice(), concat()
1
const shallowClone = obj => Object.assign({}, obj);

深拷贝

乞丐版
1
let newObj = JSON.parse( JSON.stringify( obj ) )
  1. 拷贝对象的值中如果有函数undefinedSymbol,JSON.stringify序列化后的字符串中,这个键值对丢失
  2. 拷贝Date引用类型会变成字符串
  3. 拷贝RegExp会变成空对象 {}
  4. 对象中含有 NaNInfinity 会变成 null
  5. 无法拷贝对象的原型链
  6. 无法拷贝不可枚举的属性 如Symbol
  7. 无法拷贝对象的循环引用 , 即对象成环 obj[key] = obj
🔥优化版
  • 考虑 Date、RegExp类型, 直接生成一个新的实例返回

  • 考虑数组 let target = Array.isArray(obj)? [] : {}

  • 考虑循环引用 利用WeakMap作为hash表, 检测到对象已存在于哈希表中,取出该值返回即可

  • 针对不可枚举属性以及 Symbol 类型,使用 Reflect.ownKeys()

  • 函数部分太复杂,函数的原型,多层柯里化等

  • 针对Map, Set, Error等,Object.getOwnPropertyDescriptors(obj) 也不考虑

  • 递归爆栈问题,改用循环解决,广度优先

  • 深拷贝的终极探索(99%的人都不知道)

  • 如何写出一个惊艳面试官的深拷贝?

    1
    RangeError: Maximum call stack size exceeded
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
// 检测对象 
const isObject = (obj) => {
return typeof obj === 'object' && obj != null // !!obj
}

const deepClone = (obj, hash = new WeakMap()) => {
// 值类型 直接返回
if (!isObject(obj)) return obj
// Date, RegExp constructor容易被修改丢失,被认为不安全,不推荐作为判断
// instanceof好一些
// if (obj.constructor === Date) return new Date(obj)
// if (obj.constructor === RegExp) return new RegExp(obj)
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj)
// 解决循环引用,查哈希表
if (hash.has(obj)) return hash.get(obj)
// let allDesc = Object.getOwnPropertyDescriptors(obj)
// let target = Object.create(Object.getPrototypeOf(obj),allDesc)
let target = Array.isArray(obj) ? [] : {} // 考虑数组
hash.set(obj, target)

Reflect.ownKeys(obj).forEach(key => {
if (isObject(obj[key])) {
target[key] = deepClone(obj[key], hash)
} else {
target[key] = obj[key]
}
})
return target
}
  1. 深拷贝的终极探索

实现 (5).add(3).minus(2) 功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Number.prototype.add = function(n) {
if (typeof number !== 'number') {
throw new Error('请输入数字~');
}
return this.valueOf() + n;
};
Number.prototype.minus = function(n) {
if (typeof number !== 'number') {
throw new Error('请输入数字~');
}
// return this - n
return this.valueOf() - n;
};
// (5).add(3).minus(2)

如何求数组最大值和最小值

  1. 循环后Math.max
  2. reduce
  3. sort排序后取最后
  4. ES6 … + Math.max
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1. Math.max
var arr = [6, 4, 1, 8, 2, 11, 23];

var result = arr[0];
for (var i = 1; i < arr.length; i++) {
result = Math.max(result, arr[i]);
}
console.log(result);
// 2. reduce
var arr = [6, 4, 1, 8, 2, 11, 23];

function max(prev, next) {
return Math.max(prev, next);
}
console.log(arr.reduce(max));
// 3. sort
var arr = [6, 4, 1, 8, 2, 11, 23];

arr.sort(function(a,b){return a - b;});
console.log(arr[arr.length - 1])
// 4. ES6 ...
var arr = [6, 4, 1, 8, 2, 11, 23];
console.log(Math.max.apply(null, arr))

数组去重

  • 双重for循环
  • new Set
  • indexOf
  • array.filter + indexOf
  • sort排序 + 相邻元素对比
  • Object 键值对 obj.hasOwnProperty
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
// 0. 双重for循环

// 1. Set集合,不能有重复值
const newArr2 = Array.from(new Set(arr))
// 简化
const newArr = [...new Set(arr)]

// 2. indexOf
function resetArr(arr) {
let res = []
arr.forEach(item => {
if (res.indexOf(item) === -1) {
res.push(item)
}
})
return res
}
// indexOf()方法返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1
// filter() 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。

// 3. array.filter + indexOf
const unique = (arr) => {
let res = arr.filter((item,index) => {
// 如果第一次出现,相等
return arr.indexOf(item) === index
})
return res
}
// 4. sort排序后相邻元素对比
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error!')
return
}
arr = arr.sort()
let res = []
// i从1开始算 arr[i-1]
for (let i = 0; i < arr.length; i++) {
if (arr[i] !== arr[i-1]) {
res.push(arr[i])
}
}
return res
}
// 5.obj.hasOwnProperty

// 测试
const arr = [1, 1, 2, 3, 3]
console.log(resetArr(arr)) // [1, 2, 3]
// https://github.com/mqyqingfeng/Blog/issues/27

数组扁平化 flatten

面试官连环追问:数组拍平(扁平化) flat 方法实现

1
2
var arr = [1, [2, [3, 4]]];
console.log(flatten(arr)) // [1, 2, 3, 4]

🔥循环递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 方法 1
var arr = [1, [2, [3, 4]]];

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
}


console.log(flatten(arr))

toString

如果数组元素都是Number, 可以使用;但不推荐这种 toString+split方式

1
2
3
[1, [2, [3, 4]]].toString() // "1,2,3,4"
// 场景却非常有限,如果数组是 [1, '1', 2, '2'] 此方法不适合
// 扁平化不改变原数据类型
1
2
3
4
5
6
7
8
9
10
// 方法2
var arr = [1, [2, [3, 4]]];

function flatten(arr) {
return arr.toString().split(',').map(function(item){
return +item //转为Number
})
}

console.log(flatten(arr))

🔥用reduce实现flat

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
// 方法3
var arr = [1, [2, [3, 4]]];

function flatten(arr) {
return arr.reduce(function(prev, next){
return prev.concat(Array.isArray(next) ? flatten(next) : next)
}, [])
}

// 简化
const flatten = (arr) => {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flatten(cur) : cur)
},[])
}

console.log(flatten(arr))

// 理解
// var arr = [2, [3, 4]];
// pre cur
// [] 2 0 [2]
// [2] [3,4] 1 [2] wait

// [] 3 0 [3]
// [3] 4 1 [3, 4]

// [2] [3,4] [2,3,4]

ES6 …

==[].concat(…arr)==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var arr = [1, [2, [3, 4]]];
console.log([].concat(...arr)); // [1, 2, [3, 4]]
// 这时,只可以扁平一层,顺着这个方法

// 方法4
var arr = [1, [2, [3, 4]]];

function flatten(arr) {

while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}

return arr;
}

console.log(flatten(arr))

🔥柯里化 curry

柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术

1
2
3
4
5
6
7
8
const curry = (fn, ...args) =>{
// 当参数与fn参数相同,则直接执行函数 fn.length 形参个数
if (args.length >= fn.length) return fn(...args)
// 否则返回函数,合并参数,并继续自动柯里化
return (...args2) => {
return curry(fn, ...args, ...args2);
}
}

函数组合 compose

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 // redux中的实现 不一定准确 ,待整理
// https://github.com/mqyqingfeng/Blog/issues/45
function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}

if (funcs.length === 1) {
return funcs[0]
}

return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

EventEmitter

https://juejin.cn/post/7031322059414175774#heading-16

千位分隔符

https://juejin.cn/post/6844903911686406158#heading-19

异步控制并发数

ES5继承(寄生组合继承)

参考

  1. 冴羽的博客
  2. 前端必刷手写题系列 [22]
其他文章