深入理解JavaScript系列(19):求值策略(Evaluation strategy)详解


Posted in Javascript onMarch 05, 2015

介绍

本章,我们将讲解在ECMAScript向函数function传递参数的策略。

计算机科学里对这种策略一般称为“evaluation strategy”(大叔注:有的人说翻译成求值策略,有的人翻译成赋值策略,通看下面的内容,我觉得称为赋值策略更为恰当,anyway,标题还是写成大家容易理解的求值策略吧),例如在编程语言为求值或者计算表达式设置规则。向函数传递参数的策略是一个特殊的case。

http://dmitrysoshnikov.com/ecmascript/chapter-8-evaluation-strategy/
写这篇文章的原因是因为论坛上有人要求准确解释一些传参的策略,我们这里给出了相应的定义,希望对大家有所帮助。

很多程序员都确信在JavaScript中(甚至其它一些语言),对象是按引用传参,而原始值类型按值传参,此外,很多文章都说到这个“事实”,但有多人真正理解这个术语,而且又有多少是正确的?我们本篇讲逐一讲解。

一般理论

需要注意到,在赋值理论里一般有2中赋值策略:严格——意思是说参数在进入程序之前是经过计算过的;非严格——意思是参数的计算是根据计算要求才去计算(也就是相当于延迟计算)。

然后,这里我们考虑基本的函数传参策略,从ECMAScript出发点来说是非常重要的。首先需要注意的是,在ECMAScript中(甚至其他的语如,C,JAVA,Python和Ruby中)都使用了严格的参数传递策略。

另外传递参数的计算顺序也是很重要的——在ECMAScript是左到右,而且其它语言实现的反省顺序(从右向做)也是可以用的。

严格的传参策略也分为几种子策略,其中最重要的一些策略我们在本章详细讨论。

下面讨论的策略不是全部都用在ECMAScript中,所以在讨论这些策略的具体行为的时候,我们使用了伪代码来展示。

按值传递

按值传递,很多开发人员都很了解了,参数的值是调用者传递的对象值的拷贝(copy of value),函数内部改变参数的值不会影响到外面的对象(该参数在外面的值),一般来说,是重新分配了新内存(我们不关注分配内存是怎么实现的——也是是栈也许是动态内存分配),该新内存块的值是外部对象的拷贝,并且它的值是用到函数内部的。

bar = 10

 

procedure foo(barArg):

  barArg = 20;

end

 

foo(bar)

 

// foo内部改变值不会影响内部的bar的值

print(bar) // 10

但是,如果该函数的参数不是原始值而是复杂的结构对象是时候,将带来很大的性能问题,C++就有这个问题,将结构作为值传进函数的时候——就是完整的拷贝。

我们来给一个一般的例子,用下面的赋值策略来检验一下,想想一下一个函数接受2个参数,第1个参数是对象的值,第2个是个布尔型的标记,用来标记是否完全修改传入的对象(给对象重新赋值),还是只修改该对象的一些属性。

// 注:以下都是伪代码,不是JS实现

bar = {

  x: 10,

  y: 20

}

 

procedure foo(barArg, isFullChange):

 

  if isFullChange:

    barArg = {z: 1, q: 2}

    exit

  end

 

  barArg.x = 100

  barArg.y = 200

 

end

 

foo(bar)

 

// 按值传递,外部的对象不被改变

print(bar) // {x: 10, y: 20}

 

// 完全改变对象(赋新值)

foo(bar, true)

 

//也没有改变

print(bar) // {x: 10, y: 20}, 而不是{z: 1, q: 2}

按引用传递

另外一个众所周知的按引用传递接收的不是值拷贝,而是对象的隐式引用,如该对象在外部的直接引用地址。函数内部对参数的任何改变都是影响该对象在函数外部的值,因为两者引用的是同一个对象,也就是说:这时候参数就相当于外部对象的一个别名。

伪代码:

procedure foo(barArg, isFullChange):

 

  if isFullChange:

    barArg = {z: 1, q: 2}

    exit

  end

 

  barArg.x = 100

  barArg.y = 200

 

end

 

// 使用和上例相同的对象

bar = {

  x: 10,

  y: 20

}

 

// 按引用调用的结果如下: 

foo(bar)

 

// 对象的属性值已经被改变了

print(bar) // {x: 100, y: 200}

 

// 重新赋新值也影响到了该对象

foo(bar, true)

 

// 此刻该对象已经是一个新对象了

print(bar) // {z: 1, q: 2}

该策略可以更有效地传递复杂对象,例如带有大批量属性的大结构对象。

按共享传递(Call by sharing)
上面2个策略大家都是知道的,但这里要讲的一个策略可能大家不太了解(其实是学术上的策略)。但是,我们很快就会看到这正是它在ECMAScript中的参数传递战略中起着关键作用的策略。

这个策略还有一些代名词:“按对象传递”或“按对象共享传递”。

该策略是1974年由Barbara Liskov为CLU编程语言提出的。

该策略的要点是:函数接收的是对象对于的拷贝(副本),该引用拷贝和形参以及其值相关联。

这里出现的引用,我们不能称之为“按引用传递”,因为函数接收的参数不是直接的对象别名,而是该引用地址的拷贝。

最重要的区别就是:函数内部给参数重新赋新值不会影响到外部的对象(和上例按引用传递的case),但是因为该参数是一个地址拷贝,所以在外面访问和里面访问的都是同一个对象(例如外部的该对象不是想按值传递一样完全的拷贝),改变该参数对象的属性值将会影响到外部的对象。

procedure foo(barArg, isFullChange):

 

  if isFullChange:

    barArg = {z: 1, q: 2}

    exit

  end

 

  barArg.x = 100

  barArg.y = 200

 

end
//还是使用这个对象结构

bar = {

  x: 10,

  y: 20

}

 

// 按贡献传递会影响对象 

foo(bar)

 

// 对象的属性被修改了

print(bar) // {x: 100, y: 200}

 

// 重新赋值没有起作用

foo(bar, true)

 

// 依然是上面的值

print(bar) // {x: 100, y: 200}

这个处理的假设前提是大多数语言里用到的对象,而不是原始值。

按共享传递是按值传递的特例

按共享传递这个策略很很多语言里都使用了:Java, ECMAScript, Python, Ruby, Visual Basic等。此外,Python社区已经使用了这个术语,至于其他语言也可以用这个术语,因为其他的名称往往会让大家感觉到混乱。大多数情况下,例如在Java,ECMAScript或Visual Basic中,这一策略也称之为按值传递——意味着:特殊值——引用拷贝(副本)。

一方面,它是这样的——传递给函数内部用的参数仅仅是绑定值(引用地址)的一个名称,并不会影响外部的对象。

另一方面,如果不深入研究,这些术语真的被认为吃错误的,因为很多论坛都在说如何将对象传递给JavaScript函数)。

一般理论确实有按值传递的说法:但这时候这个值就是我们所说的地址拷贝(副本),因此并没哟破坏规则。

在Ruby中,这个策略称为按引用传递。再说一下:它不是按照大结构的拷贝来传递(例如,不是按值传递),而另一方面,我们没有处理原始对象的引用,并且不能修改它;因此,这个跨术语的概念可能更会造成混乱。

理论里没有像按值传递的特殊case一样来面试按引用传递的特殊case。

但依然有必要了解这些策略在上述提到的技术中(Java, ECMAScript, Python, Ruby, other),实际上——他们用的策略就是按共享传递。

按共享与指针

对于С/С+ +,这个策略在思想上和按指针值传递是一样的,但有一个重要的区别——该策略可以取消引用指针以及完全改变对象。但在一般情况下,分配一个值(地址)指针到新的内存块(即之前引用的内存块保持不变);通过指针改变对象属性的话会影响阿东外部对象。

因此,和指针类别,我们可以明显看到,这是按地址值传递。 在这种情况下,按共享传递只是“语法糖”,像指针赋值行为一样(但不能取消引用),或者像引用一样修改属性(不需要取消引用操作),有时候,它可以被命名为“安全指针”。

然而,С/С+ +如果在没有明显指针的解引用的情况下,引用对象属性的时候,还具有特殊的语法糖:

obj->x instead of (*obj).x

和C++关系最为紧密的这种意识形态可以从“智能指针”的实现中看到,例如,在 boost :: shared_ptr里,重载了赋值操作符以及拷贝构造函数,而且还使用了对象的引用计数器,通过GC删除对象。 这种数据类型,甚至有类似的名字- 共享_ptr。

ECMAScript实现

现在我们知道了ECMAScript中将对象作为参数传递的策略了——按共享传递:修改参数的属性将会影响到外部,而重新赋值将不会影响到外部对象。但是,正如我们上面提到的,其中的ECMAScript开发人员一般都称之为是:按值传递,只不过该值是引用地址的拷贝。

JavaScript发明人布伦丹·艾希也写到了:传递的是引用的拷贝(地址副本)。所以论坛里大家曾说的按值传递,在这种解释下,也是对的。

更确切地说,这种行为可以理解为简单的赋值,我们可以看到,内部是完全不同的对象,只不过引用的是相同的值——也就是地址副本。

ECMAScript代码:

var foo = {x: 10, y: 20};

var bar = foo;

 

alert(bar === foo); // true

 

bar.x = 100;

bar.y = 200;

 

alert([foo.x, foo.y]); // [100, 200]

即两个标识符(名称绑定)绑定到内存中的同一个对象, 共享这个对象:

foo value: addr(0xFF) => {x: 100, y: 200} (address 0xFF) <= bar value: addr(0xFF)
而重新赋值分配,绑定是新的对象标识符(新地址),而不影响已经先前绑定的对象 :

bar = {z: 1, q: 2};

 

alert([foo.x, foo.y]); // [100, 200] ? 没改变

alert([bar.z, bar.q]); // [1, 2] ? 但现在引用的是新对象

即现在foo和 bar,有不同的值和不同的地址:
foo value: addr(0xFF) => {x: 100, y: 200} (address 0xFF)

bar value: addr(0xFA) => {z: 1, q: 2} (address 0xFA)

再强调一下,这里所说对象的值是地址(address),而不是对象结构本身,将变量赋值给另外一个变量——是赋值值的引用。因此两个变量引用的是同一个内存地址。下一个赋值却是新地址,是解析与旧对象的地址绑定,然后绑定到新对象的地址上,这就是和按引用传递的最重要区别。

此外,如果只考虑ECMA-262标准所提供的抽象层次,我们在算法里看到的只有“值”这个概念,实现传递的“值”(可以是原始值,也可以是对象),但是按照我们上面的定义,也可以完全称之为“按值传递”,因为引用地址也是值。

然而,为了避免误解(为什么外部对象的属性可以在函数内部改变),这里依然需要考虑实现层面的细节——我们看到的按共享传递,或者换句话讲——按安全指针传递,而安全指针不可能去解除引用和改变对象的,但可以去修改该对象的属性值。

术语版本

让我们来定义ECMAScript中该策略的术语版本。

可以称之为“按值传递”——这里所说的值是一个特殊的case,也就是该值是地址副本(address copy)。从这个层面我们可以说:ECMAScript中除了异常之外的对象都是按值传递的,这实际上是ECMAScript抽象的层面。

或针对这种情况下,专门称之为“按共享传递”,通过这个正好可以看到传统的按值传递和按引用传递的区别,这种情况,可以分成2个种情况:1:原始值按值传递;2:对象按共享传递。

“通过引用类型将对象到函数”这句话和ECMAScript无关,而且它是错误的。

结论

我希望这篇文章有助于宏观上了解更多细节,以及在ECMAScript中的实现。一如既往,如果有任何问题,欢迎讨论。

Javascript 相关文章推荐
Jquery 跨域访问 Lightswitch OData Service的方法
Sep 11 Javascript
javascript关于继承的用法汇总
Dec 20 Javascript
gameboy网页闯关游戏(riddle webgame)--仿微信聊天的前端页面设计和难点
Feb 21 Javascript
使用struts2+Ajax+jquery验证用户名是否已被注册
Mar 22 Javascript
JS简单实现DIV相对于浏览器固定位置不变的方法
Jun 17 Javascript
javascript删除html标签函数cIsHTML
Jan 09 Javascript
AngularJS路由实现页面跳转实例
Mar 03 Javascript
angularjs中ng-bind-html的用法总结
May 23 Javascript
vue生成token保存在客户端localStorage中的方法
Oct 25 Javascript
JavaScript实现简单动态进度条效果
Apr 06 Javascript
jQuery插件实现的日历功能示例【附源码下载】
Sep 07 jQuery
layui表格 列自动适应大小失效的解决方法
Sep 06 Javascript
如何实现chrome浏览器关闭页面时弹出“确定要离开此面吗?”
Mar 05 #Javascript
深入理解JavaScript系列(18):面向对象编程之ECMAScript实现
Mar 05 #Javascript
基于zepto.js实现仿手机QQ空间的大图查看组件ImageView.js详解
Mar 05 #Javascript
基于jQuery实现网页进度显示插件
Mar 04 #Javascript
基于jQuery实现仿淘宝套餐选择插件
Mar 04 #Javascript
js实现类似于add(1)(2)(3)调用方式的方法
Mar 04 #Javascript
jquery 插件实现多行文本框[textarea]自动高度
Mar 04 #Javascript
You might like
一次编写,随处运行
2006/10/09 PHP
PHP观察者模式示例【Laravel框架中有用到】
2018/06/15 PHP
用javascript实现读取txt文档的脚本
2007/07/20 Javascript
jQuery dialog 异步调用ashx,webservice数据的代码
2010/08/03 Javascript
jQuery点击自身以外地方关闭弹出层的简单实例
2013/12/24 Javascript
Jquery实现地铁线路指示灯提示牌效果的方法
2015/03/02 Javascript
详述JavaScript实现继承的几种方式(推荐)
2016/03/22 Javascript
详解VUE的状态控制与延时加载刷新
2017/03/27 Javascript
Jquery中.bind()、.live()、.delegate()和.on()之间的区别详解
2017/08/01 jQuery
详解es6超好用的语法糖Decorator
2018/08/01 Javascript
详解Vue CLI3配置之filenameHashing使用和源码设计使用和源码设计
2018/08/31 Javascript
Vue自定义指令上报Google Analytics事件统计的方法
2019/02/25 Javascript
Vue切换div显示隐藏,多选,单选代码解析
2020/07/14 Javascript
[48:00]EG vs LGD 2018国际邀请赛淘汰赛BO3 第二场 8.26
2018/08/29 DOTA
介绍Python中的一些高级编程技巧
2015/04/02 Python
Python-OpenCV基本操作方法详解
2018/04/02 Python
Python 使用list和tuple+条件判断详解
2019/07/30 Python
python自动保存百度盘资源到百度盘中的实例代码
2019/08/26 Python
Python使用plt.boxplot() 参数绘制箱线图
2020/06/04 Python
Python趣味入门教程之循环语句while
2020/08/26 Python
Django自带用户认证系统使用方法解析
2020/11/12 Python
python 实现socket服务端并发的四种方式
2020/12/14 Python
Python3爬虫RedisDump的安装步骤
2021/02/20 Python
新加坡航空官方网站:Singapore Airlines
2016/10/13 全球购物
HearthSong官网:儿童户外玩具、儿童益智玩具
2017/10/16 全球购物
英国排名第一的宠物店:PetPlanet
2020/02/02 全球购物
退休党员个人对照检查材料思想汇报
2014/09/29 职场文书
公司委托书格式范文
2014/10/09 职场文书
大学生考试作弊检讨书1000字
2014/10/14 职场文书
基层工作经验证明样本
2014/11/16 职场文书
大学生先进个人主要事迹材料
2015/11/04 职场文书
2016年党员岗位承诺书
2016/03/24 职场文书
用javascript制作qq注册动态页面
2021/04/14 Javascript
SpringBoot集成Redis,并自定义对象序列化操作
2021/06/22 Java/Android
SpringCloud之@FeignClient()注解的使用方式
2021/09/25 Java/Android
使用Apache Camel表达REST服务的方法
2022/06/10 Servers