Decorators,裝飾器的意思, 所謂裝飾就是對一個物件進行美化,讓它變得更漂亮。最直觀的例子就是房屋裝修。你買了一套房子,但是毛坯房,你肯定不想住,那就對它裝飾一下,床,桌子,電視,冰箱等一通買,房子變漂亮了,住的也舒心了,同時功能也強大了,因為我們可以看電視了,上網了。
Js中,Decorators的作用也是如此,但它作用的對象是一個類或者其屬性方法,在不改變原有功能的基礎上,增強其功能。語法非常簡單,就是在類或者其屬性方法前面加上@decorator,decorator 指的是裝飾器的名稱。裝飾器本身是一個函數,因為在函數內部,我們可以進行任意的操作從而對其進行增強。
稍微有點遺憾,Decorators並沒有被標准化,不過我們有babel, 可以利用babel進行轉化,就是配置有點麻煩,在學習之前,我們先用webpack(3版本)配置一個簡單的學習環境。
裝飾器的轉化依賴一個核心插件 babel-plugin-transform-decorators-legacy。 新建一個decorator 文件夾,npm init -y 初始化項目,安裝各種依賴 npm install webpack webpack-dev-server babel-core babel-loader babel-plugin-transform-decorators-legacy --save-dev, 然后新建index.js 作為入口文件,index.html用於展示,webpack.config.js 配置文件 ,
webpack.config.js 配置文件, 在babel-loader 的options中配置了transform-decorators-legacy 插件
const path = require('path'); module.exports = { entry: path.join(__dirname, 'index.js'), output: { path: path.join(__dirname), filename: 'bundle.js' }, module: { rules: [ { test: /\.js$/, loader: 'babel-loader', exclude: path.join(__dirname, 'node_modules'), options: { plugins: ['transform-decorators-legacy'] } } ] } }
因為webpack 打包后文件是bundle.js , 所以要在index.html 中引入 bundle.js , index.html 如下
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <script src="bundle.js"></script> </body> </html>
在index.js 中先隨便寫點東西,驗證一個配置是否正確
document.body.innerHTML = 'blue';
在package.json文件中, scripts 字段中寫入 "dev": "webpack-dev-server"
在decorator文件夾中啟動命令窗口,輸入npm run dev, 可以看到項目啟動成功,在瀏覽器中輸入locolhost:8080 ,可以看到blue 表示配置成功
環境搭建好了,現在可以學習Decorators了。首先 Decorators是作用在class上面的,所以聲明一個class,比如Car ,
class Car {
}
其次,Decorators是一個函數,那么我們就寫一個函數,直接命名為decorators 好了, 這個函數要有一個參數,就是要裝飾的對象,名稱一般命名為target, 這個也很好理解,我們都不知道對誰進行裝飾,還裝飾什么。
function decorators(target) { target.color = 'black'; }
我們給target 增加一個color屬性, 由此可以推斷出,要裝飾的類有了一個color 屬性。 裝飾一個類,就在類的上面寫上@decorators, 我們可以打印一下, 證明我們的猜測是不是正確的, 整個index.js 文件如下:
// 裝飾器函數 function decorators(target) { target.color = 'black'; } // 用@裝飾器 裝飾一個類 @decorators class Car { } console.log(Car.color); // 輸出black
這時你可能會想,我們可不可以動態設置color屬性的值? 當然可以,因為裝飾器是一個函數,我們只要返回這個函數就可以了,我們來聲明一個函數,讓它返回裝飾器函數。注意這里不能使用箭頭函數。我們把 decorators 函數做如下修改,它接受一個color 參數, 當然使用的時候也要傳遞一個參數
// 返回裝飾器的函數 function decorators (color) { return function(target){ target.color = color; } } // 使用時傳遞一個參數,如 'red' @decorators('red') class Car { } console.log(Car.color); // 輸出我們指定的參數red.
對於一個類的簡單裝飾就是這么簡單。 現在我們再來裝飾一個類的方法,同時說明一下裝飾器的由來。現在清空index.js,重寫一下Car 類,讓它有一個方法getColor
class Car { constructor(color) { this.color = color; } getColor() { return this.color; } }
使用這個類也非常簡單,就是用new 創建一個對象,然后調用getColor 方法
let carObj = new Car('black'); console.log(carObj.getColor()); // 輸出black
但是這時不小心,重新在carObj對象身上賦值了一個getColor 方法,
carObj.getColor = function(){ return 'blah blah'; }
出問題了,它輸出了 blah blah, 和我們的預想不一致,問題如下
console.log(carObj.getColor()); // 輸出blah blah, 我們可以覆蓋了getColor
在實際開發中,我們肯定不想出現這樣的問題,那怎么辦? 怎樣才能避免這要的覆蓋操作? 這時我們想到了javascript中的一條標准,給對象進行賦值操作時,如果賦值的方法名,正好在原型鏈中有,也就是說與原型鏈中的方法重名,但原型鏈中該方法定義了只讀屬性,那么賦值操作是不允許的。我們只要把原型鏈中的方法定義為只讀屬性就可以解決問題了,那怎樣才能把原型鏈中的方法定義為只讀屬性呢? 那就是用Object.defineProperty 來定義原型鏈中的方法。
這里要注意,ES6中的class語法,只是原型鏈方式的一種語法糖,我們在一個class中添加方法,實際上是向原型鏈上添加方法,也就是說getColor 方法,實際上存在於Car.prototype上, 實際上在這里,我們也可以看看getColor的默認屬性值到底是什么樣子? 當我們在一個對象上定義方法或屬性時,它都有默認的屬性描述,怎么看呢? 用 Object.getOwnProtperty
console.log(Object.getOwnPropertyDescriptor(Car.prototype, 'getColor'))
可以看到如下內容,
它的默認屬性值,writable: true, enumerable: false, configurable: true, 可寫,可配置,不可枚舉。這時我們也明白了,由於writable: true 導致了它可以被復寫。也就是說,如果我們在類中寫方法,是沒有辦法阻止它被復寫的,所以我們要用object.defineProperty 在類的外面添加方法,對它進行配置。 把getColor 從類中刪除,object.defineProperty 重新定義。整個js代碼如下:
class Car { constructor(color) { this.color = color; } } // 用Object.defineProperty 在原型鏈上定義方法,從而可以進行屬性配置 // value 的值也可以是一個函數,以前一直以為它只能是數值 Object.defineProperty(Car.prototype, 'getColor', { value:function () { return this.color; }, writable: false }) let carObj = new Car('black'); console.log(carObj.getColor()); // 輸出black carObj.getColor = function(){ return 'blah blah'; } console.log(carObj.getColor()); // 輸出black
當我們進行配置以后,縱然可以添加同名屬性,但不會被復寫了。但這又有了一個問題,如果多個屬性都要求不可復寫時,都要按照上面的方法進行配置,那就太麻煩了,所以我們要寫一個函數,對代碼進行封裝。因為我們這里只是改了descriptor ,所以我們可以把它提出來,聲明成一個變量, 然后利用函數對其進行修改。 descriptor 的初始值是什么呢?上面我們說過,系統會為每一個屬性設一個默認值,我們使用這個默認值肯定不會報錯
// 當我們在類中寫一個方法時,默認的屬性描述就是下面 let descriptor = { value: function() { return this.color; }, writable: true, configurable: true, enumerable: false }
然后再寫一個函數,命名為readonly吧,因為不可復寫嗎, 在里面修改descriptor, 並返回。 為了更為准確的說明,我們還是寫上target, key,來表示我們修改哪個對象的哪個屬性
let readonly = function(target, key, descriptor) { descriptor.writable = false; return descriptor; }
再調用 readonly 來修改我們的descriptor, 最后object.defineProperty 重新定義
descriptor = readonly(Car.prototype, 'getColor', descriptor);
Object.defineProperty(Car.prototype, 'getColor', descriptor)
這時我們的要求要達到了,整個js 代碼如下
class Car { constructor(color) { this.color = color; } } // 當我們在類中寫一個方法時,默認的屬性描述就是下面 let descriptor = { value: function() { return this.color; }, writable: true, configurable: true, enumerable: false } let readonly = function(target, key, descriptor) { descriptor.writable = false; return descriptor; } descriptor = readonly(Car.prototype, 'getColor', descriptor); Object.defineProperty(Car.prototype, 'getColor', descriptor) let carObj = new Car('black'); console.log(carObj.getColor()); // 輸出black carObj.getColor = function(){ return 'blah blah'; } console.log(carObj.getColor()); // 輸出black
我們再往下一步,只把readonly 函數留下,並且放到js 代碼的頂部,同時再把getColor 函數放到class類中, js 代碼如下
// readonly函數 let readonly = function(target, key, descriptor) { descriptor.writable = false; return descriptor; } class Car { constructor(color) { this.color = color; } getColor() { // getColor 重新寫到類中 return this.color; } } let carObj = new Car('black'); console.log(carObj.getColor()); // 輸出black carObj.getColor = function(){ return 'blah blah'; } console.log(carObj.getColor()); // 輸出blah blah
你可能好奇readonly函數怎么用? 其實它就是我們的裝飾器函數, 只要把@readonly 放到getColor 的上面, 我們相要的效果也能達到
class Car { constructor(color) { this.color = color; } @readonly // 加上readonly getColor() { return this.color; } }
這時你可能明白了,裝飾器其實是利用object.defineProperty 重新定義了屬性或方法。
正着推理已經完成了,我們再反着試一試, js代碼改為如下樣式
let readonly = function(target, key, descriptor) { console.log(target); console.log(key); console.log(descriptor); } class Car { constructor(color) { this.color = color; } @readonly getColor() { return this.color; } }
我們依次輸出了裝飾器的target, key, descripter 三個參數,target 就是Car.prototype, key 就是指getColor 本身, descriptor 就是我們的屬性描述符
也就是說,當我們把一個裝飾器函數寫到一個方法或類上時,js 引擎會自動的把target,key, descriptor 注入到裝飾器函器中,以便我們修改,從而重新定義函數,這給我們動態地修改提供了可能。