在 Javascript 中,读取、赋值、调用方法等等,几乎一切操做都是围绕“对象”展开的;长久以来,如何更好的了解和控制这些操做,就成了该语言发展中的重要问题。javascript
所谓 getter/setter,其定义通常为:php
一些 getter/setter 的常识:html
首先看看其余语言中通常的实现方式:java
一种是传统的显式 getXXX()/setXXX(v)
方法调用es6
//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
}
}复制代码
毫无疑问,显式调用命名实际上是随意的,并且各类语言都能实现express
另外一种是隐式(implicit)的 getter/settersegmentfault
//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"
)设计模式
从 2011 年的 ECMAScript 5.1 (ECMA-262) 规范开始,JavaScript 也开始支持 getter/setter;形式上,天然是和同为 ECMAScript 实现的 AS2/AS3 相同api
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 语法时应注意如下问题:
经过 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 语法时应注意如下问题:
🌰 例子:
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 setter
console.log(obj.baz); // "baz"复制代码
setter 能够用delete操做来移除:
delete o.current;复制代码
回顾前面提到过的,对象里存在的属性描述符有两种主要形式:数据属性和存取方法。描述符必须是两种形式之一,不能同时是二者。
而且在通常状况下,经过赋值来为对象添加的属性,能够由 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); // 37
o.a = 25; // 在严格模式下会抛出错误,非严格模式只是不起做用
console.log(o.a); // 37复制代码
var o = {};
Object.defineProperty(o, "a", {
get : function(){return 1;},
configurable : false //不可编辑、不可删除
});
// throws a TypeError
Object.defineProperty(o, "a", {configurable : true});
// throws a TypeError
Object.defineProperty(o, "a", {enumerable : true});
// throws a TypeError
Object.defineProperty(o, "a", {set : function(){}});
// throws a TypeError
Object.defineProperty(o, "a", {get : function(){return 1;}});
// throws a TypeError
Object.defineProperty(o, "a", {value : 12});
console.log(o.a); //1
delete o.a; // 在严格模式下会抛出TypeError,非严格模式只是不起做用
console.log(o.a); //1复制代码
Object.defineProperty(o, "conflict", {
value: 0x9f91102,
get: function() {
return 0xdeadbeef;
}
}); //抛出 TypeError,数据属性和存取方法不能混合设置复制代码
返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,而非从原型链上进行查找的属性)
语法:
//其中 prop 对应于 Object.defineProperty() 中第三个参数 descriptor
Object.getOwnPropertyDescriptor(obj, prop)复制代码
🌰 例子:
var o = {
get foo() {
return 17;
}
};
Object.getOwnPropertyDescriptor(o, "foo");
// {
// configurable: true,
// enumerable: true,
// get: /*the getter function*/,
// set: undefined
// }复制代码
直接在一个对象上定义多个新的属性或修改现有属性
语法:
//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
}
});复制代码
使用指定的原型对象及其属性去建立一个新的对象
语法:
//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);
}
}
});复制代码
__define[G,S]etter__()
做为非标准和已废弃的方法,defineGetter() 和 defineSetter() 有时会出如今一些历史代码中,并仍能运行在 Firefox/Safari/Chrome 等浏览器中
🌰 直接看例子:
var o = {
word: null
};
o.__defineGetter__('gimmeFive', function() {
return 5;
});
console.log(o.gimmeFive); // 5
o.__defineSetter__('say', function(vlu) {
this.word = vlu;
});
o.say = "hello";
console.log(o.word); //"hello"复制代码
__lookup[G,S]etter__()
一样,还有 lookupGetter() 和 lookupSetter() 两个非标准和已废弃的方法
🌰 例子:
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]}复制代码
🌰 例子:
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; })复制代码
在某些要求兼容 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);
}复制代码
在对象自己上,一个个属性的定义访问控制,有时会带来代码臃肿,甚至难以维护;了解代理和反射的概念和用法,能够有效改善这些情况。
在经典的设计模式(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的基础上添加新的功能。
在 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 属性以下:
对象的反射(reflection)是一种在运行时(runtime)探查和操做对象属性的语言能力。
在 JAVA/AS3 等语言中,反射通常被用于在运行时获取某个对象的类名、属性列表,而后再动态构造等;好比经过 XML 配置文件中的值动态建立对象,或者根据名称提取 swf 文件中的 MovieClip 等。
JS 原本也具备相关的反射API,好比 Object.getOwnPropertyDescriptor()
、Function.prototype.apply()
、in
、delete
等,但这些 API 分布在不一样的命名空间甚至全局保留字中,而且执行失败时是以抛出异常的方式进行的。这些因素使得涉及到对象反射的代码难以书写和维护。
和 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() |
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");
}复制代码
🌰 例子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!复制代码