一、语言基础
1. 变量
1.1 var关键字特点
- 可以用于保存任何类型的值
- 可以改变值的类型(合法但不推荐)
- 初始化前会保存一个特殊值undefined (如下图1.1.1-1)

- 声明作用域
- 使用var操作符定义的变量会称为包含他的函数的局部变量,使用var在函数内部定义一个变量,就意味着变量将在函数退出时被销毁,外部访问此变量则会提示该变量不存在(如下图1.1.1-2)

- 在函数内部省略var操作符,可以创建一个全局变量,只要调用一次当前函数,就是定义这个变量,并且在函数外部访问到(如下图1.1.1-3)

- 声明提升:var把所有变量的声明都会拉到函数作用域的顶部,因此多次声明同一个变量也没有问题(如下图1.1.1-4)
1.2 let 声明
- let声明范围是块级作用域,作用域仅限于该块函数内部【let&var 区别一:var声明范围是函数作用域】

- 不允许同一个作用域中出现冗余声明。嵌套使用相同的标识符不会报错是因为在同一个作用域块中没有重复声明(如下图1.1.2-1)

- 暂时性死区:let与var的另一个区别就是let声明的变量不会在作用域中被提升,在解析代码时,JavaScript引擎也会质疑出现在块后面的let声明,只不过在此之前不能以任何方式来引用未声明的变量,在let声明之前的执行瞬间被称为“暂时性死区”,在此阶段引用后面才声明的变量都会抛出ReferenceError(如图1.1.2-2)

- 全局声明:使用 let 在全局作用域中声明的变量不会成为 window 对象的属性(var 声明的变量则会)
- 条件声明:
- 在使用 var 声明变量时,由于声明会被提升,JavaScript 引擎会自动将多余的声明在作用域顶部合并为一个声明。
- 因为 let 的作用域是块,所以不可能检查前面是否已经使用 let 声明过同名变量,同时也就不可能在没有声明的情况下声明它。 就算我们使用typeof 或者try/catch想要捕捉到当前变量有没有被声明过,然后去做声明,但声明的变量也仅仅只会作用与当前块中,并不能解决实际问题(如图1.1.2-3)

- for循环中的let声明
- let较var而言可以更有效的在循环中防止迭代的变量外泄

- 经典案例:
1.3 const 声明
- const 的行为与 let 基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改 const 声明的变量会导致运行时错误,也可以直接理解为const声明的变量实际上是一种常量。
- const声明的限制只适用于它所指向的变量的引用,修改这个引用对象的内部属性并不违反const的限制
- for循环中不能使用const 未变量声明,因为迭代变量会自增
1.4 声明风格及最佳实践
- 不使用 var
有了 let 和 const,大多数开发者会发现自己不再需要 var 了。限制自己只使用 let 和 const
有助于提升代码质量,因为变量有了明确的作用域、声明位置,以及不变的值。 - const 优先,let 次之
使用 const 声明可以让浏览器运行时强制保持变量不变,也可以让静态代码分析工具提前发现不合法的赋值操作。因此,很多开发者认为应该优先使用 const 来声明变量,只在提前知道未来会有修改时,再使用 let。这样可以让开发者更有信心地推断某些变量的值永远不会变,同时也能迅速发现因意外赋值导致的非预期行为2. 数据类型
简单(基本)数据类型:Undefined,Null,Boolean,String,Number,Symbol
复杂(引用)数据类型:Object,function
2.1 typeof操作符
- 返回类型:undefined,boolean,string,number,object,function,symbol
注意:
该类型只有一个值:undefined
-
2.3 Null类型
该类型只有一个值:null,从逻辑上讲null值表示一个空对象指针
undefined是由null派生而来,undefined==null 为true
2.4 Boolean类型
该类型有两个字面量值:true false
-
2.5 Number类型
0.1+0.2 ≠0.3
- 浮点数采用了IEEE754 数值,在计算机的存储中存在着精度偏差,0.1 ,0.2转换为二进制数后会存在舍入错误
- 最大值:Number.MAX_VALUE
- 最小值:Number.MIN_VALUE
- NaN:不是数值
- 涉及到NaN的操作始终返回NaN
- NaN不等于包括NaN在内的任何值
- isNaN() 判断参数是否”不是数值”
数值转换:
字符串的特点: 字符串时不可变的,意思是一旦创建,它们的值就不能变了。要修改某个字符串值。必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到变量
- 转化为字符串:
- toString()【null,undefined没有该方法】,在对数值调用此方法是,可以接受一个底数参数,即以什么底数来输出熟知的字符串表示,通过传入参数,可以得到数值的二进制、八进制、十六进制或者其他任何有效的字符串标识
- String():如果不确定一个值是不是null或者undefined,可以使用String()转型函数,它会始终返回表示相应类型值的字符串,String()函数遵循以下原则
- 值有toString(),则调用该方法并返回值
- 如果是null,返回”null”
- 如果是undefined,返回”undefined”
- 模板字面量:ES6新增使用模板字面量定义字符串的能力,可以保留换行字符,可以跨行定义字符串
- 字符串插值:
${} - 模板字面量标签函数
2.7 Symbol类型
- 基本用法:
- 使用Symbol()函数初始化
- 可传参作为对Symbol的描述,但这个蚕食与使用symbol定义或标识完全无关
- 不能与new关键字一起作为构造函数使用
作用
object实例中都有如下属性和方法:
- constructor:用于创建当前对象的函数
- hasOwnProperty(propertyName):用于判断当前对象实例上是否存在给定的属性名
- isPrototypeOf(object):用于判断当前对象是否为另外一个对象的原型
- propertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用
- toLocaleString():返回对象的字符串表示,该字符串反映对象所在的本地化执行环境
- toString():返回对象的字符串表示
- valueOf():返回对象对应的字符串,数值或布尔值表示
3. 操作符
3.1 一元操作符
- 递增递减操作符:后缀递增(递减)在混合运算中先执行混合的运算结束后在执行递增(递减)
-
3.2 位操作符
按位非:用波浪符表示(~),它的作用时返回数值的一补数
- 按位与:用和号表示(&),有两个操作数,本质上按位与就是将两个数的每一个位对齐,执行相应的与操作
- 按位或:用管道符表示(|),同样有两个操作数,至少一位是1是返回1,全0 返回0
- 按位异或:用脱字符表示(^),同样有两个操作数,只在一位上是1的时候返回1,两位都是1或者0时返回0
- 左移:用两个小于号表示(<<),会按照指定位数将数值的所有位向左移动
- 有符号右移:用两个大于号表示(>>)
-
3.3 布尔操作符
逻辑非:一个叹号表示(!),将操作数转换为布尔值,并取反,两个叹号(!!)相当于调用的转型函数Boolean()函数,返回变量对应的真正的布尔值
- 逻辑与:两个和号表示(&&),是一种短路操作,如果第一个操作数决定了结果,那么永远不会对第二个操作数求值
逻辑或:两个管道符表示(||),也具有短路的特性,只不过对逻辑或而言,第一个操作数求值为true,第二个操作数就不会在被执行
3.4 乘性操作符
乘法
- 除法
-
3.5 指数操作符
3.6 加性操作符
3.7 关系操作符
大于:>
- 小于:<
- 大于等于:>=
- 小于等于:<=
操作符应用到不同数据类型也会发生类型转换和其他行为
等于与不等于
- 全等与不全等
3.9 条件操作符
variable = boolean_expression ? true_value : false_value3.10 赋值操作符
简单赋值用等于=表示
复合赋值操作符:*= 、/=、%=、+=、-=、<<=、>>=、>>>=3.11 逗号操作符
逗号操作符可以用来在一句语句中执行多个操作
4. 语句
4.1 if语句
if(condition) statement1 else if(condition2) statement2 else statement3
4.2 do-while语句
后测试循环语句,即循环体中的代码后才会对退出条件进行求值,循环体内部至少执行一次
do {statement} where (expression)
4.3 where语句
先测试循环语句,先检测退出条件,在执行循环体代码where (expression) statement
4.4 for语句
先测试循环语句,增加了进入循环之前的初始化代码,以及循环执行后要执行的表达式,初始化、表达式和缓缓后表达式都不是必须的。for(initialization;expression;post-loop-expression) statement
4.5 for-in语句
是一种严格的迭代语句,用于枚举对象中的非符号键(Symbol)属性for(property in expression) statement
注意:
- 因为对象的属性是无序的,不能保证返回对象属性的顺序
循环迭代的结果是null或者undefined,则不执行循环体
4.6 for-of语句
是一种严格的迭代语句,用于遍历可迭代对象的元素,如果尝试迭代的表里不支持迭代,则会抛出错误
for(property of expression) statement4.7 标签语句
4.8 break和continue语句
break:用于立即退出循环,强制执行循环后的下一条语句
- continue:用于立即退出循环,但会再次从循环顶部开始执行
4.9 with语句
用于将代码作用域设置为特定的对象with(expression)statement
let qs=location.search.substring(1);let hostName=location.hostname;let url=localtion.href// =========================with(location){let qs=search.substring(1);let hostName=hostname;let url=href}
4.10 switch语句
是与if语句紧密相关的一种流控制语句
switch(expression){case value1:statement1;break;case value2:statement2;break;...default:statement}
5. 变量、作用域与内存
5.1 原始值与引用值
原始值:简单的数据,按值访问,操作的是存储在变量中的实际值
引用值:由多个值构成的对象,按引用访问,操作的是对该对象的引用
- 动态属性
- 引用值可以随时添加修改和删除其属性和方法
- 原始值不能有属性,虽然尝试给原始值添加属性不会报错,只有引用值可以动态添加属性
- 复制值
- 原始值复制:通过变量将一个原始值赋值给另一个变量,原始值会被复制到新变量的位置(如左图)
- 引用值复制:在把引用值从一个变量赋值给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象,操作完成之后,两个变量实际上指向同一个对象,因此对象上的变化会在另一个对象中体现出来。


- 传递参数
- 函数中的参数传递是按值传递的
- 传参类型是对象的函数,定义的变量和传递的对象在函数内部指向的是同一个对象,所以当变量的属性修改的时候,传递对象也会跟着修改,所以会被误解为参数的传递是按引用类型传递的。实际上在函数内部重写obj时,这个变量引用的是一个局部对象变量,该局部对象会在函数执行完毕之时销毁
确定类型
静态作用域:也叫做词法作用域,即函数的作用域在函数定义的时候就决定了。【JavaScript采用】
- 动态作用域:与静态作用域相对,即函数的作用域是在函数调用的时候才决定的
执行上下文
执行上下文(也称上下文)是当前代码的执行环境。 变量或函数的上下文决定了他们可以访问哪些数据,以及他们的行为。
类型:
- 全局执行上下文:最外围的执行环境,在浏览器情况下,在该环境的下执行的代码会执行以下步骤:
- 创建一个window对象;
- 将this指向这个window对象;
- 通过var定义的全局变量和函数都会成为window对象的属性和方法。
- 函数执行上下文:每个函数都有自己的上下文,当代码执行到该函数时,函数的上下文被推到一个上下文栈上。在函数执行完后,上下文栈会弹出该函数的上下文,将控制权返还给之前的执行上下文。
- eval(): 执行在
eval函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用eval,所以在这里我不会讨论它
执行上下文栈:(Execution context stack,ECS),也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。
为了模拟执行上下文栈的行为,让我们定义执行上下文栈是一个数组ECStack = [ ]:
- 当 JavaScript 引擎第一次遇到脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。
- 引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。
- 当 局部的函数上下文执行完毕,它的执行上下文从栈弹出,控制流程到达全局执行上下文。一旦所有代码执行完毕,JavaScript 引擎从当前栈中移除全局执行上下文。
生命周期:每个执行上下文的生命周期都经历了:创建阶段 -> 执行阶段
①创建阶段
在 JavaScript 代码执行前,执行上下文将经历创建阶段。在创建阶段会发生三件事:
- this 值的决定,即我们所熟知的 This 绑定。
- 全局执行上下文this绑定:
this的值指向全局对象 - 函数执行上下文this绑定:
this的值取决于该函数是如何被调用的
- 全局执行上下文this绑定:
- 创建词法环境组件
- 「什么是词法环境呢?」:词法环境是ECMA中的一个规范类型 —— 基于代码词法嵌套结构用来记录标识符和具体变量或函数的关联。 简单来说,词法环境就是建立了标识符——变量的映射表。这里的标识符指的是变量名称或函数名,而变量则是实际变量原始值或者对象/函数的引用地址。
- 内部组件
- 环境记录器:是存储变量和函数声明的实际位置。
- 外部环境的引用:意味着它可以访问其父级词法环境(作用域)
- 类型
- 在全局环境中,环境记录器是对象环境记录器。存储变量、函数和参数
- 在函数环境中,环境记录器是声明式环境记录器。用来定义出现在全局上下文中的变量和函数的关系。
- 创建变量环境组件: 变量环境也是词法环境,它拥有词法环境所有的属性和组件的定义在ES6中,词法环境和变量环境唯一不同,在于词法环境存储了函数和let, const变量的绑定,而变量环境只用来存储var类型变量的绑定
②执行阶段ExecutionContext = {ThisBinding = <this value>,LexicalEnvironment = { ... },VariableEnvironment = { ... },}
此阶段,完成对所有变量的分配,最后执行代码。主要就是执行变量赋值、代码执行
如果 Javascript 引擎在源代码中声明的实际位置找不到 let 变量的值,那么将为其分配 undefined 值。
变量声明
- 使用var的函数作用域声明:
- 声明的变量会自动添加到最近的执行上下文中
- 如果函数未经声明就被初始化了,则会自动的被添加到全局上下文中
- var的声明会被那倒函数或者全局作用域的顶部,位于作用域中所有代码之前。这个现象叫做提升
- 使用let的块级作用域声明:作用域为块级,由最近的一堆包含花括号{}界定,同一作用域内不能声明两次相同的变量
- 使用const的常量声明:使用const生命的变量必须同时初始化为某个值,一经声明,在其生命周期的任何时候都不能再重新赋予新值
- 标识符查找:作用域链查找,在最近的作用域开始寻找某一值,没有找到就向外作用域查找,直到找到最顶层都没有的话则停止
5.3 垃圾回收
【垃圾回收】是一种自动的内存管理机制。在 JavaScript 内存管理中有一个概念叫做 可达性,就是那些以某种方式可访问或者说可用的值,它们被保证存储在内存中,反之不可访问则需回收。
因为计算机中的内存是有限的,如果变量、函数等动态内存只有产生而没有消亡的过程,那么内存被占满也只是时间问题罢了。
一、标记清除 【JavaScript中最常用的垃圾回收策略】
分为标记和清除两个阶段,大致流程如下
- 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
- 然后从各个根对象开始遍历,把不是垃圾的节点改成1
- 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
- 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收
优点:
标记清除算法的优点只有一个,那就是实现比较简单,打标记也无非打与不打两种情况,这使得一位二进制位(0和1)就可以为其标记,非常简单
缺点:
标记清除算法有一个很大的缺点,就是在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了 内存碎片(如下图),并且由于剩余空闲内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的问题

内存分配的三种策略:
- First-fit,找到大于等于 size 的块立即返回
- Best-fit,遍历整个空闲列表,返回大于等于 size 的最小分块
- Worst-fit,遍历整个空闲列表,找到最大的分块,然后切成两部分,一部分 size 大小,并将该部分返回
综上所述,标记清除算法或者说策略就有两个很明显的缺点
- 内存碎片化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块
- 分配速度慢,因为即便是使用 First-fit 策略,其操作仍是一个 O(n) 的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢
标记清除算法的缺点补充
归根结底,标记清除算法的缺点在于清除之后剩余的对象位置不变而导致的空闲内存不连续,所以只要解决这一点,两个缺点都可以完美解决了
而 标记整理(Mark-Compact)算法 就可以有效地解决,它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存(如下图)
二、引用计数【JavaScript中另一种垃圾回收策略】
这其实是早先的一种垃圾回收算法,它把 对象是否不再需要 简化定义为 对象有没有其他对象引用到它,如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收,他的执行流程如下
- 当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就为 1
- 如果同一个值又被赋给另一个变量,那么引用数加 1
- 如果该变量的值被其他的值覆盖了,则引用次数减 1
- 当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存
优点
引用计数算法的优点我们对比标记清除来看就会清晰很多,首先引用计数在引用值为 0 时,也就是在变成垃圾的那一刻就会被回收,所以它可以立即回收垃圾
而标记清除算法需要每隔一段时间进行一次,那在应用程序(JS脚本)运行过程中线程就必须要暂停去执行一段时间的 GC,另外,标记清除算法需要遍历堆里的活动以及非活动对象来清除,而引用计数则只需要在引用时计数就可以了
缺点
引用计数的缺点想必大家也都很明朗了,首先它需要一个计数器,而此计数器需要占很大的位置,因为我们也不知道被引用数量的上限,还有就是无法解决循环引用无法回收的问题,这也是最严重的
三、性能
垃圾回收程序周期性运行,如果内存中分配了许多变量,则可能造成性能损失,拖慢渲染的速度和帧速率,因此垃圾回收的时间调度很重要。现代的垃圾回收程序会基于对JavaScript运行时环境的探测来决定何时运行
四、内存管理
将内存占用量保持在一个较小的值可以让页面的性能更好,优化内存占用的最佳手段就是保证再执行代码是只保存必要的数据,如果数据不在必要,那么把他设置为null,从而释放其引用,这种方法叫做解除引用。
内存泄漏的场景:
- 隐匿的全局变量
- 未清除的定时器
- 不正当的闭包【解决办法:在函数调用完成之后将外部的引用关系置空】
- 未清除的DOM元素引用:考虑到性能或代码简洁方面,我们代码中进行 DOM 时会使用变量缓存 DOM 节点的引用,但移除节点的时候,我们应该同步释放缓存的引用,否则游离的子树无法释放。
- 未清除的事件监听器
- 未清除的监听者模式
- 未清除的Map、Set对象
内存管理的方法:
- 通过const和let声明提升性能
-
5.4 拓展
浅拷贝与深拷贝:浅拷贝和深拷贝都复制了值和地址,都是为了解决引用类型赋值后互相影响的问题
- 浅拷贝:只进行一层复制,深层次嵌套的引用类型还是共享内存地址,原对象和拷贝对象还是会互相影响
- 深拷贝:无限层级拷贝,深拷贝后的原对象不会和拷贝对象互相影响
- 实现浅拷贝
- Object.assign():方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
- 扩展运算符 { … }
- 数组的 slice 和 concat 方法
- 数组静态方法 Array.from
- 实现深拷贝:
JSON.parse(JSON.stringify(object)),存在问题如下:- 会忽略 undefined
- 会忽略 symbol
- 不能序列化函数
- 不能解决循环引用的对象
- 不能正确处理new Date()
- 不能处理正则 ```javascript /* undefined、symbol 和函数这三种情况,会直接忽略/ let obj = { name: ‘muyiy’, a: undefined, b: Symbol(‘muyiy’), c: function() {} } console.log(obj); // { // name: “muyiy”, // a: undefined, // b: Symbol(muyiy), // c: ƒ () // }
/ 循环引用情况下,会报错。 / let b = JSON.parse(JSON.stringify(obj)); console.log(b); // {name: “muyiy”}
let obj = { a: 1, b: { c: 2, d: 3 } } obj.a = obj.b; obj.b.c = obj.a;
let b = JSON.parse(JSON.stringify(obj)); // Uncaught TypeError: Converting circular structure to JSON
/ new Date 情况下,转换结果不正确 解决方法转成字符串或者时间戳 / new Date(); // Mon Dec 24 2018 10:59:14 GMT+0800 (China Standard Time)
JSON.stringify(new Date()); // “”2018-12-24T02:59:25.776Z””
JSON.parse(JSON.stringify(new Date())); // “2018-12-24T02:59:41.523Z”
let date = (new Date()).valueOf(); // 1545620645915
JSON.stringify(date); // “1545620673267”
JSON.parse(JSON.stringify(date)); // 1545620658688
/ 不能处理正则 / let obj = { name: “muyiy”, a: /‘123’/ } console.log(obj); // {name: “muyiy”, a: /‘123’/}
let b = JSON.parse(JSON.stringify(obj)); console.log(b); // {name: “muyiy”, a: {}}
```javascriptfunction deepClone (target, hash = new WeakMap()) { // 额外开辟一个存储空间WeakMap来存储当前对象if (target === null) return target // 如果是 null 就不进行拷贝操作if (target instanceof Date) return new Date(target) // 处理日期if (target instanceof RegExp) return new RegExp(target) // 处理正则if (target instanceof HTMLElement) return target // 处理 DOM元素if (typeof target !== 'object') return target // 处理原始类型和函数 不需要深拷贝,直接返回// 是引用类型的话就要进行深拷贝if (hash.get(target)) return hash.get(target) // 当需要拷贝当前对象时,先去存储空间中找,如果有的话直接返回const cloneTarget = new target.constructor() // 创建一个新的克隆对象或克隆数组hash.set(target, cloneTarget) // 如果存储空间中没有就存进 hash 里Reflect.ownKeys(target).forEach(key => { // 引入 Reflect.ownKeys,处理 Symbol 作为键名的情况cloneTarget[key] = deepClone(target[key], hash) // 递归拷贝每一层})return cloneTarget // 返回克隆的对象}

6. 基本引用类型
6.1 Date
一、继承的方法
二、日期格式化方法
三、日期/时间组件方法
6.2 RegExp
一、RegExp实例属性
二、RegExp实例方法
三、RegExp构造函数属性
四、模式局限
6.3 原始包装类型
一、Boolean
二、Number
三、String
6.4 单例内置对象
一、Global
二、Math
