ES6 Proxy

分享

简单谈谈

Proxy:在目标对象之前架设一层 拦截 ,当对该对象进行 访问/赋值 时,必须先通过这层拦截,从而实现对属性进行 访问/赋值 时的过滤和改写。

一个简单的例子

let obj = new Proxy({}, {
    get(target, key, receiver) {
        console.log(`getting ${key}!`);
        return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
        console.log(`setting ${key}!`);
        return Reflect.set(target, key, value, receiver);
    }
});

obj.count = 1;
// setting count!

++obj.count;
// getting count!
// setting count!
// 2

以上实现了对 {} 对象操作时的拦截(通过 obj 对象),也就是说:proxy 为开发者提供更加细粒化对数据的操作,使得开发者可以介入对数据的操作,在不改变原有操作的情况下修改数据流程。

写法

let proxy = new Proxy(target, handler);
  • target: 需要被代理的目标对象。
  • handler: 需要对对象进行代理的行为合集。

使用

属性拦截

let proxy = new Proxy({}, {
    get(target, property) {
        return 35;
    }
});

proxy.time // 35
proxy.name // 35
proxy.title // 35

通过 proxy 的介入,使得对于任何属性的访问都返回特定值,当然也可以自定规则。

未设拦截

let target = {}
let handler = {}
let proxy = new Proxy(target, handler)
proxy.a = 'b'
target.a
// "b"

当不拦截对象(也就是代理行为为空)时,对代理对象的操作可以认为是对原对象进行操作。

常用的行为

参数名词统一解释:

参数名 作用
target 被代理的对象(也就是原始对象)
propKey 访问的键值
value 设置的属性值
receiver 提供者,见注一
propDesc 某个对象属性的描述
proto 某个对象的原型
ctx 函数执行上下文
args 传入的函数参数

注一 提供者:比如当访问 proxy.foo 时,receiver 即为 proxy ,大多数情况是代理对象本身,但如果代理对象(A)是某个对象(B)的原型的话,当 B 通过原型链访问到 A 时,receiverA


可以使用拦截:一共 13 种,如下:

拦截方法名 作用
get(target, propKey, receiver) 拦截对象属性的读取,返回值。eg: proxy.foo
set(target, propKey, value, receiver) 拦截对象属性的设置。 eg: proxy.foo = 1
has(target, propKey) 拦截对象的 in 操作,返回布尔值。 eg: foo in proxy
deleteProperty(target, propKey) 拦截 delete 操作,返回布尔值。 eg: delete proxy.foo
ownKeys(taget) 拦截对象取键值的操作(注一),返回数组
getOwnPropertyDescriptor(target, propKey) 拦截 Object.getOwnPropertyDescriptor ,返回属性的描述对象。
defineProperty(target, propKey, propDesc) 拦截 Object.defineProperty[s] ,返回布尔值。
preventExtensions(target) 拦截 Object.preventExtensions ,返回布尔值。
getPrototypeOf(target) 拦截 Object.getPrototypeOf ,返回对象。
isExtensible(target) 拦截 Object.isExtensible ,返回布尔值。
setPrototypeOf(target, proto) 拦截 Object.setPrototypeOf ,返回布尔值。
apply(target, ctx, args) 拦截函数的绑定执行上下文的行为,包括函数的调用、callapply 操作。
construct(target, args) 拦截函数作为构造函数被调用时的行为。

注一:对象的取值操作包括:Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in

拦截什么?

通过一个简单的实例,来解释不同拦截的具体行为。

Get

拦截对象属性的读取操作。

let sit = {
    name: '斑码',
};

let proxy = new Proxy(sit, {
    // target:原对象,propKey:引用的键。
    get(target, propKey) {
        if (propKey in target) {
            return target[propKey];
        } else {
            throw new Error("Prop name \"" + propKey + "\" does not exist.");
        }
    }
});

proxy.name          // "斑码"
proxy.type          // 抛错,如果没有代理应该是 undefined

Set

拦截对象属性的设置操作。

let sit = {
    name: '斑码',
};

let proxy = new Proxy(sit, {
    // target:原对象,propKey:引用的键,value:设置的值。
    set(target, propKey, value) {
        // 当需要设置的属性不在对象属性中,则报错。
        if (!propKey in target) {
            throw new Error("Prop name \"" + propKey + "\" does not exist.");
        }

        return target[propKey] = value;
    }
});

proxy.name = "斑马";            // "斑码"
proxy.type = "个人博客";        // 抛错,如果没有代理 sit 对象应该会多一个 type 属性。

apply

拦截函数对象的调用。

let target = function(arr) {
    return [1, 2, 3, 4, 5, ...arr];
}

let proxy = new Proxy(target, {
    // target:原对象,propKey:this 指向,args:调用时的参数。
    apply(target, ctx, args) {
        // 获取原来的返回数据。
        let targetReturn = target.apply(ctx, args);
        // 累加后返回。
        return targetReturn.reduce((acc, item) => acc + item);
    }
});

proxy([6, 7, 8, 9, 10]);        // 55

has

拦截 HasProperty 操作。

// 以 _ 开始的属性为私有属性。
let sit = {
    name: "斑码",
    _private: true
}

let proxy = new Proxy(sit, {
    has(target, propKey) {
        if (propKey[0] === '_') {
            return false;
        }
        return propKey in target;
    }
});

'_private' in proxy;            // false,has 操作拦截后,返回了正确的结果。

construct

拦截函数的实例化操作。

let Sit = function(name) {
    this.name = name;
}

let SitProxy = new Proxy(Sit, {
    construct(target, args, newTarget) {
        console.log("proxy construct");
        if(args.length === 0){
            args = ["斑码"];
        }
        return new target(...args);
    }
});

let sit = new SitProxy();
// "proxy construct"
sit
// {name: "斑码"}
// construct 操作处理了构造函数参数的问题。

deleteProperty

拦截对象属性的删除操作(delete)。

// _ 开头的属性为私有属性。
let sit = {
    name: "斑码",
    _private: true
};

let proxy = new Proxy(sit, {
    deleteProperty(target, propKey) {
        if (propKey[0] === '_') {
            throw new Error(`can't delete private key: ${propKey}`);
        }
        delete target[propKey];
        return true;
    }
});

delete sit['_private'];     // 报错,deleteProperty 操作不允许删除私有属性。

defineProperty

拦截 Object.defineProperty 操作(包括对属性的设置)。

let sit = {
    name: "斑码",
    _private: 'test'
};

let proxy = new Proxy(sit, {
    defineProperty(target, propKey, descriptor) {
        if (propKey[0] === "_") {
            throw new Error(`can't add private key: ${propKey}`);
        }
        Object.defineProperty(target, propKey, descriptor);
        return true;
        
    }
})

proxy._private = "test";
// 报错,defineProperty 不允许设置私有属性。

Object.defineProperty(proxy, "_private", {
    value: "test"
});
// 报错,defineProperty 不允许设置私有属性。

getOwnPropertyDescriptor

拦截 Object.getOwnPropertyDescriptor 操作,返回对象下对应属性的描述符。

let sit = {
    name: "斑码",
    _private: 'test'
};

let proxy = new Proxy(sit, {
    getOwnPropertyDescriptor(target, propKey) {
        if (propKey[0] === "_") {
            throw new Error(`can't get private key descriptor: ${propKey}`);
        }
        return Object.getOwnPropertyDescriptor(target, propKey);
        
    }
})

Object.getOwnPropertyDescriptor(proxy, "name");

Object.getOwnPropertyDescriptor(proxy, "_private");
// 报错,getOwnPropertyDescriptor 不允许获取私有属性的属性描述。

getPrototypeOf

拦截获取对象原型的操作,具体有以下操作:

  • Object.prototype.__proto__
  • Object.prototype.isPrototypeOf
  • Object.getPrototypeOf
  • Reflect.getPrototypeOf
  • instanceof
let sit = {
    name: "斑码",
    _private: 'test'
};

let proxy = new Proxy(sit, {
    getPrototypeOf(target, propKey) {
        console.log('proxy getPrototypeOf');
        return Object.getPrototypeOf(target);
    }
})

proxy.__proto__                         // "proxy getPrototypeOf"

注: getPrototypeOf 拦截的返回必须为一个合理的原型(对象或是 null),否则会报错。

isExtensible

拦截 Object.isExtensible 操作。

let proxy = new Proxy({}, {
    isExtensible(target) {
        console.log("proxy isExtensible");
        return true;
    }
})

Object.isExtensible(proxy);             // "proxy isExtensible"

ownKeys

拦截获取对象键的操作,具体有以下方法:

  • Object.getOwnPropertyNames
  • Object.getOwnPropertySymbols
  • Object.keys
  • for...in
let sit = {
    name: "斑码",
    _private: 'test'
};

let proxy = new Proxy(sit, {
    ownKeys(target) {
        return Object.getOwnPropertyNames(target).filter(key => key[0] !== '_');
    }
})

Object.getOwnPropertyNames(proxy);      // ["name"],私有属性被 ownKeys 给过滤掉了。

注:

  1. ownKeys 返回的数据并非是最终结果,数组内部分数据会被移除。Object.keysfor...in 操作会过滤掉:
    • 目标对象上不存在的属性。
    • Symbol 值。
    • 不可遍历(enumerable = false)的属性。
  2. ownKeys 的返回必须是字符串或 Symbol 值组成的数组,否则会报错。
  3. 若拦截对象的某个属性是不可配置的(configurable = false),则返回数组中必须要有该值。
  4. 如果对象是不可扩展的(preventExtensions),此时,必须返回对象下所有的键值,不能多也不能少。

preventExtensions

拦截 Object.preventExtensions 操作,返回布尔值,若返回非布尔值,则自动转换为布尔值。

let proxy = new Proxy({}, {
    preventExtensions(target) {
        console.log("proxy preventExtensions");
        return false;
    }
});

Object.preventExtensions(proxy);        // "proxy preventExtensions"

setPrototypeOf

拦截 Object.setPrototypeOf 操作,返回布尔值,若返回非布尔值,则自动转换为布尔值。

let proxy = new Proxy({}, {
    setPrototypeOf(target) {
        throw new Error("can't change prototype");
    }
});

Object.setPrototypeOf(proxy, null);     // 报错,setPrototypeOf 禁止了对象修改原型。
proxy.__proto__ = null;                 // 报错

Proxy.revocable

Proxy.revocable 方法返回一个可取消的 Proxy 实例。

let target = {};
let handler = {};

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

proxy.foo = 123;
proxy.foo;                              // 123

revoke()
proxy.foo                               // TypeError: Revoked

函数返回一个代理对象和取消方法,当执行取消方法时,代理即会被销毁。

阅读更多

ES6 变量解构

简单说说 假设有这样一个场景:你需要向你的服务器获取一些基本的数据(config 对象)来初始化页面。当你需要去获取其中的配置项时, ES5 是这么写的: var config = { params1: 520, params2: 1314 }; // 为了确保下方的代码不依赖于 config 对象,重新赋值为一个更有意义的名称。 var params1 = config.params1, params2 = config.params2 console.log(params1 + ' !! ' + params2) // 520 !! 1314 看着代码量也不是很大,但这只有两个配置项,万一要有几十个呢,不得一直 congif.xxx 了。不得不得这的确很烦,那有没更简单的方式呢? ES6 提出了以下方式: var config = { params1: 520, params2:

By Breeze

ES6 Array

简单谈谈 ES5 中原本就提供了数组,这里谈谈数组下都有哪些方法新增,哪些方法进行了变更。 Array.from 在 javascript 中有这样一类的存在:类数组,它不是数组,却能像数组那么操作,如下便是一个类数组结构。 let arrayLike = { "0": "a", "1": "b", "2": "c", length: 3 }; 类数组结构:可以像数组那样取值、 for 循环,但是却不能适用数组 forEach/splice 等数组特定的方法,在 ES5 中必须使用一些特殊的方法来将一个类数组结构转化成数组,而在 ES6 中,

By Breeze

ES6 Function

默认值 想必在写函数时,都会有一个困惑:要是在这个函数调用的时候,参数没有传入时,该怎么办?设置默认值。对,所以大部分的函数都会有以下的代码。 function func(param1, param2) { param1 = param1 || {/*...*/} /* ... */ } 以上代码相信绝大多数的 javascript 开发者都写过,乍一看是没啥大问题的,但是如果传入的值是 false 呢?那么 param1 就会被替换成默认值。为了避免这个问题就应该将代码改一改: if( typeof a === 'undefined' ){ a = {/* ... */} } 这就大大增加了代码量,而且一个痛点莫名其妙的增加了:增加了代码的不可读性。 在 ES6 中允许为函数设置默认值,而且定义明确,代码的可读性大大增加。 function log(x, y = 'World') { console.log(

By Breeze