使得业务对象/类的属性更容易管理.
适用于需要围绕业务对象/类来维护管理数据的场合. 我们首先需要定义该业务对象有哪些数据类型.
能够快速对该业务对象的公开属性进行赋值,合并,导出(JsonObject)以及校验.
功能:
- 定义的属性可跟随业务类继承
- 业务对象/类赋值属性从纯对象(JSON Object).
- 克隆业务对象.
- 比较业务对象是否相同(所有属性值相同).
- 导出属性到纯对象(JSON Object).
- 定义/声明属性类型及默认值
- 支持
arrayOf
带类型的数组类型 - 支持模板属性
template
(属性值由模板内容确定):template
{string | (this) => string}:- 字符串(模板), 如,
'${author}-${uuid()}'
- 或模板函数, 如,
function() {return this.author + '-' + uuid()}
- 字符串(模板), 如,
- 导入的函数供模板使用
imports
: {Object} the optional functions could be used in the template string. - 注意: 默认模板属性为只读,不过也可以将它设置为可写. 一旦新值被写入后,模板就不再有用,除非新值是
null
或undefined
- 支持
我们经常需要管理一个对象的属性,考虑以下几点:
- 创建业务对象时将选项设置为对象的属性
var myObj = new MyObject({opt1:value1, opt2:value2})
- 赋值(设置)来自另一个对象的属性
myObj.assign({opt1:v1, opt2:v2})
- 克隆业务对象(属性值完全相同):
var newObj = myObj.clone()
- 通过分配的属性比较两个对象是否相同。
myObj.isSame(anotherObj)
- 将属性导出为普通对象或 JSON,以便将来更轻松地重新创建对象。
- 有一些内部属性不应该被导出。
- 不应导出属性的空值或默认值。
- 应该导出并分配有意义的(非英语)名称。
myObj.toObject()
andmyObj.toJSON()
JSON.stringify(myObj)
- Problem: 如何为对象属性赋值?
- replace the standard
assignPropertyTo()
method. - define the attribute's
assign(value, dest, src, name)
method on the$attributes
.- the custom attribute's
assign
the value. return the changed value.
- the custom attribute's
- replace the standard
- Problem: 如何决定应该为也许对象中的哪些属性赋值或获取属性的默认值?
- 在此对象上预先定义所有属性,即使值为 null。
- 不支持默认值
- 定义一个简单的
$attributes
属性来管理属性及默认值:{attrName: {value:'defaultValue'}, ...}
- 定义一个复杂的
$attributes
(使用Properties
类)来管理属性
- 在此对象上预先定义所有属性,即使值为 null。
所以我们有了相应的属性管理类:SimplePropertyManager
、NormalPropertyManager
和 AdvancePropertyManager
。
首先是属性规则:
- 导出的属性意味着它们是
JSON.stringify(aObj)
属性。 - 不可枚举的属性不能被
导出
(export
)和赋值
(assign
)。 - 无法导出以"
$
"开头的可枚举属性,但可以被赋值
(assign
)。 undefined
值无法导出。- 只读(可写
writable
为假)属性不能赋值。 - 属性的赋值顺序就是定义的属性的顺序。
SimplePropertyManager
: /lib/simple- 直接使用对象的属性描述符。
- 所以不支持默认值。
- 不支持对象赋值钩子函数。
- 不支持有意义的(非英文)名称(别名)。
NormalPropertyManager
: /lib/normal- 使用
$attributes
普通对象来保存声明属性 - 支持默认值
- 支持对象赋值钩子函数
- 支持有意义的(非英文)名称(别名)
- 使用
AdvancePropertyManager
: /lib/advance- 使用
$attributes
来保存声明属性 $attributes
是Properties
类的实例。- 因此您可以自定义继承自的“Properties”类。
- 支持默认值
- 支持对象赋值钩子函数
- 支持有意义的(非英文)名称(别名)
- 如果可能,支持类型检查
- 使用
$attributes
包含对象的所有属性(如属性描述符),其中key
是属性名称。 而value
值是属性描述符对象.
属性描述符对象:
name
(String): 要导出的非英文名称,默认为key
名称。value
: 属性的默认值(如果存在)。 默认为undefined
。type
(String): 属性的类型名称。 默认为undefined
。enumerable
(Boolean): defaults to true- 当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。
configurable
(Boolean): defaults to true- 当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。
- writable (Boolean): defaults to true
- 当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变。
- assign (Function(value, dest, src, name)): defaults to
undefined
.- 自定义属性赋值函数。只需“返回”更改后的值。如果返回
undefined
就不要赋值 - 注意: 它只用于从另一个对象赋值时。
- 如果单独对属性赋值,则无效。 请使用属性描述符
set
来做到这一点。 - 将其封装为智能赋值功能(仅支持 Advance Property Manager ):
- 自动添加带前缀的隐藏内部属性
- 自动添加描述符
get
函数以读取属性 - 自动添加描述符
set
函数来写入属性(调用assign
描述符)
- 自定义属性赋值函数。只需“返回”更改后的值。如果返回
- 仅适用于 Normal 和 Advance Property Manager:
assigned
(Boolean): 是否属性可被赋值. defaults:undefined
- 如果
undefined
那么是否可被赋值由此决定:enumerable isn't false and (writable isn't false or isFunction(set))
- 如果
- 智能赋值支持(Smart Assign Support) 仅适用于
AdvancePropertyManager
assigned
(Boolean|String): 当“assigned”值为字符串时启用智能赋值
支持- 如果字符串值为空(""),那么它使用
nonExported1stChar+[name]
作为内部属性名称 - 当字符串值非空,那么该
assigned
值作为内部属性名 - 根据属性的可写性,自动创建对应的
get
/set
描述符.
exported
(Boolean): 是否该属性可被导出. defaults:undefined
- 如果
undefined
那么是否可被导出由此决定:enumerable isn't false and the first char isn't "$"
- 如果
alias
(String|ArrayOf String): 该属性的别名. 用于从其它纯对象选项中赋值时clone
(Boolean): 初始化时如果值为对象,是否克隆默认属性值。defaults to true.skipDefault
(Boolean): 是否在导出的时候跳过默认值。defaults to true.
'$attributes': {
'attrName': {
name: 'exportedName',
value: 123,
enumerable: false,
type: 'String',
configurable: true,
writable: true,
assign: function(value, dest, src, name)
get: ...,
set: ....
}
}
当作为能力注入时,以下方法将被添加(或替换):
initialize(options)
: overwrite this for assign options from constructor?- 如果可能,将属性的初始化值应用于对象。
- 然后调用
赋值
(assign
) 方法.
assign(options)
: assign the options' attributes to this object.- how to decide which attribute should be assign?
- I need an attributes manage class? or just a simple attributes list?
- or define all attributes even the value is null when initialize
- this must be an optional feature.
assignPropertyTo(dest, options, attributeName, value)
: assign an attribute to dest.- you can override it to determine howto assign an object value.
assignProperty(options, attributeName, value)
: assign an atrribute. called byassign
assignTo(dest)
: assign the attributes to thisdest
object.mergeTo(dest, options)
: 将this对象的属性值写入到dest
object, 如果options对象存在,并且options中的属性值存在,那么options中的属性值将覆盖原值.- do not overwrite the already exist attributes of the
dest
.
- do not overwrite the already exist attributes of the
isSame(obj)
: compare theobj
's attributes whether is the same value with itself.clone(options)
: 克隆对象, 如果options对象存在,并且options中的属性值存在,那么options中的属性值将覆盖原值.toObject(options)
: convert it as plain object.- do not export the non-enumerable attributes or beginning with '$'
- do not export the attribute's value is null
- do not export the attribute's value is default value.
- where to get the default value?
toJSON()
: this will call thetoObject()
to return.
注意: 如果构造函数的第一个参数不是属性选项,那么你需要指定属性选项
参数位置.
有下列三种方式使你的类可以管理属性:
- 继承方式:让你的类继承自属性管理器
- inherits from PropertyManager directly.
- 通过将属性能力注入到你的类
- 你需要确保下列的成员名称没有被类使用.
- The
$attributes
成员用于normal
andadvance
PropertyManager. nonExported1stChar
成员用于改变非导出(non-exported
)属性首字符约定,默认为 '$
'.
- 前四种方法必须存在。其他是可选的。但要注意它们的依赖性。
- assign
- assignPropertyTo
- getProperties
- defineProperties
- clone (optional)
- mergeTo
- initialize (optional)
- getProperties
- assign
- assignProperty (optional)
- assignPropertyTo
- mergeTo (optional)
- getProperties
- assignPropertyTo
- exportTo (optional)
- mergeTo
- assignTo (optional)
- getProperties
- assignPropertyTo
- toObject (optional)
- exportTo
- toJSON (optional)
- toObject
- isSame (optional)
- mergeTo
- The
- 你需要确保下列的成员名称没有被类使用.
- 修饰器: property-manager-decorator
有三类属性管理器可供选择, 默认是NormalPropertyManager
,详细介绍参见前述.
SimplePropertyManager
: 最简单的属性管理器,直接使用JS的对象属性描述符,因此无法在类上定义属性NormalPropertyManager
: 常规属性管理器,通过在类上的prototype
的$attributes
纯对象管理定义的属性AdvancePropertyManager
:高级属性管理器,通过在类上的prototype
的$attributes
Properties
对象管理定义的属性
// var inherits = require('inherits-ex/lib/inherits');
// var PropertyManager = require('property-manager');
// var PropertyManager = require('property-manager/lib/normal');
// var SimplePropertyManager = require('property-manager/lib/simple');
// var AdvancePropertyManager = require('property-manager/lib/advance');
import {inherits} from 'inherits-ex'
import {SimplePropertyManager, NormaPropertyManager, AdvancePropertyManager} from 'property-manager'
const ProperManager = NormaPropertyManager
//# Only for Normal or Advance PropertyManager
var defineProperties = ProperManager.defineProperties
class MyClass extends ProperManager {
constructor(name, options) {
super()
this.name = name
// if you use the SimplePropertyManager
// you should define your properties here:
//this.defineProperties({
// 'attr1': {value:123}
// 'hidden': {value:1, enumerable: false},
// '$dontExport': {value:3, enumerable: true}
//})
// initialize ProperManager
this.initialize(options)
}
}
/*
function MyClass(name, options) {
this.name = name;
// if you use the SimplePropertyManager
// you should define your properties here:
//this.defineProperties({
// 'attr1': {value:123}
// 'hidden': {value:1, enumerable: false},
// '$dontExport': {value:3, enumerable: true}
//})
PropertyManager.call(this, options);
}
inherits(MyClass, PropertyManager);
*/
// Only for normal and advance property manager
defineProperties(MyClass, {
'attr1': {value:123},
'hidden': {value:1, enumerable: false},
'$dontExport': {value:3, enumerable: true},
'custom': {
value: {},
assign: function(value, dest, src, name) {
if (value == null) {
value = {};
}
value.exta = 123;
return value;
}
}
});
/*
function MyClassEx() {
MyClassEx.__super__.constructor.apply(this, arguments)
}
inherits(MyClassEx, MyClass);
*/
class MyClassEx extends MyClass {}
// Inherited properties from MyClass
defineProperties(MyClassEx, {'extra': {value: 'extra'}});
通过PropertyAbility
函数将属性能力注入到任意类中.
PropertyAbility(target:Function|Object, options?)
如果没有参数那么就是默认的normal
属性管理器.
options:
name
:{'simple' | 'advance' | 'normal' | 'abstract'}
选定属性管理器, 默认为normal
optionsPosition
:{number}
, 可选的属性选项参数位置,在构造函数中需要导入Json对象属性时使用.exclude
:{string[]}
不需要注入的属性能力方法名称列表,默认为空。
// var PropertyAbility = require('property-manager/ability');
import {PropertyAbility} from 'property-manager'
class MyClass {
constructor(name, options) {
// if you use the SimplePropertyManager
// you should define your properties here:
//this.defineProperties({
// 'attr1': {value:123}
// 'hidden': {value:1, enumerable: false},
// '$dontExport': {value:3, enumerable: false}
//})
this.name = name;
// initialize PropertyManager
this.initialize.apply(this, arguments);
}
}
/*
function MyClass(name, options) {
// if you use the SimplePropertyManager
// you should define your properties here:
//this.defineProperties({
// 'attr1': {value:123}
// 'hidden': {value:1, enumerable: false},
// '$dontExport': {value:3, enumerable: false}
//})
this.name = name;
this.initialize.apply(this, arguments);
}
*/
// add the property manager ability to MyClass
// the default is normal property manager
// PropertyAbility(MyClass)
// you can specified the property manager 'simple', 'advance', 'normal':
// PropertyAbility(MyClass, 'simple')
// PropertyAbility(MyClass, {name: 'simple'})
// and you can specified the options position in the arguments
// the first argument(arguments[0]) is `name`, and the second(arguments[1]) is the options
PropertyAbility(MyClass, {optionsPosition: 1});
// you can exclude some non-core methods:
//PropertyAbility(MyClass, {optionsPosition:1, exclude: ['assignTo', ...]})
// You can define your properties here to:
var defineProperties = MyClass.defineProperties;
//only for normal, advance property manager
defineProperties(MyClass, {
'attr1': {value: 123},
'hidden': {value: 1, enumerable: false},
'$dontExport': {value: 3, enumerable: true},
'date': {
assign(value, dest, src, name, {isExported}) {
let result;
if (isExported) {
result = value.toISOString()
} else if (!(value instanceof Date)) {
result = new Date(value)
}
return result;
}
}
'custom': {
value: {},
assign: function(value, dest, src, name, opts) {
if (value == null) {
value = {};
}
value.exta = 123;
return value;
}
}
});
class MyClassEx extends MyClass {}
/*
function MyClassEx() {
MyClassEx.__super__.constructor.apply(this, arguments)
}
inherits(MyClassEx, MyClass);
*/
// Inherited properties from MyClass
defineProperties(MyClassEx, {'extra': {value: 'extra'}});
使用方式非常简单,和平常的对象属性使用没啥区别. 定义属性也和Object.defineProperties
类似.
现在 MyClass
类上有了下面几个属性:
attr1
: 可导出及赋值hidden
: 不能导出及赋值date
: the date 可导出及赋值$dontExport
: 可赋值, 但不可导出custom
: 可导出及赋值, 赋值将被属性描述符中的assign
函数改变.
MyClassEx
派生至 MyClass
(注: 仅支持normal 或 advance property manager)
extra
: 可导出及赋值- 其它属性继承至
MyClass
var assert = require('assert');
var my = new MyClass('aName', {
attr1: 3,
hidden: 11222,
$dontExport: 1,
custom: {
b: 12
}
});
assert.deepEqual(my.mergeTo(), {
attr1: 3,
$dontExport: 1,
custom: {
b: 12,
exta: 123
}
});
// the `hidden` can not be assigned and exported
assert.equal(my.hidden, 1);
// the `$dontExport` can not be exported
assert.deepEqual(my.toObject(), {
attr1: 3
});
assert.equal(JSON.stringify(my), '{"attr1":3,"custom":{"b":12,"exta":123}}');
var obj = my.clone();
// compare each assigned properties.
assert.ok(obj.isSame(my));
assert.deepEqual(obj.mergeTo(), {
attr1: 3,
$dontExport: 1,
custom: {
b: 12,
exta: 123
}
});
var myEx = new MyClassEx('theClassEx', {attr1: 3, hidden:11222, $dontExport: 1, custom:{b:12}})
assert.deepEqual(myEx.mergeTo(), {extra:'extra', attr1:3, $dontExport:1, custom:{b:12, exta: 123}})
More recent changes see: CHANGELOG.md
- ES6 Class
- ESM support
- NodeJS >= 8
- feat: add the readonly to smart assigned property
@Properties
class Phone extends AdvancePropertyManager {
@Prop({
writable: false,
exported: true,
assigned: '',
}) id!: string;
}
- BROKEN CHANGE: DO NOT EXPORT the readonly property by default unless exported is true.
@Properties
class Phone extends AdvancePropertyManager {
@Prop({
writable: false,
exported: true,
}) id!: string;
}
- add the array with type supports.
import { arrayOf } from 'property-manager/lib/array';
import AdvancePropertyManager from 'property-manager/lib/advance';
import { PropertyManager as Properties, Property as Prop } from 'property-manager-decorator';
@Properties
class Phone extends AdvancePropertyManager {
@Prop() value!: string;
@Prop() codeNum!: string;
@Prop() kind!: string;
constructor(initValue?) {
super(initValue);
}
}
@Properties
class Contact extends AdvancePropertyManager {
@Prop({type: String}) name!: string;
@Prop({type: arrayOf(Phone)}) phones!: Phone[];
constructor(initValue?) {
super(initValue);
}
}
- BROKEN change
toObject
method params to(options?: IMergeOptions)
- BROKEN change
assign
method params to(src, options?: IMergeOptions)
- BROKEN change
assignTo
method params to(dest, options?: IMergeOptions)
- BROKEN change
assignPropertyTo
andassignProperty
method params to(dest, src, name: string, value, attrs?, options?: IMergeOptions)
- BROKEN change
exportTo
method params to(dest, options?: IExportOptions)
- BROKEN change
mergeTo
method params to(dest, options?: IMergeOptions)
- add
skipNull
andskipUndefined
option toIExportOptions
andIMergeOptions
- add
- add the
extends(attrs: Object, nonExported1stChar)
method to theProperties
- return a new
Properties
instance to extends properties from current instance.
- return a new
- add the inherited properties supports for
AdvancePropertyManager.defineProperties
- change the
recreate
argument default value ofdefineProperties
tofalse
forAdvancePropertyManager
andNormalPropertyManager
- set all methods and non-properties of
Properties
to be non-enumerable.
- add typed property for
AdvancePropertyManager
andNormalPropertyManager
function CustomType(value) {
if (!(this instanceof CustomType)) return new CustomType(value)
try {
value = JSON.parse(value)
} catch(err) {
this.value = value
}
}
const attrs = {
prop1: {type: CustomType, value: 111}
}
class TypedPM extends AdvancePropertyManager {
constructor(opts) {
super(opts)
}
}
TypedPM.defineProperties(attrs)
const obj = new TypedPM()
console.log(obj.prop1 instanceof CustomType)
- add the
skipExists
option to theProperties.assignTo
andProperties.assignPropertyTo
- the options to the
Properties.assignTo(dest, src, options)
exclude
(String|Array)skipDefault
(Boolean)skipExists
(Boolean)skipReadOnly
(Boolean)exported
(Boolean)
- add the alias property descriptor(Normal&Advance):
- You can define one or more aliases to assign from other object(options)
alias
(String|ArrayOf String)
- Smart assignment property supports(AdvancePropertyManager):
- broken: SMART_ASSIGN constant deprecated.
assigned
descriptor (Boolean|String):String
means SMART_ASSIGN.- it's the internal property name of the smart assignment if it's string
- the internal property name is the property name with prefix(
nonExported1stChar
) if it's an empty string
- broken: remove
attrsName
property(fixed to '$attributes')
- add the helper function: properties/define-properties.
- clone default property value if the value is an object when initializing
- the object instances will share the same one of property value if the default value of property is an object.
- howto create a new object instance when initializing default value.
- Solution 1: the
value
descriptor could be a function to create new object instance:- Problem1: it will be only available for normal and advance property manager
value: function (){return Object.create()}
- Problem2: the value can not be a function now.
- Problem1: it will be only available for normal and advance property manager
- Solution 2: check the value whether is object. if so, clone it when initializing.
- use this solution. but if someone wish all instance share the same value.
- add a descriptor to control whethe enable this. but simple can not support the custom descriptor.
clone
(Boolean): defaults to true.
- the object instances will share the same one of property value if the default value of property is an object.
- Smart assignment property supports:
- assign property descriptor (Function(value, dest, src, name)):
- It only used to assign the options from another object.
- It's no effect if the assign the property individually. should use the property descriptor
set
to do so. - maybe I should wrap it:
- add a hidden internal property with prefix(
nonExported1stChar
) - add descriptor
get
function to read the property - add descriptor
set
function to assign the property(call theassign
descriptor).
- add a hidden internal property with prefix(
- need a descriptor to control whethe enable this.
assigned
: AdvancePropertyManager::SMART_ASSIGN = 2
- enabled:
!get and !set and assigned is AdvancePropertyManager::SMART_ASSIGN
- only available for advance property manager.
- note: only
value
argument is passed intoassign
descriptor when assignment the property individually.
- assign property descriptor (Function(value, dest, src, name)):
- add the property writable check: do not assign the readonly property.
- Normal, Advance
- add the
assigned
,exported
(Boolean) to property descriptor directly.assigned
: enumerable isnt false and (writable isnt false or isFunction(set)).exported
: enumerable isnt false and the first char isnt "$"
- add the
PropertyManager::nonExported1stChar
(Char), defaults to '$'- note: the
exported
descriptor is higher prior thannonExported1stChar
.
- note: the
nonExported1stChar
option to the property manager ability.
- broken the arguments order of assign function in property descriptor are changed:
- attr.assign(value, dest, src, name, opts) instead of assign(dest, src, value, name)
MIT