整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

如何理解JavaScript代理对象(JavaScr

如何理解JavaScript代理对象(JavaScript Proxy)

avaScript的Proxy对象是一种强大且灵活的特性,它允许你拦截并自定义对对象执行的操作。自ECMAScript 6(ES6)引入以来,Proxy对象为控制对象的基本操作行为提供了一种机制,使高级用例和改进的安全性成为可能。

代理对象的基础

一个Proxy是由两个主要组件创建的:目标对象和处理器。目标对象是你想拦截操作的原始对象,处理器是一个包含名为陷阱的方法的对象,这些方法定义了这些操作的自定义行为。

创建一个Proxy

const targetObject={
  name: 'John',
  age: 25,
};
const handler={
  get(target, prop) {
    console.log(`获取属性 ${prop}`);
    return target[prop];
  },
};
const proxy=new Proxy(targetObject, handler);
console.log(proxy.name); // 输出: 获取属性 name, John

在这个例子中,get陷阱拦截属性访问并在返回实际属性值之前记录一条消息。

理解目标、属性和值

  • 目标(Target):目标是Proxy包裹的原始对象。在上面的例子中,targetObject就是目标。
  • 属性(Prop):属性表示对象上被访问的属性。在get陷阱中,prop是被访问的属性的名称。
  • 值(Value):值指的是赋给属性的值。在set陷阱中,value是被赋给属性的新值。

常见的处理器方法

  1. get(target, prop, receiver):get陷阱拦截属性访问,并允许你自定义读取属性时的行为。
  2. set(target, prop, value, receiver):set陷阱拦截属性赋值,并使你能够验证或修改被赋的值。
  3. has(target, prop):has陷阱在使用in操作符检查属性是否存在时触发。
  4. deleteProperty(target, prop):deleteProperty陷阱在使用delete操作符删除属性时被调用。
  5. apply(target, thisArg, argumentsList):apply陷阱在Proxy作为函数调用时被触发。

代理对象的应用场景

1. 数据验证

使用代理对象可以通过验证或修改属性值来强制执行数据约束。

const validatedUser=new Proxy({}, {
  set(target, prop, value) {
    if (prop==='age' && (typeof value !=='number' || value < 0 || value > 120)) {
      throw new Error('无效的年龄');
    }
    target[prop]=value;
    return true;
  },
});
validatedUser.age=30; // 有效赋值
validatedUser.age=-5; // 抛出错误: 无效的年龄

2. 日志记录

代理对象可以轻松记录属性访问情况,为调试或性能监控提供见解。

const loggedObject=new Proxy({}, {
  get(target, prop) {
    console.log(`访问属性: ${prop}`);
    return target[prop];
  },
});
loggedObject.name='Alice'; // 访问属性: name
console.log(loggedObject.name); // 访问属性: name

3. 安全性

代理对象可以通过防止未授权的属性访问或操作来增强对象安全性。

const securedObject=new Proxy({ secret: 'classified' }, {
  get(target, prop) {
    if (prop==='secret') {
      throw new Error('未授权的访问');
    }
    return target[prop];
  },
});
console.log(securedObject.publicInfo); // 访问允许
console.log(securedObject.secret); // 抛出错误: 未授权的访问

4. 记忆化

代理对象可用于记忆化,缓存耗时的函数调用结果以提高性能。

function fibonacci(n) {
  if (n <=1) {
    return n;
  }
  return fibonacci(n - 1) + fibonacci(n - 2);
}
const memoizedFibonacci=new Proxy({}, {
  get(target, prop) {
    if (!(prop in target)) {
      target[prop]=fibonacci(Number(prop));
    }
    return target[prop];
  },
});
console.log(memoizedFibonacci[10]); // 计算并缓存
console.log(memoizedFibonacci[5]);  // 从缓存中获取

实战示例:电商场景

考虑一个电商场景,你想使用代理对象来强制执行某些业务规则。

const product={
  name: 'Smartphone',
  price: 500,
  quantity: 10,
};
const securedProduct=new Proxy(product, {
  set(target, prop, value) {
    if (prop==='quantity' && value < 0) {
      throw new Error('无效的数量');
    }
    target[prop]=value;
    return true;
  },
});
securedProduct.quantity=15; // 有效赋值
securedProduct.quantity=-5; // 抛出错误: 无效的数量

在这个例子中,Proxy确保产品的数量不能被设置为负值,从而在电商上下文中执行了一个业务规则。

结束

JavaScript Proxy对象为创建动态和可定制的对象行为提供了一个多功能工具。无论是用于数据验证、日志记录、安全性还是性能优化,代理对象都为开发者提供了对对象交互的细粒度控制。理解并利用Proxy对象可以在各种实际场景中编写出更干净、可维护和安全的代码。

这篇文章中,谷歌 Robotics 研究科学家 Eric Jang 对生物学可信深度学习(BPDL)研究提出了质疑。他认为,设计反向传播的生物学可信替代方法压根就是一个错误的问题。机器学习领域的一个严重错误就是,对统计学工具和最优控制算法赋予了太多生物学意义。

选自Eric Jang博客,作者: Eric Jang,机器之心编译,编辑:魔王、张倩

生物学可信深度学习 (BPDL) 是神经科学与机器学习交叉领域中的一个活跃研究课题,主要研究如何利用在大脑中可实现的「学习规则」来训练深度神经网络。

2015 年,深度学习巨头 Yoshua Bengio 发表论文《Towards Biologically Plausible Deep Learning》,探索了更加符合生物学逻辑的深度表征学习版本。该论文的主要观点如下:

  • 负责突触权重更新的基础学习规则 (Spike-Timing-Dependent Plasticity, STDP) 源于一个简单的更新规则,该规则从机器学习的角度来看是有意义的,可以理解为在某个目标函数上的梯度下降,只要神经动态活动能将放电速率推向更好的目标函数值(可能是监督式、无监督式或奖赏驱动的);
  • 这与变分 EM 法的形式相对应,也就是使用神经动力学实现的近似而非精确的后验;
  • 我们可以利用近似来估计上述变分解释(variational interpretation)中更新隐藏状态所需的梯度,只需将激活向前和向后传播,并且用成对的层来学习形成去噪自编码器。

次年,在 NIPS 2016 Workshop 上,Yoshua Bengio 做了同名演讲,其中就探讨了「反向传播」机制的生物学可信性。

在学习过程中,大脑会调整突触以优化行为。在皮层中,突触嵌入在多层网络中,这导致我们难以确定单个突触的调整对整个系统行为的影响。而反向传播算法在深度神经网络中解决了上述问题,不过长期以来人们一直认为反向传播在生物层面上存在问题。

去年 4 月,来自 DeepMind、牛津大学和谷歌大脑的 Timothy P. Lillicrap、Adam Santoro、Geoffrey Hinton 等人在 Nature 子刊《Nature Reviews Neuroscience》发表文章,认为反向连接可能会引发神经活动,而其中的差异可用于局部逼近误差信号,从而促进大脑深层网络中的有效学习。即尽管大脑可能未实现字面形式的反向传播,但是反向传播的部分特征与理解大脑中的学习具备很强的关联性

大脑对反向传播算法的近似。

然而,讨论并未终止。最近,谷歌 Robotics 研究科学家 Eric Jang 发表博客,对 BPDL 中的反向传播观点提出质疑。

反向传播为什么一定要有生物学对应?

Eric Jang 首先列举了推动 BPDL 发展的主要原因:

  1. 深度神经网络 (DNN) 可以学习执行生物大脑能够完成的感知任务(如目标检测与识别);
  2. 如果激活单元及其权重与 DNN 的关系相当于神经元和突触之于生物大脑,那么反向传播(训练深度神经网络的主要方法)与什么类似呢?
  3. 如果使用反向传播无法实现大脑中的学习规则,那么这些规则要如何实现呢?基于反向传播的更新规则如何在遵循生物学约束的同时实现类似的性能?

有人曾列举了反向传播并非生物学可信的诸多理由,以及提出修复办法的多种算法。

来源:https://psychology.stackexchange.com/questions/16269/is-back-prop-biologically-plausible

而 Eric Jang 的反对意见主要在于,设计反向传播的生物学可信替代方法压根就是一个错误的问题。BPDL 的重要前提中包含了一个错误的假设:层激活是神经元,权重是突触,因此借助反向传播的学习必须在生物学习中有对应的部分

尽管 DNN 叫做深度「神经网络」,并在多项任务中展现出了卓越能力,但它们本质上与生物神经网络毫无关联。机器学习领域的一个严重错误就是,对统计学工具和最优控制算法赋予了太多生物学意义。这往往使初学者感到困惑。

DNN 是一系列线性操作和非线性操作的交织,序列应用于实值输入,仅此而已。它们通过梯度下降进行优化,利用一种叫做「反向传播」的动态规划机制对梯度进行高效计算。

动态规划是世界第九大奇迹,Eric Jang 认为这是计算机科学领域 Top 3 成就之一。反向传播在网络深度方面具备线性时间复杂度,因而从计算成本的角度来看,它很难被打败。许多 BPDL 算法往往不如反向传播,因为它们尝试在更新机制中利用高效的优化机制,且具备额外的约束。

如果目标是构建生物学可信的学习机制,那么 DNN 中的单元不应与生物神经元一一对应。尝试使用生物神经元模型模仿 DNN 是落后的,就像用人脑模拟 Windows 操作系统一样。这很难,而且人脑无法很好地模拟 Windows 系统。

我们反过来试一下呢:优化函数逼近器,以实现生物学可信的学习规则。这种方式较为直接:

  1. 使用模型神经元和突触连接构建神经网络的生物学可信模型。神经元利用脉冲序列、频率编码或梯度实现互相通信,并遵循任何「生物学可信」的约束。其参数需要训练。
  2. 使用计算机辅助搜索,为这些模型神经元设计生物学可信的学习规则。例如,将每个神经元的前向行为和局部更新规则建模为基于人工神经网络的决策。
  3. 更新函数逼近器,使生物学模型生成期望的学习行为。我们可以通过反向传播训练神经网络。

用来寻找学习规则的函数逼近器的选择是无关紧要的——我们真正在乎的是生物大脑如何学习像感知这样的困难任务,同时遵循已知的限制条件,如生物神经元不把所有的激活都存储在记忆中,或者只使用局部的学习规则。我们应该利用深度学习的能力找出优秀的函数逼近器,并以此来寻找优秀的生物学习规则。

「元学习」是另一种选择?

「我们应该(人工地)学习如何以生物的方式学习」并非一个全新的观点,但对于神经科学 + AI 社区来说,这一点还不够明显。元学习(学习如何学习)是近年来兴起的一个领域,它给出了获取能够执行学习行为的系统的方法,该系统有超越梯度下降的潜力。如果元学习可以帮我们找到更加样本高效或者更优秀、更鲁棒的学习器,那它为什么不能帮我们找到遵循生物学习约束的规则呢?其实,最近的几项研究 [1, 2, 3, 4, 5] 已经探索了这一问题。你确实可以使用反向传播来训练一个优于普通反向传播的独立学习规则。

Eric Jang 认为,很多研究者之所以还没理解这个观点(即我们应该用元学习方法来模拟生物学可信的回路),是因为目前算力还不够强,无法同时训练元学习器和学习器。要想制定元优化方案,我们还需要强大的算力和研究基础设施,但 JAX 等工具的出现已经让这一任务变得简单得多。

真正的生物学纯粹主义者可能会说,利用梯度下降和反向传播寻找学习规则不是一种「进化上可信的学习规则」,因为进化明显缺乏执行动态规划甚至是梯度计算的能力。但如果使元学习器在进化上可信,这一点就能得到修正。例如,用来选择优秀函数逼近器的机制其实根本不需要依赖反向传播。相反,我们可以制定一个元 - 元问题,让选择过程本身遵守进化选择的规则,但是选择过程还是使用反向传播。

所以,不要再给反向传播赋予生物学意义了!

原文链接:https://blog.evjang.com/2021/02/backprop.html

自己是一名从事了多年开发的web前端老程序员,目前辞职在做自己的web前端私人定制课程,今年年初我花了一个月整理了一份最适合2019年学习的web前端学习干货,各种框架都有整理,送给每一位前端小伙伴,想要获取的可以关注我的头条号并在后台私信我:前端,即可免费获取。

Proxy 对象(Proxy)是 ES6 的一个非常酷却鲜为人知的特性。虽然这个特性存在已久,但是我还是想在本文中对其稍作解释,并用一个例子说明一下它的用法。

什么是 Proxy

正如 MDN 上简单而枯燥的定义:

Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。

虽然这是一个不错的总结,但是我却并没有从中搞清楚 Proxy 能做什么,以及它能帮我们实现什么。

首先,Proxy 的概念来源于元编程。简单的说,元编程是允许我们运行我们编写的应用程序(或核心)代码的代码。例如,臭名昭著的 eval 函数允许我们将字符串代码当做可执行代码来执行,它是就属于元编程领域。

Proxy API 允许我们在对象和其消费实体中创建中间层,这种特性为我们提供了控制该对象的能力,比如可以决定怎样去进行它的 get 和 set,甚至可以自定义当访问这个对象上不存在的属性的时候我们可以做些什么。

Proxy 的 API

var
 p=new
 
Proxy
(
target
,
 handler
);

Proxy 构造函数获取一个 target 对象,和一个用来拦截 target 对象不同行为的 handler对象。你可以设置下面这些拦截项:

  • has —?拦截 in 操作。比如,你可以用它来隐藏对象上某些属性。
  • get —?用来拦截读取操作。比如当试图读取不存在的属性时,你可以用它来返回默认值。
  • set — 用来拦截赋值操作。比如给属性赋值的时候你可以增加验证的逻辑,如果验证不通过可以抛出错误。
  • apply — 用来拦截函数调用操作。比如,你可以把所有的函数调用都包裹在 try/catch 语句块中。

这只是一部分拦截项,你可以在 MDN 上找到完整的列表。

下面是将 Proxy 用在验证上的一个简单的例子:

const
 
Car={
 maker
:
 
'BMW'
,
 year
:
 
2018
,
};
const
 proxyCar=new
 
Proxy
(
Car
,
 
{
 
set
(
obj
,
 prop
,
 value
)
 
{
 
if
 
(
prop==='maker'
 
&&
 value
.
length 
<
 
1
)
 
{
 
throw
 
new
 
Error
(
'Invalid maker'
);
 
}
 
if
 
(
prop==='year'
 
&&
 
typeof
 value 
!=='number'
)
 
{
 
throw
 
new
 
Error
(
'Invalid year'
);
 
}
 obj
[
prop
]=value
;
 
return
 
true
;
 
}
});
proxyCar
.
maker=''
;
 
// throw exception
proxyCar
.
year='1999'
;
 
// throw exception

可以看到,我们可以用 Proxy 来验证赋给被代理对象的值。

使用 Proxy 来调试

为了在实践中展示 Proxy 的能力,我创建了一个简单的监测库,用来监测给定的对象或类,监测项如下:

  • 函数执行时间
  • 函数的调用者或属性的访问者
  • 统计每个函数或属性的被访问次数。

这是通过在访问任意对象、类、甚至是函数时,调用一个名为 proxyTrack 的函数来完成的。

如果你希望监测是谁给一个对象的属性赋的值,或者一个函数执行了多久、执行了多少次、谁执行的,这个库将非常有用。我知道可能还有其他更好的工具来实现上面的功能,但是在这里我创建这个库就是为了用一用这个 API。

使用 proxyTrack

首先,我们看看怎么用:

function
 
MyClass
()
 
{}
MyClass
.
prototype={
 isPrime
:
 
function
()
 
{
 
const
 num=this
.
num
;
 
for
(
var
 i=2
;
 i 
<
 num
;
 i
++)
 
if
(
num 
%
 i===0
)
 
return
 
false
;
 
return
 num 
!==1
 
&&
 num 
!==0
;
 
},
 num
:
 
null
,
};
MyClass
.
prototype
.
constructor=MyClass
;
const
 trackedClass=proxyTrack
(
MyClass
);
function
 start
()
 
{
 
const
 my=new
 trackedClass
();
 my
.
num=573723653
;
 
if
 
(!
my
.
isPrime
())
 
{
 
return
 
`
$
{
my
.
num
}
 is not prime
`;
 
}
}
function
 main
()
 
{
 start
();
}
main
();

如果我们运行这段代码,控制台将会输出:

MyClass
.
num is being 
set
 by start 
for
 the 
1
 time
MyClass
.
num is being get by isPrime 
for
 the 
1
 time
MyClass
.
isPrime was called by start 
for
 the 
1
 time and took 
0
 mils
.
MyClass
.
num is being get by start 
for
 the 
2
 time

proxyTrack 接受 2 个参数:第一个是要监测的对象/类,第二个是一个配置项对象,如果没传递的话将被置为默认值。我们看看这个配置项默认值长啥样:

const
 defaultOptions={
 trackFunctions
:
 
true
,
 trackProps
:
 
true
,
 trackTime
:
 
true
,
 trackCaller
:
 
true
,
 trackCount
:
 
true
,
 stdout
:
 
null
,
 filter
:
 
null
,
};

可以看到,你可以通过配置你关心的监测项来监测你的目标。比如你希望将结果输出出来,那么你可以将 console.log 赋给 stdout。

还可以通过赋给 filter 的回调函数来自定义地控制输出哪些信息。你将会得到一个包括有监测信息的对象,并且如果你希望保留这个信息就返回 true,反之返回 false。

在 React 中使用 proxyTrack

因为 React 的组件实际上也是类,所以你可以通过 proxyTrack 来实时监控它。比如:

class
 
MyComponent
 extends 
Component
{...}
export
 
default
 connect
(
mapStateToProps
)(
proxyTrack
(
MyComponent
,
 
{
 trackFunctions
:
 
true
,
 trackProps
:
 
true
,
 trackTime
:
 
true
,
 trackCaller
:
 
true
,
 trackCount
:
 
true
,
 filter
:
 
(
data
)=>
 
{
 
if
(
 data
.
type==='get'
 
&&
 data
.
prop==='componentDidUpdate'
)
 
return
 
false
;
 
return
 
true
;
 
}
}));

可以看到,你可以将你不关心的信息过滤掉,否则输出将会变得杂乱无章。

实现 proxyTrack

我们来看看 proxyTrack 的实现。

首先是这个函数本身:

export
 
function
 proxyTrack
(
entity
,
 options=defaultOptions
)
 
{
 
if
 
(
typeof
 entity==='function'
)
 
return
 trackClass
(
entity
,
 options
);
 
return
 trackObject
(
entity
,
 options
);
}

没什么特别的嘛,这里只是调用相关函数。

再看看 trackObject:

function
 trackObject
(
obj
,
 options={})
 
{
 
const
 
{
 trackFunctions
,
 trackProps 
}=options
;
 let resultObj=obj
;
 
if
 
(
trackFunctions
)
 
{
 proxyFunctions
(
resultObj
,
 options
);
 
}
 
if
 
(
trackProps
)
 
{
 resultObj=new
 
Proxy
(
resultObj
,
 
{
 
get
:
 trackPropertyGet
(
options
),
 
set
:
 trackPropertySet
(
options
),
 
});
 
}
 
return
 resultObj
;
}
function
 proxyFunctions
(
trackedEntity
,
 options
)
 
{
 
if
 
(
typeof
 trackedEntity==='function'
)
 
return
;
 
Object
.
getOwnPropertyNames
(
trackedEntity
).
forEach
((
name
)=>
 
{
 
if
 
(
typeof
 trackedEntity
[
name
]==='function'
)
 
{
 trackedEntity
[
name
]=new
 
Proxy
(
trackedEntity
[
name
],
 
{
 apply
:
 trackFunctionCall
(
options
),
 
});
 
}
 
});
}

可以看到,假如我们希望监测对象的属性,我们创建了一个带有 get 和 set 拦截器的被监测对象。下面是 set 拦截器的实现:

function
 trackPropertySet
(
options={})
 
{
 
return
 
function
 
set
(
target
,
 prop
,
 value
,
 receiver
)
 
{
 
const
 
{
 trackCaller
,
 trackCount
,
 stdout
,
 filter 
}=options
;
 
const
 error=trackCaller 
&&
 
new
 
Error
();
 
const
 caller=getCaller
(
error
);
 
const
 contextName=target
.
constructor
.
name==='Object'
 
?
 
''
 
:
 
`
$
{
target
.
constructor
.
name
}.`;
 
const
 name=`
$
{
contextName
}
$
{
prop
}`;
 
const
 hashKey=`
set_$
{
name
}`;
 
if
 
(
trackCount
)
 
{
 
if
 
(!
callerMap
[
hashKey
])
 
{
 callerMap
[
hashKey
]=1
;
 
}
 
else
 
{
 callerMap
[
hashKey
]++;
 
}
 
}
 let output=`
$
{
name
}
 is being 
set
`;
 
if
 
(
trackCaller
)
 
{
 output 
+=`
 by $
{
caller
.
name
}`;
 
}
 
if
 
(
trackCount
)
 
{
 output 
+=`
 
for
 the $
{
callerMap
[
hashKey
]}
 time
`;
 
}
 let canReport=true
;
 
if
 
(
filter
)
 
{
 canReport=filter
({
 type
:
 
'get'
,
 prop
,
 name
,
 caller
,
 count
:
 callerMap
[
hashKey
],
 value
,
 
});
 
}
 
if
 
(
canReport
)
 
{
 
if
 
(
stdout
)
 
{
 stdout
(
output
);
 
}
 
else
 
{
 console
.
log
(
output
);
 
}
 
}
 
return
 
Reflect
.
set
(
target
,
 prop
,
 value
,
 receiver
);
 
};
}

更有趣的是 trackClass 函数(至少对我来说是这样):

function
 trackClass
(
cls
,
 options={})
 
{
 cls
.
prototype=trackObject
(
cls
.
prototype
,
 options
);
 cls
.
prototype
.
constructor=cls
;
 
return
 
new
 
Proxy
(
cls
,
 
{
 construct
(
target
,
 args
)
 
{
 
const
 obj=new
 target
(...
args
);
 
return
 
new
 
Proxy
(
obj
,
 
{
 
get
:
 trackPropertyGet
(
options
),
 
set
:
 trackPropertySet
(
options
),
 
});
 
},
 apply
:
 trackFunctionCall
(
options
),
 
});
}

在这个案例中,因为我们希望拦截这个类上不属于原型上的属性,所以我们给这个类的原型创建了个代理,并且创建了个构造函数拦截器。

别忘了,即使你在原型上定义了一个属性,但如果你再给这个对象赋值一个同名属性,JavaScript 将会创建一个这个属性的本地副本,所以赋值的改动并不会改变这个类其他实例的行为。这就是为何只对原型做代理并不能满足要求的原因。

作者:前端下午茶 公号 / SHERlocked93