持续对红宝书进行输出(一)

518 阅读10分钟

1. 第四章 变量、作用域

目标:

  • 通过变量使用原始值与引用值
  • 执行上下文
  • 理解垃圾回收

1.1 原始值和引用值

  1. ECMAScript变量可以包含两种不同类型的数据:原始值引用值
    1. 原始值(primitive value)就是最简单的数据
    2. 引用值(reference value)则是由多个值构成的对象
  2. 在把一个值赋给变量时,JavaScript引擎必须确定这个值是原始值还是引用值。保存原始值的变量是按值访问的。因为操作的就是存储在变量中的实际值。
    1. 原始值: undefined null boolean number string symbol
  3. 引用值是保存在内存中的对象,在操作对象时,实际上操作的是对该对象的引用(reference)而非实际的对象本身。因此保存引用值的变量是按引用访问的。

1.1.1 动态属性

  1. 原始值和引用值的定义方式很类似,都是创建一个变量,然后给它赋一个值。不过,在变量保存了这个值之后,可以对这个值做什么,则大不同。对于引用值而言,可以随时添加、修改、删除其属性和方法。
    let person = new Object()
    person.name = 'Nicholas'
    console.log(person.name) // Nicholas
    /**
    首先创建了一个对象,并把它保存在变量person中,然后,给这个对象添加了一个名为name的属性,并给这个属性赋值了一个"Nicholas",之后就可以访问到这个属性,知道对象被销毁或属性被显示的移除。
    */ 
  1. 原始值不能有属性,尽管尝试给原始值添加属性不会报错。
let name = "Nicholas"
name.age = 23
console.log(name.age) // undefined
// 只有引用值可以动态添加后面可以使用的属性
  1. 原始类型的初始化可以只使用字面量形式。如果使用new关键字,则JavaScript会创建一个Object类型的实例,但其行为是原始值。
let name1 = "Nicholas"
let name2 = new String("Matt")
name1.age = 27
name2.age = 26
console.log(name1.age) // undefined
console.log(name2.age) // 26
console.log( typeof name1 ) // string
console.log( typeof name2 ) // Object

1.1.2 复制值

  1. 除了存储方式不同,原始值和引用值在通过变量复制时也有所不同。
let num1 = 5
let num2 = num1
// 当num2初始化为num1时,num2也会得到数值5,这个值跟num1的5是独立的
复制前的变量对象
--
--
num1(Number类型)
复制后的变量对象
--
num2(Number类型)
num1(Number类型)
  1. 把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置,区别在于,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。操作完成之后,两个变量实际上指向同一个对象
let obj1 = new Object()
let obj2 = obj1
obj1.name = "Nicholas"
console.log(obj2.name) // "Nicholas"

1.1.3 传递参数

  1. ECMAScript中所有函数的参数都是按值进行传递的。在按值传递参数时,值会被复制到一个局部变量中(即一个命名参数,或者用js的话说,就是arguments对象中的一个槽位)。在按引用传递参数时,值在内存中的位置会被保存在一个局部变量,这意味这对本地变量的修改会反映到函数外部。(而这在ECMAScript中是不可能的)
function addTen(num){
    num += 10
    return num
}
let count = 20
let result = addTen(count)
console.log(result) // 30
console.log(count) // 20
/**
 这里,函数addTen()有一个参数num,它其实一个局部变量,在函数调用时,将变量count作为函数的参数传入
 count的这个值被复制到参数num以便在函数addTen()内部使用,在函数内部这个值+10,但不会影响函数外部
 的原始变量count。参数num和变量count是互不干扰的,只不过碰巧保存了一样的值。
*/
  1. 在上述的例子中,如果变量中传递的是对象,就没有那么清楚了 eg:
function setName(obj){
    obj.name = "Nicholas"
}
let person = new Person()
setName(person)
console.log(person.name) // "Nicholas"
/**
 这里创建了一个对象并把它保存在变量person中,将这个变量传递给函数setName()方法,并被复制到参数obj中,
 在函数内部,obj和person都指向同一个对象。结果就是,即使对象是按值进行传递给函数的,obj也会通过引用访问
 对象。当函数内部给obj设置了name属性时,函数外部的这个对象也会反应这个变量,因为obj指向的对象保存在全局
 作用域的堆内存上。
*/
  1. 为了证明对象是按值进行传递的,在将上面的例子进行修改。eg:
function setName(obj){
    obj.name = "Nicholas"
    obj = new Object()
    obj.name = "Matt"
}
let person = new Object()
setName(person)
console.log(person.name) // "Nicholas"
/**
  这次,在函数内部,将obj重新定义为一个有着不同name的新对象。当person传入到函数setName作为参数时,其中name属性被设置为"Nicholas"。然后obj被设置为一个新的对象且name属性被黄色至为"Matt",如果person是按引用进行传递的,那么person的属性name的值会被修改为"Matt"(自动将指针改为指向name为"Matt"的对象),可是再次访问person.name时,它的值是"Nicholas",这表明函数中的参数的值改变之后,原始的引用仍然没变。当obj在函数内部被重写时,它变成了一个指向本地对象的指针。 而本地对象在函数执行完成之后就被销毁了
*/
  • 注意:ECMAScript中函数的参数就是局部变量

1.1.4 确定类型

  1. typeof操作符最适合用来判断一个变量是否为原始类型,也就是判断一个变量是否为string,number,boolean,undefined的最好的方式。
  2. 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 )
// 如果变量是给定的引用类型的实例,则instanceof返回true
  1. 按照定义,所有引用值都是Object的实例,因此通过instanceof操作符检测任何引用值和Object构造函数都会返回true。类似的,使用instanceof检测原始值,则始终返回false

1.2 执行上下文与作用域

  1. 变量或函数的上下文决定了它们可以访问那些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object),而这个上下文中定义的所有变量和函数都存在于这个对象上。
  2. 全局上下文是最外层的上下文。在浏览器中,全局上下文就是window对象。
  3. 上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的变量和函数.(全局上下文在应用程序退出前才会被销毁,比如关闭浏览器)
  4. 每个函数都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文执行栈上,在函数执行完之后,上下文栈会弹出该函数的执行上下文,将控制权返还给之前的执行上下文。 即程序中的执行流程就是通过这个执行上下文栈控制的。
  5. 上下文中的代码在执行时,会创建变量对象的作用域链(scope chain) 这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域链的最前端。
  6. 代码执行时的标识符解析是通过沿作用域链逐级进行搜索标识符名称完成的。搜索的过程通常从作用域链的最前端开始,然后逐级往后,一直找到标识符。如果没有标识符,通常就会报错了。
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
        // 这里可以访问color anotherColor tempColor
    }
    // 这里可以访问 anotherColor color
    swapColors() 
}
// 这里只能访问anotherColor
changeColor()
/**
    代码涉及3个上下文:全局上下文、changeColor()的局部上下文、swapColors()的局部上下文
    内部上下文可以通过作用域链访问外部上下文中的一切,但外部上下文无法访问内部上下文中的任何东西
    上下文之间的连接是线性的、有序的。
*/
  • 函数参数被认为是当前上下文中的变量,因此也跟上下文中的变量遵循相同的访问规则。

1.2.1 作用域链增强

  1. 虽然执行上下文主要有全局上下文和函数上下文两种,eval()调用内部存在第三种上下文。但是有其他方式来增强作用域链。添加的上下文对象在这个上下文代码执行完后删除。
  2. 通常在两种情况下会出现这个现象,即try/catch语句的catch块和with语句
    1. 在这两种情况下,都会在作用域前端添加一个变量对象。对于with语句来说,会向作用域链前端添加指定对象
    2. 对于catch语句而言,则会创建一个新的变量对象,这个变量对象 会包含要抛出的错误对象的声明。
function buildUrl(){
    let qs = "?debug=true
with(location){
    let url = href + qs
}
return urld
}

1.2.2 变量声明

  1. 使用var的函数作用域声明
    1. 在使用var声明变量时,变量会被自动加到最接近的上下文。
    2. 如果变量未经声明就被初始化了,那么它就会自动添加到全局上下文中
function add(num1,num2){
    sum = num1 + num2
    return sum
}
let result = add(10,20)
console.log(sum) // 30
  1. 使用let的块级作用域声明
    1. 块级作用域由最近的一对花括号{}界定
    2. let在同一作用域内不能声明两次
    3. 不能提升
    4. 暂时性死区
  2. 使用const的常量声明
    1. 使用const声明的变量必须同时初始化,一经声明,在其生命周期的任何时候都不能重新赋予新值
    2. 赋值为对象的const变量不能被重新赋值为其他引用值,但对象的键则不受限制
const obj = {}
obj.name = "Nicholas"
console.log(obje.name) // "Nicholas"
  • 如果想让整个对象都不能被修改,可以使用Object.freeze(),这样在给属性赋值时虽然不会报错,但会默认失败
const obj = Object.freeze({})
obj.name = "Nicholas"
console.log(obj.name) // undefined

1.3 垃圾回收

  • 在浏览器发展史上,用到过两种主要的标记策略: 标记清理和引用计数
  1. 标记清理
    1. mark-and-sweep 当变量进入上下文,比如在函数内部声明一个变量时,这个变量会被加上存在于上下文中的标记。当变量离开上下文时,也会被加上离开上下文的标记。
    2. 垃圾回收程序运行的时候,会标记内存中存储的所有变量,然后,他会将所有在上下文中的变量,以及被上下文中的变量引用的变量的标记去掉。在之后在被加上标记的变量就是待删除的了。最后垃圾回收程序做一次内存清理。
  2. 引用计数
    1. 对每个值都记录它引用的次数。比如声明变量并给他赋予一个引用值时,这个值的引用数为1,当引用数为0时,就说明没办法在访问这个值了,因此就需要回收了。