5分鐘帶你了解【前端裝飾器】,“高大上”的“基礎(chǔ)知識(shí)”
前言
大家好,我是林三心,用最通俗易懂的話講最難的知識(shí)點(diǎn)是我的座右銘,基礎(chǔ)是進(jìn)階的前提是我的初心。
基本介紹
裝飾器是一種以 @ 符號(hào)開頭的特殊語(yǔ)法,放在目標(biāo)代碼的前面用于包裝或擴(kuò)展代碼功能。JavaScript 的裝飾器語(yǔ)法目前仍處于提案階段,現(xiàn)階段使用的話需要通過 bable 等方式進(jìn)行編譯之后,才能在瀏覽器正常運(yùn)行。裝飾器分為兩種:類裝飾器,類成員裝飾器,分別用于裝飾我們的類以及類的成員。
基本使用(類裝飾器)
class MyClass {
constructor() {}
}
比如現(xiàn)在有一個(gè)類或者函數(shù) MyClass,它的身上沒有任何的東西,但是我想要給他加一個(gè) log 方法,那我們應(yīng)該怎么做呢?很多人回想說,直接在它身上加一個(gè) log 方法即可~
class MyClass {
constructor() {}
log() {}
}
但是這么做的話,一個(gè) class 是能做,那如果要給 1000 個(gè) class 加上 log方法呢?那豈不是每一個(gè)都得寫~很麻煩,這個(gè)時(shí)候可以使用 裝飾器 去拓展每一個(gè) class
- 可以拓展原型方法
- 可以拓展靜態(tài)屬性
裝飾器接收的參數(shù)是裝飾的目標(biāo)類,這里的 cls 就是 MyClass
function addConcole(target) {
// 拓展原型方法
target.prototype.log = function(msg) {
console.log(`[${new Date()} ${msg}`);
};
// 拓展靜態(tài)屬性
target.myName = '一個(gè)類'
return target;
}
@addConcole
class MyClass {
constructor() {}
}
const myObj = new MyClass();
myObj.log('林三心');
// [Sat Jul 08 2023 17:31:55 GMT+0800 (中國(guó)標(biāo)準(zhǔn)時(shí)間) 林三心
console.log(MyClass.myName)
// 一個(gè)類
應(yīng)用場(chǎng)景
Node路由請(qǐng)求Url(類成員裝飾器)
我們?cè)谑褂靡恍?Node 的框架時(shí),在寫接口的時(shí)候,我們可能會(huì)經(jīng)??吹竭@樣的代碼
- 當(dāng)我們請(qǐng)求路徑是 GET doc 時(shí)會(huì)匹配到findDocById
- 當(dāng)我們請(qǐng)求路徑是 POST doc 時(shí)會(huì)匹配到createDoc
class Doc {
@Get('doc')
async findDocById(id) {}
@Post('doc')
async createDoc(data) {}
}
其實(shí)這個(gè) @Get 和 @Post ,是框架提供給我們的 類成員裝飾器,是的,類成員也能使用裝飾器,類成員裝飾器接收三個(gè)參數(shù):
- target 是目標(biāo)類的原型對(duì)象
- key 表示目標(biāo)類成員的鍵名
- descriptor 是一個(gè)屬性描述符對(duì)象,它包含目標(biāo)類成員的屬性特性(例如 value、writable 等)
function Get(path) {
return function(target, key, descriptor) {
console.log({
target,
key,
descriptor
})
}
}
圖片
他的基本實(shí)現(xiàn)原理大概是這樣的
function Get(routePath) {
return function(target, key, descriptor) {
const originalMethod = descriptor.value; // 保存原始方法
descriptor.value = function() {
// 在原始方法執(zhí)行前加入邏輯
console.log('處理 Get 請(qǐng)求,路由路徑: ' + routePath);
// 執(zhí)行原始方法
const result = originalMethod.apply(this, arguments);
// 在原始方法執(zhí)行后加入邏輯
console.log('Get 請(qǐng)求處理完成');
return result;
};
return descriptor;
};
}
接口權(quán)限控制(類成員裝飾器疊加)
上面我們介紹了一下 Nodejs Url 的路由匹配基本原理,但是這是不夠的,因?yàn)楹芏嘟涌谶€需要權(quán)限控制,比如:
- GET doc 接口只能 管理員 才能訪問
- POST doc 接口只能 超級(jí)管理員 才能訪問
這也可以用裝飾器來(lái)實(shí)現(xiàn),并且裝飾器是可以疊加的~
class Doc {
@Get('doc')
@Role('admin')
async findDocById(id) {}
@Post('doc')
@Role('superAdmin')
async createDoc(data) {}
}
裝飾器疊加的執(zhí)行順序是 從下往上 的~我們可以看一下下面的例子,發(fā)現(xiàn)輸出順序是
- 2
- 1
function A () {
console.log(1)
}
function B () {
console.log(2)
}
class Doc {
@A
@B
async test() {}
}
至于權(quán)限控制的裝飾器實(shí)現(xiàn),需要根據(jù)不同業(yè)務(wù)去實(shí)現(xiàn),我這里就粗略實(shí)現(xiàn)一下
function Role(permissions) {
return function(target, key, descriptor) {
const originalMethod = descriptor.value; // 保存原始方法
descriptor.value = function() {
// 在原始方法執(zhí)行前進(jìn)行權(quán)限驗(yàn)證
const user = getCurrentUser(); // 獲取當(dāng)前用戶信息
// 檢查用戶是否擁有所需權(quán)限
const hasPermission = checkUserPermissions(user, permissions);
if (!hasPermission) {
// 如果用戶沒有權(quán)限,則拋出錯(cuò)誤或執(zhí)行其他處理
throw new Error('無(wú)權(quán)限訪問該接口');
}
// 執(zhí)行原始方法
const result = originalMethod.apply(this, arguments);
return result;
};
return descriptor;
};
}
記錄日志的裝飾器
我們想要在執(zhí)行某個(gè)函數(shù)的時(shí)候,記錄一下
- 函數(shù)調(diào)用時(shí)間
- 函數(shù)調(diào)用參數(shù)
這個(gè)時(shí)候我們也可以使用裝飾器來(lái)完成,非常方便?。?!
// 日志裝飾器函數(shù)
function logDecorator(target, key, descriptor) {
const originalMethod = descriptor.value; // 保存原始方法
descriptor.value = function(...args) {
console.log(`調(diào)用函數(shù):${key}`);
console.log(`參數(shù):${JSON.stringify(args)}`);
// 執(zhí)行原始方法
const result = originalMethod.apply(this, args);
console.log(`返回值:${result}`);
return result;
};
return descriptor;
}
// 示例類
class Example {
@logDecorator
greet(name) {
return `Hello, ${name}!`;
}
}
// 測(cè)試
const example = new Example();
example.greet('林三心');
緩存的裝飾器
如果我們執(zhí)行一個(gè)方法,獲取返回值需要經(jīng)過一系列的計(jì)算,非常耗時(shí)間,那么我們可以判斷入?yún)?,第一次時(shí)計(jì)算完緩存起來(lái),第二次的時(shí)候如果還是這個(gè)入?yún)?,就直接從緩存中去拿,這個(gè)操作也可以使用裝飾器去完成
// 緩存裝飾器函數(shù)
function cacheDecorator(target, key, descriptor) {
const cache = {}; // 緩存對(duì)象
const originalMethod = descriptor.value; // 保存原始方法
descriptor.value = function(...args) {
const cacheKey = JSON.stringify(args); // 生成緩存鍵
if (cacheKey in cache) {
console.log('從緩存中獲取結(jié)果');
return cache[cacheKey]; // 直接返回緩存結(jié)果
}
// 執(zhí)行原始方法
const result = originalMethod.apply(this, args);
console.log('將結(jié)果緩存起來(lái)');
cache[cacheKey] = result; // 緩存結(jié)果
return result;
};
return descriptor;
}
// 示例類
class Example {
@cacheDecorator
getValue(key) {
console.log('執(zhí)行函數(shù)邏輯');
return key + Math.random(); // 模擬復(fù)雜的計(jì)算邏輯
}
}
// 測(cè)試
const example = new Example();
console.log(example.getValue('foo'));
console.log(example.getValue('foo')); // 從緩存中獲取結(jié)果
防抖節(jié)流的裝飾器
對(duì)于防抖節(jié)流,我們平時(shí)可能會(huì)這么去做
class C {
onClick = debounce(fn, 100)
}
但是這么做的話會(huì)使這個(gè)函數(shù)不好拓展,所以使用裝飾器真的很方便
// 防抖裝飾器
function debounce(time) {
return function (target, key, descriptor) {
const oldFunction = descriptor.value;
let timer = null;
descriptor.value = function () {
clearTimeout(timer);
timer = setTimeout(() => {
oldFunction.apply(this, arguments)
}, time);
};
return descriptor;
}
}
// 節(jié)流裝飾器
function throttle(time) {
return function (target, key, descriptor) {
const oldFunction = descriptor.value;
let isLock = false;
descriptor.value = function() {
if(isLock) { return; }
isLock = true;
oldFunction.apply(this, arguments);
setTimeout(() => {
isLock = false;
}, time);
}
return descriptor;
}
}
class C {
@debounce(1000)
onClick() {}
@throttle(1000)
onScroll() {}
}