本文已参与掘金创作者训练营第三期「高产更文」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。
配套视频教程地址
哔哩哔哩:JS基本类型和引用类型差异性的本质-js小知识点第2期
前置知识
弱类型与强类型
js本身是弱类型语言。这里的弱,有弱化、淡化的含义。不管使用var还是let声明变量,都没有给变量指定任何类型。
跟java的对比
java语言声明变量的时候需要指定变量的类型,所以java里面的变量,在声明的时候就规定好了里面必须保存指定的类型,如果赋值其他类型的数据,就会报错。
public class Test {
public static void main(String[] args) {
int a = 1;
a = 'abc'; // 这里的操作会引起报错,不能更改变量的数据类型
System.out.println(a);
}
}
// 更多声明变量的语句
String str = "Hello"; // 声明并初始化变量str为字符串类型
double pi = 3.1415926; // 声明并初始化变量pi为双精度浮点类型
char c = 'c'; // 声明并初始化变量c的值为'c'字符类型
如上面代码所示,在声明变量a的时候就指定了变量a的类型为整型,如果将字符串赋值给整型的变量a,会引起代码报错的。
另外下面还列出了三个java里面的变量类型。在java里面可以使用int、String、double、char等这些关键字类声明变量,所以java里面变量是有类型之分。所以java是强类型语言。
静态类型与动态类型
java里面的变量类型轻易不能改变,相对来说比较安静,所以属于静态类型语言。
而在js中,声明变量的时候根本就不指定类型,且没有改变变量里保存数据类型的限制,随时都可以改变,所以js属于动态类型语言。
代码编写上的差异性
由于强弱或动静语言类型的不同,也带来了编码上的巨大差异。
由于静态类型语言对变量类型要求更加严格,所以开发人员在语法层面需要花费的精力就要多一些,好处是在开发阶段就可以避免很多由于数据类型所带来的问题。由于变量的类型固定,使得开发工具(IDE)在代码的编写阶段,就可以给出很好的智能代码提示和检测代码中的变量类型错误。从而使得代码较为规范和安全。
相对而言,动态类型语言在代码的编写阶段可以把更多精力放在业务逻辑上,由于类型不固定,所以编写代码更加灵活,但是开发工具(IDE)也无法去更好的检测代码,由此不可避免的会隐藏一些没有注意到的的跟变量类型相关的问题,可能只能通过测试才能发现,所以在代码规范上是不如静态类型语言的。
变量类型和数据类型
变量类型
js里面的变量本身是没有类型之分的。我们经常所说的变量类型实际指的是变量里面保存的值的类型。
原始类型和引用类型
既然变量在js中是没有类型的,而变量的类型取决于里面保存的数据的类型,所以从变量中到底保存的是数据本身,还是保存的只是一个数据在堆内存中的地址引用这点考虑,总体上可以分为以下两种类型:
- 原始类型(primitive value)
- 保存数据本身,数据就就保存在栈内存
- 比如数值、字符串、布尔类型
- 引用类型(reference value)
- 数据本身保存在堆内存,栈内存只是数据的地址
- 比如函数、数组,原生对象等对象类型
何为栈和堆
我们所需要知道的内存的两大部分,一部分叫做栈内存,一部分叫做堆内存,下面来看二者的区别
- 栈内存
- 是系统预先设置好连续的内存区域,因而空间较小,但读取速度较快
- 堆内存
- 是不连续的内存区域,获取空间比较灵活,所以空间较大,但读取速度较慢
由于栈和堆地址空间以上特性,所以一般用栈内存存放占用空间较小的原始类型的数据,而用堆内存存放占用空间较大的引用类型的数据,只将引用类型数据在
堆内存的地址保存在栈内存当中。
- 是不连续的内存区域,获取空间比较灵活,所以空间较大,但读取速度较慢
由于栈和堆地址空间以上特性,所以一般用栈内存存放占用空间较小的原始类型的数据,而用堆内存存放占用空间较大的引用类型的数据,只将引用类型数据在
数据类型
- 基本类型
- 比如数值、字符串、布尔类型
- 对象类型
- 比如函数、数组,原生对象等对象类型
从数据的角度去分类,分为基本类型和对象类型就比较容易理解了,只需要调用typeof函数去看返回类型就可以加一区分,要么是
object类型,要么是基本类型的number、string、boolean等。
// 基本类型
cosole.log(typeof 1)
// "number"
cosole.log(typeof 'a')
// "string"
cosole.log(typeof true)
// "boolean"
// 对象类型
cosole.log(typeof [])
// "object"
cosole.log(typeof {})
// "object"
差异性
搞清楚了两种类型数据或变量的保存形式,下面通过代码看看他们具体的差异性。这里要强调一点:对于将一个变量赋值给另一个变量的情形,都是将一个变量的内容,拷贝一份赋值另一个变量,关注点在于:变量的内容是实际的数据,还是实际数据的地址。
基本类型的数据传递
var n1 = 1;
var n2 = n1;
n1 = 2;
console.log(n1);
console.log(n2);
结果:
> 2
> 1
这个比较好理解,就是把n1中的真实的数据1拷贝一份赋值给n2,传递数据之后,二者就没有任何关系了,所以n2的改变跟n1毫无关系,n2的结果自然就保持不变了。
对象类型的数据传递
var obj1 = {
name: 'xiaoming'
}
var obj2 = obj1;
obj1.name = 'xiaohua';
console.log(obj1.name);
console.log(obj2.name);
结果:
> xiaohua
> xiaohua
对于对象类型,这里传递的就是obj1中对象在堆内存中的地址了,赋值给obj2后,二者共同指向同一对象的堆内存地址,通过对象.属性的形式,就是首先通过堆内存地址找到对象真实的数据,再找到name属性的值,可以看出改变的是同一个对象真实数据,所以会影响另一个。
给obj1重新赋值呢
var obj1 = {
name: 'xiaoming'
}
var obj2 = obj1;
obj1 = {
name: 'xiaohua'
}
console.log(obj1.name);
console.log(obj2.name);
结果:
> xiaohua
> xiaoming
现在的结果,又是为什么呢?这里要注意obj1是被重新被赋值了,而不是通过obj1.name的形式去查找之前的地址,这里的重新赋值,会使得系统重新开辟一块内存空间存放obj1,这样obj1和obj2就不再有任何联系了,结果自然就如上所示了。
再看一个比较隐蔽点的
var obj = {
name: 'xiaoming'
}
function fn(obj){
obj.name = 'xiaohua'
}
fn(obj);
console.log(obj.name);
结果:
> xiaohua
这又是为何呢?为什么函数内部的局部变量竟然能影响到函数外部的全局变量呢?这里我们知道函数fn调用时候确实是将全局变量obj通过参数的形式,传递给了函数,函数同时也通过参数obj接收到了。
但是要注意的是,在函数接收参数的时候实际上是有一个赋值的操作的:局部的形式参数变量obj=全局的实际参数变量obj,只不过两个变量同名,所以这一个赋值过程很容易就被忽略了。
因为这里同样也是拷贝全局变量里面保存的数据数据赋值给函数内部的局部变量obj,但是由于是对象类型的数据,所以这拷贝的是对象在堆内存的地址,所以函数内部的obj通过点获取属性name还是同一个对象的name属性。
如果换成基本类型的数据呢
var a = 'a';
function fn(a) {
a = 'aa';
}
fn(a);
console.log(a);
结果:
> a
这里只需要注意一点不同,拷贝的是真实的数据赋值给了局部变量a。正是这个赋值的过程才将内部的局部变量和外部的全局变量分开了,接下来,局部变量在函数内部的任何操作都不会影响外部的全局变量a了。
基本类型和对象类型值的比较
var a = 'a';
var b = 'a';
var obj1 = {
name: 'xiaoming'
}
var obj2 = {
name: 'xiaoming'
}
console.log(a == b);
console.log(obj1 == obj2);
结果:
> true
> false
这里只需要注意,由于基本类型的数据保存的就是值本身,所以两个变量只有保存同一个值,自然就相等。
而对象数据保存在变量里面的是对象在堆内存的地址,所以obj1和obj2保存的对象的内容虽然相同。但是由于二者是独立的对象,比如保存在不同的内存地址里面,对比自然就不会相等了。
结束语
好了,这就是这次给大家分享的由于不同类型数据保存形式的不同所带来的代码上的差异性。这些也都是平时开发中容易忽视的点,但是一旦遇到问题的时候就发现还是真的得自其所以然才可以游刃有余,灵活应对各种场景。我觉得,这些差异性也只有在真正的实践中才能领悟其中之真谛啊!