1. 元編程
在網(wǎng)絡(luò)上無(wú)意間看到《JavaScript權(quán)威指南》第七版的目錄,除了NodeJS外,很意外的看到有一個(gè)章節(jié)叫元編程。
第一次聽說(shuō)元編程這一概念還是來(lái)自于Ruby,《Ruby元編程》這本書,很遺憾的是這本書我只看了一點(diǎn)點(diǎn)……對(duì)于元編程,我所掌握的也就只有Open Class和method_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)用了first和last方法,但這些方法卻實(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é):
- 此處通過(guò)代理擴(kuò)展的是實(shí)例方法而非類方法。
- 考慮到數(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_record是Ruby On Rails中的ORM庫(kù),它有一個(gè)非常有用的魔法:假設(shè)存在一張數(shù)據(jù)表users,它有三個(gè)字段:
usernamenicknameemail
根據(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_usernamefind_by_nicknamefind_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é):
-
ActiveRecord是一個(gè)經(jīng)過(guò)代理的類。 - 創(chuàng)建一個(gè)
ActiveRecord類的子類,然后初始化,實(shí)際調(diào)用的是父類的構(gòu)造函數(shù),同時(shí)也會(huì)觸發(fā)代理(注意Proxy里的代碼,為了保證返回的對(duì)象依然是子類對(duì)象,手動(dòng)修改了構(gòu)造函數(shù)指向)。 -
ActiveRecord類經(jīng)過(guò)代理后,增加了動(dòng)態(tài)查詢類方法。 -
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;