NodeJS系列(4)- ECMAScript 6 (ES6) 语法(二)

发布时间 2023-06-26 19:45:07作者: 垄山小站


在 “NodeJS系列(3)- ECMAScript 6 (ES6) 语法(一)” 里,我们介绍并演示 let、const、Symbol 等 ES6 语法和概念。

本文在 “NodeJS系列(2)- NPM 项目 Import/Export ES6 模块” 的 npmdemo  项目的基础上,继续介绍并演示 函数扩展、类 等 ES6 语法和概念。

NodeJS ES6:https://nodejs.org/en/docs/es6
ECMA:https://www.ecma-international.org/publications-and-standards/standards/ecma-262/

 

1. 函数扩展

    ES6 关于函数扩展部分,主要涉及以下四个方面:参数默认值、rest参数、扩展运算符和箭头函数。

    1) 参数默认值

        ES6 函数的参数默认值,即函数的参数定义时赋值。设置了默认值的参数,不能用 let 或 const 在函数体内再次声明。

        通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是无法省略的。

        调用函数时,传入的参数值为 undefined,将触发该参数等于默认值,传入 null 不会触发。

 

    2) rest 参数

        rest 参数 “...变量名” (变量名前是三个点),用于获取函数的多余参数,这样就不需要使用 arguments 对象了。格式如下:

            function f(a, ...b) {

            }

        b 就是一个 rest 参数,b 是一个数组, b 之后不能再有其他参数。  

 

    3) 扩展运算符(spread)

        扩展运算符(spread)是三个点(...),它类似 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。


    4) 箭头函数

        JavaScript中 的 this 指向一个对象,this 的指向取决于 this 所在的位置。具体指向如下:

            (1) 在函数外单独使用,this 指向全局对象。
            (2) 在函数中,this 指向全局对象。
            (3) 在函数中,严格模式下 ("use strict") ,this 是未定义的 (undefined)。
            (4) 在对象的方法中,this 指向该方法所属的对象。
            (5) 在事件的回调函数中,this 指向接收事件的元素。

            注:使用 call() 和 apply() 方法可以将 this 指向其它对象。
                
                Nodejs 服务端和浏览器 Javascript 中的全局对象不一样,浏览器的全局对象是 Window,Nodejs 服务端的全局对象是 global,这里讨论 NodeJS 服务端运行时的 this。

        箭头函数根据当前的词法作用域而不是根据 this 机制顺序来决定 this,箭头函数会继承外层函数调用的 this 绑定,而无论 this 绑定到什么。

    示例,创建 D:\workshop\nodejs\npmdemo\es6_7.js 文件,内容如下

        // 参数默认值
        var f1 = function (a = 3, b = 4) {
            console.log(a, b)
        }

        var f2 = function (x, y = 8) {
            console.log(x, y)
        }

        f1()
        f1(undefined, null)
        f1(5)
        f1(5, 6)
        f1(null, 6)

        f2()
        f2(7)

        console.log((f1).length)   // 函数的 length 属性,返回没有指定默认值的参数个数
        console.log((f2).length)   

        console.log('--------------------------------------------------------------')

        //  rest 参数
        var f3 = function(... args) {
            console.log(args)
        }
        var f4 = function(a, ... args) {
            console.log(a, args)
        }

        f3()
        f3(1, 2, 3)
        f4(9, 5, 'A', 6)

        console.log((f3).length)  // 函数的 length 属性,不包括 rest 参数
        console.log((f4).length)

        console.log('--------------------------------------------------------------')

        // 扩展运算符
        var f5 = function(a, b, c) {
            console.log(a, b, c)
        }
        var arr1 = [11, 12, 13]
        
        f5(arr1)
        f5(...arr1)

        var arr2 = [3, 4, 5];
        arr1.push(...arr2)     // push 方法简化
        console.log(arr1)
        
        var str = 'hello'
        console.log([...str]) // 字符串转为数组

        console.log('--------------------------------------------------------------')

        // 箭头函数
        var a = 1
        var obj = {
            a: 2,
            f6: function() {
                // 返回一般函数
                return (function() {
                    console.log(this)
                    //console.log(this.a)   // 报错,this 的值为 undefined
                })
            },
            f7: function() {
                // 返回箭头函数
                return (() => {
                    console.log(this.a)     // this 指向 obj
                })  
            }
        }
        obj.f6()()
        obj.f7()()

 

    运行

        D:\workshop\nodejs\npmdemo> node es6_7

            3 4
            3 null
            5 4
            5 6
            null 6
            undefined 8
            7 8
            0
            1
            --------------------------------------------------------------
            []
            [ 1, 2, 3 ]
            9 [ 5, 'A', 6 ]
            0
            1
            --------------------------------------------------------------
            [ 11, 12, 13 ] undefined undefined
            11 12 13
            [ 11, 12, 13, 3, 4, 5 ]
            [ 'h', 'e', 'l', 'l', 'o' ]
            --------------------------------------------------------------
            undefined
            2

 


2. 类 (Class)

    在 ES6 中,class (类)作为对象的模板被引入,可以通过 class 关键字定义类。class 的本质是 function,它可以看作一个语法糖 (Syntactic sugar,也译为糖衣语法),让对象原型的写法更加清晰、更类似面向对象编程的语法。

    1) 基本概念

        JavaScript 语言中,生成实例对象的传统方法是通过构造函数。格式如下:

            // 构造函数
            function F1(a) {
                this.a = a
            }

            // 函数默认包含 constructor 属性,可以使用 Object.getOwnPropertyNames() 查看
            console.log(Object.getOwnPropertyNames(F1.prototype))    // 输出:[ 'constructor' ]

            F1.prototype.toString = function () {
                console.log(this.a)
            };

            var f1 = new F1(5);
            f1.toString()    // 输出:5


        用 ES6 提供的 Class 改写以上代码,格式如下: 

            class C1 {
                // 构造函数
                constructor(a) {
                    this.a = a
                }
            
                toString() {
                    console.log(this.a)
                }
            }

            var c1 = new C1(5);
            c1.toString()        // 输出: 5


        以上代码定义了一个 “类”,constructor() 是构造函数(或构造方法),this 关键字代表类的实例对象。定义方法 toString(),不需要加上 function 关键字。

        在 JavaScript 中,函数和类都是 function 类型,包含属性和方法。ES6 类中 prototype 继续存在,prototype 是 function 类型对象的一个属性,在函数和类定义时默认包含了 prototype,它的初始值是一个空对象。

            console.log(typeof(F1))     // 输出:function
            console.log(typeof(C1))     // 输出:function
            console.log(C1.prototype)   // 输出:{}
            console.log(C1.constructor) // 输出:[Function: Function]


        类 (class) 的实例化需要通过 new 关键字来实现。

    2) 属性

        (1) name 属性  

            命名类的 name 属性,等于类的名称,匿名类的 name 属性,等于匿名类所赋值的变量名称。           

            let f2 = class F2 {

            }
            console.log(f2.name)   // 输出: F2
            
            let f3 = class {
            
            }
            console.log(f3.name)  // 输出: f3


        (2) 原型属性

            原型属性也称为共享属性,或公有属性,定义在类的 prototype 上。格式如下:

                class C2 {

                    // 构造函数
                    constructor(a, b) {

                        // 原型属性
                        C2.prototype.a = a

                        // 实例属性
                        this.b = b
                    }                   

                    toString1() {
                        console.log(C2.prototype.a + ', ' + C2.prototype.b)
                        
                    }
                    
                    toString2() {
                        console.log(this.a + ', ' + this.b)
                    }
                }

                let c2a = new C2(1, 2)
                c2a.toString1()   // 输出:1, undefined
                c2a.toString2()   // 输出:1, 2

                let c2b = new C2(3, 4)
                c2b.toString1()   // 输出:3, undefined
                c2b.toString2()   // 输出:3, 4

                c2a.toString1()   // 输出:3, undefined
                c2a.toString2()   // 输出:3, 2

             注:实例属性不存在时,自动查询同名原型属性。
            
        (3) 实例属性

            实例属性也称为私有属性,定义在实例对象(this)上。格式如下:

                class C3 {

                    // 实例属性
                    a = 1

                    constructor (a) {
                        if (a != undefined)
                            this.a = a
                    }

                    toString1() {
                        console.log(this.a)
                    }

                    toString2() {
                        console.log(C3.prototype.a)
                    }
                }

                let c3a = new C3(5)
                let c3b = new C3(6)
                let c3c = new C3()

                c3a.toString1()    // 输出: 5
                c3b.toString1()    // 输出: 6
                c3c.toString1()    // 输出: 1

                c3a.toString2()    // 输出: undefined


        (4) 静态属性

            class 本身的属性,即直接定义在类内部的属性( Class.propname ),不需要实例化。格式如下:

                class C4 {
                    static a = 99

                    toString1() {
                        console.log(C4.a)
                    }

                    toString2() {
                        console.log(this.a)
                    }

                    toString3() {
                        console.log(C4.prototype.a)
                    }
                }

                console.log(C4.a)   // 输出: 99

                C4.a = 88
                let c4 = new C4()
                c4.toString1()      // 输出: 88
                c4.toString2()      // 输出: undefined
                c4.toString3()      // 输出: undefined 


    3) 方法

        (1) constructor 方法

            constructor 也称为构造函数,是类的默认方法,通过 new 命令创建对象实例时,自动调用该方法。
            
            每个类都至少有且仅有一个 constructor 方法,如果没有显示定义,默认会自动添加一个隐式无参数的 constructor 方法。
            
            constructor 方法默认返回实例对象,即 this,也可以指定返回对象。

        (2) 原型方法

            在类里定义的方法(比如 toString()),系统自动会把该方法添加到类的 prototype 上。格式如下:

                class C5 {
                    toString(s) {
                        console.log(s)
                    }
                }
              
                let c5 = new C5()
                c5.toString('c5.toString()')      // 输出: c5.toString()
                console.log(Object.getOwnPropertyNames(C5.prototype))          // 输出:[ 'constructor', 'toString' ]
                C5.prototype.toString('C5.prototype.toString()')            // 输出:C5.prototype.toString()

                注:原型方法不论是否实例化类,都可以调用。可以使用 Object.getOwnPropertyNames()查看 prototype 上的属性和方法。

        (3) 实例方法

            实例方法定义在实例对象(this)上。格式如下:

                class C6 {
                    constructor() {
                        this.toString = (s) => {
                            console.log(s);
                        }
                    }
                }

                C6.toString2 = function(s) {
                    console.log('2: ' + s)
                }


                let c6 = new C6()
                c6.toString('c6.toString()')      // 输出: c6.toString()
                c6.toString('c6.toString2()')      // 输出: c6.toString2()
                console.log(Object.getOwnPropertyNames(C6.prototype))           // 输出:[ 'constructor' ]

                注:实例方法只能实例化类后,才能调用。

        (4) 静态方法

            静态方法定义在类上,在静态方法里只能访问静态属性。

            class C7 {
                static a = 3
                b = 5

                static toString() {
                    console.log(this.a + ", " + this.b)
                }
            }

            C7.toString()      // 输出: C7.toString()

            let c7 = new C7()
            console.log(Object.getOwnPropertyNames(c7))         // 输出: c7.toString()
            console.log(Object.getOwnPropertyNames(C7.prototype))           // 输出:[ 'constructor' ]


    4) 封装

        类的封装主要是对类的实例属性(或私有属性)进行封装,使对象外的代码只能通过 getter 和 setter 方法来读写对象的实例属性。

        ES6 提供了使用 get 和 set 关键字定义 getter 和 setter 的特定语法,get 与 set 在一个类里必须同时出现。格式如下:

            class Person {
                constructor(name) {
                    console.log('constructor(1) -> name = ' + name)
                    this.name = name;
                    console.log('constructor(2)')
                }

                get name() {
                    console.log('get() -> this._name = ' + this._name)
                    return this._name;
                }

                set name(newName) {
                    console.log('set() -> newName = ' + newName)
                    this._name = newName;
                }
            }

            let person = new Person("NodeJS");

            console.log('\n----------------- person.name (1) -> start ------------------')
            console.log('person.name = ' + person.name);
            console.log('----------------- person.name (1) -> end ------------------\n')

            person.name = 'ES 6';
            console.log('\n----------------- person.name (2) -> start ------------------')
            console.log('person.name = ' + person.name);
            console.log('----------------- person.name (2) -> end ------------------\n')


        运行以上代码,输出如下:

            constructor(1) -> name = NodeJS
            set() -> newName = NodeJS
            constructor(2)

            ----------------- person.name (1) -> start ------------------
            get() -> this._name = NodeJS
            person.name = NodeJS
            ----------------- person.name (1) -> end ------------------

            set() -> newName = ES 6

            ----------------- person.name (2) -> start ------------------
            get() -> this._name = ES 6
            person.name = ES 6
            ----------------- person.name (2) -> end ------------------     


        从输出结果可以看出,name 属性被绑定到 get 和 set 方法,即读写 name 属性时,不会直接读写 name 属性,而是被跳转到对应的 get 和 set 方法。

        将 name 属性改为 _name 是为了避免对 get 和 set 方法的循环调用,因为 this.name = name 会触发 set 方法,假设 set 方法里使用 this.name = newName,this.name = newName 会继续触发另一个 set 方法,从而进入触发 set 方法的循环调用。


    5) 继承

        在 ES6 之前,实现正确的继承需要多个步骤,最常用的策略之一是原型继承。格式如下:

            function Human(eyes) {
                this.eyes = eyes;
            }

            Human.prototype.see = function() {
                console.log(this.eyes + ' eyes see the world');
            }

            function Man(eyes) {
                Human.call(this, eyes);
            }

            Man.prototype = Object.create(Human.prototype);
            Man.prototype.constructor = Human;


            Man.prototype.run = function() {
                console.log('running');
            }

            var man = new Man(2);
            man.see(); // 2 eyes see the world
            man.run();  // running


        ES6 通过使用 extends 和 super 关键字简化了这些步骤,格式如下:

            class Human {

                constructor(eyes) {
                    this.eyes = eyes;
                }

                see() {
                    console.log(this.eyes + ' eyes see the world');
                }
            }

            class Man extends Human {

                constructor(eyes) {
                    super(eyes);
                }

                run() {
                    console.log('running');
                }
            }

            let man = new Man(2);
            man.see(); // 2 eyes see the world
            man.run();  // running

 

        子类的 constructor 方法中必须有 super,且必须出现在 this 之前。
        
        使用 extends 不能继承一般 Javascript 对象,可以使用如下方法:

            var obj = { name: 'Javascript Object', toString: function() { console.log(this.name)}}
            class Child {

            }

            Object.setPrototypeOf(Child.prototype, obj)
            let cc = new Child()
            cc.toString()   // 输出: Javascript Object