JavaScript元編程——基于Proxy實(shí)現(xiàn)active_record動(dòng)態(tài)查找

1. 元編程

在網(wǎng)絡(luò)上無(wú)意間看到《JavaScript權(quán)威指南》第七版的目錄,除了NodeJS外,很意外的看到有一個(gè)章節(jié)叫元編程。

第一次聽說(shuō)元編程這一概念還是來(lái)自于Ruby,《Ruby元編程》這本書,很遺憾的是這本書我只看了一點(diǎn)點(diǎn)……對(duì)于元編程,我所掌握的也就只有Open Classmethod_missing而已了,不過(guò)本文也就只是使用了這么點(diǎn)簡(jiǎn)單的內(nèi)容。

1.1 Open Class

在很多面向?qū)ο蟮恼Z(yǔ)言里是無(wú)法修改一個(gè)類的,但在Ruby中如下代碼是合法的:

class Book
  attr_accessor :name

  def initialize(name)
    @name = name
  end

  def to_s
    "書名:#{@name}"
  end

end

book = Book.new("《Ruby 元編程》")

puts book.to_s

# Open Class
class Book

  def pure_name
    @name[0] == "《" && @name[-1] == "》" ? @name[1..-2] : @name
  end

end

puts book.pure_name

雖然重復(fù)定義了Book類,但后定義的pure_name方法被“加入”到了原有的類定義中。通過(guò)這種方式我們可以在任意位置對(duì)我們的代碼進(jìn)行擴(kuò)展,這一技巧被稱為Monkey Patch,以下是一個(gè)更實(shí)用一點(diǎn)的例子,我們打開了Array類。

# 通過(guò) Open Class 為數(shù)組添加一個(gè)用于求平均值的方法
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

class Array

  def average
    sum / size
  end

end

puts arr.average # 輸出 5

除了擴(kuò)展方法外,我們還可以通過(guò)這種手段使程序更具有表現(xiàn)力:

arr = [...]

arr.first # 等同于 arr[0]
arr.second # 等同于 arr[1]
arr.last # 等同于 arr[-1]

不過(guò)這種手段也容易帶來(lái)問(wèn)題,例如打開類以后覆蓋了一個(gè)已有的方法,那么極容易導(dǎo)致其它位置的方法調(diào)用出現(xiàn)問(wèn)題。

1.2. method_missing

Ruby對(duì)象在調(diào)用方法時(shí),如果不能找到目標(biāo)方法,則會(huì)嘗試執(zhí)行method_missing方法,我們可以將method_missing方法看作一層代理:

class Array

  def method_missing(method)
    case method
    when :average
      sum / size
    when :to_binary
      map{ |num| num.to_s(2) }
    end
  end

end

arr = (1..10).to_a

# 如下兩個(gè)方法都沒有直接在 Array 類中定義,而是在查詢方法失敗以后通過(guò) method_missing 方法進(jìn)行了處理
puts arr.average # 返回 5
puts arr.to_binary # 返回?cái)?shù)組元素轉(zhuǎn)為二進(jìn)制之后組成的數(shù)組

2. 基于prototype和proxy嘗試JavaScript元編程

我們知道JavaScript的類實(shí)際上是借由prototype實(shí)現(xiàn)的語(yǔ)法糖,利用prototype一樣可以實(shí)現(xiàn)類似于上述的Open Class

const indexAlias = {
    first: 0,
    second: 1,
    third: 2,
    fourth: 3,
    fifth: 4,
    sixth: 5,
    seventh: 6,
    eighth: 7,
    ninth: 8,
    tenth: 9,
    twentieth: 19,
    thirtieth: 29,
    last: -1,
}

Object.keys(indexAlias).map(alias => {
    Array.prototype[alias] = function () {
        const index = indexAlias[alias] === -1 ? this.length - 1 : indexAlias[alias];
        return this[index];
    }
});

const testArr = Object.keys(Array.from(new Array(100)));

console.log(testArr.first()); // 等同于 testArr[0]
console.log(testArr.second()); // 等同于 testArr[1]
console.log(testArr.third()); // 等同于 testArr[2]
console.log(testArr.last()); // 等同于 testArr[3]

上述例子動(dòng)態(tài)的為數(shù)組類擴(kuò)展了多個(gè)類似的方法。

不過(guò)這里有一個(gè)小細(xì)節(jié),其實(shí)我并不一定需要所有的方法,有時(shí)候可能到頭來(lái)只調(diào)用了firstlast方法,但這些方法卻實(shí)實(shí)在在的都掛到了prototype上。

基于代理來(lái)實(shí)現(xiàn)的方法動(dòng)態(tài)定義其實(shí)可以解決這個(gè)問(wèn)題。

const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const customArr = new Proxy(arr, {
    get: function (target, prop, receiver) {
        if (Reflect.has(target, prop)) return Reflect.get(...arguments);
        switch (prop) {
            case 'average':
                return function () {
                    return this.reduce((sum, item) => sum + item, 0) / this.length;
                }
        }
    }
});

console.log(customArr.average());

需要注意幾點(diǎn)細(xì)節(jié):

  1. 此處通過(guò)代理擴(kuò)展的是實(shí)例方法而非類方法
  2. 考慮到數(shù)組和對(duì)象都可以用字面量的方式完成初始化,打開Array/Object類的時(shí)候,或許prototype會(huì)更管用一些,因?yàn)?strong>prototype修改的是原有的類而代理是創(chuàng)建新的類。

當(dāng)然,完全可以將average方法直接放到prototype上,但如果我們要定義的是多個(gè)存在聯(lián)系的方法,使用這種代理會(huì)靈活的多,關(guān)于這一點(diǎn),接下來(lái)要嘗試實(shí)現(xiàn)的active_record動(dòng)態(tài)查找可能是一個(gè)不錯(cuò)的案例。

3. 基于Proxy實(shí)現(xiàn)active_record動(dòng)態(tài)查找

active_recordRuby On Rails中的ORM庫(kù),它有一個(gè)非常有用的魔法:假設(shè)存在一張數(shù)據(jù)表users,它有三個(gè)字段:

  • username
  • nickname
  • email

根據(jù)以往我們對(duì)ORM的理解,此時(shí)需要?jiǎng)?chuàng)建一個(gè)實(shí)體類,且這個(gè)實(shí)體類一眼兩個(gè)需要聲明上述的三個(gè)屬性。不過(guò),在ActiveRecord里,創(chuàng)建實(shí)體類你只需要繼承ActiveRecord即可,它會(huì)自動(dòng)的添加類屬性,同時(shí)還有包括如下三個(gè)方法在內(nèi)的大量數(shù)據(jù)讀寫方法:

  • find_by_username
  • find_by_nickname
  • find_by_email

其原理是根據(jù)數(shù)據(jù)表的字段名列表動(dòng)態(tài)定義了各字段的查詢方法。

JavaScript基于代理也可以實(shí)現(xiàn)類似的效果,下面的示例代碼沒有真正的鏈接數(shù)據(jù)庫(kù),而是使用了一個(gè)對(duì)象結(jié)構(gòu)來(lái)進(jìn)行模擬,同時(shí)為了讓示例看起來(lái)像那么回事兒,還實(shí)現(xiàn)了active_record持久化數(shù)據(jù)的兩個(gè)方法save/create。

先來(lái)看看最終的效果:

const ActiveRecord = require('./ActiveRecord');

// 1. 初始化一個(gè)數(shù)據(jù)源(模擬數(shù)據(jù)庫(kù))
const DB = {};

ActiveRecord.init({
    db: DB,
});

class User extends ActiveRecord { // 2. 定義一個(gè)實(shí)體類
}

// 3.1 創(chuàng)建一條數(shù)據(jù)的方式1: 實(shí)例化一個(gè)對(duì)象然后調(diào)用 save 方法
const yuchi = new User({
    userName: 'yuchi',
    password: '123456',
    nickName: '魚翅'
});

yuchi.save();

// 3.2 創(chuàng)建一條數(shù)據(jù)的方式2: 直接使用 create 類方法
User.create({
    userName: 'xiaoming',
    password: '11111',
    nickName: '小明'
});

// 4. 查看虛擬的數(shù)據(jù)庫(kù)數(shù)據(jù) 
console.log(DB);

// 5. 通過(guò)屬性生成的動(dòng)態(tài)查詢方法進(jìn)行查詢
console.log(User.findByUserName('yuchi'));
console.log(User.findByNickName('小明'));
console.log(User.findByPassword('11111'));

// 再創(chuàng)建一個(gè)
class Book extends ActiveRecord { }

Book.create({ name: '《我們的土地》', author: '[墨西哥] 卡洛斯·富恩特斯', pageTotal: '1036', price: '168', ISBN: '9787521211542' });
Book.create({ name: '《戛納往事》', author: '[法]吉爾·雅各布', pageTotal: '712', price: '148', ISBN: '9787308211208' });


console.log('查詢結(jié)果:', Book.findByName('《戛納往事》'));
console.log('查詢結(jié)果:', Book.findByAuthor('[法]吉爾·雅各布'));
console.log('查詢結(jié)果:', Book.findByPageTotal('712'));
console.log('查詢結(jié)果:', Book.findByPrice('168'));
console.log('查詢結(jié)果:', Book.findByISBN('9787308211208'));

以下是ActiveRecord類的實(shí)現(xiàn),它有如下細(xì)節(jié):

  1. ActiveRecord是一個(gè)經(jīng)過(guò)代理的類。
  2. 創(chuàng)建一個(gè)ActiveRecord類的子類,然后初始化,實(shí)際調(diào)用的是父類的構(gòu)造函數(shù),同時(shí)也會(huì)觸發(fā)代理(注意Proxy里的代碼,為了保證返回的對(duì)象依然是子類對(duì)象,手動(dòng)修改了構(gòu)造函數(shù)指向)。
  3. ActiveRecord類經(jīng)過(guò)代理后,增加了動(dòng)態(tài)查詢類方法。
  4. ActiveRecord類的子類實(shí)例化后得到的也是一個(gè)經(jīng)過(guò)代理的對(duì)象,代理中實(shí)現(xiàn)了一些實(shí)例方法。
// 定義基礎(chǔ)的 ActiveRecord 抽象類,并支持動(dòng)態(tài)的初始化實(shí)例屬性
class BaseActiveRecord {
    constructor(record) {
        Object.keys(record).map(item => this[item] = record[item])
    }

    // 一個(gè)用于驗(yàn)證代理后的類依然可以被繼承的基礎(chǔ)方法,也順便用于數(shù)據(jù)序列化以便于存到 DB 中
    toJSON() {
        const res = {};
        Object.keys(this).map(item => res[item] = this[item]);
        return res;
    }
}

// 代理基礎(chǔ) ActiveRecord 類
const ActiveRecord = new Proxy(BaseActiveRecord, {
    // 代理構(gòu)造方法,主要意圖在希望實(shí)例化以后返回的 AR 對(duì)象一樣是被代理過(guò)的
    construct: function (target, args, newTarget) {
        const nativeObj = new target(args[0]);
        nativeObj.__proto__ = newTarget.prototype;

        return new Proxy(nativeObj, {
            get: function (obj, prop) {
                if (Reflect.has(obj, prop)) return Reflect.get(...arguments);
                if (prop !== 'save') throw new Error(`${prop} is not a function!`)
                
                // 定義了一個(gè) save 方法,自動(dòng)根據(jù)實(shí)體類的名字將數(shù)據(jù)存到對(duì)應(yīng)的表里
                return function () {
                    const tableName = obj.__proto__.constructor.name.toLowerCase() + 's';
                    ActiveRecord.db[tableName] = (ActiveRecord.db[tableName] || []);
                    ActiveRecord.db[tableName].push(this.toJSON());
                }
            },
        });
    },
    // 代理類屬性和方法
    get: function (obj, prop, receiver) {
        if (Reflect.has(obj, prop)) return Reflect.get(...arguments);

        const tableName = receiver.prototype.constructor.name.toLowerCase() + 's';
        switch (prop) {
            // 定義一個(gè) create 方法,基本與 save 方法相同
            case 'create':
                return function () {
                    ActiveRecord.db[tableName] = (ActiveRecord.db[tableName] || []);
                    ActiveRecord.db[tableName].push(arguments[0]);
                }
            default:
                // 根據(jù)屬性動(dòng)態(tài)定義 findByAttr 方法
                if (prop.startsWith('findBy')) {
                    const attr = prop.slice(prop.indexOf('findBy') + 6, prop.length).toLowerCase();

                    return function () {
                        return ActiveRecord.db[tableName].filter(item => item[Object.keys(item).filter(item => item.toLowerCase() === attr)[0]] === arguments[0]);
                    }
                }
        }
    }
});

// 用來(lái)初始化 DB 數(shù)據(jù)源的配置
ActiveRecord.init = function (option) {
    ActiveRecord.db = option.db;
}

module.exports = ActiveRecord;

代碼:
yuchiXiong/activeRecordByProxy

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容