第四章 4.1 4.2
    4.1 原始值与引用值
    ECMAScript变量可以包含两种不同类型的数据:原始值和引用值。
    原始值(primitive value)就是最简单的数据,引用值(reference value)则是由多个值构成的对象。
    在把一个值赋给变量时,JavaScript引擎必须确定这个值是原始值还是引用值。
    上一章讨论了6种原始值:Undefined、Null、Boolean、Number、String和Symbol。
    保存原始值的变量是按值(byvalue)访问的,因为我们操作的就是存储在变量中的实际值。
    引用值是保存在内存中的对象。与其他语言不同,JavaScript不允许直接访问内存位置,因此也就不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的引用(reference)而非实际的对象本身。为此,保存引用值的变量是按引用(by reference)访问的。
    4.1.1 动态属性
    原始值和引用值的定义方式很类似,都是创建一个变量,然后给它赋一个值。
    在变量保存了这个值之后,可以对这个值做什么,则大有不同。
    对于引用值而言,可以随时添加、修改和删除其属性和方法
    4.1.2 复制值
    除了存储方式不同,原始值和引用值在通过变量复制时也有所不同。
    在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置。这两个变量可以独立使用,互不干扰
    在把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。
    区别在于,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。操作完成后,两个变量实际上指向同一个对象,因此一个对象上面的变化会在另一个对象上反映出来
    4.1.3 传递参数
    ECMAScript中所有函数的参数都是按值传递的。
    这意味着函数外的值会被复制到函数内部的参数中,就像从一个变量复制到另一个变量一样。
    如果是原始值,那么就跟原始值变量的复制一样,如果是引用值,那么就跟引用值变量的复制一样。
    为证明对象是按值传递的,例子:
    image.png
    这个例子将obj重新定义为一个有着不同name的新对象。当person传入setName()时,其name属性被设置为”Nicholas”。然后变量obj被设置为一个新对象且name属性被设置为”Greg”。
    如果person是按引用传递的,那么person应该自动将指针改为指向name为”Greg”的对象。可是,当我们再次访问person.name时,它的值是”Nicholas”。
    这表明函数中参数的值改变之后,原始的引用仍然没变。当obj在函数内部被重写时,它变成了一个指向本地对象的指针。而那个本地对象在函数执行结束时就被销毁了。
    注: ECMAScript中函数的参数就是局部变量。
    4.1.4 确定类型
    typeof操作符最适合用来判断一个变量是否为原始类型。更确切地说,它是判断一个变量是否为字符串、数值、布尔值或undefined的最好方式。
    如果值是对象或null,那么typeof返回”object”
    instanceof操作符
    image.png
    4.2 执行上下文与作用域
    执行上下文(以下简称“上下文”)的概念在JavaScript中是颇为重要的。变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variableobject),而这个上下文中定义的所有变量和函数都存在于这个对象上。
    全局上下文是最外层的上下文。根据ECMAScript实现的宿主环境,表示全局上下文的对象可能不一样。在浏览器中,全局上下文就是我们常说的window对象(第12章会详细介绍),因此所有通过var定义的全局变量和函数都会成为window对象的属性和方法。
    使用let和const的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。
    上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)。
    上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。
    代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。搜索过程始终从作用域链的最前端开始,然后逐级往后,直到找到标识符。(如果没有找到标识符,那么通常会报错。)
    4.2.1 作用域链增强
    通常在两种情况下会出现这个现象,即代码执行到下面任意一种情况时:
    ❑ try/catch语句的catch块
    ❑ with语句
    这两种情况下,都会在作用域链前端添加一个变量对象。
    对with语句来说,会向作用域链前端添加指定的对象;
    对catch语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明。
    例子:
    image.png
    with语句将location对象作为上下文,因此location会被添加到作用域链前端。
    buildUrl()函数中定义了一个变量qs。
    当with语句中的代码引用变量href时,实际上引用的是location.href,也就是自己变量对象的属性。
    在引用qs时,引用的则是定义在buildUrl()中的那个变量,它定义在函数上下文的变量对象上。
    而在with语句中使用var声明的变量url会成为函数上下文的一部分,可以作为函数的值被返回;
    但像这里使用let声明的变量url,因为被限制在块级作用域(稍后介绍),所以在with块之外没有定义。
    4.2.2 变量声明
    1.使用var的函数作用域声明
    在使用var声明变量时,变量会被自动添加到最接近的上下文。
    如果变量未经声明就被初始化了,那么它就会自动被添加到全局上下文
    2.使用let的块级作用域声明
    ES6新增的let关键字跟var很相似,但它的作用域是块级的,这也是JavaScript中的新概念。
    块级作用域由最近的一对包含花括号{}界定。换句话说,if块、while块、function块,甚至连单独的块也是let声明变量的作用域。
    let与var的另一个不同之处是在同一作用域内不能声明两次。重复的var声明会被忽略,而重复的let声明会抛出SyntaxError
    let的行为非常适合在循环中声明迭代变量。使用var声明的迭代变量会泄漏到循环外部,这种情况应该避免。
    例子:
    image.png
    严格来讲,let在JavaScript运行时中也会被提升,但由于“暂时性死区”(temporal dead zone)的缘故,实际上不能在声明之前使用let变量。
    3.使用const的常量声明
    除了let, ES6同时还增加了const关键字。使用const声明的变量必须同时初始化为某个值。一经声明,在其生命周期的任何时候都不能再重新赋予新值。
    如果想让整个对象都不能修改,可以使用Object.freeze()
    4.标识符查找
    搜索开始于作用域链前端,以给定的名称搜索对应的标识符。如果在局部上下文中找到该标识符,则搜索停止,变量确定;如果没有找到变量名,则继续沿作用域链搜索。
    (注意,作用域链中的对象也有一个原型链,因此搜索可能涉及每个对象的原型链。)
    这个过程一直持续到搜索至全局上下文的变量对象。如果仍然没有找到标识符,则说明其未声明。
    注:标识符查找并非没有代价。访问局部变量比访问全局变量要快,因为不用切换作用域。
    不过,JavaScript引擎在优化标识符查找上做了很多工作,将来这个差异可能就微不足道了。