1.编程语言及计算机基础
编程:计算机为了解决某个问题设计语言编写程序代码
计算机程序:计算机中执行的一系列指令的集合
计算机语言:机器语言,汇编语言,高级语言
计算机只能看懂机器语言,由0和1构成,使用编程语言转化为计算机语言控制计算机
编程语言有固定的格式和词汇,必须遵守
计算机组成:硬件和软件
硬件:输入设备,输出设备,cpu,硬盘,内存
软件:系统软件,应用软件
计算机内使用二进制储存数据
数据的存储单位:
bit<byte<kb<GB<TB
1B=8b
1KB=1024B
1Mb=1024KB
1GB=1024MB
1TB=1024GB
2.认识JavaScript
运行在客户端的脚本语言,不需要编译,逐行解释并执行,属于高级编程语言
浏览器本身并不会执行JS代码,而是通过内置的JS引擎来执行代码。js引擎执行代码时逐行解释成机器语言,然后由计算机执行
js由ECMAScript和DOM与BOM组成
ECMAScript规定了js的编程语法和基础核心,是所有浏览器都共同遵守的一套语法标准
DOM是对页面中各种元素进行操作
BOM是对浏览器窗口进行操作
1.js的三种书写位置:
1.行内式,直接写在元素内(可读性差,一般不使用)
2.内嵌式,写在上面
3.外部式,创建.js文件,使用引入
js中的代码大部分使用单引号
2.js的注释:
单行注释://(快捷键ctrl+/)
多行注释:/**/(快捷键shift+alt+a)
vscode建议修改多行注释的快捷键:(ctrl+shift+/)
3.js的输入输出语句:
alert(弹出警示框,由浏览器弹出)
document.write()(向html中输出内容,可以直接输出标签)
prompt(用户输入框,由浏览器弹出)
console.log(控制台输出,在检查元素中看到的)
4.js的变量:
变量简单来说就是一个装数据的容器,我们可以通过变量名获取数据,甚至修改数据,本质是在内存中申请一块用来存放数据的空间
5.变量的使用:
1.声明变量:
let 变量名// 声明一个名称为 变量名 的变量
2.赋值
变量名 = 所需值 //给 变量名 这个变量赋予所需值
以上可以结合一次性使用:
let 变量名 = 所需值
注意输入中文要加入单引号’’
6.变量语法扩展
1.更新变量
变量会被后赋予的值覆盖前面的值,类似css中的层叠性
2.可以同时生命多个变量
let不允许多次声明同一个变量
let 变量名1 = 所需值,变量名2 = 所需值,变量名3 = 所需值;
3.声明变量的特殊情况
只声明不赋值,结果是undefined
输出未声明的变量,会报错
不声明,直接赋值使用是可以使用的,但是不提倡使用
7.变量命名规范
由字母,数字,下划线和美元符号组成,严格区分大小写,不能以数字开头,不能是关键字/保留字(例如let,for,while,name这样的有特殊意义的字符),变量名尽量有意义,遵守驼峰命名法(首字母小写,后面单词首字母大写例如myFirstName)
8.JS的常量:
使用const声明的变量称之为常量
当某个变量永远不会改变的时候就可以使用const声明
常量不允许重新赋值,声明的时候必须赋值(初始化)
3.数据的类型
在计算机中,不同的数据所占的储存空间不同为了便于把数据分成所需内存大小不同的数据,充分利用空间,定义了不同的数据类型
js 的变量数据类型是只有在程序运行过程中,根据所赋予的值确定的,不主动声明,并且在使用中可以变化
1.数据类型分类.
1.简单数据类型
1.数字型number:
可以包含整数和小数
八进制:在数据前加0
十六进制:在数据前加0x
js中数字型的最大值:Number.MAX_VALUE
js中数字型的最小值:Number.MIN_VALUE
无穷大:Infinity,大于任何数值可以加入负号(-)成为无穷小
非数值:NaN
NaN是粘性的,任何对NaN进行的操作返回的都是NaN
isNaN(x):用来判断非数值,x是数字返回false,x不是数字返回true
2.布尔值类型boolean:
如true,false等价于1和0
3.字符串型string:
在引号内的,都是字符串型,比如’张三’,‘12’,‘true’
字符串引号嵌套:
js中引号使用就近原则,如果要在字符串内要使用引号,要和外面使用的引号不同:外双内单,外单内双也可以使用反引号``
字符串转义符(要写在引号内):
\n:换行
\: 显示一个\
':显示一个单引号
":显示一个双引号
\t:tab缩进
\b:空格
检测获取字符串的长度:字符串名+.length
字符串的拼接:
多个字符串之间可以使用+号拼接,可以+任何数据类型,最后结果一定是字符串型,两个数值型+相加,字符串+任何都是相连。
变量和字符串相连,要使用+变量+的结构
模板字符串:
由``组成内容包括变量时
可以不用+号 直接将变量用${}括起来,可以换行
4.undefined和null型
声明且未赋值称之为undefined型
与数值型相加为NaN
空值称之为null型,null型其实是一个未定义的对象
与数值型相加就是原先的数字
2.检测变量的数据类型
在输出中输入typeof+变量名,就可以输出变量数据类型
prompt所取的值是字符型的
3.数据类型的转换
1.转换为字符串
1.toString
变量+.toString()即可转换为字符型
2.String()
String(变量)即刻转换
3.利用+号拼接字符串的方法转换
可以+一个空的’'转换为字符串,一般更经常使用这种方式,称之为隐式转换
2.转换为数字型
1.parseInt()
在括号内加入变量名,可以转化为整数型数字型,不会进位,没有四舍五入效果,会自动去掉里面的字母(单位),但是数字前不能有字母
2.parseFloat()
在括号内加入变量名,可以转化为浮点型数字型,可以保留小数,会自动去掉里面的字母(单位),但是数字前不能有字母
3.Number()
变量写在括号内,强制转换为数字型
4.利用算术运算-*/进行隐式转换
使用其他类型的数值减去0(乘/除1)进行转换
5.在字符型前加上+号,前面不跟其他东西
3.转换为布尔型
Boolean()函数
代表空/否定的值都换转换为false,例如0,NaN,null,undefined,‘’
其余的都换转化为true
4.运算符
也被称之为操作符,用于实现赋值,比较和执行算术运算等功能
1.算术运算符
算术运算使用的符号,用于执行两个变量或值的运算
最简单的算术运算符:+,-,*,/
其他:%取余,只留余数
浮点数在算术运算内会有问题,尽量避免浮点数直接用于运算,不要直接判断两个浮点数是否相等
2.表达式和返回值
表达式:由数字,运算符和变量组成的式子
返回值:由表达式的程序运算后输出的值
3.递增递减运算符
递增/递减:++/–
实现前置递增(递减):++(–)变量,先自加1,后返回值
实现后置递增(递减):变量++(–),先返回原值,后自加1
前置和后置如果单独使用,效果是一样的
开发时一般使用后置,且代码独占一行
4.比较运算符
对两个数据进行比较,比较后会返回一个布尔值(true/false)
5.逻辑(布尔值)运算符
逻辑与:&&,简称与或者and,一假则假
逻辑或:||,简称或或者or,一真则真
逻辑非:!,简称非或者not,取反
输出为布尔值(true/false)
1.短路运算:
值或者表达式参与逻辑运算时,
1.逻辑与短路:
表达式1&&表达式2
如果第一个表达式的值为真(即不是0,null,空值,NaN,undefined这类),则返回表达式2
如果第一个表达式的值为假,则返回表达式1,后面不再执行
2.逻辑或短路:
表达式1||表达式2
如果表达式1为真,则返回表达式1,后面不再执行
如果表达式1为假,则返回表达式2
6.赋值运算符
=:直接赋值,将等号右边赋值到等号左边
+=或-=:加/减一个数后再赋值,例如age=10,age+=5,//age =15
=,/=,%=:乘,除,取余后再赋值,例如age=2,age=5 //age=10
7.运算符优先级
5.流程控制
控制代码按照什么样的顺序执行,直接影响最后的功能
js中有三种流程控制方式:顺序执行,分支执行和循环执行
顺序执行就是按照代码的先后顺序进行执行
1.分支流程控制
根据不同的条件,执行不同的代码,得到不同的结果
1.if语句
结构:
if(条件表达式){
执行语句1
}
如果if里面的条件表达式结果为真,则执行括号内的语句,如果为假,不执行
所有数字都为真,除了0,除了空字符串,所有字符串都为真
可以在后面加入else语句
在上方的结构后面加入
else{
执行语句2
}
如果条件表达式表达结果为假,执行else中的语句
2.if else if语句
属于多分支语句,多选一
结构:
if(条件表达式1){
语句1
}
else if(条件表达式2){
语句2
}
else if(条件表达式3){
语句3
}
…………
else{
最后语句,以上都不符合
}
如果满足条件1,这执行语句1,后面都不执行,退出整个if分支语句,以此类推
多分支语句还是多选1,最后只能有一个执行
3.三元表达式
结构:
条件表达式1 ? 表达式1 : 表达式2
如果为真,返回表达式1,如果为假,返回表达式2,表达式有返回值,需要一个变量储存
4.switch语句
基于不同条件执行不同代码,一般对于设置特定值执行特定代码
格式:
switch (表达式){
case value1:执行语句1;break ;
case value2:执行语句2;break ;
case value3:执行语句3;break ;
case value4:执行语句4;break ;
………………
default:以上都不满足执行的语句;
}
表达式与case内的value相匹配的时候一定是全等
如果没有break,则不会退出switch,会无视case 继续执行下一个case内的语句
5.switch和if else if语句的区别
一般情况下二者可以相互替换
switch语句一般处理case为比较确定的值的情况,ifelse语句更加灵活,可以判定范围
switch语句进行条件判断后直接执行到程序的条件语句,效率更高而ifelse有几种条件就得判断多少次
分支比较少的时候,ifelse比switch执行效率高
分支比较多时,switch语句执行效率比较高
2.循环
循环执行某些固定的代码
一组被重复执行的语句称之为循环体,能否继续重复,取决于终止条件,循环体及终止条件组成的语句称之为循环语句
1.for循环
结构:
for(初始化变量;条件表达式;操作表达式){
//循环体
}
初始化变量:就是用let声明一个普通变量,一般用于作为计数器使用
条件表达式:就是决定每次循环是否继续执行,就是终止的条件
操作表达式:每次循环最后执行的代码,一般用于计数器变量的更新
循环过程:
首先执行初始化变量,只执行一次
查看时候满足条件表达式,满足则进入循环
执行循环体
执行操作表达式,返回条件表达式查看是否满足
断点调试:
点击检查元素内sources,选择代码,在你要断点的哪一行点击左边行号,刷新网页
蓝色背景所在的位置就是现在执行的地方,点击右边下一步可以继续向下操作一次
watch内可以观察你想要查看变化的变量,可以再次点击左边行号取消
让for循环执行不同代码:
因为计数器变量每次循环都会变化,可以执行以计数器变量为核心的语句达到每次输出不同的效果
让for循环执行相同的操作:
例如求1-100的所有数的总和
在计数器i的基础上
设定一个储存结果的sum变量,初始值为0
令每次循环 sum = sum + i(sum +=i)
一直循环到i=101结束循环
双重for循环:
在一个for循环内再定义一个for循环
for(外层的初始化变量;外层的条件表达式;外层的操作表达式){
for(里层的初始化变量;里层的条件表达式;里层的操作表达式{
执行语句
}
}
外层循环执行一次,内层循环执行全部
2.while循环
结构:
while(条件表达式){
循环体
}
当条件表达式为true,则执行循环体,循环体内注意加上计数器的更新
while循环可以支持比较复杂的循环,不仅仅只支持计数器
3.do while循环
结构:
do{
循环体
} while(条件表达式)
先执行一次循环体,再判断条件,满足条件则继续执行
4.continue 和 break关键字
continue:立即退出本次循环,进入下一次循环
break:直接退出整个循环
6.js代码命名规范
1.标志符命名规范
变量尽量用名词,函数尽量用动词,要有意义
2.操作符规范
两侧各保留一个空格
3.单行注释后加一个空格
2和3可以通过shift+alt+f配合pretteir-code formater插件食用
7.数组
数组就是一组数据的集合,就是将一组数据储存在单个变量名下,可以存放任意类型的元素
1.创建数组和存储变量
1.利用new关键字创建数组:
let 数组名 = new Array(数组长度)
let 数组名 = new Array(数组元素1,数组元素2,……)
括号内可以不填创建空数组,注意A要大写!!!!!
2.利用数组字面量创建数组
let 数组名 = [数组元素1,数组元素2 ]
数组内数据一定要用逗号分隔
2.获取数组中的元素
1.数组的索引
索引:用来访问数组元素的序号(从0开始)
例如 let arr = [1(索引号0),2(索引号1),3,4]
数组可以通过索引来访问(获取),设置和修改对应数组元素
方式是:数组名[索引号]
2.遍历数组
就是将数组内的元素全部取出来,可以通过循环的方式
遍历就是把数组中的元素从头到尾访问一次(类比点名)
可以通过:
for(let i = 0;i<数组元素数(arr.length);i++)
{
需要输出的地方 arr[i]
}
计数器当做索引号使用
3.数组长度
就是在数组名后加上.length,是动态变化的,可以随元素个数变化而变化
例如:arr.length
4.数组新增元素
1.通过修改length长度新增元素
length是一个可读写的变量,可修改
例如:arr.length = 5
新增出来的元素为undefined
2.通过修改数组索引号新增/替换元素
例如:arr[没有被使用的索引] = 元素值
如果是被使用的索引,则替换
注意不要直接给数组名赋值,数组元素会全部被替换
8.函数
JS中可能会定义很多功能相同的代码,函数就是封装了一段可以重复执行的代码块,只要调用就可以实现所封装的效果,让大量代码重复使用
1.函数的使用
函数使用要先声明后调用
声明函数:
function 函数名(形参1,形参2,……){
函数体
}
函数名一般使用动词,函数不调用自己不执行
调用函数:
函数名(实参1,实参2,……)
调用函数一定要加()
函数内可以调用另外一个函数
2.函数的参数
分为形参和实参
形参就是写在封装函数上的参数,就是形式上的参数,没有实质作用
实参就是调用函数时给的参数,就是实际上的参数,参与代码运算
形参和实参如果个数不匹配:
实参多余形参:多出来的实参直接不参与运算
实参少于形参:缺少的形参可以看做是不用声明的变量,也就是空值(undefined),结果为NaN
3.函数的返回值return
函数内部不应该有输出语句,应该返回给调用者
在函数体内部结尾添加:
return 需要返回的结果
就会将结果返回给调用函数体(函数名())
只要函数遇到return 就把结果返回给调用者
在实际开发中,一般用变量来接受函数返回结果
注意return会直接终止函数,并且只能返回一个值,返回的结果是最后一个值(可以通过输出数组来迂回输出)
如果函数没有返回值,则返回undefined
4.arguments对象
当不能确定传递的实参有多少个时,arguments对象中储存了所有传递的实参
arguments是一种伪数组,没有真正数组的的一些方法,是按照索引的方式储存的,具有length属性
可以通过数组遍历的方式使用
5.函数表达式声明函数
格式:
let 变量名 = function(){
// 函数表达式
}
调用:变量名()
注意 调用使用的是变量名不是函数名,故称为匿名函数
6.立即执行函数
格式:(function(形参){})(实参);
也可以写成:
(function(形参){}(实参))
使用这个函数会立即执行,主要用于限制内部变量范围,防止污染全局变量,必须加分号
9.作用域
就是代码名字(一般是变量)内在某个范围内起作用和效果,是为了提高程序的可靠性,更重要的是减少命名冲突
1.作用域的分类(es6之前)
全局作用域:在整个script标签内,或者是一个单独的js文件内
局部(函数)作用域:在函数内部就是局部作用域,代码的名字只在函数内起效果
在不同作用域下,函数命名相同不会造成冲突
同理,变量可以分为全局变量和局部变量
全局变量,在全局下声明或者在函数内直接赋值,没有声明(let)的变量,在全局下都可以使用
局部变量,在局部作用域下的变量,函数的形参也可以看做是局部变量
全局变量只有在浏览器关闭时才会销毁,占内存资源
局部变量在程序执行完毕时就销毁,节约内存资源
2.作用域链
只要是代码就有作用域,函数内是一个局部作用域,如果函数内生成一个新的函数就又诞生一个心的作用域,根据内部函数可以访问外部函数变量的机制,用链式查找(就是一层一层往外找,找最近的一层,就近原则,往前看不往后看)决定那些数据内被内部函数访问,就称为作用域链
10.预解析
JS引擎会把JS里面所有的let和function提升到当前作用域的最前面,再按照代码书写的顺序从上往下执行
预解析分为:变量预解析(变量提升)和函数预解析(函数提升)
变量预解析只提升变量声明,不提升赋值
函数预解析就是把所有函数声明提升到当前作用域的最前面,不执行函数
11.对象
对象是一个具体的事物(苹果不是对象,这个苹果是对象),一个数据库,一个网页,一个服务器也是对象
JS中,对象是一组无序的相关属性和方法的集合,例如:字符串,数值,数组,函数
属性就是事物的特征,常用名词
变量和属性的关系:都是用来存储数据,变量单独声明并赋值,使用的时候直接写变量名;属性在对象里,使用的时候必须是对象.属性
方法就是事务的行为,常用动词
函数和方法的关系:都是实现某种功能,函数单独声明,调用也单独存在,方法在对象内,调用的时候对象.方法
1.创建对象
1.用字面量创建对象
let 对象名 = {
属性名1:值,
属性名2:值,
属性名3:值,
方法名:function(){
方法
}
}
多个属性或者方法之间用逗号隔开,方法冒号后跟的是一个匿名函数
对象属性使用:对象名.属性名,或者对象名[‘属性名’]
对象方法使用:对象名.方法名(不要忘记小括号)
对象删除:delete 对象名.属性名
对象增加/修改:对象名.属性名 = 属性值
2.利用new object创建对象
let 对象名 = new Object();
对象名.属性名 = 属性值;
对象名.方法名 = function(){
方法
};
3.用构造函数创建对象
利用之前的方法一次只能构造一个对象,如果用函数的方法就可以快速重复构造对象,构造函数就是吧对象里的一些相同的属性和方法抽象出来封装到函数里
构造函数的格式:
function 构造函数名 (){
this.属性名 = 值
this.方法 = function()
}
使用构造函数:
let 对象名 = new 构造函数名()
构造函数不需要return就可以返回结果
调用构造函数必须使用new
构造函数泛指某一大类,对象特指某一个,用构造函数创建对象的过程称为对象实例化
4.遍历对象
使用for...in遍历对象
for (let k in 对象)
console.log(k) //输出的是属性名
console.log(对象名[k]) //输出的是属性值
注意k没有单引号
2.内置对象
内置对象就是JS语言自带的一些对象,提供了一些常用或最基本的功能
1.查阅MDN文档
链接地址:https://developer.mozilla.org/zh-CN/
先查阅该方法的功能,再查阅方法和类型
2.Math对象
Math对象不是一个构造函数,不需要new来调用,可以直接使用里面的属性和方法(注意M要大写)
Math.abs(数值) – 取绝对值,可以隐式转换,但是如果是无法转换的数值输出NaN
三种取整方法:
Math.floor(数值) – 向下取整, 1.9也取1
Math.ceil(数值) – 向上取整,1.1也取2
Math.round(数值) – 四舍五入,但是.5的情况往大了取,-1.5取-1
随机数方法:
Math.random() – 返回一个随机的0-1之间的小数(包括0)
取0-n之间的随机整数:
math.floor(math.random()*(n+1))
取两个数之间的随机整数:
math.floor(math.random()*(m-n+1))+n
3.日期对象
let 日期名 = new date()
date如果没有任何参数,则返回当前系统时间
参数常用写法:
数字型: 2023,2,12
字符串型:‘2023-2-12 18:57:20’ //经常使用这种
日期格式化:
日期名.getFullYear() -- 返回本地时间给的年份
日期名.getMonth() -- 返回本地时间给的月份(0月-11月 注意加一)
日期名.getDate() -- 返回本地时间给的日期
日期名.getDay() -- 返回本地时间的星期(周日是星期0)
日期名.getHours() -- 返回本地时间给的时
日期名.getMinutes() -- 返回本地时间给的分
日期名.getSeconds() -- 返回本地时间给的秒
注意要创建了日期对象才可以调用日期格式化
获取date距离1970年1月1日过了多少毫秒(时间戳):
日期名.valueOf()或者日期名.getTime()
也可以:let 日期名 = +new Date()
还有h5新增(兼容差)的方式:let 日期名 = Date.now()
将时间戳转化为正常时间:
let d = parselnt(times/ 60/60/24); // 计算天数
let h= parselnt(总秒数/ 60/60%24) //计算小时
let m= parselnt(总秒数/60 %60); // 计算分数
let s = parselnt(总秒数%60);//计算当前秒数
4.数组对象
检测是否为数组:
参数 instanceof Array:运算符,可以检测是否为数组
Array.isArray(参数):h5新增,ie9以上才支持
添加/删除数组元素:
数组名.push(元素1,元素2):在数组的末尾添加一个或多个数组元素
push完毕之后返回的结果是新数组的长度,原数组也会发生变化
数组名.unshift(元素1,元素2):在数组的开头添加一个或多个数组元素
unshift完毕之后返回的结果也是新数组的长度,原数组也会发生变化
数组名.pop():删除数组的最后一个元素
pop的返回值是删除的元素
数组名.shift():删除数组的第一个元素
shift的返回值也是删除的元素
翻转数组:
数组名.reverse()
冒泡排序:
数组名.sort(function(a,b){
return a - b
} – 从小到大 b-a就是从大到小
数组索引:
数组名.indexof(元素值,[起始的位置]) – 显示该元素在数组中的索引,从前往后
有重复只返回第一个,没有该元素返回-1
数组名.lastindexof(元素值) – 显示该元素在数组中的索引,从后往前查找,索引号不变
有重复只返回最后一个,没有该元素返回-1
数组转化为字符串:
数组名.toString() – 转化为以,隔开的字符型
数组名.join(分隔符,默认是,)-- 转化为以选定分隔符隔开的字符型
合并数组:
数组名1.concat(数组名2); – 在数组名1后连接数组名2
截取数组:
数组名.slice(开始索引, 结束索引) – 包含开始不包含结束,不修改原数组
修改数组:
数组名.splice(开始索引, 移除的元素的个数, 要添加的元素1, 要添加的元素2,……)
从开始索引开始,移除选定的个数(没写全删),再从开始索引后添加要添加的元素,修改原数组
5.字符串对象
字符串里面的值不会变,字符串的重新赋值只是变地址,曾经的字符串仍然在内存中,因此尽量避免大量赋值,拼接字符串
返回字符位置:
字符串名.indexOf(字符,[起始的位置])-- 从选定位置开始查找返回要查找的字符所在的位置,从前往后,只找第一个匹配的
根据位置返回字符:
字符名.charAt(索引号) – 根据索引号位置打印字符
字符名.charCodeAt(索引号) – 根据索引号位置打印字符的ASCII码(判断用户按了什么键)
字符名[索引号] – h5新增的,可能会有兼容问题
合并字符串:
字符名.concat(字符串1,字符串2)-- 等效于+,+更常用
截取字符串:-- 包括开始点
字符名.substr(截取的起始位置,截取几个字符)-- 包括开始点
替换字符串:–只替换第一个
字符名.replace(被替换的字符,替换为的字符)–只替换第一个,不改变字符串
将字符转换为数组:
字符名.split(分隔符)-- 将被选定分隔符分隔的元素转化为数组
将字符转换为大小写:
字符名.toUpperCase() – 全部字符转换为大写
字符名.toLowerCase() – 全部字符转换为小写
12.JS中的数据类型
1.简单数据类型和复杂数据类型
简单数据类型在变量存储是储存的是值,又称为值类型,例如:
string , number , boolean , undefined , null(返回的是对象类型)
复杂数据类型在存储时变量中存储的是地址(引用) , 因此又称为引用数据类型,例如:
通过new关键字创建的对象(系统对象、自定义对象) , 如Object. Array. Date等
系统会把内存分为两类:
堆:储存复杂数据类型,一般由程序员分配释放 ,若程序员不释放,由垃圾回收机制回收
栈:储存简单数据类型,由操作系统自动分配释放存放函数的参数值、局部变量的值等
2.两种类型的内存分配
简单数据类型直接存放到栈里,存放的是值
复杂数据类型存放在堆里,在栈中储存指向该数据的地址(16进制)
简单数据类型的传参:
函数的形参也可以看做是一个变量,当我们把一个值类型变量作为参数传给函数的形参时 ,其实是把变量在栈空间里的值复制了一份给形参,那么在方法内部对形参做任何修改,都不会影响到的外部变量。
复杂数据类型的传参:
函数的形参也可以看做是-个变量,当我们把弓引|用类型变量传给形参时,其实是把变量在栈空间里保存的堆地址复制给了形参,形参和实参其实保存的是同一个堆地址,所以操作的是同一个对象。
简单来说,简单数据类型在函数内对形参数值进行操作不影响外部变量,复杂数据类型对形参操作会对外部变量造成影响