es6语言特性的总结(3)

es6语言特性的总结(1)在这里
es6语言特性的总结(2)在这里

在ES5中,由于没有类的概念,所以如果要使用面向对象编程的方式,就需要利用原型继承的方式。通常是创建一个构造器,然后将方法指派到该构造器的原型上。
就像这样:

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!');
  }
}

类声明与ES5仿类的区别

虽然ES6的类声明是ES5方式的一个语法糖,但是与之相比,还是存在一些区别的。

  1. 类声明不会被提升,这与函数定义不同。类声明的行为与 let 相似,因此在程序的执行到达声明处之前,类会存在于暂时性死区内。
  2. 类声明中的所有代码会自动运行在严格模式下,并且也无法退出严格模式。
  3. 类的所有方法都是不可枚举的,这是对于自定义类型的显著变化,后者必须用 Object.defineProperty() 才能将方法改变为不可枚举。
  4. 类的所有方法内部都没有 [[Construct]] ,因此使用 new 来调用它们会抛出错误。
  5. 调用类构造器时不使用 new ,会抛出错误。
  6. 试图在类的方法内部重写类名,会抛出错误。

访问器属性

自有属性需要在类构造器中创建,而类还允许你在原型上定义访问器属性。

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的方法。

静态成员

静态成员在ES5中一般是直接定义在构造器上的,如:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.createAdult = function(name) {
  return new Person(name, 18);
};

而在ES6中提供了static关键字简化了声明静态成员的方式:

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中比较高级的地方是,可以从表达式中派生出类来:

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';

如上面的例子中所示,你可以在文件中导出所有的最外层函数以及varletconst声明的变量。而这些导出的变量或公开部分则可以被其他文件利用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与异步编程

这部分的内容我在前端的异步解决方案之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编程上。这一部分的功能在实际开发中并不是特别常用,因此,这里不做过多介绍。如果感兴趣的话,可以自行查找相关文档。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容

  • 一、ES6简介 ​ 历时将近6年的时间来制定的新 ECMAScript 标准 ECMAScript 6(亦称 ...
    一岁一枯荣_阅读 6,037评论 8 25
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • 官方中文版原文链接 感谢社区中各位的大力支持,译者再次奉上一点点福利:阿里云产品券,享受所有官网优惠,并抽取幸运大...
    HetfieldJoe阅读 3,643评论 2 27
  • 以下内容是我在学习和研究ES6时,对ES6的特性、重点和注意事项的提取、精练和总结,可以做为ES6特性的字典;在本...
    科研者阅读 3,095评论 2 9
  • 无边的黑夜里,只有角落木柜旁有一束手机的光亮,男人靠在那,用无比沙哑难以追溯根源的缥缈的声音一边又一遍唱着那首歌。...
    楠欤阅读 406评论 4 3