1. 第四章 变量、作用域
目标:
- 通过变量使用原始值与引用值
- 执行上下文
- 理解垃圾回收
1.1 原始值和引用值
- ECMAScript变量可以包含两种不同类型的数据:
原始值和引用值
- 原始值(primitive value)就是最简单的数据
- 引用值(reference value)则是由多个值构成的对象
- 在把一个值赋给变量时,JavaScript引擎必须确定这个值是原始值还是引用值。保存原始值的变量是按值访问的。因为操作的就是存储在变量中的实际值。
- 原始值: undefined null boolean number string symbol
- 引用值是保存在内存中的对象,在操作对象时,实际上操作的是对该对象的引用(
reference)而非实际的对象本身。因此保存引用值的变量是按引用访问的。
1.1.1 动态属性
- 原始值和引用值的定义方式很类似,都是创建一个变量,然后给它赋一个值。不过,在变量保存了这个值之后,可以对这个值做什么,则大不同。对于引用值而言,可以随时添加、修改、删除其属性和方法。
let person = new Object()
person.name = 'Nicholas'
console.log(person.name)
- 原始值不能有属性,尽管尝试给原始值添加属性不会报错。
let name = "Nicholas"
name.age = 23
console.log(name.age)
- 原始类型的初始化可以只使用字面量形式。如果使用new关键字,则JavaScript会创建一个Object类型的实例,但其行为是原始值。
let name1 = "Nicholas"
let name2 = new String("Matt")
name1.age = 27
name2.age = 26
console.log(name1.age)
console.log(name2.age)
console.log( typeof name1 )
console.log( typeof name2 )
1.1.2 复制值
- 除了存储方式不同,原始值和引用值在通过变量复制时也有所不同。
let num1 = 5
let num2 = num1
| 复制前的变量对象 | |
|---|
| - | - |
| - | - |
| num1 | (Number类型) |
| 复制后的变量对象 | |
|---|
| - | - |
| num2 | (Number类型) |
| num1 | (Number类型) |
- 把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置,区别在于,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。操作完成之后,两个变量实际上指向同一个对象
let obj1 = new Object()
let obj2 = obj1
obj1.name = "Nicholas"
console.log(obj2.name)
1.1.3 传递参数
- ECMAScript中所有函数的参数都是按值进行传递的。在按值传递参数时,值会被复制到一个局部变量中
(即一个命名参数,或者用js的话说,就是arguments对象中的一个槽位)。在按引用传递参数时,值在内存中的位置会被保存在一个局部变量,这意味这对本地变量的修改会反映到函数外部。(而这在ECMAScript中是不可能的)
function addTen(num){
num += 10
return num
}
let count = 20
let result = addTen(count)
console.log(result)
console.log(count)
- 在上述的例子中,如果变量中传递的是对象,就没有那么清楚了
eg:
function setName(obj){
obj.name = "Nicholas"
}
let person = new Person()
setName(person)
console.log(person.name)
- 为了证明对象是按值进行传递的,在将上面的例子进行修改。eg:
function setName(obj){
obj.name = "Nicholas"
obj = new Object()
obj.name = "Matt"
}
let person = new Object()
setName(person)
console.log(person.name)
- 注意:ECMAScript中函数的参数就是局部变量
1.1.4 确定类型
- typeof操作符最适合用来判断一个变量是否为原始类型,也就是判断一个变量是否为string,number,boolean,undefined的最好的方式。
- typeof虽然对原始值判断有用,但对引用值的判断用处不太大,通常我们不关心一个值是不是对象,而是想知道它是什么类型的对象,所以可以使用instanceof操作符,
- typeof 检测函数时会返回一个
function
let a = "Nicholas"
let b
let c = true
let d = "abcd"
console.log( typeof a
console.log(person instanceof Object)
console.log( rex instanceof RegExp )
- 按照定义,所有引用值都是Object的实例,因此通过
instanceof操作符检测任何引用值和Object构造函数都会返回true。类似的,使用instanceof检测原始值,则始终返回false
1.2 执行上下文与作用域
- 变量或函数的上下文决定了它们可以访问那些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object),而这个上下文中定义的所有变量和函数都存在于这个对象上。
- 全局上下文是最外层的上下文。在浏览器中,全局上下文就是window对象。
- 上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的变量和函数.(全局上下文在应用程序退出前才会被销毁,比如关闭浏览器)
- 每个函数都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文执行栈上,在函数执行完之后,上下文栈会弹出该函数的执行上下文,将控制权返还给之前的执行上下文。 即程序中的执行流程就是通过这个执行上下文栈控制的。
- 上下文中的代码在执行时,会创建变量对象的作用域链(scope chain) 这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域链的最前端。
- 代码执行时的标识符解析是通过沿作用域链逐级进行搜索标识符名称完成的。搜索的过程通常从作用域链的最前端开始,然后逐级往后,一直找到标识符。如果没有标识符,通常就会报错了。
var color = "blue"
function changeColor(){
if(color == "blue){
color = "red"
}else {
color = "blue"
}
}
changeColor() // red
/**
这个例子,函数changeColor()的作用域链包含两个对象:一个是自己的变量对象,
另一个是全局上下文的变量对象。函数内部之所以能够访问变量color,是因为在作用域
中找到了变量color
**/
- 除此之外,布局作用域中定义的变量可用于在局部上下文替换这个全局变量
var color = "blue"
function changeColor(){
var anotherColor = "red"
function swapColors(){
var temColor = anotherColor
anotherColor = color
color = temColor
}
swapColors()
}
changeColor()
- 函数参数被认为是当前上下文中的变量,因此也跟上下文中的变量遵循相同的访问规则。
1.2.1 作用域链增强
- 虽然执行上下文主要有全局上下文和函数上下文两种,eval()调用内部存在第三种上下文。但是有其他方式来增强作用域链。添加的上下文对象在这个上下文代码执行完后删除。
- 通常在两种情况下会出现这个现象,即try/catch语句的catch块和with语句
- 在这两种情况下,都会在作用域前端添加一个变量对象。对于with语句来说,会向作用域链前端添加指定对象
- 对于catch语句而言,则会创建一个新的变量对象,这个变量对象 会包含要抛出的错误对象的声明。
function buildUrl(){
let qs = "?debug=true
with(location){
let url = href + qs
}
return urld
}
1.2.2 变量声明
- 使用var的函数作用域声明
- 在使用var声明变量时,变量会被自动加到最接近的上下文。
- 如果变量未经声明就被初始化了,那么它就会自动添加到全局上下文中
function add(num1,num2){
sum = num1 + num2
return sum
}
let result = add(10,20)
console.log(sum)
- 使用let的块级作用域声明
- 块级作用域由最近的一对花括号{}界定
- let在同一作用域内不能声明两次
- 不能提升
- 暂时性死区
- 使用const的常量声明
- 使用const声明的变量必须同时初始化,一经声明,在其生命周期的任何时候都不能重新赋予新值
- 赋值为对象的const变量不能被重新赋值为其他引用值,但对象的键则不受限制
const obj = {}
obj.name = "Nicholas"
console.log(obje.name)
- 如果想让整个对象都不能被修改,可以使用Object.freeze(),这样在给属性赋值时虽然不会报错,但会默认失败
const obj = Object.freeze({})
obj.name = "Nicholas"
console.log(obj.name)
1.3 垃圾回收
- 在浏览器发展史上,用到过两种主要的标记策略: 标记清理和引用计数
- 标记清理
- mark-and-sweep 当变量进入上下文,比如在函数内部声明一个变量时,这个变量会被加上存在于上下文中的标记。当变量离开上下文时,也会被加上离开上下文的标记。
- 垃圾回收程序运行的时候,会标记内存中存储的所有变量,然后,他会将所有在上下文中的变量,以及被上下文中的变量引用的变量的标记去掉。在之后在被加上标记的变量就是待删除的了。最后垃圾回收程序做一次内存清理。
- 引用计数
- 对每个值都记录它引用的次数。比如声明变量并给他赋予一个引用值时,这个值的引用数为1,当引用数为0时,就说明没办法在访问这个值了,因此就需要回收了。