Skip to content

4.1 ES6 函数

函数参数的扩展

默认参数

基本用法

javascript
function fn(name,age=17){
 console.log(name+","+age);
}
fn("Amy",18);  // Amy,18
fn("Amy","");  // Amy,
fn("Amy");     // Amy,17

注意点:使用函数默认参数时,不允许有同名参数。

javascript
// 不报错
function fn(name,name){
 console.log(name);
}
 
// 报错
//SyntaxError: Duplicate parameter name not allowed in this context
function fn(name,name,age=17){
 console.log(name+","+age);
}

只有在未传递参数,或者参数为 undefined 时,才会使用默认参数,null 值被认为是有效的值传递。

javascript
function fn(name,age=17){
    console.log(name+","+age);
}
fn("Amy",null); // Amy,null

函数参数默认值存在暂时性死区,在函数参数默认值表达式中,还未初始化赋值的参数值无法作为其他参数的默认值。

javascript
function f(x,y=x){
    console.log(x,y);
}
f(1);  // 1 1
 
function f(x=y){
    console.log(x);
}
f();  // ReferenceError: y is not defined

不定参数

不定参数用来表示不确定参数个数,形如,...变量名,由...加上一个具名参数标识符组成。具名参数只能放在参数组的最后,并且有且只有一个不定参数。

基本用法

javascript
function f(...values){
    console.log(values.length);
}
f(1,2);      //2
f(1,2,3,4);  //4

箭头函数

箭头函数提供了一种更加简洁的函数书写方式。基本语法是:

参数 => 函数体

基本用法:

javascript
var f = v => v;
//等价于
var f = function(a){
 return a;
}
f(1);  //1

当箭头函数没有参数或者有多个参数,要用 () 括起来。

javascript
var f = (a,b) => a+b;
f(6,2);  //8

当箭头函数函数体有多行语句,用 {} 包裹起来,表示代码块,当只有一行语句,并且需要返回结果时,可以省略 {} , 结果会自动返回。

javascript
var f = (a,b) => {
 let result = a+b;
 return result;
}
f(6,2);  // 8

当箭头函数要返回对象的时候,为了区分于代码块,要用 () 将对象包裹起来

javascript
// 报错
var f = (id,name) => {id: id, name: name};
f(6,2);  // SyntaxError: Unexpected token :
 
// 不报错
var f = (id,name) => ({id: id, name: name});
f(6,2);  // {id: 6, name: 2}

注意点:没有 this、super、arguments 和 new.target 绑定。

javascript
var func = () => {
  // 箭头函数里面没有 this 对象,
  // 此时的 this 是外层的 this 对象,即 Window 
  console.log(this)
}
func(55)  // Window 
 
var func = () => {    
  console.log(arguments)
}
func(55);  // ReferenceError: arguments is not defined

箭头函数体中的 this 对象,是定义函数时的对象,而不是使用函数时的对象。

javascript
function fn(){
  setTimeout(()=>{
    // 定义时,this 绑定的是 fn 中的 this 对象
    console.log(this.a);
  },0)
}
var a = 20;
// fn 的 this 对象为 {a: 18}
fn.call({a: 18});  // 18

不可以作为构造函数,也就是不能使用 new 命令,否则会报错

适合使用的场景

ES6 之前,JavaScript 的 this 对象一直很令人头大,回调函数,经常看到 var self = this 这样的代码,为了将外部 this 传递到回调函数中,那么有了箭头函数,就不需要这样做了,直接使用 this 就行。

javascript
// 回调函数
var Person = {
    'age': 18,
    'sayHello': function () {
      setTimeout(function () {
        console.log(this.age);
      });
    }
};
var age = 20;
Person.sayHello();  // 20
 
var Person1 = {
    'age': 18,
    'sayHello': function () {
      setTimeout(()=>{
        console.log(this.age);
      });
    }
};
var age = 20;
Person1.sayHello();  // 18

所以,当我们需要维护一个 this 上下文的时候,就可以使用箭头函数。

不适合使用的场景

定义函数的方法,且该方法中包含 this

javascript
var Person = {
    'age': 18,
    'sayHello': ()=>{
        console.log(this.age);
      }
};
var age = 20;
Person.sayHello();  // 20
// 此时 this 指向的是全局对象
 
var Person1 = {
    'age': 18,
    'sayHello': function () {
        console.log(this.age);
    }
};
var age = 20;
Person1.sayHello();   // 18
// 此时的 this 指向 Person1 对象

需要动态 this 的时候

javascript
var button = document.getElementById('userClick');
button.addEventListener('click', () => {
     this.classList.toggle('on');
});

button 的监听函数是箭头函数,所以监听函数里面的 this 指向的是定义的时候外层的 this 对象,即 Window,导致无法操作到被点击的按钮对象。

4.3 ES6 Class 类

概述

在ES6中,class (类)作为对象的模板被引入,可以通过 class 关键字定义类。

class 的本质是 function。

它可以看作一个语法糖,让对象原型的写法更加清晰、更像面向对象编程的语法。

基础用法

类定义

类表达式可以为匿名或命名。

javascript
// 匿名类
let Example = class {
    constructor(a) {
        this.a = a;
    }
}
// 命名类
let Example = class Example {
    constructor(a) {
        this.a = a;
    }
}

类声明

javascript
class Example {
    constructor(a) {
        this.a = a;
    }
}

注意要点:不可重复声明。

javascript
class Example{}
class Example{}
// Uncaught SyntaxError: Identifier 'Example' has already been 
// declared
 
let Example1 = class{}
class Example{}
// Uncaught SyntaxError: Identifier 'Example' has already been 
// declared

注意要点

类定义不会被提升,这意味着,必须在访问前对类进行定义,否则就会报错。

类中方法不需要 function 关键字。

方法间不能加分号。

javascript
new Example(); 
class Example {}

类的主体

属性

prototype

ES6 中,prototype 仍旧存在,虽然可以直接自类中定义方法,但是其实方法还是定义在 prototype 上的。 覆盖方法 / 初始化时添加方法

javascript
Example.prototype={
    //methods
}

添加方法

javascript
Object.assign(Example.prototype,{
    //methods
})

静态属性

静态属性:class 本身的属性,即直接定义在类内部的属性( Class.propname ),不需要实例化。 ES6 中规定,Class 内部只有静态方法,没有静态属性。

javascript
class Example {
// 新提案
    static a = 2;
}
// 目前可行写法
Example.b = 2;

公共属性

javascript
class Example{}
Example.prototype.a = 2;

实例属性

实例属性:定义在实例对象( this )上的属性。

javascript
class Example {
    a = 2;
    constructor () {
        console.log(this.a);
    }
}

name 属性

返回跟在 class 后的类名(存在时)。

javascript
let Example=class Exam {
    constructor(a) {
        this.a = a;
    }
}
console.log(Example.name); // Exam
 
let Example=class {
    constructor(a) {
        this.a = a;
    }
}
console.log(Example.name); // Example

方法

constructor 方法

constructor 方法是类的默认方法,创建类的实例化对象时被调用。

javascript
class Example{
    constructor(){
      console.log('我是constructor');
    }
}
new Example(); // 我是constructor

返回对象

javascript
class Test {
    constructor(){
        // 默认返回实例对象 this
    }
}
console.log(new Test() instanceof Test); // true
 
class Example {
    constructor(){
        // 指定返回对象
        return new Test();
    }
}
console.log(new Example() instanceof Example); // false

静态方法

javascript
class Example{
    static sum(a, b) {
        console.log(a+b);
    }
}
Example.sum(1, 2); // 3

原型方法

javascript
class Example {
    sum(a, b) {
        console.log(a + b);
    }
}
let exam = new Example();
exam.sum(1, 2); // 3

实例方法

javascript
class Example {
    constructor() {
        this.sum = (a, b) => {
            console.log(a + b);
        }
    }
}

类的实例化

new

class 的实例化必须通过 new 关键字。

javascript
class Example {}
 
let exam1 = Example(); 
// Class constructor Example cannot be invoked without 'new'

实例化对象

共享原型对象

javascript
class Example {
    constructor(a, b) {
        this.a = a;
        this.b = b;
        console.log('Example');
    }
    sum() {
        return this.a + this.b;
    }
}
let exam1 = new Example(2, 1);
let exam2 = new Example(3, 1);
 
// __proto__ 已废弃,不建议使用
// console.log(exam1.__proto__ == exam2.__proto__); 
 
console.log(Object.getPrototypeOf(exam1) === Object.getPrototypeOf(exam2));// true
 
Object.getPrototypeOf(exam1).sub = function() {
    return this.a - this.b;
}
console.log(exam1.sub()); // 1
console.log(exam2.sub()); // 2

decorator

decorator 是一个函数,用来修改类的行为,在代码编译时产生作用。

类修饰

一个参数

第一个参数 target,指向类本身。

javascript
function testable(target) {
    target.isTestable = true;
}
@testable
class Example {}
Example.isTestable; // true

多个参数——嵌套实现

javascript
function testable(isTestable) {
    return function(target) {
        target.isTestable=isTestable;
    }
}
@testable(true)
class Example {}
Example.isTestable; // true

实例属性

上面两个例子添加的是静态属性,若要添加实例属性,在类的 prototype 上操作即可。

方法修饰

3个参数:target(类的原型对象)、name(修饰的属性名)、descriptor(该属性的描述对象)。

javascript
class Example {
    @writable
    sum(a, b) {
        return a + b;
    }
}
function writable(target, name, descriptor) {
    descriptor.writable = false;
    return descriptor; // 必须返回
}

修饰器执行顺序

由外向内进入,由内向外执行。

javascript
class Example {
    @logMethod(1)
    @logMethod(2)
    sum(a, b){
        return a + b;
    }
}
function logMethod(id) {
    console.log('evaluated logMethod'+id);
    return (target, name, desctiptor) => console.log('excuted         logMethod '+id);
}
// evaluated logMethod 1
// evaluated logMethod 2
// excuted logMethod 2
// excuted logMethod 1

封装与继承

getter / setter

定义

javascript
class Example{
    constructor(a, b) {
        this.a = a; // 实例化时调用 set 方法
        this.b = b;
    }
    get a(){
        console.log('getter');
        return this.a;
    }
    set a(a){
        console.log('setter');
        this.a = a; // 自身递归调用
    }
}
let exam = new Example(1,2); // 不断输出 setter ,最终导致 RangeError
class Example1{
    constructor(a, b) {
        this.a = a;
        this.b = b;
    }
    get a(){
        console.log('getter');
        return this._a;
    }
    set a(a){
        console.log('setter');
        this._a = a;
    }
}
let exam1 = new Example1(1,2); // 只输出 setter , 不会调用 getter 方法
console.log(exam1._a); // 1, 可以直接访问

getter 不可单独出现

javascript
class Example {
    constructor(a) {
        this.a = a; 
    }
    get a() {
        return this.a;
    }
}
let exam = new Example(1); // Uncaught TypeError: Cannot set property // a of #<Example> which has only a getter

getter 与 setter 必须同级出现

javascript
class Father {
    constructor(){}
    get a() {
        return this._a;
    }
}
class Child extends Father {
    constructor(){
        super();
    }
    set a(a) {
        this._a = a;
    }
}
let test = new Child();
test.a = 2;
console.log(test.a); // undefined
 
class Father1 {
    constructor(){}
    // 或者都放在子类中
    get a() {
        return this._a;
    }
    set a(a) {
        this._a = a;
    }
}
class Child1 extends Father1 {
    constructor(){
        super();
    }
}
let test1 = new Child1();
test1.a = 2;
console.log(test1.a); // 2

extends

通过 extends 实现类的继承。

javascript
class Child extends Father { ... }

super

子类 constructor 方法中必须有 super ,且必须出现在 this 之前。

javascript
class Father {
    constructor() {}
}
class Child extends Father {
    constructor() {}
    // or 
    // constructor(a) {
        // this.a = a;
        // super();
    // }
}
let test = new Child(); // Uncaught ReferenceError: Must call super 
// constructor in derived class before accessing 'this' or returning 
// from derived constructor

调用父类构造函数,只能出现在子类的构造函数。

javascript
class Father {
    test(){
        return 0;
    }
    static test1(){
        return 1;
    }
}
class Child extends Father {
    constructor(){
        super();
    }
}
class Child1 extends Father {
    test2() {
        super(); // Uncaught SyntaxError: 'super' keyword unexpected     
        // here
    }
}

调用父类方法, super 作为对象,在普通方法中,指向父类的原型对象,在静态方法中,指向父类

javascript
class Child2 extends Father {
    constructor(){
        super();
        // 调用父类普通方法
        console.log(super.test()); // 0
    }
    static test3(){
        // 调用父类静态方法
        return super.test1+2;
    }
}
Child2.test3(); // 3

注意要点

不可继承常规对象。

javascript
var Father = {
    // ...
}
class Child extends Father {
     // ...
}
// Uncaught TypeError: Class extends value #<Object> is not a constructor or null
 
// 解决方案
Object.setPrototypeOf(Child.prototype, Father);

4.4 ES6 模块

概述

在 ES6 前, 实现模块化使用的是 RequireJS 或者 seaJS(分别是基于 AMD 规范的模块化库, 和基于 CMD 规范的模块化库)。

ES6 引入了模块化,其设计思想是在编译时就能确定模块的依赖关系,以及输入和输出的变量。

ES6 的模块化分为导出(export) @与导入(import)两个模块。

特点

ES6 的模块自动开启严格模式,不管你有没有在模块头部加上 use strict;

模块中可以导入和导出各种类型的变量,如函数,对象,字符串,数字,布尔值,类等。

每个模块都有自己的上下文,每一个模块内声明的变量都是局部变量,不会污染全局作用域。

每一个模块只加载一次(是单例的), 若再去加载同目录下同文件,直接从内存中读取。

export 与 import

基本用法

模块导入导出各种类型的变量,如字符串,数值,函数,类。

  • 导出的函数声明与类声明必须要有名称(export default 命令另外考虑)。
  • 不仅能导出声明还能导出引用(例如函数)。
  • export 命令可以出现在模块的任何位置,但必需处于模块顶层。
  • import 命令会提升到整个模块的头部,首先执行。
javascript
/*-----export [test.js]-----*/
let myName = "Tom";
let myAge = 20;
let myfn = function(){
    return "My name is" + myName + "! I'm '" + myAge + "years old."
}
let myClass =  class myClass {
    static a = "yeah!";
}
export { myName, myAge, myfn, myClass }
 
/*-----import [xxx.js]-----*/
import { myName, myAge, myfn, myClass } from "./test.js";
console.log(myfn());// My name is Tom! I'm 20 years old.
console.log(myAge);// 20
console.log(myName);// Tom
console.log(myClass.a );// yeah!

建议使用大括号指定所要输出的一组变量写在文档尾部,明确导出的接口。

函数与类都需要有对应的名称,导出文档尾部也避免了无对应名称。

as 的用法

export 命令导出的接口名称,须和模块内部的变量有一一对应关系。

导入的变量名,须和导出的接口名称相同,即顺序可以不一致。

javascript
/*-----export [test.js]-----*/
let myName = "Tom";
export { myName as exportName }
 
/*-----import [xxx.js]-----*/
import { exportName } from "./test.js";
console.log(exportName);// Tom
使用 as 重新定义导出的接口名称,隐藏模块内部的变量
/*-----export [test1.js]-----*/
let myName = "Tom";
export { myName }
/*-----export [test2.js]-----*/
let myName = "Jerry";
export { myName }
/*-----import [xxx.js]-----*/
import { myName as name1 } from "./test1.js";
import { myName as name2 } from "./test2.js";
console.log(name1);// Tom
console.log(name2);// Jerry

不同模块导出接口名称命名重复, 使用 as 重新定义变量名。

import 命令的特点

只读属性:不允许在加载模块的脚本里面,改写接口的引用指向,即可以改写 import 变量类型为对象的属性值,不能改写 import 变量类型为基本类型的值。

javascript
import {a} from "./xxx.js"
a = {}; // error
 
import {a} from "./xxx.js"
a.foo = "hello"; // a = { foo : 'hello' }

单例模式:多次重复执行同一句 import 语句,那么只会执行一次,而不会执行多次。import 同一模块,声明不同接口引用,会声明对应变量,但只执行一次 import 。

javascript
import { a } "./xxx.js";
import { a } "./xxx.js";
// 相当于 import { a } "./xxx.js";
 
import { a } from "./xxx.js";
import { b } from "./xxx.js";
// 相当于 import { a, b } from "./xxx.js";

静态执行特性:import 是静态执行,所以不能使用表达式和变量。

javascript
import { "f" + "oo" } from "methods";
// error
let module = "methods";
import { foo } from module;
// error
if (true) {
  import { foo } from "method1";
} else {
  import { foo } from "method2";
}
// error

export default 命令

  • 在一个文件或模块中,export、import 可以有多个,export default 仅有一个。
  • export default 中的 default 是对应的导出接口变量。
  • 通过 export 方式导出,在导入时要加{ },export default 则不需要。
  • export default 向外暴露的成员,可以使用任意变量来接收。
javascript
var a = "My name is Tom!";
export default a; // 仅有一个
export default var c = "error"; 
// error,default 已经是对应的导出变量,不能跟着变量声明语句
 
import b from "./xxx.js"; // 不需要加{}, 使用任意变量接收

复合使用

:import() 是提案,这边暂时不延伸讲解。

export 与 import 可以在同一模块使用,使用特点:

  • 可以将导出接口改名,包括 default。
  • 复合使用 export 与 import ,也可以导出全部,当前模块导出的接口会覆盖继承导出的。
javascript
export { foo, bar } from "methods";
 
// 约等于下面两段语句,不过上面导入导出方式该模块没有导入 foo 与 bar
import { foo, bar } from "methods";
export { foo, bar };
 
/* ------- 特点 1 --------*/
// 普通改名
export { foo as bar } from "methods";
// 将 foo 转导成 default
export { foo as default } from "methods";
// 将 default 转导成 foo
export { default as foo } from "methods";
 
/* ------- 特点 2 --------*/
export * from "methods";