JavaScript 的 setter、getter 和 pro
今天来学习下 JavaScript 的对象中的 setter、getter 和 proxy。
对象属性值的 [[Get]] 和 [[Put]] 操作
我们对于对象属性值的常用操作无非就是创建、修改和读取(删除操作想必都用的不多)。而对象属性值获取其实是对象属性值的 [[Get]] 操作,对象属性值的创建和修改是对象属性值的 [[Put]] 操作。
[[Get]]
获取对象属性值就是一次属性访问,访问具体步骤为:
- 在语言规范中,对象实际上是实现了 [[Get]] 操作的。对象默认内置的 [[Get]] 操作会在对象中查找是否有名称相同的属性,并返回这个属性的值。(这里其实还要注意 getter)
- 如果当前对象中并没有找到属性,就根据原型链向下查找。如果在原型链中找到同名属性则返回属性的值(找原型对象属性的方式还是用 [[Get]] 操作),如果找到原型链底端都没有找到则返回 undefined。
[[Put]]
与 [[Get]] 操作相对,[[Put]] 操作一般用于设置或创建对象的属性。但是 [[Put]] 操作要比 [[Get]] 操作更加麻烦一些:
- 检查对象属性是否设置了 setter,如果是就调用 setter。
- 检查对象属性的属性描述符中的 writable 是否为 false,如果是则无法进行修改。
- 检查对象中是否存在这个属性,如果存在则设置属性的值。
- 遍历原型链,检查对象的原型链对象中是否有这个属性。如果原型链中没有,则在对象上创建这个属性。
- 如果对象的原型链对象中有这个属性,就比较麻烦了。它分为三种情况:
- 如果在对象的原型链对象上存在同名属性,且属性标识符是可写的(
writable:true
),那就会在对象上创建这个属性,如此对象上的属性将屏蔽原型链对象上属性,称为屏蔽属性。 - 如果在对象的原型链对象上存在同名属性,且属性标识符是只读的(
writable:false
),那么就无法继续赋值,在严格模式下还会报错。 - 如果在对象的原型链对象上存在同名属性,且属性是一个 setter,那么只会调用这个 setter。
- 如果在对象的原型链对象上存在同名属性,且属性标识符是可写的(
setter 和 getter
上面提到了一些 setter 和 getter 的知识,那它们到底是什么呢?
在 ES5 中,可以使用 getter 和 setter 改写对象属性的默认操作。getter 会在获取属性值时调用,setter 会在设置属性值时调用。getter 和 setter 都是隐藏函数。
下面是 setter 和 getter 的两种定义方式:
// 方式 1
var obj = {
get a() {
return this.__a__ + 100
},
set a(val) {
this.__a__ = val
}
};
// 方式 2
Object.defineProperty(obj, "b", {
get: function() {
return this.__b__ * -1
},
set: function (val) {
this.__b__= val / 2
}
});
// setters
obj.a = 12
obj.b = 20
// getters
console.log(obj.a) // 112
console.log(obj.b) // -10
console.log(obj) // { __a__: 12, __b__: 10 }
可以看到我们传入的值分别为 12 和 20,但是在保存到对象变量前由于 setter 的计算,最后保存的值为 12 和 10。而在读取 a 和 b 时,获取到了 getter 的计算结果。
注意:当对象属性使用访问描述符(setter & getter)后,JavaScript 将忽略 value 和 writable 属性描述符。
var obj = {
b: 123
} // { b: 123 }
Object.defineProperty(obj, "b", {
get: function() {
return this.__b__ * -1
},
set: function (val) {
this.__b__= val / 2
}
});
obj // {}
Object.getOwnPropertyDescriptor(obj, "b")
// {
// onfigurable: true,
// enumerable: true
// get: f (),
// set: f (val)
// }
Proxy
Proxy 用于封装一个普通对象,并返回一个新对象。它接收两个参数,第一个参数是被代理的普通对象,第二个参数是代理行为对象。
var obj1 = {
a: 1
} // { a: 1 }
var obj2 = new Proxy(obj1, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
}) // Proxy { a: 1 }
obj2.a
// getting a!
obj2.a++
// getting a!
// setting a!
obj1 // { a: 2 }
上面例子中使用 Proxy 代理了 obj1 对象的 setter 和 getter 行为,当 obj1 有 setter 或 getter 行为时都会先经过 proxy 中的 getter 和 setter 方法。本例中的代理方法使用 Reflect.set(...) 和 Reflect.get(...) 来设置和获取对象中的属性,所以 obj1 的属性随之改变。
下面是 Proxy 中实例方法的整理
- get 方法拦截属性的读取操作。
- set 方法拦截属性的赋值操作。
- apply 方法拦截函数的调用。
- has 方法拦截
HasProperty
操作,即查找对象中是否有某属性。可用来隐藏一些属性不被in
运算符发现。 - construct 方法拦截
new
指令。即在new
指令创建实例的时候可以对对象中的参数进行一些初始化修改操作。 - deleteProperty方法拦截
delete
指令,可用来保护某些对象属性无法被删除。 - defineProperty方法拦截了Object.defineProperty操作。
- getOwnPropertyDescriptor方法拦截Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者undefined
- getPrototypeOf方法主要用来拦截获取对象原型。
- isExtensible方法拦截Object.isExtensible操作。
- ownKeys方法用来拦截对象自身属性的读取操作。
- preventExtensions方法拦截Object.preventExtensions()。该方法必须返回一个布尔值,否则会被自动转为布尔值。
- setPrototypeOf方法主要用来拦截Object.setPrototypeOf方法。
apply方法的使用
var target = function () { return 'I am the target'; };
var handler = {
apply: function () {
return 'I am the proxy';
}
};
var p = new Proxy(target, handler);
console.log(p())
// "I am the proxy"
has方法的使用
var handler = {
has (target, key) {
if (key[0] === '_') {
return false;
}
return key in target;
}
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
console.log('_prop' in proxy)
// false
construct方法的使用
var p = new Proxy(function () {}, {
construct: function(target, args) {
console.log('called: ' + args.join(', '));
return { value: args[0] * 5 + 12 };
}
});
console.log(new p(1))
console.log(new p(1).value)
// call: 1
// { value: 17 }
// call: 1
// 17
代理在某些处理对象属性的场景下是非常好用的,这个之后我们可以继续探讨下~
参考资料
- 《你不知道的 JavaScript》
- ECMAScript6 入门
- setter - JavaScript | MDN
- getter - JavaScript | MDN