在 “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