第三章
第一节 语法
严格模式
"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不等于NaNNaN进行任何操作始终返回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()返回的格式有部分区别:
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)就可以为其标记,非常简单
缺点
标记清除算法有一个很大的缺点,就是在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了
内存碎片,并且由于剩余空闲内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的问题此时再有新建对象需要分配内存时,则需要对空闲内存列表进行一次单向遍历找出大于等于
size的空闲内存才能分配。有三种分配策略First-fit,找到大于等于size的块立即返回Best-fit,遍历整个空闲列表,返回大于等于size的最小分块Worst-fit,遍历整个空闲列表,找到最大的分块,然后切成两部分,一部分size大小,并将该部分返回
这三种策略里面
Worst-fit的空间利用率看起来是最合理,但实际上切分之后会造成更多的小块,形成内存碎片,所以不推荐使用,对于First-fit和Best-fit来说,考虑到分配的速度和效率First-fit是更为明智的选择即便是
First-fit,依旧是一个比较耗时O(n)的操作,所以标记清除算法的另一个缺点就是分配速度慢
标记整理(Mark-Compact)算法
它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存
4.3.2 引用计数
见面经
4.3.3 分代垃圾回收算法
分代式垃圾回收是V8对GC的优化
该算法将堆内存分为新生代和老生代两区域,分别对两区域采用不同的垃圾回收策略
4.3.4 内存管理
本节介绍了一些手动管理内存的手段,用于提升性能。“这更多出于安全考虑而不是别的,就是为了避免运行大量JavaScript的网页耗尽系统内存而导致操作系统崩溃”。
- 最基本的, 直接 = null 解除对值的引用
function createPerson(name){
let localPerson = new Object();
localPerson.name = name;
return localPerson;
}
let globalPerson = createPerson("Nicholas");
// 解除 globalPerson 对值的引用
globalPerson = null;
localPerson 在 createPerson()执行完成超出上下文后会自 动被解除引用,不需要显式处理。但 globalPerson 是一个全局变量,应该在不再需要时手动解除其 引用,最后一行就是这么做的。
- 通过
const和let声明提升性能
相比于var,使用const 和 let 可能会更早地让垃圾回收程序介入
- 隐藏类和删除操作(避免产生过多的隐藏类)
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";
xiaoming和lisi在创建之后又都添加了job和email两个属性. 而对于像Java这样的静态语言,所有对象的属性在编译之前就会被固定,这样属性的值可以彼此间隔固定偏移量存储在内存空间中,可以轻松确认属性值在内存中的位置。而由于JavaScript动态属性赋值的特性,查找属性值需要在哈希表中查找属性的内存位置,速度慢得多。
对此,V8引擎引入了隐藏类(Hidden Class)的机制,起到给对象分组的作用。在初始化对象的时候,V8引擎会创建一个隐藏类,随后在程序运行过程中每次增减属性,就会创建一个新的隐藏类或者查找之前已经创建好的隐藏类。每个隐藏类都会记录对应属性在内存中的偏移量,从而在后续再次调用的时候能更快地定位到其位置。
以上面的代码为例。当初始化Person对象时,最开始会创建一个C0的隐藏类,且不含任何属性。随后在调用构造器函数时,随着属性的增加,引擎会生成C1,C2的过渡隐藏类,之所以存在过渡隐藏类是为了在多个对象间能够共享隐藏类。
这里,xiaoming和lisi两个对象使用的是同一个构造函数,所以它们会共享同一个隐藏类C2。随后虽然xiaoming和lisi两个对象都添加了job和email两个属性,但由于初始化顺序不同,会生成不同的隐藏类。
不同初始化顺序的对象,所生成的隐藏类是不一样的。因此,在实际开发过程中,应该尽量保证属性初始化的顺序一致,这样生成的隐藏类可以得到共享。同时,尽量在构造函数里就初始化所有对象成员,减少隐藏类的产生。
-
避免内存泄漏
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元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收
- 静态分配和对象池
理论上,如果能够合理使用分配的内存,同时避免多余的垃圾回收,那就可以保住因 释放内存而损失的性能。