本节目标在于增加对计算机底层的一些认识,掌握后可以更清楚地明了一些"坑"产生的原因。本节内容也是一些笔面试中考察的基础知识点,将有助于加快打通"任督二脉"的步伐。 # 值传送与引用传递 在上个小节中不小心又回到了选择组件,在生产项目中这种选择组件非常常用。还记得在班级管理中初始接触[选择组件](https://www.kancloud.cn/yunzhiclub/springboot_angular_guide/1364543)时遇到[教师未自动选中的问题](https://www.kancloud.cn/yunzhiclub/springboot_angular_guide/1372247)时只是草草的介绍了可以使用`compareFn`方法来比较两个对象是否相等,但却未说明原由: 根本上来讲,由于js中对变量的`判等`会根据变量类型而采取不同的方法,所以在进行两个`教师`是否相等的判断上,js会认为两个值一样的`教师`仅仅是一对双胞胎而已。如果问他两个教师是否相等,它会毫不犹豫的告诉我们:不! js为什么在对象的判等上会这么处理呢,这还得从JS的变量传递方式说起: 基本上所有的语言都归避不了值传递与引用传递的问题,也都存在值传递与引用传递。这包括了笔者当前接触的所有的语言:C、C++、JAVA、JAVASCRIPT、PHP等。同时引用传递又被称为:地址传递或指针传递。 > 注意:引用传递、地址传递及指针传递只是说法不同,教程中不会做刻意区分,用哪个词语来描绘也将比较随性。 **传递**发生了方法的调用上。比如有以下方法: ```javascript var a = function(name) { alert('the name is ' + name); }; ``` 然后调用上述方法: ``` a('mengyunzhi'); ``` ![](https://img.kancloud.cn/5d/3e/5d3ef00468f177ca8e5e63c96385403e_895x410.png) > TIP: 可以直接在chrome中的控制台来测试JS代码. 上述代码在进行调用`a`方法的过程中,将`mengyunzhi`这个字符串传递给了`a`方法。而这个将`mengyunzhi`字符串传递给`a`方法,就叫做传递。 ## 值传递简介 刚刚上面的例子就是最典型的值传递,也是在计算机语言的学习中接触的最早的一种传递方法。也比较容易理解。就是简单的在调用的过程中,把方法所需要的值传给方法。把值传给方法后,该方法对该值进行变更,不会对原来的值有任何的影响: 比如: ```javascript var a = function(name) { name = 'hello'; // ➊ alert('the name is ' + name); }; var name = 'mengyunzhi'; a(name); alert(name); // ➋ ``` * ➊ a方法接收了name值后,将其变更为`hello`。 * ➋ 原来的name还是`mengyunzhi`,并没有授`a`方法改变其接收的`name`的值的影响。 ![](https://img.kancloud.cn/8d/e9/8de9c3ea8832364f7374cd88d5499a3f_939x446.gif) ## 引用传送简介 同样是传值。只要上面的基础上稍加改动,那就不一样了: ```javascript var a = function(value) { value.name = 'hello'; // ➊ alert('a: ' + value.name); } var value = {name: 'mengyunzhi'}; a(value); // ★ alert(value.name); // ➋ ``` ![](https://img.kancloud.cn/56/12/5612c8f83584ca44ebdefdba324f77df_939x446.gif) 如上所示: * ➊ a方法接收了`value`值后,将其`name`变更为`hello`。 * ➋ 原来的`value`上的`name`已经由`mengyunzhi`被成功的变更为了`hello`。也就是说:方法`a`中对自己变量`value`的改变直接影响到了原变量的值。 之所以会这样的原因是:在★进行`a`的调用时,将`value`的**地址(引用、指针)传递给了`a`,而非`value`的值**。这种在方法调用时值的传递方式就是引用传递。 ## 二者区别 值传递与地址传递像极了上机实验中遇到难题时的两种解决方式。在上机实验中遇到自己解决不了的问题是常有的事,而向大牛求助应该是最简单最有效的一种方式。在求助的过程中按性别的不同,大概可以分为两种求救模式。 ### 男生向大牛求助 ``` 某男生:大牛哥,这是我的代码,能帮忙看看错哪里了吗? 大牛: 各种修改,各种DEBUG。 5分钟后 大牛:请修改第35行,第75行.... ``` 这就是最典型的值传递,我们把自己的代码传给了大牛。大牛接收代码后进代码进行的修改并不会影响我们计算机上的源码。这本就是拥有相同内容的**两份**代码。也就是:值传递就是最普通的`复制`+`粘贴`,经过此操作后1个变量变成了2个,对任何一个变量的修改都不会影响另外一个。 上述过程中。 ### 女生向大牛求助 情景一: ``` 某女生:大牛哥能帮帮我吗? 大牛:请求控制对方的电脑 某女生:接受控制 大牛:修改对方的代码并最终完成上机实验。 ``` 情景二: ``` 某女生:大牛哥能帮帮我吗? 大牛:你是第几排第几号 某女生:3排5号 ➊ 大牛:稍等马上就来 ➋ ... ``` * ➊ 女生向大牛传递了一个地址 * ➋ 大牛将按地址找到该女生所在的电脑 而这便是现实生活的引用传递(地址传递、指针传递)。请求大牛帮忙时,直接把自己的计算机的控制权交给了大牛。此时大牛对代码的修改就实实在在的发生在求助女生的计算机上。也就是说:无论谁在修改,修改的都是一份代码。 ### 总结 值传递:在调用时,我的变量还是我的变量,传递给你的仅是个`赝品` 地址传递:在调用时,我的变量就是你的变量,传递给你的就是我拥有的那个`真品` ## 判断 终于来到最关键的点上:什么时候进行是值传递、什么时候进行的是引用传递呢? 笔者认为这里面的规律可以简单的归纳为:该语言认为容易复制的变量按值传递、不容易复制的变量按引用传递。 javascript:该语言认为基本类型是容易被复制的,所以所有的基本类型都是按值进行传递的,javascript的基本类型为:`字符串(String)、数字(Number)、布尔(Boolean)、对空(Null)、未定义(Undefined)、Symbol`,除此以外`对象(Object)、数组(Array)、函数(Function)`不属于基本类型,则选择按引用传递。 java: 该语言的想法和js差不多,也是认为基本类型是容易被复制的,所以按值进行传递,而其它的按引用进行传递。java的基本类型为:`byte(字节型)、short(短整型)、int(整型)、long(长整型)、float(单精度浮点型)、double(双精度浮点型)、boolean(布尔型)、char(字符型)`。除此以外其它按地址传递。 php:该语言大体和java及js一致,认为基本的类型都是容易被复制的,所以按值传递。但有一点稍微不同的是:该语言认为数组也是比较容易复制的,所以数组在传值时也是按值传递的方法,而其它的非基本类型则是按地址传递。 c: 该语言相对于上述几门语言属于低级语言(无贬低,只是由于它更贴近于直接操作硬件,所以就是这种叫法)。C语言中凡是要进行地址传递的,都会明确的在编写代码时指出,比较好区分。不同于其它语言的是:即使是主类型C语言也可以选择按地址传递;而对于非主类型,则必然按地址进行传递(你无法在C语言中直接传递数组)。 # 延伸阅读 既然已经讲到这了,那么借机带领大家再深入的理解一下。否则当有一天你在较权威的书籍上看到"js没有引用传递,全部是按值传递"的论调时,一定会反过来怀疑教程的权威性。 继续阅读以前推荐先复习一下[4.6.2](https://www.kancloud.cn/yunzhiclub/springboot_angular_guide/1396784)中**图的认读**部分。 > 本教程中的理论不见得是正确的,但一定是较于当前的现实情况适用的。因为我们一直认为:"适用的就是最好的!",在学习的过程中一定要把握好学习的边界。 ## JAVASCRIPT与C 在值传递的过程中。几种语言的理论大体是相同的,但细节稍有不同。javascript传值过程大概如下: ```javascript var a = function(value1) { value1.name = 'hello'; alert('a: ' + value1.name); } var value = {name: 'meng'}; a(value); alert(value.name); ``` 则相应的执行过程如下: ![](https://img.kancloud.cn/e1/ab/e1abadaa5908a437b52cc23e02bc28e0_852x373.png) * ➊ 变量定义:开辟一块没有占用的内存给对象value(非主类型,存引用地址); * ➊ 再开辟一块没有占用的内存对象value中的属性name(非主类型,存引用地址); * ➊ 最后给name中的值分配连续的内存空间用于存字符串meng。 * ➋ 调用方法a,传入value。 * ➌ 由于value为对象,所以实际上获取的value对象的地址引用值1232H。 * ➍ 将value的地址引用值1232H传给方法a。 * ➎ 使用接收到的引用值1232H作为对象value1的引用。 * ➏ 开辟一块没有占用的内存给对象value1,该内存存入value1的引用值1232H。 所以执行`value1.name = 'hello'`内存中发生的变化笔者猜测试如下(未经考证): ![](https://img.kancloud.cn/1d/ba/1dbabbed6c75057a597dcd0eadb8a416_843x399.png) * ➐ 要存新的字符串,并要重新划分一块内存空间。(C语言是在原2340H位置做覆盖处理还是如上图一样重新开辟一块内存给`hello`,需要看具体的编码设计。JS是否重新开辟一块新的内容未验证。JAVA是重新开辟一块新内存)。 * ➑ 将原name的指向地址变更为新字符串`hello`所在的地址。 大概看清内存中的实际变化后,再重点看下➊➋➌➍➎。此5项共同决定了在进行引用传值时,接收方则会把引用的值做为数字变量来处理,复制一个而非直接利用。 ## JAVA java则不然,同样的代码,JAVA则是如下分配内存空间的: ![](https://img.kancloud.cn/04/68/0468e54906e4dda53014741a38e08d9f_851x400.png) 在➋➌➍➎步的传值过程中,java向方法中传入的并不是该变量所在内存单元的**值**,而是该变量所在内存单元的**地址**。这也是为什么有人会说JAVA是**地址**传递而js及c是均是**值**传递的原因。 ## 总结 笔者坚持认为:所有的语言都是有值传递及地址传地递的。传过去容易复制的,就会用值传递;传过去不容易复制的,就会用地址传递。而是否容易复制取决于伟大的语言创建者们的理解,比如javascript认为字符串是容易复制的,所以采用值传递;而java却认为字符串是不容易复制的,所以就采用了地址传递。至于有部分权威的书籍坚持说js没有址址传递,对于初学者而言只会增加我们理解该语言变量传递的难度,并无益处。 如果已经充分理解了上面几张主要的内存图,相信一定可以很好的理解以下代码的运行结果: ``` var a = function(value1) { ➋ value1 = {name: 'hello'}; ➌ console.log('a: ' + value1.name); ➍ } var a1 = function(value1) { ➏ value1.name = 'hello'; ➐ console.log('a: ' + value1.name); } var value = {name: 'meng'}; ➊ a(value); ➋ console.log(value.name); ➎ a1(value); ➏ console.log(value.name); ➑ ``` * ➊ value的实际值为1232H * ➋ 将1232H传入a,此时value1的值为1232H * ➌ 创建了新的对象`{name: hello}`,假设该对象的存储地址为:1240H,则此时value1的值为1240H * ➍ 获取1240H对应的对象中的name的值 * ➎ 获取1232H对应的对象的name的值(value的值仍然为1232H,并未发生变化)。 * ➏ 将1232H传入a1,此时value1的值为1232H * ➐ 设置1232H对应的对象中的name的值为hello(实际上为hello字符串对应的内存起始地址) * ➑ 获取1232H对应的对象的name的值(value的值还然为1232H,并未发生变化)。 最终结果如下: ![](https://img.kancloud.cn/6a/bd/6abdc2fbc62f886330ae86a5ca867fa0_401x336.png) # compareFn 再回到选择组件。正是由于js在对象的判等上采用的是`比较变量所在内存地址的值`的方法,该方法并不能很好的满足当前现实需求。所以angular为`select`提供了`compareFn`属性,在决定是否默认选中某个选项时,`select`将使用`compareFn`提供的方法来决定。如果该方法的值返回`true`,则选中;相反返回false则表示不选中。如果未给`select`传入`compareFn`方法,则`select`将采用默认的js的判断方法来决定该选中哪个选项。 再来回顾一下该方法: src/app/core/select/select.component.ts ``` /** * 比较函数,标识用哪个字段来比较两个对象是否为同一个对象 * @param t1 源 * @param t2 目标 */ compareFn(t1: { id: number }, t2: { id: number }) { return t1 && t2 ? t1.id === t2.id : t1 === t2; // ➊ } ``` * ➊ 如果由外界传入的klass、以及当前选项(指select里列表中的值)均存在,且两者的id值相等时,则选中该选项。 举个例子: 传入`klass = {id: 1}`,klass列表:`[{id: 1}}, {id: 2}]`。则在依次执行:`compareFn({id: 1}, {id: 1})`,返回true,该项选中;`compareFn({id: 1}, {id: 2})`,返回false,该项不选中。 就到这吧。