每个系列一本前端好书,帮你轻松学重点。
本系列来自曾供职于Google的知名前端技术专家马特·弗里斯比编写的 《JavaScript高级程序设计》(第5版)
本文开始,我们将由浅入深介绍程序是怎么从零慢慢构建起来的。
虽然是以JavaScript这门语言描述,但其他语言也类似,且不论是小程序还是大项目,10行代码还是10000行,都是如此。
数据如何存在
代码中的数据并不神秘,现实怎么存在,它就怎么存在,只不过要用代码进行定义。
比如:小明,8岁
用代码表示:
1 | const name = "小明"; |
有印象的朋友应该记得,这叫”变量“。
程序中的一切都离不开变量,那么变量存在的意义是什么。
举个例子,假设需要执行一个加法:
1 + 1 永远等于2,那么a+b呢?
就不知道了,因为相加的不是数字而是标识符,计算结果取决于a、b代表几,这里的“代表”就是一个会变化的东西,故称为变量。
因为变量的存在,一个算式不会固定地返回一个结果,而是随着情况的变化返回多种不同的结果,这就是它存在的意义。
定义与作用域
既然一切离不开变量,就要在需要的地方定义它,为了便于识别,JavaScript提供了三个关键字:var、const、let
1 | var name = "小明"; |
以上三种方式均是可行的,但使用时有区别,主要体现在:
1、作用域
2、是否可修改
作用域
顾名思义,能够起作用的区域,在一定范围内,能访问,能修改,否则会报错。
为什么需要这个范围?
想象一下,我们的代码可能分布在很多文件中,文件内又会有多个代码块,如果变量的作用范围不设限,哪里都能访问和修改,岂不乱了套,管理起来就是灾难。
先说var,这是ES6之前唯一可用的关键字,它具备函数作用域的特性,具体如下:
1 | function test(){ |
上面这段代码,定义了一个test函数,其中定义了message变量,通过在不同的时机打印变量值可以看到函数作用域的特点,在函数中定义,就只在函数中可用。
第一行的执行结果是undefined,undefined的意思是,变量存在,但没有值,这也是var的特性 — ”声明提升“,即允许在赋值之前访问,不报错,不影响程序执行,只是没有值。
在ES6之前,只有两种作用域:函数作用域、全局作用域。要么只在函数中可用,要么整个文档都可用。
下面这段代码,看起来变量name被一对花括号封闭在代码块中,但实际上无法限制它。
1 | if(true){ |
所以,在 var 独自为王的时代,变量所带来的隐患较多。
ES6之后,就有了块级作用域,也就是使用let或者const来定义。
1 | if(true){ |
这段代码中,展示了let和var的两点区别:
1、代码块内定义的变量,外部无法访问
2、即便在代码块内,定义之前也无法访问,这种现象叫“暂时性死区“,与var的”变量提升“相对应
既然let能够提供块级作用域,还需要const干什么呢,和let有什么不同?
const 是 "constant"(常量) 的缩写,用于声明一个不可重新赋值的变量。不可重新赋值,意味着两点:
1、定义的同时必须赋值
2、后续不允许修改
那什么情况适合使用常量呢?全局共用,且不允许业务代码对其进行改动的变量,如:版本号、公共访问地址等。
1 | // 未初始化 |
好,三种变量定义方式介绍完,更推荐怎样做呢?
三条原则:
- 非必要不用var,会修改的变量用let,其他都用const,特别对于公共常量,推荐”AA_BB”式全大写格式;
尽量少定义全局变量,目的是减少不必要的冲突和修改;
还有一个好处是,局部变量用完后大概率被销毁,能释放内存,全局变量长期占内存;
- 不定义不必要的变量,多一个变量就多一份理解和维护成本。
数据类型
JavaScript中的变量被称为”松散类型“,松散意为对类型没限制。
定义时不需声明类型,定义后允许被改为另一种类型。比如:
1 | let message = "hi" |
定义时是字符串,后被改为数字,在语法层面是允许的。
但正因如此,导致代码运行的不确定性大大增加,就有了现在所流行的TypeScript。
虽说JavaScript定义变量时不需要声明类型,但它仍然具备类型。
JavaScript有七种简单数据类型和一种复杂数据类型。
简单类型:String、Number、Boolean、Undefined、Null、BigInt、Symbol
复杂类型:Object
所有数据,都是用以上类型来表达的,但怎么理解简单和复杂?
简单,可理解为直给,而复杂,可以包含任意简单或复杂类型。比如:
1 | // 简单类型 |
以上这段代码,包含了上篇文提到的变量name,对象person和数组tools,展现了各自在定义上的特点。
专属特性
既然有这么多类型,只是表示上的区别吗,肯定不是,它们各有各的特性,这是理所当然,也是应用需要。
比如:
1、数字能用来做数学运算、比较大小,其他类型不能
1 | let a = 2, b = 3; |
2、字符串可以做拼接、裁剪等,其他大部分类型不能
1 | let a = 'ab', b = 'cd'; |
3、布尔值多用于条件逻辑中的“真”、“假”判断,其他类型不能
1 | if(true){ |
你可能会说,还有这样的代码呢?
1 | if(a > b){ |
对,这样的代码,只是看起来没有直接使用布尔值,但 a > b 的结果仍是个布尔值,要么是true,要么是false。
而a本身具备一个合理可用的值,那么它也会被转换为布尔值true。
特性来源
你应该注意到了,在数字类型中,用到了Math,字符串类型中用到了slice,它们怎么来的?
其实,就算是最基本的类型,有时候也被赋予一种类型叫“包装类型”,包装类型可以创建一个对象,每种对象包含着若干属性和方法,就像字符串,当它调用slice方法时,实际执行的是:
1 | 'abcd'.slice(1,3) // 代码 |
于是它就有了一堆方法可用,如:slice、substring、concat、trim等等。同理,布尔和数字类型在调用它们的方法时,执行的是 new Boolean() 和 new Number(),需要注意的是,这种创建没有可持续性,用完即毁,想接着再把它当对象用就不行了。
Math就更强大了,它是“内置对象”,顾名思义,不需要开发者创建,直接可用。
当我们写的代码开始执行时,全局上下文中会存在两个内置对象:Global 和 Math。
你可能想说,Global是什么,没见过呀,Global意思是全局对象,它在浏览器中的实现就是window对象,这个大家就熟悉了,所有的全局变量和函数,都归属于它;而Math包含很多数学方法,像上面提到的求最大值、最小值,还有绝对值,中学时我们学过的sin(正弦)、cos(余弦)等,它都具备,这就给一些需要数学计算的场景提供了方便,如图形绘制。
原始值与引用值
这里提一下二者的区别,因为前面介绍的“数字、字符串、布尔”都属于原始值,引用值还没介绍完。
除了前面见过的最基本的Object,还有很多类型属于引用值类型,比如:用于表示日期的Date,用于做字符匹配的RegExp,数组、函数等,都属于引用值类型。
什么是引用?程序中的变量,都会被存在计算机中,对于原始值来说,存储的是内容本身,采用栈结构,而引用值不存内容,只存一个引用指针(地址),采用堆结构。
它们的区别是,当发生赋值行为,原始值会创建一份值的副本,而引用值只会复制指针。
结果就是,当原始值发生了类似 a = b 的传递,后续对于a的改变不会影响b,但引用类型中,后续对a的改动会同步到b,这也就引出了一个老生常谈的话题“深/浅拷贝”。
小结
行文至此,已经聊了很多有关数据定义和特性相关内容,说多不多,说少也不少。
不多,是因为常用的确实不多,但每种类型背后所包含的细节不少,限于篇幅无法一一提及,而且一些东西虽然不常用,一旦需要,还挺高效,比如RegExp。
不论多与少,我们把它们都当做工具箱里可以随时取用的工具就可以了,像你家工具箱里有起子、扳手、透明胶、针等等,不用把它们都记下来,需要用的时候去找就行,用多了就记住了,可能某一天你还觉得不够用。
从下篇文开始,会对本篇未展开的,但在编程中比较常见,比较重要的关键知识点做进一步分享,欢迎关注~