初、中级前端应该要掌握的手写代码实现


初、中级前端应该要掌握的手写代码实现


又到了金三银四面试季了,想必很多同学已经跃跃欲试,提前开始准备面试了,本文就列举了面试过程中一些常见的手写代码实现供参考。或许很多人会问,这些手写代码实现意义何在,社区已经有很多poly-fill或者函数库供选择,何必要自己费力去折腾呢?我的理解是,在真实业务开发场景中,我们真的用不上这些自己写的方法,一个lodash库完全可以满足我们的需求,但此时你仅仅只是一个API Caller ,你经常使用到它,但对它实现原理却一无所知,哪怕它实现起来是非常简单的。所以亲自动手写出它的实现过程,对你理解其中原理是很有帮助的。另外,不要觉得用ES6语法,或者最新的语法去实现ES5甚至是ES3的方法是件可笑的事情,相反,它更能体现出你对ES6语法的掌握程度以及对JS发展的关注度,在面试中说不定会成为你的一个亮点。

模拟call

  • 第一个参数为null或者undefined时,this指向全局对象window,值为原始值的指向该原始值的自动包装对象,如 String、Number、Boolean
  • 为了避免函数名与上下文(context)的属性发生冲突,使用Symbol类型作为唯一值
  • 将函数作为传入的上下文(context)属性执行
  • 函数执行完成后删除该属性
  • 返回执行结果
<code>Function.prototype.myCall = function(context, ...args) {
context = (context ?? window) || new Object(context)
const key = Symbol()
context[key] = this
const result = context[key](...args)
delete context[key]
return result
}
复制代码/<code>

注: 代码实现使用了ES2020新特性Null判断符 ??, 详细参考阮一峰老师的ECMAScript 6 入门

模拟apply

  • 前部分与call一样
  • 第二个参数可以不传,但类型必须为数组或者类数组
<code>Function.prototype.myApply = function(context) {
context = (context ?? window) || new Object(context)
const key = Symbol()
const args = arguments[1]
context[key] = this
const result= args ? context[key](...args) : context[key]()
delete context[key]
return result
}
复制代码/<code>

注:代码实现存在缺陷,当第二个参数为类数组时,未作判断(有兴趣可查阅一下如何判断类数组)

模拟bind

  • 使用 call / apply 指定 this
  • 返回一个绑定函数
  • 当返回的绑定函数作为构造函数被new调用,绑定的上下文指向实例对象
  • 设置绑定函数的prototype 为原函数的prototype
<code>Function.prototype.myBind = function(context, ...args) {
const fn = this
const bindFn = function (...newFnArgs) {
return fn.call(
this instanceof bindFn ? this : context,
...args, ...newFnArgs
)
}
bindFn.prototype = Object.create(fn.prototype)
return bindFn
}
复制代码/<code>

模拟new

  • 创建一个新的空对象
  • 把this绑定到空对象
  • 使空对象的__proto__指向构造函数的原型(prototype)
  • 执行构造函数,为空对象添加属性
  • 判断构造函数的返回值是否为对象,如果是对象,就使用构造函数的返回值,否则返回创建的对象
<code>const createNew = (Con, ...args) => {
const obj = {}
Object.setPrototypeOf(obj, Con.prototype)
let result = Con.apply(obj, args)
return result instanceof Object ? result : obj
}
复制代码/<code>

模拟instanceof

  • 遍历左边变量的原型链,直到找到右边变量的 prototype,如果没有找到,返回 false
<code>const myInstanceOf = (left, right) => {
let leftValue = left.__proto__
let rightValue = right.prototype
while(true) {
if(leftValue === null) return false
if(leftValue === rightValue) return true
leftValue = leftValue.__proto__
}
}
复制代码/<code>

深拷贝(简单版)

  • 判断类型是否为原始类型,如果是,无需拷贝,直接返回
  • 为避免出现循环引用,拷贝对象时先判断存储空间中是否存在当前对象,如果有就直接返回
  • 开辟一个存储空间,来存储当前对象和拷贝对象的对应关系
  • 对引用类型递归拷贝直到属性为原始类型
<code>const deepClone = (target, cache = new WeakMap()) => {
if(target === null || typeof target !== 'object') {
return target
}
if(cache.get(target)) {
return target
}
const copy = Array.isArray(target) ? [] : {}
cache.set(target, copy)
Object.keys(target).forEach(key => copy[key] = deepClone(target[key], cache))
return copy
}
复制代码/<code>

缺点:无法拷贝函数、Map、Set、正则等其他类型

深拷贝(尤雨溪版)

vuex源码

  • 原理与上一版类似
<code>function find(list, f) {
return list.filter(f)[0]
}

function deepCopy(obj, cache = []) {
// just return if obj is immutable value
if (obj === null || typeof obj !== 'object') {
return obj
}

// if obj is hit, it is in circular structure
const hit = find(cache, c => c.original === obj)
if (hit) {
return hit.copy
}

const copy = Array.isArray(obj) ? [] : {}
// put the copy into cache at first
// because we want to refer it in recursive deepCopy
cache.push({

original: obj,
copy
})
Object.keys(obj).forEach(key => copy[key] = deepCopy(obj[key], cache))

return copy
}
复制代码/<code>

深拷贝(复杂版)

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

<code>const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag = '[object Arguments]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];


function forEach(array, iteratee) {
let index = -1;
const length = array.length;
while (++index < length) {
iteratee(array[index], index);
}
return array;
}

function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}

function getType(target) {
return Object.prototype.toString.call(target);
}


function getInit(target) {
const Ctor = target.constructor;
return new Ctor();
}

function cloneSymbol(targe) {
return Object(Symbol.prototype.valueOf.call(targe));
}

function cloneReg(targe) {
const reFlags = /\\w*$/;
const result = new targe.constructor(targe.source, reFlags.exec(targe));
result.lastIndex = targe.lastIndex;
return result;
}

function cloneFunction(func) {
const bodyReg = /(?<={)(.|\\n)+(?=})/m;
const paramReg = /(?<=\\().+(?=\\)\\s+{)/;
const funcString = func.toString();
if (func.prototype) {
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if (body) {
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}

function cloneOtherType(targe, type) {
const Ctor = targe.constructor;
switch (type) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(targe);
case regexpTag:
return cloneReg(targe);

case symbolTag:
return cloneSymbol(targe);
case funcTag:
return cloneFunction(targe);
default:
return null;
}
}

function clone(target, map = new WeakMap()) {

// 克隆原始类型
if (!isObject(target)) {
return target;
}

// 初始化
const type = getType(target);
let cloneTarget;
if (deepTag.includes(type)) {
cloneTarget = getInit(target, type);
} else {
return cloneOtherType(target, type);
}

// 防止循环引用
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);

// 克隆set
if (type === setTag) {
target.forEach(value => {
cloneTarget.add(clone(value, map));
});
return cloneTarget;
}

// 克隆map
if (type === mapTag) {
target.forEach((value, key) => {
cloneTarget.set(key, clone(value, map));
});
return cloneTarget;
}

// 克隆对象和数组

const keys = type === arrayTag ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone(target[key], map);
});

return cloneTarget;
}
复制代码/<code>

深拷贝(高性能版)

头条面试官:你知道如何实现高性能版本的深拷贝嘛?

<code>const MY_IMMER = Symbol('my-immer1')

const isPlainObject = value => {
if (
!value ||
typeof value !== 'object' ||
{}.toString.call(value) != '[object Object]'
) {
return false
}
var proto = Object.getPrototypeOf(value)
if (proto === null) {
return true
}
var Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor
return (
typeof Ctor == 'function' &&
Ctor instanceof Ctor &&
Function.prototype.toString.call(Ctor) ===
Function.prototype.toString.call(Object)
)
}

const isProxy = value => !!value && !!value[MY_IMMER]

function produce(baseState, fn) {
const proxies = new Map()
const copies = new Map()

const objectTraps = {
get(target, key) {

if (key === MY_IMMER) return target
const data = copies.get(target) || target
return getProxy(data[key])
},
set(target, key, val) {
const copy = getCopy(target)
const newValue = getProxy(val)
// 这里的判断用于拿 proxy 的 target
// 否则直接 copy[key] = newValue 的话外部拿到的对象是个 proxy
copy[key] = isProxy(newValue) ? newValue[MY_IMMER] : newValue
return true
}
}

const getProxy = data => {
if (isProxy(data)) {
return data
}
if (isPlainObject(data) || Array.isArray(data)) {
if (proxies.has(data)) {
return proxies.get(data)
}
const proxy = new Proxy(data, objectTraps)
proxies.set(data, proxy)
return proxy
}
return data
}

const getCopy = data => {
if (copies.has(data)) {
return copies.get(data)
}
const copy = Array.isArray(data) ? data.slice() : { ...data }
copies.set(data, copy)
return copy
}

const isChange = data => {
if (proxies.has(data) || copies.has(data)) return true
}

const finalize = data => {
if (isPlainObject(data) || Array.isArray(data)) {
if (!isChange(data)) {
return data
}
const copy = getCopy(data)

Object.keys(copy).forEach(key => {
copy[key] = finalize(copy[key])
})
return copy
}
return data
}

const proxy = getProxy(baseState)
fn(proxy)
return finalize(baseState)
}
复制代码/<code>

函数防抖

函数防抖是在事件被触发n秒后再执行回调,如果在n秒内又被触发,则重新计时。 函数防抖多用于input输入框

  • 箭头函数的this继承自父级上下文,这里指向触发事件的目标元素
  • 事件被触发时,传入event对象
  • 传入leading参数,判断是否可以立即执行回调函数,不必要等到事件停止触发后才开始执行
  • 回调函数可以有返回值,需要返回执行结果
<code> const debounce = (fn, wait = 300, leading = true) => {
let timerId, result
return function(...args) {
timerId && clearTimeout(timerId)
if (leading) {
if (!timerId) result = fn.apply(this, args)
timerId = setTimeout(() => timerId = null, wait)
} else {

timerId = setTimeout(() => result = fn.apply(this, args), wait)
}
return result
}
}
复制代码/<code>

函数节流(定时器)

函数节流是指连续触发事件,但是在 n 秒中只执行一次函数,适合应用于动画相关的场景

<code>const throttle = (fn, wait = 300) => {
let timerId
return function(...args) {
if(!timerId) {
timerId = setTimeout(() => {
timerId = null
return result = fn.apply(this, ...args)
}, wait)
}
}
}
复制代码/<code>

函数节流(时间戳)

<code>const throttle = (fn, wait = 300) => {
let prev = 0
let result
return function(...args) {
let now = +new Date()
if(now - prev > wait) {
prev = now
return result = fn.apply(this, ...args)
}
}
}
复制代码/<code>

函数节流实现方法区别

方法 使用时间戳 使用定时器 开始触发时 立刻执行 n秒后执行 停止触发后 不再执行事件 继续执行一次事件

函数节流(双剑合璧版)

<code> const throttle = (fn, wait = 300,  {
// 参数解构赋值
leading = true,
trailing = true,
} = {}) => {
let prev = 0
let timerId
const later = function(args) {
timerId && clearTimeout(timerId)
timerId = setTimeout(() => {
timerId = null
fn.apply(this, args)
}, wait)
}
return function (...args) {
let now = +new Date()
if(!leading) return later(args)
if(now - prev > wait) {
fn.apply(this, args)
prev = now
} else if(trailing) {
later(args)
}
}
}
复制代码/<code>

leading:false 表示禁用第一次执行

trailing: false 表示禁用停止触发的回调

注意:leading:false 和 trailing: false 不能同时设置。

数组去重

<code>const uniqBy = (arr, key) => {
return [...new Map(arr.map(item => [item[key], item])).values()]
}

const singers = [
{ id: 1, name: 'Leslie Cheung' },
{ id: 1, name: 'Leslie Cheung' },

{ id: 2, name: 'Eason Chan' },
]
console.log(uniqBy(singers, 'id'))

// [
// { id: 1, name: 'Leslie Cheung' },
// { id: 2, name: 'Eason Chan' },
// ]
复制代码/<code>

原理是利用Map的键不可重复

数组扁平化(技巧版)

利用 toString 把数组变成以逗号分隔的字符串,遍历数组把每一项再变回原来的类型。缺点:数组中元素必须是 Number类型,String类型会被转化成Number

<code>const str = [0, 1, [2, [3, 4]]].toString()
// '0, 1, 2, 3, 4'
const arr = str.split(',')
// ['0','1','2', '3', '4']
const newArr = arr.map(item => +item)
// [0, 1, 2, 3, 4]

const flatten = (arr) => arr.toString().split(',').map(item => +item)
复制代码/<code>

数组扁平化

reduce + 递归

<code>const flatten = (arr, deep = 1) => {
return arr.reduce((cur, next) => {
return Array.isArray(next) && deep > 1 ?
[...cur, ...flatten(next, deep - 1)] :
[...cur, next]
},[])
}

const arr = [1, [2], [3, [4]]]
flatten(arr, 1) // [1, [2], [3, [4]]]
flatten(arr, 2) // [1,2, [3, 4]]

flatten(arr, 3) // [1,2, 3, 4]


复制代码/<code>

函数柯里化

<code>const currying = fn =>
_curry = (...args) =>
args.length >= fn.length
? fn(...args)
: (...newArgs) => _curry(...args, ...newArgs)
复制代码/<code>

原理是利用闭包把传入参数保存起来,当传入参数的数量足够执行函数时,就开始执行函数

发布订阅EventEmitter

<code>class EventEmitter {
#subs = {}
emit(event, ...args) {
if (this.#subs[event] && this.#subs[event].length) {
this.#subs[event].forEach(cb => cb(...args))
}
}
on(event, cb) {
(this.#subs[event] || (this.#subs[event] = [])).push(cb)
}
off(event, offCb) {
if (offCb) {
if (this.#subs[event] && this.#subs[event].length)
this.#subs[event] = this.#subs[event].filter(cb => cb !== offCb)
} else {
this.#subs[event] = []
}
}
}
复制代码/<code>

subs是EventEmitter私有属性(最新特性参考阮一峰老师的ECMAScript 6 入门),通过on注册事件,off注销事件,emit触发事件

寄生组合继承

<code>  function Super(foo) {
this.foo = foo
}
Super.prototype.printFoo = function() {
console.log(this.foo)
}
function Sub(bar) {
this.bar = bar
Super.call(this)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
复制代码/<code>

ES6版继承

<code>  class Super {
constructor(foo) {
this.foo = foo
}
printFoo() {
console.log(this.foo)
}
}
class Sub extends Super {
constructor(foo, bar) {
super(foo)
this.bar = bar
}
}
复制代码/<code>

ES5的继承,实质是先创造子类的实例对象,然后将再将父类的方法添加到this上。 ES6的继承,先创造父类的实例对象(所以必须先调用super方法,然后再用子类的构造函数修改this


分享到:


相關文章: