ES6新特征总结

一. 块级作用域、块级变量let、块级常量const

1. 由一对 { } 界定的语句叫做块语句,这在其他编程语言中叫做复合语句。

一般理解的块:
块:由{}包括住
if(){}:是一个块
for(){}:是一个块

这些也可理解成块:
一个函数:函数作用域
script标签:全局作用域

2. JS中用var声明的变量是没有块级作用域的,只有函数作用域和全局作用域。

var x = 1;
{
  var x = 2;
}
console.log(x);

//会输出2,因为块中的var语句与块前面的var语句作用域相同
//在C或Java中,这段代码会输出 1
//这代码证明了var没有块作用域

3. 相比之下,使用 let 和 const 声明的变量是有块级作用域的。

let x = 1;
{
  let x = 2;
}
console.log(x); 

// 输出 1
// x被限制在块级作用域中
// 这里将let换成const结果也一样

4. 经典的例子(背住):

var a = [];
for (var i = 0; i < 10; i++) {
      a[i] = function () {console.log(i);};
}
a[0]();                // 10
a[1]();                // 10
a[6]();                // 10

/********************/
var i 在a【i】的时候已经赋值了,在函数的花括号{}没有赋值所以。

var a = [];
for (let i = 0; i < 10; i++) {
      a[i] = function () {console.log(i);};
}
a[0]();                // 0
a[1]();                // 1
a[6]();                // 6
```javascript
**//第一个中的i是没有块级作用域的,全局只有一个变量i,每次循环的{}中的代码都指向同一个i
//第二个中的i是有块级作用域的,当前的i只在本轮循环有效,每次循环的{}中的代码都指向不同的i
//值得注意的是:for的()是一个父作用域,{}是一个子作用域
//因为当前的i只在本轮循环有效,所以let定义的i在每次循环都会重新定义一遍,每次都是一个新变量**

5. 补充:

foo('outside');  // TypeError: foo is not a function
{
  function foo(location) {
   console.log('foo is called ' + location);
  }
  foo('inside'); // 正常工作并且打印 'foo is called inside' 
}

//使用function时也有块级作用域

6. var变量提升,let变量暂时性死区:

//var会发生变量提升现象,将变量提升到函数顶部或全局顶部 
console.log(foo); //输出undefined,不会报错
var foo = 2; 

//let没有这种情况,必须在let声明后调用该变量 (const和let一样)
//let和const的这种现象称为暂时性死区
//从块开始到let声明之间是“暂存死区”
console.log(bar); //报错ReferenceError 
let bar = 2;

7. let的错误
在同一个作用域中用let重复定义一个变量将引起TypeError

if (x) {
  let foo;
  let foo; // TypeError thrown.
}

8. const的概念

**• 必须在声明的同一语句中指定它的值
• const声明创建一个值的只读引用,也就是说只能通过const定义的变量来读这个值,不能修改这个值,但是如果这个值本身发生了变化,那么const定义的变量所对应的值也就跟着变化,比如当引动内容是对象的情况下。
• 一个常量不能和它所在作用域内的其他变量或函数拥有相同的名称。
// 下面错误,常量要求有一个初始值

const FOO;
// 下面正确,常量可以定义成对象
const MY_OBJECT = {"key": "value"};
// 下面错误,改变常量会失败
MY_OBJECT = {"OTHER_KEY": "value"};
// 对象属性并不在保护的范围内,下面会成功执行(对象本身发生变化)
MY_OBJECT.key = "otherValue";
// 定义一个常量
const MY_FAV = 20;
// 不能同名,会出错
var MY_FAV = 20; 
// 也会报错
let MY_FAV = 20;**

二. 箭头函数

基础语法:

//一般语法:
(参数1, 参数2, …, 参数N) => { 函数声明 }

//相当于:(参数1, 参数2, …, 参数N) =>{ return 表达式; }
(参数1, 参数2, …, 参数N) => 表达式(单一)

// 当只有一个参数时,圆括号是可选的:
(单一参数) => {函数声明}
单一参数 => {函数声明}

// 没有参数的函数应该写成一对圆括号
() => {函数声明}

高级语法:

(*注: 字面量一般指[1, 2, 3] 或者{name: "mdn"} 这种简洁的构造方式)
*//加括号的函数体返回对象字面表达式:
参数=> ({foo: bar})

//支持剩余参数和默认参数
(参数1, 参数2, ...rest) => {函数声明}
(参数1 = 默认值1,参数2, …, 参数N = 默认值N) => {函数声明}

//同样支持参数列表解构
let f = ([a, b] = [1, 2], {x: c} = {x: a + b}) => a + b + c;
f();  // 6

例子:
箭头函数比较适用于原本需要匿名函数的地方,比如用在数组内置方法map、filter、forEach、reduce的回调函数中

var elements = [
  'Hydrogen',
  'Helium',
  'Lithium',
  'Beryllium'
];

elements.map(function(element) { 
  return element.length; 
}); // 返回数组:[8, 6, 7, 9]

// 上面的普通函数可以改写成如下的箭头函数
elements.map((element) => {
  return element.length;
}); // [8, 6, 7, 9]

重点注意:
this 对象的指向是可变的,但是在箭头函数中,它是固定的。箭头函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。
三. 参数处理
1. 给函数设置默认参数值
在ES6以前,我们想要给参数设置默认值得这么做:

function multiply(a, b) {
  //JavaScript中函数的参数默认是undefined
  //没有值传入或者undefined被传入给b时给b一个默认值
  b = (typeof b !== 'undefined') ?  b : 1;
  return a * b;
}

multiply(5, 2); // 10
multiply(5);    // 5

现在学习了ES6之后就变得很简单:

function multiply(a, b = 1) {
  return a * b;
}

multiply(5, 2); // 10
multiply(5);    // 5
注意参数的传递是从左到右的:
function f(x = 1, y) { 
  return [x, y]; 
}

f(); // [1, undefined]
f(2); // [2, undefined]

2. 剩余参数(…)
剩余参数语法允许我们将一个不定数量的参数表示为一个数组。
语法:

function(a, b, ...theArgs) {
   ...
}
//必须是函数的最后一个参数以...为前缀
//它是由剩余实参组成的Array数组
//这个例子中的theArgs收集第三个以及之后的所有实参

实例:

function fun1(...theArgs) {
  alert(theArgs.length);//可以使用所有数组属性方法
}
fun1();  // 弹出 "0", 因为theArgs没有元素
fun1(5); // 弹出 "1", 因为theArgs只有一个元素

四. 模板字面量(模板字符串)

五. 对象的扩展

• 创建对象时可以简化方法和重名的键值对

//一般情况
var name="pan";
var age=20;
var people = {
      name: name,
      age: age,
      getName: function() {
           console.log(this.name)
      }
};
//ES6简化后
var name="pan";
var age=20;
var people = {
      name,
      age,
      getName(){
           console.log(this.name)
      }
};

• 计算属性名(属性名可以用表达式)
• 对象中如果属性名要设置为一个变量,必须要用[]包裹

var i = 0;
var a = {
  ["foo" + ++i]: i,
  ["foo" + ++i]: i,
  ["foo" + ++i]: i
};
console.log(a.foo1); // 1
console.log(a.foo2); // 2
console.log(a.foo3); // 3

六. 解构赋值

解构赋值语法是一种 Javascript 表达式,它使得将值从数组,或属性从对象,提取到不同的变量中,成为可能。
目的就是将数组中的值或对象中的属性方便的提取到其他变量中
• 解构数组语法:

var x, y;
[x, y] = [1, 2, 3];
console.log(x); // 1
console.log(y); // 2

//或者:
var [x, y] = [1, 2, 3];
console.log(x); // 1
console.log(y); // 2

小技巧(不用临时变量也能交换变量):

var a = 1;
var b = 3;
//等价于[a,b]=[3,1]
[a, b] = [b, a];
console.log(a); // 3
console.log(b); // 1

• 解构对象语法:

var o = {p: 42, q: true};
var {p, q} = o;

console.log(p); // 42
console.log(q); // true

注意:对象解构与数组解构有一个重要的不同点,数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

七. 模块(import/export)

模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个 JS 文件,里面使用export命令输出变量。

• 导出export

// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};

//上面代码是profile.js文件,保存了用户信息,
//ES6将其视为一个模块,里面用export命令对外部输出了三个变量。

//除了这种普通变量,输出的变量还可以是一个函数。

//另外,export语句输出的接口,与其对应的值是动态绑定关系,
//即通过该接口,可以取到模块内部实时的值。

//最后,export/import命令不能在块级作用域内,可以在全局作用域任何位置。

• 导入import
import语句只能在声明了type="module"的script的标签中使用。
动态导入:import(),它不需要依赖type="module"的script标签。
按照一定的条件或者按需加载模块的时候,动态import()是非常有用的。
而静态的import是初始化加载的最优选择。

// main.js
import {firstName, lastName, year} from './profile.js';

function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}
//上面代码的import命令,用于加载profile.js文件,并从中输入变量。
//import命令接受一对大括号,里面指定要从其他模块导入的变量名。
//大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。
注意:import语句会执行所加载的模块
import './lodash.js';
//执行lodash模块,但是不输入任何值

整体导入:

import * as myModule from './my-module.js';//不用加大括号{}
//将my-module里面输出的变量/函数,
//输入到myModule这个对象中作为属性/方法。
//通过myModule.属性名/myModule.方法()来调用。
•	默认导出/导入默认值:
告别使用import命令的时候,用户需要知道所要加载的变量名或函数名的现状
//先用export default语法默认导出
//既然是默认输出,那么一个模板只能使用一次
export default function foo() {
  console.log('foo');
}

//导入默认值
import customName from './default.js';//不用加大括号{}
customName(); // 'foo'

八. 类(class/extends)

ES6 中引入的 JavaScript 类实质上是 JavaScript 现有的基于原型的继承的语法糖。类语法不会为JavaScript引入新的面向对象的继承模型。
ES6中面向对象的写法就是普通的后端语言面向对象的写法,
定义类用class关键字,继承通过extends关键字,以及super关键字

// 一:类的定义和使用
// 1.通过class定义类
// 2.在类中通过constructor定义构造方法,构造方法来初始化类的属性
// 3.通过new来创建类的实例

// 二:类的继承意义何在?
// 子类继承父类之后可以获得父类的属性和方法
// 三:类的继承具体语法
// 1.通过extends来实现类的继承
// 2.通过super调用父类的构造方法
// 3.重写可以覆盖父类中的继承的方法

class Person{
    // 调用类的构造方法constructor方法
    // 初始化类的属性
    constructor(name,age){
        this.name=name;
        this.age=age;
    }
    // 定义原型上的方法
    showName(){
        console.log(this.name,this.age);
    }
}
var person=new Person("林冲",29);
console.log(person);
person.showName()
// 定义一个子类
// 继承的话会继承父类的属性和方法
class Student extends Person{
constructor(name,age,stuNum){
利用super关键字去继承父类当中的constructor方法

        super(name,age);//调用父类上的构造方法constructor方法
        this.stuNum=stuNum;
    }
    // showName(){
    //     // 在子类自身定义方法
    //     console.log(this.name,this.age,this.stuNum);
    // }
}
var student=new Student("夏东海","18","1314")
student.showName()
// 如上如果我们调用showName方法时,因为是继承父,所以我们如果之前在父上写了showName方法的时候在子类调showName方法的时候
// 会直接调用父中定义好的showName方法。如果子类上也定义了同样名字的showName方法的时候,子类的同名方法就会直接覆盖掉父类的方法。
// 如果父类有子类没写showName方法就直接用父类的showName方法,如果父类写了showName方法,子类也写了showName方法那么就会调用子类内部的方法
// 总结子类重写父类已有的方法父类的同名方法会被覆盖掉

• 如何创建类:

class Rectangle {
  constructor(height, width) { //构造方法
    this.height = height;
    this.width = width;
  }
}
//或者
let Rectangle = class {
  constructor(height, width) { //构造方法
    this.height = height;
    this.width = width;
  }
};

• 如何使用类:

class Rectangle {
    // constructor
    constructor(height, width) {
        this.height = height;
        this.width = width;
    }
    // Getter
    get area() {
        return this.calcArea()
    }
    // Method
    calcArea() {
        return this.height * this.width;
    }
}
const square = new Rectangle(10, 10);

console.log(square.area);
// 100

• 类的静态方法:
不能通过一个类实例调用静态方法(这里可Java语法有区别)

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

    static distance(a, b) { //static语法
        const dx = a.x - b.x;
        const dy = a.y - b.y;

        return Math.hypot(dx, dy);
    }
}

const p1 = new Point(5, 5);
const p2 = new Point(10, 10);
//直接通过类名调用
console.log(Point.distance(p1, p2));

• 类的继承(extends)

class Animal { 
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(this.name + ' makes a noise.');
  }
}

class Dog extends Animal {
    console.log(this.name + ' barks.');
  }
}

var d = new Dog('Mitzie');
// 'Mitzie barks.'
d.speak();

• 调用父类属性方法(super)

class Cat { 
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(this.name + ' makes a noise.');
  }
}

class Lion extends Cat {
  speak() {
    super.speak();
    console.log(this.name + ' roars.');
  }
}

九. Promise

// function n1() {
//     setTimeout(function () {
//         console.log(1);
//     }, 3000)
//     console.log("n1");
// }

// function n2() {
//     setTimeout(function () {
//         console.log(2);
//     }, 2000)
//     console.log("n2");
// }

// function n3() {
//     setTimeout(function () {
//         console.log(3);
//     }, 1000)
//     console.log("n3");
// }

// n1()
// n2()
// n3()
// 代码执行流程分析→同步函数n1执行到定时器先把定时器放在异步任务队列中执行打印n1字符串(函数n2,n3同理),n2,n3先按顺序执行
// 等同步任务执行完了之后,就同时执行异步任务队列里面的定时器
// 如上代码打印顺序为→n1→n2→n3→3→2→1
// 我们如果想打印顺序为→n1→n2→n3→1→2→3
// 我们方案有如下写法↓
// 方案一→使用函数嵌套的方式解决
// 缺点:这种解决方案如果函数嵌套过多就会形成回调地狱
// function n1() {
//     setTimeout(function () {
//         console.log(1);
//         n2()
//     }, 3000)
//     console.log("n1");
// }

// function n2() {
//     setTimeout(function () {
//         console.log(2);
//         n3()
//     }, 2000)
//     console.log("n2");
// }

// function n3() {
//     setTimeout(function () {
//         console.log(3);
//     }, 1000)
//     console.log("n3");
// }
// n1()

// 方案二
// Promise的链式调用解决
// Promise 有三种状态pendding(等待状态),resolve(成功状态)→成功状态用then函数处理,reject(失败状态)→失败状态用catch函数处理
// 在Promise任何一个环节出现错误,都会被同意的catch方法捕获,并且终止代码执行。
function num1() {
    return new Promise((res, rej) => {
        // 要做的异步操作
        setTimeout(function () {
            // 异步成功执行res方法,外部用then处理
            // 异步失败执行rej方法,外部用catch处理
            console.log("1");
            res("韩梅梅")
            // rej("韩七七")
        }, 3000)
    })
}

function num2() {
    return new Promise((res, rej) => {
        // 要做的异步操作
        setTimeout(function () {
            // 异步成功执行res方法,外部用then处理
            // 异步失败执行rej方法,外部用catch处理
            console.log("2");
            res("韩梅梅")
            // rej("韩七七")
        }, 2000)
    })
}

function num3() {
    return new Promise((res, rej) => {
        // 要做的异步操作
        setTimeout(function () {
            // 异步成功执行res方法,外部用then处理
            // 异步失败执行rej方法,外部用catch处理
            console.log("3");
            res("韩梅梅")
            // rej("韩七七")
        }, 1000)
    })
}

num1().then((res) => {
    console.log("num1已经执行完成了");
    return num2()
}).then((res)=>{
    console.log("num2已经执行完成了");
    return num3()
}).then((res)=>{
console.log(res,"num3执行完成啦(๑′ᴗ‵๑)I Lᵒᵛᵉᵧₒᵤ❤");
})

// 总结利用Promise解决回调地域的方案思路
// 第一:我们首先要明确Promise是解决异步回调地域的
// 步骤二:我们new一个Promise对象,并且在Promise对象内部的函数当中放入你想执行的异步代码并且在
// 你想执行的异步代码的后方写上调用状态为成功的回调函数执行【通常写res()或者resolve()】
// 然后我们再把写了异步执行程序的Promise对象放入到function函数封装当中,并且在function函数内部用return的方式返回出来。

// 方案三
// 使用ES7当中的acync和await的 方案操作异步的嵌套问题
// 多个异步操作按照固定的顺序执行→回调嵌套→回到地狱→Promise的链式调用→async  await
// async修饰符处理一个函数,可以让一个函数返回一个Promise对象
// 在函数内部只要不出错,都是走成功的回调函数,也就是then回调,如果出错,就会reject抛出错误.
// await修饰符→await这个修饰符不能单独使用,只能配合async使用,用来修饰一个Promise对象,可以将异步变成同步。

async function sun(){
    console.log("标识符→","sum");
}
console.log(sun());

// 如上打印sun函数调用,会输出一个"标识符→" "sum"
// 和一个Promise对象。Promise对象内部的PromiseValue的值就是函数sunreturn出来的返回值,其中Promise对象内部的PromiseValue的值,也就是Promise对象内部,成功回调函数的传入实参。