JavaScript 手寫常用程式碼
手寫防抖
防抖,即短時間內大量觸發同一事件,只會執行一次函式
,實現原理為設定一個定時器,約定在xx毫秒後再觸發事件處理,每次觸發事件都會重新設定計時器,直到xx毫秒內無第二次操作
,防抖常用於搜尋框/滾動條的監聽事件處理,如果不做防抖,每輸入一個字/滾動螢幕,都會觸發事件處理,造成效能浪費。
分解需求:
- 持續觸發不執行
- 不觸發一段時間再執行
細節處理:
this
的指向- 子函式的引數傳遞,如
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 } } }
這種方法是
-
事件首次觸發就會執行
-
事件停止後會立刻停止執行
使用定時器
當觸發事件的時候,我們設定一個定時器,再觸發事件的時候,如果定時器存在,就不執行,直到定時器執行,然後執行函式,清空定時器,這樣就可以設定下個定時器。
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) } } }
這種方法是
- 事件首次觸發後並不會立即執行
- 事件停止後不會立刻停止執行,會等最後一次執行完
時間戳與定時器的混合
由於這兩種方法會有不一樣的效果,我們可以將兩者混合一起使用,這樣會得到兩者共同的特點
首次觸發會執行,並且也會有最後一次執行
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
}
手寫call
、apply
、bind
實現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
注意兩點:
- call 改變了 this 的指向,指向到 foo
- bar 函式執行了
模擬實現思路
那麼我們該怎麼模擬實現這兩個效果呢?
試想當呼叫 call 的時候,把 foo 物件改造成如下:
var foo = {
value: 1,
bar: function() {
console.log(this.value)
}
};
foo.bar(); // 1
這個時候 this 就指向了 foo,是不是很簡單呢?
但是這樣卻給 foo 物件本身添加了一個屬性,這可不行吶!
不過也不用擔心,我們用 delete 再刪除它不就好了~
所以我們模擬的步驟可以分為:
- 將函式設為物件的屬性
- 執行該函式
- 刪除該函式
以上個例子為例,就是:
// 第一步
foo.fn = bar
// 第二步
foo.fn()
// 第三步
delete foo.fn
fn 是物件的屬性名,反正最後也要刪除它,所以起成什麼都無所謂。
實現apply
apply
與call
並沒有太多不同,只是在引數方面,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實現了什麼
bind
與call
、apply
同為更改this指向的方法,但bind
同時也需要執行以下的任務:
-
改變
this
指向 -
由於需要延遲執行,需要返回一個函式
-
引數傳入可分兩次傳入
-
當返回的函式作為構造器時,需要使的原有的this失效而讓this返回指向例項
-
需要返回的函式原型與呼叫相同
new的實現
我們看new
都做了什麼:
-
建立一個新物件,並繼承其建構函式的
prototype
,這一步是為了繼承建構函式原型上的屬性和方法 -
執行建構函式,方法內的
this
被指定為該新例項,這一步是為了執行建構函式內的賦值操作 -
返回新物件(規範規定,如果構造方法返回了一個物件,那麼返回該物件,否則返回第一步建立的新物件)
上程式碼:
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]]
深拷貝
深拷貝會拷貝所有的屬性,並拷貝屬性指向的動態分配的記憶體。當物件和它所引用的物件一起拷貝時即發生深拷貝。深拷貝相比於淺拷貝速度較慢並且花銷較大。拷貝前後兩個物件互不影響。
實現深拷貝的方法有兩種:
- 利用
JSON
物件中的parse
和stringify
- 利用遞迴來實現每一層重新建立物件並賦值
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'}};
確實實現深拷貝,但是卻也是有著缺陷:
該方法轉換時undefined
、function
、symbol
會在轉換過程中被忽略。
遞迴方法
遞迴的思想就很簡單了,就是對每一層的資料都實現一次 建立物件->物件賦值
的操作
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]