JavaScript Decorators 的簡單理解

  Decorators,裝飾器的意思, 所謂裝飾就是對一個物件進行美化,讓它變得更漂亮。最直觀的例子就是房屋裝修。你買了一套房子,可是毛坯房,你確定不想住,那就對它裝飾一下,牀,桌子,電視,冰箱等一通買,房子變漂亮了,住的也舒心了,同時功能也強大了,由於咱們能夠看電視了,上網了。javascript

  Js中,Decorators的做用也是如此,但它做用的對象是一個類或者其屬性方法,在不改變原有功能的基礎上,加強其功能。語法很是簡單,就是在類或者其屬性方法前面加上@decorator,decorator 指的是裝飾器的名稱。裝飾器自己是一個函數,由於在函數內部,咱們能夠進行任意的操做從而對其進行加強。html

  稍微有點遺憾,Decorators並無被標準化,不過咱們有babel, 能夠利用babel進行轉化,就是配置有點麻煩,在學習以前,咱們先用webpack(3版本)配置一個簡單的學習環境。java

裝飾器的轉化依賴一個核心插件 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  配置文件 , node

  webpack.config.js  配置文件, 在babel-loader 的options中配置了transform-decorators-legacy  插件webpack

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 以下web

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <script src="bundle.js"></script>
</body>
</html>

  在index.js 中先隨便寫點東西,驗證一個配置是否正確npm

document.body.innerHTML = 'blue';

  在package.json文件中, scripts 字段中寫入 "dev": "webpack-dev-server"json

  在decorator文件夾中啓動命令窗口,輸入npm run dev, 能夠看到項目啓動成功,在瀏覽器中輸入locolhost:8080 ,能夠看到blue 表示配置成功瀏覽器

  環境搭建好了,如今能夠學習Decorators了。首先 Decorators是做用在class上面的,因此聲明一個class,好比Car , babel

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 注入到裝飾器函器中,以便咱們修改,從而從新定義函數,這給咱們動態地修改提供了可能。

相關文章
相關標籤/搜索