1. 程式人生 > 實用技巧 >JavaScript 手寫常用程式碼

JavaScript 手寫常用程式碼

手寫防抖

防抖,即短時間內大量觸發同一事件,只會執行一次函式,實現原理為設定一個定時器,約定在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基礎心法--深淺拷貝

帶你徹底搞清楚深拷、淺拷貝、迴圈引用