从小白开始的编程体验(3)——变量的诞生

154 阅读10分钟

1. 做点有意义的事

前面两篇文章仅是写出了hello world,完成了我们作为开发者的初体验,控制浏览器。

现在我对那个简陋的页面做了一些改动,就像下面这样:

<script>
    let str = "hello 变量"
    console.log(str)
</script>

将script标签中的内容修改为上面的代码,保存文件,然后刷新你的浏览器,你将会看到控制台输出了我们想要的东西:

image.png

我们将一行代码拆成了两行,在第一行定义了一个变量,并在第二行打印了这个变量。

对于一个程序员来说,定义一个变量来存储一些内容可能已经成为本能一样的事情了,就像喝水一样简单,这没什么可以讲的,无非是写一个关键字,写上变量名,再写一个等号,给一个初始值而已。

是的,不光是程序员这样想,一些开发指南上也是这样写的:

变量类型 变量名;
变量类型 变量名 = 变量值;

所有的语法书上大抵都是这样的吧,根据语言的不同有些差异。

根据语言的特点,在定义变量的时候大概分为两种流派:一种是类型严格派,另一种是类型佛系派。

首先看类型严格派的作风,该门派的代表有Java、C++等著名人物。他们作风严谨,表里如一,要求门下弟子在声明变量是必须指定变量类型,且声明之后不得更改,如要强制更改必须提出书面的申请,且自行承担后果。

例如Java的变量定义:

int a = 1;
final int b = 2;
String str;
str = "hello"

然后轮到类型佛系派,该门派的成员普遍都是年轻人,看上去十分平和佛系,实际上拥有极强的自我管理意识,凭借这种严于律己宽以待人的作风在江湖上占据了一席之地,其中以JavaScript为代表,其门下的kotlin、python更是后起之秀。该门派奉行少写一点是一点的摸鱼思想,不要求显示声明变量类型,而是在运行时进行类型推断,为江湖注入了新鲜血液。

例如JS的变量定义:

var a,b;
a = 1;
b = "2";
val c = {a:1,b:2};
let d = true;
const e = "123"

头疼吗?这么多的定义方式,花花绿绿的。

没关系,我们来总结一下,慢慢梳理。首先我们来了解一下定义变量的本质。

**2. 够硬吗 **

当你要买一台电脑的时候,你会关注什么参数呢?

卖电脑的小哥通常会向你介绍XX电脑拥有16G的大内存,可以充分满足你的娱乐需求。为什么要强调内存的容量呢?

当你拆开一台电脑的时候,除了顶着一个小风扇或散热器的CPU,还有两个最显眼的物件,一个是硬盘,另一个就是内存条了。硬盘现在有两种,一种是盒子样的机械硬盘,里面是一个盘片,马达带动盘片飞速转动,磁头在盘片上寻找磁道读取数据,就像老式的唱片机。

image.png

Tips:如果你的硬盘属于机械硬盘,那么在使用过程中最好避免震动,以免磁头碰撞盘片,造成磁盘坏道。

另一种硬盘是固态硬盘,看上去就像一个内存条,无需磁头上下翻飞寻找磁道,因此读写速度优秀,价格也很优秀,容量越大,价格越贵。

无论是那种硬盘,都是数据的永久载体,断电重启之后数据仍然还在。内存就不一样了,内存的读写速度更快,但关机断电之后将被清空。

我们电脑上的软件被安装在硬盘中,但当我们运行这个软件的时候,这个软件将会被加载到内存中,那么为什么要多这一步?直接从硬盘运行不好吗?当然可以,但是会变得非常慢,你昂贵的CPU将无法发挥它应有的价值。它们之间的关系就像一个工厂一样,CPU是高效的机器,每秒可以进行千亿次的计算,但供应商的原料却跟不上,硬盘的数据传输速度跟CPU的速度相比太慢了,后来人们发明了内存,传输速度更快,但价格也更高,硬盘价格便宜,速度慢,为了实现性能与经济的平衡,最终形成了CPU-内存-硬盘的结构。

既然已经扯远了,不如扯得更远一点。实际上计算机真正的结构要比我描述的更加复杂,CPU的三级缓存等等。鉴于CPU的高性能,我们在性能监视器中会发现CPU的占用率都是很低的,只有内存会一直辛苦的工作,而硬盘则是弹性工作,忙的时候真忙,闲的时候也是真的闲。所以,如果有一天你发现你的CPU利用率逼近100%,不用怀疑,你中毒了。

image.png

3. 八一八那个你没正眼看过的东西

电脑嘛,当然是要插电的。电脑实际上只是一个机器而已,要想让一个机器能进行计算,首先得让它识数。

我们平时用到的数字都是十进制的,但是电脑不认识,怎么办呢,怎么把数字表达给一个机器呢?二进制,一种用0和1表示数字的方法,因为二进制只有两种状态,不是0就是1,电流也可以用通和断来表示,这样计算机不就识数了吗。

但是还不行啊,计算机认识了数字,可我们人类要用0和1来使用电脑也太难了吧,这个时候怎么办呢,有办法,CPU中自带一套指令集!我们平时耳熟能详的x86、x64,都是指令集的名称。

在计算机中,指示计算机硬件执行某种运算、处理功能的命令称为指令。指令是计算机运行的最小的功能单位,而硬件的作用是完成每条指令规定的功能。一台计算机上全部指令的集合,就是这台计算机的指令系统。指令系统也称指令集,是这台计算机全部功能的体现。(来源于百度百科)

可是呢,指令集又多又难学,仅仅打开一个文件就要有一大堆的指令,还是太难了。这个时候就诞生了操作系统,将指令集包装起来,从命令行模式的Dos系统,到如今的桌面化操作系统windows,它们的主要工作一直未曾改变,就是作为人机之间的桥梁、翻译者、硬件调度者。

当你每天盯着电脑桌面发呆,在文件夹中肆意张扬的时候,那个被你视而不见的操作系统,正在默默地埋头苦干,emmm,这个勤勤恳恳的员工有时候也会死机给你看呢。

image.png

4. “源”程序

无论是你安装的浏览器还是网抑云,它们都是运行在操作系统上的应用,包括我们编写出的代码。

但是我们的代码直接与操作系统交互了吗?没有,还差一层。

我们开发是使用的语言通常是高级语言,相对于低级语言,它具有更好的语义化,符合人类的思维习惯,用这种语言写出来的代码,我们称之为“源代码”,源代码需要经过编译器翻译成操作系统能够认识并执行的文件,也就是可执行文件。

但是我写出来的helloworld没有编译过程啊,你怕不是在糊弄我胖虎?

因为我们写出来的helloworld是Javascript,是一种解释型语言,而不是编译型语言,就像电视里的同声传译,实时的,而不是等人家都说完了之后再翻译。

比如Java就是编译型语言,先写出.java后缀名的源文件,然后使用javac编译为.class文件,.class是一种平台无关的可执行文件,由JVM虚拟机进行执行,虚拟机根据操作系统的不同有多个版本,将.class文件翻译成操作系统的命令。

而Javascript由浏览器内核进行内置式的编译,速度极快,所以我们写出来的helloworld不是没有编译,而是浏览器内核在负重前行。

5. 金字塔的塔顶

从二进制,到源代码,我们终于可以探讨我们的代码了!

let str = "hello 变量"

从前面的杂七杂八中我们已经知道了程序需要运行在内存中,所以我们定义一个变量的时候,自然也是在内存中做文章。

抛开编译的代码优化、词法分析和语法分析不谈,我们创建一个变量,总共需要两步:第一步声明变量,第二步给变量赋值。

当我们执行上面的一行代码时,编译器会将它拆分成两行:

let str;
str = "hello 变量"

第一行是变量声明,告诉编译器,这里有一个新的变量了,是时候申请一个内存空间了。编译器与操作系统沟通了一下,操作系统查看了一下内存,然后交给编译器一个内存地址,编译器拿到内存地址后找了一个文件夹把它放了进去,贴上写着str的标签,并放入了自己的栈区

第二行为变量str进行了赋值,编译器“看到”这里有一个=符号,这个符号表示这是一个赋值的操作,这个操作是从右向左的顺序,所以编译器先计算了等号右边的内容,通常是一个表达式。编译器发现这是一个字符串,是一个字面量,不需要进行更多的计算操作了,已经是一个最终的值了,此时编译器将这个字符串放入了自己的堆区,并记录下存放的位置,然后编译器继续查看等号左边的内容,这通常是一个变量名,str,编译器得到了一个名字,并在自己的栈区中寻找。找到了!编译器把它拿出来,放进去一张写着堆区地址的纸条。

现在我们已经有了一个变量了!使用这个变量的时候又经历了什么呢?

console.log(str)

这是一条语句,编译器如是认为。有个括号诶!那么你想要调用某个方法咯,emmm,括号里还有东西,还要传个参数进去。

此时编译器已经得知了你的“意图”,调用一个方法,编译器首先查看了要调用的是哪个方法?console.log,是要找到console对象中名为的log方法,console是浏览器的内置对象,编译器向内核询问了console.log的情况,内核告知编译器,确实有这么一个方法,你拿去用吧!编译器拿到方法之后,开始计算需要传递的参数,str,看起来是一个变量,编译器在栈区翻找,找到了一个名为str的文件夹,得到了一张写着内存地址的纸条,按照纸条上的地址找到了一个值并传递给了console.log方法。此时方法已经准备完毕,编译器通知内核可以执行这个方法了,于是我们在控制台看到了一行输出。

6. 结束了吗

“我不想知道我是怎么来的,我就想知道我的怎么没的”

变量是怎么没的呢?