前端-基础
html
1.1 html标签的类型(head, body,!Doctype) 他们的作用是什么
参考答案:
!DOCTYPE 标签:
- 它是指示 web 浏览器关于页面使用哪个 HTML 版本进行编写的指令.
head:
- 是所有头部元素的容器, 绝大多数头部标签的内容不会显示给读者
- 该标签下所包含的部分可加入的标签有 <base>, <link>, <meta>, <script>, <style>和<title>
body :
- 用于定义文档的主体, 包含了文档的所有内容
- 该标签支持 html 的全局属性和事件属性.
1.2 h5新特性
参考答案:
- 新增选择器 document.querySelector、document.querySelectorAll
- 拖拽释放(Drag and drop) API
- 媒体播放的 video 和 audio
- 本地存储 localStorage 和 sessionStorage
- 离线应用 manifest
- 桌面通知 Notifications
- 语意化标签 article、footer、header、nav、section
- 增强表单控件 calendar、date、time、email、url、search
- 地理位置 Geolocation
- 多任务 webworker
- 全双工通信协议 websocket
- 历史管理 history
- 跨域资源共享(CORS) Access-Control-Allow-Origin
- 页面可见性改变事件 visibilitychange
- 跨窗口通信 PostMessage
- Form Data 对象
- 绘画 canvas
H5移除的元素:
- 纯表现的元素:basefont、big、center、font、s、strike、tt、u
- 对可用性产生负面影响的元素:frame、frameset、noframes
1.3 伪类和伪元素
参考答案:
伪类:用于已有元素处于某种状态时为其添加对应的样式,这个状态是根据用户行为而动态变化的。
例如:当用户悬停在指定元素时,可以通过:hover来描述这个元素的状态,虽然它和一般css相似,可以为 已有元素添加样式,但是它只有处于DOM树无法描述的状态下才能为元素添加样式,所以称为伪类。
伪元素:用于创建一些不在DOM树中的元素,并为其添加样式。
例如,我们可以通过:before来在一个元素之前添加一些文本,并为这些文本添加样式,虽然用户可以看见 这些文本,但是它实际上并不在DOM文档中。
1.4 html5语义化
参考答案:
在HTML5出来之前,我们习惯于用div
来表示页面的章节或者不同模块,但是div
本身是没有语义的。但是现在,HTML5中加入了一些语义化标签,来更清晰的表达文档结构。
标签:
<title> <!--:页面主体内容。--> <hn> <!--:h1~h6,分级标题,<h1> 与 <title> 协调有利于搜索引擎优化。--> <ul> <!--:无序列表。--> <li> <!--:有序列表。--> <header> <!--:页眉通常包括网站标志、主导航、全站链接以及搜索框。--> <nav> <!--:标记导航,仅对文档中重要的链接群使用。--> <main> <!--:页面主要内容,一个页面只能使用一次。如果是web应用,则包围其主要功能。--> <article> <!--:定义外部的内容,其中的内容独立于文档的其余部分。--> <section> <!--:定义文档中的节(section、区段)。比如章节、页眉、页脚或文档中的其他部分。--> <aside> <!--:定义其所处内容之外的内容。如侧栏、文章的一组链接、广告、友情链接、相关产品列表等。--> <footer> <!--:页脚,只有当父级是body时,才是整个页面的页脚。--> <small> <!--:呈现小号字体效果,指定细则,输入免责声明、注解、署名、版权。--> <strong> <!--:和 em 标签一样,用于强调文本,但它强调的程度更强一些。--> <em> <!--:将其中的文本表示为强调的内容,表现为斜体。--> <mark> <!--:使用黄色突出显示部分文本。--> <figure> <!--:规定独立的流内容(图像、图表、照片、代码等等)(默认有40px左右margin)。--> <figcaption><!--:定义 figure 元素的标题,应该被置于 figure 元素的第一个或最后一个子元素的位置。--> <cite> <!--:表示所包含的文本对某个参考文献的引用,比如书籍或者杂志的标题。--> <blockquoto><!--:定义块引用,块引用拥有它们自己的空间。--> <q> <!--:短的引述(跨浏览器问题,尽量避免使用)。--> <time> <!--:datetime属性遵循特定格式,如果忽略此属性,文本内容必须是合法的日期或者时间格式。--> <abbr> <!--:简称或缩写。--> <dfn> <!--:定义术语元素,与定义必须紧挨着,可以在描述列表dl元素中使用。--> <address> <!--:作者、相关人士或组织的联系信息(电子邮件地址、指向联系信息页的链接)。--> <del> <!--:移除的内容。--> <ins> <!--:添加的内容。--> <code> <!--:标记代码。--> <meter> <!--:定义已知范围或分数值内的标量测量。(Internet Explorer 不支持 meter 标签)--> <progress> <!--:定义运行中的进度(进程)。-->
扩展:
语义化优点:
- 易于用户阅读,样式丢失的时候能让页面呈现清晰的结构。
- 有利于SEO,搜索引擎根据标签来确定上下文和各个关键字的权重。
- 方便其他设备解析,如盲人阅读器根据语义渲染网页
- 有利于开发和维护,语义化更具可读性,代码更好维护,与CSS3关系更和谐。
1.5 audio 标签的api
参考答案:
audio常用属性
属性 | 属性值 | 注释 |
---|---|---|
src | url | 播放的音乐的url地址(火狐只支持ogg的音乐,而IE9只支持MP3格式的音乐。chrome貌似全支持) |
preload | preload | 预加载(在页面被加载时进行加载或者说缓冲音频),如果使用了autoplay的话那么该属性失效。 |
loop | loop | 循环播放 |
controls | controls | 是否显示默认控制条(控制按钮) |
autoplay | autoplay | 自动播放 |
audio音乐格式的支持
音频格式 | Chrome | Firefox | IE9 | Opera | Safari |
---|---|---|---|---|---|
OGG | 支持 | 支持 | 支持 | 不支持 | 不支持 |
MP3 | 支持 | 不支持 | 支持 | 不支持 | 支持 |
WAV | 不支持 | 支持 | 不支持 | 支持 | 不支 |
audio属性
属性 | 注释 |
---|---|
duration | 获取媒体文件的总时长,以s为单位,如果无法获取,返回NaN |
paused | 如果媒体文件被暂停,那么paused属性返回true,反之则返回false |
ended | 如果媒体文件播放完毕返回true |
muted | 用来获取或设置静音状态。值为boolean |
volume | 控制音量的属性值为0-1;0为音量最小,1为音量最大 |
startTime | 返回起始播放时间 |
error | 返回错误代码,为uull的时候为正常。否则可以通过Music.error.code来获取具体的错误代码: 1.用户终止 2.网络错误 3.解码错误 4.URL无效 |
currentTime | 用来获取或控制当前播放的时间,单位为s。 |
currentSrc | 以字符串形式返回正在播放或已加载的文件 |
常用的控制用的函数:
函数 | 作用 |
---|---|
load() | 加载音频、视频软件 |
play() | 加载并播放音频、视频文件或重新播放暂停的的音频、视频 |
pause() | 暂停出于播放状态的音频、视频文件 |
canPlayType(obj) | 测试是否支持给定的Mini类型的文件 |
常用audio的事件:
事件名称 | 事件作用 |
---|---|
loadstart | 客户端开始请求数据 |
progress | 客户端正在请求数据(或者说正在缓冲) |
play | play()和autoplay播放时 |
pause | pause()方法促发时 |
ended | 当前播放结束 |
timeupdate | 当前播放时间发生改变的时候。播放中常用的时间处理哦 |
canplaythrough | 歌曲已经载入完全完成 |
canplay | 缓冲至目前可播放状态。 |
js
2. js基础
2.1 let const var 相关
参考答案:
var ——ES5 变量声明方式
- 在变量未赋值时,变量undefined(为使用声明变量时也为undefined)
- 作用域——var的作用域为方法作用域;只要在方法内定义了,整个方法内的定义变量后的代码都可以使用
let——ES6变量声明方式
- 在变量为声明前直接使用会报错
- 作用域——let为块作用域——通常let比var 范围要小
- let禁止重复声明变量,否则会报错;var可以重复声明
const——ES6变量声明方式
1. const为常量声明方式;声明变量时必须初始化,在后面出现的代码中不能再修改该常量的值
2. const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动
2.2 js数据类型,区别
参考答案:
基本数据类型:
Number,String,Boolean,null,undefined,symbol,bigint(后两个为ES6新增)
引用数据类型:
object,function(proto Function.prototype)
object:普通对象,数组对象,正则对象,日期对象,Math数学函数对象。
两种数据存储方式:
基本数据类型是直接存储在栈中的简单数据段,占据空间小、大小固定,属于被频繁使用的数据。栈是存储基 本类型值和执行代码的空间。
引用数据类型是存储在堆内存中,占据空间大、大小不固定。引用数据类型在栈中存储了指针,该指针指向堆 中该实体的起始地址,当解释器寻找引用值时,会检索其在栈中的地址,取得地址后从堆中获得实体。
两种数据类型的区别:
堆比栈空间大,栈比堆运行速度快。
堆内存是无序存储,可以根据引用直接获取。
基础数据类型比较稳定,而且相对来说占用的内存小。
引用数据类型大小是动态的,而且是无限的。
2.3 Object.assign的理解
参考答案:
作用:Object.assign可以实现对象的合并。
语法:Object.assign(target, ...sources)
解析:
- Object.assign会将source里面的可枚举属性复制到target,如果和target的已有属性重名,则会覆盖。
- 后续的source会覆盖前面的source的同名属性。
- Object.assign复制的是属性值,如果属性值是一个引用类型,那么复制的其实是引用地址,就会存在引用共享的问题。
2.4 constructor的理解
参考答案:
创建的每个函数都有一个prototype(原型)对象,这个属性是一个指针,指向一个对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(继承自构造函数的prototype),指向构造函数的原型对象。注意当将构造函数的prototype设置为等于一个以对象字面量形式创建的新对象时,constructor属性不再指向该构造函数。
2.5 map 和 forEach 的区别
参考答案:
相同点:
都是循环遍历数组中的每一项
每次执行匿名函数都支持三个参数,参数分别为item(当前每一项),index(索引值),arr(原数组)
匿名函数中的this都是指向window
只能遍历数组
不同点:
map()会分配内存空间存储新数组并返回,forEach()不会返回数据。
forEach()允许callback更改原始数组的元素。map()返回新的数组。
2.6 for of 可以遍历哪些对象
参考答案:
for..of..: 它是es6新增的一个遍历方法,但只限于迭代器(iterator), 所以普通的对象用for..of遍历
是会报错的。
可迭代的对象:包括Array, Map, Set, String, TypedArray, arguments对象等等
2.7 js静态类型检查
参考答案:
js是动态类型语言
静态类型语言 & 动态类型语言
静态类型语言:类型检查发生在编译阶段,因此除非修复错误,否则会一直编译失败
动态类型语言:只有在程序运行了一次的时候错误才会被发现,也就是在运行时,因此即使代码中包含了会 在运行时阻止脚本正常运行的错误类型,这段代码也可以通过编译
js静态类型检查的方法
Flow是Facebook开发和发布的一个开源的静态类型检查库,它允许你逐渐地向JavaScript代码中添加类型。
TypeScript是一个会编译为JavaScript的超集(尽管它看起来几乎像一种新的静态类型语言)
使用静态类型的优势
- 可以尽早发现bug和错误
- 减少了复杂的错误处理
- 将数据和行为分离
- 减少单元测试的数量
- 提供了领域建模(domain modeling)工具
- 帮助我们消除了一整类bug
- 重构时更有信心
使用静态类型的劣势
- 代码冗长
- 需要花时间去掌握类型
2.8 indexof
参考答案:
语法:str.indexOf(searchValue [, fromIndex])
参数:searchValue:要被查找的字符串值。
如果没有提供确切地提供字符串,[searchValue 会被强制设置为 "undefined"
], 然后在当前字符串中查 找这个值。
举个例子:'undefined'.indexOf()
将会返回0,因为 undefined
在位置0处被找到,但是 'undefine'.indexOf()
将会返回 -1 ,因为字符串 'undefined'
未被找到
fromIndex:可选
数字表示开始查找的位置。可以是任意整数,默认值为 0
。
如果 fromIndex
的值小于 0
,或者大于 str.length
,那么查找分别从 0
和str.length
开始。(译者 注: fromIndex
的值小于 0
,等同于为空情况; fromIndex
的值大于或等于 str.length
,那么结果 会直接返回 -1
。)
举个例子,'hello world'.indexOf('o', -5)
返回 4
,因为它是从位置0
处开始查找,然后 o
在位置 4
处被找到。另一方面,'hello world'.indexOf('o', 11)
(或 fromIndex
填入任何大于11
的值) 将会返回 -1
,因为开始查找的位置11
处,已经是这个字符串的结尾了。
返回值:
查找的字符串 searchValue
的第一次出现的索引,如果没有找到,则返回 -1
。
若被查找的字符串 searchValue
是一个空字符串,则返回fromIndex。如果 fromIndex
值为空,或者 fromIndex
值小于被查找的字符串的长度,返回值和以下的 fromIndex
值一样。
如果 fromIndex
值大于等于字符串的长度,将会直接返回字符串的长度(str.length
)
特点:
1. 严格区分大小写
2. 在使用indexOf检索数组时,用‘===’去匹配,意味着会检查数据类型
2.9 iframe有什么优点、缺点
参考答案:
优点:
- iframe能够原封不动的把嵌入的网页展现出来。
- 如果有多个网页引用iframe,那么你只需要修改iframe的内容,就可以实现调用的每一个页面内容的更改,方便快捷。
- 网页如果为了统一风格,头部和版本都是一样的,就可以写成一个页面,用iframe来嵌套,可以增加代码的可重用。
- 如果遇到加载缓慢的第三方内容如图标和广告,这些问题可以由iframe来解决。
缺点:
- iframe会阻塞主页面的onload事件;
- iframe和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载。会产生很多页面,不容易管理。
- iframe框架结构有时会让人感到迷惑,如果框架个数多的话,可能会出现上下、左右滚动条,会分散访问者的注意力,用户体验度差。
- 代码复杂,无法被一些搜索引擎索引到,这一点很关键,现在的搜索引擎爬虫还不能很好的处理iframe中的内容,所以使用iframe会不利于搜索引擎优化(SEO)。
- 很多的移动设备无法完全显示框架,设备兼容性差。
- iframe框架页面会增加服务器的http请求,对于大型网站是不可取的。
2.10 webComponents
参考答案:
Web Components 总的来说是提供一整套完善的封装机制来把 Web 组件化这个东西标准化,每个框架实现 的组件都统一标准地进行输入输出,这样可以更好推动组件的复用
包含四个部分
1. Custom Elements
2. HTML Imports
3. HTML Templates
4. Shadow DOM
Custom Elements
提供一种方式让开发者可以自定义 HTML 元素,包括特定的组成,样式和行为。支持 Web Components 标准的浏览器会提供一系列 API 给开发者用于创建自定义的元素,或者扩展现有元素。
HTML Imports
一种在 HTMLs 中引用以及复用其他的 HTML 文档的方式。这个 Import 很漂亮,可以简单理解为我们常见 的模板中的 include
之类的作用
HTML Templates
模板
Shadow DOM
提供一种更好地组织页面元素的方式,来为日趋复杂的页面应用提供强大支持,避免代码间的相互影响
2.11 dva的数据流流向是怎么样的
参考答案:
数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据 的时候可以通过 dispatch
发起一个 action,如果是同步行为会直接通过 Reducers
改变 State
,如果是 异步行为(副作用)会先触发 Effects
然后流向 Reducers
最终改变 State
,所以在 dva 中,数据流向非 常清晰简明,并且思路基本跟开源社区保持一致。
2.12 变量提升
参考答案:
JavaScript是单线程语言,所以执行肯定是按顺序执行。但是并不是逐行的分析和执行,而是一段一段地分析执行,会先进行编译阶段然后才是执行阶段。在编译阶段阶段,代码真正执行前的几毫秒,会检测到所有的变量和函数声明,所有这些函数和变量声明都被添加到名为Lexical Environment的JavaScript数据结构内的内存中。所以这些变量和函数能在它们真正被声明之前使用。
2.13 作用域
参考答案:
概念:作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6 的到来,为我们提供了‘块级作用域’,可通过新增命令 let 和 const 来体现。
扩展:
var ——ES5 变量声明方式
- 在变量未赋值时,变量undefined(为使用声明变量时也为undefined)
- 作用域——var的作用域为方法作用域;只要在方法内定义了,整个方法内的定义变量后的代码都可以使用
let——ES6变量声明方式
- 在变量为声明前直接使用会报错
- 作用域——let为块作用域——通常let比var 范围要小
- let禁止重复声明变量,否则会报错;var可以重复声明
const——ES6变量声明方式
const为常量声明方式;声明变量时必须初始化,在后面出现的代码中不能再修改该常量的值
const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动
2.14 HashMap 和 Array 有什么区别?
参考答案:
查找效率
HashMap因为其根据hashcode的值直接算出index,所以其查找效率是随着数组长度增大而增加的。
ArrayMap使用的是二分法查找,所以当数组长度每增加一倍时,就需要多进行一次判断,效率下降扩容数量
HashMap初始值16个长度,每次扩容的时候,直接申请双倍的数组空间。
ArrayMap每次扩容的时候,如果size长度大于8时申请size*1.5个长度,大于4小于8时申请8个,小于4时申 请4个。这样比较ArrayMap其实是申请了更少的内存空间,但是扩容的频率会更高。因此,如果数据量比较大的时候,还是使用HashMap更合适,因为其扩容的次数要比ArrayMap少很多。扩容效率
HashMap每次扩容的时候重新计算每个数组成员的位置,然后放到新的位置。
ArrayMap则是直接使用System.arraycopy,所以效率上肯定是ArrayMap更占优势。内存消耗
以ArrayMap采用了一种独特的方式,能够重复的利用因为数据扩容而遗留下来的数组空间,方便下一个ArrayMap的使用。而HashMap没有这种设计。 由于ArrayMap之缓存了长度是4和8的时候,所以如果频繁的使用到Map,而且数据量都比较小的时候,ArrayMap无疑是相当的是节省内存的。
总结
综上所述,数据量比较小,并且需要频繁的使用Map存储数据的时候,推荐使用ArrayMap。 而数据量比较大的 时候,则推荐使用HashMap。
2.15 HashMap和Object
参考答案:
Objects
和 Maps
类似的是,它们都允许你按键存取一个值、删除键、检测一个键是否绑定了值。因此(并且也没有其他内建的替代方式了)过去我们一直都把对象当成 Maps
使用。不过 Maps
和 Objects
有一些重要的区别,在下列情况里使用 Map
会是更好的选择:
Map | Object | |
---|---|---|
意外的键 | Map 默认情况不包含任何键。只包含显式插入的键。 |
一个 Object 有一个原型, 原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。注意: 虽然 ES5 开始可以用 Object.create(null) 来创建一个没有原型的对象,但是这种用法不太常见。 |
键的类型 | 一个 Map 的键可以是任意值,包括函数、对象或任意基本类型。 |
一个Object 的键必须是一个 String 或是Symbol 。 |
键的顺序 | Map 中的 key 是有序的。因此,当迭代的时候,一个 Map 对象以插入的顺序返回键值。 |
一个 Object 的键是无序的注意:自ECMAScript 2015规范以来,对象确实保留了字符串和Symbol键的创建顺序; 因此,在只有字符串键的对象上进行迭代将按插入顺序产生键。 |
Size | Map 的键值对个数可以轻易地通过size 属性获取 |
Object 的键值对个数只能手动计算 |
迭代 | Map 是 iterable 的,所以可以直接被迭代。 |
迭代一个Object 需要以某种方式获取它的键然后才能迭代。 |
性能 | 在频繁增删键值对的场景下表现更好。 | 在频繁添加和删除键值对的场景下未作出优化。 |
2.16 javascript中arguments相关的问题
参考答案:
arguments
在js中,我们在调用有参数的函数时,当往这个调用的有参函数传参时,js会把所传的参数全部存到一个叫arguments的对象里面。它是一个类数组数据
由来
Javascrip中每个函数都会有一个Arguments对象实例arguments,引用着函数的实参。它是寄生在js函数当中的,不能显式创建,arguments对象只有函数开始时才可用
作用
有了arguments这个对象之后,我们可以不用给函数预先设定形参了,可以动态地通过arguments为函数加入参数
2.17 instanceOf 原理,手动实现 function isInstanceOf (child, Parent)
参考答案:
instanceof主要作用就是判断一个实例是否属于某种类型
let person = function(){ } let no = new person() no instanceof person//true
instanceOf 原理
function new_instance_of(leftVaule, rightVaule) { let rightProto = rightVaule.prototype; // 取右表达式的 prototype 值 leftVaule = leftVaule.__proto__; // 取左表达式的__proto__值 while (true) { if (leftVaule === null) { return false; } if (leftVaule === rightProto) { return true; } leftVaule = leftVaule.__proto__ } }
其实 instanceof 主要的实现原理就是只要右边变量的 prototype 在左边变量的原型链上即可。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。
同时还要了解js的原型继承原理
我们知道每个 JavaScript 对象均有一个隐式的 proto 原型属性,而显式的原型属性是 prototype,只有 Object.prototype.proto 属性在未修改的情况下为 null 值
手动实现
function instance_of(L, R) {//L 表示左表达式,R 表示右表达式 var O = R.prototype; L = L.__proto__; while (true) { if (L === null) return false; if (O === L) // 这里重点:当 O 严格等于 L 时,返回true return true; L = L.__proto__; } } // 开始测试 var a = [] var b = {} function Foo(){} var c = new Foo() function child(){} function father(){} child.prototype = new father() var d = new child() console.log(instance_of(a, Array)) // true console.log(instance_of(b, Object)) // true console.log(instance_of(b, Array)) // false console.log(instance_of(a, Object)) // true console.log(instance_of(c, Foo)) // true console.log(instance_of(d, child)) // true console.log(instance_of(d, father)) // true
2.18 数组去重
参考答案:
1. 利用ES6 Set去重(ES6中最常用)
function unique (arr) { return Array.from(new Set(arr)) } var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}]; console.log(unique(arr)) //[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]
不考虑兼容性,这种去重的方法代码最少。这种方法还无法去掉“{}”空对象,后面的高阶方法会添加去掉重复“{}”的方法。
2. 利用for嵌套for,然后splice去重(ES5中最常用)
function unique(arr){ for(var i=0; i<arr.length; i++){ for(var j=i+1; j<arr.length; j++){ if(arr[i]==arr[j]){ //第一个等同于第二个,splice方法删除第二个 arr.splice(j,1); j--; } } } return arr; } var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}]; console.log(unique(arr)) //[1, "true", 15, false, undefined, NaN, NaN, "NaN", "a", {…}, {…}] //NaN和{}没有去重,两个null直接消失了
双层循环,外层循环元素,内层循环时比较值。值相同时,则删去这个值。
3. 利用indexOf去重
function unique(arr) { if (!Array.isArray(arr)) { console.log('type error!') return } var array = []; for (var i = 0; i < arr.length; i++) { if (array .indexOf(arr[i]) === -1) { array .push(arr[i]) } } return array; } var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}]; console.log(unique(arr)) // [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}] //NaN、{}没有去重
新建一个空的结果数组,for 循环原数组,判断结果数组是否存在当前元素,如果有相同的值则跳过,不相同则push进数组。
4. 利用sort()
function unique(arr) { if (!Array.isArray(arr)) { console.log('type error!') return; } arr = arr.sort() var arrry= [arr[0]]; for (var i = 1; i < arr.length; i++) { if (arr[i] !== arr[i-1]) { arrry.push(arr[i]); } } return arrry; } var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}]; console.log(unique(arr)) // [0, 1, 15, "NaN", NaN, NaN, {…}, {…}, "a", false, null, true, "true", undefined] //NaN、{}没有去重
利用sort()排序方法,然后根据排序后的结果进行遍历及相邻元素比对。
5. 利用对象的属性不能相同的特点进行去重(这种数组去重的方法有问题,不建议用,有待改进)
function unique(arr) { if (!Array.isArray(arr)) { console.log('type error!') return } var arrry= []; var obj = {}; for (var i = 0; i < arr.length; i++) { if (!obj[arr[i]]) { arrry.push(arr[i]) obj[arr[i]] = 1 } else { obj[arr[i]]++ } } return arrry; } var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}]; console.log(unique(arr)) //[1, "true", 15, false, undefined, null, NaN, 0, "a", {…}] //两个true直接去掉了,NaN和{}去重
6. 利用includes
function unique(arr) { if (!Array.isArray(arr)) { console.log('type error!') return } var array =[]; for(var i = 0; i < arr.length; i++) { if( !array.includes( arr[i]) ) {//includes 检测数组是否有某个值 array.push(arr[i]); } } return array } var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}]; console.log(unique(arr)) //[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}] //{}没有去重
7. 利用hasOwnProperty
function unique(arr) { var obj = {}; return arr.filter(function(item, index, arr){ return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true) }) } var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}]; console.log(unique(arr)) //[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}] //所有的都去重了
利用hasOwnProperty 判断是否存在对象属性
8. 利用filter
function unique(arr) { return arr.filter(function(item, index, arr) { //当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素 return arr.indexOf(item, 0) === index; }); } var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}]; console.log(unique(arr)) //[1, "true", true, 15, false, undefined, null, "NaN", 0, "a", {…}, {…}]
9. 利用递归去重
function unique(arr) { var array= arr; var len = array.length; array.sort(function(a,b){ //排序后更加方便去重 return a - b; }) function loop(index){ if(index >= 1){ if(array[index] === array[index-1]){ array.splice(index,1); } loop(index - 1); //递归loop,然后数组去重 } } loop(len-1); return array; } var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}]; console.log(unique(arr)) //[1, "a", "true", true, 15, false, 1, {…}, null, NaN, NaN, "NaN", 0, "a", {…}, undefined]
10. 利用Map数据结构去重
function arrayNonRepeatfy(arr) { let map = new Map(); let array = new Array(); // 数组用于返回结果 for (let i = 0; i < arr.length; i++) { if(map .has(arr[i])) { // 如果有该key值 map .set(arr[i], true); } else { map .set(arr[i], false); // 如果没有该key值 array .push(arr[i]); } } return array ; } var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}]; console.log(unique(arr)) //[1, "a", "true", true, 15, false, 1, {…}, null, NaN, NaN, "NaN", 0, "a", {…}, undefined]
创建一个空Map数据结构,遍历需要去重的数组,把数组的每一个元素作为key存到Map中。由于Map中不会出现相同的key值,所以最终得到的就是去重后的结果。
11. 利用reduce+includes
function unique(arr){ return arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]); } var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}]; console.log(unique(arr)); // [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]
12. [...new Set(arr)]
[...new Set(arr)] //代码就是这么少----(其实,严格来说并不算是一种,相对于第一种方法来说只是简化了代码)
2.19 编码和字符集的区别
参考答案:
字符集是书写系统字母与符号的集合,而字符编码则是将字符映射为一特定的字节或字节序列,是一种规则。通常特定的字符集采用特定的编码方式(即一种字符集对应一种字符编码(例如:ASCII、IOS-8859-1、GB2312、GBK,都是即表示了字符集又表示了对应的字符编码,但Unicode不是,它采用现代的模型))
扩展:
字符:在计算机和电信技术中,一个字符是一个单位的字形、类字形单位或符号的基本信息。即一个字符可以是一个中文汉字、一个英文字母、一个阿拉伯数字、一个标点符号等。
字符集:多个字符的集合。例如GB2312是中国国家标准的简体中文字符集,GB2312收录简化汉字(6763个)及一般符号、序号、数字、拉丁字母、日文假名、希腊字母、俄文字母、汉语拼音符号、汉语注音字母,共 7445 个图形字符。
字符编码:把字符集中的字符编码为(映射)指定集合中的某一对象(例如:比特模式、自然数序列、电脉冲),以便文本在计算机中存储和通过通信网络的传递。
2.20 null 和 undefined 的区别,如何让一个属性变为null
参考答案:
undefined 表示一个变量自然的、最原始的状态值,而 null 则表示一个变量被人为的设置为空对象,而不是原始状态。所以,在实际使用过程中,为了保证变量所代表的语义,不要对一个变量显式的赋值 undefined,当需要释放一个对象时,直接赋值为 null 即可。
解析:
undefined 的字面意思就是:未定义的值 。这个值的语义是,希望表示一个变量最原始的状态,而非人为操作的结果 。 这种原始状态会在以下 4 种场景中出现:
声明了一个变量,但没有赋值
访问对象上不存在的属性
函数定义了形参,但没有传递实参
使用 void 对表达式求值
因此,undefined 一般都来自于某个表达式最原始的状态值,不是人为操作的结果。当然,你也可以手动给一个变量赋值 undefined,但这样做没有意义,因为一个变量不赋值就是 undefined 。
null 的字面意思是:空值 。这个值的语义是,希望表示 一个对象被人为的重置为空对象,而非一个变量最原始的状态 。 在内存里的表示就是,栈中的变量没有指向堆中的内存对象
null 有属于自己的类型 Null,而不属于Object类型,typeof 之所以会判定为 Object 类型,是因为JavaScript 数据类型在底层都是以二进制的形式表示的,二进制的前三位为 0 会被 typeof 判断为对象类型,而 null 的二进制位恰好都是 0 ,因此,null 被误判断为 Object 类型。
2.21 数组和伪数组的区别
参考答案:
- 定义
- 数组是一个特殊对象,与常规对象的区别:
- 当由新元素添加到列表中时,自动更新length属性
- 设置length属性,可以截断数组
- 从Array.protoype中继承了方法
- 属性为'Array'
- 类数组是一个拥有length属性,并且他属性为非负整数的普通对象,类数组不能直接调用数组方法。
- 区别
本质:类数组是简单对象,它的原型关系与数组不同。
// 原型关系和原始值转换 let arrayLike = { length: 10, }; console.log(arrayLike instanceof Array); // false console.log(arrayLike.__proto__.constructor === Array); // false console.log(arrayLike.toString()); // [object Object] console.log(arrayLike.valueOf()); // {length: 10} let array = []; console.log(array instanceof Array); // true console.log(array.__proto__.constructor === Array); // true console.log(array.toString()); // '' console.log(array.valueOf()); // []
- 类数组转换为数组
- 转换方法
- 使用
Array.from()
- 使用
Array.prototype.slice.call()
- 使用
Array.prototype.forEach()
进行属性遍历并组成新的数组
- 使用
- 转换须知
- 转换后的数组长度由
length
属性决定。索引不连续时转换结果是连续的,会自动补位。 - 代码示例
- 转换后的数组长度由
let al1 = { length: 4, 0: 0, 1: 1, 3: 3, 4: 4, 5: 5, }; console.log(Array.from(al1)) // [0, 1, undefined, 3]
- ②仅考虑 0或正整数 的索引
// 代码示例 let al2 = { length: 4, '-1': -1, '0': 0, a: 'a', 1: 1 }; console.log(Array.from(al2)); // [0, 1, undefined, undefined]
- ③使用slice转换产生稀疏数组
// 代码示例 let al2 = { length: 4, '-1': -1, '0': 0, a: 'a', 1: 1 }; console.log(Array.prototype.slice.call(al2)); //[0, 1, empty × 2]
- 使用数组方法操作类数组注意地方
let arrayLike2 = { 2: 3, 3: 4, length: 2, push: Array.prototype.push } // push 操作的是索引值为 length 的位置 arrayLike2.push(1); console.log(arrayLike2); // {2: 1, 3: 4, length: 3, push: ƒ} arrayLike2.push(2); console.log(arrayLike2); // {2: 1, 3: 2, length: 4, push: ƒ}
2.22 手写一个发布订阅
参考答案:
// 发布订阅中心, on-订阅, off取消订阅, emit发布, 内部需要一个单独事件中心caches进行存储; interface CacheProps { [key: string]: Array<((data?: unknown) => void)>; } class Observer { private caches: CacheProps = {}; // 事件中心 on (eventName: string, fn: (data?: unknown) => void){ // eventName事件名-独一无二, fn订阅后执行的自定义行为 this.caches[eventName] = this.caches[eventName] || []; this.caches[eventName].push(fn); } emit (eventName: string, data?: unknown) { // 发布 => 将订阅的事件进行统一执行 if (this.caches[eventName]) { this.caches[eventName].forEach((fn: (data?: unknown) => void) => fn(data)); } } off (eventName: string, fn?: (data?: unknown) => void) { // 取消订阅 => 若fn不传, 直接取消该事件所有订阅信息 if (this.caches[eventName]) { const newCaches = fn ? this.caches[eventName].filter(e => e !== fn) : []; this.caches[eventName] = newCaches; } } }
2.23 手写数组转树
参考答案:
问题:
// 例如将 input 转成output的形式 let input = [ { id: 1, val: '学校', parentId: null }, { id: 2, val: '班级1', parentId: 1 }, { id: 3, val: '班级2', parentId: 1 }, { id: 4, val: '学生1', parentId: 2 }, { id: 5, val: '学生2', parentId: 3 }, { id: 6, val: '学生3', parentId: 3 }, ] let output = { id: 1, val: '学校', children: [{ id: 2, val: '班级1', children: [ { id: 4, val: '学生1', children: [] }, { id: 5, val: '学生2', children: [] } ] }, { id: 3, val: '班级2', children: [{ id: 6, val: '学生3', children: [] }] }] }
答案:
// 代码实现 function arrayToTree(array) { let root = array[0] array.shift() let tree = { id: root.id, val: root.val, children: array.length > 0 ? toTree(root.id, array) : [] } return tree; } function toTree(parenId, array) { let children = [] let len = array.length for (let i = 0; i < len; i++) { let node = array[i] if (node.parentId === parenId) { children.push({ id: node.id, val: node.val, children: toTree(node.id, array) }) } } return children } console.log(arrayToTree(input))
2.24 介绍下 Set、Map、WeakSet 和 WeakMap 的区别?
参考答案:
Set
- 成员不能重复;
- 只有键值,没有键名,有点类似数组;
- 可以遍历,方法有
add、delete、has
WeakSet
- 成员都是对象(引用);
- 成员都是弱引用,随时可以消失(不计入垃圾回收机制)。可以用来保存 DOM 节点,不容易造成内存泄露;
- 不能遍历,方法有
add、delete、has
;
Map
- 本质上是键值对的集合,类似集合;
- 可以遍历,方法很多,可以跟各种数据格式转换;
WeakMap
- 只接收对象为键名(null 除外),不接受其他类型的值作为键名;
- 键名指向的对象,不计入垃圾回收机制;
- 不能遍历,方法同
get、set、has、delete
;
2.25 简单说说 js 中有哪几种内存泄露的情况
参考答案:
- 意外的全局变量;
- 闭包;
- 未被清空的定时器;
- 未被销毁的事件监听;
- DOM 引用;
2.26 异步笔试题
请写出下面代码的运行结果:
// 今日头条面试题 async function async1() { console.log('async1 start') await async2() console.log('async1 end') } async function async2() { console.log('async2') } console.log('script start') setTimeout(function () { console.log('settimeout') }) async1() new Promise(function (resolve) { console.log('promise1') resolve() }).then(function () { console.log('promise2') }) console.log('script end')
题目的本质,就是考察setTimeout、promise、async await的实现及执行顺序,以及 JS 的事件循环的相关问题。
答案:
script start async1 start async2 promise1 script end async1 end promise2 settimeout
2.27 json和xml数据的区别
参考答案:
- 数据体积方面:xml是重量级的,json是轻量级的,传递的速度更快些。
- 数据传输方面:xml在传输过程中比较占带宽,json占带宽少,易于压缩。
- 数据交互方面:json与javascript的交互更加方便,更容易解析处理,更好的进行数据交互
- 数据描述方面:json对数据的描述性比xml较差
- xml和json都用在项目交互下,xml多用于做配置文件,json用于数据交互。
2.28 JavaScript有几种方法判断变量的类型?
参考答案:
使用typeof检测当需要判断变量是否是number, string, boolean, function, undefined等类型时,可以使用typeof进行判断。
使用instanceof检测instanceof运算符与typeof运算符相似,用于识别正在处理的对象的类型。与typeof方法不同的是,instanceof 方法要求开发者明确地确认对象为某特定类型。
使用constructor检测constructor本来是原型对象上的属性,指向构造函数。但是根据实例对象寻找属性的顺序,若实例对象上没有实例属性或方法时,就去原型链上寻找,因此,实例对象也是能使用constructor属性的。
2.29 代码解释题
参考答案:
题目:
var min = Math.min(); max = Math.max(); console.log(min < max); // 写出执行结果,并解释原因
答案
false
解析
- 按常规的思路,这段代码应该输出 true,毕竟最小值小于最大值。但是却输出 false
- MDN 相关文档是这样解释的
- Math.min 的参数是 0 个或者多个,如果多个参数很容易理解,返回参数中最小的。如果没有参数,则返回 Infinity,无穷大。
- 而 Math.max 没有传递参数时返回的是-Infinity.所以输出 false
2.30 代码解析题
参考答案:
题目
var company = { address: 'beijing' } var yideng = Object.create(company); delete yideng.address console.log(yideng.address); // 写出执行结果,并解释原因
答案
beijing
解析
这里的 yideng 通过 prototype 继承了 company的 address。yideng自己并没有address属性。所以delete操作符的作用是无效的。
扩展
1.delete使用原则:delete 操作符用来删除一个对象的属性。
2.delete在删除一个不可配置的属性时在严格模式和非严格模式下的区别:
(1)在严格模式中,如果属性是一个不可配置(non-configurable)属性,删除时会抛出异常;
(2)非严格模式下返回 false。
3.delete能删除隐式声明的全局变量:这个全局变量其实是global对象(window)的属性
4.delete能删除的:
(1)可配置对象的属性(2)隐式声明的全局变量 (3)用户定义的属性 (4)在ECMAScript 6中,通过 const 或 let 声明指定的 "temporal dead zone" (TDZ) 对 delete 操作符也会起作用
delete不能删除的:
(2)显式声明的全局变量 (2)内置对象的内置属性 (3)一个对象从原型继承而来的属性
5.delete删除数组元素:
(1)当你删除一个数组元素时,数组的 length 属性并不会变小,数组元素变成undefined
(2)当用 delete 操作符删除一个数组元素时,被删除的元素已经完全不属于该数组。
(3)如果你想让一个数组元素的值变为 undefined 而不是删除它,可以使用 undefined 给其赋值而不是使用 delete 操作符。此时数组元素是在数组中的
6.delete 操作符与直接释放内存(只能通过解除引用来间接释放)没有关系。
3. 异步相关
3.1 promise和 async await 区别
参考答案:
概念
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大,简单地说,Promise好比容器,里面存放着一些未来才会执行完毕(异步)的事件的结果,而这些结果一旦生成是无法改变的async await也是异步编程的一种解决方案,他遵循的是Generator 函数的语法糖,他拥有内置执行器,不需要额外的调用直接会自动执行并输出结果,它返回的是一个Promise对象。
两者的区别
- Promise的出现解决了传统callback函数导致的“地域回调”问题,但它的语法导致了它向纵向发展行成了一个回调链,遇到复杂的业务场景,这样的语法显然也是不美观的。而async await代码看起来会简洁些,使得异步代码看起来像同步代码,await的本质是可以提供等同于”同步效果“的等待异步返回能力的语法糖,只有这一句代码执行完,才会执行下一句。
- async await与Promise一样,是非阻塞的。
- async await是基于Promise实现的,可以说是改良版的Promise,它不能用于普通的回调函数。
3.2 defer和async区别
参考答案:
区别主要在于一个执行时间,defer会在文档解析完之后执行,并且多个defer会按照顺序执行,而async则是在js加载好之后就会执行,并且多个async,哪个加载好就执行哪个
解析:
在没有defer或者async的情况下:会立即执行脚本,所以通常建议把script放在body最后
<script src="script.js"></script>
async:有async的话,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。
但是多个js文件的加载顺序不会按照书写顺序进行
<script async src="script.js"></script>
derer:有derer的话,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成,并且多个defer会按照顺序进行加载。
<script defer src="script.js"></script>
3.3. 同步和异步
参考答案:
同步
- 指在 主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务。
- 也就是调用一旦开始,必须这个调用 返回结果(划重点——)才能继续往后执行。程序的执行顺序和任务排列顺序是一致的。
异步
- 异步任务是指不进入主线程,而进入 任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程。
- 每一个任务有一个或多个 回调函数。前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行。
- 程序的执行顺序和任务的排列顺序是不一致的,异步的。
- 我们常用的setTimeout和setInterval函数,Ajax都是异步操作。
3.4 实现异步的方法
参考答案:
回调函数(Callback)、事件监听、发布订阅、Promise/A+、生成器Generators/ yield、async/await
JS 异步编程进化史:callback -> promise -> generator -> async + await
async/await 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里。
async/await可以说是异步终极解决方案了。
(1) async/await函数相对于Promise,优势体现在:
- 处理 then 的调用链,能够更清晰准确的写出代码
- 并且也能优雅地解决回调地狱问题。
当然async/await函数也存在一些缺点,因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低,代码没有依赖性的话,完全可以使用 Promise.all 的方式。
(2) async/await函数对 Generator 函数的改进,体现在以下三点:
- 内置执行器。 Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。
- 更广的适用性。 co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
- 更好的语义。 async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
解析:
回调函数(Callback)
回调函数是异步操作最基本的方法。以下代码就是一个回调函数的例子:
ajax(url, () => { // 处理逻辑 })
但是回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个请求存在依赖性,你可能就会写出如下代码:
ajax(url, () => { // 处理逻辑 ajax(url1, () => { // 处理逻辑 ajax(url2, () => { // 处理逻辑 }) }) })
回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。此外它不能使用 try catch 捕获错误,不能直接 return。
事件监听
这种方式下,异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
下面是两个函数f1和f2,编程的意图是f2必须等到f1执行完成,才能执行。首先,为f1绑定一个事件(这里采用的jQuery的写法)
f1.on('done', f2);
上面这行代码的意思是,当f1发生done事件,就执行f2。然后,对f1进行改写:
function f1() { setTimeout(function () { // ... f1.trigger('done'); }, 1000); }
上面代码中,f1.trigger('done')表示,执行完成后,立即触发done事件,从而开始执行f2。
这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合",有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。
发布订阅
我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。
首先,f2向信号中心jQuery订阅done信号。
jQuery.subscribe('done', f2);
然后,f1进行如下改写:
function f1() { setTimeout(function () { // ... jQuery.publish('done'); }, 1000); }
上面代码中,jQuery.publish('done')的意思是,f1执行完成后,向信号中心jQuery发布done信号,从而引发f2的执行。 f2完成执行后,可以取消订阅(unsubscribe)
jQuery.unsubscribe('done', f2);
这种方法的性质与“事件监听”类似,但是明显优于后者。因为可以通过查看“消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
Promise/A+
Promise本意是承诺,在程序中的意思就是承诺我过一段时间后会给你一个结果。 什么时候会用到过一段时间?答案是异步操作,异步是指可能比较长时间才有结果的才做,例如网络请求、读取本地文件等
4.1 Promise的三种状态
- Pending----Promise对象实例创建时候的初始状态
- Fulfilled----可以理解为成功的状态
- Rejected----可以理解为失败的状态
这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,比如说一旦状态变为 resolved 后,就不能 再次改变为Fulfilled
let p = new Promise((resolve, reject) => { reject('reject') resolve('success')//无效代码不会执行 }) p.then( value => { console.log(value) }, reason => { console.log(reason)//reject } )
当我们在构造 Promise 的时候,构造函数内部的代码是立即执行的
new Promise((resolve, reject) => { console.log('new Promise') resolve('success') }) console.log('end') // new Promise => end
4.2 promise的链式调用
每次调用返回的都是一个新的Promise实例(这就是then可用链式调用的原因)
如果then中返回的是一个结果的话会把这个结果传递下一次then中的成功回调
如果then中出现异常,会走下一个then的失败回调
在 then中使用了return,那么 return 的值会被Promise.resolve() 包装(见例1,2)
then中可以不传递参数,如果不传递会透到下一个then中(见例3)
catch 会捕获到没有捕获的异常
接下来我们看几个例子:
// 例1 Promise.resolve(1) .then(res => { console.log(res) return 2 //包装成 Promise.resolve(2) }) .catch(err => 3) .then(res => console.log(res)) 复制代码 // 例2 Promise.resolve(1) .then(x => x + 1) .then(x => { throw new Error('My Error') }) .catch(() => 1) .then(x => x + 1) .then(x => console.log(x)) //2 .catch(console.error) 复制代码 // 例3 let fs = require('fs') function read(url) { return new Promise((resolve, reject) => { fs.readFile(url, 'utf8', (err, data) => { if (err) reject(err) resolve(data) }) }) } read('./name.txt') .then(function(data) { throw new Error() //then中出现异常,会走下一个then的失败回调 }) //由于下一个then没有失败回调,就会继续往下找,如果都没有,就会被catch捕获到 .then(function(data) { console.log('data') }) .then() .then(null, function(err) { console.log('then', err)// then error }) .catch(function(err) { console.log('error') })
Promise不仅能够捕获错误,而且也很好地解决了回调地狱的问题,可以把之前的回调地狱例子改写为如下代码:
ajax(url) .then(res => { console.log(res) return ajax(url1) }).then(res => { console.log(res) return ajax(url2) }).then(res => console.log(res))
它也是存在一些缺点的,比如无法取消 Promise,错误需要通过回调函数捕获。
生成器Generators/ yield
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,Generator 最大的特点就是可以控制函数的执行。
语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
Generator 函数除了状态机,还是一个遍历器对象生成函数。
可暂停函数, yield可暂停,next方法可启动,每次返回的是yield后的表达式结果。
yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
我们先来看个例子:
function *foo(x) { let y = 2 * (yield (x + 1)) let z = yield (y / 3) return (x + y + z) } let it = foo(5) console.log(it.next()) // => {value: 6, done: false} console.log(it.next(12)) // => {value: 8, done: false} console.log(it.next(13)) // => {value: 42, done: true}
可能结果跟你想象不一致,接下来我们逐行代码分析:
首先 Generator 函数调用和普通函数不同,它会返回一个迭代器
当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
当执行第二次 next 时,传入的参数12就会被当作上一个yield表达式的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3 = 8
当执行第三次 next 时,传入的参数13就会被当作上一个yield表达式的返回值,所以 z = 13, x = 5, y = 24,相加等于 42
我们再来看个例子:有三个本地文件,分别1.txt,2.txt和3.txt,内容都只有一句话,下一个请求依赖上一个请求的结果,想通过Generator函数依次调用三个文件
1.txt //1.txt文件
2.txt //2.txt文件
3.txt //3.txt文件
let fs = require('fs') function read(file) { return new Promise(function(resolve, reject) { fs.readFile(file, 'utf8', function(err, data) { if (err) reject(err) resolve(data) }) }) } function* r() { let r1 = yield read('./1.txt') let r2 = yield read(r1) let r3 = yield read(r2) console.log(r1) console.log(r2) console.log(r3) } let it = r() let { value, done } = it.next() value.then(function(data) { // value是个promise console.log(data) //data=>2.txt let { value, done } = it.next(data) value.then(function(data) { console.log(data) //data=>3.txt let { value, done } = it.next(data) value.then(function(data) { console.log(data) //data=>结束 }) }) })
从上例中我们看出手动迭代
Generator
函数很麻烦,实现逻辑有点绕,而实际开发一般会配合co
库去使用。co
是一个为Node.js和浏览器打造的基于生成器的流程控制工具,借助于Promise,你可以使用更加优雅的方式编写非阻塞代码。安装
co
库只需:npm install co
上面例子只需两句话就可以轻松实现
function* r() { let r1 = yield read('./1.txt') let r2 = yield read(r1) let r3 = yield read(r2) console.log(r1) console.log(r2) console.log(r3) } let co = require('co') co(r()).then(function(data) { console.log(data) }) // 2.txt=>3.txt=>结束=>undefined
我们可以通过 Generator 函数解决回调地狱的问题,可以把之前的回调地狱例子改写为如下代码:
function *fetch() { yield ajax(url, () => {}) yield ajax(url1, () => {}) yield ajax(url2, () => {}) } let it = fetch() let result1 = it.next() let result2 = it.next() let result3 = it.next()
async/await
5.1 Async/Await简介
使用async/await,你可以轻松地达成之前使用生成器和co函数所做到的工作,它有如下特点:
1. async/await是基于Promise实现的,它不能用于普通的回调函数。
2. async/await与Promise一样,是非阻塞的。
3. async/await使得异步代码看起来像同步代码,这正是它的魔力所在。
一个函数如果加上 async ,那么该函数就会返回一个 Promise
async function async1() { return "1" } console.log(async1()) // -> Promise {<resolved>: "1"}
Generator函数依次调用三个文件那个例子用async/await写法,只需几句话便可实现
let fs = require('fs') function read(file) { return new Promise(function(resolve, reject) { fs.readFile(file, 'utf8', function(err, data) { if (err) reject(err) resolve(data) }) }) } async function readResult(params) { try { let p1 = await read(params, 'utf8')//await后面跟的是一个Promise实例 let p2 = await read(p1, 'utf8') let p3 = await read(p2, 'utf8') console.log('p1', p1) console.log('p2', p2) console.log('p3', p3) return p3 } catch (error) { console.log(error) } } readResult('1.txt').then( // async函数返回的也是个promise data => { console.log(data) }, err => console.log(err) ) // p1 2.txt // p2 3.txt // p3 结束 // 结束
5.2 Async/Await并发请求
如果请求两个文件,毫无关系,可以通过并发请求
let fs = require('fs') function read(file) { return new Promise(function(resolve, reject) { fs.readFile(file, 'utf8', function(err, data) { if (err) reject(err) resolve(data) }) }) } function readAll() { read1() read2()//这个函数同步执行 } async function read1() { let r = await read('1.txt','utf8') console.log(r) } async function read2() { let r = await read('2.txt','utf8') console.log(r) } readAll() // 2.txt 3.txt
3.5 怎么解决callback多层嵌套
参考答案:
回调地狱有两种解决方案:
- Promises
- Async/await
3.6 promise的介绍与使用
参考答案:
Promise 介绍:
ES6中的Promise 是异步编程的一种方案。从语法上讲,Promise 是一个对象,它可以获取异步操作的消息。
Promise对象, 可以将异步操作以同步的流程表达出来。使用 Promise 主要有以下好处:
- 可以很好地解决回调地狱的问题(避免了层层嵌套的回调函数)。
- 语法非常简洁。Promise 对象提供了简洁的API,使得控制异步操作更加容易。
Promise 使用:
语法
var promise = new Promise((resolve, reject) => {/* executor函数 */ // ... some code if (/* 异步操作成功 */){ resolve(value); } else { reject(error); } }); promise.then((value) => { //success }, (error) => { //failure })
参数
executor
函数在Promise
构造函数执行时同步执行,被传递resolve
和reject
函数(executor
函数在Promise
构造函数返回新建对象前被调用)。
executor
内部通常会执行一些异步操作,一旦完成,可以调用resolve
函数来将promise
状态改成fulfilled
(完成),或者将promise
的状态改为rejected
(失败)。
如果在executor
函数中抛出一个错误,那么该promise
状态为rejected
。executor
函数的返回值被忽略。简单使用
function timeout(ms) { return new Promise((resolve, reject) => { setTimeout(resolve, ms, 'done'); }); } timeout(2000).then((value) => { console.log(value); //done });
原型方法
Promise.prototype.then(onFulfilled, onRejected)
语法
p.then(onFulfilled, onRejected);
p.then((value) => {// fulfillment}, (reason) => {// rejection});
含义
为Promise
实例添加状态改变时的回调函数。then
方法的第一个参数是resolved
状态的回调函数,第二个参数(可选)是rejected
状态的回调函数。链式操作
then
方法返回的是一个新的promise
,因此可以采用链式写法,即then
方法后面再调用另一个then
方法。new Promise(resolve => { resolve(1); }) .then(result => console.log(result)) //1 .then(result => { console.log(result); //undefined return 2; }) .then(result => { console.log(result); //2 throw new Error("err"); }) .then((result) =>{ console.log(result); }, (err)=>{ console.log(err); //Error: err return 3; }) .then((result) => { console.log(result); //3 })
注意:①不管是
then
方法的onfulfilled
函数参数执行还是onrejected
(可选参数)函数参数执行,then
方法返回的都是一个新的Promise
对象,都可以继续采用链式写法调用另一个then
方法。②Promise.prototype.catch()
方法返回的也是一个Promise
对象。then
方法和catch
方法可以链式操作。返回值
then
方法返回一个Promise
,而它的行为与then
中的被调用的回调函数(onfulfilled
函数/onrejected
函数)的返回值有关。
(1) 如果then
中的回调函数返回一个值,那么then
返回的Promise
将会成为接受状态,并且将返回的值作为接受状态的回调函数的参数值。new Promise((resolve, reject) => { reject(); }) .then( () => 99, () => 42 ) .then( result => console.log(result)); // 42
(2) 如果
then
中的回调函数抛出一个错误,那么then
返回的Promise
将会成为拒绝状态,并且将抛出的错误作为拒绝状态的回调函数的参数值。new Promise((resolve, reject) => { resolve(); }) .then( () => {throw new Error('err')}, () => {}) .then( () => {}, (err) => {console.log(err)}); //Error: err
(3) 如果
then
中的回调函数返回一个已经是接受状态的Promise
,那么then
返回的Promise
也会成为接受状态,并且将那个Promise
的接受状态的回调函数的参数值作为该被返回的Promise
的接受状态回调函数的参数值。new Promise((resolve, reject) => { resolve(); }) .then( () => { return new Promise(resolve => resolve('ok')); }) .then( (result) => {console.log(result)}); //ok
(4) 如果
then
中的回调函数返回一个已经是拒绝状态的Promise
,那么then
返回的Promise
也会成为拒绝状态,并且将那个Promise
的拒绝状态的回调函数的参数值作为该被返回的Promise
的拒绝状态回调函数的参数值。new Promise((resolve, reject) => { resolve(); }) .then( () => { return new Promise((resolve, reject) => { reject(new Error('err')); }); }) .then( () => {}, (err) => {console.log(err)}); //Error: err
(5) 如果
then
中的回调函数返回一个未定状态(pending
)的Promise
,那么then
返回Promise
的状态也是未定的,并且它的终态与那个Promise
的终态相同;同时,它变为终态时调用的回调函数参数与那个Promise
变为终态时的回调函数的参数是相同的。new Promise((resolve, reject) => { resolve(); }) .then(() => { return new Promise(resolve => { setTimeout(resolve, 2000, 'ok'); }); }) .then( (result) => {console.log(result)}); //ok
注意:这里是
then
方法中被调用回调函数的返回值与then
方法返回的Promise
对象状态之间的关系。
Promise.prototype.catch(onRejected)
语法
p.catch(onRejected); p.catch(function(reason) { // 拒绝 });
含义
Promise.prototype.catch
方法是.then(null, rejection)
的别名,用于指定发生错误时的回调函数,返回一个新的promise
对象。用法
const p = new Promise((resolve,reject) => { return reject(new Error('err')); //reject方法的作用,等同于抛出错误 //throw new Error('err'); }); p.then(null, (err) => { console.log(err); //Err: err }); //--------等价写法--------- p.catch(err => { console.log(err); //Err: err })
注意:由于
.catch
方法是.then(null, rejection)
的别名,故.then
中的链式操作(3)、返回值(4)等语法在.catch
中都适用。一般总是建议,
Promise
对象后面要跟catch
方法,这样可以处理Promise
内部发生的错误。catch
方法返回的还是一个Promise
对象,因此后面还可以接着调用then
方法。Promise
对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch
语句捕获。 即:当前catch
方法可以捕获上一个catch
方法(包括上一个catch
)到当前catch
(不包括当前catch
)方法之间所有的错误,如果没有错误,则当前catch
方法不执行。new Promise(() => { throw new Error('err1'); }) .then(() => {console.log(1);}) .then(() => {console.log(2);}) .catch((err) => { console.log(err); //Err: err1 throw new Error('err2'); }) .catch((err) => {console.log(err);})//Err: err2
一般来说,不要在
then
方法里面定义Reject
状态的回调函数(即then
的第二个参数),总是使用catch
方法。// bad new Promise() .then((data) => {/* success */ }, (err) => {/* error */ }); // good new Promise() .then((data) => { /* success */ }) .catch((err) => {/* error */ });
上面代码中,第二种写法要好于第一种写法,理由是第二种写法可以捕获前面
then
方法执行中的错误,也更接近同步的写法。与传统的
try/catch
代码块不同的是,即使没有使用catch
方法指定错误处理的回调函数,Promise
对象抛出的错误也不会中止外部脚本运行。new Promise((resolve) => { // 下面一行会报错,因为x没有声明 resolve(x + 2); }).then(() => { console.log('ok'); }); setTimeout(() => {console.log('over')}); //Uncaught (in promise) ReferenceError: x is not defined //over
在异步函数中抛出的错误不会被
catch
捕获到new Promise((resolve, reject) => { setTimeout(() => { throw 'Uncaught Exception!'; }, 1000); }).catch(() => { console.log('err'); //不会执行 }); new Promise((resolve, reject) => { setTimeout(() => { reject(); }, 1000); }).catch(() => { console.log('err'); //err });
在
resolve()
后面抛出的错误会被忽略new Promise((resolve, reject) => { resolve(); throw 'Silenced Exception!'; }).catch(function(e) { console.log(e); // 不会执行 });
Promise.all(iterable)
语法
var p = Promise.all([p1, p2, p3]);
含义
Promise.all
方法接受一个数组作为参数,p1
、p2
、p3
都是Promise
实例,如果不是,就会先调用下面讲到的Promise.resolve
方法,将参数转为Promise
实例,再进一步处理。(Promise.all
方法的参数可以不是数组,但必须具有Iterator
接口,且返回的每个成员都是Promise
实例。)
p
的状态由p1
、p2
、p3
决定,分成两种情况。
(1) 只有p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数。
(2) 只要p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p
的回调函数。用法
const p1 = 'p1-ok'; const p2 = Promise.resolve('p2-ok'); const p3 = new Promise((resolve) => setTimeout(resolve, 3000, 'p3-ok')); const p4 = Promise.reject('p4-err'); Promise.all([p1, p2, p3]) .then((resolves) => { resolves.forEach(resolve => { console.log(resolve); //p1-ok p2-ok p3-ok }); }) .catch(() => { console.log('err'); }); Promise.all([p1, p2, p3, p4]) .then(() => { console.log('ok'); }) .catch((err) => { console.log(err); //p4-err })
Promise.race(iterable)
语法
var p = Promise.race([p1, p2, p3]);
含义
Promise.race
方法同样是将多个Promise
实例,包装成一个新的Promise
实例。只要p1
、p2
、p3
之中有一个实例率先改变状态,p
的状态就跟着改变。那个率先改变的Promise
实例的返回值,就传递给p
的回调函数。
Promise.race
方法的参数与Promise.all
方法一样,如果不是Promise
实例,就会先调用下面讲到的Promise.resolve
方法,将参数转为Promise
实例,再进一步处理。用法
var p1 = new Promise(resolve => {setTimeout(resolve, 500, "one");}); var p2 = new Promise(resolve => {setTimeout(resolve, 100, "two");}); Promise.race([p1, p2]) .then(value => { console.log(value); // "two" }); var p3 = new Promise(resolve => {setTimeout(resolve, 100, "three");}); var p4 = new Promise((resolve, reject) => {setTimeout(reject, 500, "four");}); Promise.race([p3, p4]) .then((value) => { console.log(value); // "three" }) .catch(err => { // 未被调用 }); var p5 = new Promise(resolve => {setTimeout(resolve, 500, "five");}); var p6 = new Promise((resolve, reject) => { setTimeout(reject, 100, "six"); }); Promise.race([p5, p6]) .then((value) => { // 未被调用 }).catch((reason) => { console.log(reason); // "six" });
Promise.resolve(value)
语法
Promise.resolve(value); Promise.resolve(promise); Promise.resolve(thenable);
Promise.resolve
等价于下面的写法。Promise.resolve(value) ; // 等价于 new Promise(resolve => resolve(value));
含义
返回一个状态由给定value
决定的Promise
实例。用法
(1) 如果该值是一个Promise
对象,则直接返回该对象;const p = new Promise((resolve) => {resolve()}); const p2 = Promise.resolve(p); console.log(p === p2); //true
(2) 如果参数是
thenable
对象(即带有then
方法的对象),则返回的Promise
对象的最终状态由then
方法的执行决定;const thenable = { then(resolve, reject) { resolve(42); } }; Promise.resolve(thenable) .then((value) => { console.log(value); // 42 });
(3) 如果参数是不具有
then
方法的对象或基本数据类型,则返回的Promise
对象的状态为fulfilled
,并且将该参数传递给then
方法。Promise.resolve('Hello') .then((s) => { console.log(s); //Hello });
(4) 如果不带有任何参数,则返回的
Promise
对象的状态为fulfilled
,并且将undefined
作为参数传递给then
方法。Promise.resolve() .then((s) => { console.log(s); //undefined });
通常而言,如果你不知道一个值是否是
Promise
对象,使用Promise.resolve(value)
来返回一个Promise
对象,这样就能将该value
以Promise
对象形式使用。立即
resolve
的Promise
对象,是在本轮“事件循环”(event loop
)的结束时,而不是在下一轮“事件循环”的开始时。setTimeout(function () { console.log('three'); }, 0); Promise.resolve().then( () => { console.log('two'); }); console.log('one'); // one // two // three
Promise.reject(reason)
语法
Promise.reject(reason);
Promise.reject
等价于下面的写法。var p = Promise.reject(reason); // 等同于 var p = new Promise((resolve, reject) => reject(reason));
含义
返回一个状态为rejected
的Promise
对象,并将给定的失败信息传递给对应的处理方法。
注意:
Promise.resolve(value)
方法返回的Promise
实例的状态由value
决定,可能是fulfilled
,也可能是rejected
。Promise.reject(reason)
方法返回的Promise
实例的状态一定是rejected
。
用法
Promise.reject("Testing static reject") .then((value) => { // 未被调用 }).catch((reason) => { console.log(reason); // Testing static reject }); Promise.reject(new Error("fail")) .then((value) => { // 未被调用 }).catch((error) => { console.log(error); // Error: fail });
Promise.reject()
方法的参数,会原封不动地作为reject
的理由,变成后续方法的参数。这一点与Promise.resolve
方法不一致。const thenable = { then(resolve) { resolve('ok'); } }; Promise.resolve(thenable) .then(e => { console.log(e === 'ok'); //true }); Promise.reject(thenable) .catch(e => { console.log(e === thenable); // true });
3.7 Promise.all
参考答案:
Promise.all(iterable)
方法返回一个 Promise
实例,此实例在 iterable
参数内所有的 promise
都“完成(resolved)”或参数中不包含 promise
时回调完成(resolve);如果参数中 promise
有一个失败(rejected),此实例回调失败(reject),失败的原因是第一个失败 promise
的结果。
解析:
语法
Promise.all(iterable);
参数
iterable
一个可迭代对象,如
Array
或String
返回值
- 如果传入的参数是一个空的可迭代对象,则返回一个已完成(already resolved)状态的
Promise
- 如果传入的参数不包含任何
promise
,则返回一个异步完成(asynchronously resolved)Promise
。注意:Google Chrome 58 在这种情况下返回一个已完成(already resolved)状态的Promise
。 - 其它情况下返回一个处理中(pending)的
Promise
。这个返回的promise
之后会在所有的promise
都完成或有一个promise
失败时异步地变为完成或失败。 见下方关于“Promise.all 的异步或同步”示例。返回值将会按照参数内的promise
顺序排列,而不是由调用promise
的完成顺序决定。
3.8 与promise.all相反的是哪一个
参考答案:
Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。
扩展:
语法
Promise.race(iterable);
参数
iterable
可迭代对象,类似
Array
返回值
一个待定的 Promise
]只要给定的迭代中的一个promise解决或拒绝,就采用第一个promise的值作为它的值, 从而异步地解析或拒绝(一旦堆栈为空)。
3.9 promise实现文件读取
参考答案:
封装异步读取文件操作
- fs.readFile()方法用于异步读取文件(node核心模块)
- 将Promise的实例对象作为函数的返回值返回
- 这样函数执行完毕后就得到一个Promise对象的实例,可以通过.then方法传入成功的回调和失败的回调
const fs = require('fs'); const path = require('path'); function asyncGetFileByPath(p) { return new Promise((resolve, reject) => { // Promise对象里面的参数,会立即执行(前面说过) fs.readFile(path.join(__dirname, p), 'utf-8', (err, data) => { if (err) { reject(err); } else { resolve(data); } }) }) } asyncGetFileByPath('./files/1.txt') .then( (data) => { // 成功的回调 console.log(data); }, (err) => { // 失败的回调 console.error(err); } )
解决回调地狱
- 前面已经成功的封装了一个读取文件的函数
- 下面用它来体验一下读取多个文件
- 我们在.then()方法中,第一个参数resolve()方法中,返回一个promise对象B.
- 那么在执行.then()的resolve()方法完毕后,此时的执行环境是这个Promise的实例b
- 可以通过b的.then()方法继续传入resolve取消回调地狱,让代码趋于扁平化
const fs = require('fs'); const path = require('path'); function asyncGetFileByPath(p) { return new Promise((resolve, reject) => { // Promise对象里面的参数,会立即执行(前面说过) fs.readFile(path.join(__dirname, p), 'utf-8', (err, data) => { if (err) { reject(err); } else { resolve(data); } }) }) } asyncGetFileByPath('./files/1.txt') .then( (data) => { // 成功的回调 '1.txt' console.log(data); // 打印出 1.txt 数据 return asyncGetFileByPath('./files/2.txt') }, (err) => { // 失败的回调 console.error(err); } ) .then( // 成功的回调 '2.txt' (data) => { console.log(data); // 打印出 2.txt 中的数据 return asyncGetFileByPath('./files/3.txt') // 继续返回Promise对象的实例 }, (err) => { console.error(err); } ) .then( (data) => { // 成功的回调 '3.txt' console.log(data); // 打印出 3.txt 中的数据 }, (err) => { console.error(err); } )
3.10 用js实现sleep,用promise
参考答案:
function sleep(time) { return new Promise(resolve => setTimeout(resolve, time)) } const t1 = +new Date() sleep(3000).then(() => { const t2 = +new Date() console.log(t2 - t1) })
优点:这种方式实际上是用了 setTimeout,没有形成进程阻塞,不会造成性能和负载问题。
缺点:虽然不像 callback 套那么多层,但仍不怎么美观,而且当我们需要在某过程中需要停止执行(或者在中途返回了错误的值),还必须得层层判断后跳出,非常麻烦,而且这种异步并不是那么彻底,还是看起来别扭
3.11 实现一个 Scheduler 类,完成对Promise的并发处理,最多同时执行2个任务
参考答案:
class Scheduler { constructor() { this.tasks = [], // 待运行的任务 this.usingTask = [] // 正在运行的任务 } // promiseCreator 是一个异步函数,return Promise add(promiseCreator) { return new Promise((resolve, reject) => { promiseCreator.resolve = resolve if (this.usingTask.length < 2) { this.usingRun(promiseCreator) } else { this.tasks.push(promiseCreator) } }) } usingRun(promiseCreator) { this.usingTask.push(promiseCreator) promiseCreator().then(() => { promiseCreator.resolve() this.usingMove(promiseCreator) if (this.tasks.length > 0) { this.usingRun(this.tasks.shift()) } }) } usingMove(promiseCreator) { let index = this.usingTask.findIndex(promiseCreator) this.usingTask.splice(index, 1) } } const timeout = (time) => new Promise(resolve => { setTimeout(resolve, time) }) const scheduler = new Scheduler() const addTask = (time, order) => { scheduler.add(() => timeout(time)).then(() => console.log(order)) } addTask(400, 4) addTask(200, 2) addTask(300, 3)
3.12 循环i,setTimeout 中输出什么,如何解决(块级作用域,函数作用域)
参考答案:
for循环setTimeout输出1-10解决方式问题来源
for (var i = 0; i< 10; i++){ setTimeout((i) => { console.log(i); }, 0) }
期望:输出1到10
为什么无法输出1到十
在上面的代码中,for循环是同步代码,setTimeout是异步代码。遇到这种既包含同步又包含异步的情况,JavaScript依旧按照从上到下的顺序执行同步代码,并将异步代码插入任务队列。setTimeout的第二个参数则是把执行代码(console.log(i))添加到任务队列需等待的毫秒数,但等待的时间是相对主程序完毕的时间计算的,也就是说,在执行到setTimeout函数时会等待一段时间,再将当前任务插入任务队列。
最后,当执行完同步代码,js引擎就会去执行任务队列中的异步代码。这时候任务队列中就会有十个console.log(i)。我们知道,在每次循环中将setTimeout里面的代码“console.log(i)”放入任务队列时,i的值都是不一样的。但JavaScript引擎开始执行任务队列中的代码时,会开始在当前的作用域中开始找变量i,但是当前作用域中并没有对变量i进行定义。这个时候就会在创造该函数的作用域中寻找i。创建该函数的作用域就是全局作用域,这个时候就找到了for循环中的变量i,这时的i是全局变量,并且值已经确定:10。十个console.log“共享”i的值。这就是作用域链的问题。
解决方法
- 方法一
for (var i = 0; i< 10; i++){ setTimeout((i) => { console.log(i) }, 1000,i); }
最精简解决方案
- 方法二
for (let i = 0; i< 10; i++){ setTimeout(() => { console.log(i) }, 1000); }
最优解决方案,利用
let
形成块级作用域
- 方法三
for (var i = 0; i< 10; i++){ ((i)=>{ setTimeout(() => { console.log(i) },1000); })(i) }
IIFE(立即执行函数)
,类似于let生成了块级作用域。
- 方法四
for (var i = 0; i< 10; i++){ setTimeout(console.log(i),1000); }
直接输出,没有延迟
- 方法五
for (var i = 0; i< 10; i++){ setTimeout((()=>console.log(i))(),1000); }
同上
- 方法六
for (var i = 0; i< 10; i++){ try{ throw i }catch(i){ setTimeout(() => { console.log(i) }, 1000) } }
console.time('start'); setTimeout(function() { console.log(2); }, 10); setImmediate(function() { console.log(1); }); new Promise(function(resolve) { console.log(3); resolve(); console.log(4); }).then(function() { console.log(5); console.timeEnd('start') }); console.log(6); process.nextTick(function() { console.log(7); }); console.log(8);
3.13 js执行顺序的题目,涉及到settimeout、console、process.nextTick、promise.then
console.time('start'); setTimeout(function() { console.log(2); }, 10); setImmediate(function() { console.log(1); }); new Promise(function(resolve) { console.log(3); resolve(); console.log(4); }).then(function() { console.log(5); console.timeEnd('start') }); console.log(6); process.nextTick(function() { console.log(7); }); console.log(8);
参考答案:
综合的执行顺序就是: 3——>4——>6——>8——>7——>5——>start: 7.009ms——>1——>2
解析:
本题目,考察的就是 node 事件循环 Event Loop 我们可以简单理解Event Loop如下:
- 所有任务都在主线程上执行,形成一个执行栈(Execution Context Stack)
- 在主线程之外还存在一个任务队列(Task Queen),系统把异步任务放到任务队列中,然后主线程继续执行后续的任务
- 一旦执行栈中所有的任务执行完毕,系统就会读取任务队列。如果这时异步任务已结束等待状态,就会从任务队列进入执行栈,恢复执行
- 主线程不断重复上面的第三步
在上述的例子中,我们明白首先执行主线程中的同步任务,因此依次输出3、4、6、8
。当主线程任务执行完毕后,再从Event Loop中读取任务。
Event Loop读取任务的先后顺序,取决于任务队列(Job queue)中对于不同任务读取规则的限定。
在Job queue中的队列分为两种类型:
宏任务 Macrotask
宏任务是指Event Loop在每个阶段执行的任务
微任务 Microtask
微任务是指Event Loop在每个阶段之间执行的任务
我们举例来看执行顺序的规定,我们假设
宏任务队列包含任务: A1, A2 , A3
微任务队列包含任务: B1, B2 , B3
执行顺序为,首先执行宏任务队列开头的任务,也就是 A1 任务,执行完毕后,在执行微任务队列里的所有任务,也就是依次执行B1, B2 , B3,执行完后清空微任务队中的任务,接着执行宏任务中的第二个任务A2,依次循环。
了解完了宏任务 Macrotask
和微任务 Microtask
两种队列的执行顺序之后,我们接着来看,真实场景下这两种类型的队列里真正包含的任务(我们以node V8引擎为例),在node V8中,这两种类型的真实任务顺序如下所示:
宏任务 Macrotask队列真实包含任务:
script(主程序代码),setTimeout, setInterval, setImmediate, I/O, UI rendering
微任务 Microtask队列真实包含任务:
process.nextTick, Promises, Object.observe, MutationObserver
由此我们得到的执行顺序应该为:
script(主程序代码)—>process.nextTick—>Promises...——>setTimeout——>setInterval——>setImmediate——> I/O——>UI rendering
在ES6中宏任务 Macrotask队列又称为ScriptJobs,而微任务 Microtask又称PromiseJobs
我们的题目相对复杂,但是要注意,我们在定义promise的时候,promise构造部分是同步执行的
接下来我们分析我们的题目,首先分析Job queue的执行顺序:
script(主程序代码)——>process.nextTick——>promise——>setTimeout——>setImmediate
- 主体部分: 定义promise的构造部分是同步的,因此先输出3、4 ,主体部分再输出6、8(同步情况下,就是严格按照定义的先后顺序)
- process.nextTick: 输出7
- promise: 这里的promise部分,严格的说其实是promise.then部分,输出的是5、以及 timeEnd('start')
- setImmediate:输出1,依据上面优先级,应该先setTimeout,但是注意,setTimeout 设置 10ms 延时
- setTimeout : 输出2
4. this
4.1 call appy bind的作用和区别
参考答案:
作用:
都可以改变函数内部的this指向。
区别点:
- call 和 apply 会调用函数,并且改变函数内部this指向。
- call 和 apply 传递的参数不一样,call 传递参数arg1,arg2...形式 apply 必须数组形式[arg]
- bind 不会调用函数,可以改变函数内部this指向。
解析:
call方法
改变函数内部this指向
call()方法调用一个对象。简单理解为调用函数的方式,但是它可以改变函数的this指向。
写法:fun.call(thisArg, arg1, arg3, ...) // thisArg为想要指向的对象,arg1,arg3为参数
call 的主要作用也可以实现继承
function Person(uname, age) { this.uname = uname; this.age = age; } function Son(uname, age) { Person.call(this, uname, age); } var son = new Son("zhang", 12); console.log(son);
apply方法
apply()方法调用一个函数。简单理解为调用函数的方式,但是它可以改变函数的this指向。
写法:fun.apply(thisArg, [argsArray])
- thisArg:在fun函数运行时指定的this值
- argsArray:传递的值,必须包含在数组里面
- 返回值就是函数的返回值,因为他就是调用函数
apply的主要应用,比如可以利用apply可以求得数组中最大值
const arr = [1, 22, 3, 44, 5, 66, 7, 88, 9]; const max = Math.max.apply(Math, arr); console.log(max);
bind方法
bind()方法不会调用函数,但是能改变函数内部this指向
写法:fun.bind(thisArg, arg1, arg2, ...)
- thisArg:在fun函数运行时指定的this值
- arg1,arg2:传递的其他参数
- 返回由指定的this值和初始化参数改造的原函数拷贝
var o = { name: "lisa" }; function fn() { console.log(this); } var f = fn.bind(o); f();
bind应用
如果有的函数我们不需要立即调用,但是又需要改变这个函数的this指向,此时用bind再合适不过了
const btns = document.querySelectorAll("button"); for (let i = 0; i < btns.length; i++) { btns[i].onclick = function() { this.disabled = true; setTimeout( function() { this.disabled = false; }.bind(this), 2000 ); }; }
扩展:
主要应用场景:
- call 经常做继承。
- apply 经常跟数组有关系,比如借助于数学对象实现数组最大值最小值。
- bind 不调用函数,但是还想改变this指向,比如改变定时器内部的this指向。
4.2 this指向(普通函数、箭头函数)
参考答案:
普通函数中的this
- 谁调用了函数或者方法,那么这个函数或者对象中的this就指向谁
let getThis = function () { console.log(this); } let obj={ name:"Jack", getThis:function(){ console.log(this); } } //getThis()方法是由window在全局作用域中调用的,所以this指向调用该方法的对象,即window getThis();//window //此处的getThis()方法是obj这个对象调用的,所以this指向obj obj.getThis();//obj
- 匿名函数中的this:匿名函数的执行具有全局性,则匿名函数中的this指向是window,而不是调用该匿名函数的对象;
let obj = { getThis: function () { return function () { console.log(this); } } } obj.getThis()(); //window
上面代码中,getThi()方法是由obj调用,但是obj.getThis()返回的是一个匿名函数,而匿名函数中的this指向window,所以打印出window。 如果想在上述代码中使this指向调用该方法的对象,可以提前把this传值给另外一个变量(_this或者that):
let obj = { getThis: function () { //提前保存this指向 let _this=this return function () { console.log(_this); } } } obj.getThis()(); //obj
- 箭头函数中的this
- 箭头函数中的this是在函数定义的时候就确定下来的,而不是在函数调用的时候确定的;
- 箭头函数中的this指向父级作用域的执行上下文;(技巧:因为javascript中除了全局作用域,其他作用域都是由函数创建出来的,所以如果想确定this的指向,则找到离箭头函数最近的function,与该function平级的执行上下文中的this即是箭头函数中的this)
- 箭头函数无法使用apply、call和bind方法改变this指向,因为其this值在函数定义的时候就被确定下来。
例1:首先,距离箭头函数最近的是getThis(){},与该函数平级的执行上下文是obj中的执行上下文,箭头函数中的this就是下注释代码处的this,即obj。
let obj = { //此处的this即是箭头函数中的this getThis: function () { return ()=> { console.log(this); } } } obj.getThis()(); //obj
例2:该段代码中存在两个箭头函数,this找不到对应的function(){},所以一直往上找直到指向window。
//代码中有两个箭头函数,由于找不到对应的function,所以this会指向window对象。 let obj = { getThis: ()=> { return ()=> { console.log(this); } } } obj.getThis()(); //window
4.3 手写bind
参考答案:
- Function.prototype.bind,这样就可以让所有函数的隐式原型上都会有一个bind了。
Function.prototype.bind = function() { // TODO }
- bind的第一个形参是要绑定给函数的上下文,所以再完善一下上面的代码
Function.prototype.bind = function(context) { var fn = this; return function() { return fn.apply(context); } }
- 真正的bind函数是可以传递多个参数的,第一个参数是要绑定给调用它的函数的上下文,其他的参数将会作为预设参数传递给这个函数,如下所示
let foo = function(){ console.log(arguments); } foo.bind(null,"a","b")("c","d","e"); // {"1":"a","2":"b","3":"c","4":"d","5":"e"}
- 为了实现上面的效果,我们发现只要在返回的值上将函数合并上去就行了
Function.prototype.bind = function(context, ...args) { var fn = this; return function(...rest) { return fn.apply(context,[...args, ...rest]); } }
- 为了兼容性,替换成ES5的写法
Function.prototype.bind = function() { var args = Array.prototype.slice.call(arguments); var context = args.splice(0,1)[0]; var fn = this; return function() { let rest = Array.prototype.slice.call(arguments); return fn.apply(context, args.concat(rest)); } }
- 把函数的原型保留下来。
Function.prototype.bind = function() { var args = Array.prototype.slice.call(arguments); var context = args.splice(0,1)[0]; var fn = this; var res = function() { let rest = Array.prototype.slice.call(arguments); return fn.apply(context, args.concat(rest)); } if(this.prototype) { res.prototype = this.prototype; } return res; }
- 最后还需要再找到一种方法来判断是否对bind之后的结果使用了new操作符。
Function.prototype.bind = function() { var args = Array.prototype.slice.call(arguments); var context = args.splice(0,1)[0]; var fn = this; var noop = function() {} var res = function() { let rest = Array.prototype.slice.call(arguments); // this只和运行的时候有关系,所以这里的this和上面的fn不是一码事,new res()和res()在调用的时 候,res中的this是不同的东西 return fn.apply(this instanceof noop ? this : context, args.concat(rest)); } if(this.prototype) { noop.prototype = this.prototype; } res.prototype = new noop(); return res; }
4.4 箭头函数能否当构造函数
参考答案:
箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this
,arguments
,super
或new.target
。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。
4.5 继承,优缺点
参考答案:
- 继承的好处
- a:提高了代码的复用性
- b:提高了代码的维护性
- c:让类与类之间产生了关系,是多态的前提
- 继承的弊端
- 类的耦合性增强了,但是开发的原则:高内聚,低耦合。
4.6 js继承的方法和优缺点
参考答案:
原型链继承
实现方式:将子类的原型链指向父类的对象实例
function Parent(){ this.name = "parent"; this.list = ['a']; } Parent.prototype.sayHi = function(){ console.log('hi'); } function Child(){ } Child.prototype = new Parent(); var child = new Child(); console.log(child.name); child.sayHi();
原理:子类实例child的
__proto__
指向Child的原型链prototype,而Child.prototype指向Parent类的对象实例,该父类对象实例的__proto__
指向Parent.prototype,所以Child可继承Parent的构造函数属性、方法和原型链属性、方法
优点:可继承构造函数的属性,父类构造函数的属性,父类原型的属性
缺点:无法向父类构造函数传参;且所有实例共享父类实例的属性,若父类共有属性为引用类型,一个子类实例更改父类构造函数共有属性时会导致继承的共有属性发生变化;实例如下:var a = new Child(); var b = new Child(); a.list.push('b'); console.log(b.list); // ['a','b']
构造函数继承
实现方式:在子类构造函数中使用call或者apply劫持父类构造函数方法,并传入参数
function Parent(name, id){ this.id = id; this.name = name; this.printName = function(){ console.log(this.name); } } Parent.prototype.sayName = function(){ console.log(this.name); }; function Child(name, id){ Parent.call(this, name, id); // Parent.apply(this, arguments); } var child = new Child("jin", "1"); child.printName(); // jin child.sayName() // Error
原理:使用call或者apply更改子类函数的作用域,使this执行父类构造函数,子类因此可以继承父类共有属性
优点:可解决原型链继承的缺点
缺点:不可继承父类的原型链方法,构造函数不可复用组合继承
原理:综合使用构造函数继承和原型链继承
function Parent(name, id){ this.id = id; this.name = name; this.list = ['a']; this.printName = function(){ console.log(this.name); } } Parent.prototype.sayName = function(){ console.log(this.name); }; function Child(name, id){ Parent.call(this, name, id); // Parent.apply(this, arguments); } Child.prototype = new Parent(); var child = new Child("jin", "1"); child.printName(); // jin child.sayName() // jin var a = new Child(); var b = new Child(); a.list.push('b'); console.log(b.list); // ['a']
优点:可继承父类原型上的属性,且可传参;每个新实例引入的构造函数是私有的
缺点:会执行两次父类的构造函数,消耗较大内存,子类的构造函数会代替原型上的那个父类构造函数原型式继承
原理:类似Object.create,用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象,结果是将子对象的
__proto__
指向父对象var parent = { names: ['a'] } function copy(object) { function F() {} F.prototype = object; return new F(); } var child = copy(parent);
缺点:共享引用类型
寄生式继承
原理:二次封装原型式继承,并拓展
function createObject(obj) { var o = copy(obj); o.getNames = function() { console.log(this.names); return this.names; } return o; }
优点:可添加新的属性和方法
寄生组合式继承
原理:改进组合继承,利用寄生式继承的思想继承原型
function inheritPrototype(subClass, superClass) { // 复制一份父类的原型 var p = copy(superClass.prototype); // 修正构造函数 p.constructor = subClass; // 设置子类原型 subClass.prototype = p; } function Parent(name, id){ this.id = id; this.name = name; this.list = ['a']; this.printName = function(){ console.log(this.name); } } Parent.prototype.sayName = function(){ console.log(this.name); }; function Child(name, id){ Parent.call(this, name, id); // Parent.apply(this, arguments); } inheritPrototype(Child, Parent);
4.7 new会发生什么
参考答案:
创建空对象;
var obj = {};设置新对象的constructor属性为构造函数的名称,设置新对象的proto属性指向构造函数的prototype对象;
obj.proto = ClassA.prototype;
扩展了新对象的原型链。使用新对象调用函数,函数中的this被指向新实例对象:
ClassA.call(obj); //{}.构造函数();返回this指针。当存在显示的返回时,返回return后面的内容。新建的空对象作废。
function test() { this.name = "test"; } test.prototype = { a:{}, b:{} } var c = new test();
5. 文件引入方式
5.1 link 和 @import
参考答案:
作用:样式的导入方式
link 的使用
<link href="index.css" rel="stylesheet">
@import 的使用
<style type="text/css"> @import url(index.css); </style>
link 和 @import 的区别
引入的内容不同
link 除了引用样式文件,还可以引用图片等资源文件,而 @import 只引用样式文件
加载顺序不同
link 引用 CSS 时,在页面载入时同时加载;@import 需要页面网页完全载入以后加载
兼容性不同
link 是 XHTML 标签,无兼容问题;@import 是在 CSS2.1 提出的,低版本的浏览器不支持
对 JS 的支持不同
link 支持使用 Javascript 控制 DOM 去改变样式;而 @import 不支持
5.2 为什么link用href获取资源 script和img用src
参考答案:
src用于替换当前元素,href用于在当前文档和引用资源之间确立联系。
src
src是source的缩写,指向外部资源的位置,指向的内容将会嵌入到文档中当前标签所在位置;在请求src资源时会将其指向的资源下载并应用到文档内,例如js脚本,img图片和frame等元素
<script src ="js.js"></script>
当浏览器解析到该元素时,会暂停其他资源的下载和处理,直到将该资源加载、编译、执行完毕,图片和框架 等元素也如此,类似于将所指向资源嵌入当前标签内。这也是为什么将js脚本放在底部而不是头部
href
href
是Hypertext Reference
的缩写,指向网络资源所在位置,建立和当前元素(锚点)或当前文档(链接)之间的链接在文档中添加
link
标签,浏览器会识别该文档为css
文件,就会并行下载资源并且不会停止对当前文档的处理。这也是为什么建议使用link
方式来加载css
,而不是使用@import
方式<link href="common.css" rel="stylesheet"/>
6. ES5,ES6
6.1 es6中箭头函数
参考答案:
基本语法
ES6中允许使用箭头
=>
来定义箭头函数,具体语法,我们来看一个简单的例子:// 箭头函数 let fun = (name) => { // 函数体 return `Hello ${name} !`; }; // 等同于 let fun = function (name) { // 函数体 return `Hello ${name} !`; };
可以看出,定义箭头函在数语法上要比普通函数简洁得多。箭头函数省去了
function
关键字,采用箭头=>
来定义函数。函数的参数放在=>
前面的括号中,函数体跟在=>
后的花括号中。关于箭头函数的参数:
① 如果箭头函数没有参数,直接写一个空括号即可。
② 如果箭头函数的参数只有一个,也可以省去包裹参数的括号。
③ 如果箭头函数有多个参数,将参数依次用逗号(,)分隔,包裹在括号中即可。
// 没有参数 let fun1 = () => { console.log(111); }; // 只有一个参数,可以省去参数括号 let fun2 = name => { console.log(`Hello ${name} !`) }; // 有多个参数 let fun3 = (val1, val2, val3) => { return [val1, val2, val3]; };
关于箭头函数的函数体:
① 如果箭头函数的函数体只有一句代码,就是简单返回某个变量或者返回一个简单的JS表达式,可以省去函数体的大括号{ }。
let f = val => val; // 等同于 let f = function (val) { return val }; let sum = (num1, num2) => num1 + num2; // 等同于 let sum = function(num1, num2) { return num1 + num2; };
② 如果箭头函数的函数体只有一句代码,就是返回一个对象,可以像下面这样写:
// 用小括号包裹要返回的对象,不报错 let getTempItem = id => ({ id: id, name: "Temp" }); // 但绝不能这样写,会报错。 // 因为对象的大括号会被解释为函数体的大括号 let getTempItem = id => { id: id, name: "Temp" };
③ 如果箭头函数的函数体只有一条语句并且不需要返回值(最常见是调用一个函数),可以给这条语句前面加一个
void
关键字let fn = () => void doesNotReturn();
箭头函数最常见的用处就是简化回调函数。
// 例子一 // 正常函数写法 [1,2,3].map(function (x) { return x * x; }); // 箭头函数写法 [1,2,3].map(x => x * x); // 例子二 // 正常函数写法 var result = [2, 5, 1, 4, 3].sort(function (a, b) { return a - b; }); // 箭头函数写法 var result = [2, 5, 1, 4, 3].sort((a, b) => a - b);
箭头函数与普通函数的区别
2.1 语法更加简洁、清晰
从上面的基本语法示例中可以看出,箭头函数的定义要比普通函数定义简洁、清晰得多,很快捷。
2.2 箭头函数不会创建自己的this
箭头函数没有自己的
this
,它会捕获自己在定义时(注意,是定义时,不是调用时)所处的外层执行环境的this
,并继承这个this
值。所以,箭头函数中this
的指向在它被定义的时候就已经确定了,之后永远不会改变。var id = 'Global'; function fun1() { // setTimeout中使用普通函数 setTimeout(function(){ console.log(this.id); }, 2000); } function fun2() { // setTimeout中使用箭头函数 setTimeout(() => { console.log(this.id); }, 2000) } fun1.call({id: 'Obj'}); // 'Global' fun2.call({id: 'Obj'}); // 'Obj'
上面这个例子,函数
fun1
中的setTimeout
中使用普通函数,2秒后函数执行时,这时函数其实是在全局作用域执行的,所以this
指向Window
对象,this.id
就指向全局变量id
,所以输出'Global'
。 但是函数fun2
中的setTimeout
中使用的是箭头函数,这个箭头函数的this
在定义时就确定了,它继承了它外层fun2
的执行环境中的this
,而fun2
调用时this
被call
方法改变到了对象{id: 'Obj'}
中,所以输出'Obj'
var id = 'GLOBAL'; var obj = { id: 'OBJ', a: function(){ console.log(this.id); }, b: () => { console.log(this.id); } }; obj.a(); // 'OBJ' obj.b(); // 'GLOBAL'
上面这个例子,对象
obj
的方法a
使用普通函数定义的,普通函数作为对象的方法调用时,this
指向它所属的对象。所以,this.id
就是obj.id
,所以输出'OBJ'
。 但是方法b
是使用箭头函数定义的,箭头函数中的this
实际是继承的它定义时所处的全局执行环境中的this
,所以指向Window
对象,所以输出'GLOBAL'
。(这里要注意,定义对象的大括号{}
是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中!!)箭头函数继承而来的this指向永远不变(重要!!深入理解!!)
上面的例子,就完全可以说明箭头函数继承而来的
this
指向永远不变。对象obj
的方法b
是使用箭头函数定义的,这个函数中的this
就永远指向它定义时所处的全局执行环境中的this
,即便这个函数是作为对象obj
的方法调用,this
依旧指向Window
对象。.call()/.apply()/.bind()无法改变箭头函数中this的指向
.call()
/.apply()
/.bind()
方法可以用来动态修改函数执行时this
的指向,但由于箭头函数的this
定义时就已经确定且永远不会改变。所以使用这些方法永远也改变不了箭头函数this
的指向,虽然这么做代码不会报错。var id = 'Global'; // 箭头函数定义在全局作用域 let fun1 = () => { console.log(this.id) }; fun1(); // 'Global' // this的指向不会改变,永远指向Window对象 fun1.call({id: 'Obj'}); // 'Global' fun1.apply({id: 'Obj'}); // 'Global' fun1.bind({id: 'Obj'})(); // 'Global'
箭头函数不能作为构造函数使用
我们先了解一下构造函数的new都做了些什么?简单来说,分为四步: ① JS内部首先会先生成一个对象; ② 再把函数中的this指向该对象; ③ 然后执行构造函数中的语句; ④ 最终返回该对象实例。
但是因为箭头函数没有自己的
this
,它的this
其实是继承了外层执行环境中的this
,且this
指向永远不会随在哪里调用、被谁调用而改变,所以箭头函数不能作为构造函数使用,或者说构造函数不能定义成箭头函数,否则用new
调用时会报错let Fun = (name, age) => { this.name = name; this.age = age; }; // 报错 let p = new Fun('cao', 24);
箭头函数没有自己的arguments
箭头函数没有自己的
arguments
对象。在箭头函数中访问arguments
实际上获得的是外层局部(函数)执行环境中的值。// 例子一 let fun = (val) => { console.log(val); // 111 // 下面一行会报错 // Uncaught ReferenceError: arguments is not defined // 因为外层全局环境没有arguments对象 console.log(arguments); }; fun(111); // 例子二 function outer(val1, val2) { let argOut = arguments; console.log(argOut); // ① let fun = () => { let argIn = arguments; console.log(argIn); // ② console.log(argOut === argIn); // ③ }; fun(); } outer(111, 222);
上面例子二,①②③处的输出结果如下:
很明显,普通函数
outer
内部的箭头函数fun
中的arguments
对象,其实是沿作用域链向上访问的外层outer
函数的arguments
对象。可以在箭头函数中使用rest参数代替arguments对象,来访问箭头函数的参数列表!!
箭头函数没有原型prototype
let sayHi = () => { console.log('Hello World !') }; console.log(sayHi.prototype); // undefined
箭头函数不能用作Generator函数,不能使用yeild关键字
6.2 ES6新特性
参考答案:
变量和作用域
1.1 let 、const、 块级作用域和变量声明
let声明的变量只在所在块中生效;
let声明的变量可以解决var与for循环结合使用产生的无法取得最新变量值的问题(以往都需要通过闭包来解决这个问题);
let声明的变量不存在变量提升(从undefined->ReferenceError,其实也是一种暂时性死区)、会造成变量暂时性死区(在声明let变量之前都不能用它)、也不允许重复声明;
const声明的变量行为与let类似,只是多了两点更强的约束:1.声明时必须赋值;2.声明的变量内存地址不可变,需要注意的是:对于用const声明基本类型,值就保存在内存地址之中,意味着变量不可重新赋值;对于用const声明的对象,对象内容还是可以更改的,只是不能改变其指向。(冻结对象应该用Object.freeze())
1.2 解构赋值(按照一定的结构解析出来进行赋值)
解构赋值的使用场景:变量快捷赋值、提取数据、函数参数定义和默认值、遍历某结构
原生对象的方法扩展
2.1 String
加强了对unicode的支持、支持字符串遍历(后面有讲到实际上是部署了iterator接口)、repeat()等方法的支持、模板字符串
2.2 RegExp
构造函数第一个参数是正则表达式,指定第二个参数不再报错、u修饰符、y修饰符、s修饰符
2.3 Number
二进制和八进制新写法、新方法parseInt()、Number.EPSILON极小常量、安全整数、Math新方法
2.4 Function
函数参数默认值、rest参数、函数内部严格模式、函数的name属性、箭头函数
2.5 Array
扩展运算符...
2.6 Object 和 Symbol
(1) Object对象
支持简写:同名属性K-V可以省略一个、函数声明可以省略function;支持属性名表达式、函数名表达 式。(注意:以上2个——表达式和简写不能同时使用)。
对象的方法的name属性返回方法名,但有几个例外情况要小心。新增了Object方法
Object.is()——用于解决==和===的部分兼容问题
Object.assign()——将src的所有可枚举对象属性复制到dest对象上(浅复制)
Object.setPrototypeOf()、Object.getPrototypeOf() (Object.__proto属性)
Object.entries()、Object.keys()、Object.values()
ES6中5种遍历对象属性的方法
for-in——自身和继承的可枚举属性(除Symbol)
Object.keys()——自身非继承的可枚举属性(除Symbol)
Object.getOwnPropertyNames()——自身所有属性键名(包括不可枚举、除Symbol)
Object.getOwnPropertySymbols()——自身的所有 Symbol 属性的键名
Reflect.ownKeys()——自身的所有键名
(2)Symbol类型
ES5以前,对象属性都只能是字符串,容易造成重命名导致的冲突。Symbol提供了一种机制,可以保存 属性名是独一无二的。Symbol类型的使用注意:1)创建是调用函数,而不是new关键字 2)Symbol类 型的属性不会被for-*、Object.keys()、Object.getPropertyNames()返回,可以用后面两种方法遍历。
数据结构Set和Map
Set是一种类似数组的数据结构,区别在于其存储的成员都是不重复的,由此带来了它的一个应用就是:去重。Set通过new关键字实例化,入参可以是数组or类数组的对象。
值得注意的是:在Set中,只能存储一个NaN,这说明在Set数据结构中,NaN等于NaN。
Set实例的方法:操作方法add()、delete()、has()和clear();遍历方法:keys()、values()、entries()和forEach();扩展运算符...、数组方法map()、filter()方法也可以用于Set结构。由此它可以很方便的实现数组的交、并、差集。
WeakSet类似于Set,主要区别在于1.成员只能是对象类型;2.对象都是弱引用(如果其他对象都不再引用该对象,垃圾回收机制会自动回收该对象所占的内存,不可预测何时会发生,故WeakSet不可被遍历)
JavaScript对象Object都是键值K-V对的集合,但K取值只能是字符串和Symbol,Map也是K-V的集合,然而其K可以取任意类型。如果需要键值对的集合,Map比Object更适合。Map通过new关键字实例化。
Map实例的方法:set()、get()、has()、delete()和clear();遍历方法同Set。
Map与其它数据结构的互相转换:Map <---> 数组| Map <---> 对象| Map <---> JSON。
WeakMap类似于Map,主要区别在于:1.只接受对象作为键名;2.键名所指向的对象不计入垃圾回收机制。
元编程相关Proxy和Reflect
4.1 Proxy
对目标对象加一层“拦截”(“代理”),外界对对象的访问、修改都必须先通过这层拦截层。因而它提供了 一个机制可以对外界的访问进行过滤和改写。
用法:var proxy = new Proxy(p1,p2); p1是要被代理的目标对象,p2是配置对象。
值得注意的是:Proxy不是对目标对象透明的代理——即使不做任何拦截的情况下无法保证代理对象与目 标对象行为的完全一致。(主要原因在于代理时,目标对象内部的this会指向代理对象)
4.2 Reflect
与Proxy一样是ES6为语言层面的用于操作对象提供的新API,目前它所拥有的对象方法与Proxy对象一一对 应,引入目的:1.将Object对象上一些属于语言内部的方法放在Reflect上(目前都可以放)2.修改Object对 象上某些方法的返回值,使得更加合理化(健壮)3.让Object对象的操作从命令式完全转化为函数式
异步编程Promise、Generator和Async
在JavaScript的世界里,对于异步编程存在如下几种方案:1.回调函数;2.事件触发监听;3.发布订阅者模式;4.Promise。首先介绍Promise,然后介绍ES6提供的生成器函数,async函数。
Promise来源于社区,代表一个对象,它代表异步操作未来的一个结果(承诺)。它总共有三个状态,pending\fulfilled\rejected。另外,它的状态翻转路径只有两个:pending->fulfilled or pending->rejected,一旦状态翻转,就不可变了。它支持链式调用,支持错误传递,支持以同步代码的方式写异步操作。
Promise是一个对象,创建此对象实例的方法如下(可以理解resolve和reject是已返回的承诺对象未来回调函数的占位)
Generator函数是ES6提供的异步编程解决方案。对于Generator函数,可以将它理解为一个状态机,封装了多个内部状态;此外它还是一个遍历器生成函数,这个函数可以遍历出状态机的所有状态。
函数特征:关键字function与函数名之间有*,函数体内部yeild关键字。
生成器函数与普通函数的区别:函数调用后不执行,而是返回一个指针对象(遍历器对象)。调用对象的next()方法,执行一段yield逻辑。故函数的分段执行的,yield是暂停执行的标志,next()可以恢复执行。
yield与return的区别:yield有记忆功能,return没有;一个函数可以多次执行yeild,但只会return一次
async函数是Generator函数的语法糖,它进行了改进:1.自带执行器;2.返回值是Promise;
三家对比:使用Promise的异步代码存在大量自有API的调用,操作本身的语义夹杂其中,不是很清晰;Generator函数实现的异步代码语义比Promise清晰,但需要一个执行器;async函数的写法最简洁、符合语义,不需要执行器。
语言层面类、模块的支持
6.1 class
从 ES6 开始,JavaScript 提供了 class 关键字来定义类,尽管,这样的方案仍然是基于原型运行时系统的模拟,大部分功能ES5可以实现。
构造函数的
prototype
属性在 ES6 的“类”上面继续存在。事实上,类中所有方法都定义在类的prototype
属性上面(因而也是不可枚举的)。constructor
方法是类的默认方法,通过new
命令生成对象实例时,自动调用该方法。一个类必须有constructor
方法,如果没有显式定义,一个空的constructor
方法会被默认添加。(默认构造函数);constructor
方法默认返回实例对象(即this
),完全可以指定返回另外一个对象。注意区别:类必须使用
new
调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new
也可以执行。类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上
static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。实例属性除了定义在
constructor()
方法里面的this
上面,也可以定义在类的最顶层。私有属性和方法如何实现?1.命名上加以区别 2.将私有方法移出模块,利用公有方法调用;3.Symbol属性上(都不完美)
6.2 module
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定。
编译时加载VS运行时加载——对象VS代码
模块命令:export和import;一个文件即为一个模块,除非导入否则外部无法读取模块属性;
export支持:变量、函数和类
export
命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import
命令也是如此。输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。由于
import
是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。使用
import
命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default
命令,为模块指定默认输出。模块之间也可以继承。
JS中对象分类、及其它原生对象
Iterator
ES6之前在JS中只有Array和对象可以表示“集合”这种数据结构,ES6中增加了:Set和Map。由此,四种之间互相组合又可以定义新的数据结构。这些新定义的数据结构如何访问呢?遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作。遍历器对象本质上是一个指针对象。
只要为某个数据结构部署 了Iterator接口,则可以称此数据结构是可遍历的。iterator属性部署在Symbol上。如下对象默认部署了Iterator结口:Array Set Map String等。部署iterator结构的要点:1)在Symbol.iterator上部署;2)必须包含next()函数。默认调用iterator接口的场景:解构赋值、...扩展运算符、yeild* 。for-of循环内部调用的即是调用数据机构内部的Symbol.iterator方法。
for-in和for-of循环
for-in用于遍历对象属性,对象自身和继承的可枚举属性(不可遍历Symbol属性)
for-of循环是一种遍历所有数据机构的统一方法。实现原理是数据结构上部署的Symbol.iterator属性。
6.3 ES6 与 ES5 继承的区别
参考答案:
ES6 中有类 class 的概念,类 class 的继承是通过 extends 来实现的,ES5 中是通过设置构造函数的 prototype 属性,来实现继承的。
ES6 与 ES5 中的继承有 2 个区别,第一个是,ES6 中子类会继承父类的属性,第二个区别是,super() 与 A.call(this) 是不同的,在继承原生构造函数的情况下,体现得很明显,ES6 中的子类实例可以继承原生构造函数实例的内部属性,而在 ES5 中做不到。
解析:
下面通过 3 个 demo,来分析它们之间的区别。
- ES5 继承
直接上代码:
function A() { this.a = 'hello'; } function B() { A.call(this); this.b = 'world'; } B.prototype = Object.create(A.prototype, { constructor: { value: B, writable: true, configurable: true } }); let b = new B();
代码中,构造函数 B 继承构造函数 A,首先让构造函数 B 的 prototype 对象中的 proto 属性指向构造函数 A 的 prototype 对象,并且将构造函数 B 的 prototype 对象的 constructor 属性赋值为构造函数 B,让构造函数 B 的实例继承构造函数 A 的原型对象上的属性,然后在构造函数 B 内部的首行写上 A.call(this),让构造函数 B 的实例继承构造函数 A 的实例属性。在 ES5 中实现两个构造函数之间的继承,只需要做这两步即可。下面六幅图分别是,实例 b 的原型链及验证图,构造函数 B 的原型链及验证图,构造函数 A 的原型链及验证图。
实例 b 的原型链如下图:
实例 b 的原型链验证图:
构造函数 B 的原型链图下图:
构造函数 B 的原型链验证图图:
构造函数 A 的原型链图下图:
构造函数 B 的原型链验证图图:
从上面 6 幅图可知,构造函数 B 的实例 b 继承了构造函数 A 的实例属性,继承了构造函数 A 的原型对象上的属性,继承了构造函数 Object 的原型对象上的属性。构造函数 B 是构造函数 Function 的实例,继承了构造函数 Function 的原型对象上的属性,继承了构造函数 Object 的原型对象上的属性。 构造函数 A 是构造函数 Function 的实例,继承了构造函数 Function 的原型对象上的属性,继承了构造函数 Object 的原型对象上的属性。可看出,构造函数 A 与 构造函数 B 并没有继承关系,即构造函数 B 没有继承构造函数 A 上面的属性,在 ES6 中,用 extends 实现两个类的继承,两个类之间是有继承关系的,即子类继承了父类的方法,这是 ES6 与 ES5 继承的第一点区别,下面通过 ES6 的继承来说明这一点。
- ES6 继承
直接上代码:
class A { constructor() { this.a = 'hello'; } } class B extends A { constructor() { super(); this.b = 'world'; } } let b = new B(); 复制代码
代码中,类 B 通过 extends 关键字继承类 A 的属性及其原型对象上的属性,通过在类 B 的 constructor 函数中执行 super() 函数,让类 B 的实例继承类 A 的实例属性,super() 的作用类似构造函数 B 中的 A.call(this),但它们是有区别的,这是 ES6 与 ES5 继承的第二点区别,这个区别会在文章的最后说明。在 ES6 中,两个类之间的继承就是通过 extends 和 super 两个关键字实现的。下面四幅图分别是,实例 b 的原型链及验证图,类 B 的原型链及验证图。
实例 b 的原型链如下图:
实例 b 的原型链验证图:
类 B 的原型链图下图:
类 B 的原型链验证图图:
通过上面 4 幅图可知,在 ES6 与 ES5 中,类 B 的实例 b 的原型链与构造函数 B 的实例 b 的原型链是相同的,但是在 ES6 中类 B 继承了类 A 的属性,在 ES5 中,构造函数 B 没有继承构造函数 A 的属性,这是 ES6 与 ES5 继承的第一个区别。
- super() 与 A.call(this) 的区别
在 ES5 中,构造函数 B 的实例继承构造函数 A 的实例属性是通过 A.call(this) 来实现的,在 ES6 中,类 B 的实例继承类 A 的实例属性,是通过 super() 实现的。在不是继承原生构造函数的情况下,A.call(this) 与 super() 在功能上是没有区别的,用 babel 在线转换 将类的继承转换成 ES5 语法,babel 也是通过 A.call(this) 来模拟实现 super() 的。但是在继承原生构造函数的情况下,A.call(this) 与 super() 在功能上是有区别的,ES5 中 A.call(this) 中的 this 是构造函数 B 的实例,也就是在实现实例属性继承上,ES5 是先创造构造函数 B 的实例,然后在让这个实例通过 A.call(this) 实现实例属性继承,在 ES6 中,是先新建父类的实例对象this,然后再用子类的构造函数修饰 this,使得父类的所有行为都可以继承。下面通过 2 段代码说明这个问题。
代码 1:
function MyArray() { Array.call(this); } MyArray.prototype = Object.create(Array.prototype, { constructor: { value: MyArray, writable: true, configurable: true } }); var colors = new MyArray(); colors[0] = "red"; colors.length;
这段代码的思路就是,让构造函数 MyArray 继承原生构造函数 Array,然后验证 MyArray 的实例是否具有 Array 实例的特性。
代码 1 执行结果如下图:
从结果可以看出,MyArray 的实例并不具有 Array 实例的特性,之所以会发生这种情况,是因为 MyArray 的实例无法获得原生构造函数 Array 实例的内部属性,通过 Array.call(this) 也不行。
代码 2:
class MyArray extends Array { constructor() { super(); } } var arr = new MyArray(); arr[0] = 12; arr.length; 复制代码
代码 2 执行结果如下图:
从结果可以看出,通过 super(),MyArray 的实例具有 Array 实例的特性。
6.4 哪些类型能被扩展操作符...扩展
参考答案:
适用类型:数组、对象、字符串。
复杂数据类型都可以,当要转化为可迭代数据结构时可设置对象的迭代器对扩展运算符扩展出来的值进行操作。
基础数据只有string可以使用扩展运算符,number,boolean,null,undefined无效
6.5 事件扩展符用过吗(...),什么场景下
参考答案:
扩展运算符的应用场景
// 1、函数调用 function add(x, y) { return x + y; } add(...[4, 38]); function f(v, w, x, y, z) { } f(-1, ...[0, 1], 2, ...[3]); // 123456789 //2.往数组里push多个元素 var arr1 = [0, 1, 2]; var arr2 = [3, 4, 5]; arr1.push(...arr2); console.log(arr1); //[0,1,2,3,4,5] //123456 //3.替代函数的apply方法 function f(x, y, z) { } var args = [0, 1, 2]; f.apply(null, args); //ES5 的写法 f(...args); //ES6的写法 // 123456 //4.求一个数组的最大数简化 Math.max.apply(null, [14, 3, 77]) //ES5 的写法 Math.max(...[14, 3, 77]) //ES6 的写法,等同于Math.max(14, 3, 77) //1234 //5.扩展运算符后面可以放表达式 const arr = [...(5 > 0 ? ['a'] : []),'b']; console.log(arr); //['a','b'] //1234 //6.与解构赋值结合,用于生成数组 const a1 = [1, 2]; const a2 = [...a1]; //写法1 const [...a2] = a1; //写法2 const [first, ...rest] = [1, 2, 3, 4, 5]; first //1 rest //[2, 3, 4, 5] const [first, ...rest] = []; first //undefined rest //[] const [first, ...rest] = ["foo"]; first //"foo" rest //[] //1234567891011121314151617 //7.合并数组 [...arr1, ...arr2, ...arr3] //[ 'a', 'b', 'c', 'd', 'e' ] 123 //8.数组的克隆——————————————————————特别注意 var arr1 = [0, 1, 2]; var arr2 = [...arr1]; arr1[0]=100; console.log(arr2); //[0, 1, 2] /* 乍一看,arr2与arr1不共用引用地址,arr2不随着arr1变化,接着往下看 */ var arr1 = [0, [1,11,111], 2]; var arr2 = [...arr1]; arr1[1][0]=100; console.log(arr2); //[0, [100,11,111], 2]
6.6 让不同的浏览器兼容ES6的方法
参考答案:
针对 ES6 的兼容性问题,很多团队为此开发出了多种语法解析转换工具,把我们写的 ES6 语法转换成 ES5,相当于在 ES6 和浏览器之间做了一个翻译官。比较通用的工具方案有 babel,jsx,traceur,es6-shim 等。