在ES5中,因为没有类的概念,因此若是要使用面向对象编程,就须要利用原型继承的方式。一般是建立一个构造器,而后将方法指派到该构造器的原型上。
就像这样:javascript
function Cat(name) {
this.name = name;
}
Cat.prototype.speak = function() {
console.log('Mew!');
}复制代码
ES6引入了class
关键字后就再也不须要这样作了。不过须要明白的是ES6中的类仅仅是以上面这种方式做为基础的一个语法糖而已。
ES6中类声明已class
关键字开始,其后是类的名称;剩余部分的语法部分看起来就像对象字面量中的方法简写,而且在方法之间不须要使用逗号。同时容许你在其中使用特殊的 constructor 方法名称直接定义一个构造器,而不须要先定义一个函数再把它看成构造器使用。前端
class Cat {
constructor(name) {
this.name = name;
}
speak() {
console.log('Mew!');
}
}复制代码
虽然ES6的类声明是ES5方式的一个语法糖,可是与之相比,仍是存在一些区别的。java
自有属性须要在类构造器中建立,而类还容许你在原型上定义访问器属性。webpack
class Person {
constructor(name, age) {
this.age = age;
this.name = name;
}
get firstName() {
return this.name.split(' ')[0];
}
set firstName(value) {
let lastName = this.name.split(' ')[1];
this.name = value + ' ' + lastName;
}
}
let person = new Person('Michael Jackson', 35);
console.log(person.firstName); //'Michael'
person.firstName = 'Marry';
console.log(person.name); // 'Marry Jackson'复制代码
在读取访问器属性的时候,会调用getter方法,而写入值的时候,会调用setter方法。这相似于ES5中使用Object.definePropery
的方法。web
静态成员在ES5中通常是直接定义在构造器上的,如:编程
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.createAdult = function(name) {
return new Person(name, 18);
};复制代码
而在ES6中提供了static
关键字简化了声明静态成员的方式:promise
class Person {
constructor(name, age) {
this.age = age;
this.name = name;
}
static createAdult(name) {
return new Person(name, 18);
}
}复制代码
ES5中实现继承的方式有不少种,可是若是要实现严格的继承,步骤较为繁琐。为了简化继承的关系,ES6中使用类让这项工做变得更简单。若是你熟悉面向对象语言,如java等,那么extends这个关键你必定不会陌生。一样的,在ES6中使用extends
关键字来指定当前类所须要继承的函数便可。生成的类的原型会被自动调整,而你还能调用 super() 方法来访问基类的构造器。浏览器
class Person {
constructor(country) {
this.country = country;
}
}
class Chinese extends Person{
constructor() {
super('China');
}
speak() {
console.log('I come from ' + this.country);
}
}复制代码
派生类中的方法老是会屏蔽基类中的同名方法,所以,若是你须要使用父类中定义的方法的话,可使用super
关键字来进行访问。如:异步
class Person {
constructor(country) {
this.country = country;
}
speak() {
console.log('I come from ' + this.country);
}
}
class Chinese extends Person{
constructor() {
super('China');
}
speak() {
super.speak();
console.log('I am a Chinese');
}
}
const chinese = new Chinese();
chinese.speak();
//I come from China.
//I am a Chinese.复制代码
另外一个在ES6中比较高级的地方是,能够从表达式中派生出类来:async
let SerializableMixin = {
serialize() {
return JSON.stringify(this);
}
};
let AreaMixin = {
getArea() {
return this.length * this.width;
}
};
//混入
function mixin(...mixins) {
var base = function() {};
Object.assign(base.prototype, ...mixins);
return base;
}
class Square extends mixin(AreaMixin, SerializableMixin) {
constructor(length) {
super();
this.length = length;
this.width = length;
}
}
var x = new Square(3);
console.log(x.getArea()); // 9
console.log(x.serialize()); // "{"length":3,"width":3}"复制代码
利用extends
继承内置对象的时候,容易出现的一个问题是会返回内置对象实例的方式,在继承后会返回子类的实例。如:
class SubArray extends Array {
}
const subArr = new SubArray(1,2,3);
const filteredArr = subArr.filter(value => value > 1);
console.assert(filteredArr instanceof SubArray); //true复制代码
若是须要想让其返回实例类型是Array
能够利用Symbol.species
这个符号来处理:
class SubArray extends Array {
//这里使用static,代表是静态访问器属性
static get [Symbol.species]() {
return Array;
}
}复制代码
利用以前介绍的new.target
能够实现一个抽象类,原理就是当用户调用new
直接建立实例的时候,抛出错误。:
class BaseClass {
constructor() {
if(new.target === BaseClass) {
throw new Error('该类不能直接实例化')
}
}
}复制代码
随着项目的规模愈来愈大,如今模块化已经成为开发过程当中必备的流程。以前,咱们可能借助RequireJS等工具进行模块化管理,而如今ES6已经提供了模块系统。
先来了解一下基本语法:
模块( Modules )本质上就是 包含JS 代码的文件。在一个js文件中,你可使用export
关键字,将代码公开给其余模块。
// sayHello.js
export function sayHello() {
console.log('hello');
}
// funcs.js
export function fun1() { .... }
export function func2() { .... }
export const value1 = 'value1';复制代码
如上面的例子中所示,你能够在文件中导出全部的最外层函数
、类
以及var
、let
或const
声明的变量。而这些导出的变量或公开部分则能够被其余文件利用import
语法进行导入后引用。
//单个导入
import {sayHello} from './sayHello.js';
//多个导入
import {func1, func2} from './funs.js';
sayHello(); // hello复制代码
为了确保浏览器与Node.js之间保持良好的兼容性,建议使用相对路径的写法。
若是须要将整个模块当作单一的对象进行导入,可使用*
通配符:
//使用as关键字为导出对象设置别名,模块中全部导出都将做为属性存在
import * as funcs from './funcs.js';
funcs.func1();
funsc.func2();复制代码
若是不想用原来模块中的命名,能够经过as
关键字来指定别名。
//as前面为模块原先的名称,后面是别名,使用别名后sayHello为undefined
import { sayHello as say } from './sayHello.js';
say();复制代码
你可使用export
关键字来导出默认模块:
// sayHello.js
export default function() {
console.log('hello');
}
// main.js
import sayHello from './sayHello.js';
sayHello();复制代码
能够注意到,这里默认导出的时候,不须要使用花括号,而直接为其命名便可。这种写法也较为简洁。当一个文件中,同时存在默认导出模块和非默认导出模块的时候,导出的时候,默认导出模块须要写在前面,例如:
import sayHello,{ func1 } from './sayHello.js'; //此处略去导出过程
//或者使用以下方式
import {default as sayHello, func1} from './sayHello.js';复制代码
当一个文件中没有使用export
语句进行导出的时候,其实咱们仍是能够import
进行导入的。一般是被用于建立polyfill与shim的时候。
//sayHello.js
const name = 'scq000';
function sayHello() {
console.log('hello');
}
// main.js
import './sayHello.js';
sayHello();
console.log(name);复制代码
虽说如今在项目中一般都使用webpack来处理模块代码,但也须要知道其余加载模块的方式。
你可使用<script type="module">
的方式进行模块的加载,默认浏览器会采用defer
属性,一旦页面文档彻底被解析后,模块就会按次序执行。若是须要异步加载的话,能够加上async
关键字。
另外,若是是使用Web Worker或Server Worker之类的worker的话,能够经过下面这种方式加载模块:
let worker = new Worker('module.js', { type: 'module' });复制代码
迭代器和生成器一般是一块儿来使用的。迭代器的目的是为了更加方便地遍历对象,而生成器用来生成可迭代的对象。使用迭代器的过程当中,你能够结合for...of
语句以及...
扩展符来遍历对象的值。
在ES6中,迭代器是专门用来设计迭代的对象,带有特殊的接口。全部的迭代器都带有next
方法,用来返回一个结果。这里咱们来手工实现一个迭代器:
function createIterator() {
var i = 0;
return {
next() {
var done = false;
var value;
if (i < 3) {
value = i * 2;
i++;
} else {
done = true;
value = undefined;
}
return { value: value, done: done }
}
}
}
let iterator = new createIterator();
iterator.next(); // {value: 0, done: false}
iterator.next(); // {value: 0, done: false}
iterator.next(); // {value: 4, done: false}
iterator.next(); // {value: undefined, done: true}复制代码
集合对象(Set、Map、Array)提供了三种内置的迭代器:entries,keys,values,这三个方法都会返回一个迭代器,用来方便地获取键值对等信息。ES6中定义了可迭代对象(iterable object),如Set、Map、Array以及字符串等均可以利用for...of
语法来进行遍历操做。原理其实就是调用它们内置的默认迭代器。对于用户自定义的对象,若是也要让它们支持for...of
语法,则须要去定义Symbol.iterator
属性。具体例子,能够查看符号那一部分的内容。
生成器(generator)是可以返回迭代器的函数。一般定义的时候,咱们会利用function
关键字以后的(*)号表示,使用yield
语句输出每一次的数据。
function *getNum() {
yield 1;
yield 2;
yield 3;
}
const nums = getNum();
for(let num of nums) {
console.log(num);
}
//1,2,3复制代码
这部分的内容我在前端的异步解决方案之Promise和Await/Async中有详细的阐述,若是感兴趣的能够看一下。
为了让开发者可以建立内置对象,ES6经过代理( proxy )的方式暴露了对象上的内部工做。使用代理可以拦截并改变 JS 引擎的底层操做,如日志、对象虚拟化等。而反射( reflect )则是反映了对底层的默认行为操做。
接下来这个例子,将演示如何利用代理和反射的方式对对象的内置行为作修改:
//要修改的默认对象
let target = {
name: 'scq000',
age: 23
};
//代理对象
let proxy = new Proxy(target, {
has(trapTarget, key) {
if(key === 'age') {
return false;
}else {
//调用默认的行为
return Reflect.has(trapTarget, key);
}
}
});
console.log('value' in proxy); //true
console.log('age' in proxy); //false复制代码
能够看到,上面这个例子使用代理对象拦截了in
操做符的默认行为并做出了修改。has
这个方法称做陷阱函数,它可以响应对in
操做的访问操做。trapTarget
则是这个函数的目标对象,has
方法接受一个额外的参数key
是对应着须要检查的属性。一旦检查到属性名为age
,则返回false
,这样就能隐藏这个属性。
如下是一些经常使用的代理陷阱以及反射所对应的默认行为:
代理陷阱 | 被重写的行为 | 默认行为 |
---|---|---|
get/set | 读取/写入一个属性值 | Reflect.get/Reflect.set |
has | in运算符 | Reflect.has |
deleteProperty | delete运算符 | Reflect.deleteProperty |
getPropertyOf/setPropertyOf | Object.getPropertyOf/setPropertyOf | Reflefct.getPropertyOf/setPropertyOf |
目前,反射和代理在浏览器上还不支持,主要仍是用在NodeJS编程上。这一部分的功能在实际开发中并非特别经常使用,所以,这里不作过多介绍。若是感兴趣的话,能够自行查找相关文档。