JavaScript / ES6
- -----------------数据类型---------------
- JavaScript的数据类型
- 为什么有BigInt
- 原始数据类型、引用数据类型
- undefined 与 null
- typeof 操作符
- instanceof 操作符
- 判断数据类型的方式
- 判断数组的方法
- 0.1 + 0.2 !== 0.3 的原因,如何解决?
- 安全地获得undefined值
- NaN
- isNaN 与 Number.isNaN
- ------------单个数据的类型转换---------------
- 将某个值转成Boolean
- 将某个值转成Number
- 将某个值转成String
- ToPrimitive 对象转成原始数据类型
- ------------多个数据的类型转换---------------
- x + y
- - * / 减乘除
- x == y
- ------------------- 其它操作符 -----------------
- && 与 || 操作符
- == 操作符、=== 操作符 与 Objece.is()
- ---------------- JavaScript 基础 -----------------
- new操作符的原理
- Object.assign和扩展运算符
- 深拷贝与浅拷贝
- DOM
- BOM
- 对AJAX的理解
- 封装一个AJAX
- Ajax,Axios,Fetch的区别
- 关注点分离原则
- 软件架构模式 MVC, MVP, MVVM **
- 延迟加载JavaScript脚本的方法
- 判断一个对象是否为空对象的方法
- 正则表达式
- 对JSON的理解
- use strict 严格模式
- 事件流
- 事件模型
- 事件委托
- preventDefault(),stopPropagation(),return false
- JavaScript 本地缓存 **
- ------------------- 关于数组 -----------------
- 数组的原生方法
- 数组的遍历方法
- for...in 和 for...of 的区别
- 数组去重
- 数组的乱序输出
- 数组的扁平化
- 数组元素求和
- 类数组对象
- 类数组对象如何转化为数组
- arguments
- -----------------原型与原型链-----------
- 原型链:__ proto __ 、prototype 、constructor
- -------声明变量、作用域链、闭包-----------
- 变量声明 var,let,const
- 变量提升、函数提升
- 作用域链
- 闭包
- ----------执行上下文与this的指向-----------
- 执行上下文
- 词法环境与变量环境
- this的指向规则
- -----------------ES6相关---------------
- let 、 const 、 var 的区别
- const声明的变量可以修改吗
- 箭头函数与普通函数的区别
- rest参数
- ES6的模板语法
- 解构赋值
- 尾调用优化
- Set
- Map 与 Object 的比较
- Map 与 WeakMap
- Symbol
- proxy
- Iterator 和 for...of 循环
- Generator
- Class
- Module
- ----------------面向对象编程---------------
- 对象的创建方式
- 对象的继承方式
- --------------异步编程/事件循环---------------
- 异步编程的实现方式
- 对Promise的理解
- Generator和yield
- 对async/await的理解
- setTimeout、setInterval、requestAnimationFrame
- 事件循环EventLoop
- Node的事件循环
- --------------垃圾回收/内存泄漏---------------
- 垃圾回收机制
- 内存泄漏
-----------------数据类型---------------
JavaScript的数据类型
JS共有八种数据类型:
- Undefined
- Null
- Boolean
- Number
- String
- Object
- Symbol(ES6新增)
- BigInt(ES6新增)
为什么有BigInt
- JavaScript中有Number.MAX_SAFE_INTEGER,表示最大安全数字,在这个范围内不会出现精度缺失的问题
- 但一旦超过这个范围,大数计算结果就不准确
- 因此提出了BigInt
原始数据类型、引用数据类型
原始数据类型有:
- Undefined
- Null
- Boolean
- Number
- String
- Symbol
- BigInt
引用数据类型:
- Object 普通对象
- Array 数组
- Date 日期
- Function 函数
- Math 数学函数
- RegExp 正则对象
原始数据类型和引用数据类型的区别(存储的位置不同):
- 原始数据类型直接存在 栈Stack 中的,是占据空间小,大小固定,被频繁使用的简单数据段
- 引用数据类型是存储在 堆Heap 中的,是占据空间大,大小不固定的复杂对象。(引用数据类型是在栈中存储了指针,该指针指向堆中该数据实体的起始地址。)
堆与栈 —— 在数据结构中:
- 栈的存取方式是先进后出
- 堆是一个优先队列,按照优先级大小进行排序
堆与栈 —— 在操作系统中:
- 栈区内存 由编译器 自动 分配与释放
- 堆区内存 由开发者 手动 分配与释放
undefined 与 null
- Undefined与Null都是基本数据类型,它们都只有一个值,分别是undefined与null
- undefined表示的是未定义,即已声明但未初始化的变量保存的值就是undefined
- null表示的是空对象,意在保存对象却暂无真正保存对象的变量,应该让该变量保存null值;
- 声明但未初始化的变量(Undefined类型)使用typeof返回的是字符串’undefined’;
- 对Null类型使用typeof(typeof null)返回的是字符串‘object’,null实际上是空对象指针
- null == undefined 返回true
- null === undefined 返回false
typeof 操作符
typeof操作符的返回值(字符串):
- undefined(未定义的变量、声明但未初始化的变量)
- boolean(变量是Boolean类型)
- number(变量是Number类型)
- string(变量是String类型)
- object(变量是Object类型、数组、Null)
- function(若变量的值是函数)
注意:
- typeof对Undefined类型返回的是‘undefined’
- typeof对Null类型返回的是’object’,null是空对象指针
值得注意的是:未声明的变量只能进行typeof操作
instanceof 操作符
描述:A instanceof B,A对象是否为B构造函数的一个实例对象
- 判断当前实例对象A的原型链上能否找到B类型的原型对象
- 故要遍历实例A的原型链,若找到了B的原型对象,就返回true
- 否则返回false
function myInstanceof(a,b){
a = a._proto_; //起点设为实例a的原型对象
b = b.prototype; //b类型的原型对象
while(true){
if(!a){
return false;
}
if(a === b){
return true;
}
a = a._proto_; //若找不到,继续往上找
}
}
判断数据类型的方式
① typeof 的返回值(字符串)
- undefined(未定义的变量、声明但未初始化的变量)
- boolean(变量是Boolean类型)
- number(变量是Number类型)
- string(变量是String类型)
- object(变量是Object类型、数组、Null)
- function(若变量的值是函数)
缺点:
- Object对象、数组、Null都会返回字符串‘object’。这是typeof操作符不太准确的地方
② instanceof 操作符
A instanceof B, 即判断 A对象是否为B构造函数的一个实例对象
- 原理:判断某个对象实例A的原型链中是否能找到B类型构造函数的原型对象(是否存在构造函数的prototype属性)
缺点:
- instanceof 只能正确判断引用数据类型,不能判断基本数据类型
③ constructor
- 每一个函数都有prototype属性, 该属性是一个指针,指向原型对象
- 原型对象内有constructor属性,也是指针,指向prototype属性所在的函数;
- 函数的实例可以继承并获得原型对象的constructor属性,从而通过constructor去访问构造函数,因而可判断数据类型
缺点:
- prototype有可能被重写
④ Object.prototype.toString.call()
上面三种方法都各有缺点:
- typeof 操作符对于Object对象、数组、和Null都会返回object字符串
- instanceof 操作符只能判断引用数据类型,不能判断基本数据类型
- constructor会担心prototype被改写
所以最好用有一种是 Object.prototype.toString.call()
大多数对象的toString方法都是重写过的,所以使用Object原型对象上的toString方法,去判断类型。
也就是使用Object.prototype.toString.call()
console.log(Object.prototype.toString.call("jerry"));//[object String]
console.log(Object.prototype.toString.call(12));//[object Number]
console.log(Object.prototype.toString.call(true));//[object Boolean]
console.log(Object.prototype.toString.call(undefined));//[object Undefined]
console.log(Object.prototype.toString.call(null));//[object Null]
console.log(Object.prototype.toString.call({name: "jerry"}));//[object Object]
console.log(Object.prototype.toString.call(function(){}));//[object Function]
console.log(Object.prototype.toString.call([]));//[object Array]
console.log(Object.prototype.toString.call(new Date));//[object Date]
console.log(Object.prototype.toString.call(/\d/));//[object RegExp]
function Person(){};
console.log(Object.prototype.toString.call(new Person));//[object Object]
判断数组的方法
不可以使用typeof,会返回object
上一题的后三种方法都可以:instanceof,constructor,Object.prototype.toString.call()
- instanceof操作符 :obj instanceof Array;
- constructor
- Object.prototype.toString.call()方法
- 利用原型链 obj.__ proto __ === Array.prototype
- Array.prototype.isPrototypeOf(obj) 判断Array.prototype是否是obj的的原型对象
- ES6的方法:Array.isArray(obj)
0.1 + 0.2 !== 0.3 的原因,如何解决?
原因:
- 这是关于JavaScript数字运算的精度缺失问题
- 在JS中,浮点数是用64位固定长度来表示。其中1位符号位,11位指数位,52位尾数位。
- 0.1与0.2转为二进制形式后,是一个无限循环小数。然而只有52位去保存,所以会精度缺失。取出来后转为十进制就不是0.1和0.2了
方法一:利用ES6的Number.EPSILON
- ES6中提供了Number.EPSILON属性,值为2^(-52)。可以理解为机器精度,设置了允许误差范围
- 只需判断 0.1 + 0.2 - 0.3 < Number.EPSILON :若成立,则返回true,否则返回false
function test(a,b){
return Math.abs(a-b) < Number.EPSILON;
}
console.log(test(0.1+0.2,0.3)); // true
方法二:将浮点数化为整数
- 0.1 与 0.2 本身以二进制形式存储后,变成了无限循环小数
- 将其重新转化为整数,再进行计算
function test(a,b){
console.log((a*1000 + b*1000)/1000)); // 0.3
}
安全地获得undefined值
- undefined本身并非保留字,因此可能被用作变量或赋值
- 因此若后续需要用undefined作为判断条件,结果可能会受影响
- 可以通过void(0)获得纯正的undefined
NaN
定义:
- NaN 即 Not A Number,用于表示本应该返回数值的操作数却没有返回数值的情况
- 返回NaN,可以避免抛出错误,避免代码因此停止执行
特点:
- 包含NaN的任何操作数,都返回NaN
- NaN与任何值都不相等 (NaN == NaN // false;NaN !== NaN //true)
- typeof NaN 返回 ‘number’
isNaN 与 Number.isNaN
- isNaN()接收参数后,若不是数值,则会尝试将其转化为数值。无法转化成数值的参数则返回true,表示它 Not A Number
- Number.isNaN,会先判断参数是否为数字,是的话再进一步判断是否为NaN
- Number.isNaN 对于NaN的判断更加准确,因为会先判断是否为Number;isNaN可以接受任何类型的参数
------------单个数据的类型转换---------------
数据类型可以进行转换,转成Boolean类型、Number类型和String类型;
需要记住转换的方式和规则
将某个值转成Boolean
转成Boolean类型常见的有两种方式:
- !!value 即对值进行两次取反
- Boolean(value) 使用Boolean方法显示转换
假值列表
- undefined
- null
- false
- +0 , -0 , NaN
- “” (空字符串)
注意,属于假值列表内的值,转成Boolean结果都为false
也就是说,假值列表外的值,转为Boolean结果都为true
// 使用!!两次取反
console.log(!!undefined); // false
console.log(!!null); // false
console.log(!!false); // false
console.log(!!+0); // false
console.log(!!-0); // false
console.log(!!NaN);// false
console.log(!!""); // false
// 使用Boolean()方法显示转换
console.log(Boolean(undefined));
console.log(Boolean(null));
console.log(Boolean(false));
console.log(Boolean(+0));
console.log(Boolean(-0));
console.log(Boolean(NaN));
console.log(Boolean(""));
将某个值转成Number
转成Number类型常见有两种方式:
- +value
- Number(value)
基本的转换规则:
- Undefined类型的值:undefined是转成NaN
- Null类型的值:null是转成0
- Boolean的值:true转为1,false转为0
- String类型的值:如果字符串中只包含有效数字(包括浮点数、十六进制),则会忽略前导零输出十进制数值;如果字符串为空,则输出0;若字符串包含上述之外的字符,则输出NaN
- Symbol类型的不可以转成Number,会报错!!
// 基本转换规则
console.log(+undefined); // NaN
console.log(+null); // 0
console.log(+true); // 1
console.log(+false);// 0
console.log(+'123');// 123
console.log(+""); // 0
console.log(+"123x"); //NaN
// Symbol -> 报错
// 使用Number进行显示转换,结果相同
console.log(Number(undefined));
console.log(Number(null));
console.log(Number(true));
console.log(Number(false));
console.log(Number("123"));
console.log(Number(""))
console.log(Number("123x"))
Object类型 即引用数据类型转成Numberd的规则!!
- 数组只有一个元素,且为数字或者数字字符串,则转成对应的数字
- 空数组,转为0
- 日期Date也可以转成Number
- 其它情况都是NaN
注意:其实数组和对象转成数字类型,都会先转成原始数据类型,这个过程叫ToPrimitive,然后再按上面的规则转成数字
console.log(+[]); // 0
console.log(+[10]) // 10
console.log(+['10']) // 10
console.log(+new Date()) // 1644911468454
console.log(+[1,2]) // NaN
console.log(+[1,"2"]) // NaN
console.log(+{}) // NaN
还有两种不常用的,专门用于字符串转换
- parseInt()
- parseFloat()
parseInt()函数 – 专门转换字符串
- 忽略字符串前面的空格,直至找到第一个非空格字符;
- 若第一个非空格字符是数字字符,则会继续解析,直至解析完或遇到一个非数字字符
- 若第一个非空格字符不是数字字符或负号,则返回NaN
- 能识别各类进制,但最好提供基数作第二个参数表示进制类型
console.log(parseInt('AF',16)); // 正确识别是16进制数
console.log(parseInt('AF')); // 第一个非空格字符并非数字字符或负号,直接返回NaN
parseFloat函数 – 专门转换字符串
- 也是从第一个字符开始解析,直至解析完或遇到无效的浮点数字字符(第二个小数点无效)
- 始终忽略前导零,只解析十进制数(没有第二个参数指定基数)
- 十六进制格式的字符串始终会变成0;
将某个值转成String
转成String类型有两种方式
- “” + value,即value前加上 空字符串和加号
- String(value) 使用String方法显示转换
转换规则
- Undefined类型:undefined会转成’undefined’
- Null类型:null会转成’null’
- Boolean类型:true转成’true’,false转成’false’
- Number类型:直接转成数字字符串
引用数据类型
- 如果是空数组,转成空字符串 “”
- 如果是非空数组,则把数组中每一项拿出,并用逗号分隔,形成字符串
- 如果是对象,转成 ‘[object Object]’
console.log(''+undefined) // 'undefined'
console.log(''+null) // 'null'
console.log(''+true) // 'true'
console.log(''+false) // 'false'
console.log(''+111) // '111'
console.log(''+[]) // ''
console.log(''+[1,2,3]) // '1,2,3'
console.log(''+[1,2,'x']) // '1,2,x'
console.log(''+{}) // [object Object]
console.log(''+{a:1}) // [object Object]
ToPrimitive 对象转成原始数据类型
有时候我们要把引用数据类型,即对象{}或者数组[],转成原始数据类型
实际上就是调用了[[ToPrimitive]]
ToPrimitive(input,type)
如果input为Date对象,则type默认为String
其他情况下,type都认为是Number
当type是Number
- 会先调用valueOf方法,如果是原始值就返回结果
- 如果不是,进一步调用 toString方法,如果是原数组则返回
- 如果不是,抛出错误
当type是String
- 先调用toString方法,如果是原始值就返回结果
- 如果不是,进一步调用 valueOf方法,如果是原数组则返回
- 如果不是,抛出错误
------------多个数据的类型转换---------------
x + y
先判断x和y的成分,有如下四种情况。
- ① 如果有一方是String类型,则应该把另外一方转成String类型
- ② 如果一方是Number类型,另外一方是引用数据类型,双方都转为String类型,再拼接成一个String类型
- ③ 如果一方是Number,一方是原始数据类型,则应该把原始数据类型转换为Number类型
- ④ 有一方为NaN,则结果都为NaN
- ⑤ 都是Number,才进行单纯的加法
确定好情况,再根据单个数据的类型转换进行具体转换
情况①:一方为String,则将另外一方转为String
console.log('test' + undefined)
console.log('test' + null)
console.log('abc' + true)
console.log('abc' + false)
console.log('test' + 2022)
console.log('test' + [])
console.log('test' + [1,2,'a'])
console.log('test' + {})
console.log('test' + {a:1})
// 结果
testundefined
testnull
abctrue
abcfalse
test2022
test
test1,2,a
test[object Object]
test[object Object]
情况②:一方为Number,而另外一方是引用数据类型,都转成String
console.log(2022 + []);
console.log(2022 + [1,2,3])
console.log(2022 + {})
console.log(2022 + {a:1})
// 结果
2022
20221,2,3
2022[object Object]
2022[object Object]
情况③: 一方为Number,另外一方是基本数据类型,则将基本数据类型转成Number
console.log(2022 + undefined)
console.log(2022 + null)
console.log(2022 + true)
console.log(2022 + false)
// 结果
NaN
2022
2023
2022
undefined转为Number是NaN,相当于2022 + NaN,则输出NaN
null是转为0
true是转为1
false是转为0
空字符串转为0
纯数字字符串
题目
console.log({} + 1);
- 一方是Number,一方是引用数据类型,都转成String类型
- {}.valueOf().toString() 得到 “[object Object]”
- 1得到 “1”
- 进行拼接,得到 “[object Object]1”
console.log([] + 1 )
- 一方是Number,一方引用数据类型,都转成String
- 1就得到’1’
- []调用[[ToPrimitive]],即 [].valueOf().toString()得到的是空字符串’’
- ‘’+’1‘
- ’1‘
console.log([1, 2, 3] + 0)
- 一方是Number,一方是引用数据类型,都转成String
- [1,2,3].valueOf().toString(),得到 ‘1,2,3’
- 0得到’0‘
- 进行拼接
- 得到 ’1,2,30‘
console.log(![] + [])
- 逻辑非运算优先级更高,所以先运算 ![]
- ![] 相当于 !Boolean([]),空数组不属于假值列表,返回true,再取反,得到 false
- 变成 false + []
- 右边的空数组是引用数据类型,调用primitive,转成字符串
- [].valueOf().toString(),得到的是空字符串“”
- false + “”
- 一方是字符串,所以把另外一方 false也转成字符串,得到 ‘false’
- ‘false’ + ‘’
- 所以是 ‘false’
console.log([] + {})
console.log({} + []);
- 两方都是引用数据类型,调用[[ToPrimitive]]
- {}.valueOf().toString(),得到的是 “[object Object]”
- [].valueOf().toString(),得到的是空字符串""
- 拼接
- 结果是 “[object Object]”
注意{} + [] 有可能得到别的结果。前面的{}有可能被认作代码块,相当于是 +[],那么相当于是 Number([]),把空数组转成Number,得到0
console.log({} + {}) // '[object Object][object Object]'
console.log([] + []) // ''
- * / 减乘除
减乘除会把非Number转成Number,与加法不同
console.log(1 - true);
- true是Boolean,转成Number,得到1
- 1 -1 结果是 0
console.log('0' - 0);
- ‘0’ 转成Number,得到0
- 0-0还是0
console.log(false - true);
- false转为Number,得到0
- true转为Number,得到1
- 0 - 1,结果是-1
console.log({} - [])
- {}.valueOf().toString(),得到 “[object Object]”
- [].valueOf().toString(), 得到空数组 “”,可以转为0
- "[object Object]"转成Number得到NaN
- NaN - 0,得到NaN
console.log(false - [])
- false可以直接转为Number,得到0
- []要先调用 [[toPrimitive]],[].valueOf().toString(),得到空数组""
- ""转为Number,得到0
- 0 - 0,结果为0
x == y
关于 x == y,强制类型转换有如下步骤:
- 首先,判断x与y的类型是否相同。若是同种类型则比较大小
- 类型若不同,则进行强制类型转换
- 判断是否在比较undefined与null,是则返回true
- 判断二者类型是否分别为String和Number。若是,则将String转为Number
- 判断是否有一方是Boolean。若是,则将Boolean转为Number
- 判断是否一方为Object,一方为String、Nunber或Symbol。若是,则将Object转为原始类型 (调用[ToPrimitive])。
- 如果双方都是对象,那么就看是否指向同一对象
- NaN与什么都不等,NaN == NaN返回false
- +0 == -0 返回true
具体流程图为:
题目
console.log ( [] == 0 ); //true
- ==两边一方为数组即对象,一方为Number。所以要将Object转为原始数据类型,即调用[[ToPrimitive]]
- 数组会先调用valueOf方法,得到数组自身,但还并非原始数据类型,进而继续调用toString()方法
- [].toString() => “”。空数组转成字符串,得到的是空字符串
- 现在相当于比较 “” == 0
- 现在一方是String,一方是Number。故应将String转为Number,空字符串转为Number得到0
- 0 == 0,故为true
console.log( [] == false ); // true
- 一方为Boolean,所以false要先转成0
- 之后和上个例子相同
console.log ( ![] == 0 ); //true
- 逻辑非运算!的优先级更高,所以先运算 ![]
- ![] 相当于 !Boolean([]),需要把空数组[]转为Boolean类型再取反
- 空数组转为Boolean类型,不属于假值列表,故得到true
- true再取反,得到false
- 现在相当于比较 false == 0
- 一方为Boolean类型。故应将Boolean转为Number
- false转为Number,得到0
- 0 == 0 成立
console.log ( [] == ! [] ); //true
- 如上,会先运算 ![],即 !Boolean([]),得到false
- 相当于比较 [] == false
- 现在一方是Boolean,所以要把Boolean类型转为Number,故false转为0
- 相当于比较 [] == 0
- 现在一方是 数组Object,一方是Number,所以数组[]要调用 [[ToPrimitive]]
- [].valueOf().toString(),得到空数组 “”
- 即比较 “” == 0
- 一方为String,一方为Number,所以把空字符串转为Number
- 空字符串转为Number,得到0
- 0 == 0,故成立
console.log ( [] == [] ); //false
console.log ( {} == {} ); //false
- 双方都是对象,它们指向的地址是不一样的!是两个不同的对象!所以返回false
console.log({} == !{}); //false
- !{}的运算优先级更高,相当于计算 !Boolean({})
- 空对象转为Boolean,不在假值列表,所以转为true,再取反,得到false
- 相当于比较 {} == 0
- 现在一方是Object,一方是Number
- {}要转成原始数据类型,先调用valueOf,得到 [object Object],并非原始数据类型,进一步调用toString
- 也就是 {}.valueOf().toString(),得到 “[object Object]”
- 现在比较 “[object Object]” == 0
- 一方为字符串,一方为Number,要把字符串转为Number
- 但显然 “[object Object]”转成Number得到的是NaN
- NaN == 0 得到false
console.log( {} == false ); // false
- 首先,一方为Boolean,所以先将false转为Number,得到0
- {} == 0,现在一方是对象Object,一方是Number,所以对象需要调用[[ToPrimitive]],即 {}.valueOf().toString()
- 得到的是 “[object Object]”
- “[object Object]” == 0,一方是String,一方是Number,需要把String转为Number
- "[object Object]"转成Number,得到NaN
- NaN == 0,返回false
console.log( [2] == 2 ); // true
- 一方是数组属于Object,一方是Number,所以数组要调用[[ToPrimitive]]
- 也就是 [2].valueOf().toString()
- [2].valueOf()得到的是数组本身,也就是[2],进一步调用toString()
- [2].toString(),转成字符串,得到’2’
- 相当于比较 “2” == 2
- 一方是String,一方是Number,故应将String转为Number
- "2"可以转成2
- 2 == 2 故为true
console.log( ['0'] == false ); // true
- 一方是Boolean,所以先将Boolean转为Number,false转成 0
- [‘0’] == 0
- 一方是数组,一方是Number,所以数组要调用 [[ToPrimitive]]
- [‘0’].valueOf().toString(),得到 ‘0’
- ‘0’ == 0 一方是String,一方是Number,所以String要转成Number
- '0’转成0
- 0 == 0 所以成立
console.log( [undefined] == false ); //true
console.log( [null] == false ); // true
- 首先一方是false,所以先将Boolean转为Number,得到0
- [null] == 0,一方是数组属于Object,一方是数字Number
- 数组需要调用[[ToPrimitive]],即[null].valueOf().toString();
- 注意!undefined和null其实就是空,所以得到空数组,进而得到空字符串
- “” == 0
- 空字符串转为 0
- 0 == 0,故成立
console.log( undefined == false );//false
console.log( null == false); //false
console.log( undefined == null);
- 一方为Boolean,false会转为Number,得到0
- undefined == 0?没有然后了,返回false
- null == 0? 没有然后了,返回false
补充一下,最近做到一个很坑的题目:
console.log('test' == new String('test'))
console.log('test' === new String('test'))
console.log(new String('test') == new String('test'))
console.log(new String('test') === new String('test'))
//true,false,false,false
- 使用new String()得到的是一个对象。
- 第一个会进行强制类型转换, new String(‘test’).valueOf().toString()
- 第二个===,类型不同,直接返回false
- 两个对象进行比较,比较的是地址。显然不是一个对象,地址不同,都返回false
------------------- 其它操作符 -----------------
&& 与 || 操作符
- 会将操作数转换为Boolean
- && 的优先级高于 ||
- && 与 || 都有短路特性
如:
(表达式1)&& (表达式2)
- 若表达式1为true,返回的是表达式2(结果由表达式2决定);
- 若表达式1为false,表达式2不会被计算,返回表达式1(结果由表达式1直接决定)
(表达式1) || (表达式2)
- 若表达式1为true,表达式2不会计算,直接返回表达式1
- 若表达式1为false,返回的是表达式2(结果由表达式2决定)
3||2&&5||0的计算步骤
- 2&&5,返回表达式2,即返回5
- 3||5,返回表达式1,即返回3
- 3||0,返回表达式1,即返回3
== 操作符、=== 操作符 与 Objece.is()
- == 操作符会先判断A与B的数据类型,若不相等,则会按照规则进行强制类型转换
- === 操作符不会进行强制类型转换,若A与B类型不等,直接返回false
- Object.is(),大部分情况的结果与 === 操作符相同。但处理了特殊情况:+0 与 -0 不相等;NaN 与 NaN相等
console.log(+0 == -0)
console.log(+0 === -0)
console.log(Object.is(+0,-0))
console.log(NaN == NaN)
console.log(NaN === NaN)
console.log(Object.is(NaN,NaN))
// true
// true
// false
// false
// false
// true
---------------- JavaScript 基础 -----------------
new操作符的原理
使用new操作符后,创建对象实例,经过以下几个步骤:
- 创建一个空对象obj
- 将obj的 __ proto __ 指向原型对象(constructor.prototype)
- 让构造函数的this指向新的对象obj,执行构造函数的内容
- 返回结果
Object.create(proto,[propertiesObject])
- proto参数指定新创建对象的原型对象
function new_(constructor, ...args){
// step1-2 创建一个obj对象,并指定其 __ proto __ 为 constructor.prototype
const obj = Object.create(constructor.prototype);
// step3 让构造函数内部的this指向obj,并执行
const result = constructor.apply(obj,args);
return result instanceof Object ? result : obj;
}
function Person(name,age){
this.name = name;
this.age = age;
}
const person1 = new_(Person,'Silam',22)
Object.assign和扩展运算符
一、Object.assign()
let outObj = {
inObj: {a: 1, b: 2}
}
let newObj = Object.assign({}, outObj)
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}
二、扩展运算符
let outObj = {
inObj: {a: 1, b: 2}
}
let newObj = {...outObj}
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}
结论:
- Object.assign与扩展运算符都是浅拷贝
- Object.assign()的第一个参数是目标对象,后面的所有参数作为源对象。把所有的源对象合并放到目标对象中
- 扩展运算符(…),会将数组或对象中的每一个值拷贝到新的数组或对象当中
深拷贝与浅拷贝
一、普通赋值拷贝
- 基本数据类型,互不影响
- 引用数据类型,互相影响
二、浅拷贝
- 对象中的基本数据类型互不影响
- 对象中的引用数据类型互相影响
- 三种实现:Object.assign()、扩展运算符(…)、Array.prototype.slice()
三、深拷贝
- 对象中的基本数据类型互不影响
- 对象中的引用数据类型互不影响
- 三种实现:JSON.parse(JSON.stringify(obj))、JQ的extend方法、lodash的cloneDeep()
深拷贝① JSON.parse(JSON.stringify(obj))
注意,有弊端:undefind、symbol、function会消失!!!
const obj1 = {
a:1,
b:[1,2,3],
c:{
c1:'c1'
},
d:undefined,
e:Symbol('e'),
f:function(){
console.log('function')
}
}
const obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj2) // undefined,symbol,function都没了
深拷贝② lodash的cloneDeep方法
const obj1 = {
a:1,
b:[1,2,3],
c:{
c1:'c1'
}
}
const _ = require('lodash');
const obj2 = _.cloneDeep(obj1);
console.log(obj2)
深拷贝③ Jquery的extend方法
const obj1 = {
a:1,
b:[1,2,3],
c:{
c1:'c1'
}
}
const $ = require('jquery');
const obj2 = $.extend(true,{},obj1);
console.log(obj2)
深拷贝④ 递归
function myCloneDeep(obj){
if(!obj || typeof obj !== 'object') return obj;
let newObject = Array.isArray(obj) ? [] : {};
for(let key in obj){
newObject[key] = typeof obj[key] === 'object' ? myCloneDeep(obj[key]) : obj[key]
}
return newObject;
}
const obj1 = {
a:1,
b:[1,2,3],
c:{
c1:'c1'
}
}
const obj2 = myCloneDeep(obj1);
console.log(obj2)
DOM
DOM的概述
- DOM(Document Object Modal),即文档对象模型。是对文档的结构化表述,并提供了对文档结构的访问方式。
- DOM有三种节点:元素节点、文本节点和属性节点
创建节点
DOM操作可以创建:
- 元素节点 createElement
- 文本节点 createTextNode
- 属性节点 createAttribute
- 文档碎片 createDocuentFragment
createElement —— 创建元素节点
const divE1 = document.createElement('div');
createTextNode —— 创建文本节点
const testE1 = document.createTextNode('content');
createAttribute —— 创建属性节点
const attribute = document.createAttribute('customAttribute');
createDocumentFragment —— 创建文档碎片
const fragment = document.createDocumentFragment();
// 文档碎片是一个轻量级文档
// 主要用于存储临时节点
// 在需要时,将文档碎片中的内容一次性全部添加到文档中
获取节点
和CSS选择器可以对应
- document.getElementById —— 通过ID获取
- document.getElementsByClassName —— 通过类名Class获取
- document.getElementsByTagName —— 通过标签名获取
- document.getElementsByName —— 通过属性名
- document.querySelector —— 通过选择器获取一个元素(第一个)
- document.querySelectorAll —— 通过选择器获得一组元素
- document.documentElement —— 获取html标签
- document.body —— 获取body标签
更新节点
- innerHTML
- innerText
- textContent
- style
const ele = document.getElementById('testdiv');
ele.innerHTML = ...;
ele.innerText = ...;
ele.textContent = ...; // 与innerText的兼容性不同
ele.style.color = '';
ele.style.font;
添加节点
- a.appendChild(b) —— 将子节点b添加到父节点里的最后
- setAttribute(a,b) —— 添加属性节点,a为属性名,b为属性值
删除节点
- parent.removeChild(self),self是要待移除的节点,parent是该节点的父节点
const self = document.getElementById('to-be-removed');
const parent = self.parentElement
parent.removeChild(self);
BOM
- BOM Browser Object Modal,即浏览器对象模型
- 将浏览器当作对象来看待
- BOM的核心是window,window对象又有location、navigator和screen对象
对AJAX的理解
AJAX 全称为 Async JavaScript and XML
原理:通过XMLHttpRequest对象向服务器发送异步请求,从服务器获得数据,然后利用JavaScript来操作DOM而更新页面的对应部分。
Ajax可以实现异步请求,即可以在不重新加载整个页面的情况下,对网页的某些部分进行更新。(传统的网页若不使用Ajax,更新内容则会对整个页面进行加载)
一、创建XMLHttpRequest对象
const request = new XMLHttpRequest();
二、与服务器建立连接
request.open(method,url)
- method 即请求方式,常见有GET、POST
- url 服务器的地址
三、向服务器发送数据
request.send([body]);
如果请求方法是GET
- 需要将数据添加到open方法中的url中
- 需要将send方法中的参数设置为null
四、XMLHttpRequest对象需绑定onreadystatechange事件
onreadystatechange事件主要监听服务器端的通信状态,主要监听的是XMLHttpRequest.readyState属性:
- readyState == 0,UNSENT,即未调用open方法
- readyState == 1,OPENED,即未调用send方法
- readyState == 2,HEADERS_RECEIVED,send方法已调用,已经返回响应头和响应状态
- readyState == 3,LOADING,正在下载响应体
- readyState == 4,DONE,整个请求过程完毕
只要XMLHttpRequest.readyState一变化,就会触发readystatechange事件,XHLHttpRequest.responseText则是服务器端返回的响应结果
const request = new XMLHttpRequest()
request.onreadystatechange = function(e){
if(request.readyState === 4){ // 整个请求过程完毕
if(request.status >= 200 && request.status <= 300){
console.log(request.responseText) // 服务端返回的结果
}else if(request.status >=400){
console.log("错误信息:" + request.status)
}
}
}
request.open('POST','http://xxxx')
request.send()
封装一个AJAX
//封装一个ajax请求
function ajax(options) {
//创建XMLHttpRequest对象
const xhr = new XMLHttpRequest()
//初始化参数的内容
options = options || {}
options.type = (options.type || 'GET').toUpperCase()
options.dataType = options.dataType || 'json'
const params = options.data
//发送请求
if (options.type === 'GET') {
xhr.open('GET', options.url + '?' + params, true)
xhr.send(null)
} else if (options.type === 'POST') {
xhr.open('POST', options.url, true)
xhr.send(params)
//接收请求
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
let status = xhr.status
if (status >= 200 && status < 300) {
options.success && options.success(xhr.responseText, xhr.responseXML)
} else {
options.fail && options.fail(status)
}
}
}
}
调用AJAX
ajax({
type: 'post',
dataType: 'json',
data: {},
url: 'https://xxxx',
success: function(text,xml){//请求成功后的回调函数
console.log(text)
},
fail: function(status){请求失败后的回调函数
console.log(status)
}
})
Ajax,Axios,Fetch的区别
一、Ajax
- 原理:通过XMLHttpRequest对象向服务器发送异步请求,从服务器获得数据,然后利用JavaScript来操作DOM而更新页面的对应部分。
- 作用:Ajax可以实现异步请求,即可以在不重新加载整个页面的情况下,对网页的某些部分进行更新。(传统的网页若不使用Ajax,更新内容则会对整个页面进行加载)
Ajax的缺点:
- 本身是针对MVC编程,不符合MVVM的新浪潮
- Ajax是基于原生XHR(XMLHttpRequest)开发的,而XHR官网本身的文档架构不清晰
- 不符合关注点分离的原则
二、Axios
- 原理:axios也是对XMLHttpRequest对象的封装,只不过是基于Promise去实现,符合最新的ES规则
三、fetch(号称是AJAX的替代品)
- 原理:fetch是基于promise设计的,使用的是原生JS。它并不是Ajax的进一步封装,没有使用XMLHttpRequest对象
优点:
- 基于promise实现,可以使用async,await
- 语法更加简洁
- 没有使用XMLHttpRequest
缺点:
- fetch只对网络请求报错,即使返回400、500状态码仍然认为是成功的,并不会进行reject。
- fetch默认不会携带cookie,需要添加配置项: fetch(url, {credentials: ‘include’})
- fetch 没有办法原生监测请求的进度,而 XHR 可以。
关注点分离原则
关注点分离 即 Separation Of Concerns SOC,是软件架构设计的原则。
如果关注点杂糅在一起,会大大增加软件的复杂性。
SOC的具体说明:
- 好的架构设计必须把变化点错落有致地封装到软件系统的不同部分。想要做到一点,必须进行关注点分离。
- 好的架构必须使每个关注点分离:系统中的一个部分发生变化,不会影响到其他部分
- 即使需要改变,也可以清晰地识别出哪些部分需要改变
- 如果需要拓展架构,影响将会是最小化,已经可以工作的每个部分仍可以继续工作。
软件架构模式 MVC, MVP, MVVM **
延迟加载JavaScript脚本的方法
JS脚本会阻塞文档的解析,会发生下面的事情:
- 浏览器暂停对文档的解析,停止构建DOM Tree
- 将控制权交给JavaScript引擎
- 待JavaScript加载、解析、执行完毕后,再恢复对文档的解析
所以,延迟JS脚本的加载,有利于文档的解析。
有以下方法:
- script标签添加defer
- script标签添加async
- 把script标签放在文档的底部
- 使用setTimeout
插播,回忆一下defer与async的区别:
- defer:遇到script标签后不会停止对文档的解析:后续文档的解析与script的加载解析同步进行,但是script的执行要等到文档解析完毕
- async:同样不会停止对文档的解析:文档的解析与script的加载解析同步进行,但script在加载解析完就立即执行
- 执行顺序:多个defer能按加载顺序执行,多个async不能保证执行顺序
判断一个对象是否为空对象的方法
方法一:JSON.stringify()
console.log(JSON.stringify(obj) == '{}');
方法二:Object.getOwnPropertyNames()
- Object.getOwnPropertyNames()方法返回的是对象obj所有自身属性的属性名组成的数组
- 若数组长度不为0,表明对象自身拥有属性,则不是空对象
console.log(Object.getOwnPropertyNames(obj).length == 0)
方法三:Object.keys()
- Object.keys()方法返回的是对象obj所有可枚举属性的属性名组成的数组
- 判断数组长度是否为0
console.log(Object.keys(obj).length == 0);
注意:
- Object.getOwnPropertyNames()返回的是对象obj自身所有属性,包括了不可枚举的;
- Object.keys()返回的是仅是可枚举属性
正则表达式
创建正则表达式对象:
- 通过两个反斜杠 / /
- 通过new RegExp()
相关常用的方法
- 字符串的方法 match、replace
- 正则表达式对象上的方法 test
只匹配一个字符/字符串:
/reg/ 匹配第一个'reg'字符串
/[reg]/ 匹配r、e、g三个字母中任意一个字母,匹配出第一个
/[a-z]/ 匹配a-z中的字母,匹配出第一个
/[0-9]/ 匹配0-9的数字,匹配出第一个
量词匹配
- value * :value匹配0次或多次
- value +:value匹配1次或多次
- value ?:value匹配0次或一次
- value{2}:value匹配2次
- value{2,4}:value匹配2,3,4次
- value{2,}:value匹配2次或多次
正则选项,即在正则表达式后添加正则选项
- i 匹配结果忽略大小写
- g 全局匹配(不止一个)
- m 多行匹配
/[a-z]/i 可以匹配a-z的字母,不分大小写
/[0-9]/g 可以匹配出所有0-9的数字
边界匹配,^和$
- ^ value :字符串得以value开始
- value $ :字符串得以…结束
字符匹配
- . 匹配除了换行符外的任何字符
- \d 匹配数字
- \D 匹配非数字
- \s 匹配空白字符
- \S 匹配非空白字符
- \w 匹配a-z A-Z 0-9 下划线_
- \W 相当于 [ ^ A-Z a-z 0-9 _]
常见应用
//匹配QQ号
var regex = /^[1-9][0-9]{4,10}$/g;
//手机号码
var regex = /^1[34578]\d{9}$/g;
//用户名
var regex = /^[a-zA-Z\$][a-zA-Z0-9_\$]{4,16}$/;
对JSON的理解
- JSON是一种基于文本的、轻量级的数据交换格式。它可以被任何一种语言读取和作为数据格式来交换
- 前端:将一个符合JSON格式的数据结构序列化为JSON字符串,传递给后端;
- 后端:将JSON字符串进行解析,得到相应的数据结构
- JSON.stringify():传入一个符合JSON格式的数据结构,将其转成JSON字符串
- JSON.parse():将JSON字符串转为JS数据结构
另外:JSON.parse(JSON.stringify(obj)) 还是实现深拷贝的一种方法。
use strict 严格模式
use strict 使得 JavaScript 在更严格的条件下运行,即为严格模式
设严格模式有以下目的
- 消除JavaScript语法的不合理、不严谨之处,减少怪异行为;
- 消除代码的不安全之处,保证安全运行
- 提高编译器效率,提高运行速度
- 为未来新版本的JS做好铺垫
严格模式的区别:
- 不可以使用with语句
- 不可以用this关键字指向全局对象
- 对象不可以有重名的属性
事件流
事件流
- 当子元素所绑定的事件被用户触发时,页面接收事件的顺序是什么,就涉及到事件流。事件流即事件的传播方式。
- 事件流有三个阶段:事件捕获、处于目标、事件冒泡
- 事件捕获:由上到下的传播方式,从不具体的节点逐级向下传播到最具体的节点。(Netscape事件流)
- 事件冒泡:由下到上的传播方式,从最具体的元素接收再逐级向上传播到document节点。(IE事件流)
事件模型
事件模型,分作以下三类:
- DOM0级(原始事件模型)
- DOM2级(标准事件模型)
- IE事件模型
DOM0级(原始事件模型)
绑定/移除方式:
// 直接在html中绑定onclick事件
<input type='button' onclick="func()">
// 在js中绑定onclick事件
var btn = document.getElementById('.btn');
btn.onclick = func;
//移除DOM0级事件
btn.onclick = null;
优点:
- 可以直接在html和js中绑定事件,移除事件则设置为null即可,比较简单
- 有很好的跨浏览器优势,可以最快的速度去绑定事件
缺点
- 绑定的速度过快,有可能页面还未加载完毕而导致绑定的事件无法正常执行
- 只支持事件冒泡,不支持事件捕获
- 同一个类型的事件只能绑定一次(如果有多的,后续会覆盖)
DOM2级(标准事件模型)
绑定/移除方式:
element.addEventListener(event, handler, useCapture);
element.removeEventListener(eventType, handler, useCapture)
- eventType 指定事件类型(没有on)
- handler 指定事件处理函数
- useCapture 是否使用事件捕获。一般为false使用事件冒泡与IE保持一致
DOM2级(标准事件模型)共有三个流程:
- 事件捕获阶段:事件从document一直向下传播到目标元素,依次检查经过的节点,若有绑定事件监听函数则执行;
- 处于目标阶段:事件到达目标元素,触发目标元素的监听函数
- 事件冒泡阶段:事件从目标元素向上冒泡到document,依次检查经过的节点,若有绑定事件监听函数则执行
特点:
- 可以绑定多个同类型事件,不会冲突覆盖
- 第三个参数useCapture,若设置true,则在事件捕获阶段执行事件处理函数;若设置为false,则在事件冒泡阶段执行事件处理函数
IE事件模型
绑定/移除方式
attachEvent(eventType, handler)
detachEvent(eventType, handler)
IE事件模型有两个流程:
- 事件处理阶段:事件到达目标元素,触发目标元素的监听函数
- 事件冒泡阶段:事件从目标元素向上冒泡到document,依次检查经过的节点,若有绑定事件监听函数则执行
事件委托
概述:
- 事件委托就是将一个或一组元素的事件委托给它的父元素,真正绑定事件的是外层元素,而不是目标元素
- 当目标元素触发事件时,会通过事件冒泡机制从而触发外层元素的绑定事件,是在外层元素执行事件处理函数。
- 适合事件委托的事件有:click、mousedown、mouseup、keydown、keyup、keypress
应用情景:
- 有一个列表list,当中每个list-item都需要响应事件:若为每个li去绑定事件,将消耗巨大的内存。
- 动态增加、删除li元素:需要对即将增加的li元素绑定事件,对即将删除的元素解绑事件,操作繁琐。
事件委托的优点:
- 可以减少内存消耗,提升性能
- 进行动态绑定,减少重复工作
事件委托的局限:
- 对于像focus,blur这样没有事件冒泡机制的事件,无法进行事件委托。(事件委托是基于事件冒泡机制进行的)
preventDefault(),stopPropagation(),return false
- preventDefault() 即,阻止默认事件。如点击链接,链接不会被打开(默认事件被阻止了),但仍会继续事件冒泡。
- stopPropagation() 即,阻止事件冒泡。默认事件会执行,但绑定的事件不会向上进行冒泡。
- return false 相当于 preventDefault + stopPropagation
JavaScript 本地缓存 **
JS的本地缓存主要有以下4种:
- Cookie
- sessionStorage
- localStorage
- indexedDB
所谓 WebStorage 有以下2种
- sessionStorage
- localStorage
下面正式介绍这四种本地缓存
一、Cookie
基本特性:
- HTTP是无状态协议。即不论是会频繁被使用的数据与否,在下一次使用时都会再次被传输该数据,占用资源影响性能。Cookie则是一种辨别用户身份的数据
- Cookie是存储在浏览器本地的
- Cookie只能保存String类型对象
- Cookie安全性不是很高
- 单个Cookie大小不超过4K,多数浏览器限制一个站点不能超过20个Cookie,一个浏览器最多就能有300个Cookie
------------------- 关于数组 -----------------
数组的原生方法
一、增
- push() —— 接收任意数量的参数,添加到数组的末尾,返回数组的最新长度
- unshift() —— 接收任意数量的参数,添加到数组的头部,返回数组的最新长度
- splice() —— 接收三个参数:开始位置、删除元素的数量(设为0)、插入的元素;返回的是被删除元素的数组(此时是空数组)
- concat() —— 创建当前数组的副本,并将参数添加到副本数组的末尾,并返回新构建的数组
注意:
- push、unshift 和 splice 会对数组造成影响;
- concat 不会对原数组产生影响,返回的是新数组。
二、删
- pop() —— 删除数组的最后一项,length-1,并返回被删除的项
- shift() —— 删除数组的第一项,length-1,并返回被删除的项
- splice() —— 接收两个参数:开始位置、删除元素的数量;返回的是被删除元素的数组
- slice() —— 接收两个参数:开始位置start和结束位置end(不包括该元素);对数组进行切片,返回start与end(不包含)的数组
注意:
- pop、shift 和 splice 会对数组造成影响
- slice 不会对原数组产生影响
三、改
- splice() —— 接收三个参数:开始位置、删除元素的数量、插入的元素;返回的是被删除元素的数组;
注意:splice方法会对原数组产生影响
四、查
- indexOf —— 若元素存在,返回其首次出现位置的下标;若不存在,返回-1
- includes —— 若元素存在,返回true;若不存在,返回false
- find —— 返回第一个匹配的元素
五、排序
- reverse() —— 对数组逆序
- sort() —— 对数组进行排序
六、转换
- join() —— 接收一个参数,作为字符串分隔符;返回带有分隔符的所有元素的字符串
let array = [1,2,3,4,5];
console.log(array.join('、')); // 字符串:1、2、3、4、5
七、迭代
都不改变原数组
- some —— 对数组的每一项执行传入的函数,有一项元素返回true,则返回true
- every —— 对数组的每一项执行传入的函数,每一项都返回true,则返回true
- filter —— 对数组的每一项执行传入的函数,函数返回true的项会组成新的数组,结果返回新的数组
- map —— 对数组的每一项执行传入的函数,每一项执行函数后得到新的结果,返回由新结果组成的新数组
- forEach —— 无返回值
数组的遍历方法
some和every是判断为主:
some方法 —— 遍历数组的每一项,对其执行传入的函数,有一项元素返回true,则返回true
const array = [1,2,3,4,5];
console.log(array.some(item => item>4)) // true
every方法 —— 遍历数组的每一项,对其执行传入的函数,只有所有项都返回true,才返回true
const array = [1,2,3,4,5];
console.log(array.every(item => item>4)) // false
filter方法 —— 遍历数组的每一项,对其执行传入的函数,返回true的项会组成新的数组,结果返回新的数组
因为filter方法会返回匹配为true的项组成的新数组,所以它可以进行链式调用
const array = [1,2,3,4,5];
console.log(array.filter(item => item>4)) // [5]
map方法 —— 遍历数组的每一项,对其执行传入的函数,每一项都会得到新的结果,由新的结果组成新数组,结果返回新的数组,不会改变原数组。
因为map方法会返回由新结果组成的新数组,所以它可以进行链式调用
const array = [1,2,3,4,5];
console.log(array.map(item => item*4))
console.log(array)
// [ 4, 8, 12, 16, 20 ]
// [ 1, 2, 3, 4, 5 ]
forEach方法 —— 遍历数组的每一项,将每一项元素传给回调函数。
回调函数中可以接收三个参数:
- item —— 当前元素 (必须)
- index —— 元素的索引值 (可选)
- array —— 当前元素所属的数组对象(可选)
forEach方法可以接收第二个参数,用于指定回调函数内部的this(前提是回调函数不能是箭头函数,因为箭头函数没有this)
注意:前四种都有返回值,forEach方法没有返回值,也不会改变原数组
const array1 = [1,2,3,4,5];
const array2 = [6,7,8,9,10];
array1.forEach((item,index) => console.log(item + ':'+ index))
array1.forEach(function(item,index){
console.log(this[index])
},array2) ;// 输出 6,7,8,9,10
还有 for…in,for…of
还有 indexOf,includes,find等查找方法
for…in 和 for…of 的区别
for…of循环是ES6新增的遍历方法,它的本质是调用数据结构内部的Iterator接口(遍历器对象),也就是访问Symbol.Iterator属性。
区别:
- for…in循环获取的是对象的键名;for…of循环获取的是对象的键值
- for…in会遍历对象的整条原型链,性能差;for…of只遍历当前对象
- for…in会返回数组的所有可枚举属性(包括原型链上的可枚举属性);for…of只返回数组对应下标的值。
总结:
- for…in适合用于遍历对象
- for…of可以用于遍历所有具有Iterator接口的数据结构,原生即具有Iterator接口的数据结构有 Array,String,Set,Map,类数组对象(Arguments和NodeList)
注意:
- 普通的对象直接用for…of循环是会报错的,因为它没有部署Iterator接口,即没有Symbol.Iterator属性。
- 对于普通的对象,可以为它添加[Symbol.Iterator]属性,指向一个迭代器,则可以用for…of循环;或者用Array.from(obj)转成数组,则可以用for…of循环
数组去重
方法一、双重for循环 + splice方法
原理:从数组的第一项元素开始遍历,判断后面的元素当中是否有与被查的元素相同,有则用splice方法删除
function method(arr){
for(var i = 0; i<arr.length; i++){
for(var j = i+1; j < arr.length; j++){
if(arr[i]==arr[j]){
arr.splice(j,1);
j--;
}
}
}
return arr;
}
方法二、利用indexOf方法
原理:indexOf方法可以返回被查元素在数组中第一次出现的位置下标,如果不存在则返回-1
创建一个新数组array_,逐个遍历原数组array的元素是否在新数组中。利用IndexOf方法若返回-1,则说明该元素还未出现在新数组array_中,则将该元素push进去。
function method(arr){
var array_ = []; // build a new array
for(var i = 0; i < arr.length; i++){
if(array_.indexOf(arr[i]) == -1){
//arr[i]还未出现在array_中
array_.push(arr[i]);
}
}
return array_
}
方法三、利用includes方法
和方法二类似
function method(arr){
var array_ = [];
for(var i = 0; i < arr.length; i++){
if(!array_.includes(arr[i])){
array_.push(arr[i]);
}
}
return array_;
}
方法四、利用filter和indexOf方法
indexOf返回的是元素在数组中首次出现位置的下标,如果首次出现的下标与当前的index不同,说明是重复的。
function method(arr){
return arr.filter((item,index)=>{
return arr.indexOf(item) === index;
})
}
方法五、利用ES6的数据结构Set
Set是ES6提供的数据结构,类似于数组,但不同的是其数据成员得是唯一的,不得重复
可以先用new Set()构造函数生成一个Set数据结构,它不会把重复的数据添加进去,从而实现去重;最后再用Array.from将其转换为数组。
function method(arr){
return Array.from(new Set(arr));
}
数组的乱序输出
var arr = [1,2,3,4,5,6,7,8,9,10];
for (var i = 0; i < arr.length; i++) {
const randomIndex = Math.round(Math.random() * (arr.length - 1 - i)) + i;
[arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];
}
console.log(arr)
数组的扁平化
方法一、递归
如果某一项是数组,则进一步调用flatten函数
let arr = [1, [2, [3, 4, 5]]];
function flatten(arr) {
let result = [];
for(let i = 0; i < arr.length; i++) {
if(Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i]));
} else {
result.push(arr[i]);
}
}
return result;
}
flatten(arr); // [1, 2, 3, 4,5]
方法二、扩展运算符
let arr = [1, [2, [3, 4]]];
function flatten(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
方法三、ES6的flat方法
let arr = [1, [2, [3, 4]]];
function flatten(arr) {
return arr.flat(Infinity);
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
数组元素求和
一、reduce方法
reduce(function(accumulator,currentValue), 0)
accumulator累加器,并设置初始值为0
let arr=[1,2,3,4,5,6,7,8,9,10]
let sum = arr.reduce( (total,i) => total += i,0);
console.log(sum);
二、遍历数组逐个相加
类数组对象
类数组对象与数组相似,具有以下特点:
- 有length属性
- 能使用[],通过下标去访问元素
- 但不能使用数组特有的方法
常见的类数组对象有:
- arguments
- NodeList,即DOM方法返回的结果
NodeList可以通过以下代码获得,举例如:
- childNodes
- getElementByName
- getElementByClassName
- getElementByTagName
类数组对象如何转化为数组
将类数组对象转化为数组通常有以下几种方法
- Array.prototype.slice.call(arrayLike)
- Array.prototype.splice.call(arrayLike,0)
- Array.prototype.concat.apply([],arrayLike)
- Array.from(arrayLike)
arguments
一、 arguments参数列表,属于类数组对象
二、 类数组对象有以下三点特征,在arguments上的描述如下:
- 具有length属性:arguments.length 可以访问其实参列表的大小
- 可通过下标访问元素:arguments[0],arguments[1]
- 不能调用数组特有的方法
三、 此外arguments还有 __ proto __ 与 callee 两个属性值得我们注意:
- __ proto __ :指向的是Object,也能说明它不是数组,而是类数组对象
- callee:表示的是对函数本身的引用。
callee的两个作用
- arguments.callee.length表示的是形参列表大小(arguments.length是实参列表大小);
- 通过 arguments.callee() 来实现递归
-----------------原型与原型链-----------
原型链:__ proto __ 、prototype 、constructor
基本关系:
- 函数f有prototype指针,指向原型对象 f.prototype
- 原型对象f.prototype中有constructor,指向构造函数f
- f1是函数f的实例,其中有__proto__ (dunder proto)指向原型对象f.prototype。另外,它可以继承原型对象f.prototype中的constructor,指回构造函数f
原型链:
- 当访问一个对象的属性时,若找不到,则会去其原型对象找。找对象实例的原型对象,是通过 __ proto __ 去访问的。
- 通过 __ proto __ 不断连接对象实例与原型对象,得到了原型链
- 原型链的终点是null,即 __ proto __ 最后指向null
按部分分析:
部分一、
关系:Foo()是构造函数,f1是其实例对象,Foo.prototype是原型对象
- Foo()应当有prototype指向原型对象,即Foo.prototype
- 原型对象Foo.prototype有constructor指回构造函数Foo()
- 实例对象f1有 __ proto __ 指向原型对象Foo.prototype
- 实例对象f1从原型对象Foo.prototype中,继承constructor指针,指向构造函数Foo()
部分二、
原型链:通过__ proto __ 不断连接实例对象与原型对象,终点是null
部分三、
- 也类似图一的三角关系:Foo()函数其实也是Function()函数的一个实例,Function()是构造函数,自然有Function.prototype原型对象
部分四、
- 也是一个三角关系,此时Object()是Function()的一个实例
部分五、
- 这也是一个三角关系,但是略复杂:Function()的构造函数就是Function()自身,相当于Function()构造函数与实例重合了
-------声明变量、作用域链、闭包-----------
变量声明 var,let,const
- 作用域: var是函数作用域(局部或全局);let和const是块级作用域({}花括号内)
- 特性: var会变量提升;let和const是暂时性死区(在声明之前不可以使用)
- 声明: var可以重复声明;let和const不可以重复声明变量
- 初始化: var和let可以不用初始化,const声明后必须初始化
- 更改: var和let可以更改指针指向,const不能改变指针指向
注意:如果没有通过var let const去声明变量,直接对变量进行赋值,那么这个变量就会意外暴露成全局变量,有可能会造成内存泄漏!
变量提升、函数提升
变量提升与函数提升的规则可简单总结为:
- 提升的仅仅是变量或函数的声明部分,赋值语句待在原地不动
- 提升到作用域的最顶上
- 函数声明提升的优先级高于变量提升
- 函数表达式不会被提升
- 对于变量声明来说,若有重复声明,后续的重复声明会被优化忽略
- 对于函数声明来说,后定义的函数声明会覆盖先前的函数声明
具体参见 变量提升、函数提升
为什么会进行变量提升与函数提升:
- JavaScript代码有三个阶段:加载、解析、执行。也就是说JS代码在执行之前会经过JS引擎的解析
- 在代码执行之前,也就是解析阶段,会创建全局执行上下文和函数执行上下文,也就是会创建执行上下文。
- 执行上下文有三个生命周期:创建阶段、执行阶段和回收阶段;
- 其中的创建阶段做了三个事情:this的绑定、创建词法环境、创建变量环境
- 而词法环境则记录了函数的声明、let和const的绑定情况
- 而变量环境则记录的是var的绑定情况。
- 总结:JS解析阶段,创建了执行上下文 => 执行上下文的创建阶段,又完成了词法环境和变量环境的创建 => 词法环境和变量环境的创建,也就记录了函数的声明和变量的声明情况。
优点1:提升性能
- 在JS的解析阶段,JS引擎会检查语法,进行预编译。主要是对即将会使用到的变量和函数提前声明,这个操作只进行一次。而在执行阶段,需要访问一个变量和函数时,就通过作用域链去查找。
- 如果不这么做,那么在执行阶段,每遇到一次变量或函数则需要对其进行一次解析,这是没有必要的!
优点2:容错性好
- 变量提升和函数提升,可以一定程度提高JS的容错性,让一些不太规范的代码也能执行不报错
缺点:可能会覆盖变量
作用域链
作用域:决定了代码区块中变量和其它资源的可见性
作用域有三类:
- 全局作用域
- 函数作用域
- 块级作用域
全局作用域:
- 任何不在函数内或大括号内声明的变量都拥有全局作用域
- 任何未定义而直接赋值的变量都自动声明为全局作用域
- 所有window对象的属性拥有全局作用域
- 过多的全局作用域会污染全局命名空间,引起命名冲突
函数作用域:
- 在函数内部声明的变量具有函数作用域,只能在函数内部被访问
- 只能内层访问外层,外层不能访问里面
块级作用域:
- ES6新增的let和const是块级作用域,只可以在花括号内{}访问
作用域链:
- 描述:当使用一个变量时,JS引擎会首先在当前作用域下查找。若找不到该变量,则会依次向上访问外层作用域,直至访问到window对象停止。
- 作用:保证对执行环境有权访问的所有变量和函数的有序访问;通过作用域链,内层作用域可以逐层访问外层作用域的变量和函数。
- 注意:作用域链是在声明阶段即可查看清楚,与调用情况无关,调用情况只影响this的指向!!
闭包
将从概念,由来,作用,应用,注意事项几个方面来回答:
- 简单来说,闭包是 函数与该函数对词法环境/变量环境的引用 的一个组合。
- 闭包无处不在:当创建一个函数时,在JS引擎的解析阶段,即代码执行之前,也会相应地创建函数执行上下文。而执行上下文的创建阶段,会创建好词法环境和变量环境。也就因此创建好了闭包。
- 作用1: 闭包机制能通过作用域链,使得内层函数可以访问外层函数内的变量和资源。
- 作用2: 闭包的存在,保存了该函数对其词法环境/变量环境的引用,即使执行上下文被回收,仍保存了对词法环境和变量环境的引用。换句话说,闭包机制能延长变量的生命周期。
- 应用1:柯里化函数
- 应用2:bind的实现
- 应用3:promise实现
- 注意事项:如果没有对变量设置为null,有可能会造成内层泄漏
----------执行上下文与this的指向-----------
执行上下文
① 执行上下文的类型,分作三种类型:
- 全局执行上下文 :任何不在函数内部的就是处于全局执行上下文。浏览器中的全局对象就是window对象,而全局执行上下文中的this则指向window对象。一个程序中只有一个全局执行上下文。
- 函数执行上下文 : 当一个函数被调用时,就会为该函数创建一个新的函数执行上下文。函数执行上下文可以有多个。
- eval函数执行上下文
全局执行上下文与函数执行上下文:
- 全局执行上下文环境(变量声明、函数声明)
- 函数执行上下文环境(变量声明、函数声明、this、arguments)
② 执行上下文栈
- JavaScript引擎通过执行上下文栈来管理执行上下文
- 当执行 JS代码前,会首先遇到全局代码,则为此创建一个全局执行上下文,并压入执行上下文栈。
- 每当遇到一个函数调用,就会为该函数创建一个新的函数执行上下文,并压入执行上下文栈。
- JS引擎会执行位于执行上下文栈顶的函数,当函数执行完成后,相应的执行上下文会从栈顶弹出。
- 继续执行下一个位于执行上下文栈顶的函数。
- 当所有的代码执行完毕后,从栈中弹出全局执行上下文。
③ 执行上下文的生命周期,共有三个阶段:
- 创建阶段(JS引擎的解析阶段,函数被调用,但还未执行)
- 执行阶段
- 回收阶段
创建阶段 做以下三个事情:
- 绑定this
- 创建词法环境
- 创建变量环境
执行阶段 做以下三个事情:
- 变量赋值
- 函数引用
- 执行代码逻辑
回收阶段
- 当代码执行完毕后,当前执行上下文出栈,等待垃圾机制回收
词法环境与变量环境
一、词法环境:由两个部分组成:
- 环境记录:用于储存变量与函数声明的实际位置
- 对外部环境的引用:当前可以访问的外部词法环境
词法环境可分作两类:
- 全局环境:它没有对外部环境的引用,对外部环境的引用为null;有一个全局对象,this指向这个全局对象
- 函数环境:其环境记录包含了用户定义的变量、函数和arguments对象;对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。
二、变量环境:变量环境也是一个词法环境;
区别只在于:
- 词法环境用于存储函数声明,变量let和const绑定
- 变量环境只用于变量var绑定
this的指向规则
this是什么:
- 当函数被调用时,会创建相应的执行上下文。
- 在执行上下文生命周期的创建阶段,发生三个事情:this的绑定,创建词法环境,创建变量环境。
- 所以函数中this的指向,需要看函数的调用情况,也就是看其执行上下文的情况。
- 函数中的this总是指向调用该函数的对象。
- 优先级:new绑定 > 显示绑定 > 隐式绑定 > 默认绑定
① 默认绑定
- 默认绑定有四种情况:函数在全局中直接被调用、函数调用链(A调用B,B调用C)、作为参数传入到另外一个函数中直接调用(setTimeout、forEach)、函数被赋值
- 四种情况都有个共同点:目标函数在 真正 被执行的时候,不是被某个对象调用,而是作为一个独立函数独立调用。
- 非严格模式下,默认绑定的this指向window;严格模式下,this指向undefined
② 隐式绑定
- 函数作为某个对象的方法被调用,此时函数的this应该指向最后调用它的对象
③ 显示绑定
- apply、call、bind可以显示修改函数中this的指向
- 第一个参数都是this要改指的对象;在非严格模式下,如果没有这个参数或者为undefined/null,this指向window
- 如果在严格模式下,第一个参数是undefined,this指向undefined;参数是null,则this指向null
- apply的第二个参数是数组,以数组的形式传递参数;call和bind可以接收多个参数,以参数列表的形式传递。
- apply和call是一次性传递参数;bind可以多次传递参数,即支持函数柯里化
- apply和call是立即执行绑定this后的函数;bind是返回绑定this后的函数。
④ new绑定
- new操作符会创建一个新的实例对象,构造函数中的this指向这个实例对象。
⑤ 箭头函数
- 箭头函数没有自己的this,它需要继承其父级函数的执行上下文的this
- 箭头函数无法被显示修改this
-----------------ES6相关---------------
let 、 const 、 var 的区别
- 作用域: var是函数作用域(局部或全局);let和const是块级作用域({}花括号内)
- 特性: var会变量提升;let和const是暂时性死区(在声明之前不可以使用)
- 声明: var可以重复声明;let和const不可以重复声明变量
- 初始化: var和let可以不用初始化,const声明后必须初始化
- 更改: var和let可以更改指针指向,const不能改变指针指向
const声明的变量可以修改吗
- const声明的变量,得直接初始化,使其成为常量
- const只是不能改变指针指向,即不能让变量又指向别的内存空间
- 该内存空间里的数据是可以修改的
箭头函数与普通函数的区别
① 箭头函数的this
- 箭头函数没有自己的this
- 它需要捕获其上一级父级函数的执行上下文中的this,也就是将父级执行上下文中的this作为自己的this
- 因为没有自己的this,所以无法通过apply/call/bind去显式修改this的指向
② 箭头函数没有arguments对象
- 箭头函数没有自己的arguments对象
- 在箭头函数中访问arguments对象,实际上访问的是箭头函数的外层函数中的arguments对象
③ 箭头函数没有prototype原型对象
④ 箭头函数不能进行new调用
当进行一个new操作时,发生的步骤如下
- 创建一个空对象实例obj
- 将obj的 __ proto __ 指向构造函数的原型对象(constructor.prototype)
- 让构造函数的this指向新的对象obj,执行构造函数的内容
- 返回结果
前面说到,构造函数没有自己的this,也没有prototype原型对象,所以不能作为构造函数进行new调用
⑤ 箭头函数不能作Generator函数,不能使用yield关键字
⑥ 箭头函数的写法简洁
- 如果没有参数,写一个空括号即可
- 如果只有一个参数,可以不写括号
- 如果有多个参数,用逗号分隔开
- 如果不需要返回值,也只有一句语句,那么可以添加一个void,省略大括号
- 如果返回值只有一句,也可以省略大括号
rest参数
- rest参数形如 …items :可以接收多个入参,将多个入参整合形成一个数组即items。
- 常用于获取函数的多余参数,或者处理函数参数个数不确定的情况。
- rest参数只能是函数参数列表中的最后一个
- 函数的length属性不包括rest参数
- 可以避免使用类数组对象arguments(arguments对象是类数组对象,不能使用数组特定的方法。若要使用,还需将argument对象转化成数组,rest参数则不用,它是一个真正的数组)
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
ES6的模板语法
在ES6提出模板语法之前,拼接字符串是个很痛苦的事情;
需要用到许多的引号和加号,得仔细检查有没有拼错,可读性大大降低。
ES6引入模板语法,是增强版的字符串,用反引号标识。优势:
- 支持用 ${} 去嵌入变量,而不需要使用引号和加号去拼接
- 支持定义多行字符串,且所有的空格和缩进都会被保留
- 支持任意JavaScript表达式,从而可以进行运算、引用对象属性、调用函数
// 支持用 ${} 去嵌入变量,而不需要使用引号和加号去拼接
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
// 支持定义多行字符串,且所有的空格和缩进都会被保留
`In JavaScript this is
not legal.`
// 支持任意JavaScript表达式,从而可以进行运算、引用对象属性、调用函数
let x = 1;
let y = 2;
`${x} + ${y} = ${x + y}`
// "1 + 2 = 3"
`${x} + ${y * 2} = ${x + y * 2}`
// "1 + 4 = 5"
let obj = {x: 1, y: 2};
`${obj.x + obj.y}`
解构赋值
- ES6提供解构赋值,是一种新的提取数据的模式,能够从数组或者对象中有针对性地拿到想要的数据
- 数组解构,是以元素的位置为匹配条件,从而获取数据
- 对象解构,是以属性的名称为匹配条件,从而获取数据
尾调用优化
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);
}
- 尾调用得是函数的最后一步操作,不一定是在函数的尾部
- 函数的调用执行是基于执行上下文和执行上下文栈进行管理的,因为尾调用已经是函数的最后一步操作,则不必再保存当前的执行上下文,可以节省内存。
- 条件:尾调用优化只在严格模式下可用
Set
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]
// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5
Map 与 Object 的比较
Map和Object都是键值对的集合,有如下区别:
- 键的类型:Map的键可以是任意类型;Object的键只能是简单数据类型(String、Symbol)
- 键的顺序:Map的键是有顺序的;Object的键是无序的
- 可否迭代:Map是直接可迭代的;Object自身不支持
- Size:Map可以通过size属性访问键值对个数;Object需要通过Object.keys(obj).length方法
- 性能:频繁增删键值对、存储大量键值对时选择使用Map会比较好
Map 与 WeakMap
Map
- size属性:map.size 可返回当前map对象的成员总数
- Map.prototype.set(key,value):插入/更新一个成员,返回当前的Map对象
- Map.prototype.get(key)
- Map.prototyoe.has(key)
- Map.prototype.delete(key) 根据key删除该成员,成功删除返回true
- Map.prototype.clear() 清除所有成员
三个遍历器和一个遍历方法
- Map.prototype.keys() 返回键名的遍历器
- Map.prototype.values() 返回键值的遍历器
- Map.prototype.entries() 返回所有成员的遍历器
- Map.prototype.forEach() 遍历Map的所有成员
WeakMap 的设计目的
- 有时候想在某个对象上存放一些数据,但是造成了对该对象的引用。
- 不需要使用对象时,我们必须手动删除该对象的引用,否则垃圾回收机制并不会回收内存。
- 一旦忘记手动删除引用,就会造成内存泄漏。
- WeakMap是弱引用。垃圾回收机制并不考虑其在内:只要该对象的其他引用都被清除,垃圾回收机制就会释放所占用的内存。不需要我们手动删除WeakMap的引用
WeakMap与Map差不多,也是键值对的集合,主要有两点区别:
- WeakMap 只接受对象作为键名(null除外),不接受其他类型的值作为键名
- WeakMap 键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内
Symbol
概述:
- 在ES5张,对象属性名都是字符串类型,当添加新的属性或方法时,很容易与已有的属性名/方法名产生冲突
- Symbol是ES6引入的一种新的原始数据类型,表示独一无二的值。
- Symbol可以作为属性的名字,保证属性的名字都是独一无二的,从根本上防止属性名产生冲突。
let s1 = Symbol('foo');
let s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"
使用:
- 通过Symbol()函数生成
- 不可以用new操作符(Symbol值不是对象,会报错)
- 可以接收一个参数,作为对该Symbol值的描述。主要是在控制台显示,或转为字符串时,能作明显的区分
proxy
Proxy用于 修改某些操作的默认行为,等同于在语言层面做出修改,是一种元编程,即对编程语言进行编程。
// 通过new操作符和Proxy构造函数来生成proxy实例
// target:所要拦截的目标对象
// handler:是一个配置对象,用于定制拦截的行为
var proxy = new Proxy(target,handler)
理解:在目标对象之前架设一层拦截,外界对目标对象的访问都必须通过这层拦截。可以对外界的访问进行过滤和改写(定制)。
Iterator 和 for…of 循环
一、
遍历器Iterator是一个接口,为不同的数据结构提供统一的访问机制。
任何数据结构只要部署Iterator接口,就可以完成遍历操作(依次处理该数据结构的所有成员)
简单来说,Iterator的三个作用
- 为各种数据结构提供一个统一的访问机制
- 使得数据结构中的成员能够以某种次序排练
- ES6提供了for…of循环,Iterator接口主要被for…of消费
// 返回一个遍历器对象,可以使用next方法逐次访问数据结构的成员
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
二、
原生具备Iterator接口的数据结构有:
- Array
- String
- Set
- Map
- 类数组对象(函数的arguments对象、NodeList对象)
总结:
- ES6规定,默认的Iterator接口部署在数据结构的Symbol.Iterator属性中。
- 一个数据结构具有Symbol.Iterator属性,就认为是“可遍历的”。
- 而调用for…of循环,则会自动去寻找Iterator接口
- 原生具备Iterator接口的数据结构,可以直接使用for…of循环;
- 而普通的对象若想要具备可以被for…of循环利用的Iterator接口,则必须在Symbol.Iterator上部署(原型链上的对象有该方法也可)
Generator
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
- Generator函数在function关键字与函数名之间有一个星号*
- 在函数内部使用yield表达式定义了各个内部状态,如例子有hello、world、ending三个状态
- 调用Generator函数后,返回的是一个遍历器对象,不会执行函数中的内容。得使用遍历器对象的next方法,使得指针指向下一个状态。
- 总的来说,Generator函数是分段执行的:yield表达式定义了不同内部状态,是函数暂停执行的标识,遇到yield表达式会返回一个对象{value:当前的状态,done:是否执行完毕};Generator返回的遍历器对象使用next方法,可以恢复函数的执行,直至遇到下一个yield暂停执行标识或者函数的结束。
Generator本身并不是为了异步而设计的,它除了用于异步编程,还可以进行迭代、控制输出等等。
注意:async实质上是Generator函数的语法糖,会更加简洁。
Class
普通用法:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
// 等同于
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
};
- ES6的Class写法可以看作是一个语法糖,它只是让原型对象的写法更加清晰、更像面向对象编程的语法。
- Point类内有一个constructor构造器,里面的this指向的是对象实例。通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加。
- 方法都是定义在Point.prototype原型对象上,类的实例上调用方法实际上都是调用原型对象上的方法
- 必须用new操作符调用类,否则会报错(普通构造函数不用new也可以被执行)
ES6 Class继承:
class Point { /* ... */ }
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
- 通过extends关键字进行继承
- 子类的构造函数中,一定要调用super(),而super()也只能在子类的构造函数中使用:ES6的继承机制,是先将父类的属性和方法,添加到一个空对象上面,再把这个对象作为子类的实例。
- 调用super(),就是调用父类的构造函数,创造好了父类实例对象,再给子类作为子类的实例对象,否则子类根本没有this。
- super() 等价于 father.prototype.constructor.call(son);
- 如果super作为对象,在普通方法中super指向父类的原型对象,在静态方法中,super指向父类。
Module
一、ES6 Module的使用 (三个命令)
① export命令 规定模块的对外接口
- 可以导出单个变量或函数
- 可以使用大括号批量导出变量和函数,一目了然,推荐使用
- 可以使用 as 为导出的变量和函数取别名
- export命令可以位于模块的任何位置,只要是模块顶层即可。也就是说,export命令处于块级作用域内的话,就会报错!
// 一、导出单个变量或函数
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
// 二、使用大括号批量导出变量或函数 (推荐使用!!!)
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export { firstName, lastName, year };
// 三、另外,可以用as为输出的变量/函数设置别名
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
// 四、export命令可以位于模块的任何位置,只要在模块顶层即可。
// 如果在块级作用域内,那么就会报错!
function foo() {
export default 'bar' // SyntaxError 报错!!!
}
foo()
② import命令 加载导出模块的变量、方法
- 可以用大括号单个或批量地从其它模块导入变量或函数。(注意大括号里面的变量名、函数名得与导出模块的对外接口一致)
- 可以用 as 为输入的变量、函数重命名
- import命令输入的变量是只读的 read-only
- import命令有提升效果,会提升到整个模块的头部,首先执行(在编译阶段执行)
- import命令是静态执行(编译阶段即执行),不能使用表达式和变量(这些是在运行才能得到的结果)
// 一、使用大括号批量地从其它模块导入变量或函数,名字要一致
import { firstName, lastName, year } from './profile.js';
function setName(element) {
element.textContent = firstName + ' ' + lastName;
}
// 二、可以使用 as 为输入的变量或函数取别名
import { lastName as surname } from './profile.js';
// 三、import命令输入的变量是只读的 read-only,不准改写对外接口!
// 但如果输入的变量是个对象,还是可以修改对象里的内容的!
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
a.foo = 'hello'; // 合法操作
// 四、import命令有提升效果,会提升到整个模块的头部,在编译阶段执行,首先执行
foo();
import { foo } from 'my_module';
// 五、import命令是静态的
// 报错:使用了表达式
import { 'f' + 'oo' } from 'my_module';
// 报错:使用了变量
let module = 'my_module';
import { foo } from module;
// 报错:使用了if结构
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
③ export default命令 指定模块的默认输出
export default命令,是给用户提供方便,不需要提前阅读文档去了解有什么变量名、函数名,指定模块的默认输出。
- import命令在导入export default时,不需要使用大括号
- import命令在导入export default时,可以指定任意的名字
- 一个模块中只能有一个默认输出,也就是说export default只能使用一次
- export default本质是输出一个叫default的变量,允许为它取任意名字
二、Module使用的意义
- 代码封装,方便维护变量和方法
- 代码复用,可以减少代码量
- 依赖管理,大型项目需要引入大量资源,在多人合作时,避免维护困难
- 组件化,现在用react或者是vue等框架去开发,所谓的组件化思想其实也是依赖模块化去实现的。
三、ES6的Module和CommonJS的区别
- 区别一、ES6 Module的import命令是静态的,对于模块的依赖关系建立于代码编译阶段;CommonJS则是动态的,对于模块的依赖建立在代码执行阶段。
- 区别二、ES6 Module导入的变量是对原值的引用;CommonJS导入的变量只是对原值的拷贝。
----------------面向对象编程---------------
对象的创建方式
- 工厂模式
- 构造函数模式
- 原型模式
- 构造函数模式+原型模式
- 动态原型模式
- 寄生构造函数模式
前言:尽管可以使用Object构造函数或者是对象字面量的方式去创建单个对象,但是在用于创建大量对象时,这些方法都会产生大量的重复代码。
① 工厂模式的出现,
- 原理:用函数来封装创建对象的内部细节
- 优点:通过调用函数,实现了代码复用,解决了这种产生大量重复代码的问题
- 缺点:但是,它无法将创建的对象实例与某个类型联系起来
// 工厂模式
function createPerson(name,age,job){
var obj = new Object();
obj.name = name;
obj.age = age;
obj.job = job;
obj.sayName = function(){
console.log(this.name);
}
return obj;
|
② 构造函数模式
- 原理:使用new操作符去调用构造函数,会:1.创建了一个对象实例obj;2.对象实例obj的 __ proto __ 属性指向构造函数的原型对象constructor.prototype;3.构造函数内的this指向这个对象实例obj,执行函数内的语句;4.返回结果
- 优点:构造函数模式的使用,使得对象实例obj与构造函数之间建立起了关系。对象实例obj继承了原型对象的constructor指针可以指回构造函数。因此,所创建的对象实例与构造函数则建立了联系,可以判断对象实例的类型了。
- 缺点:但是,如果构造函数中包括了函数方法的实现,那么每创建一个对象实例,则也会创建一个函数对象的实例。创建多个完成相同任务的函数对象实例,是完全没有必要的,浪费了内存空间。
// 构造函数模式
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
console.log(this.name);
}
}
③ 原型模式
- 原理:每一个函数都有prototype属性,指向的是原型对象。原型对象包含了由构造函数所创建的对象实例所有可以共享的属性和方法。因此可以在原型对象上添加公用属性和方法。
- 优点:可以将完成相同任务的函数方法,当作公有方法添加到原型对象上,从而避免了创建大量的函数对象实例。
- 缺点1:但是,一个对象实例去改变原型对象中某个引用类型的值,那么将会影响所有对象实例的访问。因为这个引用类型对象是所有对象实例所共享的!
- 缺点2:不能传参 —— 原型模式省略了为构造函数传参这个步骤,所有实例对象都将保存最开始原型对象上所设置的值。
// 原型模式
function Person(){}
Person.prototype.name = 'Silam';
Person.prototype.age = '22';
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function(){
console.log(this.name);
}
④ 构造函数模式+原型模式
- 原理:利用构造函数模式传参去定义对象实例的私有属性;利用原型模式定义所有对象实例可共享的属性和方法。构造函数模式+原型模式,使得对象实例obj,构造函数Person,原型对象Person.prototype都建立起了联系。
- 优点:集成前面所有的优点,即 代码复用、可以判断对象实例的类型、避免创建大量函数对象实例、公有私有分明
- 缺点:唯一的缺点就是封装性不好
⑤ 动态原型模式
- 原理:通过if判断某个公有属性/方法是否存在于原型对象中,如果不存在则添加到原型对象上。添加之后,所有的对象实例则都可以访问到原型对象上这个新添加的公有属性/方法。则很好的把原型模式转移到构造函数内部。
// 动态原型模式
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
// 如果创建的对象实例中访问不到sayName
// 无法通过__ proto __访问原型对象上的sayName方法
// 则在原型对象上添加该方法
if(typeof this.sayName !== 'function'){
Person.prototype.sayName = function() {
console.log(this.name);
}
}
}
⑥ 寄生构造函数模式
- 其实,寄生构造函数除了把使用的包装函数叫做构造函数(Person)之外,其它都与工厂模式一模一样!
- 一般是用于对一种已有类型的对象,在其实例对象时,对其实例对象进行拓展。
- 缺点也是无法识别对象实例的类型。
对象的继承方式
- 1.原型链
- 2.借用构造函数
- 3.组合继承(1+2)最常用,但有缺点
- 4.原型式继承
- 5.寄生式继承
- 6.寄生式组合继承
① 原型链继承
// 原型链继承
// 改写子类的原型对象,使其是父类的一个实例!!!
// 子类的原型对象是父类的一个实例!!!
subType.prototype = new SuperType();
- 原理: 子类的原型对象是父类的一个实例对象:子类实例对象通过__ proto __ 可以访问到子类的原型对象中的所有属性和方法;继续通过 __ proto __ 又可向上再访问到父类的原型对象中的所有属性和方法;
- 优点: 实现简单
- 缺点1: 可以在子类构造函数中为子类实例添加私有属性/方法;但是如果要新增原型属性/方法,则需要在 subType.prototype = new SuperType(); 语句后添加。因为我们是改写了子类的原型对象,使其成为父类的一个实例,所以必须在改写之后的原型对象中添加。
- 缺点2: 无法实现多继承
- 缺点3: 原型对象中如果某个属性包含引用类型的值,某个实例对这个原型属性更改,将在所有的实例中得到体现
- 缺点4: 创建子类实例时,无法在不影响所有子类实例的情况下向父类的构造函数传递参数。因为子类的原型对象是父类的实例,若传递参数,将影响子类的原型对象,进而影响所有的子类实例。
② 借用构造函数
// 借用构造函数
// 在子类的构造函数中调用父类的构造函数,通过apply/call实现
function subType(){
superType.call(this,args); // 调用父类构造函数,并传参
this.subAge = 23; // 设定子类的实例属性
}
- 原理: 在子类的构造函数中调用父类构造函数,用apply/call实现
- 优点: 可以在子类构造函数中,调用父类构造函数时同时传参
- 缺点1: 在子类的构造函数中调用父类构造函数,只能继承父类的实例属性/方法,父类原型对象上的属性/方法则是不可见的
- 缺点2: 同样的,仅仅借用构造函数来实现继承,每个子类实例都拥有父类函数的对象实例,浪费空间
③ 组合继承
function SubType(name, age){
// 借用构造函数去继承实例属性
// 第二次调用SuperType()
SuperType.call(this, name);
this.age = age;
}
// 通过原型链去继承原型属性/方法
// 第一次调用SuperType()
SubType.prototype = new SuperType();
// 重写SubType.prototype的constructor属性,指向自己的构造函数SubType
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
};
- 原理: 用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承。
- 缺点: 总是会调用两次父类的构造函数:一次是实现原型链继承,一次是在子类构造函数中被借用。
④ 原型式继承
// 原型式继承:使用object()方法或ES5规范的Object.create()方法
// Func是构造函数,使其prototype指向obj,obj成为原型对象
// 返回 new Func() 实例,实例的原型对象 __proto__ 就是obj
function object(obj){
function Func(){}
Func.prototype = obj;
return new Func();
}
ES5 规范:Object.create(proto,[propertiesObject])
- 原理: 使用object方法,或者使用 Object.create() 方法。传入一个对象,使其成为新创建实例对象的原型对象
- 缺点1: 对于包含引用类型值的原型属性,被所有实例所共享
- 缺点2: 使用object(obj),无法传递参数
⑤ 寄生式继承
// 寄生式继承
// 调用object函数(原型式继承的基础)
// 在对其返回的实例对象进一步增强,添加属性或方法
// 返回增强后的对象
function createAnother(original){
var obj_ = object(original);
obj_.sayHi = function(){
alert("hi");
};
return obj_;
}
- 原理: 在原型式继承的基础上,对返回的实例对象进行增强,返回增强后的实例对象。
- 缺点1: 与原型式继承一样,包含引用类型值的原型属性,会被所有实例所共享
- 缺点2: 使用object(obj)方法,无法传递参数
⑥ 寄生式组合继承
第三种继承方式,即组合继承,最大的缺点就是会调用两次父类的构造函数:一次是在实现原型链继承(让子类的原型对象成为父类的实例);一次是借用构造函数,在子类构造函数中调用了父类构造函数。
调用两次父类的构造函数,无疑创建了两份父类的实例属性:一份实例属性放在了子类原型对象subType.prototye中;一份放在了子类实例中。
显然,我们没有必要让子类拥有两份父类的实例属性。其实同样也是没有必要为了实现原型链继承,让子类的原型对象又拥有了父类的实例属性!
所以寄生式组合继承,原理则是在通过寄生式继承,来实现对父类原型对象方法的继承,避免实现原型链继承时调用父类构造函数。
// 创建一个原型对象为父类原型对象的实例对象prototype
// 让这个父类的实例对象prototype的constructor指回subType
// 并让这个父类实例对象prototype成为子类的原型对象
// 通过寄生式继承,实现了“子类的原型对象是父类的一个实例”
// !!!但又避免了调用父类的构造函数,从而避免了在子类原型对象上添加父类的实例对象
function inheritPrototype(subType,superType){
let prototype = Object.create(superType.prototype);
protoType.constructor = subType;
subType.prototype = prototype;
}
--------------异步编程/事件循环---------------
异步编程的实现方式
异步编程有几种实现方式:
- 回调函数: 实现简单。但多个回调函数嵌套则会形成回调地狱,各个部分高度耦合,不易于维护。
- Promise: 使用Promise可以把嵌套的回调函数,改作链式调用。但是多个then方法的链式调用,也会使得代码语义不明确
- Generator: Generator是分段执行的: yield表达式定义了不同内部状态,是函数暂停执行的标识,遇到yield表达式会返回一个对象{value:当前的状态,done:是否执行完毕};Generator返回的遍历器对象使用next方法,可以恢复函数的执行,直至遇到下一个yield暂停执行标识或者函数的结束。
- async/await: 是Generator的语法糖,相当于自动执行的Generator函数。使用上更加简洁,用同步的形式去编写异步代码
对Promise的理解
Promise是一种异步编程的实现方案
- Promise能被new操作符调用,搭配Promise构造函数创建一个Promise对象实例
- Promise有三种状态:pending、fulfilled、rejected
- Promise构造函数接收一个参数,这个参数叫executor,是一个函数,而且是立即执行的
- executor中又有两个参数,是两个函数:resolve和reject
- resolve和reject函数是promise内部实现的
- resolve函数是Promise的状态从pending变成fulfilled时调用,它会将promise的状态记作fulfilled,且保存成功信息
- reject函数是Promise的状态从pending变成rejected时调用,它会将Promise的状态记作rejected,且保存失败原因
- promise的状态只能从pending变到fulfilled或者是从pending变到rejected,变化后就会凝固,不会再发生变化
- promise的原型对象上有then、catch、all、race、finally方法,定义在原型对象prototype上,这样每个promise实例都可以访问到。
then方法
- then方法接收两个参数,也是两个回调函数,即onFulfilled函数和onRejected函数。
- then方法会直接执行,但如果promise的状态还是pending,则将回调函数暂存起来
- 当promise的状态变成fulfilled时,onFulfilled函数则会放到微任务队列中等待执行;当promise的状态变成rejected时,onRejected函数则会放到微任务队列中等待执行
- then方法会返回一个promise对象,才能实现链式调用
catch方法
- catch方法,当执行then出现错误时,不会停止执行,而是进入到catch方法中
all方法
- all方法接收一个数组,数组的每一项都是一个promise实例对象。
- 当数组中每一项promise实例对象的状态都变成fulfilled,则all方法的promise对象也会变成fulfilled状态
- 如果有一项promise实例对象的状态变成了rejected,则all方法的promise对象会变成rejected状态
race方法
- race是赛跑的意思
- 与all方法一样,接收一个数组,且每一项都是promise实例对象
- 不同的是,race方法会捕获最先完成状态变更的promise实例对象:当第一个promise对象状态变成fulfilled,那么race方法的promise对象状态也变成fulfilled;如果第一个promise对象状态变成rejected,race方法的promise对象也变成rejected
finally方法
- finally是特殊的then方法
- 它不管上一个promise的状态如何,都会执行(都会放到微任务队列当中);而then方法需要promise的状态变成fulfilled,才会放到微任务队列中
- finally的回调函数不接受任何参数,如果传参输出的也是undefined
- finally如果没有抛出异常,返回的是上一个promise对象;then返回的是新的promise对象
promise的优缺点
- 优点:采用promise可以把嵌套的回调函数改作链式调用,可避免回调地狱。
- 缺点1:但是如果有多个层级的then方法,也会使得代码可读性不高
- 缺点2:一旦new了一个Promise实例,它会立即执行executor,且无法中途取消
- 缺点3:当promise实例对象处于pending状态,无法得知进展到哪一个阶段。
Generator和yield
对async/await的理解
async
- async函数执行完且不抛出错误,返回的是一个Promise(onFulfilled)对象
- 情况1:如果有明确的return 'success’语句,那么相当于 return Promise.resolve(‘success’),返回了一个fulfilled的promise,且结果是success
- 情况2:如果没有明确的return语句,那么相当于 return Promise.resolve(),只不过没有结果,是undefined
await
- await等待的是一个表达式的计算结果,可以是promise对象,也可以是其它值
- 情况1:如果表达式的计算结果不是promise对象,那么await的运算结果就是表达式的计算结果
- 情况2:如果表达式的计算结果是一个promise对象,那么await后面的代码相当于promise.then,会被阻塞!且需要等待promise的状态变成fulfilled后,才会把后面的代码放到微任务队列中。并且,await的运算结果是resolve后保存成功的值
总结:
- async/await其实是Generator的语法糖,相当于自动执行的Generator函数。
- 使用上更加优雅和简洁,可以用同步的形式去编写异步代码(多层promise.then读起来不是太清晰)
- async,await可以用try - catch去处理异常
setTimeout、setInterval、requestAnimationFrame
事件循环EventLoop
首先关于事件循环,有以下几个关键概念:
- Call Stack 调用栈:后进先出。 所有的代码都要放到调用栈等待主线程执行。函数被调用时,添加到Call Stack顶部;执行完则退出Call Stack。
- Event Table 事件表格: 事件表格中存储着 异步事件与异步事件对应的回调函数 的映射关系,即 异步事件 -> 对应的回调函数
- Event Queue 事件队列: 先进先出。 当事件表格中的异步事件被触发时,会把对应的回调函数放到事件队列中。又分宏任务队列和微任务队列;
- 宏任务:整体的script代码、setTimeout、setInterval
- 微任务:promise.then、promise.finally、process.nextTick
事件循环Event Loop的概述:
- 每一轮事件循环都应该从一个宏任务开始。可以说,有多少个宏任务就有多少轮事件循环。特别的,第一轮事件循环应该从第一个宏任务开始,而第一个宏任务也就是整体的script代码
- 每一轮事件循环都先拿出一个宏任务来执行:如果遇到同步代码,则放到调用栈中等待主线程执行;遇到异步代码,判断它是宏任务还是微任务,适时 地将其放到宏任务队列/微任务队列中。
- 宏任务有整体script代码、setTimeout、setInterval,遇到直接放到宏任务队列中。
- 微任务有promise.then、promise.finally、process.nextTick。注意,只有promise.then中的回调函数,需要等到promise对象的状态变成fulfilled了,才能放到微任务队列中。
- 执行完同步代码后,如果微任务队列不为空,则执行微任务队列中的微任务。
- 本轮宏任务的同步代码和微任务队列中的微任务执行完毕,则进行下一轮事件循环,从宏任务队列中拿出下一个宏任务来开始。
Node的事件循环
- Node中的事件循环有6个阶段:timers、pending callbakcs、idle/prepare、Poll、check、close callbacks
- 看过一些技术文章,有团队在尝试将浏览器的事件循环和Node中的事件循环统一起来
--------------垃圾回收/内存泄漏---------------
垃圾回收机制
- JavaScript具有自动垃圾回收机制,也就是GC Garbage Collection
- 其基本原理就是:垃圾收集器周期性地找出那些不再继续使用的变量,释放其内存
- 主要有两种策略:标记清除和引用计数
标记清除
- 当变量进入环境,就把这个变量标记为“进入环境”
- 从逻辑上讲,永远不能释放进入环境的变量所占用的内存(有可能随时需要被使用)
- 当变量离开环境时,就被标记为“离开环境”
- 首先,垃圾回收器会把内存中的所有变量加上标记;然后把环境中的变量和被环境中变量所引用的变量的标记都去掉;剩下还留有标记的变量,则是准备删除的变量。
引用计数 (跟踪记录每个值被引用的次数)
- 当声明一个变量,并且把一个引用类型的值保存到该变量中,这个值的引用次数为1;
- 如果同一个值又赋值给另外一个变量,这个值的引用次数就加1;
- 如果某个保存这个值的变量取得了另外一个值,则这个值的引用次数减1;
- 当这个值的引用次数变成0,就可以把它占用的内存空间回收回来。
而引用计数有可能出现循环引用的情况:
譬如 对象A的某个属性指向对象B,且对象B的某个属性指向对象A ,就会造成循环引用
function example(){
var objA = new Object(); // 对象A的引用次数为1
var objB = new Object(); // 对象B的引用次数为1
objA.a = objB; // 对象B的引用次数+1,为2
objB.b = objA; // 对象A的引用次数+1,为2
}
如上,当example函数执行完毕后,两个对象的引用次数都不会是0,都为2,这就是循环引用,其占用的内存得不到释放。
总结:
即使JavaScript有垃圾回收机制,但还是要关注内存泄漏问题。譬如像循环引用这种情况,就需要我们通过检查:是否存在一些不再有用的值,但却仍存在着对它们的引用,如果是的话, 应该将值设为null手动释放引用
内存泄漏
常见造成内存泄漏有以下五种情况
① 意外的全局变量
- 使用未声明的变量,那么该变量就变成了全局变量
- 也有可能是因为this,倘若this因为执行上下文,指向了全局window,那么也会出现全局变量。但可以通过严格模式避免
function example(){
test = 'no no no'; // 使用未声明的变量,test变成全局变量
this.example = 'no no no';
}
example() ;
// 函数在全局中被调用,故函数内部的this指向了全局window
// this.example 也相当于 window.example,example也变成了全局变量
// 不过这个可以通过严格模式去避免
② 被遗忘的setInterval、回调函数
- 如果设置了setInterval,又忘记用clearInterval取消它,setInterval是会一直执行直至页面卸载的
- 此时如果回调函数中有对外部变量的引用,那么这个变量会一直留在内存中,无法被回收
③ 没有清理对DOM元素的引用
- 某个变量引用了某个DOM元素
- 即使文档卸载了DOM元素,如果没有手动解除引用
- 该变量还是存在着对DOM元素的引用,也就无法被回收
// 变量ref是对ref DOM元素的引用
const ref = document.getElementById('ref');
// 尽管文档卸载了该DOM元素
document.body.removeChild(ref);
// 变量ref仍然保存着对该DOM元素的引用
console.log(ref);
// 应该手动将该变量设置为null,解除引用
ref = null;
④ 闭包
function father(){
var obj = document.createElement('test');
function son(){
console.log(obj); // 闭包
}
obj = null; // 解除引用
}
⑤ 没有取消事件监听addEventListener
- 通过addEventListener设置事件监听,在不用的时候又忘记用removeEventListener取消事件监听