ES6

ES6

ECMAScript

ECMAScript通常看做JavaScript的标准化规范,实际上JavaScript是ECMAScript的扩展语言,ECMAScript只是提供了最基本的语法。

JavaScript = ECMAScript + BOM + DOM

  • 2015年开始ES保持每年一个版本的迭代,并且开始按照年份命名。
  • 相比于ES5.1的变化比较大
  • 自此,标准命名规则发生变化
  • ES6泛指是2015之后的所有新标准,以后要看清楚是特指还是泛指

新特性的分类

  • 解决原有语法上的一些问题或者不足
  • 对原有语法进行增强
  • 全新的对象、全新的方法、全新的功能
  • 全新的数据类型和数据结构

作用域

  1. 全局作用域
  2. 函数作用域
  3. 块状作用域
  4. 动态作用域
对象 类型
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在函数作用域中,但是某些值共享?

  1. return
  2. 闭包

块级作用域(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

特点

  1. 有块级作用域
{
    let a = 1
    console.log(a)
}
console.log(a) //a is not defined
  1. 全局变量不能用过window(全局对象)访问
var b = 3
let c = 4
console.log(b, c) // 3,4
console.log(window.b, window.c) //3,undefined
  1. 不能重复声明变量
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)
  1. 不会进行变量提升
function test() {
    console.log(a)
    let a = 1
}
test() // Cannot access 'a' before initialization 不能在初始化之前调用

const

特点

  1. 所有let的属性都有

  2. 只能定义常量,不能被修改

cosnt a = 2
a = 3 //Assignment to constant variable 类型错误
  1. 声明时必须初始化
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。

严格理解:

  1. 按照索引方式存储数据
  2. 具备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
  1. Array.from
let array = Array.from({ length: 5 })
  1. Array.of
let array = Array.of(1,2,3,4,5) //参数是依此放进去的元素,可以一个参数可以多个
console.log(array) //[1,2,3,4,5]
  1. 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表达式或函数

文章作者: Hao Jie
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Hao Jie !
  目录