手写 JS 源码
前言
在面试过程中经常会有手写源码或者是某些 JS 代码的底层原理环节.同时我们也可以学习或者手写一些源码的实现,来加深对代码的理解,从而让我们在项目中能够更加得心应手的去使用.
JS API 的实现
call、apply、bind 的实现
call 和 apply 的实现利用了 this 的指向原理.所以我们可以先了解一下 this 指向的问题,从而实现该方法
this 指向问题
最精简的解释:this 指向调用函数时用的引用
JS 规定,当存在一个对象调用函数时,那么该函数的 this 指向的就是该对象 如果一个函数不存在对象调用它,那么该函数的 this 指向的就是全局的 window
call 实现
基于上面的原理,我们就可以改变函数的 this 指向了
Object.prototype.myCall=function(context,...args){
//判断是否为object或者function
if(typeof context=='object'|| typeof context=='function'){
context = context||window
}else{
//否则,创建一个空的object
context= Object.create(null)
}
//将当前函数挂载到context属性上
context.__fn__ = this
//执行该函数,在这里,因为context调用了该函数,所以该函数的this指向的就是传入的context
const res = context.__fn__(...args)
//删除挂载的属性
delete context.__fn__
return res
}
apply 实现
apply 和 call 的实现原理是相同的,不同的点在于 apply 接受一个数组作为我们的参数
Object.prototype.myApply=function(context,args){
if(typeof context=='object'||typeof context=='function'){
context = context||window
}else{
context = Object.create(null)
}
context.__fn__ =this
const res = context.__fn__(...args)
delete context.__fn__
return res
}
bind 的实现
bind 的和 call 的差别在于它不是一个立即执行的函数,而是返回一个闭包函数
Object.prototype.myBind = function(context,...args1){
return (...args2)=>{
return this.myCall(context,...args1,...args2)
}
}
手写 Promise
关于手写 Promise,有个大神介绍的很详细.大家可以参考一下
new 的源码实现
关键字 new 主要是做了三件事情
- 根据我们的构造函数的原型生成一个临时对象
- 执行构造函数并传入 this 和相关参数
- 如果该构造函数返回一个对象我们就返回生成的对象,否则返回我们创建的临时对象
function myNew(fn,...args){
const obj = {}
Object.setPrototypeOf(obj,fn.prototype)
const res = fn.call(obj,...args)
return res instanceof Object?res:obj
}
Array.prototype.reduce 的实现
MDN 上的解释:reduce 方法对数组中的每个元素执行一个由您提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。 reducer 函数:reducer 函数是一个纯函数,通俗来讲就是当你给他提供一个相同的值时,不论执行多少次,它返回的值都是相同的,
reduce 函数接收两个参数,第一个参数就是 reducer、第二个参数就是初始值 reducer 函数接收四个参数,(累计器,当前值,当前索引,源数组)
Array.prototype.myReduce=function(callback,initVal){
//有初始值把初始值作为accumulator,否则将数组第一个数作为accumulator
const accumulator = initVal?initVal||this[0]
//存储源数组
let _this = this
for(let i = initVal?0:1;i<this.length;i++){
accumulator = callback(accumulator,this[i],i,_this)
}
return accumulator
}
Array.prototype.map
map 函数接受两个参数,第一个参数 callback 是生成新数组元素的函数,第二个参数是执行 callback 时被用作 this
Array.prototype.myMap=function(callback,thisArg){
return this.reduce(accumulator,curr,index,arr)=>{
accumulator.push(callback.call(thisArg,curr,index,arr))
return accumulator
},[])
}
数组扁平化
数组扁平化方案
let arr=[1,[1,[1,[1,[1]]]]]
//使用Array.prototype.flat
arr.flat(4)
//手动解析
function flating(arr){
while (arr.some(item=>Array.isArray(item))) {
arr = [].concat(...arr)
}
return arr
}
手动实现 Array.prototype.flat
引用 MDN 上对 flat 方法的描述:该方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回
Array.prototype.myFlat = function(num=1){
if(Array.isArray(this)){
}else{
throw this+'.flat is not a function'
}
}
手写一个 instanceof
instanceof 能够精确判断对象类型,但是只能用在对象上,但是 instanceof 只支持 Object 类型 所以我们可以根据对象的原型进行判断
function mInstanceof(cur,target){
//先解决不能满足条件的情况
if(typeof cur!=='object'||cur==null)return false
let p = cur.prototype
while(true){
//在原型链上判断
if(p ==target.prototype)return true
if(p.prototype==null)return false
p=Object.getPrototypeOf(p)
}
}
手动实现一个原型链继承
我们可以通过一个中间构造函数进行桥接,实现原型的赋值
function mExtend(child,parent){
const f=function(){}
f.prototype = parent.prototype
child.prototype = new f()
child.prototype.constructor = child
child.super = parent.prototype
return child
}
封装部分
手写函数聚合
函数的聚合简单来说,就是顺序执行一个函数数组.主要是利用了 reduce 的特性
const compose = function(fns){
let len = fns.length
//先判断传入的数组长度为0或者为1的情况
if(len ===0)return arg=>arg
if(len ===1)return fns[0]
return fns.reduce((a,b)=>(...args)=>b(a(...args)))
}
手动实现一个深拷贝
深拷贝最最简单的办法可以使用 JSON.parse(JSON.stringify(obj)),但是这种方法肯定不能令人满意. 因为 JS 中不同数据类型存储位置不同,我们在深拷贝中需要关注的就是对象和数组了
//这里利用了WeakMap的key可以存储对象的特性
function clone(target,map = new WeakMap()){
if(typeof target=='object'){
let cloneTarget = Array.isArray(target)?[]:{}
//处理相互引用的情况
if(map.has(target)){
return target
}
map.set(target,cloneTarget)
for(let key in target){
//递归拷贝
cloneTarget[key] = clone(target[key],map)
}
return cloneTarget
}else{
return target
}
}
手动实现一个 EventEmitter
EventEmitter 本质是一个发布订阅模式,其中维护了一个对象,对象中的 value 以数组的形式存储了我们注册的事件
class EventEmitter{
constructor(){
this.obj={}
}
//添加事件
on(type,callback){
this.obj[type]=this.obj[type]||[]
if(this.obj[type].indexOf(callback)==-1){
this.obj.push(callback)
}
return this
}
//派发事件
emit(type,...args){
let callbackList=this.obj[type]
callbackList.forEach(item=>item(...args))
}
//清除事件
off(type,callback){
let callbackList= this.obj[type]
if(Array.isArray(callbackList)){
if(callback){
//如果callback存在,清除指定事件
const index = callbackList.indexOf(callback)
if(index!==-1){
//如果该事件存在,则清除
callbackList.splice(index,1)
}
}else{
//否则清除所有事件
callbackList.length=0
}
}
return this
}
//注册的事件,执行一次后清除
once(type,callback){
const proxy =function(...args){
callback(...args)
this.off(type,proxy)
}
this.on(type,proxy)
return this
}
}
函数柯里化
函数柯里化的定义:把接受多个参数的函数变成一个接受单一参数的函数,当接受的参数数量不满足形参数量时,返回接受剩余参数的函数,否则返回处理结果
其原理简单来说就是利用了闭包函数存储中间参数,最后返回执行的结果
//基本实现
function curry(fns,...args1){
let arr=[]
return function(...args2){
arr=arr.concat[...args1,...args2]
if(arr.length!==fns.length){
return curry(fns,...arr)
}else{
return fns(...arr)
}
}
}
//最终实现
let currying = (fns,...args1)=>(...args2)=>(
(arg)=>fns.length==arg.length?fns(...arg):currying(fns,...arg)
)([...args1,...args2])
函数防抖
函数防抖可以简单理解为在 n 秒内只能触发一次,如果 n 秒内又触发了事件,那么计时器会被重制重新开始计时
//初次触发会立即执行该函数
function debounce(fn,wait){
let timer = null
return function(...args){
if(!timer)fn(...args)
clearTimeout(timer)
timer = setTimeout(() => {
timer=null
}, wait);
}
}
//初次触发等到计时完毕后再触发该函数
function debounce2(fn,wait){
let timer = null
return function(...args){
if(timer) clearTimeout(timer)
timer= setTimeout(()=>{
fn(...args)
},wait)
}
}
函数节流
函数节流可以理解为频繁的触发一个事件时,函数会按照固有的频率去执行,它的主要作用是稀释函数的执行频率. 可以用在屏幕滚动事件或者鼠标滑动事件等地方
function throttle(fn,wait){
let canRun =true
return function(...args){
if(!canRun) return
fn(...args)
canRun=false
setTimeout(()=>{
canRun=true
},wait)
}
}