因为所有笔记有接近4W的字数,所以分开为上、下两部分,第二部份内容计划于明后两天更新。
若是你以为写的不错请给一个star
,若是你想阅读上、下两部分所有的笔记,请点击全文javascript
在ECMAScript6
标准定稿以前,已经开始出现了一些实验性的转译器(Transpiler)
,例如谷歌的Traceur
,能够将代码从ECMAScript6
转换成ECMAScript5
。但它们大多功能很是有限,或难以插入现有的JavaScript
构建管道。
可是,随后出现了的新型转译器6to5
改变了这一切。它易于安装,能够很好的集成现有的工具中,生成的代码可读,因而就像野火同样逐步蔓延开来,6to5
也就是如今鼎鼎大名的Babel
。html
JavaScript核心的语言特性是在标准ECMA-262中被定义,该标准中定义的语言被称做ECMAScript,它是JavaScript的子集。 java
演变之路:git
停滞不前
:逐渐兴起的Ajax
开创了动态Web
引用的新时代,而自1999年第三版ECMA-262
发布以来,JavaScript
却没有丝毫的改变。转折点
:2007年,TC-39
委员会将大量规范草案整合在了ECMAScript4
中,其中新增的语言特性涉足甚广,包括:模块、类、类继承、私有对象成员等众多其它的特性。分歧
:然而TC-39
组织内部对ECMAScript4
草案产生了巨大的分歧,部分红员认为不该该一次性在第四版标准中加入过多的新功能,而来自雅虎、谷歌和微软的技术负责人则共同提交了一份ECMAScript3.1
草案做为下一代ECMAScript
的可选方案,其中此方案只是对现有标准进行小幅度的增量修改。行为更专一于优化属性特性、支持原生JSON
以及为已有对象增长新的方法。从未面世的ECMAScript4
:2008年,JavaScript
创始人Brendan Eich
宣布TC-39
委员一方面会将合理推动ECMAScript3.1
的标准化工做,另外一方面会暂时将ECMAScript4
标准中提出的大部分针对语法及特性的改动搁置。ECMAScript5
:通过标准化的ECMAScript3.1
最终做为ECMA-262
第五版于2009年正式发布,同时被命名为ECMAScript5
。ECMAScript6
:在ECMAScript5
发布后,TC-39
委员会于2013年冻结了ECMAScript6
的草案,再也不添加新的功能。2013年ECMAScript6
草案发布,在进过12个月各方讨论和反馈后。2015年ECMAScript6
正式发布,并命名为ECMAScript 2015
。过去JavaScript中的变量声明机制一直令咱们感到困惑,大多数类C语言在声明变量的同时也会建立变量,而在之前的JavaScript中,什么时候建立变量要看如何声明的变量,ES6引入块级做用域可让咱们更好的控制做用域。 es6
问:提高机制(hoisting
)是什么?
答:在函数做用域或全局做用域中经过关键字var
声明的变量,不管其实是在哪里声明的,都会被当成在当前做用域顶部声明的变量,这就是咱们常说的提高机制。
如下实例代码说明了这种提高机制:github
function getValue (condition) {
if (condition) {
var value = 'value'
return value
} else {
// 这里能够访问到value,只不过值为undefined
console.log(value)
return null
}
}
getValue(false) // 输出undefined
复制代码
你能够在以上代码中看到,当咱们传递了false
的值,但依然能够访问到value
这个变量,这是由于在预编译阶段,JavaScript
引擎会将上面函数的代码修改为以下形式:正则表达式
function getValue (condition) {
var value
if (condition) {
value = 'value'
return value
} else {
console.log(value)
return null
}
}
复制代码
通过以上示例,咱们能够发现:变量value
的声明被提高至函数做用域的顶部,而初始化操做依旧留在原处执行,正由于value
变量只是声明了而没有赋值,所以以上代码才会打印出undefined
。数组
块级声明用于声明在指定的做用于以外无妨访问的变量,块级做用域存在于:函数内部和块中。浏览器
let
声明:缓存
let
声明和var
声明的用法基本相同。let
声明的变量不会被提高。let
不能在同一个做用域中重复声明已经存在的变量,会报错。let
声明的变量做用域范围仅存在于当前的块中,程序进入块开始时被建立,程序退出块时被销毁。根据let
声明的规则,改动上面的代码后像下面这样:
function getValue (condition) {
if (condition) {
// 变量value只存在于这个块中。
let value = 'value'
return value
} else {
// 访问不到value变量
console.log(value)
return null
}
}
复制代码
const
声明:const
声明和let
声明大多数状况是相同的,惟一的本质区别在于,const
是用来声明常量的,其声明后的变量值不能再被修改,即意味着:const
声明必须进行初始化。
const MAX_ITEMS = 30
// 报错
MAX_ITEMS = 50
复制代码
咱们说的const变量值不可变,须要分两种类型来讲: 值类型:变量的值不能改变。 引用类型:变量的地址不能改变,值能够改变。
const num = 23
const arr = [1, 2, 3, 4]
const obj = {
name: 'why',
age: 23
}
// 报错
num = 25
// 不报错
arr[0] = 11
obj.age = 32
console.log(arr) // [11, 2, 3, 4]
console.log(obj) // { name: 'why', age: 32 }
// 报错
arr = [4, 3, 2, 1]
复制代码
由于let
和const
声明的变量不会进行声明提高,因此在let
和const
变量声明以前任何访问(即便是typeof
也不行)此变量的操做都会引起错误:
if (condition) {
// 报错
console.log(typeof value)
let value = 'value'
}
复制代码
问:为何会报错?
答:JavaScript
引擎在扫描代码发现变量声明时,要么将它们提高至做用域的顶部(var
声明),要么将声明放在TDZ
(暂时性死区)中(let
和const
声明)。访问TDZ
中的变量会触发错误,只有执行变量声明语句以后,变量才会从TDZ
中移出,随后才能正常访问。
咱们都知道:若是咱们在全局做用域下经过var
声明一个变量,那么这个变量会挂载到全局对象window
上:
var name = 'why'
console.log(window.name) // why
复制代码
但若是咱们使用let
或者const
在全局做用域下建立一个新的变量,这个变量不会添加到window
上。
const name = 'why'
console.log('name' in window) // false
复制代码
在ES6
早期,人们广泛认为应该默认使用let
来代替var
,这是由于对于开发者而言,let
实际上与他们想要的var
同样,直接替换符合逻辑。但随着时代的发展,另外一种作法也愈来愈普及:默认使用const
,只有肯定变量的值会在后续须要修改时才会使用let
声明,由于大部分变量在初始化后不该再改变,而预料之外的变量值改变是不少bug
的源头。
本章节中关于unicode
和正则部分未整理。
模板字面量是扩展ECMAScript
基础语法的语法糖,其提供了一套生成、查询并操做来自其余语言里内容的DSL
,且能够免受XSS
注入攻击和SQL
注入等等。
在ES6
以前,JavaScript
一直以来缺乏许多特性:
HTML
插入通过安全转换后的字符串的能力。而在ECMAScript 6
中,经过模板字面量的方式对以上问题进行了填补,一个最简单的模板字面量的用法以下:
const message = `hello,world!`
console.log(message) // hello,world!
console.log(typeof message) // string
复制代码
一个须要注意的地方就是,若是咱们须要在字符串中使用反撇号,须要使用\
来进行转义,以下:
const message = `\`hello\`,world!`
console.log(message) // `hello`,world!
复制代码
自JavaScript
诞生起,开发者们就一直在尝试和建立多行字符串,如下是ES6
以前的方法:
在字符串的新行最前方加上\
能够承接上一行代码,能够利用这个小bug
来建立多行字符串。
const message = 'hello\ ,world!'
console.log(message) // hello,world
复制代码
在ES6
以后,咱们可使用模板字面量,在里面直接换行就能够建立多行字符串,以下:
在模板字面量中,即反撇号中全部空白字符都属于字符串的一部分。
const message = `hello ,world!`
console.log(message) // hello
// ,world!
复制代码
模板字面量于普通字符串最大的区别是模板字符串中的占位符功能,其中占位符中的内容,能够是任意合法的JavaScript
表达式,例如:变量,运算式,函数调用,甚至是另一个模板字面量。
const age = 23
const name = 'why'
const message = `Hello ${name}, you are ${age} years old!`
console.log(message) // Hello why, you are 23 years old!
复制代码
模板字面量嵌套:
const name = 'why'
const message = `Hello, ${`my name is ${name}`}.`
console.log(message) // Hello, my name is why.
复制代码
标签指的是在模板字面量第一个反撇号前方标注的字符串,每个模板标签均可以执行模板字面量上的转换并返回最终的字符串值。
// tag就是`Hello world!`模板字面量的标签模板
const message = tag`Hello world!`
复制代码
标签能够是一个函数,标签函数一般使用不定参数特性来定义占位符,从而简化数据处理的过程,就像下面这样:
function tag(literals, ...substitutions) {
// 返回一个字符串
}
const name = 'why'
const age = 23
const message = tag`${name} is ${age} years old!`
复制代码
其中literals
是一个数组,它包含:
substitutions
也是一个数组:
name
的值,即:why
。age
的值,即:23
。经过以上规律咱们能够发现:
literals[0]
始终表明字符串的开头。literals
总比substitutions
多一个。咱们能够经过以上这种模式,将literals
和substitutions
这两个数组交织在一块儿从新组成一个字符串,来模拟模板字面量的默认行为,像下面这样:
function tag(literals, ...substitutions) {
let result = ''
for (let i = 0; i< substitutions.length; i++) {
result += literals[i]
result += substitutions[i]
}
// 合并最后一个
result += literals[literals.length - 1]
return result
}
const name = 'why'
const age = 23
const message = tag`${name} is ${age} years old!`
console.log(message) // why is 23 years old!
复制代码
经过模板标签能够访问到字符串转义被转换成等价字符串前的原生字符串。
const message1 = `Hello\nworld`
const message2 = String.raw`Hello\nworld`
console.log(message1) // Hello
// world
console.log(message2) // Hello\nworld
复制代码
在ES6
以前,你可能会经过如下这种模式建立函数并为参数提供默认值:
function makeRequest (url, timeout, callback) {
timeout = timeout || 2000
callback = callback || function () {}
}
复制代码
代码分析:在以上示例中,timeout
和callback
是可选参数,若是不传入则会使用逻辑或操做符赋予默认值。然而这种方式也有必定的缺陷,若是咱们想给timeout
传递值为0
,虽然这个值是合法的,但由于有或逻辑运算符的存在,最终仍是为timeout
赋值2000
。
针对以上状况,咱们应该经过一种更安全的作法(使用typeof
)来重写一下以上示例:
function makeRequest (url, timeout, callback) {
timeout = typeof timeout !== 'undefined' ? timeout : 2000
callback = typeof callback !== 'undefined' ? callback : function () {}
}
复制代码
代码分析:尽管以上方法更安全一些,但咱们任然须要额外的撰写更多的代码来实现这种很是基础的操做。针对以上问题,ES6
简化了为形参提供默认值的过程,就像下面这样:
对于默认参数而言,除非不传或者主动传递undefined才会使用参数默认值(若是传递null,这是一个合法的参数,不会使用默认值)。
function makeRequest (url, timeout = 2000, callback = function () {}) {
// todo
}
// 同时使用timeout和callback默认值
makeRequest('https://www.taobao.com')
// 使用callback默认值
makeRequest('https://www.taobao.com', 500)
// 不使用默认值
makeRequest('https://www.taobao.com', 500, function (res) => {
console.log(res)
})
复制代码
在ES5
非严格模式下,若是修改参数的值,这些参数的值会同步反应到arguments
对象中,以下:
function mixArgs(first, second) {
console.log(arguments[0]) // A
console.log(arguments[1]) // B
first = 'a'
second = 'b'
console.log(arguments[0]) // a
console.log(arguments[1]) // b
}
mixArgs('A', 'B')
复制代码
而在ES5
严格模式下,修改参数的值再也不反应到arguments
对象中,以下:
function mixArgs(first, second) {
'use strict'
console.log(arguments[0]) // A
console.log(arguments[1]) // B
first = 'a'
second = 'b'
console.log(arguments[0]) // A
console.log(arguments[1]) // B
}
mixArgs('A', 'B')
复制代码
对于使用了ES6
的形参默认值,arguments
对象的行为始终保持和ES5
严格模式同样,不管当前是否为严格模式,即:arguments
老是等于最初传递的值,不会随着参数的改变而改变,老是可使用arguments
对象将参数还原为最初的值,以下:
function mixArgs(first, second = 'B') {
console.log(arguments.length) // 1
console.log(arguments[0]) // A
console.log(arguments[1]) // undefined
first = 'a'
second = 'b'
console.log(arguments[0]) // A
console.log(arguments[1]) // undefined
}
// arguments对象始终等于传递的值,形参默认值不会反映在arguments上
mixArgs('A')
复制代码
函数形参默认值,除了能够是原始值的默认值,也能够是表达式,即:变量,函数调用也是合法的。
function getValue () {
return 5
}
function add (first, second = getValue()) {
return first + second
}
console.log(add(1, 1)) // 2
console.log(add(1)) // 6
复制代码
代码分析:当咱们第一次调用add(1,1)
函数时,因为未使用参数默认值,因此getValue
并不会调用。只有当咱们使用了second
参数默认值的时候add(1)
,getValue
函数才会被调用。
正由于默认参数是在函数调用时求值,因此咱们能够在后定义的参数表达式中使用先定义的参数,便可以把先定义的参数当作变量或者函数调用的参数,以下:
function getValue(value) {
return value + 5
}
function add (first, second = first + 1) {
return first + second
}
function reduce (first, second = getValue(first)) {
return first - second
}
console.log(add(1)) // 3
console.log(reduce(1)) // -5
复制代码
在前面已经提到过let
和const
存在暂时性死区,即:在let
和const
变量声明以前尝试访问该变量会触发错误。相同的道理,在函数默认参数中也存在暂时性死区,以下:
function add (first = second, second) {
return first + second
}
add(1, 1) // 2
add(undefined, 1) // 抛出错误
复制代码
代码分析:在第一次调用add(1,1)
时,咱们传递了两个参数,则add
函数不会使用参数默认值;在第二次调用add(undefined, 1)
时,咱们给first
参数传递了undefined
,则first
参数使用参数默认值,而此时second
变量尚未初始化,因此被抛出错误。
JavaScript
的函数语法规定:不管函数已定义的命名参数有多少个,都不限制调用时传入的实际参数的数量。在ES6
中,当传入更少的参数时,使用参数默认值来处理;当传入更多数量的参数时,使用不定参数来处理。
咱们以underscore.js
库中的pick
方法为例:
pick方法的用法是:给定一个对象,返回指定属性的对象的副本。
function pick(object) {
let result = Object.create(null)
for (let i = 1, len = arguments.length; i < len; i++) {
let item = arguments[i]
result[item] = object[item]
}
return result
}
const book = {
title: '深刻理解ES6',
author: '尼古拉斯',
year: 2016
}
console.log(pick(book, 'title', 'author')) // { title: '深刻理解ES6', author: '尼古拉斯' }
复制代码
代码分析:
在ES6
中提供了不定参数,咱们可使用不定参数的特性来重写pick
函数:
function pick(object, ...keys) {
let result = Object.create(null)
for (let i = 0, len = keys.length; i < len; i++) {
let item = keys[i]
result[item] = object[item]
}
return result
}
const book = {
title: '深刻理解ES6',
author: '尼古拉斯',
year: 2016
}
console.log(pick(book, 'title', 'author')) // { title: '深刻理解ES6', author: '尼古拉斯' }
复制代码
不定参数在使用的过程当中有几点限制:
setter
之中使用不定参数。// 报错,只能有一个不定参数
function add(first, ...rest1, ...rest2) {
console.log(arguments)
}
// 报错,不定参数只能放在最后一个参数
function add(first, ...rest, three) {
console.log(arguments)
}
// 报错,不定参数不能用在对象字面量`setter`之中
const object = {
set name (...val) {
console.log(val)
}
}
复制代码
在ES6
的新功能中,展开运算符和不定参数是最为类似的,不定参数可让咱们指定多个各自独立的参数,并经过整合后的数组来访问;而展开运算符可让你指定一个数组,将它们打散后做为各自独立的参数传入函数。
在ES6
以前,咱们若是使用Math.max
函数比较一个数组中的最大值,则须要像下面这样使用:
const arr = [4, 10, 5, 6, 32]
console.log(Math.max.apply(Math, arr)) // 32
复制代码
代码分析:在ES6
以前使用这种方式是没有任何问题的,但关键的地方在于咱们要借用apply
方法,并且要特别当心的处理this
(第一个参数),在ES6
中咱们有更加简单的方式来达到以上的目的:
const arr = [4, 10, 5, 6, 32]
console.log(Math.max(...arr)) // 32
复制代码
问:为何ES6
会引入函数的name
属性。
答:在JavaScript
中有多重定义函数的方式,于是辨别函数就是一项具备挑战性的任务,此外匿名函数表达式的普遍使用也加大了调试的难度,为了解决这些问题,在ESCAScript 6
中为全部函数新增了name
属性。
在函数声明和匿名函数表达式中,函数的name
属性相对来讲是固定的:
function doSomething () {
console.log('do something')
}
let doAnotherThing = function () {
console.log('do another thing')
}
console.log(doSomething.name) // doSomething
console.log(doAnotherThing.name) // doAnotherThing
复制代码
尽管肯定函数声明和函数表达式的名称很容易,但仍是有一些其余状况不是特别容易识别:
getter
和setter
:在对象上存在get + 属性
的get
或者set
方法。bind
:经过bind
函数建立的函数,name
为会带有bound
前缀anonymous
。let doSomething = function doSomethingElse () {
console.log('do something else')
}
let person = {
// person对象上存在name为get firstName的方法
get firstName () {
return 'why'
},
sayName: function () {
console.log('why')
},
sayAge: function sayNewAge () {
console.log(23)
}
}
console.log(doSomething.name) // doSomethingElse
console.log(person.sayName.name) // sayName
console.log(person.sayAge.name) // sayNewAge
console.log(doSomething.bind().name) // bound doSomethingElse
console.log(new Function().name) // anonymous
复制代码
在JavaScript
中函数具备多重功能,能够结合new
使用,函数内的this
值指向一个新对象,函数最终会返回这个新对象,以下:
function Person (name) {
this.name = name
}
const person = new Person('why')
console.log(person.toString()) // [object Object]
复制代码
在ES6
中,函数有两个不一样的内部方法,分别是:
具备[[Construct]]方法的函数被称为构造函数,但并非全部的函数都有[[Construct]]方法,例如:箭头函数。
[[Call]]
:若是不经过new
关键字进行调用函数,则执行[[Call]]
函数,从而直接执行代码中的函数体。[[Construct]]
:当经过new
关键字调用函数时,执行的是[[Construct]]
函数,它负责建立一个新对象,而后再执行函数体,将this
绑定到实例上。在ES6
以前,若是要判断一个函数是否经过new
关键词调用,最流行的方法是使用instanceof
来判断,例如:
function Person (name) {
if (this instanceof Person) {
this.name = name
} else {
throw new Error('必须经过new关键词来调用Person')
}
}
const person = new Person('why')
const notPerson = Person('why') // 抛出错误
复制代码
代码分析:这段代码中,首先会判断this
的值,看是不是Person
的实例,若是是则继续执行,若是不是则抛出错误。一般来讲这种作法是正确的,可是也不是十分靠谱,有一种方式能够不依赖new
关键词也能够把this
绑定到Person
的实例上,以下:
function Person (name) {
if (this instanceof Person) {
this.name = name
} else {
throw new Error('必须经过new关键词来调用Person')
}
}
const person = new Person('why')
const notPerson = Person.call(person, 'why') // 不报错,有效
复制代码
为了解决判断函数是否经过new
关键词调用的问题,ES6
引入了new.target
这个元属性
问:什么是元属性?
答:元属性是指非对象的属性,其能够提供非对象目标的补充信息。当调用函数的[[Construct]]
方法时,new.target
被赋值为new
操做符的目标,一般是新建立对象的实例,也就是函数体内this
的构造函数;若是调用[[Call]]
方法,则new.target
的值为undefined
。
根据以上new.target
的特色,咱们改写一下上面的代码:
在函数外使用new.target是一个语法错误。
function Person (name) {
if (typeof new.target !== 'undefined') {
this.name = name
} else {
throw new Error('必须经过new关键词来调用Person')
}
}
const person = new Person('why')
const notPerson = Person.call(person, 'why') // 抛出错误
复制代码
在ECMAScript 3
和早期版本中,在代码块中声明一个块级函数严格来讲是一个语法错误,可是全部的浏览器任然支持这个特性,却又由于浏览器的差别致使支撑程度稍有不一样,因此最好不要使用这个特性,若是要用可使用匿名函数表达式。
// ES5严格模式下,在代码块中声明一个函数会报错
// 在ES6下,由于有了块级做用域的概念,因此不管是否处于严格模式,都不会报错。
// 但在ES6中,当处于严格模式时,会将函数声明提高至当前块级做用域的顶部
// 当处于非严格模式时,提高至外层做用域
'use strict'
if (true) {
function doSomething () {
console.log('do something')
}
}
复制代码
在ES6
中,箭头函数是其中最有趣的新增特性之一,箭头函数是一种使用箭头=>
定义函数的新语法,但它和传统的JavaScript
函数有些许不一样:
this
、super
、arguments
和new.target
这些值由外围最近一层非箭头函数所决定。[[Construct]]
函数,因此不能经过new
关键词进行调用,若是使用new
进行调用会抛出错误。new
关键词进行调用,因此没有构建原型的须要,也就没有了prototype
这个属性。this
的值不可改变(即不能经过call
、apply
或者bind
等方法来改变)。arguments
绑定,因此必须使用命名参数或者不定参数这两种形式访问参数。箭头函数的语法多变,根据实际的使用场景有多种形式。全部变种都由函数参数、箭头和函数体组成。
表现形式之一:
// 表现形式之一:没有参数
let reflect = () => 5
// 至关于
let reflect = function () {
return 5
}
复制代码
表现形式之二:
// 表现形式之二:返回单一值
let reflect = value => value
// 至关于
let reflect = function (value) {
return value
}
复制代码
表现形式之三:
// 表现形式之三:多个参数
let reflect = (val1, val2) => val1 + val2
// 或者
let reflect = (val, val2) => {
return val1 + val2
}
// 至关于
let reflect = function (val1, val2) {
return val1 + val2
}
复制代码
表现形式之四:
// 表现形式之四:返回字面量
let reflect = (id) => ({ id: id, name: 'why' })
// 至关于
let reflect = function (id) {
return {
id: id,
name: 'why'
}
}
复制代码
箭头函数的语法简洁,很是适用于处理数组。
const arr = [1, 5, 3, 2]
// 非箭头函数排序写法
arr.sort(function(a, b) {
return a -b
})
// 箭头函数排序写法
arr.sort((a, b) => a - b)
复制代码
尾调用指的是函数做为另外一个函数的最后一条语句被调用。
尾调用示例:
function doSomethingElse () {
console.log('do something else')
}
function doSomething () {
return doSomethingElse()
}
复制代码
在ECMAScript 5
的引擎中,尾调用的实现与其余函数调用的实现相似:建立一个新的栈帧,将其推入调用栈来表示函数调用,即意味着:在循环调用中,每个未使用完的栈帧都会被保存在内存中,当调用栈变得过大时会形成程序问题。
针对以上可能会出现的问题,ES6
缩减了严格模式下尾调用栈的大小,当所有知足如下条件,尾调用再也不建立新的栈帧,而是清除并重用当前栈帧:
知足以上条件的一个尾调用示例:
'use strict'
function doSomethingElse () {
console.log('do something else')
}
function doSomething () {
return doSomethingElse()
}
复制代码
不知足以上条件的尾调用示例:
function doSomethingElse () {
console.log('do something else')
}
function doSomething () {
// 没法优化,没有返回
doSomethingElse()
}
function doSomething () {
// 没法优化,返回值又添加了其它操做
return 1 + doSomethingElse()
}
function doSomething () {
// 可能没法优化
let result = doSomethingElse
return result
}
function doSomething () {
let number = 1
let func = () => number
// 没法优化,该函数是一个闭包
return func()
}
复制代码
递归函数是其最主要的应用场景,当递归函数的计算量足够大,尾调用优化能够大幅提高程序的性能。
// 优化前
function factorial (n) {
if (n <= 1) {
return 1
} else {
// 没法优化
return n * factorial (n - 1)
}
}
// 优化后
function factorial (n, p = 1) {
if (n <= 1) {
return 1 * p
} else {
let result = n * p
return factorial(n -1, result)
}
}
复制代码
对象字面量扩展包含两部分:
function
关键字。经过对象方法简写语法建立的方法有一个name
属性,其值为小括号前的名称。
const name = 'why'
const firstName = 'first name'
const person = {
name,
[firstName]: 'ABC',
sayName () {
console.log(this.name)
}
}
// 至关于
const name = 'why'
const person = {
name: name,
'first name': 'ABC',
sayName: function () {
console.log(this.name)
}
}
复制代码
在使用JavaScript
比较两个值的时候,咱们可能会习惯使用==
或者===
来进行判断,使用全等===
在比较时能够避免触发强制类型转换,因此深受许多人的喜好。但全等===
也并不是是彻底准确的,例如: +0===-0
会返回true
,NaN===NaN
会返回false
。针对以上状况,ES6
引入了Object.is
方法来弥补。
// ===和Object.is大多数状况下结果是相同的,只有极少数结果不一样
console.log(+0 === -0) // true
console.log(Object.is(+0, -0)) // false
console.log(NaN === NaN) // false
console.log(Object.is(NaN, NaN)) // true
复制代码
问:什么是Mixin
?
答:混合Mixin
是JavaScript
中实现对象组合最流行的一种模式。在一个mixin
中,一个对象接受来自另外一个对象的属性和方法(mixin
方法为浅拷贝)。
// mixin方法
function mixin(receiver, supplier) {
Object.keys(supplier).forEach(function(key) {
receiver[key] = supplier[key]
})
return receiver
}
const person1 = {
age: 23,
name: 'why'
}
const person2 = mixin({}, person1)
console.log(person2) // { age: 23, name: 'why' }
复制代码
因为这种混合模式很是流行,因此ES6
引入了Object.assign
方法来实现相同的功能,这个方法接受一个接受对象和任意数量的源对象,最终返回接受对象。
若是源对象中有同名的属性,后面的源对象会覆盖前面源对象中的同名属性。
const person1 = {
age: 23,
name: 'why'
}
const person2 = {
age: 32,
address: '广东广州'
}
const person3 = Object.assign({}, person1, person2)
console.log(person3) // { age: 32, name: 'why', address: '广东广州' }
复制代码
Object.assign方法不能复制属性的get和set。
let receiver = {}
let supplier = {
get name () {
return 'why'
}
}
Object.assign(receiver, supplier)
const descriptor = Object.getOwnPropertyDescriptor(receiver, 'name')
console.log(descriptor.value) // why
console.log(descriptor.get) // undefined
console.log(receiver) // { name: 'why' }
复制代码
在ECMAScript 5
严格模式下,给一个对象添加剧复的属性会触发错误:
'use strict'
const person = {
name: 'AAA',
name: 'BBB' // ES5环境触发错误
}
复制代码
但在ECMAScript 6
中,不管当前是否处于严格模式,添加剧复的属性都不会报错,而是选取最后一个取值:
'use strict'
const person = {
name: 'AAA',
name: 'BBB' // ES6环境不报错
}
console.log(person) // { name: 'BBB' }
复制代码
ES5
中未定义对象属性的枚举顺序,由浏览器厂商自行决定。而在ES6
中严格规定了对象自有属性被枚举时的返回顺序。
规则:
Symbol
键按照它们被加入对象的顺序排序。根据以上规则,如下这些方法将受到影响:
Object.getOwnPropertyNames()
。Reflect.keys()
。Object.assign()
。不肯定的状况:
for-in
循环依旧由厂商决定枚举顺序。Object.keys()
和JSON.stringify()
也同for-in
循环同样由厂商决定枚举顺序。const obj = {
a: 1,
0: 1,
c: 1,
2: 1,
b: 1,
1: 1
}
obj.d = 1
console.log(Reflect.keys(obj).join('')) // 012acbd
复制代码
ES5
中,对象原型一旦实例化以后保持不变。而在ES6
中添加了Object.setPrototypeOf()
方法来改变这种状况。
const person = {
sayHello () {
return 'Hello'
}
}
const dog = {
sayHello () {
return 'wang wang wang'
}
}
let friend = Object.create(person)
console.log(friend.sayHello()) // Hello
console.log(Object.getPrototypeOf(friend) === person) // true
Object.setPrototypeOf(friend, dog)
console.log(friend.sayHello()) // wang wang wang
console.log(Object.getPrototypeOf(friend) === dog) // true
复制代码
在ES5
中,若是咱们想重写对象实例的方法,又须要调用与它同名的原型方法,能够像下面这样:
const person = {
sayHello () {
return 'Hello'
}
}
const dog = {
sayHello () {
return 'wang wang wang'
}
}
const friend = {
sayHello () {
return Object.getPrototypeOf(this).sayHello.call(this) + '!!!'
}
}
Object.setPrototypeOf(friend, person)
console.log(friend.sayHello()) // Hello!!!
console.log(Object.getPrototypeOf(friend) === person) // true
Object.setPrototypeOf(friend, dog)
console.log(friend.sayHello()) // wang wang wang!!!
console.log(Object.getPrototypeOf(friend) === dog) // true
复制代码
代码分析:要准确记住如何使用Object.getPrototypeOf()
和xx.call(this)
方法来调用原型上的方法实在是有点复杂。并且存在多继承的状况下,Object.getPrototypeOf()
会出现问题。
根据以上问题,ES6
引入了super
关键字,其中super
至关于指向对象原型的指针,因此以上代码能够修改以下:
super关键字只出如今对象简写方法里,普通方法中使用会报错。
const person = {
sayHello () {
return 'Hello'
}
}
const dog = {
sayHello () {
return 'wang wang wang'
}
}
const friend = {
sayHello () {
return super.sayHello.call(this) + '!!!'
}
}
Object.setPrototypeOf(friend, person)
console.log(friend.sayHello()) // Hello!!!
console.log(Object.getPrototypeOf(friend) === person) // true
Object.setPrototypeOf(friend, dog)
console.log(friend.sayHello()) // wang wang wang!!!
console.log(Object.getPrototypeOf(friend) === dog) // true
复制代码
在ES6
以前从未正式定义过"方法"的概念,方法仅仅是一个具备功能而非数据的对象属性。而在ES6
中正式将方法定义为一个函数,它会有一个内部[[HomeObject]]
属性来容纳这个方法从属的对象。
const person = {
// 是方法 [[HomeObject]] = person
sayHello () {
return 'Hello'
}
}
// 不是方法
function sayBye () {
return 'goodbye'
}
复制代码
根据以上[[HomeObject]]
的规则,咱们能够得出super
是如何工做的:
[[HomeObject]]
属性上调用Object.getPrototypeOf()
方法来检索原型的引用。this
绑定而且调用相应的方法。const person = {
sayHello () {
return 'Hello'
}
}
const friend = {
sayHello () {
return super.sayHello() + '!!!'
}
}
Object.setPrototypeOf(friend, person)
console.log(friend.sayHello()) // Hello!!!
复制代码
代码分析:
friend.sayHello()
方法的[[HomeObject]]
属性值为friend
。friend
的原型是person
。super.sayHello()
至关于person.sayHello.call(this)
。解构是一种打破数据结构,将其拆分为更小部分的过程。
在ECMAScript 5
及其早期版本中,为了从对象或者数组中获取特定数据并赋值给变量,编写了许多看起来同质化的代码:
const person = {
name: 'AAA',
age: 23
}
const name = person.name
const age = person.age
复制代码
代码分析:咱们必须从person
对象中提取name
和age
的值,并把其值赋值给对应的同名变量,过程极其类似。假设咱们要提取许多变量,这种过程会重复更屡次,若是其中还包含嵌套结构,只靠遍历是找不到真实信息的。
针对以上问题,ES6
引入了解构的概念,按场景可分为:
咱们使用ES6
中的对象结构,改写以上示例:
const person = {
name: 'AAA',
age: 23
}
const { name, age } = person
console.log(name) // AAA
console.log(age) // 23
复制代码
必须为解构赋值提供初始化程序,同时若是解构右侧为null或者undefined,解构会发生错误。
// 如下代码为错误示例,会报错
var { name, age }
let { name, age }
const { name, age }
const { name, age } = null
const { name, age } = undefined
复制代码
咱们不只能够在解构时从新定义变量,还能够解构赋值已存在的变量:
const person = {
name: 'AAA',
age: 23
}
let name, age
// 必须添加(),由于若是不加,{}表明是一个代码块,而语法规定代码块不能出如今赋值语句的左侧。
({ name, age } = person)
console.log(name) // AAA
console.log(age) // 23
复制代码
使用解构赋值表达式时,若是指定的局部变量名称在对象中不存在,那么这个局部变量会被赋值为undefined
,此时能够随意指定一个默认值。
const person = {
name: 'AAA',
age: 23
}
let { name, age, sex = '男' } = person
console.log(sex) // 男
复制代码
目前为止咱们解构赋值时,待解构的键和待赋值的变量是同名的,但如何为非同名变量解构赋值呢?
const person = {
name: 'AAA',
age: 23
}
let { name, age } = person
// 至关于
let { name: name, age: age } = person
复制代码
let { name: name, age: age } = person
含义是:在person
对象中取键为name
和age
的值,并分别赋值给name
变量和age
变量。
那么,咱们根据以上的思路,为非同名变量赋值能够改写成以下形式:
const person = {
name: 'AAA',
age: 23
}
let { name: newName, age: newAge } = person
console.log(newName) // AAA
console.log(newAge) // 23
复制代码
解构嵌套对象任然与对象字面量语法类似,只是咱们能够将对象拆解成咱们想要的样子。
const person = {
name: 'AAA',
age: 23,
job: {
name: 'FE',
salary: 1000
},
department: {
group: {
number: 1000,
isMain: true
}
}
}
let { job, department: { group } } = person
console.log(job) // { name: 'FE', salary: 1000 }
console.log(group) // { number: 1000, isMain: true }
复制代码
let { job, department: { group } } = person
含义是:在person
中提取键为job
、在person
的嵌套对象department
中提取键为group
的值,并把其赋值给对应的变量。
数组的解构赋值与对象解构的语法类似,但简单许多,它使用的是数组字面量,且解构操做所有在数组内完成,解构的过程是按值在数组中的位置进行提取的。
const colors = ['red', 'green', 'blue']
let [firstColor, secondColor] = colors
// 按需解构
let [,,threeColor] = colors
console.log(firstColor) // red
console.log(secondColor) // green
console.log(threeColor) // blue
复制代码
与对象同样,解构数组也能解构赋值给已经存在的变量,只是能够不须要像对象同样额外的添加括号:
const colors = ['red', 'green', 'blue']
let firstColor, secondColor
[firstColor, secondColor] = colors
console.log(firstColor) // red
console.log(secondColor) // green
复制代码
按以上原理,咱们能够轻松扩展一下解构赋值的功能(快速交换两个变量的值):
let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(a); // 2
console.log(b); // 1
复制代码
与对象同样,数组解构也能够设置解构默认值:
const colors = ['red']
const [firstColor, secondColor = 'green'] = colors
console.log(firstColor) // red
console.log(secondColor) // green
复制代码
当存在嵌套数组时,咱们也可使用和解构嵌套对象的思路来解决:
const colors = ['red', ['green', 'lightgreen'], 'blue']
const [firstColor, [secondColor]] = colors
console.log(firstColor) // red
console.log(secondColor) // green
复制代码
在解构数组时,不定元素只能放在最后一个,在后面继续添加逗号会致使报错。
在数组解构中,有一个和函数的不定参数类似的功能:在解构数组时,可使用...
语法将数组中剩余元素赋值给一个特定的变量:
let colors = ['red', 'green', 'blue']
let [firstColor, ...restColors] = colors
console.log(firstColor) // red
console.log(restColors) // ['green', 'blue']
复制代码
根据以上解构数组中的不定元素的原理,咱们能够实现同concat
同样的数组复制功能:
const colors = ['red', 'green', 'blue']
const concatColors = colors.concat()
const [...restColors] = colors
console.log(concatColors) // ['red', 'green', 'blue']
console.log(restColors) // ['red', 'green', 'blue']
复制代码
当咱们定一个须要接受大量参数的函数时,一般咱们会建立能够可选的对象,将额外的参数定义为这个对象的属性:
function setCookie (name, value, options) {
options = options || {}
let path = options.path,
domain = options.domain,
expires = options.expires
// 其它代码
}
// 使用解构参数
function setCookie (name, value, { path, domain, expires } = {}) {
// 其它代码
}
复制代码
代码分析:{ path, domain, expires } = {}
必须提供一个默认值,若是不提供默认值,则不传递第三个参数会报错:
function setCookie (name, value, { path, domain, expires }) {
// 其它代码
}
// 报错
setCookie('type', 'js')
// 至关于解构了undefined,因此会报错
{ path, domain, expires } = undefined
复制代码
在ES6
以前,JavaScript
语言只有五种原始类型:string
、number
、boolean
、null
和undefiend
。在ES6
中,添加了第六种原始类型:Symbol
。
可使用typeof
来检测Symbol
类型:
const symbol = Symbol('Symbol Test')
console.log(typeof symbol) // symbol
复制代码
能够经过全局的Symbol
函数来建立一个Symbol
。
const firstName = Symbol()
const person = {}
person[firstName] = 'AAA'
console.log(person[firstName]) // AAA
复制代码
能够在Symbol()
中传递一个可选的参数,可让咱们添加一段文本描述咱们建立的Symbol
,其中文本是存储在内部属性[[Description]]
中,只有当调用Symbol
的toString()
方法时才能够读取这个属性。
const firstName = Symbol('Symbol Description')
const person = {}
person[firstName] = 'AAA'
console.log(person[firstName]) // AAA
console.log(firstName) // Symbol('Symbol Description')
复制代码
全部可使用可计算属性名的地方,均可以使用Symbol
。
let firstName = Symbol('first name')
let lastName = Symbol('last name')
const person = {
[firstName]: 'AAA'
}
Object.defineProperty(person, firstName, {
writable: false
})
Object.defineProperties(person, {
[lastName]: {
value: 'BBB',
writable: false
}
})
console.log(person[firstName]) // AAA
console.log(person[lastName]) // BBB
复制代码
ES6
提供了一个能够随时访问的全局Symbol
注册表来让咱们能够建立共享Symbol
的能力,可使用Symbol.for()
方法来建立一个共享的Symbol
。
// Symbol.for方法的参数,也被用作Symbol的描述内容
const uid = Symbol.for('uid')
const object = {
[uid]: 12345
}
console.log(person[uid]) // 12345
console.log(uid) // Symbol(uid)
复制代码
代码分析:
Symbol.for()
方法首先会在全局Symbol
注册变中搜索键为uid
的Symbol
是否存在。Symbol
。Symbol
,并使用这个键在Symbol
全局注册变中注册,随后返回新建立的Symbol
。还有一个和Symbol
共享有关的特性,可使用Symbol.keyFor()
方法在Symbol
全局注册表中检索与Symbol
有关的键,若是存在则返回,不存在则返回undefined
:
const uid = Symbol.for('uid')
const uid1 = Symbol('uid1')
console.log(Symbol.keyFor(uid)) // uid
console.log(Symbol.keyFor(uid1)) // undefined
复制代码
其它原始类型没有与Symbol
逻辑相等的值,尤为是不能将Symbol
强制转换为字符串和数字。
const uid = Symbol.for('uid')
console.log(uid)
console.log(String(uid))
// 报错
uid = uid + ''
uid = uid / 1
复制代码
代码分析:咱们使用console.log()
方法打印Symbol
,会调用Symbol
的String()
方法,所以也能够直接调用String()
方法输出Symbol
。然而尝试将Symbol
和一个字符串拼接,会致使程序抛出异常,Symbol
也不能和每个数学运算符混合使用,不然一样会抛出错误。
Object.keys()
和Object.getOwnPropertyNames()
方法能够检索对象中全部的属性名,其中Object.keys
返回全部能够枚举的属性,Object.getOwnPropertyNames()
不管属性是否能够枚举都返回,可是这两个方法都没法返回Symbol
属性。所以ES6
引入了一个新的方法Object.getOwnPropertySymbols()
方法。
const uid = Symbol.for('uid')
let object = {
[uid]: 123
}
const symbols = Object.getOwnPropertySymbols(object)
console.log(symbols.length) // 1
console.log(symbols[0]) // Symbol(uid)
复制代码
ES6
经过在原型链上定义与Symbol
相关的属性来暴露更多的语言内部逻辑,这些内部操做以下:
Symbol.hasInstance
:一个在执行instanceof
时调用的内部方法,用于检测对象的继承信息。Symbol.isConcatSpreadable
:一个布尔值,用于表示当传递一个集合做为Array.prototype.concat()
方法的参数时,是否应该将集合内的元素规整到同一层级。Symbol.iterator
:一个返回迭代器的方法。Symbol.match
:一个在调用String.prototype.match()
方法时调用的方法,用于比较字符串。Symbol.replace
:一个在调用String.prototype.replace()
方法时调用的方法,用于替换字符串中的子串。Symbol.search
:一个在调用String,prototype.search()
方法时调用的方法,用于在字符串中定位子串。Symbol.split
:一个在调用String.prototype.split()
方法时调用的方法,用于分割字符串。Symbol.species
:用于建立派生对象的构造函数。Symbol.toPrimitive
:一个返回对象原始值的方法。Symbol.toStringTag
:一个在调用Object.prototype.toString()
方法时使用的字符串,用于建立对象描述。Symbol.unscopables
:一个定义了一些不可被with
语句引用的对象属性名称的对象集合。重写一个由well-known Symbol
定义的方法,会致使对象内部的默认行为被改变,从而一个普通对象会变为一个奇异对象。
每个函数都有Symbol.hasInstance
方法,用于肯定对象是否为函数的实例,而且该方法不可被枚举、不可被写和不可被配置。
function MyObject () {
// 空函数
}
Object.defineProperty(MyObject, Symbol.hasInstance, {
value: function () {
return false
}
})
let obj = new MyObject()
console.log(obj instanceof MyObject) // false
复制代码
代码分析:使用Object.defineProperty
方法,在MyObject
函数上改写Symbol.hasInstance
,为其定义一个老是返回false
的新函数,即便obj
确实是MyObject
的实例,但依然在进行instanceof
判断时返回了false
。
注意若是要触发Symbol.hasInstance调用,instanceof的左操做符必须是一个对象,若是为非对象则会致使instanceof始终返回false。
在JavaScript
数组中concat()
方法被用于拼接两个数组:
const colors1 = ['red', 'green']
const colors2 = ['blue']
console.log(colors1.concat(colors2, 'brown')) // ['red', 'green', 'blue', 'brown']
复制代码
在concat()
方法中,咱们传递了第二个参数,它是一个非数组元素。若是Symbol.isConcatSpreadable
为true
,那么表示对象有length
属性和数字键,故它的数值型键会被独立添加到concat
调用的结果中,它是对象的可选属性,用于加强做用于特定对象类型的concat
方法的功能,有效简化其默认特性:
const obj = {
0: 'hello',
1: 'world',
length: 2,
[Symbol.isConcatSpreadable]: true
}
const message = ['Hi'].concat(obj)
console.log(message) // ['Hi', 'hello', 'world']
复制代码
在JavaScript
中,字符串与正则表达式常常一块儿出现,尤为是字符串类型的几个方法,能够接受正则表达式做为参数:
match
:肯定给定字符串是否匹配正则表达式。replace
:将字符串中匹配正则表达式的部分替换为给定的字符串。search
:在字符串中定位匹配正则表示位置的索引。split
:按照匹配正则表达式的元素将字符串进行分割,并将分割结果存入数组中。在ES6
以前,以上几个方法没法使用咱们本身定义的对象来替代正则表达式进行字符串匹配,而在ES6
以后,引入了与上述几个方法相对应Symbol
,将语言内建的Regex
对象的原生特性彻底外包出来。
const hasLengthOf10 = {
[Symbol.match] (value) {
return value.length === 10 ? [value] : null
},
[Symbol.replace] (value, replacement) {
return value.length === 10 ? replacement : value
},
[Symbol.search] (value) {
return value.length === 10 ? 0 : -1
},
[Symbol.split] (value) {
return value.length === 10 ? [,] : [value]
}
}
const message1 = 'Hello world'
const message2 = 'Hello John'
const match1 = message1.match(hasLengthOf10)
const match2 = message2.match(hasLengthOf10)
const replace1 = message1.replace(hasLengthOf10)
const replace2 = message2.replace(hasLengthOf10, 'AAA')
const search1 = message1.search(hasLengthOf10)
const search2 = message2.search(hasLengthOf10)
const split1 = message1.split(hasLengthOf10)
const split2 = message2.split(hasLengthOf10)
console.log(match1) // null
console.log(match2) // [Hello John]
console.log(replace1) // Hello world
console.log(replace2) // AAA
console.log(search1) // -1
console.log(search2) // 0
console.log(split1) // [Hello John]
console.log(split2) // [,]
复制代码
Symbol.toPrimitive
方法被定义在每个标准类型的原型上,而且规定了当对象被转换为原始值时应该执行的操做,每当执行原始值转换时,总会调用Symbol.toPrimitive
方法并传入一个值做为参数。
对于大多数标准对象,数字模式有如下特性,根据优先级的顺序排序以下:
valueOf()
方法,若是结果为原始值,则返回。toString()
方法,若是结果为原始值,则返回。一样对于大多数标准对象,字符串模式有如下有限级顺序:
toString()
方法,若是结果为原始值,则返回。valueOf()
方法,若是结果为原始值,则返回。在大多数状况下,标准对象会将默认模式按数字模式处理(除Date
对象,在这种状况下,会将默认模式按字符串模式处理),若是自定义了Symbol.toPrimitive
方法,则能够覆盖这些默认的强制转换行为。
function Temperature (degress) {
this.degress = degress
}
Temperature.prototype[Symbol.toPrimitive] = function (hint) {
switch (hint) {
case 'string':
return this.degress + '℃'
case 'number':
return this.degress
case 'default':
return this.deress + ' degress'
}
}
const freezing = new Temperature(32)
console.log(freezing + '') // 32 degress
console.log(freezing / 2) // 16
console.log(String(freezing)) // 32℃
复制代码
代码分析:咱们在对象Temperature
原型上重写了Symbol.toPrimitive
,新方法根据参数hint
指定的模式返回不一样的值,其中hint
参数由JavaScript
引擎传入。其中+
运算符触发默认模式,hint
被设置为default
;/
运算符触发数字模式,hint
被设置为number
;String()
函数触发字符串模式,hint
被设置为string
。
在JavaScript
中,若是咱们同时存在多个全局执行环境,例如在浏览器中一个页面包含iframe
标签,由于iframe
和它外层的页面分别表明不一样的领域,每个领域都有本身的全局做用域,有本身的全局对象,在任何领域中建立的数组,都是一个正规的数组。然而,若是将这个数字传递到另一个领域中,instanceof Array
语句的检测结果会返回false
,此时Array
已是另外一个领域的构造函数,显然被检测的数组不是由这个构造函数建立的。
针对以上问题,咱们很快找到了一个相对来讲比较实用的解决方案:
function isArray(value) {
return Object.prototype.toString.call(value) === '[object Array]'
}
console.log(isArray([])) // true
复制代码
与上述问题有一个相似的案例,在ES5
以前咱们可能会引入第三方库来建立全局的JSON
对象,而在浏览器开始实现JSON
全局对象后,就有必要区分JSON
对象是JavaScript
环境自己提供的仍是由第三方库提供的:
function supportsNativeJSON () {
return typeof JSON !== 'undefined' && Object.prototype.toString.call(JSON) === '[object JSON]'
}
复制代码
在ES6
中,经过Symbol.toStringTag
这个Symbol
改变了调用Object.prototype.toString()
时返回的身份标识,其定义了调用对象的Object.prototype.toString.call()
方法时返回的值:
function Person (name) {
this.name = name
}
Person.prototype[Symbol.toStringTag] = 'Person'
const person = new Person('AAA')
console.log(person.toString()) // [object Person]
console.log(Object.prototype.toString.call(person)) // [object Person]
复制代码
Set
集合是一种无重复元素的列表,一般用来检测给定的值是否在某个集合中;Map
集合内含多组键值对,集合中每一个元素分别存放着可访问的键名和它对应的值,Map
集合常常被用来缓存频繁取用的数据。
在ES6
尚未正式引入Set
集合和Map
集合以前,开发者们已经开始使用对象属性来模拟这两种集合了:
const set = Object.create(null)
const map = Object.create(null)
set.foo = true
map.bar = 'bar'
// set检查
if (set.foo) {
console.log('存在')
}
// map取值
console.log(map.bar) // bar
复制代码
以上程序很简单,确实可使用对象属性来模拟Set
集合和Map
集合,但却在实际使用的过程当中有诸多的不方便:
const map = Object.create(null)
map[5] = 'foo'
// 本意是使用数字5做为键名,但被自动转换为了字符串
console.log(map['5']) // foo
复制代码
const map = Object.create(null)
const key1 = {}
const key2 = {}
map[key1] = 'foo'
// 本意是使用key1对象做为属性名,但却被自动转换为[object Object]
// 所以map[key1] = map[key2] = map['[object Object]']
console.log(map[key2]) // foo
复制代码
const map = Object.create(null)
map.count = 1
// 本意是检查count属性是否存在,实际检查的确是map.count属性的值是否为真
if (map.count) {
console.log(map.count)
}
复制代码
Set
集合是一种有序列表,其中含有一些相互独立的非重复值,在Set
集合中,不会对所存的值进行强制类型转换。
其中Set
集合涉及到的属性和方法有:
Set
构造函数:可使用此构造函数建立一个Set
集合。add
方法:能够向Set
集合中添加一个元素。delete
方法:能够移除Set
集合中的某一个元素。clear
方法:能够移除Set
集合中全部的元素。has
方法:判断给定的元素是否在Set
集合中。size
属性:Set
集合的长度。Set
集合的构造函数能够接受任何可迭代对象做为参数,例如:数组、Set
集合或者Map
集合。
const set = new Set()
set.add(5)
set.add('5')
// 重复添加的值会被忽略
set.add(5)
console.log(set.size) // 2
复制代码
使用delete()
方法能够移除集合中的某一个值,使用clear()
方法能够移除集合中全部的元素。
const set = new Set()
set.add(5)
set.add('5')
console.log(set.has(5)) // true
set.delete(5)
console.log(set.has(5)) // false
console.log(set.size) // 1
set.clear()
console.log(set.size) // 0
复制代码
Set
集合的forEach()
方法和数组的forEach()
方法是同样的,惟一的区别在于Set
集合在遍历时,第一和第二个参数是同样的。
const set = new Set([1, 2])
set.forEach((value, key, arr) => {
console.log(`${value} ${key}`)
console.log(arr === set)
})
// 1 1
// true
// 2 2
// true
复制代码
由于Set
集合不能够像数组那样经过索引去访问元素,最好的作法是将Set
集合转换为数组。
const set = new Set([1, 2, 3, 4])
// 方法一:展开运算符
const arr1 = [...set]
// 方法二:Array.from方法
const arr2 = Array.from(set)
console.log(arr1) // [1, 2, 3, 4]
console.log(arr2) // [1, 2, 3, 4]
复制代码
经过以上对Set
集合的梳理,咱们能够发现:只要Set
实例中的引用存在,垃圾回收机制就不能释放该对象的内存空间,因此咱们把Set
集合看做是一个强引用的集合。为了更好的处理Set
集合的垃圾回收,引入了一个叫Weak Set
的集合:
Weak Set集合只支持三种方法:add、has和delete。
const weakSet = new WeakSet()
const key = {}
weakSet.add(key)
console.log(weakSet.has(key)) // true
weakSet.delete(key)
console.log(weakSet.has(key)) // false
复制代码
Set
集合和Weak Set
集合有许多共同的特性,但它们之间仍是有必定的差异的:
Weak Set
集合只能存储对象元素,向其添加非对象元素会致使抛出错误,同理has()
和delete()
传递非对象也一样会报错。Weak Set
集合不可迭代,也不暴露任何迭代器,所以也不支持forEach()
方法。Weak Set
集合不支持size
属性。ES6
中的Map
类型是一种存储着许多键值对的有序列表,其中的键名和对应的值支持全部的数据类型,键名的等价性判断是经过调用Object.is
方法来实现的。
const map = new Map()
const key1 = {
name: 'key1'
}
const key2 = {
name: 'key2'
}
map.set(5, 5)
map.set('5', '5')
map.set(key1, key2)
console.log(map.get(5)) // 5
console.log(map.get('5')) // '5'
console.log(map.get(key1)) // {name:'key2'}
复制代码
与Set
集合相似,Map
集合也支持如下几种方法:
has
:判断指定的键名是否在Map
集合中存在。delete
:在Map
集合中移除指定键名及其对应的值。clear
:移除Map
集合中全部的键值对。const map = new Map()
map.set('name', 'AAA')
map.set('age', 23)
console.log(map.size) // 2
console.log(map.has('name')) // true
console.log(map.get('name')) // AAA
map.delete('name')
console.log(map.has('name')) // false
map.clear()
console.log(map.size) // 0
复制代码
在初始化Map
集合的时候,也能够像Set
集合传入数组,但此时数组中的每个元素都是一个子数组,子数组中包含一个键值对的键名和值两个元素。
const map = new Map([['name', 'AAA'], ['age', 23]])
console.log(map.has('name')) // true
console.log(map.has('age')) // true
console.log(map.size) // 2
console.log(map.get('name')) // AAA
console.log(map.get('age')) // 23
复制代码
Map
集合中的forEach()
方法的回调参数和数组相似,每个参数的解释以下:
Map
集合自己const map = new Map([['name', 'AAA'], ['age', 23]])
map.forEach((key, value, ownMap) => {
console.log(`${key} ${value}`)
console.log(ownMap === map)
})
// name AAA
// true
// age 23
// true
复制代码
Weak Map
它是一种存储着许多键值对的无序列表,集合中的键名必须是一个对象,若是使用非对象键名会报错。
Weak Map集合只支持set()、get()、has()和delete()。
const key1 = {}
const key2 = {}
const key3 = {}
const weakMap = new WeakMap([[key1, 'AAA'], [key2, 23]])
weakMap.set(key3, '广东')
console.log(weakMap.has(key1)) // true
console.log(weakMap.get(key1)) // AAA
weakMap.delete(key1)
console.log(weakMap.has(key)) // false
复制代码
Map
集合和Weak Map
集合有许多共同的特性,但它们之间仍是有必定的差异的:
Weak Map
集合的键名必须为对象,添加非对象会报错。Weak Map
集合不可迭代,所以不支持forEach()
方法。Weak Map
集合不支持clear
方法。Weak Map
集合不支持size
属性。咱们在平常的开发过程当中,极可能写过下面这样的代码:
var colors = ['red', 'gree', 'blue']
for(var i = 0, len = colors.length; i < len; i++) {
console.log(colors[i])
}
// red
// green
// blue
复制代码
代码分析:虽然循环语句的语法简单,可是若是将多个循环嵌套则须要追踪多个变量,代码复杂度会大大增长,一不当心就会错误使用了其它for
循环的跟踪变量,从而形成程序出错,而ES6
引入迭代器的宗旨就是消除这种复杂性并减小循环中的错误。
问:什么是迭代器?
答:迭代器是一种特殊的对象,它具备一些专门为迭代过程设计的专有接口,全部迭代器都有一个叫next
的方法,每次调用都返回一个结果对象。结果对象有两个属性,一个是value
表示下一次将要返回的值;另一个是done
,它是一个布尔类型的值,当没有更多可返回的数据时返回true
。迭代器还会保存一个内部指针,用来指向当前集合中值的位置,每调用一次next
方法,都会返回下一个可用的值。
在了解迭代器的概念后,咱们使用ES5
语法来建立一个迭代器:
function createIterator (items) {
var i = 0
return {
next: function () {
var done = i >= items.length
var value = !done ? items[i++] : undefined
return {
done: done,
value: value
}
}
}
}
var iterator = createIterator([1, 2, 3])
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next()) // { value: 2, done: false }
console.log(iterator.next()) // { value: 3, done: false }
console.log(iterator.next()) // { value: undefined, done: true }
复制代码
正如上面那样,咱们使用了ES5
语法来建立咱们本身的迭代器,它的内部实现很复杂,而ES6
除了引入了迭代器的概念还引入了一个叫生成器的概念,使用它咱们可让建立迭代器的过程更加简单一点。
问:什么是生成器?
答:生成器是一种返回迭代器的函数,经过function
关键字后的*
号来表示,函数中会用到新的关键词yield
。
function * createIterator () {
yield 1
yield 2
yield 3
}
const iterator = createIterator()
console.log(iterator.next().value) // 1
console.log(iterator.next().value) // 2
console.log(iterator.next().value) // 3
复制代码
正如咱们上面的输出结果同样,它和咱们使用ES5
语法建立的迭代器输出结果是一致的。
生成器函数最重要的一点是:每执行完一条yield
语句,函数就会自动终止:咱们在ES6
以前,函数一旦开始执行,则一直会向下执行,一直到函数return
语句都不会中断,但生成器函数却打破了这一惯例:当执行完一条yield
语句时,函数会自动中止执行,除非代码手动调用迭代器的next
方法。
咱们也能够在循环中使用生成器:
function * createIterator (items) {
for(let i = 0, len = items.length; i < len; i++) {
yield items[i]
}
}
const it = createIterator([1, 2, 3])
console.log(it.next()) // { done: false, value: 1 }
console.log(it.next()) // { done: false, value: 2 }
console.log(it.next()) // { done: false, value: 3 }
console.log(it.next()) // { done: true, value: undefined }
复制代码
yield关键字只能在生成器内部使用,在其余地方使用会致使抛出错误,即便是在生成器内部的函数中使用也是如此。
function * createIterator (items) {
items.forEach(item => {
// 抛出错误
yield item + 1
})
}
复制代码
问:可迭代对象有什么特色?
答:可迭代对象具备Symbol.iterator
属性,是一种与迭代器密切相关的对象。Symbol.iterator
经过指定的函数能够返回一个做用于附属对象的迭代器。在ES6
中,全部的集合对象(数组、Set
集合以及Map
集合)和字符串都是可迭代对象,这些对象中都有默认的迭代器。因为生成器默认会为Symbol.iterator
属性赋值,所以全部经过生成器建立的迭代器都是可迭代对象。
ES6
新引入了for-of
循环每执行一次都会调用可迭代对象的next
方法,并将迭代器返回的结果对象的value
属性存储在一个变量中,循环将持续执行这一过程直到返回对象的done
属性的值为true
。
const value = [1, 2, 3]
for (let num of value) {
console.log(num);
}
// 1
// 2
// 3
复制代码
能够经过Symbol.iterator
来访问对象的默认迭代器
const values = [1, 2, 3]
const it = values[Symbol.iterator]()
console.log(it.next()) // {done:false, value:1}
console.log(it.next()) // {done:false, value:2}
console.log(it.next()) // {done:false, value:3}
console.log(it.next()) // {done:true, value:undefined}
复制代码
因为具备Symbol.iterator
属性的对象都有默认的迭代器对象,所以能够用它来检测对象是否为可迭代对象:
function isIterator (object) {
return typeof object[Symbol.iterator] === 'function'
}
console.log(isIterator([1, 2, 3])) // true
console.log(isIterator('hello')) // true,字符串也能够迭代,原理等同于数组
console.log(isIterator(new Set())) // true
console.log(isIterator(new Map)) // true
复制代码
默认状况下,咱们本身定义的对象都是不可迭代对象,但若是给Symbol.iterator
属性添加一个生成器,则能够将其变为可迭代对象。
let collection = {
items: [1, 2, 3],
*[Symbol.iterator] () {
for (let item of this.items) {
yield item
}
}
}
for (let value of collection) {
console.log(value)
}
// 1
// 2
// 3
复制代码
在ES6
中有三种类型的集合对象:数组、Set
集合和Map
集合,它们都内建了以下三种迭代器:
entries
:返回一个迭代器,其值为多个键值对。values
:返回一个迭代器,其值为集合的值。keys
:返回一个迭代器,其值为集合中的全部键名。entries()
迭代器:
const colors = ['red', 'green', 'blue']
const set = new Set([1, 2, 3])
const map = new Map([['name', 'AAA'], ['age', 23], ['address', '广东']])
for (let item of colors.entries()) {
console.log(item)
// [0, 'red']
// [1, 'green']
// [2, 'blue']
}
for (let item of set.entries()) {
console.log(item)
// [1, 1]
// [2, 2]
// [3, 3]
}
for (let item of map.entries()) {
console.log(item)
// ['name', 'AAA']
// ['age', 23]
// ['address', '广东']
}
复制代码
values
迭代器:
const colors = ['red', 'green', 'blue']
const set = new Set([1, 2, 3])
const map = new Map([['name', 'AAA'], ['age', 23], ['address', '广东']])
for (let item of colors.values()) {
console.log(item)
// red
// green
// blue
}
for (let item of set.values()) {
console.log(item)
// 1
// 2
// 3
}
for (let item of map.values()) {
console.log(item)
// AAA
// 23
// 广东
}
复制代码
keys
迭代器:
const colors = ['red', 'green', 'blue']
const set = new Set([1, 2, 3])
const map = new Map([['name', 'AAA'], ['age', 23], ['address', '广东']])
for (let item of colors.keys()) {
console.log(item)
// 0
// 1
// 2
}
for (let item of set.keys()) {
console.log(item)
// 1
// 2
// 3
}
for (let item of map.keys()) {
console.log(item)
// name
// age
// address
}
复制代码
不一样集合类型的默认迭代器:每个集合类型都有一个默认的迭代器,在for-of
循环中,若是没有显示的指定则使用默认的迭代器:
Set
集合:默认迭代器为values
。Map
集合:默认为entries
。const colors = ['red', 'green', 'blue']
const set = new Set([1, 2, 3])
const map = new Map([['name', 'AAA'], ['age', 23], ['address', '广东']])
for (let item of colors) {
console.log(item)
// red
// green
// blue
}
for (let item of set) {
console.log(item)
// 1
// 2
// 3
}
for (let item of map) {
console.log(item)
// ['name', 'AAA']
// ['age', 23]
// ['address', '广东']
}
复制代码
解构和for-of循环:若是要在for-of
循环中使用解构语法,则能够简化编码过程:
const map = new Map([['name', 'AAA'], ['age', 23], ['address', '广东']])
for (let [key, value] of map.entries()) {
console.log(key, value)
// name AAA
// age 23
// address 广东
}
复制代码
自ES6
发布以来,JavaScript
字符串的行为慢慢变得更像数组了:
let message = 'Hello'
for(let i = 0, len = message.length; i < len; i++) {
console.log(message[i])
// H
// e
// l
// l
// o
}
复制代码
DOM
标准中有一个NodeList
类型,表明页面文档中全部元素的集合。ES6
为其添加了默认的迭代器,其行为和数组的默认迭代器一致:
let divs = document.getElementByTagNames('div')
for (let div of divs) {
console.log(div)
}
复制代码
咱们在前面的知识中已经知道,咱们可使用展开运算符把一个Set
集合转换成一个数组,像下面这样:
let set = new Set([1, 2, 3, 4])
let array = [...set]
console.log(array) // [1, 2, 3, 4]
复制代码
代码分析:在咱们所用...
展开运算符的过程当中,它操做的是Set
集合的默承认迭代对象(values
),从迭代器中读取全部值,而后按照返回顺序将他们依次插入到数组中。
const map = new Map([['name', 'AAA'], ['age', 23], ['address', '广东']])
const array = [...map]
console.log(array) // [['name', 'AAA'], ['age', 23], ['address', '广东']]
复制代码
代码分析:在咱们使用...
展开运算符的过程当中,它操做的是Map
集合的默承认迭代对象(entries
),从迭代器中读取多组键值对,依次插入数组中。
const arr1 = ['red', 'green', 'blue']
const arr2 = ['yellow', 'white', 'black']
const array = [...arr1, ...arr2]
console.log(array) // ['red', 'green', 'blue', 'yellow', 'white', 'black']
复制代码
代码分析:在使用...
展开运算符的过程当中,同Set
集合同样使用的都是其默认迭代器(values
),而后按照返回顺序依次将他们插入到数组中。
若是给迭代器next()
方法传递参数,则这个参数的值就会替代生成器内部上一条yield
语句的返回值。
function * createIterator () {
let first = yield 1
let second = yield first + 2
yield second + 3
}
let it = createIterator()
console.log(it.next(11)) // {done: false, value: 1}
console.log(it.next(4)) // {done: false, value: 6}
console.log(it.next(5)) // {done: false, value: 8}
console.log(it.next()) // {done: true, value: undefined}
复制代码
代码分析:除了第一个迭代器,其它几个迭代器咱们能很快计算出结果,但为何第一个next()
方法的参数无效呢?这是由于传给next()
方法的参数是替代上一次yield
的返回值,而在第一次调用next()
方法以前不会执行任何yield
语句,所以给第一次调用的next()
方法传递参数,不管传递任何值都将被舍弃。
除了给迭代器传递数据外,还能够给他传递错误条件,让其恢复执行时抛出一个错误。
function * createIterator () {
let first = yield 1
let second = yield first + 2
// 不会被执行
yield second + 3
}
let it = createIterator()
console.log(it.next()) // {done: false, value: 1}
console.log(it.next(4)) // {done: false, value: 6}
console.log(it.throw(new Error('break'))) // 抛出错误
复制代码
正如咱们上面看到的那样,咱们想生成器内部传递了一个错误对象,迭代器恢复执行时会抛出一个错误,咱们可使用try-catch
语句来捕获这种错误:
function * createIterator () {
let first = yield 1
let second
try {
second = yield first + 2
} catch(ex) {
second = 6
}
yield second + 3
}
let it = createIterator()
console.log(it.next()) // {done: false, value: 1}
console.log(it.next(4)) // {done: false, value: 6}
console.log(it.throw(new Error('break'))) // {done: false, value: 9}
console.log(it.next()) // {done: true, value: undefined}
复制代码
因为生成器也是函数,所以能够经过return
语句提早退出函数执行,对于最后一次next()
方法调用,能够主动为其指定一个返回值。
function * createIterator () {
yield 1
return 2
// 不会被执行
yield 3
yield 4
}
let it = createIterator()
console.log(it.next()) // {done: false, value: 1}
console.log(it.next()) // {done: false, value: 2}
console.log(it.next()) // {done: true, value: undefined}
复制代码
代码分析:在生成器中,return
语句表示全部的操做都已经完成,属性值done
会被设置成true
,若是同时提供了响应的值,则属性value
会被设置为这个值,而且return
语句以后的yield
不会被执行。
展开运算符和for-of循环会直接忽略经过return语句指定的任何返回值,由于只要done被设置为true,就当即中止读取其余的值。
const obj = {
items: [1, 2, 3, 4, 5],
*[Symbol.iterator] () {
for (let i = 0, len = this.items.length; i < len; i++) {
if (i === 3) {
return 300
} else {
yield this.items[i]
}
}
}
}
for (let value of obj) {
console.log(value)
// 1
// 2
// 3
}
console.log([...obj]) // [1, 2, 3]
复制代码
咱们能够将两个迭代器合二为一,这样就能够建立一个生成器,再给yield
语句添加一个星号,以达到将生成数据的过程委托给其余迭代器。
function * createColorIterator () {
yield ['red', 'green', 'blue']
}
function * createNumberIterator () {
yield [1, 2, 3, 4]
}
function * createCombineIterator () {
yield * createColorIterator();
yield * createNumberIterator();
}
let it = createCombineIterator()
console.log(it.next().value) // ['red', 'green', 'blue']
console.log(it.next().value) // [1, 2, 3, 4]
console.log(it.next().value) // undefined
复制代码