JS
1.JS有哪些数据类型
基本数据类型:Number,String,Boolean,Undefined,Null,Symbol,BigInt
引用数据类型:Object
其中Symbol和BigInt是ES6中新增的数据类型
Symbol代表创建后独一无二且不可变的数据类型,他主要是为了解决可能出现的全局变量冲突问题
BigInt是一种数字类型的数据,它可以表示任意精度格式的整数,使用BigInt可以安全地存储和操作大整数,即使这个数已经超出了Number能够表示的安全整数范围
2.堆和栈
在操作系统中,内存被分为堆和栈
- 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈
- 堆区内存由开发者分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收
在数据结构中
- 栈中数据的存取方式为先进后处
- 堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定
数据的存储方式
- 原始数据类型直接存储在栈(stack)中的简单数据,占空间小,大小固定,属于被频繁引用的数据,所以放入栈中存储
- 引用数据类型存储在堆(heap)中的对象,占据空间大,大小不固定。如果存储在栈中,将会影响程序运行的性能,引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始位置。当解释器寻找引用值时,会首先检索其在栈中的地址,获取地址后从堆中获得实体
3.数据类型检测
常用的四种方法:typeof,instanceof,constructor,toString
| typeof | instanceof | constructor | Object.prototype.toString.call |
|---|---|---|---|
| 使用简单 | 能检测出引用类型 | 能检测出所有类型,除了null和undefined | 能检测出所有类型 |
| 智能检测出除null以外的基本类型和引用类型中的function | 不能检测出基本类型,且不能跨iframe | 易被修改,且不能跨iframe | IE6以下,undefined和null均为Object |
4.判断数组的方式
- Object.prototype.toString.call()
- ES6的Array.isArray()
- instanceof
- Array.prototype.isPrototypeOf
- 原型链
5.unll和undefined的区别
首先undefined和null都是基本数据类型,这两个基本数据类型都只有一个值,那就是它本身
undefined:未定义,null:空对象。一般变量声明了但还没有定义的时候会返回undefined,unll主要用于赋值给一些可能会返回对象的变量,作为一个初始化
undefined在JavaScript中不是保留字,这意味着可以用undefined来作为一个变量,但这种做法是危险的,它会影响对undefined值的判断,可以通过 void 0来获取安全的undefined值。
当对这两种类型使用typeof进行判断时,null类型会返回“Object”,这是一个历史遗留问题。当使用双等号对两种类型进行比较时会返回true,三等号会返回false。
5.1.如何获取安全的undefined值
因为undefined是一个标识符,可以被当作变量来使用和赋值,但这样会影响undefined正常判断。表达式void_没有返回值,因此返回结果是undefined。
void并不是该表表达式的结果,只是让表达式不返回结果。因此可以用void 0来获得undefined
6.typeof null为什么返回Object
在JavaScript第一个版本中所有的值都存储在32位的单元中,每个单元包含一个小的类型标签(1-3bit) 以及当前要存储值的真实数据,类型标签存储在每个单元的低位中,共有五种数据类型
000: object - 当前存储的数据指向一个对象
1:int - 当前存储的数据是一个 31 位的有符号整数
010:double - 当前存储的数据指向一个双精度的浮点数
100:string - 当前存储的数据指向一个字符串
110:boolean - 当前存储的数据指向布尔值
如果最低位是1,则类型标签标志位的长度只有一位;如果最低位是0,则类型标签标志位的长度占三位,为存储其他四种数据类型提供了额外两个bit的长度
有两种特殊数据类型
- undefined的值是(-2)三十次方,一个超出整数范围的数字
- unll的值是机器码NULL指针,NULL指针的值全是0
也就是说null的类型标签也是000,和Object的类型标签是一样的,所以会被判定为Object
6.1.typeof NaN结果是什么
返回:Number,NaN是一个“警戒值”,用于指出数字类型中的错误情况,执行数学运算没有成功,这是失败后返回的结果
NaN是一个特殊值,它和自身不相等,是唯一一个非自反的值。
所谓非自反就是说,与谁都不相等,包括它本身。但是 NaN != NaN下会返回true
6.2.isNaN和Number.isNaN函数有什么区别
函数isNaN接收参数后,会尝试将这个参数转化为数值,任何不能转化为数值的值都会返回true,会影响NaN的判断
函数Number.isNaN会首先判断传入参数是否为数字,是数字再继续判断isNaN,不会进行数据转换,判断更为准确
7. 为什么0.1+0.2 !== 0.3,如何让其相等(精度丢失)
计算机是通过二进制的方式存储的,所以0.1+0.2是计算两数的二进制之和
在JS中只有一种数字类型:Number,是标准的双精度浮点数。在二进制中,双精度浮点数的小数部分最多保留52位,再加上前面的1,其实保留53位有效数字,剩余的遵循“0舍1入”的原则。
据上述原则,0.1和0.2的二进制数相加,再转化为十进制就是:0.300000000004,所以不相等
解决方法:设置一个误差范围,通常称为“机器精度”。对JS来说,这个值通常为2-52,在ES6中,提供了Number.EPSILON属性,它的值是2-52,只要判断0.1+0.2-0.3小于Number.EPSILON,就可以判断为0.1+0.2 == 0.3
8.==类型转换规则是什么
判断流程:
-
首先判断两者类型是否相同,相同则比较大小
-
类型不同会进行类型转换
-
判断是否在对比
null和undefined,是的话就返回true- 判断两者是否为
string和number,是的话会将字符串转换为number - 判断其中一方是否为
boolean,是的话就把boolean专为number再进行判断 - 判断其中一方是否为
object且另一方为string、number或者symbol,是的话就会把object转化为原始类型再进行判断
- 判断两者是否为
8.1.其他值转化成string的转换规则
- Null转换为“null”,Undefined转换为”undefined“
- Boolean类型,true转换为”true“,false转换为”false“
- Number类型的值直接转换,不过极大和极小值会使用指数形式
- Symbol类型的值直接转换,但是只允许显示强制转换,使用隐式强制转换会产生错误
- 对于普通对象,如果有自己的
toString()方法,字符串化时就会调用该方法并使用其返回值。否则会调用Object.prototype.toString()
8.2.其他值转化成number的转换规则
- Undefined类型转换为 NaN
- Null类型转换为0
- Boolean类型,true转换为1,false转换为0
- String类型的值转换等同使用
Number()函数进行转换,如果包含非数值则转换为NaN,空字符串为0 - Symbol类型的值不能转换为数字,会报错
- 对象(包括数组)会先被转换为相应的基本类型,如果返回的是非数字的基本类型,则再遵循上述规则强制转换为数字
为了将值转换为相应的基本类型,隐式转换会首先检查该值是否有valueOf()方法。如果有并且返回基本类型,就是使用该值进行强制类型转换。如果没有就是用toString()的返回值来进行强制换装
如果valueOf()和toString()均不返回基本类型值,会产生 TypeError 错误
8.3.其他值转化成Boolean的转换规则
以下这些都是假值:
Undefined、Null、False、+0、-0、NaN、” “
假值的布尔强制转换结果为 false。从逻辑上说,假值列表以外都是真值
9.||和&&操作符的返回值
||和&&首先会对第一个操作数执行条件判断,如果不是布尔值就会强制转换成布尔值,然后再执行条件判断
对于||来说,如果条件判断结果为true就返回第一个操作数的值,如果是false就返回第二个操作数的值
&&则相反,如果条件判断结果是true就返回第二个操作数的值,如果是false就返回第一个操作数的值
10.Object.is()与===、==的区别
使用双等号进行相等判断时,如果两边的类型不一致,则会进行强制类型转换后再进行比较
使用三等号进行相等判断时,如果两边类型不一致,不会做强制类型转换,直接返回false
使用Object.is()进行相等判断是,一般情况下与三等号的判断相同,但是他处理了一些特殊情况,比如 -0和+0不相等,两个NaN是相等的。
11.JavaScript的包装类型
在JavaScript中,基本类型没有属性和方法,但是为了便于操作的基本类型的值,在调用基本类型的属性和方法时JavaScript会在后台隐式地将基本类型转换为对象
const a = 'abc'
a.length; //3
12.this
this是一个在运行时才绑定的引用,在不同情况下会绑定不同的对象
12.1.如何判断this指向
-
函数调用模式,当一个函数不是一个对象属性时,直接作为函数来调用时,this指向全局
-
方法调用模式,当一个函数作为一个对象的方法来调用时,this指向这个对象
-
构造器调用模式,如果函数用
new调用时,函数执行前会新创建一个对象,this指向这个新创建的对象 -
apply、call、bind调用模式,这三个方法都可以显示的指定调用函数的this指向。
- apply接收两个参数:this绑定的对象和参数数组。
- call接收的参数:先是this绑定的对象,剩余的是传入函数执行的参数。故在使用call方法时,传递给函数的参数必须逐个列举出
- bind方法通过传入一个对象,返回一个this绑定了传入对象的新函数。这个函数的this指向除了使用
new时会改变,其他情况不会改变
12.2.this绑定的优先级
new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
13.JSON
JSON是一种基于文本的轻量级数据交换格式,它可以被任何的编程语言读取和作为数据格式来传递
在项目开发中,使用JSON作为前后端数据交换的方式。在前端通过将一个符合JSON格式的数据解构序列化为JSON字符串,然后将它传递到后端,后端通过JSON格式的字符串解析后生成对应的数据结构,以此来实现前后端的数据传递
因为JSON的语法是基于JS的,所以很容易弄混JSON和JS中的对象。JSON中对象格式更严格,比如JSON中属性值不能是函数,不能出现NaN这样的属性值,因此大多数JS对象是不符合JSON对象的格式
JS中提供了两个函数来实现JS数据解构和JSON格式的转换处理
- JSON.stringify,通过传入一个人符合JSON格式的数据解构,将其转换为JSON字符串。如果传入的数据结构不符合JSON格式,那么在序列化的时候会对这些值进行对应的特殊处理,使其符合规范。在前端像后端发送数据时,可以调用这个函数将数据对象转化为JSON格式的字符串
- JSON.parse() ,将JSON格式的字符串转化为JS数据解构,如果传入的字符串不是标准的JSON格式,会抛出错误。当后端接收到JSON字符串时,可以通过这个方法解析出JS数据解构,以此进行数据访问
13.1.String和JSON.stringify()的区别
String('abc') // abc
JSON.stringify('abc') // abc
String({ key: "value" }) // [object object]
JSON.stringify({ key: "value" }) // { "key": "value" }
String([1, 2, 3]) // 1, 2, 3
JSON.stringify([1, 2, 3]) // [1, 2, 3]
const obj = {
title: "devpoint"
toString() {
return "obj"
}
}
String(obj) //obj
JSON.stringify(obj) //{"title": "devpoint"}
当需要将一个数组和一个普通对象转换为字符串时,经常使用JSON.stringify
如果需要对象toString方法重写,则需要使用String()
在其他情况下,使用String()将变量转换为字符串
14.伪数组(类数组)
一个拥有length属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组方法。
常见的类数组对象有arguments和DOM方法的返回结果。还有函数也可以被看作是一个类数组,因为它含有length属性值,代表可以接收的参数个数
14.1.类数组转换成数组的方法有哪些
常见的类数组转换为数组的方法有以下几种
-
通过
call调用数组的slice方法来实现转换Array.prototype.slice.call(arrayLike) -
通过
call调用数组的splice方法来实现转换Array.prototype.splice.call(arrayLike, 0) -
通过
apply调用数组的concat方法来实现转换Array.prototype.concat.apply([], arrayLike) -
通过
Array.from()方法来实现转换Array.from(arrayLike)
14.2.为什么函数arguments参数是类数组而不是数组
arguments是一个对象,它的属性是从0开始依次递增的数字,还有callee和length等属性,与数组相似。但没有数组常见的方法和属性,如forEach、reduce等,所以叫它们类数组。
14.3.如何遍历类数组
-
将数组的方法应用到类数组上,这时就可以使用
call和apply方法function foo() { Array.prototype.forEach.call(arguments, a => console.log(a);) } -
使用
Array.from方法将类数组转换成数组function foo() { const arrArgs = Array.from(arguments) arrArgs.forEach(a => console.log(a);) } -
使用展开运算符将类数组转换为数组
function foo() {
const arrArgs = [...arguments]
arrArgs.forEach(a => console.log(a);)
}
14.4.数组遍历的方法
| 方法 | 特点 |
|---|---|
| forEach() | 数组方法,不改变原数组长度,没有返回值 |
| map() | 数组方法,不改变原数组长度,有返回值,可链式调用 |
| filter() | 数组方法,过滤数组,返回 包含符合条件的数组,可链式调用 |
| for...of | 遍历具有Iterator迭代器的对象属性,返回的是数组元素、对象的属性值。不能遍历普通的obj对象,将异步循环变成同步循环 |
| every() 和 some() | 数组方法,some()只要一个是true,便返回true。every()只要一个是false,便返回false。 |
| find() findIndex() | 数组方法,find()返回的是第一个符合条件的值,findIndex()返回的是第一个符合条件的值的索引。 |
| reduce() reduceRight() | 数组方法,reduce()对数组正序操作;reduceRight()对数组逆序操作 |
14.5.forEach和map的区别
14.6.forEach如何跳出循环
forEach是不能通过break 或return来跳出循环的。因为forEach的回调函数形成了一个作用域,在里面使用break 或return并不会跳出,只会被当作continue
可以利用try catch
function getItemById (arr, id) {
var item = null;
try {
arr.forEach(function (curItem, i) {
if (curItem.id == id) {
item = curItem
throw Error()
}
})
} catch (e) {
return item
}
}
15.use strict是什么,有什么用
use strict是严格模式,这种模式使得JavaScript在更严格的条件下运行,设置该模式的目的在于:
- 消除JavaScript语法的不合理,不严谨之处,减少怪异行为
- 消除代码运行的不安全之外,保证代码运行的安全
- 提高编译器效率,增加运行速度
- 为未来新版本的JavaScript做铺垫
区别:
- 禁止使用with语句
- 禁止this关键字只想全局
- 对象不能重名
16.深拷贝与浅拷贝
深拷贝是开辟一个新的栈,两个对象属性完全相同,但是对应不同的地址,修改一个对象的属性,不会改变另一个对象的属性。常见的拷贝方式有:
- _.cloneDeep()
- jQuery.extend()
- JSON.stringify()
- 手写循环递归
浅拷贝,是创建新的数据,这个数据有着原始数据的属性值的精确拷贝。如果属性是基本类型,拷贝的就是基本类的值。如果属性是引用类型,拷贝的则是内存地址。常见的浅拷贝有:
- Object.assign
- Object.create
- slice
- concat()
- 展开运算符
16.1.JSON.stringify深拷贝的缺点
如果obj里有时间对象,则JSON.stringify后再JSAON.parse的结果是字符串形式,而不是对象形式
如果obj里有RegExp,则打印出来的是空对象
如果对象中有函数或者undefined,会被直接丢弃掉
如果json里有对象是由构造函数生成的,则会丢掉对象的constructor
17.lodash
lodash是一个一致性、模块化、高性能的 JavaScript 食用工具库
- _.cloneDeep深度拷贝
- _.reject 根据条件去除某个元素
- _.drop(array, [ n = 1 ]) 作用:将 array 中前 n 个元素去掉,然后返回剩余部分
18.(a == 1 && a == 2 && a == 3)有可能为true吗
-
重写toString() 或 valueOf()
let a = { i: 1, toString: function () { return a.i ++; } } -
数组
数组的toString接口默认调用数组的
join方法,重写join方法。定义a为数字,每次比较时就会调用toString()方法,把数组的shift方法覆盖toString即可。let a = [1, 2, 3] a.toString = a.shift把toString改为valueOf效果是一样的
let a = [1, 2, 3] a.valueOf = a.shift -
使用Object.defineProperty()
Object.defineProperty()用于定义对象中的属性,接受三个参数:object对象、对象中的属性、属性描述符。属性描述符中get:访问该属性时自动调用
var _a = 1
Object.defineProperty(this, 'a', {
get: function () {
return _a ++
}
})
19.深度遍历和广度遍历
对于算法来说,无非是时间换空间,空间换时间。深度优先不需要记住所有的节点,所以占用空间小,而广度优先需要记录所有的节点占用空间大。深度优先有回溯的操作(无路可走需要回头)所以相对而言时间会长一点
深度优先采用的是堆栈形式,先进后出。广度优先则采用的是队列形式,先进先出。
20.移动端如何实现上拉加载,下拉刷新
上拉加载 本质是页面触底,或者快要触底时的动作,判断页面触底我们需要了解页面的几个属性:
scrollTop:滚动视图的高度距离window顶部的距离,它会随着页面往上滚动而不断增加,初始值是0,它是一个变化的值clientHeigth:定值,屏幕可视高度scrollHeight:页面不滚动时也存在,此时scrollHeight等于clientHeigth。scrollHeight表示body所有元素的总长度(包括body元素自身的padding
综上得出一个触底公式
下拉刷新 的原生实现,主要分三步
- 监听原生
touchstrat事件,记录其初始位置的值e.touche[0].pageY - 监听原生
touchmove事件,记录并计算当前滑动的位置值与初始位置的差值,大于0表示向下拉动。并借助CSS3的translateY属性使元素跟随手势向下滑动对应的差值,同时设置一个允许滑动的最大值。 - 监听元素
touchend事件,若此元素滑动到最大值,则触发callback,同时将translateY重置为0,元素回到初始位置
21.JS中数组和函数在内存中是如何存储的
JavaScript中数组的存储大致分两种情况:
- 同类型数据的数组分配连续的内存空间
- 存在非同类型数据的数组使用哈希映射分配存储空间
提示:可以想象一下连续的内存空间只需要根据索引(指针)直接计算存储位置即可。如果是哈希映射则需要先计算索引值,如果索引值有冲突的场景还需要进行二次查找(需要知道哈希的存储方式)
闭包与作用域
官方说法:闭包就是指有权访问另一个函数作用域中的变量的函数
MDN说法:闭包是一种特殊的对象。它由两个部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成
深度回答
浏览器在加载页面内会把代码放到栈内存中执行,函数进栈执行会产生一个私有上下文(EC),此上下文能保护里面的使用变量(AO)不受外界干扰,并且如果当前执行上下问中的某些内容,被上下文以外的内容占用,当前上下文不会出栈释放,这样可以保存里面的变量和变量值,所以变量是一种保护内部私有变量的机制
1.1闭包的作用
- 使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以外部调用闭包函数时访问到函数内部的变量,这种方法叫做创建私有变量
- 已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收
1.2.闭包在项目中的应用场景
在实际项目中,会基于闭包把自己编写的模块内容包裹起来,这样编写就可以保护自己的代码是私有的,防止和全局变量或者是其他的代码冲突,这一点是利用保护机制
但是不建议过度使用闭包,因为使用不被释放的上下文,是占用栈内存的空间,过多的使用会导致内存的泄露
解决闭包带来的内存泄露问题的方法:使用完后手动释放
1.3.闭包的使用场景
- return回一个函数
- 函数作为参数
- IIFF(自执行函数)
- 循环赋值
- 使用回调函数就是在使用闭包
- 节流防抖
- 函数柯里化
1.4.闭包的执行过程
形成私有上下文
近栈执行
一系列操作
- 初始化作用域(两头<当前作用域,上级作用域>)
- 初始化this
- 初始化arguments
- 赋值形参
- 变量提升
- 代码执行
遇到变量就先看是否是自己私有的,不是自己私有的按照作用域链上查找,如果不是上级的就继续线上查找,直到EC,变量的查找其实就是一个作用域链的拼接过程,拼接查询的链式就是作用域链
正常情况下,代码执行完成之后,私有上下文出栈被回收。但是遇到特殊情况,如果当前私有上下文执行完成之后中的某个东西被执行上下文以外的东西占用,则当前私有上下文就不会出栈释放,也就形成了不被销毁的上下文,闭包
2.执行上下文的类型
-
全局执行上下文
任何不在函数内部的都是全局上下文,它首先会创建一个全局的
window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文 -
函数执行上下文
当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个
-
eval函数执行上下文执行在
eval函数中的代码会有属于他自己的执行上下文,不过eval函数不经常使用。不做多介绍
2.1.执行上下文栈是什么
JavaScript引擎使用执行上下文栈来管理上下文
当JavaScript执行代码时,首先会遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕后,从栈中弹出全局执行上下文。
3.作用域
作用域就是变量的有效范围,在一定的空间里可以对变量数据进行读写操作,这个空间就是变量的作用域
-
全局作用域
直接写在script标签的JS代码,都在全局作用域。在全局作用域下声明的变量叫全局变量
全局变量在全局的任何位置都可以使用,全局作用域中无法访问到局部作用域中的变量
全局作用域在页面打开时创建,页面关闭时销毁
所有window对象的属性拥有全局作用域
- var和function命令声明的全局变量和函数是window对象的属性和方法
- let,const,class声明的全局变量,不属于window对象的属性
-
函数作用域(局部作用域)
调用函数时会创建函数作用域,函数执行完毕之后,作用域销毁。每调用一次函数就会创建一个新的函数作用域,他们之间是相互独立的
在函数作用域中可以访问全局变量,在函数外部无法访问到函数内的变量
当在函数作用域操作一个两遍时,它会先在自身作用域中寻找,如果有就直接使用,如果没有就向上一作用域中寻找,直到找到全局作用域,如果全局作用域中仍然没有找到,就会报错
-
块级作用域
在ES6之前JavaScript采用的是函数作用域+词法作用域,ES6引入了块级作用域
任何一对花括号{}中的语句集都属一个块,在块中使用let和const声明的变量,外部是访问不到的。这种作用域的规则就叫块级作用域
通过var声明的变量或者非严格模式下创建的函数声明没有块级作用域
-
词法作用域
词法作用域是静态作用域,无论函数在哪里被调用,无论如何被调用,它的词法作用域都只由函数被声明时所处的位置决定
编译的词法分析阶段基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它们进行查找
词法作用域就是在写代码时就已经决定了变量的作用域
3.1.作用域链
在JS中使用一个变量的时候,首先JS引擎会尝试在当前作用域去寻找该变量,如果没有找到,再到它的上层作用域寻找,以此类推直到找到该变量或者已经找到了全局变量,这样的变量作用域访问的链式解构,被称为作用域链
深度回答
作用域链本质是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象
3.2.作用域链的作用
保证对执行环境有权访问所有的变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数
3.3.作用域的常见场景
作用域常见的运用场景:模块化
由于JS并未原生支持模块化导致了一些问题,比如全局作用域污染和变量名冲突,代码结构臃肿且复用性不高。在正式的模块化方案出台之前,开发者为了解决上述问题,使用了函数作用域来创建模块的方案
3.4.如何延长作用域链
执行环境有两种:全局和局部(函数),但是有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除
具体来说,执行以下两个语句时,作用域链会延长
try - catch语句的catch块:会创建一个新的变量对象,包含的是被抛出的错误对象的声明with语句:会将指定的对象添加到作用域链中
4.预解析
JS引擎在运行一份代码的时候,会按照下面的步骤进行工作
- 把变量的声明提升到当前作用域的最前面,只会提升声明,不会提升赋值
- 把函数的声明提升到当前作用域的最前面,只会提升声明,不会提升调用
- 先提升function,再提升var
4.1.变量提升与函数提升的区别
变量提升
JavaScript代码执行前引擎会先进行预编译,预编译期间会将变量声明与函数声明提升至其对应作用域的最顶端,函数内声明的变量只会提升至该函数作用域的最顶端,当函数内部定义的一个变量与外部相同时,那么函数内部的这个变量就会上升到最顶端
函数提升
函数提升只会是提升函数声明式的写法,函数表达式的写法不存在函数提升
函数提升的优先级大于变量提升的优先级,即函数提升在变量提升之上
5.浏览器的垃圾回收机制
5.1.内存的生命周期
JS中分配的内存,一般有如下生命周期
- 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
- 内存使用:即读写内存,也就是使用变量、函数等
- 内存回收:使用完毕,由垃圾回收自动回收不再使用的内存
全局变量一般不会回收,一般局部变量的值不用了会被自动回收掉
5.2.垃圾回收
概念:JavaScript代码运行时,需要分配内存空间来存储变量和值。当变量不参与运行时,就需要系统回收被占用的内存空间。
回收机制: JavaScript具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理是找不到不再使用的变量,然后释放掉其占用的内存
JavaScript中存在两种变量:局部变量和全局变量。全局变量的生命周期会持续到页面卸载;局部变量的生命周期从函数执行开始,直到函数执行结束,在这个过程中,局部变量会在堆中或者栈中存储它们的值,当函数执行结束后,这些局部变量不再被使用,它们所占用的空间就会被释放。
不过,当局部变量被外部函数使用时,其中一种情况就是闭包,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会被回收
5.3.垃圾回收的方式
-
引用计数法
这个用的相对较少,IE采用的引用计数法。引用计数就是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,那么这个值的引用次数就减1.当这个引用次数变为0时,说明这个变量就没有价值,因此
-
标记清除法
现代的浏览器已经不再使用引用计数法了,大多是基于标记算法的某些改进算法,总体思想是一致的
标记清除是浏览器常见的垃圾回收机制,当变量进入执行环境时,就标记这个变量“进入环境”,被标记“进入环境”的变量是不能被回收的,因为它们正在被使用。当变量离开环境时,就会被标记为“离开环境”,被标记为“离开环境”的变量会被内存释放。
垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再加上的标记会被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成了内存清楚工作,销毁那些带标记的值,并回收他们所占用的内存空间
5.4.如何减少垃圾回收
虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收带来的代价比较大,所有应该尽量减少垃圾回收
- 对数组进行优化:在清空一个数组时,最简单的方法就是赋值为
[],但是与此同时会创建一个新的空对象,所有应该尽量减少垃圾回收 - 对
object进行优化:对象尽量复用,对于不再使用的对象,将其设置为null,尽快被回收 - 对函数进行优化:在循环中的函数表达式,如果可以复用,尽量放在函数外面
5.5.哪些情况会导致内存泄露
- 意外的全部变量:由于使用未声明的变量,而意外创建了一个全局变量,而使这个变量一直留在内存中无法被回收
- 被遗忘的计时器或回调函数:设置了
setInterval定时器,忘记取消它,如果循环函数有对外部变量的引用的话,这个变量会被一直留在内存中,而无法被收回 - 脱离DOM的引用:获取一个DOM元素的引用,而后这个元素被删除,但是一直保留对这个元素的引用,所以无法被回收
- 闭包:不合理的闭包使用,从而导致某些变量一直被留在内存中
6.函数与函数式编程
主要的编程范式有三种:命令式编程、声明式编程、函数编程
相比命令式编程,函数式编程更加强调执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂运算,而非设计一个复杂的执行过程
6.1.函数式编程的优缺点
优点
- 更好的状态管理:因为它的宗旨是无状态,或者说更少的状态,能最大化的减少这些未知、优化代码、减少出错的情况
- 更简单的复用:固定输入→固定输出,没有其他外部影响,并且无副作用。这样代码复用时,完全不需要考虑它的内部实现和外部影响
- 更优雅的组合:网页是由各个组件组成的,一个函数也可能是由多个小函数组成的。更强的复用性,带来更强大的组合
- 隐形好处:减少代码量,提高维护性
缺点
- 性能:函数式编程相对于指令式编程,性能绝对是一个短板,因为它往往会对一个方法进行过度包装,从而产生上下文切换的性能开销
- 资源占用:在JS中为实现对象状态的不可变,往往会创建新的对象。因此,它对垃圾回收所产生的压力会远超其他编程方式
- 递归陷阱:在函数式编程中,为了实现迭代,通常采用递归操作
6.2.纯函数
纯函数是对给定的输入返回相同输出的函数,并且要求所有的数据都是不可变,即 纯函数 = 无状态 + 数据不可变
- 函数内部传入指定的值,就会返回唯一的值
- 不会造成作用域的变化,例如修改全局变量或者引用传递的参数
优势
- 使用纯函数,可以生产测试代码
- 不依赖外部环境计算,不会产生副作用,提高函数的复用性
- 可读性强,更便于组装
6.3.组合函数
在函数式编程中,有一个很重要的概念就是组合函数。实际上就是把处理的函数数据像管道一样连接起来,然后让数据穿过这些管道,得到最终的结果
组合函数大致思想就是将多个函数组合成一个函数。c(b(a(1)))这种写成compose(c,b,a)(1) 。但是注意这里如果一个函数都没有传入,那就是传入的是什么就返回什么,并且函数的执行顺序是和传入的顺序相反的
//funcs(数组):记录的是所有的函数
//利用了柯里化思想,函数执行,生产一个闭包,预先把这些信息存储,供下级上下文使用
return (x) => {
var len = funcs.length
//如果没有函数执行,直接返回结果
if (len === 0) return x
if (len === 1) funcs[0](x)
return funcs.reduceRight((res,func) => {
return func(res)
}, x)
}
}
var resFn = compose(c,b,a)
resFn(1)
组合式函数的思想,在很多框架中也被使用,例如 redux,实现效果和上面的代码等价
6.5.惰性函数
惰性载入表示函数执行的分支只会在函数第一次调用的时候执行,在第一次调用过程中,该函数会被覆盖为另一个按照合适方法执行的函数,这样任何对原函数的调用就不用再经过执行的分支了
惰性函数相当于有记忆功能,当它已经判断了一遍,第二遍就不会再判断了
比如现在要求写一个test函数,这个函数返回首次调用时的new Date().getTime(),注意是首次,而且不允许有全局变量的污染
var compose = (...funcs) => {
var test = (function () {
var t = null
return function () {
if(t) {
return t
}
t = new Date().getTime()
return t
}
})();
//用惰性函数实现
var test = function () {
var t = new Date().getTime()
test = function () {
return t
}
return test()
}
6.6.高阶函数
高阶函数是指使用其他函数作为参数,或者返回一个函数作为结果的函数
6.7.函数柯里化
柯里化是把接收多个参数的函数转换为接收一个单一参数的函数(最初函数的第一个参数),并且返回接受余下的参数且返回结果的新函数的技术
就比如我有一个函数是add(a, b). 这里面是要传a,b两个参数 但是柯里化就会变成。add(a)(b) 变成了两个执行函数。 那就是add(a)返回的是一个包含后面b这个参数结果的纯函数了
- 参数复用:需要输入多个参数,最终只需要输入一个,其余通过
arguments来获取 - 提前确认:避免重复去判断某一条件是否符合,不符合则
return不再继续执行下面的操作 - 延迟运行:避免重复的去执行程序,等真正需要的结果的时候再执行
6.8.箭头函数
是用箭头=>来定义函数,箭头函数相当于匿名函数,并且简化了函数定义
特征
- 箭头函数没有this,this指向定义箭头函数所处的外部环境,箭头函数的this永远不会变,call、bind、apply也无法改变
- 箭头函数不能当作一个构造函数,因为this的指向问题
- 箭头函数没有
arguments在箭头函数内部访问这个变量,访问的是外部环境的arguments,可以使用 ... 代替 - 箭头函数只能声明成匿名函数,但是可以通过表达式的方式让箭头函数具名
- 箭头函数没有原型
prototype
6.9.函数递归
如果一个函数在内部调用自身,这个函数就是递归函数
其核心思想是把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解
一般来说,递归需要有边界条件、递归前进阶段和递归返回阶段。当边界条件不满足时,递归前进,当边界条件满足时,递归返回
优点:结果清晰,可读性强
缺点:效率低,调用栈可能会溢出,其实每一次函数调用会在内存栈中分配空间,而每一个进程的栈的容量都是有限的,当调用的层次太多时,就会超出栈的容量,从而导致栈溢出。
6.10.尾递归
函数尾位置调用自身,或是一个尾调用本身的其他函数
在递归调用的过程中系统为每一层的返回点、局部变量等开辟了栈来存储,递归次数过多容易造成栈溢出
这时,就可以使用尾递归,即一个函数中所有递归形式的调用都出现在函数的末尾,对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误
6.11.函数传参,传递复杂数据类型和简单数据类型有什么区别
传递复杂数据类型传递的是引用地址,修改会改变
简单数据类型传递的是具体的值,不会相互影响
let a = 8
function fn(a) {
a = 9
}
fn(a)
console.log(a); // 8
let a = { age: 8 }
function fn(a) {
a.age = 9
}
fn(a)
console.log(a.age); //9
6.12.函数声明与函数表达式的区别
函数声明:function开头,有函数提升
函数表达式:不是function开头,没有函数提升
6.13.函数缓存
函数缓存就是将函数运算过的结果进行缓存
本质上是用空间(缓存储存)换时间(计算过程)
常用于缓存数据计算结果和缓存对象
实现 :主要依靠闭包,柯里化,高阶函数
应用场景
- 对于昂贵的函数调用,执行复杂计算的函数
- 对于具有有限且高度重复输入范围的函数
- 对于具有重复输入值的递归函数
- 对于纯函数,及每次使用特定输入调用时返回相同输出的函数
6.14.call、apply、bind三者的同异
call的写法
Function.call(obj,[param1[,param2[,...paramN]]])
- 调用call的对象必须是函数Function
- call的第一个参数是对象,Function的调用者,将会指向这个对象。如果不传,则默认为全局对象
- 第二个参数开始,就可以接收任意个参数。每个参数会映射到相应的Function的参数上。但是如果将所有的参数作为数组传入,它们会作为一个整体映射到Function对应的第一个参数上,之后的参数都为空
call的使用场景
-
对象继承
function superClass () { this.a = 1 this.print = function () { console.log(this.a); } } function subClass () { superClass.call(this) this.print() } subClass() //1subClass通过call方法,继承了superClass的print方法和a变量。此外superClass还可以扩展自己的其他方法
-
借用方法 如果类数组想使用Arrary原型链上的方法,可以这样
let domNodes = Arrary.prototype.slice.call(document.getElementsByTagName("*"))
apply的写法
Function.apply(obj[,argArrary])
- 它的调用者必须是函数Function,并且只接受两个参数,第一个参数的规则与call一致
- 第二个参数,必须是数组或者类数组,它们会被转化成类数组,传入Function中,并且会映射到Funcion对应的参数上。这是call和apply之间很重要的一个区别
func.appply(obj,[1,2,3])
//func接收到的实际参数是 1,2,3
func.apply(obj, {
0: 1,
1: 2,
2: 3,
length: 3
})
// func接收到的实际参数是 1,2,3
apply的一些用法
-
Math.max 获取数组中最大的一项
let max = Math.max.apply(null, arrary)同理,要获取数组中最小的一项,可以这样
let min = Math.min.apply(null, arrary) -
实现两个数组合并。在ES6的扩展运算符出现之前,可以用Array.prototype.push来实现
Array.prototype.push.apply(arr1,arr2)
bind的使用
bind方法与apply和call比较类似,也能改变函数体内的this指向。不同的是,bind方法的返回值是函数,并且需要稍后调用才会执行。而apply和call则是立即执行。
function add (a, b) {
return a + b
}
function sub (a, b) {
return a - b
}
add.bind(sub, 5, 3) //这时,并不会返回 8
add.bind(sub, 5, 3)() //调用后,返回 8
如果bind的第一个参数是null或者undefined,this就指向全局对象window
共同点
- 都可以改变this指向
- 三者第一个参数都是
this要指向的对象,如果没有这个参数或者参数为undefined或null,则默认指向全局window
不同点
- call和apply会调用函数,并且改变函数内部的this指向
- call和apply传递的参数不一样,call传递参数使用逗号隔开,apply使用数组传参,且apply和call是一次性传入参数,而bind可以分多次传入
- bind是返回绑定this之后的函数
应用场景
- call经常做继承
- apply经常跟数组有关,比如借助于数学对象实现数组最大值,最小值
- bind不调用函数,但是还想改变this指向,比如改变定时器内部的this指向
7.创建对象的方式
-
字面量的形式直接创建对象
-
函数方法
- 工厂模式用于创建对象最常用的设计模式,不暴露创建对象的具体逻辑,而是将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂
- 构造函数模式
- 原型模式
- 构造函数模式+原型模式 这是创建自定义类型的最常见方法
- 动态原型模式
- 寄生构造函数模式
-
class创建
8.JavaScript内置对象有哪些?列举该对象常用方法
Number数值对象
- Number.toFixed() 采用定点计数法格式化数字
- Number.toString() 将一个数字转换为字符串
- Number.valueOf() 返回原始数值
String字符串对象
- Length 获取字符串长度
- split() 将一个字符串切割成数组
- concat() 连接字符串
- indexOf() 返回一个子字符串在原始字符串中的索引值。如果没有找到,则返回固定值 -1
- lastIndexOf() 从后向前检索一个字符串
- slice() 抽取一个子串
Boolean布尔对象
- Boolean.toString() 将布尔值转化成字符串
- Boolean.valueOf() 布尔对象的原始值的布尔值
Array数组对象
- join() 将数组转换成字符串,返回一个字符串
- reverse() 将数组中各个元素颠倒顺序
- delete 运算符只能删除数组元素的值,所占空间还在,总长度没变(arr.length)
- shift() 删除数组中第一个元素,返回删除的值,并将长度减 1
- pop() 删除数组中最后一个元素,返回删除的值,并将长度减 1
- unshift() 往数组前面添加一个或多个数组元素,长度会改变
- push() 往数组尾部添加一个或多个数组元素,长度会改变
- concat() 连接数组
- slice() 切割数组
- splice() 插入、删除或者替换数组元素
- toString() 将数组转化成字符串
- forEach() 遍历所有元素
- every() 判断所有元素是否符合条件
- sort() 对数组元素进行排序
- map() 对元素重新组装,生产新数组
- filter() 过滤符合条件的元素
- find() 查找,返回满足提供的测试函数的第一个元素的值。否则返回undefined
- some() 判断是否有一个满足条件,返回布尔
- fill() 填充数组
- flat() 数组扁平化
Function函数对象
- Function.argument 传递给函数的参数
- Function.apply() 将函数作为一个对象的方法调用
- Function.call() 将函数作为一个对象的方法调用
- Function.caller 调用当前函数的函数
- Function.length 已声明的参数个数
- Function.prototype 对象类的原型
- Function.toString 把函数转化为字符串
Date时间对象
- Date.getFullYear() 返回Date对象的年份字段
- Date.getMonth() 返回Date对象的月份字段
- Date.getDate() 返回一个月中的某一天
- Date.getDay() 返回一周中的某一天
- Date.getHours() 返回Date对象的小时字段
- Date.getMinutes() 返回Date对象的分钟字段
- Date.getSeconds() 返回Date对象的秒字段
- Date.getMilliseconds() 返回Date对象的毫秒字段
- Date.getTime() 返回Date对象的毫秒字段
Math数学对象
- Math对象是一个
静态对象 - Math.PI 圆周率
- Math.abs() 绝对值
- Math.ceil() 向上取整(整数加1,小数去掉)
- Math.floor() 向下取整(直接去掉小数)
- Math.round() 四舍五入
- Math.pow(x, y) 求x的y次方
- Math.sqrt() 求平方根
RegExp正则表达式
- RegExp.exec() 检索字符串中指定的值。返回找到的值,并确定其位置
- RegExp.test() 检索字符串中指定的值,返回 true 和 false
- RegExp.toString() 把正则表达式换成字符串
- RegExp.globa 判断是否设置了 “ g ”修饰符
- RegExp.ignoreCase 判断是否设置了 “ i ”修饰符
- RegExp.lastIndex 用于规定下次匹配的起始位置
- RegExp.source 返回正则表达式的匹配模式
Error异常对象
- Error.message 设置或返回一个错误信息(字符串)
- Error.name 设置或返回一个错误名
- Error.toString() 把Error对象转换成字符串
9.hasOwnProperty、instanceof方法
hasOwnProperty() 方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)
instanceof 运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上
10.原型对象
构造函数的内部的prototype属性指向的对象,就是构造函数的原型对象
原型对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个实例对象后,在这个对象的内部将包含一个指针(proto),这个指针指向构造函数的原型对象,在ES6中这个指针被称为对象的原型
10.1.原型链
原型链是一种查找规则
当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象找这个属性,这个原型对象有会有自己的原型,于是这样一直找下去,这种链式查找过程称之为原型链
10.2.原型链的终点是什么
原型链的尽头是null,也就是Object.prototype.proto
11.异步编程的实现方式
JavaScript中的异步机制可以分为以下几种
- 回调函数:使用回调函数的方式的缺点是,多个回调函数嵌套的时候会造成回调地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的维护
- Promise:可以将嵌套的回调函数作为链式调用,但是使用这种方法,会造成多个
then的链式调用,会使代码语义不明确 - generator:可以在函数执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数的执行权转移出去,当异步函数执行完毕时再将执行权转移回来。因此在generator内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式需要考虑的问题是何时将函数的控制权转移回来,因此需要有一个自动执行 generator 的机制,比如说co模块等方式来实现 generator 的自动执行
- async函数:是generator和promise实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个await语句的时候,如果语句返回一个promise对象,那么函数就会等待promise对象的状态变为resolve后再继续向下执行。因此可以将异步逻辑转化为同步的顺序来书写,并且这个函数可以自动执行
11.1.并发与并行的区别
并发是宏观概念,分别有A、B任务,在一段时间内通过任务间的切换完成了这两个任务,这种情况称之为并发
并行是微观概念,假设CPU中存在两个核心,可以同时完成A、B任务。称之为并行
11.2.setTimeout、setInterval的区别
setTimeout 执行该语
句时,是立即把当前定时器代码推入事件队列,当定时器在事件列表中满足设置的时间值时将传入的函数加入任务队列,之后的执行就交给任务队列负责。但是如果此时任务队列不为空,则需等待,所以执行定时器内代码的时间可能会大于设置的时间
返回值timeoutID是一个正整数,表示定时器的编号。这个值可以传递给clearTimeout来取消该定时器
setInterval 重复调用一个函数或执行一个代码片段,每次精确的隔一段时间推入一个事件
(但是,事件的执行时间不一定不准确,还有可能是这个事件还没执行完毕,下一个事件就来了)它返回一个interval ID,该ID是唯一的时间间隔标识,可以通过调用clearInterval()来移除定时器
11.3.什么是回调地狱,会带来什么问题
回调函数层层嵌套,叫做回调地狱。回调地狱会造成代码可复用性不强,阅读性差,迭代性差,扩展性差等问题。
11.4.Promise
promise是异步编程的一种解决方案,他是一个对象,可以获取异步操作的消息,它的出现大大改善了异步编程的困境,避免了地狱回调。它比传统的解决方案回调函数和事件更合理强大
promise本身只是一个容器,真正异步的是它的两个回调 resolve() 和 reject()
promise本质不是控制异步代码的执行顺序(无法控制),而是控制异步代码结果处理的顺序
11.5.Promise实例有哪些状态
三个状态:Pending(进行中)、Resolved(已完成)、Rejected(已拒绝)
当把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成Resolved,没有完成失败了就变成Rejected
注意:一旦从进行状态变成为其他状态就永远不能更改状态栏
11.6.创建Promise实例的方法
-
new Promise((resolve,reject) => {...})
-
Promise.resolve
返回值是一个promise对象,可以对返回值进行.then调用
Promise.resolve(11).then(function(value){ console.log(value) //打印出11 }) -
Promise.reject
new promise的快捷形式,也创建一个promise对象Promise.reject(new Error('出错了'))
11.7.Promise的实例方法
-
then
then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为
resolved调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中第二个参数可以省略。then方法返回的是一个新的Promise实例(不是原来那个Promise实例)因此可以采用链式写法,即then后面再调用另一个then方法 -
catch
该方法相当于then方法的第二个参数,指向
reject的回调函数。不过cacth方法还有一个作用,就是在执行resolve回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch方法中 -
finally
用于指定不管Promise对象最后的状态如何,都会执行操作
如下,服务器使用Promise处理请求,然后使用finally方法关掉服务器
server.listen(port).then(function () { // ... }) .finally(server.stop)finally方法的回调函数不接受任何参数。这意味着,前面Promise状态无从得知。finally方法里面的操作,与状态无关,不依赖Promise的执行结果
11.8.Promise的静态方法
- all :成功时返回一个结果数组,失败返回最先被reject失败状态的值
- race:哪个结果获得最快,就返回那个结果,无论结果本身是成功状态还是失败状态
- any:返回最快的成功结果,如果全部失败就返回失败结果
- resolve、reject:用来生成对应状态的Promise实例
11.9.一个Promise指定多个回调函数,都会调用吗
都会调用,成功状态放在then的第一个参数里调用,失败状态放在then第二个参数里调用
11.10.改变Promise状态和指定回调函数谁先谁后
都有可能,正常情况下是先指定回调函数再改变状态,但也可以先改变状态再指定回调函数
如何改变状态再指定回调
- 在执行器中直接调用resolve() / rejecte()
- 延迟更长时间再调用 then()
什么时候才能得到数据
- 如果先指定的回调,那当状态发生改变时,回调函数就会调用,得到数据
- 如果先改变的状态,那当指定回调时,回调函数就会调用,得到数据
11.11.Promise.then() 返回新的Promise的结果状态由什么决定
由then()指定的回调函数执行的结果决定
- 如果抛出异常,新Promise变为rejected,参数为抛出的异常
- 如果返回的是非Promise的任意值,新Promise变为resolved,value为返回结果
- 如果返回的是另一个新Promise,此Promise的结果就会变成新的Promise的结果
11.12.Promise如何串联多个操作任务
Promise的then()返回一个新的Promise,可以开成 then() 的链式调用。通过 then() 的链式调用串连多个同步、异步任务
11.13.Promise异常穿透
Promise对象的错误具有”冒泡“性质,会一直向后传递,直到捕获为止。错误总会被下一个catch语句捕获
当使用Promise的then链式调用时,可以在最后指定失败的回调
前面任何操作出了异常,都会传到最后失败的回调中处理
11.14.如何中断Promise链
当使用Promise的then链式调用时,在中间中断,不再调用后面的回调函数。在回调函数中返回一个pendding状态的promise对象
11.15.promise的缺点
代码层面
- 无法取消Promise,一旦新建就会立即执行,无法中途取消
- 如果不设置回调函数,Promise内部抛出错误,不会反应到外部
- 当处于pending状态时,无法得知目前进展到哪一阶段(刚刚开始还是即将完成)
语法层面
- Promise虽然摆脱了回调地狱,但是then的链式调用会带来额外的负担
- Promise传递中间值非常麻烦
- Promise的调试很差,由于没有代码块,不能在一个返回表达式的箭头函数中设置断点,如果在
.then代码块中使用调试器的进步(step-over)功能,调试器并不会进入后续的.then代码块,因为调试器只能跟踪同步代码的每一步
12.async/await
async:Generator函数的语法糖,也就是处理异步操作的另一种 高级写法
实现原理:将Generator函数和自动执行器,包装在一个库里
async function fn(args) {
//..
}
//等同于
function fn(args) {
return spawn(function () { //spawn函数是自动执行器
})
}
12.1.async函数的返回值
async函数返回一个Promise对象。函数内部return语句的返回值,会成为then方法回调函数的参数。
async函数内部抛出错误,会导致返回的Promise对象变为 reject状态。抛出的错误对象会被catch方法回调函数接收到
12.2.await到底在等待什么
await等待的是一个表达式,这个表达式的计算结果是Promise对象或者其他值(换句话说,就是没有特殊限定) await不仅用于等Promise对象,它可以等任意表达式的结果
await表达式的运算结果取决于它等的是什么
- 如果它等到的不是一个Promise对象,那await表达式的运算结果就是它等到的对象
- 如果它等的是一个Promise对象,它会阻塞后面的代码,等着Promise对象resolve,然后得到resolve的值,作为await表达式的结果
12.3.什么是顶层await
允许模块在顶层独立使用await命令,使上面那行代码不会的报错。它的主要目的是使用await解决模块异步加载的问题
12.4.await使用注意点
- await命令后面的promise对象,运行结果可能是
rejected,所以最好把await命令放在try...catch代码块中 - 多个
await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发 await命令只能用在async函数之中,如果用在普通函数,会报错async函数可以保留运行堆栈
12.5.async语法怎么捕获异常
通过.catch()或者 try/catch来捕获
try/catch能捕获所有异常,try语句抛出错误后会执行catch语句,try语句后面的内容不会执行
.catch()只能捕获异步方法中reject错误,并且.catch()语句之后的内容会继续执行
12.6.async/await对比promise的优势
- 代码读起来更加同步,Promise虽然摆脱了地狱回调,但是
then的链式调用会带来额外的阅读负担 - Promise传递中间值非常麻烦,而async/await几乎是同步的写法,非常优雅
- 错误处理友好,async/await可以用try/catch,Promise的错误非常冗余
- 调试友好,Promise调试很差,由于没有代码块,不能在一个返回表达式的箭头函数中设置断点,如果在
.then代码块中使用调试器的步进(step-over)功能,调试器并不会进入后续的.then代码块,因为调试器只能跟踪同步代码的每一步
13.ES6
13.1.let、const、var
let:声明变量、没有变量提升、不可重复声明、具有块级作用域、声明变量后可以在使用时赋值
const:只读变量、没有变量提升、不可重复声明、具有块级作用域、声明变量后必须马上赋值
-
块级作用域: 块作用域由
{}包括,let和const具有块级作用域,var不存在块级作用域,块级作用域解决了两个问题- 内层变量可能覆盖外层变量
- 用来计数的循环变量泄露为全局变量
-
变量提升:var存在变量提升,let和const不存在变量提升,变量只能在声明后使用,否则会报错
-
给全局添加属性:浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会
-
重复声明:var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的变量。let和const不允许重复声明变量
-
暂时性死区:在使用let、const命令声明变量之前,该变量都是不可用的,这在语法上称为 暂时性死区 。使用var声明的变量不存在暂时性死区
-
初始值设置:在变量声明时,var和let可以不用设置初始值,const必须设置初始值
-
指针指向:var声明的变量允许改变指针。let和const都是ES6新增的用于创建变量的语法。let创建的变量是可以更改指针方向的(可以重新赋值)。但const声明的变量不允许改变指针
13.2.字符串新增方法
includes:返回布尔值,表示是否找到了参数字符串
startWith:返回布尔值,表示参数字符串是否在原字符串的头部
endWitch:返回布尔值,表示参数字符串是否在原字符串的尾部
repeat:返回一个新字符串,表示将原字符串重复n次
13.3.数组新增方法
Array.from() :将类数组或者可迭代对象创建为一个新的数组,不改变原数组并返回这个新的数组
Array.of() :创建一个具有可变数量参数的新数组实例
findIndex:根据给定的回调函数,找到函数的第一个元素索引,找不到返回-1
find:根据给定的回调函数,找到匹配的第一个元素,找不到返回undefined
fill:将给定值填充数组
const arr = [1,2,3,4]
arr.fill('肖', 1,3) //[1,'肖','肖',4]
keys:返回一个可迭代的对象,其内容为数组key
values:返回一个可迭代的对象,其内容为数组的值
entries:返回一个可迭代的对象,其内容是一个数组,索引0为原数组的元素,1为原数组的值
const keys = arr.keys()
for (const i of keys) {
console.log(i) // 遍历结果 0,1,2
}
const values = arr.values()
for (const i of values) {
console.log(i) // 遍历结果 1,true,肖
}
const iterator = arr.iterator()
console.log(Array.from(iterator)) //[[0,1], [1.true], [2,"肖"]]
padStart(): 在字符串开头填充空格
padEnd():在字符串结尾填充空格
13.4.对象新增方法
Object.is() 用于解决NaN≠NaN,+0===-0的问题
console.log(NaN === NaN) //false
console.log(+0===-0) //true
console.log(Object.is(NaN,NaN)) //true
console.log(Object.is(+0,-0)) //false
Object.assign() 将所有可枚举属性的值从一个或多个源对象复制到目标对象,并返回目标对象
const person = Object.assign({},{name:"刘潇"},{age:18})
console.log(person //{name:"刘潇",age:18}
Object.values() 返回一个给定对象自身的所有可枚举属性值的数组
Object.entries() 返回一个给定对象自身可枚举属性的键值对
Object.getOwnPropertyDescriptors() 返回给定对象所有自有属性的属性描述符
14.DOM
DOM事件流又称为事件传播,是页面中接收事件的顺序。包括三个阶段
- 事件捕获阶段
capture phase,为截获事件提供机会 - 处于目标阶段
target phase,实际的目标元素接收到事件 - 事件冒泡阶段
bubbing phase,可在这阶段对事件做出响应
14.1.事件冒泡
事件开始由最具体的元素(文档中嵌套层次最深的那个节点)接收到后,开始逐级向上传播到较为不具体的节点
14.2.事件捕获
事件开始由较为不具体的节点接收后,开始逐级向下传播到最具体的元素上
事件捕获最大的作用在于:事件在到达预定目标之前可以捕获到它
14.3.事件委托
利用事件冒泡机制,在较上层位置的元素上添加一个事件监听,来管理该元素及其所有子孙元素上的某一类的所有事件
适用场景:在绑定大量事件的时候,可以选择事件委托
优点:事件委托可以减少事件注册数量,节省内存占用。当新增子元素时,无需再次做事件绑定,因此非常适合动态添加元素(Vue解析模板时,会对新创建的元素,额外进行绑定)
14.4.DOM常用操作
-
创建节点
createElement:创建新元素,接收一个参数,即要创建元素的标签名
const divEl = document.createdElement("div")createTextNode:创建一个文本节点
const tetxEl = document.createTextNode("content")createDocumentFragment:用来创建文档碎片,它表示一种轻量级的文档,主要是用来存储临时节点,然后把文档碎片的内容一次性添加到DOM中
const fragment = document.createDocumentFragment()createAttribute:创建属性节点,可以是自定义属性
const dataAttribute = document.crateAttribute('custom') -
获取节点
querySelector:传入任何有效的css选择器,即可选中单个DOM元素(首个)如果页面上没有指定的元素时,返回
nullquerySelectorAll:返回一个包含节点子树内所有与之相匹配的节点,如果没有相匹配的,则返回一个空节点列表
需要注意的是,该方法返回的是一个
NodeList的静态实例,他是一个静态的“快照”,而非“实时”的查询关于获取DOM元素的方法还有如下,不做一一赘述
document.getElementById('id属性值') //返回拥有指定id的对象引用 document.getElementByClassName("class属性值") //返回拥有指定class对象集合 document.getElementByTagName('标签名') //返回拥有指定标签名的对象集合 document.getElementByNmae('name属性值') //返回拥有指定名称的对象集合 document/element.querySelector('css选择器') //返回第一个匹配的元素 document/element.querySelectorAll('css选择器') //返回所有匹配的元素 document.documentElement //获取页面中的HTML标签 document.body //获取页面中的body标签 document.all[''] //获取页面中所有元素节点的对象集合 -
更新节点
innerHTML:不但可以修改一个DOM节点的文本内容,还可以通过HTML片段修改DOM节点内部的子树
innerText、tetxContent:自动对字符串进行HTML编码,保证无法设置任何HTML标签。两者的区别在于读取属性时,
innerHTML不返回隐藏元素的文本,而textContent返回所有文本style:DOM节点的style属性对应所有的css,可以直接获取或设置。遇到 "-" 需要转化为驼峰命名
-
添加节点
innerHTML:如果这个DOM节点是空的,例如
<div></div>,那么直接用innerHTML = <span>child<span>就可以修改DOM节点内容,相当于添加了新的DON节点如果这个节点不为空,那就不能这么做,因为
innerHTML会直接替换掉原来所有的子节点appendChild:把一个子节点添加到父节点的最后一个子节点
如果是获取DOM元素后再添加操作,这个js节点是已经存在当前文档树中,因此这个节点首先会从原先的位置删除,再插入到新的位置
如果动态添加新的节点,则先创建一个新的节点,然后插入到指定的位置
insertBefore:把子节点插入到指定的位置
parentElement.insertBefore(newElement, referenceElement)子节点会插入到
referenceElement之前setAttribute:添加一个属性节点,如果元素中已有该属性改变属性值
-
删除节点
removeChild:删除一个节点,首先要获取该节点本身以及它的父节点,然后调用父节点的
removeChild把自己删除//拿到待删除节点 const self = document.getElementById('to-be-move') //拿到父节点 const parent = self.parentElement //删除 const remove = parent.removeChild(self)
14.5.DOM树,document对象
DOM树:浏览器根据html标签生成的js对象。以HTMLDocument为根节点,其余节点为子节点,组织成一个树的数据结构的表示就是DOM树
document对象:DOM里面提供的一个对象,它提供的属性和方法都是用来访问和操作网页内容的
14.6.传统on注册(L0)和事件监听注册(L2)的区别
传统on注册(L0)
- 同一个对象,后面注册的事件会覆盖前面注册(同一个事件)
- 直接使用null覆盖就可以实现事件解绑
- 都是冒泡阶段执行的
事件监听注册(L2)
- 语法:addEventListener(事件类型,事件处理函数,是否使用捕获)
- 后面注册的事件不会覆盖前面注册的事件(同一个事件)
- 可以通过第三个参数去确定是在冒泡或捕获阶段执行
- 必须使用removeEventListener(事件类型,事件处理函数,获取捕获或冒泡阶段
- 匿名函数无法被解绑
14.7.常用DOM事件
鼠标事件
- 鼠标单击:click
- 鼠标双击:dblclick
- 鼠标移入/移出:mouseover/mouseout(支持冒泡);mouseenter/mouseleave(不支持冒泡)
- 鼠标移动:mousemove
- 鼠标按下:mousedown
- 鼠标松开:mouseup
键盘事件
- 键盘输入:input
- 键盘按下:keydown
- 焦点:focus
- 失去焦点:blur
页面事件
- 页面滚动:scroll
- 页面大小变化:resize
14.8.怎么阻止事件冒泡
阻止事件冒泡:e.stopPropagation()
阻止默认事件
e.preventDefault() //谷歌及IE8以上
window.event.returnValue = false //IE8及以下
return false //无兼容问题,但不能用于节点直接onclick绑定函数
14.9.如何判断元素是否在可视区域
getBondingClientReact
Element.getBoundingClientReact方法返回元素的大小及其对于视口的位置。返回的是一个对象,对象里有这八个属性:left、right、bottom、top、width、height、x、y
IntersectionObserver
IntersectionObserver接口提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewpoint)交叉状态的方法。祖先元素与视窗(viewport)被称为 根(root)
通俗来说:IntersectionObserver是用来监听某个元素与视口的交叉状态
14.10.如何遍历页面中所有的元素
createNodeIterator
const body = document.getElementsByTagName('body')[0]
const it = document.createNodeIterator(body)
let root = it.nextNode()
while(root) {
console.log(root)
root = it.nextNode()
}
14.11.BOM对象
BOM(Browser Object Model)浏览器对象模型
window:navigator、location、document、history、screen
window对象是一个全局对象,也可以说是JavaScript中的顶级对象。像document、alert()、console.log()这些都是window的属性,基本BOM的属性和方法都是window的。所有通过的var定义在全局作用域的变量、函数都会变成window对象的属性和方法。window对象下的属性和方法调用的时候可以省略window
location对象的常用方法
location的数据类型是对象,它拆除并保存了URL地址的各个组成部分,常用的属性和方法:
- href:获取完整的URL地址,对其赋值时用于地址转跳
- search:获取地址中携带的参数,符号?后面部分
- hash:获取地址中的哈希值,符号#后面部分
- reload:方法用来刷新当前页面,传入参数true的时候表示强制刷新
navigator对象(获取浏览器平台和版本数据)
navigator的数据类型是对象,该对象记录了浏览器自身相关的信息,常用的属性和方法:
通过userAgent检测浏览器的版本以及平台
history对象(管理浏览器历史记录)
history的数据类型是对象,主要管理历史记录,该对象与浏览器地址栏的操作相对应,如前进,后退、历史记录。
15.观察者模式
观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都将得到通知,并自动更新观察者模式属于行为型模式,行为型模式关注的是对象之间的通讯,观察者模式就是观察者和被观察者之间的通讯
15.1.发布订阅模式
发布订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类型,无需了解那些订阅者可能存在
同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者
15.2.观察者模式和发布订阅的区别
在观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录,然而在发布订阅模式中,发布者和订阅者不知道双方的存在。它们只有通过消息代理进行通信
在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反
观察者模式大多数是同步的,比如事件触发,Subject就会去调用观察者的方法,而发布订阅模式大多数时候是异步的
15.3.如何判断当前的Js代码是否在浏览器环境中运行
如果JavaScript在浏览器环境中运行,则会有一个全局对象:window。因此可以通过以下方法判断环境
typeof window.self !== "undefined"
//在web worker或者serive worker下是无法获取到windows全局变量,所以需要通过self变量判断