在 Javascript 中,读取、赋值、调用方法等等,几乎一切操作都是围绕“对象”展开的;长久以来,如何更好的了解和控制这些操作,就成了该语言发展中的重要问题。
1. JS 对象的访问控制
[1.1] 熟悉的 getter/setter
所谓 getter/setter,其定义一般为:
- 一个 getter 方法不接受任何参数,且总是返回一个值
- 一个 setter 总是接受一个参数,且并不会返回值
一些 getter/setter 的常识:
- 也被称为存取方法,是访问方法(access methods)中最常用的两个
- 用来封装私有成员方法,以隔离外界对其的直接访问
- 也可以在存取过程中添加其他的逻辑,保证了外部调用的简洁性
- 实现了对象或类内部逻辑的灵活性,保留了改变的可能
- 在很多 IDE 中可以自动生成
首先看看其他语言中一般的实现方式:
一种是传统的显式 getXXX()/setXXX(v) 方法调用
//JAVA public class People { private Integer _age; public Integer getAge() { return this._age; } public void setAge(Integer age) { this._age = age; } public static void main(String[] args) { People p = new People(); p.setAge(18); System.out.println(p.getAge().toString()); //18 }}
毫无疑问,显式调用命名其实是随意的,而且各种语言都能实现,另一种是隐式(implicit)的 getter/setter
//AS2 class Login2 { private var _username:String; public function get userName():String { return this._username; } public function set userName(value:String):Void { this._username = value; }}var lg = new Login2;lg.userName = "tom";trace(lg.userName); //"tom"//C# class People{ private string _name; public string name { get { return _name; } set { _name = value; } } } People p = new People();p.name = "tom";Console.WriteLine(p.name)//PHP class MyClass { private $firstField; private $secondField; public function __get($property) { if (property_exists($this, $property)) { return $this->$property; } } public function __set($property, $value) { if (property_exists($this, $property)) { $this->$property = $value." world"; } }}$mc = new MyClass;$mc->firstField = "hello";echo $mc->firstField; //"hello world" 隐式存取方法需要特定语言的支持,使用起来感觉就是读取属性(var x = obj.x)或给属性赋值(obj.x = "foo")
[1.2] ES5 中的 getter 和 setter
从 2011 年的 ECMAScript 5.1(ECMA-262)规范开始,JavaScript 也开始支持 getter/setter;形式上,自然是和同为 ECMAScript 实现的 AS2/AS3 相同
getter 的语法:
// prop 指的是要绑定到给定函数的属性名{get prop() { ... } }// 还可以使用一个计算属性名的 expression 绑定到给定的函数, 注意浏览器兼容性{get [expression]() { ... } }
例子:
var obj = { log: ['example','test'], get latest() { if (this.log.length == 0) return undefined; return this.log[this.log.length - 1]; }}console.log(obj.latest); // "test"var expr = 'foo';var obj2 = { get [expr]() { return 'bar'; }};console.log(obj2.foo); // "bar"
使用 get 语法时应注意以下问题:
- 可以使用数值或字符串作为标识
- 必须不带参数
- 不能与另一个get或具有相同属性的数据条目的对象字面量中出现
通过 delete 操作符删除 getter:
delete obj.latest;
以下展示了一种进阶的用法,即首次调用时才取值(lazy getter),并且将 getter 转为普通数据属性:
get notifier() { delete this.notifier; return this.notifier = document.getElementById('myId');},
setter 的语法:
//prop 指的是要绑定到给定函数的属性名//val 指的是分配给prop的值{set prop(val) { . . . }}// 还可以使用一个计算属性名的 expression 绑定到给定的函数, 注意浏览器兼容性{set [expression](val) { . . . }}
使用 set 语法时应注意以下问题:
- 标识符可以是数字或字符串
- 必须有一个明确的参数
- 在同一个对象中,不能为一个已有真实值的变量使用 set ,也不能为一个属性设置多个 set
例子:
var language = { set current(name) { this.log.push(name); }, log: []}language.current = 'EN';console.log(language.log); // ['EN']language.current = 'FA';console.log(language.log); // ['EN', 'FA']var expr = "foo";var obj = { baz: "bar", set [expr](v) { this.baz = v; }};console.log(obj.baz); // "bar"obj.foo = "baz"; // run the setterconsole.log(obj.baz); // "baz"
setter 可以用delete操作来移除:
delete o.current;
[1.4] 用 Object.defineProperty() 精确定义对象成员
回顾前面提到过的,对象里存在的属性描述符有两种主要形式:数据属性和存取方法。描述符必须是两种形式之一,不能同时是两者。
并且在一般情况下,通过赋值来为对象添加的属性,可以由 for...in 或 Object.keys 方法遍历枚举出来;且通过这种方式添加的属性值可以被改变,也可以被删除。
var obj = { _c: 99, get c() { return this._c; }};obj.a = 'foo';obj.b = function() { alert("hello world!");};console.log( Object.keys(obj) ); //["_c", "c", "a", "b"]for (var k in obj) console.log(k); //"_c", "c", "a", "b"delete obj.b;delete obj.c;console.log(obj.b, obj.c); //undefined, undefined
对于这样定义的数据属性或存取方法,无法控制其是否可被 delete,也无法限制其是否能被枚举
而使用 Object.defineProperty() 则允许改变这些默认设置
同样从 ECMAScript 5.1 规范开始,定义了 Object.defineProperty() 方法。用于直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象
其语法为:
//obj 需要被操作的目标对象//prop 目标对象需要定义或修改的属性的名称//descriptor 将被定义或修改的属性的描述符Object.defineProperty(obj, prop, descriptor)
其中 descriptor 可以设置的属性为:
属性 描述 应用于 configurable 是否能被修改及删除 数据属性、存取方法 enumerable 是否可被枚举 数据属性、存取方法 value 属性值 数据属性 writable 是否能被赋值运算符改变 数据属性 get getter 方法 存取方法 set setter 方法 存取方法 需要了解的是,从 IE8 开始有限支持这个方法(非 DOM 对象不可用)
例子:
var o = {};o.a = 1;// 等同于 :Object.defineProperty(o, "a", { value : 1, writable : true, configurable : true, enumerable : true});var o = {};var bValue;Object.defineProperty(o, "b", { get : function(){ //添加存取方法 return bValue; }, set : function(newValue){ bValue = newValue; }, enumerable : true, configurable : true});var o = {};Object.defineProperty(o, "a", { value : 37, writable : false //定义了一个“只读”的属性});console.log(o.a); // 37o.a = 25; // 在严格模式下会抛出错误,非严格模式只是不起作用console.log(o.a); // 37var o = {};Object.defineProperty(o, "a", { get : function(){return 1;}, configurable : false //不可编辑、不可删除});// throws a TypeErrorObject.defineProperty(o, "a", {configurable : true}); // throws a TypeErrorObject.defineProperty(o, "a", {enumerable : true}); // throws a TypeErrorObject.defineProperty(o, "a", {set : function(){}}); // throws a TypeErrorObject.defineProperty(o, "a", {get : function(){return 1;}});// throws a TypeErrorObject.defineProperty(o, "a", {value : 12});console.log(o.a); //1delete o.a; // 在严格模式下会抛出TypeError,非严格模式只是不起作用console.log(o.a); //1Object.defineProperty(o, "conflict", { value: 0x9f91102, get: function() { return 0xdeadbeef; } }); //抛出 TypeError,数据属性和存取方法不能混合设置相关方法:Object.getOwnPropertyDescriptor()
返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,而非从原型链上进行查找的属性)
语法:
//其中 prop 对应于 Object.defineProperty() 中第三个参数 descriptorObject.getOwnPropertyDescriptor(obj, prop)
例子:
var o = { get foo() { return 17; }};Object.getOwnPropertyDescriptor(o, "foo");// {// configurable: true,// enumerable: true,// get: /*the getter function*/,// set: undefined// }相关方法:Object.defineProperties()
直接在一个对象上定义多个新的属性或修改现有属性
语法:
//prop 和 descriptor 的定义对应于 Object.defineProperty()Object.defineProperties(obj, { prop1: descriptor1, prop2: descriptor2, ...})
例子:
var obj = {};Object.defineProperties(obj, { 'property1': { value: true, writable: true }, 'property2': { value: 'Hello', writable: false }});
相关方法:Object.create()
使用指定的原型对象及其属性去创建一个新的对象
语法:
//proto 为新创建对象的原型对象//props 对应于 Object.defineProperties() 中的第二个参数Object.create(proto[, props])
例子:
// 创建一个原型为null的空对象 var o = Object.create(null);var o2 = {};// 以字面量方式创建的空对象就相当于: var o2 = Object.create(Object.prototype);var foo = {a:1, b:2};var o = Object.create(foo, { // foo会成为所创建对象的数据属性 foo: { writable:true, configurable:true, value: "hello" }, // bar会成为所创建对象的访问器属性 bar: { configurable: false, get: function() { return 10 }, set: function(value) { console.log("Setting `o.bar` to", value); } }});[1.5] __define[G,S]etter__()
作为非标准和已废弃的方法,defineGetter() 和 defineSetter() 有时会出现在一些历史代码中,并仍能运行在 Firefox/Safari/Chrome 等浏览器中
直接看例子:
var o = { word: null};o.__defineGetter__('gimmeFive', function() { return 5;});console.log(o.gimmeFive); // 5o.__defineSetter__('say', function(vlu) { this.word = vlu;});o.say = "hello";console.log(o.word); //"hello"[1.6] __lookup[G,S]etter__()
同样,还有 lookupGetter() 和 lookupSetter() 两个非标准和已废弃的方法
- lookupGetter() 会返回对象上某个属性的 getter 函数
例子:
var obj = { get foo() { return Math.random() > 0.5 ? "foo" : "bar"; }};obj.__lookupGetter__("foo") // (function (){return Math.random() > 0.5 ? "foo" : "bar"})
如果换成标准的方法,则是:
Object.getOwnPropertyDescriptor(obj, "foo").get// (function (){return Math.random() > 0.5 ? "foo" : "bar"})
而如果那个访问器属性是继承来的:
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(obj), "foo").get // function __proto__() {[native code]}
- lookupSetter() 会返回对象的某个属性的 setter 函数
例子:
var obj = { set foo(value) { this.bar = value; }};obj.__lookupSetter__('foo')// (function(value) { this.bar = value; })// 标准且推荐使用的方式。Object.getOwnPropertyDescriptor(obj, 'foo').set;// (function(value) { this.bar = value; })
[1.7] 用 onpropertychange 兼容古早浏览器
在某些要求兼容 IE6/IE7 等浏览器的极端情况下,利用 IE 支持的 onpropertychange 事件,也是可以模拟 getter/setter 的
要注意这种方法仅限于已加载到文档中的 DOM 对象
function addProperty(obj, name, onGet, onSet) { var oldValue = obj[name], getter = function () { return onGet.apply(obj, [oldValue]); }, setter = function (newValue) { return oldValue = onSet.apply(obj, [newValue]); }, onPropertyChange = function (event) { if (event.propertyName == name) { // 暂时移除事件监听以免循环调用 obj.detachEvent("onpropertychange", onPropertyChange); // 把改变后的值传递给 setter var newValue = setter(obj[name]); // 设置 getter obj[name] = getter; obj[name].toString = getter; // 恢复事件监听 obj.attachEvent("onpropertychange", onPropertyChange); } }; // 设置 getter obj[name] = getter; obj[name].toString = getter; obj.attachEvent("onpropertychange", onPropertyChange);}
2. JS中的代理和反射
在对象本身上,一个个属性的定义访问控制,有时会带来代码臃肿,甚至难以维护;了解代理和反射的概念和用法,可以有效改善这些状况。
[2.1] 传统的代理模式
在经典的设计模式(Design Pattern)中,代理模式(Proxy Pattern)被广泛应用;其定义为:
在代理模式中,一个代理对象(Proxy)充当着另一个目标对象(Real Subject)的接口。代理对象居于目标对象的用户(Client)和目标对象本身的中间,并负责保护对目标对象的访问。
典型的应用场景为:
- 对目标对象的访问控制和缓存
- 延迟目标对象的初始化
- 访问远端对象
举个例子:
function Book(id, name) { this.id = id; this.name = name;}function BookShop() { this.books = {};}BookShop.prototype = { addBook: function(book) { this.books[book.id] = book; }, findBook: function(id) { return this.books[id]; }}function BookShopProxy() {}BookShopProxy.prototype = { _init: function() { if (this.bookshop) return; else this.bookshop = new BookShop; }, addBook: function(book) { this._init(); if (book.id in this.bookshop.books) { console.log('existed book!', book.id); return; } else { this.bookshop.addBook(book); } }, findBook: function(id) { this._init(); if (id in this.bookshop.books) return this.bookshop.findBook(id); else return null; }}var proxy = new BookShopProxy;proxy.addBook({id:1, name:"head first design pattern"});proxy.addBook({id:2, name:"thinking in java"});proxy.addBook({id:3, name:"lua programming"});proxy.addBook({id:2, name:"thinking in java"}); //existed book! 2 console.log(proxy.findBook(1)); //{ id: 1, name: 'head first design pattern' } console.log(proxy.findBook(3)); //{ id: 3, name: 'lua programming' }
显然,以上示例代码中展示了使用代理来实现延迟初始化和访问控制。
值得一提的是,代理模式与设计模式中另一种装饰者模式(Decorator Pattern)容易被混淆,两者的相同之处在于都是对原始的目标对象的包装;不同之处在于,前者着眼于提供与原始对象相同的API,并将对其的访问控制保护起来,而后者则侧重于在原有API的基础上添加新的功能。
[2.2] ES6 中的 Proxy
在 ECMAScript 2015 (6th Edition, ECMA-262) 标准中,提出了原生的 Proxy 对象。用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)
语法:
let p = new Proxy(target, handler);
proxy 对象的目标对象 target,可以是任何类型的对象,如 Object、Array、Function,甚至另一个 Proxy 对象;在进行let proxy=new Proxy(target,handle)的操作后,proxy、target两个对象会相互影响。即:
let target = { _prop: 'foo', prop: 'foo'};let proxy = new Proxy(target, handler);proxy._prop = 'bar';target._attr = 'new' console.log(target._prop) //'bar' console.log(proxy._attr) //'new'
而 handler 也是一个对象,其若干规定好的属性是定义好一个个函数,表示了当执行目标对象的对应访问时所执行的操作;最常见的操作是定义 getter/setter 的 get 和 set 属性:
let handler = { get (target, key){ return key in target ? target[key] : -1; //默认值 }, set (target, key, value) { if (key === 'age') { //校验 target[key] = value > 0 && value < 100 ? value : 0 } return true; }};let target = {};let proxy = new Proxy(target, handler);proxy.age = 22 //22
可以注意到,和 ES5 中对象本身的 setter 不同的是, proxy 中的 setter 必须有返回值;
并且应该也很容易理解,不光是名字相同,Proxy 对象也的确符合经典的代理模式 -- 由代理对象对目标对象的 API 进行封装和保护,隐藏目标对象,控制对其的访问行为。
除了可以定义 getter/setter,较完整的 handler 属性如下:
- "get": function (oTarget, sKey)
- "set": function (oTarget, sKey, vValue)
- "enumerate": function (oTarget, sKey)
- "ownKeys": function (oTarget, sKey)
- "has": function (oTarget, sKey)
- "defineProperty": function (oTarget, sKey, oDesc)
- "deleteProperty": function (oTarget, sKey)
- "getOwnPropertyDescriptor": function (oTarget, sKey)
- "getPrototypeOf(oTarget)"
- "setPrototypeOf(oTarget, oPrototype)"
- "apply(oTarget, thisArg, argumentsList)":
- "construct(oTarget, argumentsList, newTarget)"
- "isExtensible(oTarget)"
- "preventExtensions(oTarget)"
[2.3] 反射
对象的反射(reflection)是一种在运行时(runtime)探查和操作对象属性的语言能力。
在 JAVA/AS3 等语言中,反射一般被用于在运行时获取某个对象的类名、属性列表,然后再动态构造等;比如通过 XML 配置文件中的值动态创建对象,或者根据名称提取 swf 文件中的 MovieClip 等。
JS 本来也具有相关的反射API,比如 Object.getOwnPropertyDescriptor() 、Function.prototype.apply()、in、delete等,但这些 API 分布在不同的命名空间甚至全局保留字中,并且执行失败时是以抛出异常的方式进行的。这些因素使得涉及到对象反射的代码难以书写和维护。
[2.4] ES6 中的 Reflect
和 Proxy 同时,在 ECMAScript 2015 (6th Edition, ECMA-262) 中,引入了 Reflect 对象,用来囊括对象反射的若干方法。
反射方法 相似操作 Reflect.apply() Function.prototype.apply() Reflect.construct() new target(...args) Reflect.defineProperty() Object.defineProperty() Reflect.deleteProperty() delete target[name] Reflect.enumerate() 供 for...in 操作遍历到的属性 Reflect.get() 类似于 target[name] Reflect.getOwnPropertyDescriptor() Object.getOwnPropertyDescriptor() Reflect.getPrototypeOf() Object.getPrototypeOf() Reflect.has() in 运算符 Reflect.isExtensible() Object.isExtensible() Reflect.ownKeys() Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target)) Reflect.preventExtensions() Object.preventExtensions() Reflect.set() target[name] = val Reflect.setPrototypeOf() Object.setPrototypeOf()
- Reflect 与 ES5 的 Object 有点类似,包含了对象语言内部的方法,Reflect 也有和 Proxy 互相一一对应的若干种方法。
- Proxy 相当于去修改设置对象的属性行为,而Reflect则是获取对象的这些行为(的原始版本)。两者经常搭配使用。
- Reflect 没有构造函数,可被调用的都是其静态方法。
var target = {
a: 1
};
var proxy = new Proxy(target, {
get: function(tgt, key) {
console.log("Get %s", key);
return tgt[key] + 100;
},
set: function(tgt, key, val) {
console.log("Set %s = %s", key, val);
return tgt[key] = "VAL_" + val;
}
});
proxy.a = 2;
//Set a = 2 console.log(proxy.a);
//Get a //VAL_2100 console.log(Reflect.get(target, "a"));
//VAL_2 Reflect.set(target, "a", 3);
console.log(Reflect.get(target, "a"));
// 3
可以看到,如果直接在 Proxy 中存取目标对象的值,很可能调用多余的 getter/setter;而搭配 Reflect 中对应的方法使用则可有效避免此情况
同时应注意到,在执行失败时,这些方法并不抛出错误,而是返回 false;这极大的简化了处理:
//In ES5 var o = {};Object.defineProperty(o, 'a', { get: function() { return 1; }, configurable: false});try { Object.defineProperty(o, 'a', { configurable: true });} catch(e) { console.log("Exception");}//In ES2015 var o = {};Reflect.defineProperty(o, 'a', { get: function() { return 1; }, configurable: false});if( !Reflect.defineProperty(o, 'a', { configurable: true }) ) { console.log("Operation Failed");}
[2.5] 配合使用 Proxy/Reflect
例子1:为对象的每个属性设置 getter/setter
//in ES5 var obj = { x: 1, y: 2, z: 3};function trace1() { var cache = {}; Object.keys(obj).forEach(function(key) { cache[key] = obj[key]; //避免循环 setter Object.defineProperty(obj, key, { get: function() { console.log('Get ', key); return cache[key]; }, set: function(vlu) { console.log('Set ', key, vlu); cache[key] = vlu; } }) });}trace1();obj.x = 5;console.log(obj.z);// Set x 5 // Get z // 3//in ES6 var obj2 = { x: 6, y: 7, z: 8};function trace2() { return new Proxy(obj2, { get(target, key) { if (Reflect.has(target, key)) { console.log('Get ', key); } return Reflect.get(target, key); }, set(target, key, vlu) { if (Reflect.has(target, key)) { console.log('Set ', key, vlu); } return Reflect.set(target, key, vlu); } });}const proxy2 = trace2();proxy2.x = 99;console.log(proxy2.z);// Set x 99 // Get z // 8
例子2:跟踪方法调用
var obj = { x: 1, y: 2, say: function(word) { console.log("hello ", word) }};var proxy = new Proxy(obj, { get(target, key) { const targetValue = Reflect.get(target, key); if (typeof targetValue === 'function') { return function (...args) { console.log('CALL', key, args); return targetValue.apply(this, args); } } else { console.log('Get ', key); return targetValue; } }});proxy.x;proxy.y;proxy.say('excel!');// Get x // Get y // CALL say [ 'excel!' ] // hello excel!
总结
- getter/setter 也被称为存取方法,是访问方法中最常用的两个
- 可以用访问方法封装保护原对象,并保留逻辑的灵活性
- ES5 中开始支持了隐式的 get 和 set 访问方法,可以通过 delete 删除
- 使用 使用 Object.defineProperty() 也可以设置 getter/setter 等
- 历史上利用 Object.prototype.define[G,S]etter() 和 onpropertychange 实现存取方法的兼容
- 可以利用代理和反射改善传统的访问控制
- 代理对象居于目标对象的用户和目标对象本身的中间,并负责保护对目标对象的访问
- ES6 原生的 Proxy 对象。用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)
- 对象的反射是一种在运行时探查和操作对象属性的语言能力
- ES6 引入了 Reflect 对象,用来囊括对象反射的若干方法
- Reflect 有和 Proxy 一一对应的若干种方法,经常搭配使用