- 使用 getter 和 setter 控制访问对象的属性
- 通过代理控制对象的访问
- 使用代理解决交叉访问的问题
function Ninja(level) {
this.skillLevel = level;
}
const ninja = new Ninja(100);
定义了构造函数 Ninja,使用该构造函数创建实例 ninja,它仅具有一个属 性 skillLevel
。然后,如果我们想要改变属性 skillLevel
,我们可以通过代码实现: ninja.skillLevel = 20
。
- 我们需要避免意外的错误发生,例如错误赋值。举例来说,需要避免赋了错误 类型的值:
ninja.skillLevel = "high"
。 - 我们需要记录
skillLevel
属性的变化。 - 我们需要在网页的 UI 中显示
skillLevel
属性的值。我们自然需要显示skillLevel
属性的更新值,但是如何轻松地做到这一点呢?
通过 getter
和 setter
方法,我们可以很优雅地实现这一切。
使用 getter 和 setter 保护私有属性
function Ninja() {
let skillLevel;
this.getSkillLevel = () => skillLevel;
this.setSkillLevel = value => {
skillLevel = value;
};
}
const ninja = new Ninja();
ninja.setSkillLevel(100);
console.log(ninja.getSkillLevel() === 100);
在 JavaScript 中,可以通过两种方式定义 getter 和 setter。
- 通过对象字面量定义,或在 ES6 的 class 中定义。
- 通过使用内置的 Object.defineProperty 方法。
在对象字面量中定义 getter 和 setter
const ninjaCollection = {
ninjas: ['Yoshi', 'Kuma', 'Hattori'],
get firstNinja() {
report('Getting firstNinja');
return this.ninjas[0];
},
set firstNinja(value) {
report('Setting firstNinja');
this.ninjas[0] = value;
}
};
console.log(ninjaCollection.firstNinja === 'Yoshi');
ninjaCollection.firstNinja = 'Hachi';
console.log(
ninjaCollection.firstNinja === 'Hachi' &&
ninjaCollection.ninjas[0] === 'Hachi'
);
在 ES6 的 class 中使用 getter 和 setter
class NinjaCollection {
constructor() {
this.ninjas = ['Yoshi', 'Kuma', 'Hattori'];
}
get firstNinja() {
report('Getting firstNinja');
return this.ninjas[0];
}
set firstNinja(value) {
report('Setting firstNinja');
this.ninjas[0] = value;
}
}
const ninjaCollection = new NinjaCollection();
针对指定的属性不一定需要同时定义 getter 和 setter。例如,通常我们仅提供 getter。如果在某些情况下需要写入属性值,具体的行为取决于代码是在严格模式还是非严格模式。如果在非严格模式下,对仅有 getter 的属性赋值不起作用,JavaScript 引擎默默地忽略我们的请求。另一方面,如果在严格模式下,JavaScript 引擎将会抛出异常,表明我们试图将给一个仅有 getter 没有 setter 的属性赋值。
Object.defineProperty
function Ninja() {
let _skillLevel = 0;
Object.defineProperty(this, 'skillLevel', {
get: () => {
console.log('The get method is called');
return _skillLevel;
},
set: value => {
console.log('The set method is called');
_skillLevel = value;
}
});
}
const ninja = new Ninja();
console.log(typeof ninja._skillLevel === 'undefined');
console.log(ninja.skillLevel === 0);
ninja.skillLevel = 10;
console.log(ninja.skillLevel === 10);
function Ninja() {
let _skillLevel = 0;
Object.defineProperty(this, 'skillLevel', {
get: () => _skillLevel,
set: value => {
if (!Number.isInteger(value)) {
throw new TypeError('Skill level should be a number');
}
_skillLevel = value;
}
});
}
const ninja = new Ninja();
ninja.skillLevel = 10;
console.log(ninja.skillLevel === 10);
try {
ninja.skillLevel = 'Great';
console.log('Should not be here');
} catch (e) {
console.log('Setting a non-integer value throws an exception');
}
const shogun = {
name: 'Yoshiaki',
clan: 'Ashikaga',
get fullTitle() {
return this.name + ' ' + this.clan;
},
set fullTitle(value) {
const segments = value.split(' ');
this.name = segments[0];
this.clan = segments[1];
}
};
console.log(shogun.name === 'Yoshiaki');
console.log(shogun.clan === 'Ashikaga');
console.log(shogun.fullTitle === 'Yoshiaki Ashikaga');
shogun.fullTitle = 'Ieyasu Tokugawa';
console.log(shogun.name === 'Ieyasu');
console.log(shogun.clan === 'Tokugawa');
console.log(shogun.fullTitle === 'Ieyasu Tokugawa');
可以将代理 理解为通用化的 setter 与 getter,区别是每个 setter 与 getter 仅能控制单个对象属性,而代理可用于对象交互的通用处理,包括调用对象的方法。
通过 Proxy 构造器创建代理
const emperor = { name: 'Komei' };
const representative = new Proxy(emperor, {
get: (target, key) => {
console.log('Reading ' + key + ' through a proxy');
return key in target ? target[key] : "Don't bother the emperor!";
},
set: (target, key, value) => {
console.log('Writing ' + key + ' through a proxy');
target[key] = value;
}
});
console.log(emperor.name === 'Komei');
console.log(representative.name === 'Komei');
console.log(emperor.nickname === undefined);
console.log(representative.nickname === "Don't bother the emperor!");
representative.nickname = 'Tenno';
console.log(emperor.nickname === 'Tenno');
console.log(representative.nickname === 'Tenno');
需要强调的是,激活代理方法与 getter 和 setter 是一致的。一旦执行交互(如访问代理对象属性),就会隐式调用对应的 get 方法,此时 JavaScript 引擎的执行过程与显示调用的普通函数类似。
在本例中,我们使用 get 与 set,还有许多其他的内置方法用于定义各种对象的行为。例如:
- 调用函数时激活
apply
,使用new
操作符时激活construct
。 - 读取/写入属性时激活
get
与set
。 - 执行
for-in
语句时激活enumerate
。 - 获取和设置属性值时激活
getPrototypeOf
与setPropertyOf
。
现在我们知道了代理的工作原理以及如何创建代理对象,让我们研究一些实际用处,比如如何使用代理记录日志、性能测量、自动填充属性和实现可以进行负索引的数组。
不使用代理实现日志记录
function Ninja() {
let _skillLevel = 0;
Object.defineProperty(this, 'skillLevel', {
get: () => {
console.log('skillLevel get method is called');
return _skillLevel;
},
set: value => {
console.log('skillLevel set method is called');
_skillLevel = value;
}
});
}
const ninja = new Ninja();
ninja.skillLevel;
ninja.skillLevel = 4;
使用代理更易于在对象上添加日志
function makeLoggable(target) {
return new Proxy(target, {
get: (target, property) => {
console.log('Reading ' + property);
return target[property];
},
set: (target, property, value) => {
console.log('Writing value ' + value + ' to ' + property);
target[property] = value;
}
});
}
let ninja = { name: 'Yoshi' };
ninja = makeLoggable(ninja);
console.log(ninja.name === 'Yoshi');
ninja.weapon = 'sword';
例如我们想要评估计算一个数值是否是素数的函数的性能:
function isPrime(number) {
if (number < 2) {
return false;
}
for (let i = 2; i < number; i++) {
if (number % i === 0) {
return false;
}
}
return true;
}
isPrime = new Proxy(isPrime, {
apply: (target, thisArg, args) => {
console.time('isPrime');
const result = target.apply(thisArg, args);
console.timeEnd('isPrime');
return result;
}
});
isPrime(1299827);
简单定义 isPrime
方法,我们需要评估 isPrime
函数的性能,并且不能修改该函数的代码。我们可以使用代理包装该函数,添加一个一旦调用该函数就会被触发的方法:
isPrime = new Proxy(isPrime, {
apply: (target, thisArg, args) => {
//...
}
});
使用 isPrime
函数作为代理的目标对象。同时,添加 apply
方法,当调用 isPrime
函数时就会调用 apply
方法。我们将新创建的代理对象赋值给 isPrime
标识符。这样,我们无需修改 isPrime
函数内部代码,就可以调用 apply
方法实现 isPrime
函数的性能评估,程序代码的其余部分可以完全无视这些变化。
假设需要抽象计算机的文件夹结 构模型,一个文件夹对象既可以有属性,也可以是文件夹。现在假设你需要长路径的文件模型,如:
rootFolder.ninjasDir.firstNinjaDir.ninjaFile = 'yoshi.txt';
为了创建这个长路径文件模型,你可能会按照以下思路设计代码:
const rootFolder = new Folder();
rootFolder.ninjasDir = new Folder();
rootFolder.ninjasDir.firstNinjaDir = new Folder();
rootFolder.ninjasDir.firstNinjaDir.ninjaFile = 'yoshi.txt';
使用代理自动填充属性
function Folder() {
return new Proxy(
{},
{
get: (target, property) => {
console.log('Reading ' + property);
if (!(property in target)) {
target[property] = new Folder();
}
return target[property];
}
}
);
}
const rootFolder = new Folder();
try {
rootFolder.ninjasDir.firstNinjaDir.ninjaFile = 'yoshi.txt';
console.log('An exception wasn’t raised');
} catch (e) {
console.log('An exception has occurred');
}
使用负索引来逆向检索数组元素
const ninjas = ['Yoshi', 'Kuma', 'Hattori'];
ninjas[0]; //"Yoshi"
ninjas[1]; //"Kuma"
ninjas[2]; //"Hattori"
ninjas[-1]; //undefined
ninjas[-2]; //undefined
ninjas[-3]; //undefined
JavaScript 不支持数组负索引,但是,我们可以使用代理进行模拟。
使用代理实现数组负索引
function createNegativeArrayProxy(array) {
if (!Array.isArray(array)) {
throw new TypeError('Expected an array');
}
return new Proxy(array, {
// 返回新的代理。该代理使用传入的数组作为代理目标
get: (target, index) => {
// 当读取数组元素时调用 get 方法。
index = +index; // 使用一元+操作符将属性名变成的数值。
return target[index < 0 ? target.length + index : index]; // 如果访问的是负向索引,则逆向访问数组。如果访问的是正向索引,则正常访问数组。
},
set: (target, index, val) => {
index = +index;
return (target[index < 0 ? target.length + index : index] = val);
}
});
}
const ninjas = ['Yoshi', 'Kuma', 'Hattori'];
const proxiedNinjas = createNegativeArrayProxy(ninjas);
console.log(
ninjas[0] === 'Yoshi' && ninjas[1] === 'Kuma' && ninjas[2] === 'Hattori'
);
console.log(
proxiedNinjas[0] === 'Yoshi' &&
proxiedNinjas[1] === 'Kuma' &&
proxiedNinjas[2] === 'Hattori'
);
console.log(
typeof ninjas[-1] === 'undefined' &&
typeof ninjas[-2] === 'undefined' &&
typeof ninjas[-3] === 'undefined'
);
console.log(
proxiedNinjas[-1] === 'Hattori' &&
proxiedNinjas[-2] === 'Kuma' &&
proxiedNinjas[-3] === 'Yoshi'
);
proxiedNinjas[-1] = 'Hachi';
console.log(proxiedNinjas[-1] === 'Hachi' && ninjas[2] === 'Hachi');
事实上,我们所有的操作都通过代理添加了一个间接层,使我们能够实现所有这些很酷的特性,但与此同时它引入了大量的额外的处理,会影响性能。为了测试性能问题,我们利用数组负索引的示例,比较正常数组访问元素时的执行时间和通过代理数组访问元素的执行时间:
function measure(items) {
const startTime = new Date().getTime();
for (let i = 0; i < 500000; i++) {
items[0] === 'Yoshi';
items[1] === 'Kuma';
items[2] === 'Hattori';
}
return new Date().getTime() - startTime;
}
const ninjas = ['Yoshi', 'Kuma', 'Hattori'];
const proxiedNinjas = createNegativeArrayProxy(ninjas);
console.log("Proxies are around", Math.round(measure(proxiedNinjas) / measure(ninjas)));
// chrome: Proxies are around 23
// firefox: Proxies are around 51
在 Chrome 浏览器,代理数组的执行时间大约为正常数组的 20 倍,在 Firefox 浏览器大约为 50 倍。尽管使用代理可以创造性地控制对象的访问,但是大量的控制操作将带来性能问题。可以在多性能不敏感的程序里使用代理,但是若多次执行代码时仍然要小心谨慎。像往常一样,我们建议你彻底地测试代码的性能。
- 我们可以使用 getter、setter 和代理监控对象。
- 通过使用访问器方法(getter 和 setter),我们可以对对象属性的访问进行控制。
- 可以通过内置的
Object.defineProperty
方法定义访问属性,或在对象字面量中使用get
和set
语法或 ES6 的class
。 - 当读取对象属性时会隐式调用
get
方法,当写入对象属性时隐式调用set
方法。 - 使用
getter
方法可以定义计算属性,在每次读取对象属性时计算属性值;同理,setter
方法可用于实现数据验证与日志记录。
- 可以通过内置的
- 代理是 JavaScript ES6 中引入的,可用于控制对象。
- 代理可以定制对象交互时行为(例如,当读取属性或调用方法时)。
- 所有的交互行为都必须通过代理,指定的行为发生时会调用代理方法。
- 使用代理可以优雅地实现以下内容。
- 日志记录。
- 性能测量。
- 数据校验。
- 自动填充对象属性(以此避免讨厌的 null 异常)。
- 数组负索引。
- 代理效率不高,所以在需要执行多次的代码中需要谨慎使用。建议进行性能 测试。