快速简明教程: es6

#video

ES6, 对 Javascript 基础语法做了非常大的变更。本文就其中较常用的语法进行快速讲解。主要内容包括:

  • 变量 vs 常量
  • 箭头函数 =>
  • 对象 vs 类
  • 关键字 this
  • 数组的枚举
  • 展开操作符
  • 异步控制
  • 代码模块化

变量 vs 常量

在 ES6 中变量定义方式有三种,分别是:

  • 直接命名
  • 关键字 var 命名, 作用范围是 function, 函数区间
  • 关键字 let 字, 作用范围是 block, 块区间

常量定义,使用的关键字是:

  • const,作用范围是 block, 块区间

常量不可以被再次赋值.

箭头函数

在 ES6 中,简化了原有的 javascript 函数语法格式。

首先,回顾一下 javascript 函数定义。

const square = function (number) {
  return number * number;
};
console.log(square(3));

箭头函数语法,则是,删除 function 关键字,使用 => 替代。

  • 如果函数,只有一个函数参数,还可以删除();
const square = (number) => {
  return number * number;
};
console.log(square(3));
  • 如果函数体,只有一行,并且,通过 return 关键字进行返回; 还可以继续简化,直接删除 return 关键字,同时,删除 {}
const square = (number) => number * number;
console.log(square(3));

对象 vs 类

对象定义

javascript 中定义对象,其实就是一个简单的 key / value 的 map 对象。

const person = {
  name: "jayl",
  sex: "male",
};
console.log(person);

person.name = "liu";
console.log(person);

虽然,对象本身是一个常量,但是,对象内部属性是可以发生变更的。

解构对象

在 ES5 中,获取对象属性,需要进行显示声明:

const obj = {
  a: "a",
  b: "b",
  c: "c",
};

const a = obj.a;
const b1 = obj.b;
const c = obj.c;

而 ES6 提供了更简单的解构方式:

const obj = {
  a: "a",
  b: "b",
  c: "c",
};

const { a, b: b1, c } = obj;

console.log(a, b1, c);

注意,其中别名 b1 的使用。

类定义

ES6 类的定义,使用关键字 class. 在类的定义时,通常注意点有:

  • 类名,建议使用大写字母开头,非必须。
  • 类定义,只能提供一个初始化函数,即 constructor 函数。非必须。
class Person {
  constructor(name) {
    this.name = name;
  }
  print() {
    console.log(this);
  }
}

//匿名类
const Dog = class {};

类的继承

类的继承,同样不是什么新东西,大部分面向对象的开发语言都会提供类似功能。在 ES6 中类的继承,功能类似。不过,在 ES6 中,继承类的构造函数中,必须显示的调用父类构造函数,使用 super 关键字。

继承关系,使用 extends 关键字进行继承。

class Teacher extends Person {
  constructor(name, age) {
    super(name);
    this.age = age;
  }
  print() {
    console.log(this);
  }
}

关键字 this

关键字 this 是一个与执行期上下文联系紧密的对象引用。this 引用的就是当前执行期的对象。所以,在不同的执行期,可能相同的代码,this 引用的对象是不同的。在开发过程中,使用 this 关键字时,必须保持高度警惕。

令人崩溃的 this

如果我们在浏览器中,执行以下代码,会得到令人崩溃的结果。如下:

class A {
  constructor(name) {
    this.name = name;
  }
  print() {
    console.log("class this = ", this);
  }
}

const a = new A("jay");
a.print(); // 对象 a

const f = a.print;
f(); // 为定义 undefined

console.log("console this =", this); // 对象 windows

const obj = {
  p1: function () {
    console.log("obj this =", this);
  },
  p2: () => {
    console.log("obj this =", this);
  },
};
obj.p1(); // 对象 obj
obj.p2(); // 对象 windows

结论 关键字 this, 与函数、对象、类无关,只与执行期上下文有关

函数 bind 绑定

虽然,在关键字 this 的使用上,经常会得到令人崩溃的结果。但是,我们依然有办法解决 this 的指向不明问题。那就是 bind 绑定函数。

我们知道,关键字 this 的使用,一定是在函数中进行调用的。那么,我们只需要针对函数的两种不同形式进行区分即可。

  • 普通函数
function f1(arg) {
  console.log(this);
  console.log(arg);
}
f1(1); // this => window, 1
f1.bind(1, 2)(1); // this => 1, 2
f1.bind("hello")(1); // this => "hello",1
f1.bind("hello", "world")(1); // this => "hello","world"
f1.bind({ a: "a" })(1); // this => {a: "a"},1

结论 在普通函数中,bind 除了可以绑定执行期对象 this 以外,还可以绑定任意对象值。

  • 箭头函数
// 箭头函数
const f2 = (arg) => {
  console.log(this);
  console.log(arg);
};
f2(1); // this => window, 1
f2.bind({ a: "a" }, 2)(1); // this => window, 2

箭头函数 在箭头函数中, 无法通过 bind 绑定执行期对象 this,但是参数绑定依然有效

为了避免对于关键字 this 的误用,特别是在将函数作为参数传递的过程中,显示绑定执行期对象 是更好的编程写法。

数组枚举

在 ES6 中,增加了两种枚举数组的方式。

for 函数

在 ES5 中可以使用 for 循环对数据进行枚举, ES6 中增加了新的 of 关键字。

在 ES5 中,通过 for 进行循环枚举

var arr = ["a", "b", "c"];
// ES5
for (var i = 0; i < arr.length; i++) {
  console.log(arr[i]);
}

在 ES6 中, for 循环可以这样写:

var arr = ["a", "b", "c"];
// ES5
for (let i of arr) {
  console.log(i);
}

map 函数

除了以上两种枚举方式,ES6 增加了一个 map 函数,除了可以进行普通的数组枚举以外,它还可以对枚举元素进行返回处理,返回成为一个数组对象。

首先,使用 map 函数进行普通的枚举操作。如:

const colors = ["red", "yellow", "green"];
colors.map((color) => {
  console.log(color);
});

map 枚举元素进行返回,我们将得到一个数组对象。

const colors = ["red", "yellow", "green"];
var items = colors.map((color) => {
  return "<li>" + color + "</li>";
});
console.log(items);

使用箭头函数简化代码:

const colors = ["red", "yellow", "green"];
var items = colors.map((color) => "<li>" + color + "</li>");
console.log(items);

对于字符串的拼接,我们还可以使用,es5 中的 模板字符串 特性进一步简化。如下:

const colors = ["red", "yellow", "green"];
var items = colors.map((color) => `<li>${color}</li>`;
console.log(items);

展开操作符

在 ES6 中,增加了 ... 展开操作符的使用。展开可以针对数组或者是对象。

数组展开

// ES6
let arr1 = [1, 2, 3];
let arr2 = ["a", "b", "c"];
let arr3 = [...arr1, ...arr2];

console.log(arr3); // [1, 2, 3, "a", "b", "c"]

对象展开

let obj1 = { name: "jayl" };
let obj2 = { sex: "male" };

let obj3 = { ...obj1, ...obj2, location: "shanghai" };
console.log(obj3);

异步控制

在 javascript 中传统的异步实现是通过 callback 即回调的形式实现的。但是回掉,有很多问题,不如大家常说的 callback hole, 回调黑洞,即在回调中再嵌套回调,就会出现回调黑洞问题。

异步对象 Promise

es6 中引入 Promise 对象,就可以帮助我们更好的处理回调嵌套问题。

  • 基础用法

首先,创建一个 Promise 对象,Promise 对象提供一个用户定义的执行函数,同时,执行函数提供了两个通知函数参数,分别是 resolvereject. 这两个参数,分别用于执行函数成功与失败时的通知操作,相当于回调通知。

let p1 = new Promise((resolve, reject) => {
  //执行函数实现逻辑
  console.log("executor is running ...");

  let err = false;
  if (!err) {
    //执行函数成功
    resolve("executed: succeed");
  } else {
    //执行函数失败
    reject("executed: failed");
  }
});

如果,现在什么都不做,我们发现,异步对象创建完成后,会立即执行了函数的主体部分。

现在,我们处理其异步通知,分别使用 then 处理成功通知, catch 处理失败通知。

p1.then((v) => {
  console.log("then =>", v);
}).catch((e) => {
  console.log("catch =>", e);
});

执行后,我们发现,成功回调的处理被调用了。如果要测试失败通知,将之前的 let err = true 即可。

除了使用 catch 的方式捕获错误外,还可以直接在then中, 使用第二个参数,设置失败事件。

p1.then(
  (v) => {
    console.log("then =>", v);
  },
  (reason) => {
    console.log("reason =>", e);
  }
);

效果同前面一样。

最后,我们增加一个,finally 处理,即不论执行函数失败与否,都必须执行finally过程。

p1.then((v) => {
  console.log("then =>", v);
})
  .catch((e) => {
    console.log("catch =>", e);
  })
  .finally(() => {
    console.log("finnally done");
  });
  • 异步嵌套

既然,Promise 提供更好的异步嵌套实现。那么,现在我们实现一组嵌套功能。

很明显,异步嵌套的代码应该在 then 函数中实现。

为了实现链式调用,我们在 then 函数实现中需要返回一个新的 Promise 对象。在 then 实现中返回一个新的 Promise 对象。

let p1 = new Promise((resolve, reject) => {
  //执行函数实现逻辑
  console.log("executor 1 is running ...");
  let err = false;
  if (!err) {
    //执行函数成功
    resolve("executor 1: succeed");
  } else {
    //执行函数失败
    reject("executor 1: failed");
  }
});

//在 p1 的 then 实现中返回一个新的 Promise 对象,实现链式调用
p1.then((v) => {
  console.log("executor 1 result:", v);

  return new Promise((resolve, reject) => {
    //执行函数实现逻辑
    console.log("executor 2 is running ...");
    let err = false;
    if (!err) {
      //执行函数成功
      resolve("executor 2: succeed");
    } else {
      //执行函数失败
      reject("executor 2: failed");
    }
  });
}).then((v) => {
  console.log("executor 2 result:", v);
});

这就是 Promise 实现嵌套调用的原理。

但是,我们经常看到的代码,在 then 实现中并不会显示的返回一个 Promise 异步对象,而是返回一个具体的值对象。测试后,会发现这样写,同样可以实现 then 的链式调用。

很明显,在 Promise 异步对象的 then 实现层,它已经帮我们返回了一个新Promise 异步对象。只不过该新的Promise 异步对象,就是一个常量返回实现而已。

可以简单模拟以下,这个过程:

let p1 = new Promise((resolve, reject) => {
  //执行函数实现逻辑
  console.log("executor 1 is running ...");
  let err = false;
  if (!err) {
    //执行函数成功
    resolve("executor 1: succeed");
  } else {
    //执行函数失败
    reject("executor 1: failed");
  }
});

p1.then((v) => {
  return "object value";
}).then((v) => {
  console.log("v =>", v);
});
// 会得到 v => object value 的输出。

这是因为,Promise 对象会自行对具体的值对象,创建一个新的 Promise 对象。只是,在代码中没有表现出来。手动实现的话,就是这样的:

let p1 = new Promise((resolve, reject) => {
  //执行函数实现逻辑
  console.log("executor 1 is running ...");
  let err = false;
  if (!err) {
    //执行函数成功
    resolve("executor 1: succeed");
  } else {
    //执行函数失败
    reject("executor 1: failed");
  }
});

p1.then((v) => {
  //return "object value";
  return new Promise((resolve, reject) => resolve("object value"));
}).then((v) => {
  console.log("v =>", v);
});
// 会得到 v => object value 的输出。

既然,对于在 then 函数中返回具体的值对象,Promise 会自动创建一个新的 Promise 对象,并通过 resolve 返回该值。那么异常情形,又是如何处理的:

let p1 = new Promise((resolve, reject) => {
  //执行函数实现逻辑
  console.log("executor 1 is running ...");
  let err = false;
  if (!err) {
    //执行函数成功
    resolve("executor 1: succeed");
  } else {
    //执行函数失败
    reject("executor 1: failed");
  }
});

p1.then((v) => {
  throw new Error("error ... raised");
}).catch((v) => {
  console.log(v);
});

如果在 then 函数实现中,主动抛出一个 Error 对象,那么 Promise 就会创建一个新的 Promise 对象, 通过 reject 返回异常。

了解了以上原理,现在无论想要嵌套多少个 Promise 对象就都不是问题了。

关键字 async

了解了 Promise 对象原理,也许你会问,有没有更简单的创建方式。答案是,有。我们可以通过 async 关键字快速创建一个 Promise 对象。唯一与传统创建 Promise 方式不同的是,通过async 关键字方式创建的不是 Promise 对象,而是一个 Promise 定义。具体 Promise 对象,是在 async 函数调用时创建的。

async function foo() {
  return "hello async";
}

foo().then((v) => {
  console.log(v);
});

如果,要在 async 函数实现抛出异常,代码就是:

async function foo() {
  throw new Error("foo exception");
}

foo().catch((v) => {
  console.log(v);
});

关键字 await

await 关键字,顾名思义,就是等待一个异步函数执行完成的意思。在一些特定的情形中,虽然我们调用的是异步函数,但是在具体实现中,可能需要等待其异步执行完成,才能继续执行主函数代码。此时,await 关键字就派上用场了。

await 关键字的使用,必须注意,后续等待的是一个异步函数。等待异步函数 resolvereject之后,才会返回。如果等待的是普通函数,不会有任何影响。

async function getName() {
  return "Jayl";
}

async function hello() {
  let name = await getName();
  return `hello ${name}`;
}

hello().then((v) => {
  console.log(v);
});

代码模块化

模块化,是代码组织的一种方式。通过模块化管理,代码组织结构更加清晰,可以大大提升可阅读性。

es6 中模块化管理代码的方式,需要注意点是:

  • 拆分后的代码需要进行 export 导出处理
  • 引用模块化代码使用 import 导入关键字

现在,我们就将上述案例中的,Person, Teacher 两个类进行模块化处理。分别创建新的代码文件:person.jsteacher.js.

将类代码拷贝到文件中,完成后,进行 export 导出处理。

export class Person {
  constructor(name) {
    this.name = name;
  }
  print() {
    console.log("person =>", this);
  }
}

再处理, teacher.js.

import { Person } from "./person.js";

export class Teacher extends Person {
  constructor(name, age) {
    super(name);
    this.age = age;
  }
  print() {
    console.log("teacher =>", this);
  }
}

因为,Teacher 类需要继承 Person,所以还需要 import 导入 Person 类定义。

module.js 文件中,进行具体类对象操作:

import { Person } from "./person.js";
import { Teacher } from "./teacher.js";

const person = new Person("tomy");
person.print();

const teacher = new Teacher("Jay", 35);
teacher.print();

注意

因为样例在浏览器中测试执行,所以在 import 其它模块时,文件名后缀不能省略。否则,浏览器会加载失败。

同时,在浏览器加载module模块时,需要显示指明脚本类型,为module; 因为默认情况下,浏览器是按 javascript 脚本进行加载。

<script type="module" src="module.js"></script>

命名空间与默认导出

在模块化处理代码的过程中,具体模块会导出一个 {}对象结构体, 所有导出的值、函数、或是类,都会在这个命名空间中。如在 person.js 中,

export class Person {
  constructor(name) {
    this.name = name;
  }
  print() {
    console.log("person =>", this);
  }
}

实际上,该模块导出的是:{ Person }.

所以,在模块引用时,使用的语法格式是 :

import { Person } from "person.js";

这种导出方式,称之为命名空间方式,即 namespace export。

如果不想使用这样的命名对象导出,我们可以通过 default 关键字,取代默认导出对象。

export default class Person {
  constructor(name) {
    this.name = name;
  }
  print() {
    console.log("person =>", this);
  }
}

此时,类 Person 就会作为默认对象导出。那么引用是,就可以是下面的语法格式:

import Person from "person.js";

后序

更详细的 ES6 语法只能仍需要大家参考官方的文档。本文只是帮助大家快速了解其关键及常用的语法。

← 快速简明教程: sass 快速简明教程: webpack →

评论