ES6
ECMAScript
ECMAScript通常看做JavaScript的标准化规范,实际上JavaScript是ECMAScript的扩展语言,ECMAScript只是提供了最基本的语法。
JavaScript = ECMAScript + BOM + DOM
- 2015年开始ES保持每年一个版本的迭代,并且开始按照年份命名。
- 相比于ES5.1的变化比较大
- 自此,标准命名规则发生变化
- ES6泛指是2015之后的所有新标准,以后要看清楚是特指还是泛指
新特性的分类
- 解决原有语法上的一些问题或者不足
- 对原有语法进行增强
- 全新的对象、全新的方法、全新的功能
- 全新的数据类型和数据结构
作用域
- 全局作用域
- 函数作用域
- 块状作用域
- 动态作用域
对象 | 类型 |
---|---|
global/window | 全局作用域 |
function | 函数作用域(局部作用域) |
{} | 块状作用域(if语句,for语句) |
this | 动态作用域 |
全局作用域
在全局使用var定义的变量为全局变量
案例一:
var abc = 1234
abcd = 2345
delete abc //false
console.log(abc) //1234
delete abcd //true
console.log(abcd) //abcd is not defined
// abc是一个全局对象,但是abcd不是全局变量,而是作为window对象的属性存在的,
// 但是因为window是全局对象,所以看上去它也具备全局属性,拥有全局作用域
案例二:
function test(){
ab = 45
}
test()
console.log(ab) //45
//在函数内部没有使用var定义的变量,都挂载在window上,不是全局变量,但是拥有全局作用域
函数作用域
在函数内部定义的变量,拥有函数作用域/局部作用域
function test(){
var a = 3
return a + 4
}
console.log(test()) // 7
console.log(a) //a is not defined
如何让a在函数作用域中,但是某些值共享?
- return
- 闭包
块级作用域(ES6新增)
ES5
// ES5
function test(){
var a = 3
if (a === 3) {
var b = 4
console.log('abc')
} else {
console.log('abcd')
}
console.log(b) // 4
return a + 4
}
//在if的块中无法形成壁垒,在{}中定义的变量在外界还是可以使用的
//ES6将{}中的东西进行了独立
function test () {
var a = 3
function test2 () {
var b = 4
return a + b
}
return test2
}
/* test里面的变量对test2是共享的,a的值是可以取到的。
根据函数的作用域链:
执行test2中的函数,首先定义b,然后return中找a,没有找到就去上一个函数中找,找到了a
如果在test中找不到a,最后会一直找到window
*/
ES6
function test(){
var a = 3
if (a === 3) {
let b = 4
console.log('abc')
} else {
console.log('abcd')
}
console.log(b) // b is not defined
return a + 4
}
// 如果想要使用块状作用域,但是此时不能用var,因为var有一个变量提升机制。
// 但凡看到了var,就会提升到当前作用域最顶层,所以只能使用let,const
动态作用域
this 是非常特殊的关键词标识符,在每个函数的作用域中被自动创建。
只能在执行阶段才能决定变量的作用域。
window.a = 3
function test () {
console.log(this.a)
}
test() // 3
test.bind({ a:100 })() // 100
// 因为this是一个动态指向,不是固定指向。所以我们称这个为动态作用域。
// bind是让函数动态绑定到一个对象上去,这个时候this指向对象本身,所以会导致同一个函数有不同的效果。
词法作用域
- js采用词法(静态)作用域,因此开启动态作用域请借助bind,with,eval等。
- bash采用的是动态作用域
静态作用域 | 动态作用域 |
---|---|
变量的作用域是在定义时决定而不是执行时决定,一般通过静态分析就能确定。 | 只能在执行阶段才能决定变量的作用域。 |
// 经过验证,js默认采用静态作用域
// a 在foo调用的时候没有在当前函数作用域中找到,所以按照书写代码顺序往外层找,就是var a = 2,而不是取bar函数里面找
function foo() {
console.log(a) // 2
}
function bar() {
var a = 3
foo()
}
var a = 2
bar()
let 与 const
let
解决var存在的问题
let的出现是为了解决一些var存在的问题
- 同名变量,不在一个作用域,互不影响
// 下面两个i,虽然名字一样,但是不在一个作用域,所以互不影响
for (let i = 0; i < 3; i++) {
let i = 'foo'
console.log(i) // foo
}
- 解决循环嵌套计数器命名相同混乱的问题
const arr = [1, 2, 3, 4]
for(let i = 0; i < 3; i++) {
for(let i = 0; i < 4 ; i++) {
console.log(arr[i])
}
}
// 1 2 3 4 1 2 3 4 1 2 3 4
- 解决计数器循环中有异步变量被改变的问题(解决原理:闭包
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i)
}, 0)
}
// 输出三个3
特点
- 有块级作用域
{
let a = 1
console.log(a)
}
console.log(a) //a is not defined
- 全局变量不能用过window(全局对象)访问
var b = 3
let c = 4
console.log(b, c) // 3,4
console.log(window.b, window.c) //3,undefined
- 不能重复声明变量
var b = 3
let c = 4
console.log(b, c) // 3,4
var b = 4
console.log(b) //4
let c = 5 // Identifier 'c' has already been declared c已经被声明
console.log(c)
- 不会进行变量提升
function test() {
console.log(a)
let a = 1
}
test() // Cannot access 'a' before initialization 不能在初始化之前调用
const
特点
所有let的属性都有
只能定义常量,不能被修改
cosnt a = 2
a = 3 //Assignment to constant variable 类型错误
- 声明时必须初始化
const a //Missing initializer in const declaration 声明的时候缺少初始化
a = 2
为什么const定义对象,其属性可以改变,但是常量不可以?
const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动,对于简单类型的数据(数值,字符串,布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。
但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指针,const只能保证这个指针指向的内存地址是固定的,至于它指向的内存地址中保存的数据结构是不是可变的,就完全不能控制了。因此将一个对象声明为常量要小心。
最佳实践
不用var,主用const,遇到可变的使用let
数据的重要功能【遍历、转换、生成、查找】
ES5中遍历有多少种方法?
const arr = [1,2,3,4,5]
- for 循环
for (let i = 0;i < arr.length; i++) {
console.log(arr[i]) // 1 2 3 4 5
}
- forEach 数组遍历方法
arr.forEach(function(item){
console.log(item) //1 2 3 4 5
})
代码简洁上看,forEach比for循环写法要简洁。
但是forEach不支持reak和continue,每个元素必须遍历到
// for循环可以使用break控制遍历停止
for (let i = 0;i < arr.length; i++) {
if(arr[i] === 2){
break
}
console.log(arr[i]) // 1
}
// for循环可以使用continue控制遍历跳过
for (let i = 0;i < arr.length; i++) {
if(arr[i] === 2){
continue
}
console.log(arr[i]) // 1 3 4 5
}
//forEach不支持break和continue
arr.forEach(function(item){
if(item === 2) {
break // or continue 都会报错 Unsyntactic break 语法不支持
}
console.log(item)
})
arr.forEach(function(item){
if(item === 2) {
return false //跳过本此循环 类似continue
}
console.log(item) // 1 3 4 5
})
- every 数组遍历方法
arr.every(function(item){
console.log(item) // 1
})
// every继不继续遍历取决于返回值,为true则继续遍历,false退出。默认是false
// 如果要全部遍历完成 需要返回true
arr.every(function(item){
console.log(item) // 1 2 3 4 5
return true
})
// 可以通过返回的true or false 来控制循环
arr.every(function(item){
if(item === 2) {
return false //相当于for循环中的break
}
console.log(item) // 1
return true
})
- for-in
for-in本身是未object遍历的,而不是为数组。虽然可以遍历数组,但是有瑕疵。
for-in中可以使用continue和break,但是不支持写return,会报错Illegal return statement
// 之所以数组可以用for-in取遍历
// 是因为1.数组是对象的一种 2.数组是可遍历的
for (let index in arr) {
console.log(index, arr[index])
}
// 0 1
// 1 2
// 2 3
// 3 4
// 4 5
//瑕疵一:
// 因为arr是一个对象,所以可以定义属性并赋值
arr.a = 8
for (let index in arr) {
console.log(index, arr[index])
}
// 再次进行遍历
// 0 1
// 1 2
// 2 3
// 3 4
// 4 5
// a 8 这个时候a不是索引,而是字符串,如果这里我们还是以为遍历之后只是返回索引就容易出现bug
//瑕疵二:
for(let index in arr) {
if(index === 2) {
continue
}
console.log(index, arr[index])
}
// 0 1
// 1 2
// 2 3
// 3 4
// 4 5
//为什么可以使用continue但是continue却没有起作用
//因为这个时候index是字符串而不是索引
//解决方法一:只判断值,不判断类型
for(let index in arr) {
if(index == 2) {
continue
}
console.log(index, arr[index])
}
// 0 1
// 1 2
// 3 4
// 4 5
//解决方法二:将index隐式转化成数字类型
for(let index in arr) {
if(index * 1 === 2) {
continue
}
console.log(index, arr[index])
}
// 0 1
// 1 2
// 3 4
// 4 5
for-of(ES6新增)
// item不是下标,直接是值,且可以使用break终止循环
for(let item of arr) {console.log(item) // 1 2 3 4 5
}
为什么es6要新增一个for-of?
要判断一个对象是不是可遍历的,不能说其是不是一个数组 or Object。
ES6允许用户自定义数据结构,这种数据结构既不是数组,也不是Object,但是都是可遍历的。这种数据结构进行遍历,不能用数组的也不能用for-in,就需要新增一种for-of去遍历
举例子
// 定义一个数据结构,想要遍历拿到类别中的最低值
const Price = {
A: [1.5, 2.3, 4.5],
B: [3, 4, 5],
C: [0.5, 0.8, 1.2]
}
for (let key in Price) {
console.log(key, Price[key])
// [1.5, 2.3, 4.5]
// [3, 4, 5]
// [0.5, 0.8, 1.2]
}
// 使用for-in只能返回数组,无法直接把三个最低值遍历出来
for-of不能直接遍历对象? —— 关于可迭代接口
for-of可以用来遍历Set结构和Map结构,但是不可以直接遍历object,因为其不是可遍历的对象,Iterable接口没有实现。
数组、Set和Map的原型对象上有迭代器,对象方法上面没有
我们调用一下数组上面的iterator方法
const arr = ['foo', 'bar', 'baz']
console.log(arr[Symbol.iterator]())
// 返回一个iterator的对象,其原型对象上面有next方法
// Array Iterator {}
// __proto__: Array Iterator
// next: ƒ next()
// Symbol(Symbol.toStringTag): "Array Iterator"
// __proto__: Object
const iterator = arr[Symbol.iterator]()
console.log(iterator.next()) // { value: 'foo', done: false }
console.log(iterator.next()) // { value: 'bar', done: false }
console.log(iterator.next()) // { value: 'baz', done: false }
console.log(iterator.next()) // { value: undefined, done: true }
可以看到for-of内部的循环规则,里面有一个迭代器。只要对象可以实现Iterable接口就可以使用for-of进行循环。下面对一个对象进行可迭代改造。
- 优势和缺点
循环方式 | 优势 | 缺点 | 特点 |
---|---|---|---|
for循环 | 支持break和continue | 不支持return | |
forEach | 写法比for简洁明了 | 全部遍历,不支持break和continue | return false相当于continue |
every | 可以支持类似for的break和continue | ||
for-in | 可以遍历object | 遍历数组的时候存在瑕疵 | 不支持return |
for-of(ES6新增) | 可以遍历除数组和object之外可遍历的数据结构,支持break和continue | 不支持return |
- return只能用在函数体内,出现在代码的任何地方都会报错
将伪数组转换成数组
什么是伪数组?
简单理解:
具备一些数组的特性:可遍历、有长度。但是不能直接调用数组的API,类似于 arguments 和 DOM nodeList。
严格理解:
- 按照索引方式存储数据
- 具备length属性
{0:’2’,1:’b’,length:2} 这种类型都可以称之为伪数组
为什么要将伪数组转换成数组?
如果想要使用数组的API,就需要将伪数组转换为数组
- ES5的转换
let args = [].slice.call(arguments) //collection
// arguments 只能在函数体内使用
// ES6已经废弃arguments的使用
let imgs = [].slice.call(document.querySelectorAll('img')) // NodeList
- ES6的转换
let args = Array.from(arguments)
let imgs = Array.from(document.querySelectorAll('img'))
大致说一下Array.from这个函数
Array.from(arrayLike,mapFn,thisArg)
// 第一个参数:伪数组,必须
//第二个参数:遍历函数,非必须
//第三个函数:this对象,非必须
举一个例子:
初始化一个长度为5的数组
//ES5
let array = Array(5)
array.forEach(function (item) {
item = 1
})
console.log(array) // [empty × 5] forEach只会遍历存在的元素
//使用for循环可以遍历,但是依旧是先声明,后赋值
for (let i = 0, len = array.length; i < len; i++) {
array[i] = 1
}
console.log(array) // [1,1,1,1,1]
// 先将数组转化为5个空字符串数组,然后每个遍历赋值
let arr = Array(6).join(' ').split('').map(item => 1)
console.log(array) // [1,1,1,1,1]
// ES6
//使用Array.from可以达到初始化并填充的效果
let array = Array.from({ length: 5 }, function () { return 1 })
console.log(array) // [1,1,1,1,1]
// 上面之所以第一个参数可以传{ length: 5 },是因为第一个参数应该传伪数组
//使用Array.fill可以快速实现初始化并填充
let array = Array(5).fill(1)
console.log(array) //[1,1,1,1,1]
创建新数组
- ES5
let array = Array(5)
let array = ['', ''] //无法定义长度,只能每个都初始化为空字符串
- ES6
- Array.from
let array = Array.from({ length: 5 })
- Array.of
let array = Array.of(1,2,3,4,5) //参数是依此放进去的元素,可以一个参数可以多个
console.log(array) //[1,2,3,4,5]
- Array.prototype.fill
let array = Array(5).fill(1) //参数是依此放进去的元素,可以一个参数可以多个
console.log(array) //[1,1,1,1,1]
Array.fill(value,start,end)
第一个参数:填充值
第二个参数:起始位置,默认为数组的开始
第三个函数:结束位置,默认为数组的结束
[start,end)
let array = [1, 2, 3, 4, 5]
console.log(array.fill(8, 2, 4)) // [1,2,8,8,5]
其他
箭头函数不适用场景
- 使用箭头函数内的this值为父级作用域的this值
// 作为构造函数,一个方法需要绑定到对象
const Person = (name, points) => {
this.name = name
this.points = points
}
const Bob = new Person('Bob', 5)
// 此声明对象会报错
1、生成一个新的对象
2、把构造函数this的值指向新生成的对象
3、把这个对象绑定它的原型对象
4、返回这个新生成的对象
但是使用箭头函数this值没有绑定到新生成的对象上,导致报错
Person.prototype.updatePoints = () => {
this.points++
console.log(this.points)
}
// 返回NaN
// 这两个操作都需要使用function函数才能正确绑定this值
- 绑定事件时
const btn = document.querySelector('.btn')
btn.addEventListener('click', () => {
this.classList.add('enlarge')
setTimeout(() => {
this.classList.remove('enlarge')
}, 2000)
})
// 绑定事件报错,因为箭头函数内this不指向btn
// 需要使用function函数,之后内部setTimeout使用箭头函数,this指向btn
- 需要使用arguments对象时
const sum = () => {
return Array.from(arguments)
.reduce((prevSum, value) => prevSum + value, 0)
}
// 调用sum方法报错,因为箭头函数中没有arguments对象,所以需要使用function函数
// 或者使用Rest参数的方法
const sum = (...args) => {
return args.reduce((prevSum, value) => prevSum + value, 0)
}
模板字符串
const person = 'Bob'
const age = 5
const sentence = `${person} is ${age + 5} years old.`
const template = `
<div>
<p>${person}</p>
</div>
`
//大括号内的变量,可以是变量、对象属性、js表达式或函数