ES6 常用新特性讲解(下)

ES6 常用新特性讲解(下)

上篇:ES6 常用新特性讲解(上)

箭头函数

箭头函数无疑是 ES6 中一个相当重要的新特性。

特性

  • 共享父级 this 对象
  • 共享父级 arguments
  • 不能当做构造函数

语法

最简表达式

var arr = [1, 2, 3, 4, 5, 6];
// Before
arr.filter(function(v) {
    return v > 3;
});
// After
arr.filter(v => v > 3); // => [4, 5, 6]

前后对比很容易理解,可以明显看出箭头函数极大地减少了代码量。
完整语法

var arr = [1, 2, 3, 4, 5, 6];
arr.map((v, k, thisArr) => {
    return thisArr.reverse()[k] * v;
})  // => [6, 10, 12, 12, 10, 6]

一个简单的首尾相乘的算法,对比最简表达式我们可以发现,函数的前边都省略了 function 关键字,但是多个入参时需用括号包裹入参,单个入参是时可省略括号,入参写法保持一致。后面使用胖箭头 => 连接函数名与函数体,函数体的写法保持不变。

函数上下文 this

// Before
var obj = {
    arr: [1, 2, 3, 4, 5, 6],
    getMaxPow2: function() {
        var that = this,
            getMax = function() {
                return Math.max.apply({}, that.arr);
            };
        return Math.pow(getMax(), 2);
    }
}
// After
var obj = {
    arr: [1, 2, 3, 4, 5, 6],
    getMaxPow2: function() {
        var getMax = () => {
            return Math.max.apply({}, this.arr);
        }
        return Math.pow(getMax(), 2);
    }
}

注意看中第 5 行 var that = this 这里声明的一个临时变量 that。在对象或者原型链中,我们以前经常会写这样一个临时变量,或 that 或 _this,诸如此类,以达到在一个函数内部访问到父级或者祖先级 this 对象的目的。
如今在箭头函数中,函数体内部没有自己的 this,默认在其内部调用 this 的时候,会自动查找其父级上下文的 this 对象(如果父级同样是箭头函数,则会按照作用域链继续向上查找),这无疑方便了许多,我们无需在多余地声明一个临时变量来做这件事了。
注意

  1. 某些情况下我们可能需要函数有自己的 this,例如 DOM 事件绑定时事件回调函数中,我们往往需要使用 this 来操作当前的 DOM,这时候就需要使用传统匿名函数而非箭头函数。
  2. 在严格模式下,如果箭头函数的上层函数均为箭头函数,那么 this 对象将不可用。

另,由于箭头函数没有自己的 this 对象,所以箭头函数不能当做构造函数。

父级函数 arguments

我们知道在函数体中有 arguments 这样一个伪数组对象,该对象中包含该函数所有的入参(显示入参 + 隐式入参),当函数体中有另外一个函数,并且该函数为箭头函数时,该箭头函数的函数体中可以直接访问父级函数的 arguments 对象。

function getSum() {
    var example = () => {
        return Array
            .prototype
            .reduce
            .call(arguments, (pre, cur) => pre + cur);
    }
    return example();
}
getSum(1, 2, 3); // => 6

由于箭头函数本身没有 arguments 对象,所以如果他的上层函数都是箭头函数的话,那么 arguments 对象将不可用。

最后再巩固一下箭头函数的语法:

  1. 当箭头函数入参只有一个时可以省略入参括号;
  2. 当入参多余一个或没有入参时必须写括号;
  3. 当函数体只有一个 return 语句时可以省略函数体的花括号与 return 关键字。

类 & 继承

类也是 ES6 一个不可忽视的新特性,虽然只是句法上的语法糖,但是相对于 ES5,学习 ES6 的类之后对原型链会有更加清晰的认识。

特性

  • 本质为对原型链的二次包装
  • 类没有提升
  • 不能使用字面量定义属性
  • 动态继承类的构造方法中 super 优先 this

类的定义

/* 类不会被提升 */
let puppy = new Animal('puppy'); // => ReferenceError
class Animal {
    constructor(name) {
        this.name = name;
    }
    sleep() {
        console.log(`The ${this.name} is sleeping...`);
    }
    static type() {
        console.log('This is an Animal class.');
    }
}
let puppy = new Animal('puppy');
puppy.sleep();    // => The puppy is sleeping...
/* 实例化后无法访问静态方法 */
puppy.type();     // => TypeError
Animal.type();    // => This is an Animal class.
/* 实例化前无法访问动态方法 */
Animal.sleep();   // => TypeError
/* 类不能重复定义 */
class Animal() {} // => SyntaxError

以上我们使用 class 关键字声明了一个名为 Animal 的类。

虽然类的定义中并未要求类名的大小写,但鉴于代码规范,推荐类名的首字母大写。

两点注意事项:

  1. 在类的定义中有一个特殊方法 constructor(),该方法名固定,表示该类的构造函数(方法),在类的实例化过程中会被调用(new Animal(‘puppy’));
  2. 类中无法像对象一样使用 prop: value 或者 prop = value 的形式定义一个类的属性,我们只能在类的构造方法或其他方法中使用 this.prop = value 的形式为类添加属性。

最后对比一下我们之前是怎样写类的:

function Animal(name) {
    this.name = name;
}
Animal.prototype = {
    sleep: function(){
        console.log('The ' + this.name + 'is sleeping...');
    }
};
Animal.type = function() {
    console.log('This is an Animal class.');
}

class 关键字真真让这一切变得清晰易懂了~

类的继承

class Programmer extends Animal {
    constructor(name) {
        /* 在 super 方法之前 this 不可用 */
        console.log(this); // => ReferenceError
        super(name);
        console.log(this); // Right!
    }
    program() {
        console.log("I'm coding...");
    }
    sleep() {
        console.log('Save all files.');
        console.log('Get into bed.');
        super.sleep();
    }
}
let coder = new Programmer('coder');
coder.program(); // => I'm coding...
coder.sleep();   // => Save all files. => Get into bed. => The coder is sleeping.

这里我们使用 class 定义了一个类 Programmer,使用 extends 关键字让该类继承于另一个类 Animal。
如果子类有构造方法,那么在子类构造方法中使用 this 对象之前必须使用 super() 方法运行父类的构造方法以对父类进行初始化。
在子类方法中我们也可以使用 super 对象来调用父类上的方法。如示例代码中子类的 sleep() 方法:在这里我们重写了父类中的 sleep() 方法,添加了两条语句,并在方法末尾使用 super 对象调用了父类上的 sleep() 方法。
俗话讲:没有对比就没有伤害 (*゜ー゜*),我们最后来看一下以前我们是怎么来写继承的:

function Programmer(name) {
    Animal.call(this, name);
}
Programmer.prototype = Object.create(Animal.prototype, {
    program: {
        value: function() {
            console.log("I'm coding...");
        }
    },
    sleep: {
        value: function() {
            console.log('Save all files.');
            console.log('Get into bed.');
            Animal.prototype.sleep.apply(this, arguments);
        }
    }
});
Programmer.prototype.constructor = Programmer;

如果前文类的定义中的前后对比不足为奇,那么这个。。。
给你一个眼神,自己去体会 (⊙ˍ⊙),一脸懵逼.jpg

模块

啊哈,终于写到最后一部分了。

模块系统是一切模块化的前提,在未推出 ES6 Module 标准之前,相信大伙儿已经被满世界飞的 AMD、CMD、UMD、CommonJS 等等百花齐放的模块化标准搞的晕头转向了吧。但是,现在 TC39 在 ECMAScript2015(ES6) 版本里终于推出了正式的模块化规范,前端模块系统的大一统时代已经到来了!

OMG,这段话写的好燃 orz

废话有点多。。。
下面咱们来了解一个这个模块系统的基本规范。

为方便描述,下文中导出对象指一切可导出的内容(变量、函数、对象、类等),勿与对象(Object)混淆。
导入对象同理。

特性

  • 封闭的代码块

每个模块都有自己完全独立的代码块,跟作用域类似,但是更加封闭。

  • 无限制导出导出

一个模块理论上可以导出无数个变量、函数、对象属性、对象方法,甚至一个完整的类。但是我们应该时刻牢记**单一职责**这一程序设计的基本原则,不要试图去开发一个臃肿的巨大的面面俱到的模块,合理控制代码的颗粒度也是开发可维护系统必不可少的一部分。

  • 严格模式下运行

模块默认情况下在严格模式下运行(‘use strict;’),这时候要注意一些取巧甚至有风险的写法应该避免,这也是保证代码健壮性的前提。

模块的定义与导出

内联导出

export const DEV = true;
export function example() {
    //...
}
export class expClass {
    //...
}
export let obj = {
    DEV,
    example,
    expClass,
    //...
}

使用 export 关键字,后面紧跟声明关键字(let、function 等)声明一个导出对象,这种声明并同时导出的导出方式称作内联导出
未被导出的内容(变量、函数、类等)由于独立代码块的原因,将仅供模块内部使用(可类比成一种闭包)。
对象导出

// module example.js
const DEV = true;
function example() {
    //...
}
class expClass {
    //...
}
let obj = {
    DEV,
    example,
    expClass,
    //...
}
// module example.js
export {DEV, example, expClass, obj};
export {DEV, example as exp, expClass, obj};

相对于内联导出,上边的这种方式为对象导出。我们可以像写普通 JS 文件一样写主要的功能逻辑,最后通过 export 集中导出。
在导出时我们可以使用 as 关键字改变导出对象的名称。
默认导出

export default {DEV, example as exp, expClass, obj};
// OR
export default obj;
// OR
export default const DEV = true;

我们可以在 export 关键字后接 default 来设置模块的默认导出对象,需要注意的是:一个模块只能有一个默认导出
先不多说,后面讲导入的时候再细讲相互之间的关联。

模块的导入与使用

前文我们定义了一个名为 example 的模块,写在文件 example.js中,下面我们来导入并使用这个模块。

import example from './example.js';
// OR
import default as example from './example.js';

使用 import 关键字导入一个模块,上边这两种写法是等效的。默认导入对象既是模块默认导出对象,即对应模块定义中的 export default 所导出的内容。
此外我们还可以这样导入一个模块:

import {DEV, example} from './example.js';
import * as exp from './example.js';
import {default as expMod, * as expAll, DEV, example as exp} from './example.js';

这种导入方式对应模块定义中的 export {DEV, example, expClass, obj}export const DEV = true。下面我们逐行分析:
第一行,我们使用对象导入的方式导入一个模块内容,可能有些人已经发现,这跟解构赋值很相似,但也有不同,下面会讲到。需要注意的是形参对象({DEV, example})与模块定义中导出的名称必须保持一致
第二行,导入时可以使用通配符 * 配合 as 关键字一次性导出模块中所有内容,最终导入的内容放在 exp 对象中。
第三行,在使用对象导入来导入一个模块的指定内容时,也可以使用 as 关键字更改最终导入对象的名称,这里表现出与解构赋值的一个不同之处,忘记解构赋值的小伙伴可以翻翻前文对比一下哈~
最后,在导入一个模块后我们就可以直接使用模块的函数、变量、类等了,完整的代码示例:

import {DEV, example, expClass as EC} from './example.js';
if(DEV) {
    let exp = new EC();
    // anything you want...
    example();
}

好嘞!到这里,ES6 常用的 8 个新特性就讲完了,恭喜你耐心地看完了。

原文标题: ES6 常用新特性讲解(下)

原文出处: 知乎专栏

原文链接: https://zhuanlan.zhihu.com/p/26590942

发表评论

登录后才能评论