《JavaScript高级程序设计第4版》第4章-变量、作用域与内存学习笔记

变量、作用域与内存

本文是作者在阅读《JavaScript高级程序设计第4版》的读书笔记。

1.1 原始值和引用值

在JavaScript中有六种原始数据类型:Undefined、Null、String、Boolean、Number、Symbol,保存他们的变量都是按值访问的,也就是它们是存储在栈当中的,我们操作的是实际的值。

引用值是保存在内存中的对象。JavaScript 不允许直接访问内存位置,不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的引用(reference)而非实际的对象本身。为此,保存引用值的变量是按引用(by reference)访问的。

也就是说我们只能访问的是存储在存储在栈内存中的地址,而不能真正访问到其地址所对应的堆内存中的对象。

注意:在JavaScript这门语言中String并不是引用,而是原始值。

1.1.1 动态属性

对于引用值来说,可以动态的为其添加属性。原始值能动态的添加属性,但也不会报错,会输出undefined。

对于引用值

let person = new Object();
person.name = 'fjx';
console.log(person.name);// fjx

对于引用值

let a = 1;
a.name = "fjx";
console.log(a.name);//undefined

只有引用值可以动态添加后面可以使用的属性。

原始类型的初始化可以只使用原始字面量形式。如果使用的是 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 复制值

原始值和引用值除了在存储上有差距外,它们在赋值上也是有差距的。

对于向Number等原始值来说,它赋值就是把原来的值赋值到另外一个新副本中,它们两个是完全独立的。

image-20211121145234531

通过上图我们可以看到,是这样的,它们是完全互相独立的。下面我们使用代码来验证:

let num = 1;
let num1 = num;
num = 2;
console.log(num,num1);// 2 1

程序的输出的是2 1,这也就意味着num的改变不会改变num1的值,它们的完全互相独立的。

突然想到一个问题,既然Number是原始值,那么它为什么会有方法呢?通过查阅资料后续会有解释为什么。大概是包装基本类型。

引用值从一个变量赋给另一个变量,存储在变量中的值也会被复制到新变量所在的位置。区别在于,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。操作完成后,两个变量实际 上指向同一个对象,因此一个对象上面的变化会在另一个对象上反映出来,

image-20211121150515748

代码验证

let person = new Object();
person.name = 'fjx';
let t = person;
person.name = '123';
console.log(person.name,t.name);//123 123

这充分说明了,它们指向的是同一个对象。

1.1.3 传递参数

记住一句话:在JavaScript中所有的函数传参都是按值传递的。

在按值传递参数时,值会被复制到一个局部变量(即一个命名参数,或者用 ECMAScript 的话说, 就是 arguments 对象中的一个槽位)。在按引用传递参数时,值在内存中的位置会被保存在一个局部变 量,这意味着对本地变量的修改会反映到函数外部。

如果是原始值,那么就跟原始值变量的复制一样,如果是 引用值,那么就跟引用值变量的复制一样。

只需要记住一句话就可以解决所有的问题。下面通过代码来说明。

对于原始值来说:

let num = 1;
function f(num){
    num++;
    console.log(num);//2
}
f(num);
console.log(num);//1

这完全可以体现出原始值就是按照函数式按值传递的规则来传递参数的。

那么对于引用值呢?很多人都认为引用在传递参数的时候是按照引用来传递的其实不然,它也是安装值来传递的。代码验证

function setName(obj){
    obj.name = 'fjx';
}
let person = new Object();
person.name = 'hh';
setName(person);
console.log(person.name);// fjx

可以看到输出的是fjx所以很多人都以为是按照引用传递的参数,这里只是相当于在函数内部创建了一个新的地址,然后使用这个地址的引用访问到对象中的属性。那么再举一个例子,就可以看出引用类型也是按照值来传递参数的了。

let person = new Object();
person.name = 'fjx';
function setName(obj){
    obj.name = 't';
    obj = new Object();
    obj.name = 'sad';
}
setName(person);
console.log(person.name);//t

结果是明显的,输出的是t,如果真的是按照引用传递参数的话,那么此时的person应该指向的一个新的对象,它name属性的值应该是sad,所以这很明显可以看出是按值传递参数,所以把person 的地址传递给obj,obj在按照这个地址引用到堆内存中的对象,所以这是可以改变person的属性的,但是如果对obj重新赋值,那么它将丢失对person对象的引用,所以再也不误改变外部person的值。

如果是按照引用传递参数的话,那么外部的person的name应该是sad,可惜不是的。

1.1.4 确定类型

使用typeof操作符是确定一个变量是否是原始值的最佳方案。其实也是有一定的范围的,对于

  • undefined
  • 字符串
  • 数值
  • 布尔值

来说判断是很准确的。

let a = 1;
let b = undefined;
let c = "123";
let d = true;
console.log(typeof a);
console.log(typeof b);
console.log(typeof c);
console.log(typeof d);
/*number
undefined
string
boolean*/	

但是对于原始值null或者对象(引用)来说,typeof就不那么好用了。

let a = null;
let b = new Object();
console.log(typeof a);//object
console.log(typeof b);//object

他们的结果都是object

**typeof 虽然对原始值很有用,但它对引用值的用处不大。我们通常不关心一个值是不是对象, 而是想知道它是什么类型的对象。**为了解决这个问题,ECMAScript 提供了 instanceof 操作符。

语法res = var intanceof constructor

如果变量是给定的引用类型(通过原型链来确定,第8章详讲)

function Person(){
    
}
let person = new Person();
let x = new Array();
let y = new RegExp();
console.log(person instanceof Object);// true
console.log(person instanceof Person);//true
console.log(x instanceof Array);// true
console.log(y instanceof RegExp);//true

可以看到,person是Object的实例,也是Person的实例。

按照定义,所有引用值都是 Object 的实例,因此通过 instanceof 操作符检测任何引用值和 Object 构造函数都会返回 true。类似地,如果用 instanceof 检测原始值,则始终会返回 false, 因为原始值不是对象。

image-20211121161016989

1.2 执行上下文与作用域

在本书中说明了执行上下文就是作用域。

理解JavaScript的执行上下文 - 知乎 (zhihu.com)

指向上下文主要是全局上下文、函数上下文和块级作用域,但还是有eval()调用的内部存在第三种上下文。

变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每个执行上下文(简称为上下文)都有一个关联的变量对象(variable object), 而这个上下文中定义的所有变量和函数都存在于这个对象上。

image-20211121173206631

其实就是一个大环境。

虽然代码无法访问这个变量对象,但是后台处理数据的时候会用到。

**全局上下文就是最外层的上下文,根据运行的JavaScript的环境不同,那么全局上下文的变量对象也是不相同的。**在浏览器中,全局上下文就是window对象,虽有通过var声明的全局变量和函数都会称为window对象的属性和方法。使用let和const声明的顶级变量不会称为window的属性。

对于运行在nodejs中的JavaScript代码来说,它的全局上下文就不是window了。

上下文在其所以的代码被执行完之后会被销毁(但是闭包可以使其保留),全局上下文是在浏览器关闭,或者关闭网页的时候被销毁。

每个函数都有自己的上下文,当代码执行进入函数时,函数的上下文会被压入到一个上下文栈,当函数执行完成后,该上下文就会被弹出栈,将控制权交给之前的上下文,这就是ECMASript来控制代码执行的方式。

代码执行流每进入一个新上下文,都会创建一个作用域链,用于搜索变量和函数。

作用域链:

image-20211121164845725

image-20211121164950814

对于作用域链来说,内部上下文可以访问外部上下文的一切,但是外部上下文不能访问内部上下文,也就是说作用域链的搜索顺序是至下而上的,直到搜索到全局上下文。对于函数的参数来说,它们是属性这个函数的上下文中,存储在arguments对象中。

1.2.1 作用域链增强

try/cath 语句的cath 和 with语句 会增加上下文

1.2.2 变量声明

直到ES5.1之前都是使用的var关键字来声明变量的,到了ES6新增了letcosnt两个关键字。

一、使用var声明变量

使用var声明变量,它会自动添加到最近的上下文的变量对象中,如果变量未经声明就被使用,那么它会被自动添加到全局上下文中。

但是不声明变量直接使用的方法及其的错误,在严格模式下时会报错的。千万不要使用这个方法。

使用var来声明变量还会造成变量提升,也就是说这个变量会被拿到函数或者全局作用域的最顶部,位于所有的代码之前。

image-20211121170508343

这个打印变量不会报错,而是有可能会吃下undefined,看打印的位置在哪里。

还有一点全局作用域下的var声明的变量会自动称为window的属性,严格模式下也是如此。

ES6 新增的 let 关键字跟 var 很相似,但它的作用域是块级的,这也是 JavaScript 中的新概念。块 级作用域由最近的一对包含花括号{}界定。换句话说,if 块、while 块、function 块,甚至连单独 的块也是 let 声明变量的作用域。

let 和 const 在同一作用域下不能重复声明同一个变量。

let 的行为非常适合在循环中声明迭代变量。使用 var 声明的迭代变量会泄漏到循环外部,这种情 况应该避免。

使用 const 声明的变量必须同时初始化为某个值。 一经声明,在其生命周期的任何时候都不能再重新赋予新值。赋值为对象的 const 变量不能再被重新赋值为其他引用值,但对象的键则不受限制。也就是说对象的属性和方法是可以被改变的。

于const 声明暗示变量的值是单一类型且不可修改,JavaScript 运行时编译器可以将其所有实例 都替换成实际的值,而不会通过查询表进行变量查找。谷歌的 V8 引擎就执行这种优化。

开发实践表明,如果开发流程并不会因此而受很大影响,就应该尽可能地多使用 const 声明,除非确实需要一个将来会重新赋值的变量。这样也可以优化代码的执行速度。

var、let和const的区别

  1. var声明的变量(变量提升)在浏览器中会成为window的属性,严格模式下也是如此
  2. let、const不存在变量提升,而且存在暂时性死区,也就是说早代码运行到初始化那个变量的语句时,是不能访问该变量的。
  3. 它们的作用域不同,var的作用域相当于是函数作用域,它会自动添加到最近的上下文中,而let、const 的作用域是块级作用域。
  4. var运行重复定义,let、const不行
  5. const声明的值是不允许改变的,必须初始化,但是对象的属性和方法是可以改变的。
  6. 尽量使用const,可以优化代码。
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值