前言
在我刚学习JavaScript时,我一直对其中的变量很迷惑,为什么分了种类之后,还有类型?为什么变量的初始化有那么多方式?为什么变量复制的时候有那么多条条框框?
待着这些迷惑去网上找答案,有些文章确实能帮助我理解,但多数都是“结论型”的,仿佛都在告诉你:“这个就是这样,记住就行了”。但这样会很迷惑,我无法了解它的原理,以至于在面试和工作中,依然停留在“死记硬背”的阶段,这一度让我很难受。
于是便采用了最朴实的方法:读书。在看了大量的书和文章之后,我带着自己的理解,这里用最简单、直白的方式,一步一步地写了这篇关于变量的文章,希望能帮助和我一样困惑的人,解决面试中的问题
此外,该文章参考了一些前辈的优秀文章,后面会把链接附上,供大家欣赏
最后,文章难免有出错之处,希望大家能谅解。
1 变量
对于学过前端的人,对js的变量类型再熟悉不过了,该篇文章不会去讲每种类型的用法,而更侧重于阐明变量的本质区别 相比于其他类型的语言,比如C、Java等,JavaScript中的变量可谓是独树一帜。 我们都知道JavaScript里面的变量是松散类型的,这就意味着,一个变量的值和类型可以随时随地改变。这样一来JavaScript就有了很强的灵活性,不过也会带来不少问题。
1.1 变量的类型:包装对象与属性
在js中,定义变量有2种方式:字面量和构造函数
let str = 'hello' //使用字面量定义
let str1 = String('hello') //使用字面量定义
let str2 = new String('hello') //使用构造函数定义
但是这2者有什么不同呢,尝试着比较一下str和str1、str2是否相等
console.log(str === str1) //>> true
console.log(str === str2) //>> false
可以发现,str与str2不相等,但明明值都是hello
,这里就要提一下JavaScript中的包装对象了
JavaScript中规定规定,万物皆对象,该语言在设计时提供了String、Boolean、Number三个包装对象(构造函数),这三个包装对象的作用是为了能够创建这三个基本数据类型对象
也就是说,使用new String
创建出来的字符串是个对象,我们可以检测一下它的类型,发现类型是对象

console.log(typeof str2) //>> object
既然是对象,那么就可以添加自定义属性,我们给str和str2都添加一个name属性,输出发现str是undefined,str2可以正常输出
str.name = '我是字符串'
str2.name = '我是字符串'
console.log(str.name) // undefined
console.log(str2.name) //>> 我是字符串
所以,看到这里,我们大致可以了解到,对于Number、String、Boolean这3类数据类型:
- 使用字面量的声明方式,得到的变量就是简单的数据类型
- 使用
new 包装类
的形式声明出来的是对象 - 即使值一样,2种声明方式得到的结果也不同
所以,为了在写代码的时候避免歧义,一般规定:创建字符串,数字,布尔值时,必须使用字面量的方式,而不要使用构造函数,因为基本数据类型是不该有自定义属性的
//推荐
let num = 123
let str = 'hello'
let bool = false
//不推荐
let num = new Number(123)
let str = new String('hello')
let bool = new Boolean(false)
为什么?因为这是这门语言的设计缺陷,借鉴了Java的设计,将数据类型分为原始值和引用值,大家只好尽量避免了
1.2 变量的类型:基本类型和引用类型
JavaScript的变量值可以包含2种不同类型:基本类型和引用类型。
基本类型就是我们常见的简单数据类型,如Undefined、Null、Boolean、Number、String
和Symbol
。这些值是保存在栈内存中,是按值访问的,我们平时操作的值就是存储在变量中的实际值。
引用类型可以统称为对象(Object)类型,比如常见的Array、Object、Function
等。 这些值保存在堆内存中,与其他语言不同,JavaScript不允许直接访问对象所在的内存空间。 在操作对象时,实际操作的是对该对象的引用,而非对象本身。因此保存引用类型值的变量是按引用访问的。
上面引出了几个概念 基本类型
、引用类型
、栈内存
、堆内存
、按值访问
、按引用访问
下面将会详细对这些概念进行解释
1.3 栈内存和堆内存
在说变量类型的区别之前,先了解一下前置知识:栈内存和堆内存
JavaScript之父布莱登·艾奇,在设计JavaScript之时,借鉴了Java虚拟机(JVM)的内存处理机制。
JavaScript代码在执行时,会在我们电脑的内存里开辟内存空间(其他语言的代码也是如此),用于存储代码运行时的变量的值,这里的内存就是我们通常说的8G、16G、32G的电脑内存条。
虽说开辟的内存空间用来存储变量值,但是对于不同的变量值类型,其处理方式也不同,因此在JavaScript中,开辟的内存空间分为2类:栈内存和堆内存
简单来说,在JavaScript中: 栈内存用于存储基本类型的变量值 堆内存用于存储引用类型的变量值
我们定义的基本类型的变量,其变量值都会被如实放到栈内存中。 而引定义的用类型的变量,其值的存储方式就不同了, js会将值的访问地址存储到栈内存中,而将引用类型的值存储到堆内存中。
至于栈内存和堆内存的详情区别,大家可以参考这篇《内存中堆和栈的区别》
这可能难以理解,我们结合代码和示意图来理解
let a = 25 //保存在栈内存中
let b = 'hello' //保存在栈内存中
let c = false //保存在栈内存中
let d = [1,2,3] //变量d存在栈内存中,[1,2,3]作为对象存在堆内存中
let e = { x:1, y:2 } //变量e存在栈内存中,{ x:1, y:2 }作为对象存在堆内存中
如上图,当我们访问a,b,c
等基本变量时,会直接从栈内存中读取到具体值。
而访问堆内存中d,e
等引用类型的值时,要分2步走:
- 从栈中获取该对象值的访问地址
- 再从堆内存中取得我们需要的对象数据
看到这不仅有人会疑问,这么抽象的概念和我平时开发有什么关系呢?不仅有,而且关系可大了,接着来看一下变量复制的区别
1.4 变量复制
JavaScript中变量的复制是很有意思的事,涉及到变量的浅拷贝和深拷贝,这也是为什么把变量分类和内存分类放在前面来说,因为二者是紧密关联的
1.4.1 基本类型复制
在我们平时开发中,经常遇到变量复制的时候,比如:
let x = 25
let y = x
y = 26
console.log(x) // >> 25
console.log(y) // >> 26
上面先定义了变量x = 25
,接着定义变量y
,并将x
的值复制给它。
然后输出x
和y
的值,分别是25和26,修改y
的值并不会影响x
(这不废话嘛)。其过程如下图所示:
在复制基本类型的数据时,系统会在栈内存中开辟一个新的空间,为新的变量分配一个新的值,这个新的值就是从原来的变量复制过来的,之后原有的值和新的值保持相互独立,互不影响。
举个例子大家就会明白了,有一天你在写文档,你的同事想要一份你写的文档,然后你复制了一份你的文档,通过钉钉发给他了,他接收后修改文档,你这边的文档不会被修改(肯定改不了啊)因为你把文档本身发给他了啊
1.4.2 引用类型复制
引用类型的复制就截然不同了,我们把x
定义为对象,并将其复制给y
,接着修改y
的age
属性,然后输出一下。发现,x
的age
属性也变了,但是明明没有修改的x
的值
let x = {
name: 'Tom',
age: 25
}
let y = x
y.age = 26
console.log(x.age) //>> 26
console.log(y.age) //>> 26
- 引用类型的值发生复制时,同样会为新的值在栈内存中开辟一个新的空间,不同的是,这个新的值,是我们上面说的“值的访问地址”,是一个指针(C语言中的概念)
- 这个地址(指针)指向堆内存中的对象,复制完成后,
x
和y
都指向同一个对象 - 因此修改一个值,会影响另一个值
其过程如下图所示:
完成复制后,两个变量引用的是同一个值,所以,修改其中一个值就会影响另外一个,因为本质上二者在内存中访问的是同一份数据。
还是举个例子,有一天你在写文档,你的同事想要一份你写的文档,你说我的文档太大,存存到网盘上了,我把网盘地址发你你自己看吧,你把地址发给他后,他进到网盘没有下载文档,而是在线编辑了一下,然后文档就被修改了,你们俩访问的文档是同一份
这样解释是不是就很容易理解,基本类型是复制的值,类似于你把文件直接发给他。而引用类型复制是传递的地址,你把文件地址发给他,让他自己去看。
但为什么这么设计呢,这就不得说一下JavaScript的历史这门语言的历史了:
简单来说:
- JavaScript是国外公司推出的编程语言产品,用于快速抢占浏览器市场
- JavaScript之父Brendan Eich (布兰登·艾奇),是公司雇佣的程序员,用来设计一门新的语言
- 布兰登·艾奇 只用了10天就完成了设计
- JavaScript 借鉴(抄袭)了C、Java、Python等语言
如此看起来,编程语言也是程序员为了完成KPI设计出来的,正如我们上面提的借鉴了C语言的指针、Java的JVM(虚拟机)的内存管理模型 所以,变量的复制问题,是一个历史遗留问题。这样像极了我们在日常开发中到处去搜功能代码一样。
1.4.3 变量作为函数参数时的复制
这部分在在第二篇文章《函数》会做详细讲解,不过其和值的复制也有很大关系,这里就先提一下
JavaScript中规定:所有函数的参数都是按值传递的。这意味着函数外部的值会被复制到函数内部的参数中,就像一个变量复制到另一个变,其中:
如果参数是基本类型的,就和我们上面说的基本类型的复制一样。 如果参数是引用类型的,就和我们上面说的引用类型的复制一样。
看起来说了,但又好像没说,直接看代码示意图:
function add(num) {
num += 10
return num
}
let count = 20
let result = add(count)
console.log(result) //>> 30
console.log(count) //>> 20 没有变化
定义了一个函数,接受一个参数num
,在函数内部将其加上10,返回结果,用变量result
保存。 接着定义一个变量count = 20
,调用函数,将count
作为参数传递进去。然后输出一下,发现原有的count
的值没有发生变化,到这里都是正常的。但如果函数的参数传递的是对象,那就没那么清楚了,接着再看下面的示例
function setAge(obj) {
obj.age = 25
}
let person = new Object()
person.age = 1
console.log(person.age) //>> 1
setAge(person)
console.log(person.age) //>> 25
这次,创建了一个对象,并把它保存在变量person
中,然后将其age
属性设置为1。同样定义一个setAge
函数,接收一个对象作为参数,修改对象的age
属性。 然后调用函数,将person
传递进去,接着输出person
的age
属性,发现age
变成了25
到这里大家可能就有了结论,“如果参数是引用类型的,那么在函数内部修改了参数,会反映到函数外部的变量,这就意味着参数是按引用传递的”
等等,这句话真的对吗?和前面说的JavaScript中规定有点不同。 先别着急下定义,接着看下面的代码:
function setAge(obj) {
obj.age = 25
obj = new Object()
obj.age = 100
}
let person = new Object()
person.age = 1
console.log(person.age) //>> 1
setAge(person)
console.log(person.age) //>> 25
代码唯一的变化是,setAge
函数多了2行代码,把参数obj
的age
属性设置为25后,接着obj
被设置为一个新的对象,并且age
属性被设置为100。
如果按照之前我们的总结:
“如果参数是引用类型的,那么在函数内部修改了参数,会反映到函数外部的变量,这就意味着参数是按引用传递的” 那么person
的age
属性应该是100,但是当调用函数后,输出person.age
后,发现其值是25 这说明,“在函数内部修改参数,又不会反映到外部”。和之前的总结刚好相反,至于为什么,原因就在新增的2行代码
obj = new Object()
obj.age = 100
这两行代码其实就是一个重新赋值的操作,定义了一个新的对象,并将其赋值给obj
,既然是新对象,那么参数obj
的指针就指向那个new Object()
对象,就和外部的person
没关系了,所以修改obj
不会影响到外部的person
对象
所以,我们可以看出来,引用类型作为参数传递时,具体还得看函数内部是怎么处理的。 这里就涉及到函数里面的知识点:形参、实参、arguments对象了,由于篇幅较长,这里就不再叙述,大家可以去看我的另一篇文章《函数》。
1.5 如何确定变量类型
1.5.1 typeof
大家都知道,使用typeof操作符,最适合用来判断一个基本类型的变量的类型是否为字符串、数值、布尔或者undefined。
但是如果值是对象或者null,那么typeof都会返回object,如下代码:
let s = 'str'
let b = true
let i = 22
let u
let n = null
let o = {}
let a = []
console.log(typeof s) //>> string
console.log(typeof b) //>> boolean
console.log(typeof i) //>> number
console.log(typeof u) //>> undefined
console.log(typeof n) //>> object
console.log(typeof o) //>> object
console.log(typeof a) //>> object
奇怪的事情出现了,为什么 typeof null
的值是object?
简单来说这还是JavaScript语言设计的一个缺陷,之前说过,布兰登·艾奇10天就把JavaScript设计出来了,难免有考虑不周的地方。
如果非要深究原理,就涉及到JavaScript的原型和原型链了,对象原型链的尽头就是null
大家可以试着输出一下下面这行代码。
let o = {}
let a = []
console.log(typeof o) //>> object
console.log(typeof a) //>> object
console.log(Object.__proto__.__proto__.__proto__) //>> null
console.log(Object.prototype.__proto__) //>> null
这里我们不深究原型链,毕竟不是本篇的重点,后面会有单独的文章去详细说。
回到上面的代码, 变量o
和a
一个是对象,一个数组,但typeof返的却都是object。那个该如何判断一个变量到底是什么类型的对象呢?
1.5.2 instanceof
为此,JavaScript提供了一个新的操作符instanceof
,查看变量是不是给定构造函数的实例。如果是则返回true
,否则返回false
let o = {}
let a = []
// o 是 Object的实例吗?
console.log(o instanceof Object) //>> true
// o 是 Array的实例吗?
console.log(o instanceof Array) //>> false
// a 是 Array的实例吗?
console.log(a instanceof Array) //>> true
// a 是 Object的实例吗?
console.log(a instanceof Object) //>> false
用法
变量 instanceof 构造函数
用instanceof
操作符可以用来确定对象的具体类型
1.5.3 Object.prototype.toString.call
除了typeof和instanceof,使用该方法也能判断一个变量的具体类型,而且无论是基本类型还是引用类型,都可以精准判断
不同的是,这里使用了call
借调了Object原型链上的方法,至于原型链、call,我们暂且先不管心,后续有文章会单独介绍,这里主要看一下该方法如何使用
let s = 'str'
let n = 123
let o = {}
let a = []
console.log(Object.prototype.toString.call(s)) // >> [object String]
console.log(Object.prototype.toString.call(n)) // >> [object Number]
console.log(Object.prototype.toString.call(o)) // >> [object Object]
console.log(Object.prototype.toString.call(o)) // >> [object Array]
Object.prototype.toString.call
是一个函数,将变量传递进去,就返回一个字符串 字符串后面包含了该变量的具体类型,如Number Boolean Array Map
。这样一来,我们可以做一个简单的封装,用来判断变量是不是给定的数据类型
function isType(data, type) {
const typeObj = {
"[object String]": "string",
"[object Number]": "number",
"[object Boolean]": "boolean",
"[object Null]": "null",
"[object Undefined]": "undefined",
"[object Object]": "object",
"[object Array]": "array",
"[object Function]": "function",
"[object Date]": "date", // Object.prototype.toString.call(new Date())
"[object RegExp]": "regExp",
"[object Map]": "map",
"[object Set]": "set",
"[object HTMLDivElement]": "dom", // document.querySelector('#app')
"[object WeakMap]": "weakMap",
"[object Window]": "window", // Object.prototype.toString.call(window)
"[object Error]": "error", // new Error('1')
"[object Arguments]": "arguments"
};
let name = Object.prototype.toString.call(data); // 借用Object.prototype.toString()获取数据类型
let typeName = typeObj[name] || "未知类型"; // 匹配数据类型
return typeName === type; // 判断该数据类型是否为传入的类型
}
console.log(
isType({}, "object"), //>> true
isType([], "array"), //>> true
isType(new Date(), "object"), //>> false
isType(new Date(), "date") //>> true
);
1.6 深拷贝与浅拷贝
我们再次回到引用类型复制的那段代码
let x = {
name: 'Tom',
age: 25
}
let y = x
y.age = 26
console.log(x.age) //>> 26
console.log(y.age) //>> 26
如何才能修改y
的值,而又不影响x
呢? 其实可以参考之前函数传值的时候,我们可以新建一个空对象,重新赋值赋值给y,然后x的name
和age
属性逐个复制给y
let x = {
name: 'Tom',
age: 25
}
let y = new Object()
y.name = x.name
y.age = x.age
y.age = 12 //将y的age修改为12
console.log(y.age) //>> 12
console.log(x.age) //>> 25 x.age不受影响
正如上面的代码,变量y重新指向一个新的空对象new Object()
, 到这变量里x
,y
都指向各自的对象,二者互不影响。 接着将x
的属性逐个复制给y
,这样就完成了最基本的浅拷贝
1.6.1 浅拷贝
为什么说是浅拷贝,这个“浅”字该如何理解?,我们看接着看代码
let x = {
name: 'Tom',
age: 25,
address: {
city: '上海',
}
}
let y = new Object()
y.name = x.name
y.age = x.age
y.address = x.address
y.address.city = '苏州'
console.log(y.address.city) //>> 苏州
console.log(x.address.city) //>> 苏州 x.address.city受影响
这次,给变量x增加了一个引用类型的属性address
,同样把它复制给y
,但不同的是,当修改了y.address.city
后,x.address.city
也变成了苏州。
参考我们前面说过引用类型的复制,这不难理解,因为address是个对象,对象复制是复制的地址,所以y.address
和x.address
指向堆内存的同一个对象。
看到这里,上面的“浅”其实就是无法复制引用类型的属性值
这里小结一下:
- 浅拷贝中,原始值和副本共享同样的属性。
- 浅拷贝会完整拷贝基本类型的值。
- 浅拷贝只拷贝了引用类型的地址。
- 浅拷贝中如果修改了拷贝对象会影响到原始对象,反之亦然。
- js中,数组和对象的赋值默认为浅拷贝。
使用上面的属性逐个赋值的方式,也可以完成浅拷贝,但是当对象属性比较多的时候就比较麻烦了,通常我们使用以下方式实现浅拷贝
1.6.1.1 for循环
定义个拷贝函数,接收一个要拷贝的原始对象,然后在函数体内,根据原始对象的类型,创建一个新数组或者对象,然后将原始对象的属性逐个复制到新对象上,接着返回新对象
function simpleCopy(originObj) {
let copyObj
if(Object.prototype.toString.call(originObj) === '[object Array]' ) {
copyObj = []
}
if(Object.prototype.toString.call(originObj) === '[object Object]') {
copyObj = {}
}
for (let i in originObj) {
copyObj[i] = originObj[i];
}
return copyObj;
}
使用
let x = {
name: 'Tom',
age: 25,
address: {
city: '上海',
}
}
let y = simpleCopy(x)
y.address.city = '苏州'
console.log(y.address.city) //>> 苏州
console.log(x.address.city) //>> 苏州
1.6.1.2 Object.assign()
ES6新增了一个对象方法Object.assign,用于合并对象,可以借助该方法实现浅拷贝,用法如下:
let x = {
name: 'Tom',
age: 25,
address: {
city: '上海',
}
}
let y = {}
Object.assign(y, x) //把x浅拷贝给y
console.log(y.address.city) //>> 苏州
console.log(x.address.city) //>> 苏州
1.6.2 深拷贝
与浅拷不同的是,深拷贝是指递归复制原对象的属性给新对象。 深拷贝结束后,新对象在堆内存中新开辟一块存储空间,二者没有任何关联。
- 深拷贝中,新对象和原对象不共享属性
- 深拷贝递归的复制属性
- 深拷贝得到的新对象不会影响到原对象,反之亦然
- 深拷贝中,所有的基本类型数据默认执行深拷贝,比如Boolean, null, Undefined, Number,String等
1.6.2.1 JSON方法
使用js自带的JSON
方法,先将原始对象转为字符串再转为对象,然后赋值给新对象。 因为字符串是基本类型,所以独立存储在栈区,再转为对象,会重新在堆内存开辟空间,所以就完成了一个深拷贝。
let x = {
name: 'Tom',
age: 25,
address: {
city: '上海',
}
}
let y = JSON.parse(JSON.stringify(x))
console.log(y.address.city) //>> 苏州
console.log(x.address.city) //>> 上海
优点:简单明了,方便记忆 缺点:当对象里面出现函数的时候就不适用了,看下面代码。
let x = {
name: 'Tom',
age: 25,
address: {
city: '上海',
},
say: function() { //新增了一个函数
console.log('你好')
}
}
let y = JSON.parse(JSON.stringify(x))
console.log(y.say) //>> undefined 提示函数未定义
1.6.2.2 使用递归
使用递归深拷贝的本质就是:如果原对象的属性依然是引用类型,那就继续调用拷贝函数,每次函数执行都会新建一个空对象,作为newObj
的属性;如果原对象的属性是基本类型,那就直接复制。
function deepCopy(obj) {
let newobj = obj.constructor === Array ? [] : {};
if (typeof obj !== 'object') {
return obj;
} else {
for (var i in obj) {
if (typeof obj[i] === 'object'){ //判断对象的这条属性是否为引用类型
newobj[i] = deepCopy(obj[i]); //若是,进行嵌套调用
}else{
newobj[i] = obj[i]; //若不是,直接复制
}
}
}
return newobj; //返回深度克隆后的对象
}
let x = {
name: 'Tom',
age: 25,
address: {
city: '上海',
},
say: function() {
console.log('你好')
}
}
let y = deepClone(x)
console.log(y.address.city) //>> 苏州
console.log(x.address.city) //>> 上海
console.log(y.say()) //>> 你好
优点:能够实现对象和数组的深拷贝 缺点:如果拷贝的对象嵌套过深的话,会对性能有一定的消耗
1.6.2.3 ES6的解构运算符 ...
ES6新增了解构运算符...,可以更简洁地完成深拷贝
let x = {
name: 'Tom',
age: 25,
address: {
city: '上海',
},
say: function() {
console.log('你好')
}
}
let y = { ...x }
console.log(y.address.city) //>> 苏州
console.log(x.address.city) //>> 上海
console.log(y.say()) //>> 你好
1.6.2.4 使用第三方库
在工作中,经常使用第三方库实现深拷贝,比如lodash或者Underscore
npm i --save lodash
const _ = require('lodash');
let x = {
name: 'Tom',
age: 25,
address: {
city: '上海',
},
say: function() {
console.log('你好')
}
}
let y = _.cloneDeep(x) // 使用lodash内置的深度克隆方法
console.log(y.address.city) //>> 苏州
console.log(x.address.city) //>> 上海
console.log(y.say()) //>> 你好
1.7 变量声明与作用域
本来作用域这部分内容是想和执行上下文、作用域、作用域链一起写的,但是变量声明也涉及到了块级作用域,就先带着变量浅说一下
放到最后说变量的声明是因为这部分内容大家都很熟悉,而且和前面的内容关系不大,这里算是老生常谈了
1.7.1 变量作用域
变量作用域就:在这个区域内的定义变量,出了这个区域就无法访问到,这个区域,就是变量起作用的地方。在JavaScript中,变量作用域分为2种:全局作用域和局部作用域。局部作用域可以访问全局作用域的变量,而全局作用域无法访问局部作用域的变量
而局部作用域根据表现形式又分为函数作用域和块级作用域 用张图来解释一下:
1.7.1.1 局部作用域:函数作用域
用代码来解释全局作用域和函数作用域就是:
<script>
var a = '皮卡丘' //在全局作用域中
function add() {
var sum = 0 //在函数作用域中
consoe.log(a)
}
console.log(a) //>> 皮卡丘
console.log(sum) //>> Uncaught ReferenceError: sum is not defined
add() //>> 皮卡丘
</script>
如上,a
定义在全局作用域内,任何地方都可见,所以函数add
内能访问到a
;而sum
定义在函数add
内,属于局部作用域,后面的打印命令console.log(sum)
在函数add
之外执行的,访问不到函数add
内的sum
,因此输出Uncaught ReferenceError: sum is not defined
。
任意代码片段外面用函数包装起来,就好像加了一层防护罩似的,可以将内部的变量和函数隐蔽起来,形参函数的局部作用域,外部无法访问到内部的内容。
用图来解释就是:
举个例子,就像蒸包子一样,全局变量是最底部那一笼的蒸汽,自下而上往上冒,上面的笼子就像局部作用域,都可以享受到下面笼子的蒸汽,而不能反过来。
关于作用域,大家先了解这么多,至于为什么全局作用域不能访问局部作用域等细节,放到后面的《作用域、执行上下文、作用域链》中讲解
但块级作用域又是什么呢?这个要结合var和let关键字一起说,继续往下看
1.7.1.2 局部作用域:块级作用域
块级作用域:属于局部作用域的一种,由距离最近的一对花括号构成。换句话说,if块、while块、switch块都可以构成块级作用域。 块级作用域中,使用let声明的变量,以及使用const声明的常量,在块外部是无法访问的
if(true){
let a = 0
var b = 0
}
console.log(a) //>> a未定义
console.log(b) //>> 0 可以访问
while(1) {
let c = 0
var d = 0
}
console.log(c) //>> d未定义
console.log(d) //>> 0 可以访问
1.7.2 使用var声明变量
在ES6之前,声明变量是使用var关键字,如:
var a = 1
var s = 'hello'
var f = false
1.7.2.1 var存在变量提升
使用var声明的变量,会被提升到当前变量所在的作用域顶部,位于所有的代码之前。这个现象叫做“提升”(hoisting)。这样的作用是让代码不用考虑变量是否声明就可以直接使用。
因此,下面的代码是等价的
var age = 12
//等价于
name = 12
var name
下面的函数也是等价的
function bar() {
var age = 12
}
//等价于
function bar() {
var age
age = 12
}
通过在变量声明之前使用变量,可以验证变量确实被提升了。这样一来,提前使用变量意味着会输出undefined
,而不是Reference Error
console.log(age) //undefined
var age = 12
function bar() {
console.log(age) //undefined
var age = 12
}
1.7.2.2 不使用var会声明全局变量
==另外,如果不使用var声明变量,那么变量就会变成全局变量,任何地方都可以访问到,前面说过,全局变量在任何作用域都能访问,所以就会有如下现象:==
function bar() {
name = 12 //name此时是全局变量
console.log(name)
}
function foo() {
console.log('这是全局下的变量:' + name) //可以访问到name
}
console.log(name) //>> 12
bar() //>> 12
foo() // >> 这是全局下的变量:12
!!!切记,任何时候都不应该在函数内部声明全局变量,这样会造成不可预估的错误,如果需要全局变量,请使用var关键字在所有代码之前声明
1.7.2.3 var不遵循块级作用域
在块级作用域里使用var声明的变量,在块外面可以访问
if(true){
let a = 0
var b = 0
}
console.log(a) //>> a未定义
console.log(b) //>> 0 可以访问
while(1) {
let c = 0
var d = 0
}
console.log(c) //>> d未定义
console.log(d) //>> 0 可以访问
1.7.3 使用let声明变量
ES6新增的let关键字跟var很相似,都可以用来声明变量,大部分时候,它们的作用都是相同的,但也存在着一些差异。
let a = 1
var b = 1
var add = function(){}
let sum = function(){}
1.7.3.1 let遵循块级作用域
在块级作用域里使用let声明的变量,在块外面不可以访问
if(true){
let a = 0
}
console.log(a) //>> a未定义
while(true){
let b = 0
}
console.log(b) //>> b未定义
//函数的花括号也是块级作用域的一种,但一般我们称之为函数作用域,因为在函数作用域中使用var声明的变量,外界也是无法访问
function foo(){
let c = 0
}
console.log(c) //>> c未定义,没什么奇怪的,使用var声明也会报错,因为c属于函数作用域
//这不是声明的对象,而是一个对立的块,ES6新增的语法
// JavaScript引擎会根据里面的内容识别解析它
{
let d = 0
}
console.log(d) //>> d未定义
1.7.3.2 let没有变量提升
var不同的是,let声明的变量不会“提升”:
console.log(a) //>> undefined
console.log(b) //>> Uncaught ReferenceError: b is not defined
var a = 10
let b = 10
1.7.3.3 let不能重复声明变量
另一个不同的地方是,var可以重复声明变量,重复的var声明会被忽略,而let不可以,重复声明会报错:
var a
var a
{
let b
let b //>> Uncaught SyntaxError: Identifier 'b' has already been declared
}
1.7.4 使用const声明常量
ES6还增加了关键字const,用来声明常量。 const声明常量的同时必须赋值,此外,一旦声明,其值就无法更改。 除了这些,const有let的所有特性,比如块级作用域、没有变量提升、无法重复声明
1.7.4.1 const声明常量时必须赋值
const a //>> Uncaught SyntaxError: Missing initializer in const declaration
console.log(a)
定义了常量a,但没有赋值,报错
1.7.4.2 const声明的常量无法重新赋值
const b = 1
b = 2 //>> Uncaught TypeError: Assignment to constant variable.
定义了常量b = 1,修改为2,报错,因为常量是无法重新赋值。 但是对于引用类型的常量,是可以更改属性的值,但不能重新赋值,因为重新赋值就是在堆内存中新开辟存储空间:
const c = {
name: '皮卡丘'
}
c.name = '猪猪'
console.log(c.name) //>> 猪猪
//重新赋值(覆盖)会报错
c = { //>> Uncaught TypeError: Assignment to constant variable.
name: '猪猪'
}
如果想让对象的属性都不能修改,可以使用Object.freeze方法,来冻结对象,这样再修改属性值时,不会报错,但会默认失败:
const c = Object.freeze({ name: '皮卡丘' })
c.name = '猪猪'
console.log(c.name) //>> 皮卡丘
1.8 总结
JavaScript的变量可以保存2中数据类型的值:基本类型和引用类型 基本类型包括
Undefined、Null、Boolean、Number、String
和Symbol
基本类型保存在栈内存上 引用类型保存在堆内存上
基本类型复制是直接创建一个新的副本 引用类型复制是复制的指针,而不是对象本身
函数参数的复制由函数体内部决定,是直接修改还是重新赋值
typeof 可以确定基本类型的数据类型,null除外 instanceof 用于判断变量是不是给定引用类型的实例 Object.prototype.toString.call 可以精准判断所有的数据类型
浅拷贝只能拷贝基本类型的值,对于嵌套的引用类型,拷贝的依然是地址 深拷贝无论是基本类型还是引用类型,拷贝的都是具体值
作用域分为全局作用域和局部作用域 局部作用域可以访问全局作用域的变量,反过来不行 局部作用域又分为函数作用域和块级作用域 块级作用域只有在使用let和const时才有效
var声明的变量会提升到当前作用域的代码顶部 var声明的变量没有块级作用域的限制
let声明的变量不会提升 let不能重复声明变量 let声明的变量有块级作用域的限制
const用来声明常量 const声明常量时必须赋值 const声明的常量值无法更改,但如果是引用类型的常量,可以更改值的属性 const遵循let的所有规则
另外,关于堆和栈的区别总结如下:
栈(stack)中主要存放一些基本类型的变量和对象的引用, 其优势是存取速度比堆要快,并且栈内的数据可以共享,但缺点是存在栈中的数据大小与生存期必须是确定的,缺乏灵活性;
栈内存中为这个变量分配内存空间,当超过变量的作用域后,JS 会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。
堆(heap)用于复杂数据类型(引用类型)分配空间,例如数组对象、object 对象;它们的大小可能随时改变,是不确定的,运行时动态分配内存空间的,因此存取速度较慢。
堆内存中分配的内存需要程序员手动释放,如果不释放,而系统内存管理器又不自动回收这些堆内存的话动态分配堆内存,那就一直被占用。