• [ES6深度解析]11:代理(Proxies)


    这就是我们今天要做的事情:

    var obj = new Proxy({}, {
      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);
      }
    });
    

    把这段代码当成第一个例子对你来说有点复杂。稍后我会解释所有部分。现在,看看我们创建的对象:

    > obj.count = 1;
        setting count!
    > ++obj.count;
        getting count!
        setting count!
        2
    

    这是怎么回事?我们正在拦截这个对象的属性访问。我们重载了.操作符。

    代理是如何实现的?

    计算机技术中中最好的技巧叫做虚拟化。这是一种非常通用的技术,用于做令人惊讶的事情。下面是它的工作原理。

    1. 随便拿一张照片:image
    2. 在这个照片中随便把某个东西圈起来:image
    3. 白色圆圈把照片分成两部分:圈内和圈外。现在我们可以把圈内或者圈外的部分用其他任何东西替换掉。只有一个规则,向后兼容规则image
      如上图,圈内部分被替换,圈内的替代者必须表现得像以前一样,以至于圈外的人注意到有什么变化。

    在经典的计算机科学电影中,比如《楚门的世界》(The Truman Show)和《黑客帝国》(The Matrix),你会对这种黑客手法很熟悉。在这些电影中,一个人处于“圈内”,而世界的其余部分被一种精心设计的正常幻觉所取代。

    为了满足向后兼容性规则,替代者可能需要巧妙地设计。但真正的技巧在于画出正确的“圈”

    所谓“圈”,就是API的边界 —— 一个接口。接口指定了两段代码如何交互以及每个部分对另一部分的期望。因此,如果一个接口被设计到系统中,那么边界就已经为你画好了。你知道你可以替换任何一方,而另一方不会在意。

    当没有现成的接口时,你就必须发挥创造性。一些最酷的软件专家一直在以前没有API边界的地方绘制API边界,并通过巨大的工程努力把接口创造出来。

    虚拟内存、硬件虚拟化、Docker、Valgrind等等在不同程度上,所有这些项目都涉及到在现有系统中创造出新的和出乎意料的接口。在某些情况下,需要花费数年时间和新的操作系统特性,甚至是新的硬件才能使新接口正常工作。

    最好的虚拟化技术人员会对正在被虚拟化的东西有新的理解。要为某些东西编写API,你必须理解它。一旦你理解了,你就能做出惊人的事情。

    ES6引入了对JavaScript最基本概念——对象的虚拟化支持。

    什么是对象?

    花点时间。考虑考虑。当你知道什么是对象时向下滚动。
    image

    这个问题对我来说太难了!我从来没听过一个真正令人满意的定义。

    这是意外吗?定义基础概念一直非常困难,看看在《欧几里得元素》中,最初的几个定义是如何做出的。因此,当ECMAScript语言规范没有其他帮助性概念时,只能将对象定义为“object类型的成员”,这就是一个很好的例子。

    后来,规范补充说:“对象是属性的集合。”这还算是个不错的定义。如果你想要一个定义,现在就这个基本可以了。

    我之前说过,要为一个对象写一个API,你必须理解它。所以在某种程度上,我承诺过,如果我们完成了对象API和接口的编写,我们将更好地理解Object,我们将能够做令人惊奇的事情。

    因此,让我们跟随ECMAScript标准委员会的脚步,看看如何为JavaScript对象定义API和接口。我们需要什么样的方法?Object能做什么?

    这多少取决于Object。DOM元素对象可以做某些事情;AudioNode对象做其他事情。但是有一些基本的能力是所有Object都共有的:

    • 对象有属性。可以获取和设置属性、删除它们等等。
    • 对象有原型(prototypes)。这就是继承在JS中的工作方式。
    • 有些对象是函数或构造函数。你可以调用他们。

    几乎所有JS程序对Object的处理都是使用属性原型函数完成的。甚至Element或AudioNode对象的特殊行为也可以通过调用方法来访问,这些方法只是继承了函数属性。

    因此,当ECMAScript标准委员会定义了一组14个内部方法(所有对象的通用接口)时,他们最终聚焦于这三个基本的东西就不足为奇了。

    完整的列表可以在ES6标准的表5和表6中找到。在这里我只描述一些。奇怪的双括号[[]]强调这些是内部方法,对普通JS代码是隐藏的。不能像普通方法那样调用、删除或覆盖这些方法。

    • obj.[[Get]](key, receiver) 获取属性的值。当JS代码执行obj.propobj[key]时调用。obj是当前搜索的对象;receiver是我们第一次开始搜索这个属性的对象。有时我们必须搜索几个对象。obj可能是receiver原型链上的一个对象。

    • obj.[[Set]](key, value, receiver) 赋值给对象的属性。当JS代码执行obj.prop = valueobj[key] = value时调用。在类似obj.prop += 2这样的赋值中,[[Get]]方法先被调用,然后又调用了[[Set]]++--也是如此。

    • obj.[[HasProperty]](key) 检查属性是否存在。当JS代码执行key in obj时调用。

    • obj.[[Enumerate]]() 列出obj的可枚举属性。当JS代码执行for (key in obj) ...时调用。这将返回一个迭代器对象,这就是for-in循环获取对象属性名的方式。

    • obj.[[GetPrototypeOf]]() 返回obj对象的原型。当JS代码执行obj.__proto__Object.getPrototypeOf(obj)时调用。

    • functionObj.[[Call]](thisValue, arguments) 调用一个方法。当JS代码执行functionObj()x.method()时调用。

    • constructorObj.[[Construct]](arguments, newTarget) 调用一个构造函数。当JS代码执行new Date(2890, 6, 2)类似的代码时调用。newTarget参数在这里扮演了子类的作用。

    在整个ES6标准中,只要有可能,对Object做任何事情的语法或内置函数都是根据14个内部方法来实现的。ES6在Object的核心周围划出了清晰的边界。代理可以让您用任意的JS代码替换标准类型的Object核心部分。

    当我们开始讨论重写这些内部方法时,记住,我们讨论的是重写核心语法的行为,比如obj.propObject.keys()等内置函数。

    Proxy 代理

    ES6定义了一个新的全局构造函数Proxy。它有两个参数:一个目标对象(target)和一个处理程序对象(handler)。一个简单的例子如下:

    var target = {}, handler = {};
    var proxy = new Proxy(target, handler);
    

    Proxy - target对象

    我可以用一句话告诉你proxy的行为:所有Proxy的内部方法都被转发到target对象。也就是说,如果某个函数调用proxy.[[Enumerate]](),JS会执行target.[[Enumerate]]()

    我们将做一些导致proxy.[[Set]]()被调用的事情。

    proxy.color = "pink";
    

    刚刚发生了什么?proxy.[[Set]]()应该已经调用了target.[[Set]](),所以应该已经在target上创建了一个新属性。

    > target.color
        "pink"
    

    在大多数情况下,这个proxy的行为与它的target完全相同。这种错觉的真实性是有限度的。你会发现proxy !== targetproxy有时会通不过target通过的类型检查。例如,即使proxytarget是一个DOM元素,proxy也不是一个真正的元素;所以像document.body.appendChild(proxy)这样的代码会因为TypeError而失败。

    Proxy - handler对象

    现在让我们回到处理程序对象。这就是让代理变得有用的地方。处理程序对象(handler object)的方法可以覆盖代理(proxy)的任何内部方法

    例如,如果你想拦截所有对对象属性赋值的尝试,你可以通过定义handler.set()方法来实现:

    var target = {};
    var handler = {
      set: function (target, key, value, receiver) {
        throw new Error("Please don't set properties on this object.");
      }
    };
    var proxy = new Proxy(target, handler);
    
    > proxy.name = "angelina";
        Error: Please don't set properties on this object.
    

    处理程序方法的完整列表记录在Proxy的MDN页面上。有14个方法,它们与ES6中定义的14个内部方法一致。所有处理程序方法都是可选的。如果内部方法没有被处理程序拦截,那么它将被转发到target,就像我们前面看到的那样。

    示例1:不可思议的自动创建对象

    我们现在对代理有足够的了解,可以尝试用它们来做一些非常奇怪的事情,一些没有代理就不可能做的事情。这是我们的第一个练习。创建一个函数Tree(),能做这些事情:

    > var tree = Tree();
    > tree
        { }
    > tree.branch1.branch2.twig = "green";
    > tree
        { branch1: { branch2: { twig: "green" } } }
    > tree.branch1.branch3.twig = "yellow";
        { branch1: { branch2: { twig: "green" },
                     branch3: { twig: "yellow" }}}
    

    注意所有中间对象branch1branch2branch3是如何在需要时神奇地自动创建的。方便,对吗?这怎么可能呢?直到现在,这一切都不可能成功。但是对于代理,这只需要几行代码。我们只需要利用tree.[[Get]]()。如果你喜欢挑战,你可能会想在继续阅读之前尝试一下。

    image

    下面是我的方案:

    function Tree() {
      return new Proxy({}, handler);
    }
    
    var handler = {
      get: function (target, key, receiver) {
        if (!(key in target)) {
          target[key] = Tree();  // 自动创建一个子tree
        }
        return Reflect.get(target, key, receiver);
      }
    };
    

    注意最后对Reflect.get()的调用。事实证明,在代理处理程序方法(handler)中,有一种非常常见的需求,即能够达到这样的效果:“现在只需执行委托给目标target的默认行为”。所以ES6定义了一个新的Reflect对象,其中有14个方法,你可以用它们来完成这个任务。

    示例2:只读视图

    我想我可能给人留下了代理很容易使用的错误印象。再举一个例子,看看是否正确。这一次,我们的赋值更加复杂:我们必须实现一个函数readOnlyView(object),它接受任何对象,并返回一个行为与该对象类似的代理,只是不能改变这个对象。例如,它应该是这样的:

    > var newMath = readOnlyView(Math);
    > newMath.min(54, 40);
        40
    > newMath.max = Math.min;
        Error: can't modify read-only view(只读视图)
    > delete newMath.sin;
        Error: can't modify read-only view(只读视图)
    

    我们如何实现它?

    第一步是拦截所有内部方法,如果我们允许它们通过,这些方法将修改目标对象。有五个这样的方法:

    function NOPE() {
      throw new Error("can't modify read-only view");
    }
    
    var handler = {
      // Override all five mutating methods.
      set: NOPE,
      defineProperty: NOPE,
      deleteProperty: NOPE,
      preventExtensions: NOPE,
      setPrototypeOf: NOPE
    };
    
    function readOnlyView(target) {
      return new Proxy(target, handler);
    }
    

    这样就起作用了。它阻止了通过只读视图进行赋值、属性定义等操作。这个计划有漏洞吗?

    最大的问题是[[Get]]方法其他方法仍然可能返回可变对象。因此,即使某个对象x是只读视图,x.prop也可以是可变的!那是个大漏洞。要填补这个漏洞,我们必须添加一个handler.get()方法:

    var handler = {
      ...
    
      // 把可能的结果都包装成只读
      get: function (target, key, receiver) {
        // 最开始,执行默认的行为get
        var result = Reflect.get(target, key, receiver);
    
        // 确保get返回的是一个不可变对象
        if (Object(result) === result) {
          // 返回结果是一个对象
          return readOnlyView(result);
        }
        // 返回结果是一个简单类型,已经是不可变的
        return result;
      },
    
      ...
    };
    

    这还不够。其他方法也需要类似的代码,包括getPrototypeOfgetOwnPropertyDescriptor。然后还有更多的问题。当通过这种代理调用getter其他方法时,传递给getter其他方法this值通常是代理proxy本身。但是正如我们前面看到的,许多访问器方法执行了代理无法通过的类型检查。最好在这里用目标对象target代替代理proxy。你知道怎么做吗?

    从中得到的教训是,创建代理很容易,但创建具有直觉行为的代理却相当困难。

    其他小细节

    • 代理真正有用的是什么?
      1.当你希望观察或记录对对象的访问时,它们将便于调试。测试框架可以使用它们来创建模拟对象。
      2.如果你需要稍微超出普通对象能力的行为:例如惰性填充属性,代理就很有用。
      3.我讨厌提出这个问题,但要了解使用代理的代码中发生了什么,最好的方法之一是将代理的处理程序对象包装在另一个代理中,该代理每次访问处理程序方法时都会记录到控制台。
      4.代理可以用来限制对对象的访问,就像我们对readOnlyView所做的那样。

    • 代理中利用WeakMap
      在我们的readOnlyView示例中,每次访问一个对象(例如.branch1)时,我们都会创建一个新的代理。把我们创建的每个代理都缓存在WeakMap中可以节省大量内存,因此无论一个对象被传递给readOnlyView多少次,都只会为它创建一个代理。这是WeakMap的一个好用例。

    • 可撤销的代理
      ES6还定义了另一个函数Proxy.revocable(target, handler)。它创建一个代理,就像new Proxy(target, handler)一样,只是这个代理可以稍后撤销。(Proxy.revocable返回一个带有.proxy属性和.revoke方法的对象。)一旦代理被撤销,它就不再工作了;它的所有内部方法都回收。

    • 对象一致性(特性不变)
      在某些情况下,ES6需要代理处理程序方法来报告与目标对象状态一致的结果。它这样做是为了在所有对象(甚至是代理)中强制执行关于不变性的规则。例如,代理不能声明为是不可扩展的,除非它的目标确实不可扩展。

    确切的规则太复杂了,不能在这里详细说明,但如果您曾经看到过类似“代理不能将不存在的属性报告为不可配置的”(proxy can't report a non-existent property as non-configurable)这样的错误消息,这就是原因所在。最有可能的补救办法是改变代理报告本身的内容。另一种可能性是在动态中改变目标,以反映代理报告的内容。

    现在我们知道什么是Object对象了吗?

    我们刚刚说的是:“对象是属性的集合。”

    我并不完全满意这个定义,甚至认为我们理所当然地加入了原型可调用性。我认为“集合”这个词太过慷慨了,因为代理的定义很糟糕。它的处理程序方法可以做任何事情。他们可以返回随机结果。

    通过弄清楚对象可以做什么,对这些方法进行标准化,并将虚拟化添加为每个人都能使用的一流特性,ECMAScript标准委员会已经扩展了可能性领域。

    对象现在几乎可以是任何东西。

    对于“什么是对象”这个问题,也许最诚实的答案是:现在把12个必需的内部方法作为一个定义。对象是JS程序中具有[[Get]]操作、[[Set]]操作等等的一种东西。

    本文来自博客园,作者:Max力出奇迹,转载请注明原文链接:https://www.cnblogs.com/welody/p/15188814.html

    如果觉得文章不错,欢迎点击推荐

  • 相关阅读:
    磁盘冗余 ---RAID磁盘管理
    linux磁盘管理
    linux基础命令
    Apache配置rewrite
    memcache运维整理
    mysql主从配置
    rsync相关整理
    Lua 学习笔记(六)
    Lua 学习笔记(五)
    Lua 学习笔记(四)
  • 原文地址:https://www.cnblogs.com/welody/p/15188814.html
Copyright © 2020-2023  润新知