1. 元编程
在网络上无意间看到《JavaScript 权威指南》第七版的目录,除了NodeJS
外,很意外的看到有一个章节叫元编程。
第一次听说元编程这一概念还是来自于Ruby
,《Ruby 元编程》这本书,很遗憾的是这本书我只看了一点点……对于元编程,我所掌握的也就只有Open Class
和method_missing
而已了,不过本文也就只是使用了这么点简单的内容。
1.1 Open Class
在很多面向对象的语言里是无法修改一个类的,但在Ruby
中如下代码是合法的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 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
class Book
def pure_name @name[0] == "《" && @name[-1] == "》" ? @name[1..-2] : @name end
end
puts book.pure_name
|
虽然重复定义了Book
类,但后定义的pure_name
方法被 “加入” 到了原有的类定义中。通过这种方式我们可以在任意位置对我们的代码进行扩展,这一技巧被称为Monkey Patch
,以下是一个更实用一点的例子,我们打开了Array
类。
1 2 3 4 5 6 7 8 9 10 11 12
| arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
class Array
def average sum / size end
end
puts arr.average
|
除了扩展方法外,我们还可以通过这种手段使程序更具有表现力:
1 2 3 4 5
| arr = [...]
arr.first arr.second arr.last
|
不过这种手段也容易带来问题,例如打开类以后覆盖了一个已有的方法,那么极容易导致其它位置的方法调用出现问题。
1.2. method_missing
Ruby
对象在调用方法时,如果不能找到目标方法,则会尝试执行method_missing
方法,我们可以将method_missing
方法看作一层代理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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
puts arr.average puts arr.to_binary
|
2. 基于 prototype 和 proxy 尝试 JavaScript 元编程
我们知道JavaScript
的类实际上是借由prototype
实现的语法糖,利用prototype
一样可以实现类似于上述的Open Class
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| 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()); console.log(testArr.second()); console.log(testArr.third()); console.log(testArr.last());
|
上述例子动态的为数组类扩展了多个类似的方法。
不过这里有一个小细节,其实我并不一定需要所有的方法,有时候可能到头来只调用了first
和last
方法,但这些方法却实实在在的都挂到了prototype
上。
基于代理来实现的方法动态定义其实可以解决这个问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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());
|
需要注意几点细节:
- 此处通过代理扩展的是实例方法而非类方法。
- 考虑到数组和对象都可以用字面量的方式完成初始化,打开
Array/Object
类的时候,或许prototype
会更管用一些,因为prototype 修改的是原有的类而代理是创建新的类。
当然,完全可以将average
方法直接放到prototype
上,但如果我们要定义的是多个存在联系的方法,使用这种代理会灵活的多,关于这一点,接下来要尝试实现的active_record
动态查找可能是一个不错的案例。
3. 基于 Proxy 实现 active_record 动态查找
active_record
是Ruby On Rails
中的ORM
库,它有一个非常有用的魔法:假设存在一张数据表users
,它有三个字段:
根据以往我们对ORM
的理解,此时需要创建一个实体类,且这个实体类一眼两个需要声明上述的三个属性。不过,在ActiveRecord
里,创建实体类你只需要继承ActiveRecord
即可,它会自动的添加类属性,同时还有包括如下三个方法在内的大量数据读写方法:
find_by_username
find_by_nickname
find_by_email
其原理是根据数据表的字段名列表动态定义了各字段的查询方法。
JavaScript
基于代理也可以实现类似的效果,下面的示例代码没有真正的链接数据库,而是使用了一个对象结构来进行模拟,同时为了让示例看起来像那么回事儿,还实现了active_record
持久化数据的两个方法save/create
。
先来看看最终的效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| const ActiveRecord = require('./ActiveRecord');
const DB = {};
ActiveRecord.init({ db: DB, });
class User extends ActiveRecord { }
const yuchi = new User({ userName: 'yuchi', password: '123456', nickName: '鱼翅' });
yuchi.save();
User.create({ userName: 'xiaoming', password: '11111', nickName: '小明' });
console.log(DB);
console.log(User.findByUserName('yuchi')); console.log(User.findByNickName('小明')); console.log(User.findByPassword('11111'));
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('查询结果:', Book.findByName('《戛纳往事》')); console.log('查询结果:', Book.findByAuthor('[法] 吉尔·雅各布')); console.log('查询结果:', Book.findByPageTotal('712')); console.log('查询结果:', Book.findByPrice('168')); console.log('查询结果:', Book.findByISBN('9787308211208'));
|
以下是ActiveRecord
类的实现,它有如下细节:
ActiveRecord
是一个经过代理的类。- 创建一个
ActiveRecord
类的子类,然后初始化,实际调用的是父类的构造函数,同时也会触发代理(注意Proxy
里的代码,为了保证返回的对象依然是子类对象,手动修改了构造函数指向)。 ActiveRecord
类经过代理后,增加了动态查询类方法。ActiveRecord
类的子类实例化后得到的也是一个经过代理的对象,代理中实现了一些实例方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| class BaseActiveRecord { constructor(record) { Object.keys(record).map(item => this[item] = record[item]) }
toJSON() { const res = {}; Object.keys(this).map(item => res[item] = this[item]); return res; } }
const ActiveRecord = new Proxy(BaseActiveRecord, { 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!`) 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) { case 'create': return function () { ActiveRecord.db[tableName] = (ActiveRecord.db[tableName] || []); ActiveRecord.db[tableName].push(arguments[0]); } default: 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]); } } } } });
ActiveRecord.init = function (option) { ActiveRecord.db = option.db; }
module.exports = ActiveRecord;
|
代码:
yuchiXiong/activeRecordByProxy