代码改变世界

【读书笔记】【深入理解ES6】#12-代理(Proxy)和反射(Reflection)API

2017-12-01 18:31  佳佳的博客  阅读(470)  评论(0编辑  收藏  举报

代理(Proxy)是一种可以拦截并改变底层JavaScript引擎操作的包装器,在新语言中通过它暴露内部运作的对象,从而让开发者可以创建内建的对象。

数组问题

在ECMAScript6出现之前,开发者不能通过自己定义的对象模仿JavaScript数组对象的行为方式。当给数组的特定元素赋值时,影响到该数组的length属性,也可以通过length属性修改数组元素。

let colors = ["red", "green", "blue"];

console.log(colors.length); // 3

colors[3] = "black";

console.log(colors.length); // 4
console.log(colors[3]); // "black"

colors.length = 2;

console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"

Note

数值属性和length属性具有这种非标准行为,因而在ECMAScript中数组被认为是奇异对象(exotic object,与普通对象相对)。

代理和反射

调用 new Proxy() 可创建代替其他目标(taget)对象的代理,它虚拟化了目标,所以二者看起来功能一致。

代理可以拦截 JavaScript 引擎内部目标的底层对象操作,这些底层操作被拦截后会触发响应特定操作的陷阱函数。

反射API可以Reflect对象的形式出现,对象中方法的默认特性与相同的底层操作一致,而代理可以覆写这些操作,每个代理陷阱对应一个命名和参数都相同的Reflect方法。

代理陷阱 覆写的特性 默认特性
get 读取一个属性值 Reflect.get()
set 写入一个属性值 Reflect.set()
has in 操作符 Reflect.has()
deleteProperty delete 操作符 Reflect.deleteProperty()
getPrototypeOf Object.getPrototypeOf() Reflect.getPrototypeOf()
setPrototypeOf Object.setPrototypeOf() Reflect.setPrototypeOf()
isExtensible Object.isExtensible() Reflect.isExtensible()
preventExtensions Object.preventExtensions() Reflect.preventExtensions()
getOwnPropertyDescriptor Object.getOwnPropertyDescriptor() Reflect.getOwnPropertyDescriptor()
defineProperty Object.defineProperty() Reflect.defineProperty()
ownKeys Object.keys()、Object.getOwnPropertyNames()和Object.getOwnPropertySymbols() Reflect.ownKeys()
apply 调用一个函数 Reflect.apply()
construct 用new调用一个函数 Reflect.construct()

创建一个简单的代理

Proxy构造函数有两个参数

  • 目标(target)
  • 处理程序(handler)
    处理程序是定义一个或多个陷阱的对象,在代理中,出了专门为操作定义的陷阱外,其余操作均使用默认特性。
    不使用任何陷阱的处理程序等价于简单的转发代理。
let target = {};
let proxy = new Proxy(target, {});

proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"

target.name = "target";
console.log(proxy.name); // "target"
console.log(target.name); // "target"

使用set陷阱验证属性

set陷阱接受4个参数:

  • trapTarget
    用于接收属性(代理的目标)的对象
  • key
    要写入的属性键(字符串或Symbol类型)
  • value
    被写入属性的值
  • receiver
    操作发生的对象(通常是代理)

Reflect.set()是set陷阱对应的反射方法和默认特性,它和set陷阱一样也接受同样的4个参数。

let target = {
    name: "target"
};

let proxy = new Proxy(target, {
    set(trapTarget, key, value, receiver) {
        if (!trapTarget.hasOwnProperty(key)) {
            if (isNaN(value)) {
                throw new TypeError("属性必须是数字");
            }
        }

        return Reflect.set(trapTarget, key, value, receiver);
    }
});

proxy.count = 1;
console.log(proxy.count); // 1
console.log(target.count); // 1

proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"

// 抛出错误:
// Uncaught TypeError: 属性必须是数字
proxy.anotherName = "proxy";

用get陷阱验证对象结构(Object Shape)

get陷阱接受3个参数:

  • trapTarget
    被读取属性的源的对象(代理的目标)
  • key
    被读取的属性键
  • value
    操作发生的对象(通常是代理)

Reflect.get()也接受同样3个参数并返回属性的默认值。

let proxy = new Proxy({}, {
    get(trapTarget, key, receiver) {
        if (!(key in receiver)) {
            throw new TypeError("属性" + key + "不存在");
        }

        return Reflect.get(trapTarget, key, receiver);
    }
});

proxy.name = "proxy";
console.log(proxy.name); // "proxy"

// 抛出错误:
// Uncaught TypeError: 属性nme不存在
console.log(proxy.nme);

使用has陷阱隐藏已有属性

可以用in操作符来检测给定对象中是否含有某个属性,如果自由属性或原型属性匹配的名称或Symbol就返回true。

let target = {
    value: 42
};

console.log("value" in target); // true
console.log("toString" in target); // true

在代理中使用has陷阱可以拦截这些in操作并返回一个不同的值。

in陷阱接受2个参数:

  • trapTarget
    读取属性的对象(代理的目标)
  • key
    要检查的属性值(字符串或Symbol)
let target = {
    name: "target",
    vlaue: 42
};

let proxy = new Proxy(target, {
    has (trapTarget, key) {
        if (key === "value") {
            return false;
        } else {
            return Reflect.has(trapTarget, key);
        }
    }
});

console.log("value" in proxy); // false
console.log("name" in proxy); // true
console.log("toString" in proxy); // true

用deleteProperty陷阱防止删除属性

delete操作符可以从对象中删除属性,如果成功则返回true,不成功则返回false。
在严格模式下,如果你尝试删除一个不可配置(nonconfigurable)属性则会导致程序抛出错误,而在非严格模式下只是返回false。

每当通过delete操作符删除对象属性时,deleteProperty陷阱都会被调用,它接受2个参数:

  • trapTarget
    要删除属性的对象(代理的目标)
  • key
    要删除的属性键(字符串或Symbol)

Reflect.deleteProperty()方法为deleteProperty陷阱提供默认实现,并且接受同样的两个参数。

let target = {
    name: "target",
    value: 42
};

let proxy = new Proxy(target, {
    deleteProperty(trapTarget, key) {
        if (key === "value") {
            return false;
        } else {
            return Reflect.deleteProperty(trapTarget, key);
        }
    }
});

console.log("value" in proxy); // true

let result1 = delete proxy.value;
console.log(result1); // false

console.log("value" in proxy); // true

console.log("name" in proxy); // true

let result2 = delete proxy.name;
console.log(result2); // true

console.log("name" in proxy); // false

原型代理陷阱

ES6中新增的Object.setPrototypeOf()方法,它被用于作为ES5中的Object.getPrototype()方法的补充。通过代理中的setPrototypeOf陷阱和getPrototypeOf陷阱可以拦截这两个方法的执行过程。

setPrototypeOf陷阱接受2个参数:

  • trapTarget
    接受原型设置的对象(代理的目标)
  • proto
    作为原型使用的对象

传入Object.setPrototypeOf()方法和Reflect.setPrototypeOf()方法的均是以上两个参数。
getPrototypeOf陷阱、Object.getPrototypeOf()方法和Reflect.getPrototypeOf()方法只接受参数trapTarget。

原型代理陷阱的运行机制

原型代理陷阱有一些限制:

  1. getPrototypeOf陷阱必须返回对象或null

  2. 在setPrototypeOf陷阱中,如果操作失败则返回的一定是false,此时Object.setPrototypeOf()会抛出错误,如果setPrototypeOf返回了任何不是false的值,那么Object.setPrototypeOf()便假设操作成功。

let target = {};
let proxy = new Proxy(target, {
    getPrototypeOf(trapTarget) {
        return null;
    },
    setPrototypeOf(trapTarget, proto) {
        return false;
    }
});

let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);

console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // false
console.log(proxyProto); // null

// 成功
Object.setPrototypeOf(target, {});

// 给不存在的属性赋值会抛出错误:
// Uncaught TypeError: 'setPrototypeOf' on proxy: trap returned falsish
Object.setPrototypeOf(proxy, {});

可以使用Reflect上的对应方法实现这两个陷阱的默认行为。

let target = {};
let proxy = new Proxy(target, {
    getPrototypeOf(trapTarget) {
        return Reflect.getPrototypeOf(trapTarget);
    },
    setPrototypeOf(trapTarget, proto) {
        return Reflect.setPrototypeOf(trapTarget, proto);
    }
});

let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);

console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // true

// 成功
Object.setPrototypeOf(target, {});

// 成功:
Object.setPrototypeOf(proxy, {});

对象可扩展性陷阱

ECMAScript 5 已经通过 Object.preventExtensions() 方法和 Object.isExtensible() 方法修正了对象的可扩展性;
ECMAScript 6 可以通过代理中的 preventExtensions 和 isExtensible 陷阱拦截这两个方法并调用底层对象。
两个陷阱都接受唯一参数 trapTarget 对象,并调用它上面的方法。
isExtensible 陷阱返回的一定是一个 boolean 值,表示对象是否可扩展;
preventExtensions 陷阱返回的也一定是布尔值,表示操作是否成功。

Reflect.preventExtensions() 方法和 Reflect.isExtensible() 方法实现了相应陷阱中的默认行为,二两都返回布尔值。

两个基础示例

默认实现:

let target = {};
let proxy = new Proxy(target, {
    isExtensible(trapTarget) {
        return Reflect.isExtensible(trapTarget);
    },
    preventExtensions(trapTarget) {
        return Reflect.preventExtensions(trapTarget);
    }
});

console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true

Object.preventExtensions(proxy);

console.log(Object.isExtensible(target)); // false
console.log(Object.isExtensible(proxy)); // false

使用陷阱使 Object.preventExtensions() 对 proxy 失效。

let target = {};
let proxy = new Proxy(target, {
    isExtensible(trapTarget) {
        return Reflect.isExtensible(trapTarget);
    },
    preventExtensions(trapTarget) {
        return false;
    }
});

console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true

// 抛出错误:
// Uncaught TypeError: 'preventExtensions' on proxy: trap returned falsish
Object.preventExtensions(proxy);

console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true

属性描述符陷阱

ECMAScript 5 最重要的特性之一是可以使用 Object.defineProperty() 方法定义属性特性(property attribute)。可以通过 Object.getOwnPropertyDescriptor() 方法来获取这些属性。
在代理中可以分别用 defineProperty 陷阱和 getOwnPropertyDescriptor 陷阱拦截 Object.defineProperty() 方法和 Object.getOwnPropertyDescriptor() 方法的调用。

defineProperty 陷阱接受以下参数:

  • trapTarget
    要定义属性的对象(代理的目标)
  • key
    属性的键(字符串或Symbol)
  • descriptor
    属性的描述符对象

操作成功后返回 true,否则返回 false。

getOwnPropertyDescriptor 陷阱接受以下参数:

  • trapTarget
    要定义属性的对象(代理的目标)
  • key
    属性的键(字符串或Symbol)

最终返回描述符。

陷阱默认行为示例:

let proxy = new Proxy({}, {
    defineProperty(trapTarget, key, descriptor) {
        return Reflect.defineProperty(trapTarget, key, descriptor);
    },
    getOwnPropertyDescriptor(trapTarget, key) {
        return Reflect.getOwnPropertyDescriptor(trapTarget, key);
    }
});

Object.defineProperty(proxy, "name", {
    value: "proxy"
});

console.log(proxy.name); // "proxy"

let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");

console.log(descriptor.value); // "proxy"

给 Object.defineProperty() 添加限制

defineProperty 陷阱返回布尔值来表示操作是否成功。
返回 true 时,Object.defineProperty() 方法成功执行;
返回 false 时, Object.defineProperty() 方法抛出错误。

例:阻止 Symbol 类型的属性

let proxy = new Proxy({}, {
    defineProperty(trapTarget, key, descriptor) {
        if (typeof key === "symbol") {
            return false;
        }

        return Reflect.defineProperty(trapTarget, key, descriptor);
    }
});

Object.defineProperty(proxy, "name", {
    value: "proxy"
});

console.log(proxy.name); // "proxy"

let nameSymbol = Symbol("name");

// 抛出错误:
// Uncaught TypeError: 'defineProperty' on proxy: trap returned falsish for property 'Symbol(name)'
Object.defineProperty(proxy, nameSymbol, {
    value: "proxy"
});

描述符对象限制

defineProperty 陷阱

defineProperty 陷阱的描述对象已规范化,只有下列属性会被传递给 defineProperty 陷阱的描述符对象。

  • enumerable
  • configurable
  • value
  • writable
  • get
  • set
let proxy = new Proxy({}, {
    defineProperty(trapTarget, key, descriptor) {
        console.log(descriptor.value); // "proxy"
        console.log(descriptor.name); // undefined

        return Reflect.defineProperty(trapTarget, key, descriptor);
    }
});

Object.defineProperty(proxy, "name", {
    value: "proxy",
    name: "custom"
});

getOwnPropertyDescriptor 陷阱

getOwnPropertyDescriptor 陷阱的返回值必须是 null、undefined或一个对象;
如果返回对象,则对象自己的属性只能是 enumerable、configurable、vlaue、writable、get和set;
在返回的对象中使用不被允许的属性会抛出一个错误。

let proxy = new Proxy({}, {
    getOwnPropertyDescriptor(trapTarget, key) {
        return {
            name: "proxy"
        };
    }
});

// 给不存在的属性赋值会抛出错误
// Uncaught TypeError: 'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for property 'name' which is either non-existant or configurable in the proxy target
let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");

这条限制可以确保无论代理中使用了什么方法,Object.getOwnPropertyDescriptor() 返回值的结构总是可靠的。

ownKeys 陷阱

ownKeys 陷阱可以拦截内部方法 [[OwnPropertyKeys]],通过返回一个数组的值可以覆写其行为。
这个数组被用于 Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols() 和 Object.assign() 4个方法,Object.assign() 方法用数组来确定需要复制的属性。

ownKeys 陷阱通过 Reflect.ownKeys() 方法实现默认的行为,返回的数组中包含所有自有属性的键名,字符串类型和 Symbol 类型的都包含在内。
Object.getOwnPropertyNames() 方法和 Object.keys() 方法返回的结果将 Symbol 类型的属性名排除在外;
Object.getOwnPropertySymbols() 方法返回的结果将字符串类型的属性名排除在外;
Object.assign() 方法支持字符串和 Symbol 两种类型。

ownKeys 陷阱唯一接受的参数时操作的目标,返回值必须是一个数组或类数组对象,否则就抛出错误。

例:过滤任何以下划线字符开头的属性名称。

let proxy = new Proxy({}, {
    ownKeys(trapTarget) {
        return Reflect.ownKeys(trapTarget).filter(key => {
            return typeof key !== "string" || key[0] !== "_";
        });
    }
});

let nameSymbol = Symbol("name");

proxy.name = "proxy";
proxy._name = "private";
proxy[nameSymbol] = "symbol";

let names = Object.getOwnPropertyNames(proxy),
    keys = Object.keys(proxy),
    symbols = Object.getOwnPropertySymbols(proxy);

console.log(names.length); // 1
console.log(names[0]); // "name"

console.log(keys.length); // 1
console.log(keys[0]); // "name"

console.log(symbols.length); // 1
console.log(symbols[0]); // "Symbol(name)"

函数代理的 apply 和 construct 陷阱

所有代理陷阱中,只有 apply 和 construct 的代理目标是一个函数。
函数有两个内部方法 [[Call]] 和 [[Construct]],apply 陷阱和 construct 陷阱可以覆写这些内部方法。
若使用 new 操作符调用函数,则执行 [[Construct]] 方法;若不用,则执行 [[Call]] 方法。

apply 陷阱和 Reflect.apply() 都接受以下参数:

  • trapTarget
    被执行的函数(代理的目标)
  • thisArg
    函数被调用时内部this的值
  • argumentList
    传递给函数的参数数组

当使用 new 调用函数时调用的 construct 陷阱接受以下参数:

  • trapTarget
    被执行的函数(代理的目标)
  • argumentList
    传递给函数的参数数组

Reflect.construct() 方法也接受这两个参数,其还有一个可选的第三个参数 newTarget。

let target = function() { return 42; },
    proxy = new Proxy(target, {
        apply: function(trapTarget, thisArg, argumentList) {
            return Reflect.apply(trapTarget, thisArg, argumentList);
        },
        construct: function(trapTarget, argumentList) {
            return Reflect.construct(trapTarget, argumentList);
        }
    });

// 一个目标是函数的代理开起来也像一个函数
console.log(typeof proxy); // function
console.log(proxy()); // 42
var instance = new proxy();
console.log(instance instanceof proxy); // true
console.log(instance instanceof target); // true

验证函数参数

例:验证所有参数必须是数字:

// 将所有参数相加
function sum(...values) {
    return values.reduce((previous, current) => previous + current, 0);
}

let sumProxy = new Proxy(sum, {
    apply: function(trapTarget, thisArg, argumentList) {
        argumentList.forEach((arg) => {
            if (typeof arg !== "number") {
                throw new TypeError("所有参数必须是数字");
            }
        });

        return Reflect.apply(trapTarget, thisArg, argumentList);
    },
    construct: function(trapTarget, argumentList) {
        throw new TypeError("该函数不可通过new来调用");
    }
});

console.log(sumProxy(1, 2, 3, 4)); // 10

// 抛出错误
// Uncaught TypeError: 所有参数必须是数字
console.log(sumProxy(1, "2", 3, 4));

// 抛出错误
// Uncaught TypeError: 该函数不可通过new来调用
let result = new sumProxy();

可调用的类构造函数

使用 apply 陷阱创建实例

class Person {
    constructor(name) {
        this.name = name;
    }
}

let PersonProxy = new Proxy(Person, {
    apply: function(trapTarget, thisArg, argumentList) {
        return new trapTarget(...argumentList);
    }
});

let me = PersonProxy("JiaJia");
console.log(me.name); // JiaJia
console.log(me instanceof Person); // true
console.log(me instanceof PersonProxy); // true

可撤销代理

可以使用 Proxy.revocable() 方法创建可撤销的代理,该方法采用与 Proxy 构造函数相同的参数:目标对象和代理处理程序。
返回值是具有以下属性的对象:

  • proxy
    可被撤销的代理对象
  • revoke
    撤销代理要调用的函数
let target = {
    name: "target"
};

let { proxy, revoke } = Proxy.revocable(target, {});

console.log(proxy.name); // target

revoke();

// 抛出错误
// Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
console.log(proxy.name);

解决数组问题

数组问题

let colors = ["red", "green", "blue"];

console.log(colors.length); // 3

colors[3] = "black";

console.log(colors.length); // 4
console.log(colors[3]); // black

colors.length = 2;

自定义数组类型

function toUint32(value) {
    return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}

function isArrayIndex(key) {
    let numericKey = toUint32(key);
    return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}


class MyArray {
    constructor(length = 0) {
        this.length = length;
        return new Proxy(this, {
            set(trapTarget, key, value) {
                let currentLength = Reflect.get(trapTarget, "length");

                if (isArrayIndex(key)) {
                    let numericKey = Number(key);
                    if (numericKey >= currentLength) {
                        Reflect.set(trapTarget, "length", numericKey + 1);
                    }
                } else if (key === "length") {
                    if (value < currentLength) {
                        for (let index = currentLength - 1; index >= value; index--) {
                            Reflect.deleteProperty(trapTarget, index);
                        }
                    }
                }

                return Reflect.set(trapTarget, key, value);
            }
        });
    }
}

let colors = new MyArray(3);
console.log(colors instanceof MyArray); // true
console.log(colors.length); // 3

let colors2 = new MyArray(5);
console.log(colors.length); // 3
console.log(colors2.length); // 5

colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
colors[3] = "black";

console.log(colors.length); // 4

colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
console.log(colors[0]); // "red"

将代理作为原型

let target = {};
let proxy = new Proxy(target, {
    defineProperty(trapTarget, name, descriptor) {
        return false;
    }
});
let newTarget = Object.create(proxy);

Object.defineProperty(newTarget, "name", {
    value: "newTarget"
});

console.log(newTarget.name); // "newTarget"
console.log(newTarget.hasOwnProperty("name")); // true
console.log(Object.getPrototypeOf(newTarget) === proxy); // true

关于 Object.create() 方法可以参照 这里

上例中 newTarget 的原型是代理,但是在对象上定义属性的操作不需要操作对象原型,所以没有触发代理中的陷阱。

尽管代理作为原型使用时及其受限,但有几个陷阱仍然有用。

在原型上使用 get 陷阱

let target = {};
let thing = Object.create(new Proxy(target, {
    get(trapTarget, key, value) {
        throw new ReferenceError(`${key} deesn't exist`);
    }
}));

thing.name = "thing";
console.log(thing.name); // "thing"

// 抛出异常:
// Uncaught ReferenceError: unknown deesn't exist
let unknown = thing.unknown;

访问对象上不存在的属性时,会触发原型中的 get 陷阱。

在原型上使用 set 陷阱

let target = {};
let thing = Object.create(new Proxy(target, {
    set (trapTarget, key, value, receiver) {
        return Reflect.set(trapTarget, key, value, receiver);
    }
}));

console.log(thing.hasOwnProperty("name")); // false

// 触发set陷阱
thing.name = "thing";
console.log(thing.name); // "thing"
console.log(thing.hasOwnProperty("name")); // true

// 不触发set陷阱
thing.name = "boo";
console.log(thing.name); // "boo"

在原型上使用 has 陷阱

let target = {}
let thing = Object.create(new Proxy(target, {
    has (trapTarget, key) {
        return Reflect.has(trapTarget, key);
    }
}));

// 触发 has 陷阱
console.log("name" in thing); // false
thing.name = "thing";
// 不触发 has 陷阱
console.log("name" in thing); // true

第一次 in 操作符触发 has 陷阱,是因为 name 不是 thing 的自有属性。

将代理用作类的原型

function NoSuchProperty() {

}

let proxy = new Proxy({}, {
    get (trapTarget, key, receiver) {
        throw new ReferenceError(`${key} doesn't exist`);
    }
});

NoSuchProperty.prototype = proxy;

class Square extends NoSuchProperty {
    constructor(length, width) {
        super();
        this.length = length;
        this.width = width;
    }
}

let shape = new Square(2, 6);

let shapeProto = Object.getPrototypeOf(shape);
console.log(shapeProto === proxy); // false

let secondLevelProto = Object.getPrototypeOf(shapeProto);
console.log(secondLevelProto === proxy); // true

let area1 = shape.length * shape.width;
console.log(area1); // 12

// 由于 wdth 不存在,抛出错误:
// Uncaught ReferenceError: wdth doesn't exist
let area2 = shape.length * shape.wdth;