乐趣区

关于javascript:精简版js高频手写

数组的 map 办法

      Array.prototype.myMap = function(fn, thisValue) {let res = []
            thisValue = thisValue||[]
            let arr = this
            for(let i=0; i<arr.length; i++) {res.push(fn.call(thisValue, arr[i],i,arr))
            }
            return res
        }
        
// 2.reduce 实现
Array.prototype.myMap = function(fn,thisValue){var res = [];
     thisValue = thisValue||[];
     this.reduce(function(pre,cur,index,arr){return res.push(fn.call(thisValue,cur,index,arr));
     },[]);
     return res;
}

手写 reduce

 function reduce(arr, cb, initialValue){var num = initValue == undefined? num = arr[0]: initValue;
     var i = initValue == undefined? 1: 0
     for (i; i< arr.length; i++){num = cb(num,arr[i],i)
     }
     return num
 }
 
 function fn(result, currentValue, index){return result + currentValue}
 var arr = [2,3,4,5]
 var b = reduce(arr, fn,10) 
 console.log(b)   // 24

数组扁平化

1. es6 提供的新办法 flat(depth)

let a = [1,[2,3]]; 
a.flat(); // [1,2,3] 
a.flat(1); //[1,2,3]

无需晓得数组的维度,depth 的值设置为 Infinity。

let a = [1,[2,3,[4,[5]]]]; 
a.flat(Infinity); // [1,2,3,4,5]  a 是 4 维数组

2. 利用 cancat

function flatten(arr) {var res = [];
     for (let i = 0, length = arr.length; i < length; i++) {if (Array.isArray(arr[i])) {res = res.concat(flatten(arr[i])); //concat 并不会扭转原数组
     //res.push(...flatten(arr[i])); // 或者用扩大运算符 
     } else {res.push(arr[i]);
       }
     }
     return res;
 }
 let arr1 = [1, 2,[3,1],[2,3,4,[2,3,4]]]
flatten(arr1); //[1, 2, 3, 1, 2, 3, 4, 2, 3, 4]

函数柯里化

柯里化的定义:接管一部分参数,返回一个函数接管残余参数,接管足够参数后,执行原函数。

/**
 * 将函数柯里化
 * @param fn    待柯里化的原函数
 * @param len   所需的参数个数,默认为原函数的形参个数
 */
function curry(fn,len = fn.length) {return _curry.call(this,fn,len)
}
​
/**
 * 直达函数
 * @param fn    待柯里化的原函数
 * @param len   所需的参数个数
 * @param args  已接管的参数列表
 */
function _curry(fn,len,...args) {return function (...params) {let _args = [...args,...params];
         if(_args.length >= len){return fn.apply(this,_args);
         }else{return _curry.call(this,fn,len,..._args)
         }
    }
}

咱们来验证一下:

let _fn = curry(function(a,b,c,d,e){console.log(a,b,c,d,e)
});
​
_fn(1,2,3,4,5);     // print: 1,2,3,4,5
_fn(1)(2)(3,4,5);   // print: 1,2,3,4,5
_fn(1,2)(3,4)(5);   // print: 1,2,3,4,5
_fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5

手写 call

Function.prototype.myCall=function(context=window){  // 函数的办法,所以写在 Fuction 原型对象上
 if(typeof this !=="function"){   // 这里 if 其实没必要,会主动抛出谬误
    throw new Error("不是函数")
 }
 const obj=context||window   // 这里可用 ES6 办法,为参数增加默认值,js 严格模式全局作用域 this 为 undefined
 obj.fn=this      //this 为调用的上下文,this 此处为函数,将这个函数作为 obj 的办法
 const arg=[...arguments].slice(1)   // 第一个为 obj 所以删除, 伪数组转为数组
 res=obj.fn(...arg)
 delete obj.fn   // 不删除会导致 context 属性越来越多
 return res
}
// 用法:f.call(obj,arg1)
function f(a,b){console.log(a+b)
 console.log(this.name)
}
let obj={name:1}
f.myCall(obj,1,2) // 否则 this 指向 window
obj.greet.call({name: 'Spike'}) // 打进去的是 Spike

手写 apply(arguments[this, [参数 1,参数 2 …..] ])

Function.prototype.myApply=function(context){  // 箭头函数从不具备参数对象!!!!!这里不能写成箭头函数
 let obj=context||window
 obj.fn=this
 const arg=arguments[1]||[]    // 若有参数,失去的是数组
 let res=obj.fn(...arg)
 delete obj.fn
 return res
} 
function f(a,b){console.log(a,b)
 console.log(this.name)
}
let obj={name:'张三'}
f.myApply(obj,[1,2])  //arguments[1]

手写 bind

this.value = 2
var foo = {value: 1};
var bar = function(name, age, school){console.log(name) // 'An'
 console.log(age) // 22
 console.log(school) // '家里蹲大学'
}
var result = bar.bind(foo, 'An') // 预置了局部参数 'An'
result(22, '家里蹲大学') // 这个参数会和预置的参数合并到一起放入 bar 中

简略版本

Function.prototype.bind = function(context, ...outerArgs) {
 var fn = this;
 return function(...innerArgs) {   // 返回了一个函数,...rest 为理论调用时传入的参数
 return fn.apply(context,[...outerArgs, ...innerArgs]);  // 返回扭转了 this 的函数,// 参数合并
 }
}

new 失败的起因:

例:

// 申明一个上下文
let thovino = {name: 'thovino'}
​
// 申明一个构造函数
let eat = function (food) {
 this.food = food
 console.log(`${this.name} eat ${this.food}`)
}
eat.prototype.sayFuncName = function () {console.log('func name : eat')
}
​
// bind 一下
let thovinoEat = eat.bind(thovino)
let instance = new thovinoEat('orange')  // 实际上 orange 放到了 thovino 外面
console.log('instance:', instance) // {}

生成的实例是个空对象

new 操作符执行时,咱们的 thovinoEat 函数能够看作是这样:

function thovinoEat (...innerArgs) {eat.call(thovino, ...outerArgs, ...innerArgs)
}

在 new 操作符进行到第三步的操作 thovinoEat.call(obj, ...args) 时,这里的 obj 是 new 操作符本人创立的那个简略空对象 {},但它其实并没有替换掉thovinoEat 函数外部的那个上下文对象 thovino。这曾经超出了call 的能力范畴,因为这个时候要替换的曾经不是 thovinoEat 函数外部的 this 指向,而应该是 thovino 对象。

换句话说,咱们心愿的是 new 操作符将 eat 内的 this 指向操作符本人创立的那个空对象。然而实际上指向了 thovinonew 操作符的第三步动作并没有胜利

可 new 可继承版本

Function.prototype.bind = function (context, ...outerArgs) {
 let that = this;
​
function res (...innerArgs) {if (this instanceof res) {
         // new 操作符执行时
         // 这里的 this 在 new 操作符第三步操作时,会指向 new 本身创立的那个简略空对象{}
         that.call(this, ...outerArgs, ...innerArgs)
     } else {
         // 一般 bind
         that.call(context, ...outerArgs, ...innerArgs)
     }
     }
     res.prototype = this.prototype //!!!return res
}

手动实现 new

  1. 创立一个空对象 obj;
  2. 将空对象的隐式原型(proto)指向构造函数的 prototype。
  3. 应用 call 扭转 this 的指向
  4. 如果无返回值或者返回一个非对象值,则将 obj 返回作为新对象;如果返回值是一个新对象的话那么间接间接返回该对象。
function Person(name,age){
 this.name=name
 this.age=age
}
Person.prototype.sayHi=function(){console.log('Hi!我是'+this.name)
}
let p1=new Person('张三',18)
​
//// 手动实现 new
function create(){let obj={}
 // 获取构造函数
 let fn=[].shift.call(arguments)  // 将 arguments 对象提出来转化为数组,arguments 并不是数组而是对象!!!这种办法删除了 arguments 数组的第一个元素,!!这里的空数组外面填不填元素都没关系,不影响 arguments 的后果      或者 let arg = [].slice.call(arguments,1)
 obj.__proto__=fn.prototype
 let res=fn.apply(obj,arguments)    // 扭转 this 指向,为实例增加办法和属性
 // 确保返回的是一个对象(万一 fn 不是构造函数)
 return typeof res==='object'?res:obj
}
​
let p2=create(Person,'李四',19)
p2.sayHi()

细节:

[].shift.call(arguments)  也可写成:let arg=[...arguments]
 let fn=arg.shift()  // 使得 arguments 能调用数组办法, 第一个参数为构造函数
 obj.__proto__=fn.prototype
 // 扭转 this 指向,为实例增加办法和属性
 let res=fn.apply(obj,arg)

手写 promise(常考 promise.all, promise.race)

const STATUS = {
 PENDING: 'pending',
 FULFILLED: 'fulfilled',
 REJECTED: 'rejected'
}

class MyPromise {
 // 构造函数接管一个执行回调
 constructor(executor) {
     this._status = STATUS.PENDING // Promise 初始状态
     this._value = undefined // then 回调的值
     this._resolveQueue = [] // resolve 时触发的胜利队列
     this._rejectQueue = [] // reject 时触发的失败队列
    ​
 // 应用箭头函数固定 this(resolve 函数在 executor 中触发,不然找不到 this)const resolve = value => {const run = () => {if (this._status === STATUS.PENDING) {
             this._status = STATUS.FULFILLED // 更改状态
             this._value = value // 贮存以后值,用于 then 回调
            ​
             // 执行 resolve 回调
             while (this._resolveQueue.length) {const callback = this._resolveQueue.shift()
                 callback(value)
             }
         }
     }
     // 把 resolve 执行回调的操作封装成一个函数, 放进 setTimeout 里, 以实现 promise 异步调用的个性(标准上是微工作,这里是宏工作)setTimeout(run)
 }
​
 // 同 resolve
 const reject = value => {const run = () => {if (this._status === STATUS.PENDING) {
         this._status = STATUS.REJECTED
         this._value = value
        ​
         while (this._rejectQueue.length) {const callback = this._rejectQueue.shift()
             callback(value)
         }
     }
 }
     setTimeout(run)
 }

     // new Promise()时立刻执行 executor, 并传入 resolve 和 reject
     executor(resolve, reject)
 }
​
 // then 办法, 接管一个胜利的回调和一个失败的回调
 function then(onFulfilled, onRejected) {
  // 依据标准,如果 then 的参数不是 function,则疏忽它, 让值持续往下传递,链式调用持续往下执行
  typeof onFulfilled !== 'function' ? onFulfilled = value => value : null
  typeof onRejected !== 'function' ? onRejected = error => error : null

  // then 返回一个新的 promise
  return new MyPromise((resolve, reject) => {
    const resolveFn = value => {
      try {const x = onFulfilled(value)
        // 分类探讨返回值, 如果是 Promise, 那么期待 Promise 状态变更, 否则间接 resolve
        x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
      } catch (error) {reject(error)
      }
    }
  }
}
​
  const rejectFn = error => {
      try {const x = onRejected(error)
        x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
      } catch (error) {reject(error)
      }
    }
    switch (this._status) {
      case STATUS.PENDING:
        this._resolveQueue.push(resolveFn)
        this._rejectQueue.push(rejectFn)
        break;
      case STATUS.FULFILLED:
        resolveFn(this._value)
        break;
      case STATUS.REJECTED:
        rejectFn(this._value)
        break;
    }
 })
 }
 catch (rejectFn) {return this.then(undefined, rejectFn)
}
// promise.finally 办法
finally(callback) {return this.then(value => MyPromise.resolve(callback()).then(() => value), error => {MyPromise.resolve(callback()).then(() => error)
  })
}
 

 // 动态 race 办法
 static race(promiseArr) {return new MyPromise((resolve, reject) => {
        promiseArr.forEach(p => {MyPromise.resolve(p).then(value => {resolve(value)
          }, error => {reject(error)
          })
        })
      })
    }
}
// 动态 all 办法
 static all(promiseArr) {
      let count = 0
      let result = []
      return new MyPromise((resolve, reject) =>   {if (!promiseArr.length) {return resolve(result)
        }
        promiseArr.forEach((p, i) => {MyPromise.resolve(p).then(value => {
            count++
            result[i] = value
            if (count === promiseArr.length) {resolve(result)
            }
          }, error => {reject(error)
          })
        })
      })
    }

11. 手写原生 AJAX

  1. 创立 XMLHttpRequest 实例
  2. 收回 HTTP 申请
  3. 服务器返回 XML 格局的字符串
  4. JS 解析 XML,并更新部分页面
    不过随着历史进程的推动,XML 曾经被淘汰,取而代之的是 JSON。
function ajax(url) {const p = new Promise((resolve, reject) => {let xhr = new XMLHttpRequest()
    xhr.open('get', url)
    xhr.onreadystatechange = () => {if (xhr.readyState == 4) {if (xhr.status >= 200 && xhr.status <= 300) {resolve(JSON.parse(xhr.responseText))
        } else {reject('申请出错')
        }
      }
    }
    xhr.send()  // 发送 hppt 申请})
  return p
}
let url = '/data.json'
ajax(url).then(res => console.log(res))
  .catch(reason => console.log(reason))

12. 手写节流防抖函数

函数节流与函数防抖都是为了限度函数的执行频次,是一种性能优化的计划,比方利用于 window 对象的 resize、scroll 事件,拖拽时的 mousemove 事件,文字输出、主动实现的 keyup 事件。

节流:间断触发事件然而在 n 秒中只执行一次函数

例:(连续不断动都须要调用时用,设一时间距离),像 dom 的拖拽,如果用消抖的话,就会呈现卡顿的感觉,因为只在进行的时候执行了一次,这个时候就应该用节流,在肯定工夫内屡次执行,会晦涩很多。

防抖:指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会从新计算函数执行工夫。

例:(连续不断触发时不调用,触发完后过一段时间调用),像仿百度搜寻,就应该用防抖,当我连续不断输出时,不会发送申请;当我一段时间内不输出了,才会发送一次申请;如果小于这段时间持续输出的话,工夫会从新计算,也不会发送申请。

防抖的实现:

function debounce(fn, delay) {if(typeof fn!=='function') {throw new TypeError('fn 不是函数')
     }
     let timer; // 保护一个 timer
     return function () {var _this = this; // 取 debounce 执行作用域的 this(原函数挂载到的对象)
         var args = arguments;
         if (timer) {clearTimeout(timer);
         }
         timer = setTimeout(function () {fn.apply(_this, args); // 用 apply 指向调用 debounce 的对象,相当于_this.fn(args);
         }, delay);
     };
}

// 调用​
input1.addEventListener('keyup', debounce(() => {console.log(input1.value)
}), 600)

节流的实现:

function throttle(fn, delay) {
  let timer;
  return function () {
    var _this = this;
    var args = arguments;
    if (timer) {return;}
    timer = setTimeout(function () {fn.apply(_this, args); // 这里 args 接管的是外边返回的函数的参数,不能用 arguments
      // fn.apply(_this, arguments); 须要留神:Chrome 14 以及 Internet Explorer 9 依然不承受类数组对象。如果传入类数组对象,它们会抛出异样。timer = null; // 在 delay 后执行完 fn 之后清空 timer,此时 timer 为假,throttle 触发能够进入计时器
    }, delay)
  }
}

div1.addEventListener('drag', throttle((e) => {console.log(e.offsetX, e.offsetY)
}, 100))

手写 Promise 加载图片

function getData(url) {return new Promise((resolve, reject) => {
    $.ajax({
      url,
      success(data) {resolve(data)
      },
      error(err) {reject(err)
      }
    })
  })
}
const url1 = './data1.json'
const url2 = './data2.json'
const url3 = './data3.json'
getData(url1).then(data1 => {console.log(data1)
  return getData(url2)
}).then(data2 => {console.log(data2)
  return getData(url3)
}).then(data3 =>
  console.log(data3)
).catch(err =>
  console.error(err)
)

14. 函数实现一秒钟输入一个数

ES6:用 let 块级作用域的原理实现

for(let i=0;i<=10;i++){   // 用 var 打印的都是 11
 setTimeout(()=>{console.log(i);
 },1000*i)
}

不必 let 的写法:原理是用立刻执行函数发明一个块级作用域

for(var i = 1; i <= 10; i++){(function (i) {setTimeout(function () {console.log(i);
        }, 1000 * i)
    })(i);
}

15. 创立 10 个标签,点击的时候弹出来对应的序号?

var a
for(let i=0;i<10;i++){a=document.createElement('a')
 a.innerHTML=i+'<br>'
 a.addEventListener('click',function(e){console.log(this)  //this 为以后点击的 <a>
     e.preventDefault()  // 如果调用这个办法,默认事件行为将不再触发。// 例如,在执行这个办法后,如果点击一个链接(a 标签),浏览器不会跳转到新的 URL 去了。咱们能够用 event.isDefaultPrevented() 来确定这个办法是否 (在那个事件对象上) 被调用过了。alert(i)
 })
 const d=document.querySelector('div')
 d.appendChild(a)  //append 向一个已存在的元素追加该元素。}

16. 实现事件订阅公布(eventBus)

实现 EventBus 类,有 on off once trigger 性能,别离对应绑定事件监听器,解绑,执行一次后解除事件绑定,触发事件监听器。这个题目面字节和快手都问到了,最近忙,答案会在后续更新

class EventBus {on(eventName, listener) {}
    off(eventName, listener) {}
    once(eventName, listener) {}
    trigger(eventName) {}}

const e = new EventBus();
// fn1 fn2
e.on('e1', fn1)
e.once('e1', fn2)
e.trigger('e1') // fn1() fn2()
e.trigger('e1') // fn1()
e.off('e1', fn1)
e.trigger('e1') // null

实现:

      // 申明类
      class EventBus {constructor() {this.eventList = {} // 创建对象收集事件
        }
        // 公布事件
        $on(eventName, fn) {
          // 判断是否公布过事件名称? 增加公布 : 创立并增加公布
          this.eventList[eventName]
            ? this.eventList[eventName].push(fn)
            : (this.eventList[eventName] = [fn])
        }
        // 订阅事件
        $emit(eventName) {if (!eventName) throw new Error('请传入事件名')
          // 获取订阅传参
          const data = [...arguments].slice(1)
          if (this.eventList[eventName]) {this.eventList[eventName].forEach((i) => {
              try {i(...data) // 轮询事件
              } catch (e) {console.error(e + 'eventName:' + eventName) // 收集执行时的报错
              }
            })
          }
        }
        // 执行一次
        $once(eventName, fn) {
          const _this = this
          function onceHandle() {fn.apply(null, arguments)
            _this.$off(eventName, onceHandle) // 执行胜利后勾销监听
          }
          this.$on(eventName, onceHandle)
        }
        // 勾销订阅
        $off(eventName, fn) {
          // 不传入参数时勾销全副订阅
          if (!arguments.length) {return (this.eventList = {})
          }
          //eventName 传入的是数组时, 勾销多个订阅
          if (Array.isArray(eventName)) {return eventName.forEach((event) => {this.$off(event, fn)
            })
          }
          // 不传入 fn 时勾销事件名下的所有队列
          if (arguments.length === 1 || !fn) {this.eventList[eventName] = []}
          // 勾销事件名下的 fn
          this.eventList[eventName] = this.eventList[eventName].filter((f) => f !== fn
          )
        }
      }
      const event = new EventBus()

      let b = function (v1, v2, v3) {console.log('b', v1, v2, v3)
      }
      let a = function () {console.log('a')
      }
      event.$once('test', a)
      event.$on('test', b)
      event.$emit('test', 1, 2, 3, 45, 123)

      event.$off(['test'], b)

      event.$emit('test', 1, 2, 3, 45, 123)
退出移动版