欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JavaScript高级程序设计第四版学习--第四章

程序员文章站 2022-07-07 18:54:53
...

title: JavaScript高级程序设计第四版学习–第四章
date: 2021-5-16 20:57:45
author: Xilong88
tags: JavaScript

本章内容
通过变量使用原始值与引用值
理解执行上下文
理解垃圾回收
可能出现的面试题:
1、原始值和引用值的区别
2、参数传值是什么方式?
3、了解过执行上下文吗?
4、了解过作用域链吗?
5、了解过作用域链增强吗?
6、如何判断对象类型?(这里提到有一种)
7、谈谈JavaScript垃圾回收机制
8、了解内存泄漏吗?如何规避?
9、性能优化?(这章里关于:隐藏类,常量池和静态分配)

知识点:

1.两种不同类型的数据:原始值和引用值。原
始值 (primitive value)就是最简单的数据,引用值 (reference value)
则是由多个值构成的对象。

2.保存原始值的变量是按值 (by
value)访问的,因为我们操作的就是存储在变量中的实际值。

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

4.原始值不能有属性,尽管尝试给原始值添加属性不会报错。比如:

let name = "Nicholas";
name.age = 27;
console.log(name.age);  // undefined

5.原始类型的初始化可以只使用原始字面量形式。如果使用的
是new 关键字,则JavaScript会创建一个Object 类型的实例,但其行为
类似原始值。

6.在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置,这两个变量可以独立使用,互不干扰

7.在把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复
制到新变量所在的位置。区别在于,这里复制的值实际上是一个指针,
它指向存储在堆内存中的对象。

8.参数都是按值传递的,在按值传递参数时,值会被复制到一个局部变量(即一个命名参数,或
者用ECMAScript的话说,就是arguments 对象中的一个槽位),就算是引用类型,也是传的引用的指针值,也是传值。

function setName(obj) {
  obj.name = "Nicholas";
  obj = new Object();
  obj.name = "Greg";
}
let person = new Object();
setName(person);
console.log(person.name);  // "Nicholas"

9.我们通常不关心一个值是不是对象,而是想知道它是什么类型的对象。用instanceof:

result = variable instanceof constructor
console.log(person instanceof Object);  // 变量person是Object吗?
console.log(colors instanceof Array);   // 变量colors是Array吗?
console.log(pattern instanceof RegExp); // 变量pattern是RegExp吗?

10.通过instanceof 操
作符检测任何引用值和Object 构造函数都会返回true

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

12.全局上下文是最外层的上下文,当代码执行流进入函数时,函数的上
下文被推到一个上下文栈上。在函数执行完之后,上下文栈会弹出该函
数上下文,将控制权返还给之前的执行上下文。ECMAScript程序的执
行流就是通过这个上下文栈进行控制的。

13.上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。

这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域链的最前端。

如果上下文是函数,则其活动对象(activation object)用作变量对象。活动对象最初只有一个定义变量:arguments 。(全局上下文中没有这个变量。)

作用域链中的下一个变量对象来自包含上下文,再下一个对象来自再下一个包含上下文。以此类推直至全局上下文;

全局上下文的变量对象始终是作用域链的最后一个变量对象。

代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。搜索过程始终从作用域链的最前端开始,然后逐级往后,直到找到标识符。(如果没有找到标识符,那么通常会报错。)

14.局部作用域中定义的变量可用于在局部上下文中替换全局变量。

var color = "blue";
function changeColor() {
  let anotherColor = "red";
  function swapColors() {
    let tempColor = anotherColor;
    anotherColor = color;
    color = tempColor;
    // 这里可以访问color、anotherColor和tempColor
  }
  // 这里可以访问color和anotherColor,但访问不到tempColor
  swapColors();
}
// 这里只能访问color
changeColor();

内部上下文可以通过作用域链访问
外部上下文中的一切,但外部上下文无法访问内部上下文中的任何东
西。上下文之间的连接是线性的、有序的。每个上下文都可以到上一级
上下文中去搜索变量和函数,但任何上下文都不能到下一级上下文中去
搜索

15.作用域链增强:

某些语句会导
致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被
删除。通常在两种情况下会出现这个现象,即代码执行到下面任意一种
情况时:

try /catch 语句的catch 块
with 语句

这两种情况下,都会在作用域链前端添加一个变量对象。对with 语句
来说,会向作用域链前端添加指定的对象;

function buildUrl() {
  let qs = "?debug=true";
  with(location){
    let url = href + qs;
  }
  return url;
}

对catch 语句而言,则会创
建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明。

16.在使用var 声明变量时,变量会被自动添加到最接近的上下文,假如不带var,会被添加到全局上下文。

未经声明而初始化变量是JavaScript编程中一个非常常见的错误,会导致很多问题。为此,读者在初始化变量之前一
定要先声明变量。

17.let作用域是块级的,并且有暂时性死区,const不能修改,const引用的属性和方法,可以修改,要想不修改,用Object。freeze();导致静默失败。

const o3 = Object.freeze({});
o3.name = 'Jake';
console.log(o3.name); // undefined

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

算一个小细节的性能优化

如果开发流程并不会因此而受很大影响,就应该尽可能地多使用const 声明,除非确实需要一个将来会重新赋值的变量。这样可以从根本上保证提前发现重新赋值导致的bug。

19.当在特定上下文中为读取或写入而引用一个标识符时,必须通过搜索确定这个标识符表示什么。

如果在局部上下文中找到该标识符,则搜
索停止,变量确定;如果没有找到变量名,则继续沿作用域链搜索。(注意,作用域链中的对象也有一个原型链,因此搜索可能涉
及每个对象的原型链。)

这个过程一直持续到搜索至全局上下文的
变量对象。如果仍然没有找到标识符,则说明其未声明。

20.使用块级作用域声明并不会改变搜索流程,但可以给词法层级添加
额外的层次:

var color = 'blue';
function getColor() {
  let color = 'red';
  {
    let color = 'green';
    return color;
  }
}
console.log(getColor()); // 'green'

在局部变量color 声明之后的任何代码都无法访问
全局变量color ,除非使用完全限定的写法window.color

21.JavaScript是使用垃圾回收的语言,也就是说执行环境负责在代码执行时
管理内存。

基本思路很简单:确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,
即垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收
集时间)就会自动运行。

标记清理和引用计数。

JavaScript最常用的垃圾回收策略是标记清理 (mark-and-sweep)。当变量进入上下文,比如在函数内部声明一个变量时,这个变量会被加上存在于上下文中的标记。

而在上下文中的变量,逻辑上讲,永远不应该释放它们的内存,因为只要上下文中的代码在运行,就有可能用到它们。

当变量离开上下文时,也会被加上离开上下文的标记。给变量加标记的方式有很多种。比如,当变量进入上下文时,反转某一位;或者可以维护“在上下文中”和“不在上下文中”两个变量列表,可以把变量从一个列表转移到另一个列表。标记过程的实现并不重要,关键是策略。

垃圾回收程序运行的时候,会标记内存中存储的所有变量(记住,标记方法有很多种)。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。

在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理 ,销毁带标记的所有值并收回它们的内存。

22.引用计数:被引用就引用数+1,被覆盖,引用数-1,然后为0时就被回收。

但是可能出现循环引用问题如:

function problem() {
  let objectA = new Object();
  let objectB = new Object();
  objectA.someOtherObject = objectB;
  objectB.anotherObject = objectA;
}

DOM和js对象的循环引用:

let element = document.getElementById("some_element");
let myObject = new Object();
myObject.element = element;
element.someObject = myObject;

为避免类似的循环引用问题,应该在确保不使用的情况下切断原生
JavaScript对象与DOM元素之间的连接。

myObject.element = null;
element.someObject = null;

把变量设置为null 实际上会切断变量与其之前引用值之间的关系。当下次垃圾回收程序运行时,这些值就会被删除,内存也会被回收。

23.频繁的回收垃圾,性能会被降低,所以我们要避免频繁的垃圾回收。

把变量赋值为null断开引用,下次回收时,引用会被回收,全部null

解除对一个值的引用并不会自动导致相关内存被回收。解
除引用的关键在于确保相关的值已经不在上下文里了,因此它在下次垃
圾回收时会被回收。

24.使用const和let有利于提升性能,因为他们的作用域更清晰,更能判断什么时候回收。

使用“隐藏类”也可以变量提升:

V8在将解释后的
JavaScript代码编译为实际的机器码时会利用“隐藏类”

如:

function Article() {
  this.title = 'Inauguration Ceremony Features Kazoo Band';
}
let a1 = new Article();
let a2 = new Article();

V8会在后台配置,让这两个类实例共享相同的隐藏类,因为这两
个实例共享同一个构造函数和原型。

a2.author = 'Jake';

此时两个Article 实例就会对应两个不同的隐藏类。根据这种操作
的频率和隐藏类的大小,这有可能对性能产生明显影响。

解决方案就是避免JavaScript的“先创建再补充”(ready-fire-
aim)式的动态属性赋值,并在构造函数中一次性声明所有属性,
如下所示:

function Article(opt_author) {
  this.title = 'Inauguration Ceremony Features Kazoo Band';
  this.author = opt_author;
}
let a1 = new Article();
let a2 = new Article('Jake');

同样不能用delect删除,用null,这样就可以继续共享隐藏类。

function Article() {
  this.title = 'Inauguration Ceremony Features Kazoo Band';
  this.author = 'Jake';
}
let a1 = new Article();
let a2 = new Article();
a1.author = null;

这也是个细节。

25.不合理的引用,导致内存泄漏,也就是有一块内存我们用不到,它也不释放,就浪费了内存。

方法里不带var声明变量,生成的全局变量,造成内存泄漏

定时器引用了一个变量,定时器不消失,变量就不释放。

闭包也会导致内存泄漏

26.针对23知识点,可以采用静态分配和常量池来解决频繁回收对象的问题。

也就是说时,垃圾回收会监控程序的对象回收情况,对象更新越频繁就会导致这个程序被反复进行内存回收。

采用静态分配也就是说,对象先创建好,要用的时候再分配。

如(伪实现):

// vectorPool是已有的对象池
let v1 = vectorPool.allocate();
let v2 = vectorPool.allocate();
let v3 = vectorPool.allocate();
v1.x = 10;
v1.y = 5;
v2.x = -3;
v2.y = -6;
addVector(v1, v2, v3);
console.log([v3.x, v3.y]); // [7, -1]
vectorPool.free(v1);
vectorPool.free(v2);
vectorPool.free(v3);
// 如果对象有属性引用了其他对象
// 则这里也需要把这些属性设置为null
v1 = null;
v2 = null;
v3 = null;

我们根据计算出来的大小,给对象池设一个大小,这样来减少垃圾回收,提高性能。

这个也是性能优化的一个细节。

这一章讲了作用域和内存的问题,在前端性能优化中也很关注,所以要多看。