【轻聊前端】小角色,大用途——变量

现今各种框架、工具‘横行’,到处在讲原理和源码,更有跨端技术需要我们去探索,但如果基本功不好,学什么都是事倍功半,效果很不好,花费时间的同时打击自信心,所以,踏实学好基础是前提。此篇文章,为我所计划的【轻聊前端】系列第二篇,旨在系统地、逻辑性地把原生JavaScript知识分享给大家,帮助各位较为轻松地理清知识体系,更好地理解和记忆,我尽力而为,望不负期待。

上篇文提到,不论什么程序语言,都在做三件事:

数据存储、数据处理、数据传输

本文就聊存储。

存储,即为数据找到一个空间来存放,这个空间的载体就是“变量”。

JavaScript发展到现在,数据类型没有大的变化,但声明数据的方式发生过明显变化。

先看数据类型。

数据类型

JavaScript当中的数据类型有7种(原本是6种,ES6之后新增一种):

字符串(String)、数字(Number)、布尔(Boolean)、Null、Undefined、符号(Symbol)、对象(Object)

它们又分为基础类型引用类型

基本类型可理解为——不可再分的,不包含任何其他类型的类型,存在空间中的就是值本身。

如:String、Number、Boolean、Null、Undefined、Symbol

而引用类型,存的是一个地址,这个地址又可以存放各种不同类型的数据,包括“基本类型”和“引用类型”。

如:Object、Array等。

用一个简单的例子来看它们的区别。

基本类型

1
2
3
4
5
6
var a = 3
var b;
b = a;
a = 4;
console.log(a); //4
console.log(b); //3

引用类型

1
2
3
4
5
6
7
8
var c = {
name:'张三'
}
var d;
d = c;
console.log(d.name); //张三
c.name = '李四';
console.log(d.name); //李四

可以看出,基本类型在传递时,相当于把值拷贝了一份,二者相互不影响,而引用类型的赋值行为仅仅传递了地址,它们指向的仍是同一个空间,所以一个变另一个也会变。

注意措辞:“赋值行为”。并非不能够做到相互不影响,只是单纯地“赋值”做不到。

这就引出了前端圈经常讨论的话题——深拷贝浅拷贝,先提一下,不细说,领会引用类型是什么即可。

关于基本数据类型,看似不打眼,但几乎每份前端笔试题里都会有它,也正因为平时不太在意,写错、写漏、多写的情况经常发生,应该怎么记呢?

  • String和Number很好记,只有数字能够进行数学运算,而字符串通常用来表示文本信息。

  • Boolean,用于条件判断,true(真)/false(假)。

  • Null,只有值null;

  • Undefined,只有值undefined;

前面三种较好理解,只需注意后两种。

从逻辑上说,null是空对象指针,访问不存在的对象时会是null,也常用null来初始化一个尚未赋值的对象;

Undefined是为了区分null和未赋值变量而添加的,“未赋值变量”包括基本类型变量、数组中没有值的索引位及未定义值的对象属性等。

如此以来,前五种基本类型就记住了,再加上ES6新增的Symbol,就是完整的基本类型。

至于同样很常见的Object、Array、Function之类,都不是,为什么,它们符合“不可再分,不包含任何其他类型”吗?显然不~

变量声明

ES6之前,声明一个变量只能用 var 关键词,ES6之后多了 let 和 const。

后两者和前者的区别就是,后两者使得JavaScript当中具备了块级作用域

提到“块级作用域”,就先讲一下什么是“作用域”。

顾名思义,作用域就是能够起作用的区域,起什么作用?——可获取,可操作

作用域跟什么有关呢?跟在哪里声明有关,即声明位置决定作用域

而且作用域遵循“由内向外”的查询规则,先就近在小的范围查询,查不到再往外,直到全局作用域。

ES6之前,JavaScript当中最为大家熟知的就是“全局作用域”和“函数作用域”。

var

比如:

  • 在代码的最外层定义一个变量a
1
var a;

它就属于全局,其他任何代码都可以对其进行访问或者修改。

  • 在函数当中定义一个变量a
1
2
3
4
5
function test(){
var a = 10;
}
test();
console.log(a) //Uncaught ReferenceError: a is not defined

会得到a并没有定义的报错。

但也存在另一种“疏忽”的情况,就是未使用 var 关键字

1
2
3
4
5
function test(){
a = 10;
}
test();
console.log(a) //10

这时a会暴露到全局,又可以被访问,这类疏忽最好不要犯。

只有“函数”才有自己的作用域吗?严格说不是,但因为那些语句少有使用场景且背离最佳实践,所以暂不了解也没有影响。

letconst

ES6之前没有真正被广泛定义的“块级作用域”。

“块”是指“代码块”,被大括号包裹起来的一段代码可以看做一个代码块。如if、while、function等。

JavaScript当中的变量除了被定义在全局,就是被包裹在各种代码块中,但并不代表它们被限制在了代码块中。

let、const,和var最明显的三点不同。

  • 将变量限制在了代码块
  • 不可重复定义
  • 不会被提升

一个个看.

块作用域

1
2
3
4
5
6

if(true){
let a = 4;
const b = 6;
}
console.log(a,b) //undefined undefined

我们在if条件判断的代码里定义了两个变量a,b,外部访问均为undefined,这说明,在if代码块的外部无法正常访问代码块内定义的变量。

重复定义

1
2
3
4
5
if(true){
let a = 4;
var a = 6;
}
Uncaught SyntaxError: Identifier 'a' has already been declared

变量a已经被let声明,就不能再次被声明。

不被提升

前面没有说提升,放在这里刚好做比较。

使用 var 声明的变量,在其作用域内会被提升到最顶部,以便被其他代码使用。

1
2
3
4
5
function test(){
console.log(a) //undefined
var a = 10;
}
test();

咦,这不是 undefined 么,也不是 10 啊,提升到哪了?

“坑”就在此。

1
2
var a;  //这叫'声明'
var a = 10; //这是在声明的同时定义初始值

所以,提升只是声明被提升,而赋值未提升,才会看到上面输出的是undefined。

当然,变量提升不止如此,函数(function)同样,可以在声明函数的前面使用,到函数部分再详聊。

上面说了最明显也是最重要的三个区别,还有一个区别,即在全局定义变量时,使用 var ,变量属于全局对象 window,而使用 let 和 const 不会这样。

再来看 let 和 const

1
2
3
4
5
6
function test(){
console.log(a,b);
let a = 10;
const b = 10;
}
test(); //Uncaught ReferenceError: Cannot access 'a' before initialization

这段代码甚至没走到声明变量的地方就报错了,在初始化之前不能访问变量a,就证明 let 和 const 的声明不会被提升。

值得说明的是,虽然ES6之前的 var 存在提升的现象,但比较提倡的做法仍是在所属代码块的顶部将所有变量一起定义,而不是随意散落在代码段中,ES6中增加了 let 和 const 后更是如此,像这样:

1
2
3
4
5
6
function test(){
let a = "",
b = 0,
c = true;
//其他代码
}

说完 let、const 和 var 的区别,说说 let 和 const 的区别。

const 可看做 constant 的缩写,constant 的意思是“固定的、常量”,即不可修改。

所以,const 在声明的同时必须赋值,且在使用范围内不可修改

其实ES6之前,我们就需要在程序中定义常量,且约定常量使用大写字母和下划线结合的方式命名,比如:MAX_COUNT。

至于是否可变,就靠程序员来遵守规则,程序员往往是不可靠的…

const的出现从语言层面加了限制,使其不可更改。

1
2
3
const a = 7;
a = 8;
//Uncaught TypeError: Assignment to constant variable.

可以看到,用 const 定义了一个变量并赋值之后,改变它的值时就报错了。

but,并不是用 const 声明的一切都不可更改,const声明的限制只应用到变量类型的最外层。换句话说,如果变量类型为基本类型,其本身不可更改,如果是引用类型,则引用类型本身受限制,键不受限制。

像下面这样就可以正常运行。

1
2
3
4
5
const a = {
name:'张三'
}
a.name = '李四';
console.log(a.name) //李四

综上,现在定义变量依然可以使用 var,但多数项目中都已被 let 或 const 占据,所以,在确定值不变的情况下就用 const,否则可使用let。

变量命名

很多人在刚学编程时不会想到,命名会是个让人头疼的问题。

有几个方面原因:

一、本身词汇量匮乏

二、每个变量都会有关联变量,无形中增加了用词量

三、不仅意思要对,还不宜太生僻

四、要考虑适用范围,较通用还是更具体,某种程度上也会形成一个“命名空间”。

大致需要遵循如下几个原则:

  • 表意
    较为明晰地表达意思,比如:

是什么,isXX

有什么,hasXX

干什么,getXX

等等。

  • 统一/通用
    业界通用结合团队通用,这样一来,不论是团队进新人,还是你突然要介入一个新项目,都不用花太多时间或沟通成本来搞懂程序的意图和逻辑。

  • 少定义全局
    一、全局变量是一直存在的,长期占用内存。

二、全局变量全局可用,这就为其修改增加了不确定性,增加了意外出现的风险。

所以多数情况下,我们只需定义必要的全局变量,其他变量定义在局部就好。

  • 少定义变量

语言本身对变量的定义是不限制的,所以容易变得随意,减少不必要的变量定义,可以适量减少存储空间、传递次数和处理环节。

但也不必矫枉过正,还是要兼顾“简洁、高效、易读”,仅仅简洁了,却很难读,就得不偿失了。

附赠

  • 常见面试题之 变量如何存储?

基本类型存储在中,引用类型存储在中。

栈:只能在某一端进行添加或者删除的数据结构,比较形象的比喻是“叠盘子”,后进先出。存取速度较快,但数据大小和生命周期确定。

堆:生活中有“书堆”、“垃圾堆”等,指可动态分配的空间,用于动态分配和释放程序所使用的对象。因为对象可以扩展,放在堆中可以不断扩展。

  • 常见面试题之 for循环——输出几?
1
2
3
4
5
for ( var i=1; i<=5; i++) { 
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}

setTimeout是定时器函数,由“时间间隔”和“回调函数”组成,可以在某间隔时间后执行特定动作,如上是打印一个值。

按道理,这段代码应该是隔一秒输出一个数字,从1到6。

实际情况是,连续输出5个6,为什么?

一、for中用var定义的变量会泄露到全局

二、setTimeout是异步函数,javascript代码在浏览器中执行的任务是有优先顺序的,当异步函数执行的时候,外层循环已经进行完毕,即i的值已经被加到6。

ES6之前,解决这个问题的常见方法是“闭包”,这个后面会聊,但ES6之后,有了更方便的处理方案,就是使用let来声明变量i。修改如下:

1
2
3
4
5
for ( let i=1; i<=5; i++) { 
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}

这个时候,当循环每进行一次,变量i都进入了一个独立的作用域当中,然后被单独输出,就会得到预期的结果。

总结

到此,关于“变量”的讨论告一段落。

标题叫“小角色,大用途”,因为变量就像是程序大楼的一砖一瓦,很不起眼,却无处不在。

常用的数字、字符、对象、函数、数组等等,不论简单或是复杂,都是数据,都存储在变量中。

不仅如此,变量还能起到“缓存”的作用,比如,经常会获取页面DOM元素的时候将其保存在一个变量内,然后拿这个变量去做其他的事,或者把从数据库当中请求回来的值存到一个变量中,后续再用直接拿变量的值,毕竟DOM查询和发请求拉数据都需要时间,这无疑节省了时间。

编程正是由于变量的存在而被赋予无限可能,如果没有变量,都是固定的值,就没有变化和个性可言,有了变量,就能在需要的时候产生不同的数据,构成各种丰富的、个性化的网页内容。

聊的够多了,我们下篇见。