JS高级程序设计阅读笔记(个人学习用)

152 阅读9分钟

第三章

第一节 语法

严格模式

"use strict"可以在脚本开头,也可以在函数体开头

语句结尾

不加分号的话,解析器会尝试在合适的位置补上分号纠正语法错误。所以会影响性能。

关键字和保留字

不能用作标识符或属性名

3.3 变量

var

声明变量但不初始化,默认值是undefined

改变变量值的类型合法,但不推荐

变量(声明)提升,但赋值初始化不会提升

多次用var声明同一个变量也合法

let

let声明的范围是块作用域,var声明的范围是函数作用域。全局、函数、代码块三者区分

let 也不允许同一个块作用域中出现冗余声明(声明同一个变量)。只要存在let就不行,不管是let先var后,还是var先let后

全局作用域中声明的变量不会成为window的属性

暂时性死区

在解析代码时,JavaScript 引擎也会注意出现在块后面的 let 声明,只不过在此之前不能以任何方

式来引用未声明的变量。在 let 声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此

阶段引用任何后面才声明的变量都会抛出 ReferenceError

const

const 的行为与 let 基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改 const 声明的变量会导致运行时错误。

注意const只会限制变量的改变,如果是一个对象则不被限制。

使用优先级 const > let > var

3.4 数据类型

typeof操作符

注意点:

  • 在ECMAScript中,函数被认为是对象,而不是另外一种数据类型。但是函数也有自己特殊的属性。为此,typeof可以区分函数和其他普通对象。
  • 调用typeof null 返回的是"object"。这是因为特殊值 null 被认为是一个对空对象的引用。更深层地理解,因为null和object的存储单元最后三位“识别符”都是000

undefined null

  • null值表示一个空指针
  • undefined不必要显式的赋值,只声明不初始化会自动赋值undefined。对于null,任何时候,只要变量要保存对象,而当时又没有那个对象可保存,就要用 null 来填充该变量。
  • Number()时,null返回0,undefined返回NaN

Number

第一个数字是0表示八进制。如果有非法数字(对于8进制,非法数字就是9)。则自动视为忽略0的十进制数字。

对于严格模式,前缀0o表示八进制。0x表示十六进制。

NaN

Not a Number. 用来表示数值的操作失败了。

  • 0、0相除返回NaN。但是分母为零,分子不为零,会返回 Infinity 或 -Infinity
  • NaN不等于NaN
  • NaN进行任何操作始终返回NaN

转数值类型的三个函数 Number(), parseInt(), parseFloat()

Null -> 0,

undefined -> NaN,

" " -> 0,

"000000011" => 11

  • parseInt() 最好始终传第二个参数,作为底数。
  • parseFloat() 不会识别特殊进制的前缀0。另外两个都可以识别

String

字符字面量,(转义字符)。前面加反斜杠 \ 。转义字符无论多长,在计算字符串长度时,都只算一个。

转字符串方法

  • x.toString()
  • String(x) . 如果x不是null或undefined,则调用x的toString()方法。否则返回"null","undefined"。

模版字面量 ES6

  • 可换行,也因此常用于定义 HTML 模板
  • 可使用插值表达式添加变量,或是可执行的js表达式
 let str = `生成一个随机数:${ Math.random() }`

标签函数 ES6

ES6新增用于和模版字符串配套使用。标签函数在调用时,除了普通的函数调用方式,还可以直接使用模版字符串调用

 tagFoo();//这是一个标签函数
 tagFoo`一个模板字符串`;//这是一个标签函数
 function tagFoo(templateStrings, ...insertVars) {
     console.log({ templateStrings, insertVars });
 }
 tagFoo`一个普通的模板字符串`; //{ templateStrings: [ '一个普通的模板字符串' ], insertVars: [] }
 tagFoo`一个有插值的模板字符串:${'var'}`; //{ templateStrings: [ '一个有插值的模板字符串:', '' ], insertVars: [ 'var' ] }
 tagFoo`一个有插值的模板字符串:${'var1'},${'var2'}`; //{ templateStrings: [ '一个有插值的模板字符串:', '-', '' ], insertVars: [ 'var1', 'var2' ] }

Symbol ES6

  • 作用:是确保对象属性使用唯一标识符,不会发生属性(命名)冲突的危险。
  • 作用举例:手写call, apply, bind方法
  • Symbol()函数不能与 new 关键字一起作为构造函数使用。
 let mySymbol = new Symbol(); // TypeError: Symbol is not a constructor

Object

“ECMAScript中的对象其实就是一组数据和功能的集合”

开发者可以通过创建 Object 类型的实例来创建自己的对象,然后再给对象添加属性和方法:

let o = new Object();

通过上述方法创建的object实例都有如下的属性和方法

  • constructor:指向创建该实例时使用的构造函数,也就是Object()
const o = new Object()
const o = {}
o.constructor //Object()const a = new Array()
const a = []
a.constructor //Array()const n = new Number()
n.constructor //Number()
  • hasOwnProperty(propertyName)用于判断当前对象实例上(不是原型)上是否存在给定的属性。要检查的属性名必须是字符串或符号
  • propertyIsEnumerable(propertyName)用于判断当前对象是否为另一个对象的原型。
  • toLocaleString()返回对象的字符串表示,和toString()返回的格式有部分区别:

juejin.cn/post/690536…

  • toString()返回对象的字符串表示
  • valueOf()返回对象对应的字符串、数值或布尔值表示。通常和toString()的返回值相同

3.5 操作符

一元操作符

前缀和后缀 递增/减 的区别

//这种情形没有区别
let num = 19
num++ //20
++num //20
​
//涉及和别的数据运算就会产生区别
let num1 = 2;
let num2 = 20;
let num3 = num1-- + num2; //22
let num4 = num1 + num2; //21
​
let num1 = 2;
let num2 = 20;
let num3 = --num1 + num2; // 21 
let num4 = num1 + num2; // 21 

位操作

32位二进制

ECMAScript 中的所有数值都以 IEEE 754 64 位格式存储,但位操作并不直接应用到 64 位表示,而是先把值转换为 32 位整数,再进行位操作,之后再把结果转换为 64 位。对开发者而言,就好像只有 32 位整数一样,因 为 64 位整数存储格式是不可见的。既然知道了这些,就只需要考虑 32 位整数即可。

二补数(补码)

负值的二进制运算方法--二补数(补码)

(1) 确定绝对值的二进制表示(如,对于18,先确定 18 的二进制表示);

(2) 找到数值的一补数(或反码),换句话说,就是每个 0 都变成 1,每个 1 都变成 0;

(3) 给结果加 1。

位操作符号

  • 按位非 ~

操作就是对数值取反并减1.(也就是补码只做(1)(2)步)。

应用:比普通的操作更快

console.time("普通取反并减一")
let num = 25
let num2 = -num - 1
console.log(num2); //-26
console.timeEnd("普通取反并减一")
​
console.time("按位非取反并减一")
let n = 25
let n2 = ~n
console.log(n2) //-26
console.timeEnd("按位非取反并减一")
​
//普通取反并减一: 0.005126953125 ms
//按位非取反并减一: 0.008056640625 ms
  • 按位与&

将两个数的每一位对齐,然后基于与逻辑计算0、1. 两位都是1结果为1,其他结果都是0.

let result = 25 & 3;
console.log(result); // 1

25 = 0000 0000 0000 0000 0000 0000 0001 1001

3 = 0000 0000 0000 0000 0000 0000 0000 0011


AND = 0000 0000 0000 0000 0000 0000 0000 0001

  • 按位或|

和按位与类似。有任意一位为

  • 按位异或^

只在一位上是1的时候返回1(两位都是1或0则返回0)

  • 左移<<
let oldValue = 2; // 等于二进制 10
let newValue = oldValue << 5; // 等于二进制 1000000,即十进制 64 

注意,左移会保留它所操作数值的符号。比如,如果2 左移 5 位,将得到64,而不是正 64。

  • 有符号右移>>

实际上是左移的逆运算

  • 无符号右移>>>

对于正数,无符号右移和有符号右移结果相同。

对于负数,无符号右移将负数的二进制当作正数的二进制进行处理。比如:

let oldValue = -64; // 等于二进制 11111111111111111111111111000000
let newValue = oldValue >>> 5; // 等于十进制 134217726 
//二进制:00000111111111111111111111111110

布尔操作符

  • 逻辑非!

使用技巧!!相当于调用了Boolean()

  • 逻辑与&&

逻辑与操作符是一种短路操作符,意思就是如果第一个操作数决定了结果,那么永远不会对第二个操作数求值。

即如果&&左侧的值为false,右侧的表达式不会被运行,即便存在错误

  • 逻辑或||

同样与逻辑与类似,逻辑或操作符也具有短路的特性。只不过对逻辑或而言,第一个操作数求值为 true,第二个操作数就不会再被求值了。

乘性操作符

  • 乘法*
  • 除法/
  • 取模%

指数操作符

  • Math.pow() || **
console.log(Math.pow(3, 2); // 9
console.log(3 ** 2); // 9
console.log(Math.pow(16, 0.5); // 4
console.log(16** 0.5); // 4 let squared = 3;
squared **= 2;
console.log(squared); // 9 

加性操作符

加减

关系操作符

小于(<)、大于(>)、小于等于(<=)和大于等于(>=)

相等操作符

  • 等于==和不等于!=
  • 全等===和不全等!==

条件操作符

variable = boolean_expression ? true_value : false_value;

let max = (num1 > num2) ? num1 : num2;

赋值操作符

=

乘后赋值(*=) 除后赋值(/=) 取模后赋值(%=) 加后赋值(+=) 减后赋值(-=) 左移后赋值(<<=) 右移后赋值(>>=) 无符号右移后赋值(>>>=)

逗号操作符

逗号操作符可以用来在一条语句中执行多个操作,如下所示:

let num1 = 1, num2 = 2, num3 = 3;

语句

语句,也称流控制语句

if 语句

do-while语句

do-while 语句是一种后测试循环语句,即循环体中的代码执行后才会对退出条件进行求值。换句话说,循环体内的代码至少执行一次

while语句

先测试循环语句

for语句

for...in

for...of

标签语句及应用

标签语句用于给语句加标签,一般和break,continue一起使用来彻底退出嵌套循环

//普通的嵌套循环,输出1到25,num++运行了25次
let num = 0;
for(let i = 0; i < 5; i++){
  for(let j = 0; j < 5; j++){
    num++
    console.log(num);
  }
}
​
//没有使用标签,break只能推出当前j的循环。
//即当i==2,j==2时退出,继续运行i==3,j==0.而不是彻底退出
//输出1到22
let num = 0;
for(let i = 0; i < 5; i++){
  for(let j = 0; j < 5; j++){
    if(i == 2 && j == 2){
      break
    }
    num++
    console.log(num);
  }
}
​
//配合标签使用的break
//即当i==2,j==2时彻底退出两层循环。
//输出1到12
let num = 0;
mylabel:
for(let i = 0; i < 5; i++){
  for(let j = 0; j < 5; j++){
    if(i == 2 && j == 2){
      break mylabel
    }
    num++
    console.log(num);
  }
}

break和continue语句

break语句会立即退出循环。

continue会立即退出当前的这次循环

with语句

用于将代码作用域设置为特定的对象。严格模式不允许使用with语句,并且with语句影响性能且很难调试,不推荐使用。

//不使用with
let qs = location.search.substring(1);
let hostName = location.hostname;
let url = location.href; 
​
//上面的代码的每一行都是用到了location对象
//如果使用with语句,就可以减少代码量
with(location) {
 let qs = search.substring(1);
 let hostName = hostname;
 let url = href;
} 

switch 语句

switch 语句在比较每个条件的值时会使用全等操作符,因此不会强制转换数据类 型(比如,字符串"10"不等于数值 10)。

函数

无特别

第四章 变量、作用域与内存

4.2.1 作用域链增强

with(不推荐使用)

4.3 垃圾回收(两种方式)

JavaScript通过自动内存管理实现内存分配和闲置资源回收,因此不需要开发者额外跟踪内存使用。

4.3.1 标记清理

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

浏览器引擎在执行标记清理算法时,需要从出发点去遍历内存中所有的对象去打标记,而这个出发点有很多,我们称之为一组 对象,而所谓的根对象,其实在浏览器环境中包括又不止于 全局Window对象文档DOM树

过程类似于:

  • 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0

  • 然后从各个根对象开始遍历,把不是垃圾的节点改成1

  • 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间

  • 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收

    优点

    标记清除算法的优点只有一个,那就是实现比较简单,打标记也无非打与不打两种情况,这使得一位二进制位(0和1)就可以为其标记,非常简单

    缺点

    标记清除算法有一个很大的缺点,就是在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了 内存碎片,并且由于剩余空闲内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的问题

    img

    此时再有新建对象需要分配内存时,则需要对空闲内存列表进行一次单向遍历找出大于等于size的空闲内存才能分配。有三种分配策略

    • First-fit,找到大于等于 size 的块立即返回
    • Best-fit,遍历整个空闲列表,返回大于等于 size 的最小分块
    • Worst-fit,遍历整个空闲列表,找到最大的分块,然后切成两部分,一部分 size 大小,并将该部分返回

    这三种策略里面 Worst-fit 的空间利用率看起来是最合理,但实际上切分之后会造成更多的小块,形成内存碎片,所以不推荐使用,对于 First-fitBest-fit 来说,考虑到分配的速度和效率 First-fit 是更为明智的选择

    即便是First-fit,依旧是一个比较耗时O(n)的操作,所以标记清除算法的另一个缺点就是分配速度慢

    img

标记整理(Mark-Compact)算法

它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存

img

4.3.2 引用计数

见面经

4.3.3 分代垃圾回收算法

分代式垃圾回收是V8对GC的优化

该算法将堆内存分为新生代老生代两区域,分别对两区域采用不同的垃圾回收策略

img

4.3.4 内存管理

本节介绍了一些手动管理内存的手段,用于提升性能。“这更多出于安全考虑而不是别的,就是为了避免运行大量JavaScript的网页耗尽系统内存而导致操作系统崩溃”。

  • 最基本的, 直接 = null 解除对值的引用
function createPerson(name){ 
 let localPerson = new Object(); 
 localPerson.name = name; 
 return localPerson; 
} 
let globalPerson = createPerson("Nicholas"); 
// 解除 globalPerson 对值的引用
globalPerson = null;

localPersoncreatePerson()执行完成超出上下文后会自 动被解除引用,不需要显式处理。但 globalPerson 是一个全局变量,应该在不再需要时手动解除其 引用,最后一行就是这么做的。

  • 通过 constlet 声明提升性能

相比于var,使用constlet 可能会更早地让垃圾回收程序介入

  • 隐藏类和删除操作(避免产生过多的隐藏类)

JavaScript作为动态语言,可以“先创建再补充”(ready-fire-aim),比如:

function Person(name, age) {
    this.name = name;
    this.age = age;
}
​
var xiaoming = new Person("xiaoming", 32);
var lisi = new Person("lisi", 20);xiaoming.email = "xiaoming@qq.com";
xiaoming.job = "teacher";lisi.job = "chef";
lisi.email = "lisi@qq.com";

xiaominglisi在创建之后又都添加了jobemail两个属性. 而对于像Java这样的静态语言,所有对象的属性在编译之前就会被固定,这样属性的值可以彼此间隔固定偏移量存储在内存空间中,可以轻松确认属性值在内存中的位置。而由于JavaScript动态属性赋值的特性,查找属性值需要在哈希表中查找属性的内存位置,速度慢得多。

对此,V8引擎引入了隐藏类(Hidden Class)的机制,起到给对象分组的作用。在初始化对象的时候,V8引擎会创建一个隐藏类,随后在程序运行过程中每次增减属性,就会创建一个新的隐藏类或者查找之前已经创建好的隐藏类。每个隐藏类都会记录对应属性在内存中的偏移量,从而在后续再次调用的时候能更快地定位到其位置。

以上面的代码为例。当初始化Person对象时,最开始会创建一个C0的隐藏类,且不含任何属性。随后在调用构造器函数时,随着属性的增加,引擎会生成C1,C2的过渡隐藏类,之所以存在过渡隐藏类是为了在多个对象间能够共享隐藏类。

这里,xiaominglisi两个对象使用的是同一个构造函数,所以它们会共享同一个隐藏类C2。随后虽然xiaominglisi两个对象都添加了jobemail两个属性,但由于初始化顺序不同,会生成不同的隐藏类。

不同初始化顺序的对象,所生成的隐藏类是不一样的。因此,在实际开发过程中,应该尽量保证属性初始化的顺序一致,这样生成的隐藏类可以得到共享。同时,尽量在构造函数里就初始化所有对象成员,减少隐藏类的产生。

hiddenClass.png

  • 避免内存泄漏

    JavaScript中的内存泄漏大部分是由不合理的引用导致的。

    • 意外声明的全局变量
    function setName() {
     name = 'Jake';
    } 
    

    没有使用var, let, const , 解释器会把变量name当作window的属性来创建,相当于window.name = 'Jake'。只要window本身不被清理就不会消失。

    • 没有清理的定时器, 其实也是闭包
    let name = 'Jake';
    setInterval(() => {
     console.log(name);
    }, 100); 
    

    只要定时器一直运行,回调函数中引用的name就会一直占用内存。垃圾回收程序也就不会清理外部变量。

    • 错误使用的闭包
    let outer = function() {
     let name = 'Jake';
     return function() {
        return name;
     };
    }; 
    

    调用 outer()会导致分配给 name 的内存被泄漏。以上代码执行后创建了一个内部闭包,只要返回 的函数存在就不能清理 name,因为闭包一直在引用着它。假如 name 的内容很大(不止是一个小字符串),那可能就是个大问题了。

    • 脱离DOM的引用

    获取一个DOM元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收

  • 静态分配和对象池

理论上,如果能够合理使用分配的内存,同时避免多余的垃圾回收,那就可以保住因 释放内存而损失的性能。

第五章 基本引用类型