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