1. defineProperty

ES5 提供了 Object.defineProperty 方法,该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。

语法:Object.defineProperty(obj, prop, descriptor)
obj: 要在其上定义属性的对象。
prop: 要定义或修改的属性的名称。
descriptor: 将被定义或修改的属性的描述符。

举例:

var obj = {};
Object.defineProperty(obj, "num", {
    value : 1,
    writable : true,
    enumerable : true,
    configurable : true
});
//  对象 obj 拥有属性 num,值为 1

虽然我们可以直接添加属性和值,但是使用这种方式,我们能进行更多的配置。

1.1 descriptor

函数的第三个参数 descriptor 所表示的属性描述符有两种形式:数据描述符存取描述符

两者均具有以下两种键值:

  • configurable: 当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,也能够被删除。默认为 false。
  • enumerable:当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。

数据描述符同时具有以下可选键值:

  • value: 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
  • writable: 当且仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变。默认为 false

存取描述符同时具有以下可选键值:

  • get:一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值就是属性值。默认为 undefined。
  • set:一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined。

注意:属性描述符必须是数据描述符或者存取描述符两种形式之一,不能同时是两者。

1.2 Setters 和 Getters

defineProperty中的 getset,这两个方法又被称为 gettersetter。由 gettersetter 定义的属性称做存取器属性

当程序查询存取器属性的值时,JavaScript 调用 getter方法。这个方法的返回值就是属性存取表达式的值。

当程序设置一个存取器属性的值时,JavaScript 调用 setter 方法,将赋值表达式右侧的值当做参数传入 setter。

举例:

var obj = {}, value = null;
Object.defineProperty(obj, "num", {
    get: function(){
        console.log('执行了 get 操作')
        return value;
    },
    set: function(newValue) {
        console.log('执行了 set 操作')
        value = newValue;
    }
})

obj.num = 1 // 执行了 set 操作

console.log(obj.num); // 执行了 get 操作 // 1

封装一下监控数据改变的方法:

function Archiver() {
    var value = null;
    // archive n. 档案
    var archive = [];

    Object.defineProperty(this, 'num', {
        get: function() {
            console.log('执行了 get 操作')
            return value;
        },
        set: function(value) {
            console.log('执行了 set 操作')
            value = value;
            archive.push({ val: value });
        }
    });

    this.getArchive = function() { return archive; };
}

var arc = new Archiver();
arc.num; // 执行了 get 操作
arc.num = 11; // 执行了 set 操作
arc.num = 13; // 执行了 set 操作
console.log(arc.getArchive()); // [{ val: 11 }, { val: 13 }]

1.3 watch API

既然可以监控数据的改变,就可以实现当数据改变的时候,自动进行渲染工作。

我们可以通过watch 函数监听对象的某个属性值的改变,来执行dom的变化。

var obj = {
    value: 1
}

watch(obj, "value", function(newvalue){
    document.getElementById('container').innerHTML = newvalue;
})

document.getElementById('button').addEventListener("click", function(){
    obj.value += 1
});

watch函数:

function watch(obj, name, func){
        var value = obj[name];

        Object.defineProperty(obj, name, {
            get: function() {
                return value;
            },
            set: function(newValue) {
                value = newValue;
                func(value)
            }
        });

        if (value) obj[name] = value
    }

1.4 defineProperty 的缺点

Object.defineProperty不支持数组的拦截

2. proxy

使用 defineProperty 只能重定义属性的读取(get)设置(set)行为,到了 ES6,提供了 Proxy,可以重定义更多的行为,比如 indelete函数调用等更多行为。

Proxy直接可以劫持整个对象,并返回一个新对象,不管是操作便利程度还是底层功能上都远强于Object.defineProperty

ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。我们来看看它的语法:

var proxy = new Proxy(target, handler);
  • target —— 是要包装的对象,可以是任何东西,包括函数。
  • handler —— 代理配置:带有“钩子”(“traps”,即拦截操作的方法)的对象。比如 get 钩子用于读取 target 属性,set 钩子写入 target 属性等等。

proxy 进行操作,如果在 handler 中存在相应的钩子,则它将运行,并且 Proxy 有机会对其进行处理,否则将直接对 target 进行处理。

2.1 handler

Proxy 是一种特殊的“奇异对象”。它没有自己的属性。如果 handler 为空,则透明地将操作转发给 target

要激活更多功能,让我们添加钩子。

对于对象的大多数操作,JavaScript 规范中都有一个所谓的“内部方法”,它描述了最底层的工作方式。 例如 [[Get]],用于读取属性的内部方法, [[Set]],用于写入属性的内部方法,等等。这些方法仅在规范中使用,我们不能直接通过方法名调用它们。

Proxy 钩子会拦截这些方法的调用。它们在代理规范和下表中列出。

对于每个内部方法,此表中都有一个钩子:可用于添加到 new Proxy 时的 handler 参数中以拦截操作的方法名称:

内部方法Handler 方法何时触发
[[Get]]get读取属性
[[Get]]set写入属性
[[HasProperty]]hasin 运算符
[[Delete]]deletePropertydelete 操作
[[Call]]applyproxy 对象作为函数被调用
[[Construct]]constructnew 操作
[[GetPrototypeOf]]getPrototypeOfObject.getPrototypeOf
[[SetPrototypeOf]]setPrototypeOfObject.setPrototypeOf
[[IsExtensible]]isExtensibleObject.isExtensible
[[PreventExtensions]]preventExtensionsObject.preventExtensions
[[DefineOwnProperty]]definePropertyObject.defineProperty, Object.defineProperties
[[GetOwnProperty]]getOwnPropertyDescriptorObject.getOwnPropertyDescriptor, for…in, Object.keys/values/entries
[[OwnPropertyKeys]]ownKeysObject.getOwnPropertyNames, Object.getOwnPropertySymbols, for…in, Object/keys/values/entries

其中大多数用于返回值:

  • [[Set]] 如果值已成功写入,则必须返回 true,否则返回 false。
  • [[Delete]] 如果已成功删除该值,则必须返回 true,否则返回 false。
  • ……依此类推,我们将在下面的示例中看到更多内容。

2.1.1 不使用 handler

首先,让我们创建一个没有任何钩子的代理:

let target = {};
let proxy = new Proxy(target, {}); // 空的handler对象

proxy.test = 5; // 写入 Proxy 对象 (1)
alert(target.test); // 返回 5,test属性出现在了 target 上!

alert(proxy.test); // 还是 5,我们也可以从 proxy 对象读取它 (2)

for(let key in proxy) alert(key); // 返回 test,迭代也正常工作! (3)

由于没有钩子,所有对 proxy 的操作都直接转发给 target。

  1. 写入操作 proxy.test= 会将值写入 target
  2. 读取操作 proxy.test 会从 target 返回对应的值。
  3. 迭代 proxy 会从 target 返回对应的值。

我们可以看到,没有任何钩子,proxy 是一个 target 的透明包装.

2.1.2 带 “get” 钩子的默认值

要拦截读取操作,handler 应该有 get(target, property, receiver) 方法。

  • target —— 是目标对象,该对象作为第一个参数传递给 new Proxy,
  • property —— 目标属性名,
  • receiver —— 如果目标属性是一个 getter 访问器属性
let numbers = [0, 1, 2];

numbers = new Proxy(numbers, {
  get(target, prop) {
    if (prop in target) {
      return target[prop];
    } else {
      return 0; // 默认值
    }
  }
});

alert( numbers[1] ); // 1
alert( numbers[123] ); // 0 (没有这样的元素)

2.1.3 使用 “set” 钩子进行验证

set(target, property, value, receiver):

  • target —— 是目标对象,该对象作为第一个参数传递给 new Proxy
  • property —— 目标属性名称,
  • value —— 目标属性要设置的值,
  • receiver —— 与 get 钩子类似,仅与 setter 访问器相关。

如果写入操作成功,set 钩子应该返回 true,否则返回 false(触发 TypeError)。

假设我们想要一个专门用于数字的数组。如果添加了其他类型的值,则应该抛出一个错误:

let numbers = [];

numbers = new Proxy(numbers, { // (*)
  set(target, prop, val) { // 拦截写入操作
    if (typeof val == 'number') {
      target[prop] = val;
      return true;
    } else {
      return false;
    }
  }
});

numbers.push(1); // 添加成功
numbers.push(2); // 添加成功
alert("Length is: " + numbers.length); // 2

numbers.push("test"); // TypeError (proxy 的 `set` 操作返回 false)

对于 set操作, 它必须在成功写入时返回 true

2.1.4 使用 “ownKeys” 和 “getOwnPropertyDescriptor” 进行迭代

Object.keysfor..in 循环和大多数其他遍历对象属性的方法都使用 [[OwnPropertyKeys]]内部方法(由 ownKeys 钩子拦截) 来获取属性列表。

在下面的示例中,我们使用 ownKeys 钩子拦截 for..inuser 的遍历,还使用 Object.keysObject.values 来跳过以下划线 _ 开头的属性:

let user = {
  name: "John",
  age: 30,
  _password: "***"
};

user = new Proxy(user, {
  ownKeys(target) {
    return Object.keys(target).filter(key => !key.startsWith('_'));
  }
});

// "ownKeys" 过滤掉 _password
for(let key in user) alert(key); // name,然后是 age

// 对这些方法同样有效:
alert( Object.keys(user) ); // name,age
alert( Object.values(user) ); // John,30

2.1.5 “has” 钩子

has(target, property)

  • target —— 是目标对象,作为第一个参数传递给 new Proxy
  • property —— 属性名称

示例如下

let range = {
  start: 1,
  end: 10
};

range = new Proxy(range, {
  has(target, prop) {
    return prop >= target.start && prop <= target.end
  }
});

alert(5 in range); // true
alert(50 in range); // false

2.2 Reflect

Reflect 是一个内置对象,可简化的创建 Proxy

以前的内部方法,比如[[Get]][[Set]] 等等都只是规范,不能直接调用。

Reflect 对象使调用这些内部方法成为可能。它的方法是内部方法的最小包装。

这是 Reflect 执行相同操作和调用的示例:

操作Reflect 调用内部方法
obj[prop]Reflect.get(obj, prop)[[Get]]
obj[prop] = valueReflect.set(obj, prop, value)[[Set]]
delete obj[prop]Reflect.deleteProperty(obj, prop)[[Delete]]
new F(value)Reflect.construct(F, value)[[Construct]]

例如:

let user = {};

Reflect.set(user, 'name', 'John');

alert(user.name); // John

Reflect 允许我们使用函数(Reflect.constructReflect.deleteProperty,……)执行操作(newdelete,……)。

对于每个可被 Proxy 捕获的内部方法,Reflect 都有一个对应的方法 Reflect,其名称和参数与 Proxy 钩子相同。

因此,我们可以用 Reflect 来将操作转发到原始对象。

在此示例中,钩子getset 透明地(好像它们都不存在)将读/写操作转发到对象,并显示一条消息:

let user = {
  name: "John",
};

user = new Proxy(user, {
  get(target, prop, receiver) {
    alert(`GET ${prop}`);
    return Reflect.get(target, prop, receiver); // (1)
  },
  set(target, prop, val, receiver) {
    alert(`SET ${prop}=${val}`);
    return Reflect.set(target, prop, val, receiver); // (2)
  }
});

let name = user.name; // shows "GET name"
user.name = "Pete"; // shows "SET name=Pete"
  • Reflect.get 读取一个对象属性
  • Reflect.set 写入对象属性,成功返回 true ,否则返回 false

如果钩子想要将调用转发给对象,则只需使用相同的参数调用 Reflect.<method> 就足够了。

在大多数情况下,我们可以不使用 Reflect 完成相同的事情,例如,使用Reflect.get(target, prop, receiver) 读取属性可以替换为 target[prop]。尽管有一些细微的差别。

2.2.1 为什么 Reflect.get 更好

我们有一个带有一个 _name 属性和一个 getter 的对象 user

let user = {
  _name: "Guest",
  get name() {
    return this._name;
  }
};

let userProxy = new Proxy(user, {
  get(target, prop, receiver) {
    return target[prop]; // (*) target = user
  }
});

let admin = {
  __proto__: userProxy,
  _name: "Admin"
};

// Expected: Admin
alert(admin.name); // 输出:Guest (?!?)

读取 admin.name 应该返回 "Admin",而不是 "Guest"

问题实际上出在代理中,在 (*)行。

  1. 当我们读取 admin.name,由于 admin 对象自身没有对应的的属性,搜索将转到其原型userProxy
  2. 从代理读取 name 属性时,get 钩子会触发并从原始对象返回 target[prop] 属性,在 (*)
  3. 当调用 target[prop] 时,若 prop 是一个 getter,它将在 this=target 上下文中运行其代码。因此,结果是来自原始对象 targetthis._name 即来自 user

为了解决这种情况,我们需要 get 钩子的第三个参数 receiver。它保证传递正确的 thisgetter。在我们的情况下是 adminReflect.get 可以做到的。如果我们使用它,一切都会正常运行。

let user = {
  _name: "Guest",
  get name() {
    return this._name;
  }
};

let userProxy = new Proxy(user, {
  get(target, prop, receiver) { // receiver = admin
    return Reflect.get(target, prop, receiver); // (*)
  }
});


let admin = {
  __proto__: userProxy,
  _name: "Admin"
};

alert(admin.name); // Admin

现在 receiver,保留了对正确 this 的引用(即admin)的引用,该引用将在 (*) 行中使用Reflect.get传递给getter

Reflect 调用的命名方式与钩子完全相同,并且接受相同的参数。它们是通过这种方式专门设计的。

因此, return Reflect... 会提供一个安全的提示程序来转发操作,并确保我们不会忘记与此相关的任何内容。

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐