本节目标在于增加对计算机底层的一些认识,掌握后可以更清楚地明了一些"坑"产生的原因。本节内容也是一些笔面试中考察的基础知识点,将有助于加快打通"任督二脉"的步伐。
# 值传送与引用传递
在上个小节中不小心又回到了选择组件,在生产项目中这种选择组件非常常用。还记得在班级管理中初始接触[选择组件](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,该项不选中。
就到这吧。
- 序言
- 第一章:Hello World
- 第一节:Angular准备工作
- 1 Node.js
- 2 npm
- 3 WebStorm
- 第二节:Hello Angular
- 第三节:Spring Boot准备工作
- 1 JDK
- 2 MAVEN
- 3 IDEA
- 第四节:Hello Spring Boot
- 1 Spring Initializr
- 2 Hello Spring Boot!
- 3 maven国内源配置
- 4 package与import
- 第五节:Hello Spring Boot + Angular
- 1 依赖注入【前】
- 2 HttpClient获取数据【前】
- 3 数据绑定【前】
- 4 回调函数【选学】
- 第二章 教师管理
- 第一节 数据库初始化
- 第二节 CRUD之R查数据
- 1 原型初始化【前】
- 2 连接数据库【后】
- 3 使用JDBC读取数据【后】
- 4 前后台对接
- 5 ng-if【前】
- 6 日期管道【前】
- 第三节 CRUD之C增数据
- 1 新建组件并映射路由【前】
- 2 模板驱动表单【前】
- 3 httpClient post请求【前】
- 4 保存数据【后】
- 5 组件间调用【前】
- 第四节 CRUD之U改数据
- 1 路由参数【前】
- 2 请求映射【后】
- 3 前后台对接【前】
- 4 更新数据【前】
- 5 更新某个教师【后】
- 6 路由器链接【前】
- 7 观察者模式【前】
- 第五节 CRUD之D删数据
- 1 绑定到用户输入事件【前】
- 2 删除某个教师【后】
- 第六节 代码重构
- 1 文件夹化【前】
- 2 优化交互体验【前】
- 3 相对与绝对地址【前】
- 第三章 班级管理
- 第一节 JPA初始化数据表
- 第二节 班级列表
- 1 新建模块【前】
- 2 初识单元测试【前】
- 3 初始化原型【前】
- 4 面向对象【前】
- 5 测试HTTP请求【前】
- 6 测试INPUT【前】
- 7 测试BUTTON【前】
- 8 @RequestParam【后】
- 9 Repository【后】
- 10 前后台对接【前】
- 第三节 新增班级
- 1 初始化【前】
- 2 响应式表单【前】
- 3 测试POST请求【前】
- 4 JPA插入数据【后】
- 5 单元测试【后】
- 6 惰性加载【前】
- 7 对接【前】
- 第四节 编辑班级
- 1 FormGroup【前】
- 2 x、[x]、{{x}}与(x)【前】
- 3 模拟路由服务【前】
- 4 测试间谍spy【前】
- 5 使用JPA更新数据【后】
- 6 分层开发【后】
- 7 前后台对接
- 8 深入imports【前】
- 9 深入exports【前】
- 第五节 选择教师组件
- 1 初始化【前】
- 2 动态数据绑定【前】
- 3 初识泛型
- 4 @Output()【前】
- 5 @Input()【前】
- 6 再识单元测试【前】
- 7 其它问题
- 第六节 删除班级
- 1 TDD【前】
- 2 TDD【后】
- 3 前后台对接
- 第四章 学生管理
- 第一节 引入Bootstrap【前】
- 第二节 NAV导航组件【前】
- 1 初始化
- 2 Bootstrap格式化
- 3 RouterLinkActive
- 第三节 footer组件【前】
- 第四节 欢迎界面【前】
- 第五节 新增学生
- 1 初始化【前】
- 2 选择班级组件【前】
- 3 复用选择组件【前】
- 4 完善功能【前】
- 5 MVC【前】
- 6 非NULL校验【后】
- 7 唯一性校验【后】
- 8 @PrePersist【后】
- 9 CM层开发【后】
- 10 集成测试
- 第六节 学生列表
- 1 分页【后】
- 2 HashMap与LinkedHashMap
- 3 初识综合查询【后】
- 4 综合查询进阶【后】
- 5 小试综合查询【后】
- 6 初始化【前】
- 7 M层【前】
- 8 单元测试与分页【前】
- 9 单选与多选【前】
- 10 集成测试
- 第七节 编辑学生
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 功能开发【前】
- 4 JsonPath【后】
- 5 spyOn【后】
- 6 集成测试
- 7 @Input 异步传值【前】
- 8 值传递与引入传递
- 9 @PreUpdate【后】
- 10 表单验证【前】
- 第八节 删除学生
- 1 CSS选择器【前】
- 2 confirm【前】
- 3 功能开发与测试【后】
- 4 集成测试
- 5 定制提示框【前】
- 6 引入图标库【前】
- 第九节 集成测试
- 第五章 登录与注销
- 第一节:普通登录
- 1 原型【前】
- 2 功能设计【前】
- 3 功能设计【后】
- 4 应用登录组件【前】
- 5 注销【前】
- 6 保留登录状态【前】
- 第二节:你是谁
- 1 过滤器【后】
- 2 令牌机制【后】
- 3 装饰器模式【后】
- 4 拦截器【前】
- 5 RxJS操作符【前】
- 6 用户登录与注销【后】
- 7 个人中心【前】
- 8 拦截器【后】
- 9 集成测试
- 10 单例模式
- 第六章 课程管理
- 第一节 新增课程
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 async管道【前】
- 4 优雅的测试【前】
- 5 功能开发【前】
- 6 实体监听器【后】
- 7 @ManyToMany【后】
- 8 集成测试【前】
- 9 异步验证器【前】
- 10 详解CORS【前】
- 第二节 课程列表
- 第三节 果断
- 1 初始化【前】
- 2 分页组件【前】
- 2 分页组件【前】
- 3 综合查询【前】
- 4 综合查询【后】
- 4 综合查询【后】
- 第节 班级列表
- 第节 教师列表
- 第节 编辑课程
- TODO返回机制【前】
- 4 弹出框组件【前】
- 5 多路由出口【前】
- 第节 删除课程
- 第七章 权限管理
- 第一节 AOP
- 总结
- 开发规范
- 备用