JavaScript高级程序设计(第5版):程序中的数据表达

每个系列一本前端好书,帮你轻松学重点。

本系列来自曾供职于Google的知名前端技术专家马特·弗里斯比编写的 《JavaScript高级程序设计》(第5版)

本文开始,我们将由浅入深介绍程序是怎么从零慢慢构建起来的。

虽然是以JavaScript这门语言描述,但其他语言也类似,且不论是小程序还是大项目,10行代码还是10000行,都是如此。

数据如何存在

代码中的数据并不神秘,现实怎么存在,它就怎么存在,只不过要用代码进行定义。

比如:小明,8岁

用代码表示:

1
2
const name = "小明";
let age = 8;

有印象的朋友应该记得,这叫”变量“。

程序中的一切都离不开变量,那么变量存在的意义是什么。

举个例子,假设需要执行一个加法:

1 + 1 永远等于2,那么a+b呢?

就不知道了,因为相加的不是数字而是标识符,计算结果取决于a、b代表几,这里的“代表”就是一个会变化的东西,故称为变量。

因为变量的存在,一个算式不会固定地返回一个结果,而是随着情况的变化返回多种不同的结果,这就是它存在的意义。

定义与作用域

既然一切离不开变量,就要在需要的地方定义它,为了便于识别,JavaScript提供了三个关键字:var、const、let

1
2
3
var name = "小明";
const name = "小明";
let name = "小明";

以上三种方式均是可行的,但使用时有区别,主要体现在:

1、作用域

2、是否可修改

作用域

顾名思义,能够起作用的区域,在一定范围内,能访问,能修改,否则会报错。

为什么需要这个范围?

想象一下,我们的代码可能分布在很多文件中,文件内又会有多个代码块,如果变量的作用范围不设限,哪里都能访问和修改,岂不乱了套,管理起来就是灾难。

先说var,这是ES6之前唯一可用的关键字,它具备函数作用域的特性,具体如下:

1
2
3
4
5
6
function test(){
console.log(message) // undefined
var message = 'hi'
console.log(message); // 'hi'
}
console.log(message) // 出错

上面这段代码,定义了一个test函数,其中定义了message变量,通过在不同的时机打印变量值可以看到函数作用域的特点,在函数中定义,就只在函数中可用。

第一行的执行结果是undefined,undefined的意思是,变量存在,但没有值,这也是var的特性 — ”声明提升“,即允许在赋值之前访问,不报错,不影响程序执行,只是没有值。

在ES6之前,只有两种作用域:函数作用域、全局作用域。要么只在函数中可用,要么整个文档都可用。

下面这段代码,看起来变量name被一对花括号封闭在代码块中,但实际上无法限制它。

1
2
3
4
if(true){
var name = 'Bob'
}
console.log(name) // Bob''

所以,在 var 独自为王的时代,变量所带来的隐患较多。

ES6之后,就有了块级作用域,也就是使用let或者const来定义。

1
2
3
4
5
6
if(true){
console.log(age); // ReferenceError: age 未定义
let age = 26;
console.log(age); // 26
}
console.log(age); // ReferenceError: age 未定义

这段代码中,展示了let和var的两点区别:

1、代码块内定义的变量,外部无法访问

2、即便在代码块内,定义之前也无法访问,这种现象叫“暂时性死区“,与var的”变量提升“相对应

既然let能够提供块级作用域,还需要const干什么呢,和let有什么不同?

const"constant"(常量) 的缩写,用于声明一个不可重新赋值的变量。不可重新赋值,意味着两点:

1、定义的同时必须赋值

2、后续不允许修改

那什么情况适合使用常量呢?全局共用,且不允许业务代码对其进行改动的变量,如:版本号、公共访问地址等。

1
2
3
4
5
6
// 未初始化
const age; // 错误

// 尝试修改
const age = 26;
age = 36; // TypeError

好,三种变量定义方式介绍完,更推荐怎样做呢?

三条原则:

  • 非必要不用var,会修改的变量用let,其他都用const,特别对于公共常量,推荐”AA_BB”式全大写格式;
  • 尽量少定义全局变量,目的是减少不必要的冲突和修改;

    还有一个好处是,局部变量用完后大概率被销毁,能释放内存,全局变量长期占内存;

  • 不定义不必要的变量,多一个变量就多一份理解和维护成本。

数据类型

JavaScript中的变量被称为”松散类型“,松散意为对类型没限制。

定义时不需声明类型,定义后允许被改为另一种类型。比如:

1
2
let message = "hi"
message = 100;

定义时是字符串,后被改为数字,在语法层面是允许的。

但正因如此,导致代码运行的不确定性大大增加,就有了现在所流行的TypeScript。

虽说JavaScript定义变量时不需要声明类型,但它仍然具备类型。

JavaScript有七种简单数据类型和一种复杂数据类型。

简单类型:String、Number、Boolean、Undefined、Null、BigInt、Symbol

复杂类型:Object

所有数据,都是用以上类型来表达的,但怎么理解简单和复杂?

简单,可理解为直给,而复杂,可以包含任意简单或复杂类型。比如:

1
2
3
4
5
6
7
8
9
10
// 简单类型
const name = "小明"
const age = 26

// 复杂类型
const person = {
name:"小明",
age:26,
tools:["铅笔","橡皮"]
}

以上这段代码,包含了上篇文提到的变量name,对象person和数组tools,展现了各自在定义上的特点。

专属特性

既然有这么多类型,只是表示上的区别吗,肯定不是,它们各有各的特性,这是理所当然,也是应用需要。

比如:

1、数字能用来做数学运算、比较大小,其他类型不能

1
2
3
let a = 2, b = 3;
a * b // 6
Math.min(a,b) // 2

2、字符串可以做拼接、裁剪等,其他大部分类型不能

1
2
3
let a = 'ab', b = 'cd';
a + b // 'abcd'
'abcd'.slice(1,3) // 'bc'

3、布尔值多用于条件逻辑中的“真”、“假”判断,其他类型不能

1
2
3
if(true){
// 执行这里
}

你可能会说,还有这样的代码呢?

1
2
3
4
5
6
7
if(a > b){
// 执行这里
}
// 或者
if(a){
// 执行这里
}

对,这样的代码,只是看起来没有直接使用布尔值,但 a > b 的结果仍是个布尔值,要么是true,要么是false。

而a本身具备一个合理可用的值,那么它也会被转换为布尔值true。

特性来源

你应该注意到了,在数字类型中,用到了Math,字符串类型中用到了slice,它们怎么来的?

其实,就算是最基本的类型,有时候也被赋予一种类型叫“包装类型”,包装类型可以创建一个对象,每种对象包含着若干属性和方法,就像字符串,当它调用slice方法时,实际执行的是:

1
2
3
'abcd'.slice(1,3)  // 代码

new String('abcd').slice(1,3) // 内部执行

于是它就有了一堆方法可用,如:slice、substring、concat、trim等等。同理,布尔和数字类型在调用它们的方法时,执行的是 new Boolean() 和 new Number(),需要注意的是,这种创建没有可持续性,用完即毁,想接着再把它当对象用就不行了。

Math就更强大了,它是“内置对象”,顾名思义,不需要开发者创建,直接可用。

当我们写的代码开始执行时,全局上下文中会存在两个内置对象:GlobalMath

你可能想说,Global是什么,没见过呀,Global意思是全局对象,它在浏览器中的实现就是window对象,这个大家就熟悉了,所有的全局变量和函数,都归属于它;而Math包含很多数学方法,像上面提到的求最大值、最小值,还有绝对值,中学时我们学过的sin(正弦)、cos(余弦)等,它都具备,这就给一些需要数学计算的场景提供了方便,如图形绘制。

原始值与引用值

这里提一下二者的区别,因为前面介绍的“数字、字符串、布尔”都属于原始值,引用值还没介绍完。

除了前面见过的最基本的Object,还有很多类型属于引用值类型,比如:用于表示日期的Date,用于做字符匹配的RegExp,数组、函数等,都属于引用值类型。

什么是引用?程序中的变量,都会被存在计算机中,对于原始值来说,存储的是内容本身,采用栈结构,而引用值不存内容,只存一个引用指针(地址),采用堆结构。

它们的区别是,当发生赋值行为,原始值会创建一份值的副本,而引用值只会复制指针。

结果就是,当原始值发生了类似 a = b 的传递,后续对于a的改变不会影响b,但引用类型中,后续对a的改动会同步到b,这也就引出了一个老生常谈的话题“深/浅拷贝”。

小结

行文至此,已经聊了很多有关数据定义和特性相关内容,说多不多,说少也不少。

不多,是因为常用的确实不多,但每种类型背后所包含的细节不少,限于篇幅无法一一提及,而且一些东西虽然不常用,一旦需要,还挺高效,比如RegExp。

不论多与少,我们把它们都当做工具箱里可以随时取用的工具就可以了,像你家工具箱里有起子、扳手、透明胶、针等等,不用把它们都记下来,需要用的时候去找就行,用多了就记住了,可能某一天你还觉得不够用。

从下篇文开始,会对本篇未展开的,但在编程中比较常见,比较重要的关键知识点做进一步分享,欢迎关注~