首页 > 编程语言 > 详细

JavaScript 手写常用代码

时间:2020-10-07 21:54:07      阅读:39      评论:0      收藏:0      [点我收藏+]

手写防抖

防抖,即短时间内大量触发同一事件,只会执行一次函数,实现原理为设置一个定时器,约定在xx毫秒后再触发事件处理,每次触发事件都会重新设置计时器,直到xx毫秒内无第二次操作,防抖常用于搜索框/滚动条的监听事件处理,如果不做防抖,每输入一个字/滚动屏幕,都会触发事件处理,造成性能浪费。

分解需求:

  1. 持续触发不执行
  2. 不触发一段时间再执行

细节处理:

  1. this的指向
  2. 子函数的参数传递,如event对象
function debounce(func, wait){
  let timeout
  return function(){
    let context = this
    let args = arguments
    
    clearTimeout(timeout)
    timeout = setTimeout(function(){
      func.apply(this,args)
    },wait)
  }
}

手写节流

防抖是延迟执行,而节流是间隔执行,函数节流即每隔一段时间就执行一次,和防抖的区别在于,防抖每次触发事件都重置定时器,而节流在定时器到时间后再清空定时器。目前有两种方式实现节流,一种是使用时间戳另一种是使用定时器

使用时间戳

使用时间戳,当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。

function throttle(func,awit){
  let context,args
  let previous = 0
  return function(){
    context = this
    args = arguments
    let now = +new Date()
    //判断当前时间-之前时间如果大于时间周期,则执行
    if(now - previous > awit){
      func.apply(context,args)
      previous = now
    }
  }
}

这种方法是

  1. 事件首次触发就会执行

  2. 事件停止后会立刻停止执行

使用定时器

当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。

function throttle(func,wait){
  let context,args,timeout
  return function(){
    context = this
   	args = arguments
    if(!timeout){
      timeout = setTimeout(function(){
        timeout = null
        func.apply(context,args)
      },wait)
    }
  }
}

这种方法是

  1. 事件首次触发后并不会立即执行
  2. 事件停止后不会立刻停止执行,会等最后一次执行完

时间戳与定时器的混合

由于这两种方法会有不一样的效果,我们可以将两者混合一起使用,这样会得到两者共同的特点

首次触发会执行,并且也会有最后一次执行

function throttle(func,wait){
  let context,args,timeout
  let previous = 0
  //定时器延迟执行的函数
  let later = function(){
    previous = +new Date()
    timeout = null
    func.apply(context,args)
  }
  let throttled function(){
    context = this
    args = arguments
    let now = +new Date()
    let remaing = wait - (now - previous)
    //判断是否有剩余时间,也就是判断是否是首次触发和是否还有剩余时间
    if(remaing <= 0){
      if(timeout){
        clearTimeout(timeout)
        timeout = null
      }
      func.apply(context,args)
  		previous = +new Date()
    }
    //判断有剩余时间,再判断是否有定时器,如果没有则设置定时器,也就是最后一次执行
    else if(!timeout){
      timeout = setTimeout(later,remaing)
    }
  }
  return throttled
}

手写callapplybind

实现call

先上终版实现代码:

//在函数对象原型链上增加mycall属性
Function.prototype.mycall = function(context){
  var context = context || window;    //判断传过来的对象是否为空,为空则指向全局执行上下文
  context.fn = this 									//将调用者赋给 context 的一个属性
  
  var args = []											  //定义一个用来存放传过来参数的类数组对象
  for(let i=1;i<arguments.length;i++){//将类数组对象arguments除第一个外其他放进数组里
    args.push(arguments[i])
  }
  
  var result=context.fn(...args)			//执行调用者函数,并接收返回参数
  
  delete context.fn										//删除调用者的函数
  return result 											//返回结果
}

call实现了什么

举个例子:

var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call(foo); // 1

注意两点:

  1. call 改变了 this 的指向,指向到 foo
  2. bar 函数执行了

模拟实现思路

那么我们该怎么模拟实现这两个效果呢?

试想当调用 call 的时候,把 foo 对象改造成如下:

var foo = {
    value: 1,
    bar: function() {
        console.log(this.value)
    }
};

foo.bar(); // 1

这个时候 this 就指向了 foo,是不是很简单呢?

但是这样却给 foo 对象本身添加了一个属性,这可不行呐!

不过也不用担心,我们用 delete 再删除它不就好了~

所以我们模拟的步骤可以分为:

  1. 将函数设为对象的属性
  2. 执行该函数
  3. 删除该函数

以上个例子为例,就是:

// 第一步
foo.fn = bar
// 第二步
foo.fn()
// 第三步
delete foo.fn

fn 是对象的属性名,反正最后也要删除它,所以起成什么都无所谓。

实现apply

applycall并没有太多不同,只是在参数方面,call是一个一个传参,而apply是多个参数的数组传参(或者类数组对象)。

终版代码:

Function.prototype.myCall = function(context = window, ...args) {
  
  let fn = Symbol("fn");
  context[fn] = this;

  let res = context[fn](...args);//重点代码,利用this指向,相当于context.caller(...args)

  delete context[fn];
  return res;
}

实现bind

最终代码:

Function.prototype.mybind = function(context){
  if(typeof this !==‘function‘){
    throw new TypeError(‘Errror‘)
  }
  const _this = this;
  const args = [...arguments].slice(1);
  return function F(){
    return  res  = this instanceof F ?  new _this(...args,...arguments) 
    :  _this.apply(context,args.concat(...arguments))
  }
}

bind实现了什么

bindcallapply同为更改this指向的方法,但bind同时也需要执行以下的任务:

  1. 改变this指向

  2. 由于需要延迟执行,需要返回一个函数

  3. 参数传入可分两次传入

  4. 当返回的函数作为构造器时,需要使的原有的this失效而让this返回指向实例

  5. 需要返回的函数原型与调用相同

new的实现

我们看new都做了什么:

  1. 创建一个新对象,并继承其构造函数的prototype,这一步是为了继承构造函数原型上的属性和方法

  2. 执行构造函数,方法内的this被指定为该新实例,这一步是为了执行构造函数内的赋值操作

  3. 返回新对象(规范规定,如果构造方法返回了一个对象,那么返回该对象,否则返回第一步创建的新对象)

上代码:

function objectFactory() {
		//使用一个新的对象,用于接收原型并返回
    var obj = new Object(),
		//将第一个参数(也就是构造函数)进行接收
    Constructor = [].shift.call(arguments);
		//将原型赋给新对象的_proto_
    obj.__proto__ = Constructor.prototype;
		//利用构造函数继承将父函数的属性借调给子函数
    var ret = Constructor.apply(obj, arguments);
		//如果构造函数已经返回对象则返回他的对象
  	//如果构造函数未返回对象,则返回我们的新对象
    return ret instanceof Object ? ret : obj;
};

数组去重

双重循环

方法比较繁琐点,但兼容性好点,不失为一种方法。

const unique = function(arr){
    let newarr = []
    let isrepeat
    for(let i =0;i<arr.length;i++){
        isrepeat=false
        for(let j=0;j<newarr.length;j++){
            if(arr[i] === newarr[j]){
                isrepeat=true
                break
            }
        }
        if(!isrepeat) newarr.push(arr[i])
    }
    return newarr
}

indexOf() + filter()

基本思路:如果索引不是第一个索引,说明是重复值。

const unique = function(arr){
    let res
    return res = arr.filter((item,index) => {
        return arr.indexOf(item) === index
    })
    return res 
}

Map

得益于Map的数据结构,查询速度极快,所以所消耗时间也极少

const unique = function(arr){
    const newarr = []
    const map = new Map()
    for(let i =0;i<arr.length;i++){
        if1(!map.get(arr[i])){
            map.set(arr[i],1)
            newarr.push(arr[i])
        }
    }
    return newarr
}

Set

甚至可以一行代码实现

const unique = function(arr){
    return [...new Set(arr)]
}

扁平化

对于[1, [1,2], [1,2,3]]这样多层嵌套的数组,我们如何将其扁平化为[1, 1, 2, 1, 2, 3]这样的一维数组呢:

单纯递归

对于这种树状结构,最方便的方式就是用递归

function flatten(arr) {
    let res = []
    for(let i =0;i<arr.length;i++){
        if(Array.isArray(arr[i])){
            res=res.concat(flatten(arr[i]))
        }else{
            res.push(arr[i])
        }
    }
    return res
}

reduce + 递归

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

ES6的flat()

const arr = [1, [1,2], [1,2,3]]
arr.flat(Infinity)  // [1, 1, 2, 1, 2, 3]

深浅拷贝

深浅拷发生在JavaScript的引用数据类型中。

浅拷贝

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

以下几个方法都可以实现浅拷贝

  • concat()
  • slice()
  • Object.assign :const returnedTarget = Object.assign(target, source);
  • ...展开运算符

以上方法都是实现浅拷贝的方法,他们对于首层元素都会一一复制属性,但是如果是多层引用的话,也只会复制地址,不会复制值。

以下用concat()做示例

const originArray = [1,[1,2,3],{a:1}];
const cloneArray = originArray.concat();
console.log(cloneArray === originArray); // false
cloneArray[1].push(4);
cloneArray[2].a = 2; 
cloneArray.push(6); 
console.log(originArray); // [1,[1,2,3,4],{a:2}]
console.log(cloneArray)  // [1,[1,2,3,4],{a:2},[6]]

深拷贝

深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。

实现深拷贝的方法有两种:

  1. 利用JSON对象中的parsestringify
  2. 利用递归来实现每一层重新创建对象并赋值

JSON.stringify/parse方法

JSON.stringify :是将一个 JavaScript 值转成一个 JSON 字符串。

JSON.parse :是将一个 JSON 字符串转成一个 JavaScript 值或对象。

const originArray = [1,2,3,4,5];
const cloneArray = JSON.parse(JSON.stringify(originArray));
console.log(cloneArray === originArray); // false

const originObj = {a:‘a‘,b:‘b‘,c:[1,2,3],d:{dd:‘dd‘}};
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj === originObj); // false

cloneObj.a = ‘aa‘;
cloneObj.c = [1,1,1];
cloneObj.d.dd = ‘doubled‘;

console.log(cloneObj); // {a:‘aa‘,b:‘b‘,c:[1,1,1],d:{dd:‘doubled‘}};
console.log(originObj); // {a:‘a‘,b:‘b‘,c:[1,2,3],d:{dd:‘dd‘}};

确实实现深拷贝,但是却也是有着缺陷:

该方法转换时undefinedfunctionsymbol 会在转换过程中被忽略。

递归方法

递归的思想就很简单了,就是对每一层的数据都实现一次 创建对象->对象赋值 的操作

function cloneDeep(source,hash = new WeakMap()){
    if(! typeof source === ‘object‘) return source
  	//如果hash 中存在就直接返回,避免循环引用
    if(hash.has(source)) return hash.get(source)
    const targetObj = Array.isArray(source) ? [] : {}
    //在 hash 存储复制的对象
    hash.set(source,targetObj)
  	//将循环复制对象里的属性
    for(let key in source){
      	//对原型上的属性不进行处理
        if(source.hasOwnProperty(key)){
          //判断 遍历的是不是一个对象
            if(source[key] && typeof source[key] === ‘object‘){
                // targetObj[keyt] = Array.isArray(source[key]) ? [] : {}
								//递归深拷贝
                targetObj[key] = cloneDeep(source[key],hash)
            }else{
                targetObj[key] = source[key]
            }
        }
    }
    return targetObj
}

setTimeout模拟实现setInterval

    //主要使用递归的方式进行模拟
		let  i =0
    function  newSetTime(func,mine){
      function insed(){
        i++
        func()
        setTimeout(insed,mine)
      }
      setTimeout(insed,mine)

    }
    function like(){
      console.log(i)
    }
    newSetTime(like,1000)

判断数据类型

function getType(obj){
  if(obj === null) return obj;
  return typeof obj == ‘object‘ ? Object.prototype.toString.call(obj).replace(‘[object ‘,‘‘).replace(‘]‘,‘‘).toLowerCase():typeof obj;
}

柯里化

function curry(fn, args) {
    var length = fn.length;
    var args = args || [];
    return function(){
        newArgs = args.concat(Array.prototype.slice.call(arguments));
        if (newArgs.length < length) {
            return curry.call(this,fn,newArgs);
        }else{
            return fn.apply(this,newArgs);
        }
    }
}

function multiFn(a, b, c) {
    return a * b * c;
}

var multi = curry(multiFn);

multi(2)(3)(4);
multi(2,3,4);
multi(2)(3,4);
multi(2,3)(4)

实现多参数的链式调用

function add() {
  let args = [].slice.call(arguments);
  
  let fn = function(){
   let fn_args = [].slice.call(arguments)
   return add.apply(null,args.concat(fn_args))
  }
  
  fn.toString = function(){
    return args.reduce((a,b)=>a+b)
  }
  
  return fn
}
add(1); 			// 1
add(1)(2);  	// 3
add(1)(2)(3);// 6
console.log(add(1)(2, 3)(4)); // 6
add(1, 2)(3); // 6
add(1, 2, 3); // 6

洗牌算法

const arr = [1,2,3,4,5,6,7,8,9,10];
const shuffle = ([...arr]) => {
  let m = arr.length;
  while (m) {
    const i = Math.floor(Math.random() * m--);
    [arr[m], arr[i]] = [arr[i], arr[m]];
  }
  return arr;
};
console.log(shuffle(arr))
// [10, 9, 7, 5, 6, 4, 1, 2, 8, 3]

搬运文章

【进阶4-3期】面试题之如何实现一个深拷贝

JavaScript基础心法--深浅拷贝

带你彻底搞清楚深拷、浅拷贝、循环引用

JavaScript 手写常用代码

原文:https://www.cnblogs.com/chuncode/p/13489625.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!