《新时期的Node.js入门》学习日记-用ES6书写Node

0xGeekCat · 2020-8-29 · 次阅读


新时代的EMCAScript

JavaScript是EMCAScript标准的一种实现,事实上是JavaScript发明在先,随后被作为EMCA的一种标准确定下来,称为ECMAScript,简称ES

2015年ES6(ES2015)正式发布,并且计划每年发布一个新版本,均以ES201X来命名

JavaScript的缺陷

JavaScript长期以来都被视为一种玩具语言;仅在浏览器中做一些简单操作的脚本语言

在Web发展的初级阶段,JavaScript主要做一些dom操作,然后又出现了jQuery、Underscore等一系列类库对其做了扩展,可以认为是语法糖

Ajax出现后开发者们意识到可以用JavaScript做更多的事情,随后出现了更多复杂的Web应用,JavaScript渐渐地被重视起来

但这并不能掩盖JavaScript先天的不足

  • 几乎无法支持模块化
  • 没有很好的面向对象支持
  • 没有局部作用域
  • ❗️各种让人“惊喜”的语法细节,例如0.1+0.2或者[] == []

这些问题还没有经过广泛讨论和改进就被定义成标准也由于当时时代的原因,倒逼后来的Node也要跟着实现同样的缺陷。不过ES2015之后这种情况已经大有改善

使用nvm管理Node版本

nvm是目前流行的Node版本管理工具,可以在当前的系统中安装多个不同版本的Node,并且可以自由切换,nvm同样可以使用npm进行安装

👇nvm常用命令

  • nvm install ﹤version﹥:安装某个版本的Node
  • nvm use ﹤version﹥:切换到某个版本的Node
  • nvm ls:列出当前安装的所有Node版本,并且显示当前使用的Node版本

截屏2020-08-29 下午2.24.35

块级作用域

ES5中的作用域

ES5中只有两种作用域

  • 全局作用域
  • 函数作用域

在JavaScript文件中声明一个变量

var name = '0xGeekCat'

代码中的name变量属于全局作用域,这表示在同一文件中的任何位置都可以访问到该变量,如果想创建一个新的作用域,只能通过声明一个新的函数来实现

截屏2020-08-29 下午2.40.16

块级作用域

大多数常用的编程语言中都有块级作用域的概念,以C语言为例,C语言可以用一对花括号来定义一个块级作用域,而在ES5中没有这样的设计

截屏2020-08-29 下午2.44.50

在if代码块中定义的变量在外部依然可以访问到。要想避免这种情况,通常使用一个闭包来隔离作用域

function foo() {
    var x = 1
    if (x) {
        (function () {
            var x = 2
            console.log(x) 👈 2
        }())
    }
    console.log(x) 👈 1
}

foo()

但实现一个闭包要写不少冗余的代码

变量提升

🔔变量提升是指JavaScript解释器会将变量的声明提到当前作用域最前的现象,Node也继承了这一点

截屏2020-08-29 下午3.01.28

最后打印出的是undefined,这可能有点让人费解,因为执行打印一行的代码时,变量msg还未被定义,应该出现ReferenceError: msg is not defined的错误才对;实际上,在👆代码中存在一个隐式的变量提升,变量msg的声明被提到代码最前面

👇实际上的执行

var msg
console.log(msg)
msg = '0xGeekCat'

之所以输出undefined,是因为变量提升仅仅提升变量的声明,并没有提升变量的值

let关键字

let关键字会创建一个块级作用域,使用let关键字声明的变量只能在当前块级作用域中使用

var i = 0
for (let i = 0; i < 3; i++) {
    console.log(i) // 0 1 2
}
console.log(i) // 0

for循环内部使用let声明的变量,和外部使用var声明的处于不同的作用域中,值互不影响

重复声明

在同一个块级作用域中,不能使用let关键字重复声明同一个变量

截屏2020-08-29 下午3.16.34

在不同块级作用域中则无此限制

let a = 15

function scope() {
    let a = 10
    console.log(a) // 10
    if (true) {
        let a = 5
        console.log(a) // 5
    }
}
scope()
console.log(a) // 15

变量提升

let关键字可以解决ES5中变量提升带来的问题,下面的代码会抛出一个错误而不是打印出undefined

截屏2020-08-29 下午3.19.49

👆看似let没有变量提升,但实际上其内部也存在变量提升,从开头命名的真正赋值这个范围被称为Temporal Dead Zone(临时死区),在临时死区内无法对变量进行取值或者赋值,表现出的行为就和没有变量提升一样,❌此处体会上暂时有点不清晰

const关键字

声明为const的变量不可以再被修改,也不能被重复声明,这表示const变量必须在声明的同时进行初始化,const的另一个适用场景是用于模块引入

截屏2020-08-29 下午3.28.32

const声明的变量不能修改,但使用const修饰的对象却可以修改,这表明const内部是依靠指针来判断对象是否被修改的

const b = {name: '0xGeekCat'}
b.name = 'hacker'
console.log(b.name) // hacker

数组

数组是JavaScript中的全局对象

find()和findIndex()

和原有的findIndexOf方法作用基本相同,用于查找数据中第一个符合条件的数组成员

find函数会返回相应元素的值,findIndex方法会返回对应元素的索引,它们都只匹配第一个满足条件的元素;这两个方法都接受一个回调函数作为参数,对于数据的每个成员都会按顺序执行这个回调函数,因此可以自定义筛选条件

let array = [1, 2, -3, -4]

console.log(array.find(function (n) {
    return n < 0
})) // -3

console.log(array.findIndex(function (n) {
    return n < 0
})) // 2

比起原有的findIndexOf方法,新增的两个方法可以用来处理NaN

const arr = ['a', 'b', NaN, 'c']

console.log(arr.indexOf('d')) // -1
console.log(arr.indexOf(NaN)) // -1 👈 返回-1 是因为 NaN !=== NaN
console.log(arr.indexOf('b')) // 1

console.log(arr.findIndex(function (x) {
    return Number.isNaN(x) // 2
}))

from()方法

from方法用于将一个array-like object转换成数组

JavaScript中的参数对象arguments就是一个array-like object,可以通过[]来访问其中的元素,也可以通过length属性来得到对象的长度。但是无法通过array对象的方法,例如pop或者push来操作它

截屏2020-08-29 下午4.06.00

🔔通常一个对象有length属性而且其值大于0,那么就可以看作是array-like object

新建一个array-like object

var a = {}

var i = 0
while (i < 10) {
    a[i] = i * i
    i ++
}

a.length = i

ES5中,开发者可以使用Array.prototype.slice方法来将array-like对象转换成真正的数组,

❗️如果要转换一个现有的object,通常还要调用call方法,否则会返回一个空数组

var a = {}

// ES5
var a1 = Array.prototype.slice.call(a)
a1.push('0xGeekCat')
console.log(a1) // [ '0xGeekCat' ]

// ES6
var a2 = Array.from(a)
a2.push('0xGeekCat') 
console.log(a2)  // [ '0xGeekCat' ]

from方法为什么没有定义在prototype

from用于将非数组对象转换为一个array对象,而对于一个现有的array对象,在上面部署from方法没有意义

fill()方法

fill方法用一个给定的值来填充数组,通常用来初始化一个新建的array对象

var a = new Array(5)
console.log(a) // [ <5 empty items> ]

a.fill(1)
console.log(a) // [ 1, 1, 1, 1, 1 ]

ES5中初始化一个数组,通常要借助applymap方法,这是一种比较hack的方法,而在ES6直接调用fill方法就可以完成

// ES5
var array = Array.apply(null, new Array(5)).map(function () {
    return 0
})

// ES6
var array = new Array(5).fill(0)

数组的遍历

ES6提供三个新的方法用于遍历数组

  • entries
  • keys
  • values

区别在于keys是对键名的遍历,values是对键值的遍历,entries是对键值对的遍历

var a = ['a', 'b', 'c']

for (let i of a.keys()) {
    console.log(i)
}

for (let i of a.entries()) {
    console.log(i)
}

截屏2020-08-29 下午4.44.07

事实上凡是内部实现iterator接口的对象都可以使用这三个方法进行遍历。上面的代码中没有使用values方法,原因是该方法目前还没有得到广泛支持,在某些环境中可能存在兼容性问题

TypedArray

ES2015新增了TypedArray类型,它和array的区别在于TypedArray的元素必须是同一类型。TypedArray的本意是为JavaScript提供访问二进制数据的能力,由于Node已经内置了Buffer类型,因此TypedArray在Node中的出镜率相对不高,在TypedArray标准推出后,Node中Buffer类型的底层改用TypedArray实现

函数

参数的默认值

ES6允许给函数的参数赋一个默认值

function greed(x = 'hello', y = 'world') {
    console.log(x, y)
}

greed() // hello world

不能在方法体内用let关键字声明与参数同名的变量,否则会出现SyntaxError

截屏2020-08-29 下午5.12.07

❌此处与书中描述有出入,书中指的是含有默认值的变量

除了允许赋默认值外,ES6还允许赋默认参数,即接收一个数组名作为参数,前面使用三个点号...spread运算符来标识,避免了在调用方法时写一堆参数

Spread运算符

Spread运算符ES6提出的新运算符,其实它并算不上新概念,在C语言中就已经出现,spread运算符可以和数组结合使用,扩展的参数

var arr1 = [1, 2, 3]
var arr2 = [4, 5]
console.log(...arr1, ...arr2)

var name = [...'0xGeekCat']
console.log(name)

截屏2020-08-29 下午5.21.13

spread运算符也可以作为函数的参数,表示该函数有多个参数,也可以在函数调用时使用

function func(x, y, z) {
    return x + y + z
}

var args = [1, 2, 3]
console.log(func(...args)) // 6

箭头函数

ES6提出了一种新的箭头函数,它以=﹥定义匿名函数

var func = a => a 
👇
var func = function (a) {
    return a
}

在多参数的情况下

var func = (arg1 = 'hello', arg2 = 'world') => console.log(arg1, arg2)

除简洁之外,箭头函数还有另一个优点

🔔修复this作用域问题

var date = {
    year: 2020,
    month: 8,
    day: 29,
    getDate: function () {
        var func = function () {
            return this.year + '/' + this.month + '/' + this.day
        }
        return func()
    }
}

console.log(date.getDate()) // undefined/undefined/undefined

导致👆输出的原因是func()方法内部的this指向全局对象而非date对象

ES5中的一种解决方法是使用临时变量保存外部this

var date = {
    year: 2020,
    month: 8,
    day: 29,
    getDate: function () {
        var self = this
        console.log(self)
        var func = function () {
            return self.year + '/' + self.month + '/' + self.day
        }
        return func()
    }
}

console.log(date.getDate())

截屏2020-08-29 下午6.33.29

ES6中使用箭头函数则可以完美解决问题

var date = {
    year: 2020,
    month: 8,
    day: 29,
    getDate: function () {
        var func = () => this.year + '/' + this.month + '/' + this.day
        return func()
    }
}

console.log(date.getDate()) // 2020/8/29

匿名函数中的this

ES5中,匿名函数默认是指向全局对象的,在浏览器中为Window对象,在Node中大部分情况下都会指向global对象,定时器API略有不同

function foo() {
    var func = () => console.log(this)
    func()
}
foo()

截屏2020-08-29 下午6.47.49

截屏2020-08-29 下午6.48.52

setTimeout中匿名函数的this指向,Node和浏览器不同,在Chrome中输出window对象,在Node控制台中输出一个Timeout对象

function foo() {
    setTimeout(function () {
        console.log(this)
    })
}
foo()

使用箭头函数后,this的指向和foo函数内部相同

function foo() {
    this.name = '0xGeekCat'
    setTimeout(() => {
        console.log(this)
        console.log(this.name)
    })
}
foo()

截屏2020-08-29 下午6.59.31

🔔此时浏览器中this还是指向Windows,证明实际上匿名函数内部并没有定义this,仅仅是引用外面一层的this

箭头函数的陷阱

箭头函数本身没有定义this,在箭头函数内部使用this关键字时,它开始在代码定义的位置向上找,直到遇见第一个this,这带来了很大的便利但有时也会出现一些问题

function Person() {
    this.name = '0xGeekCat'
}

Person.prototype.greet = () => console.log(this.name)

var person = new Person()
person.greet() // undefined

👆原型方法的定义位于顶层,箭头函数中的this就会指向全局对象

JavaScript中的this是在运行时基于函数的执行环境决定的;如果在浏览器中运行,this指向了Window对象

🔔最好还是使用最基本的定义形式,而不是用prototype原型对象

var EventEmitter = require('events').EventEmitter 👈 导入EventEmitter类

class Producer extends EventEmitter {
    constructor() {
        super();
        this.status = 'ready'
    }
}

var producer = new Producer()

producer.on('begin', function () {
    console.log(this.status) // ready
})

producer.on('begin', () => {
    console.log(this.status) // undefined
})

producer.emit('begin')

原因同上,箭头函数的外层还是全局对象,这个隐藏的bug很难在code review阶段被发现

Set和Map

ES6提出了两种新的数据结构,SetMapSet的实现类似于数组,和普通数组的不同之处在于**Set中不能包含重复数据**

Set和WeakSet

Set的使用

var set = new Set([1, 2, 3, 4, 4, 5])
console.log(set) // Set(5) { 1, 2, 3, 4, 5 } 👈 自动过滤重复数据

set.add(6)
set.delete(5)
console.log(set.has(6)) // true

for (let i of set) {
    console.log(i) // 1 2 3 4 5 6
}

set.clear()
console.log(set) // Set(0) {}

Set的遍历

let set  = new Set([1, 2, 3])

for (let i of set.keys()) {
    console.log(i)
}

for (let i of set.values()) {
    console.log(i)
}

for (let i of set.entries()) {
    console.log(i)
}

截屏2020-08-29 下午8.40.27

WeakSet

WeakSetSet的主要区别在于**WeakSet的成员只能是对象**

截屏2020-08-29 下午8.46.59

WeakSet中的weak一词指的是弱引用,它表示WeakSet中存储的是对象的弱引用,这是一个垃圾回收中的概念,在垃圾回收器的扫描过程中,一旦发现了只有弱引用的对象,就会在回收阶段将其内存回收

这也就表示WeakSet中存储的对象如果没有被其他的对象所引用,其内存空间就会被回收。由于开发者通常无法控制垃圾回收器的运行,因此WeakSet中的值是无法预测的。 WeakSet不支持遍历,也不能用size属性来得到其大小

WeakSet的优点在于对垃圾回收有利,假设在一个局部作用域中产生了一个中间值的对象,如果作用域之外没有引用这个对象,那么就可以使用WeakSet来存储它,在离开局部作用域之后,该对象就会在下一轮垃圾回收时被销毁

Map和WeakMap

Map

Map表示由键值对组成的有序集合,有序表现在Map的遍历顺序为插入顺序,在ES5中虽然也有类似的结构,但ES5中键值对的键值只能为字符串类型,ES6新增的Map则支持多种类型作为键值,包括对象和布尔值,Map爷提供了一系列方法来访问或操作其中的数据

var obj = {c: 3}

var map = new Map([
    ['a', 1],
    ['b', 2],
    [obj, 3],
])

console.log(map.size)
console.log(map.has('a')) // 判断是否存在键值对
console.log(map.get('b')) // 获取某键值对的值
map.set('d', 4) // 如果键值对不存在则添加,存在则覆盖
map.delete('a')

console.log(map)
for (let key of map.keys()) {
    console.log(key)
}

for (let value of map.values()) {
    console.log(value)
}

for (let m of map.entries()) {
    console.log(m)
}

map.clear()
console.log(map)

截屏2020-08-29 下午9.02.40

WeakMap

WeakMap用法和WeakSet相似,作为key的变量必须是个对象,关于弱引用的特性和WeakSet相同,不再叙述

Iterator

ES6中的Iterator

ES6中的Iterator接口通过Symbol.iterator属性来实现,如果对象设置了Symbol.iterator属性,就表示该对象是可以被遍历的,就可以用next方法遍历

👇给对象加上Iterator接口

var Iter = {
    [Symbol.iterator] : function () {
        var i = 0
        return {
            next: function () {
                return ++i
            }
        }
    }
}

var obj = new Iter[Symbol.iterator]()
console.log(obj.next()) // 1
console.log(obj.next()) // 2

👆代码给Iter对象加上了[Symbol.iterator]接口,这个方法的特点是每次调用next方法,返回值就增加1,由于此处代码没有设置边界条件,就算一直调用next也不会出错

ES6Iterator广泛存在于各种数据结构

var arr = [1, 2, 3]
console.log(arr[Symbol.iterator])

var set = new Set([1, 2, 3])
console.log(set[Symbol.iterator])

var map = new Map([
    ['a', 1],
    ['b', 2]
])
console.log(map[Symbol.iterator])

var str = '0xGeekCat'
console.log(str[Symbol.iterator])

var obj = {}
console.log(obj[Symbol.iterator]) // undefined

截屏2020-08-29 下午9.30.47

❗️普通对象没有iterator接口

Iterator的遍历

ES6中所有内部实现了Symbol.iterator接口的对象都可以使用for / of循环进行遍历

👆自定义的Iter对象不可以进行循环,需要进行修改

function Iter(array) {
    this.array = array
}

Iter.prototype[Symbol.iterator] = function () {
    let index = 0
    let next = () => {
        if (index < this.array.length) {
            return {
                value: this.array[index++],
                done: false
            }
        } else {
            return {
                value: undefined,
                done: true
            }
        }
    }
    return {next: next}
}

let iter = new Iter(['a', 'b'])

for (let i of iter) {
    console.log(i) // a b
}

对象

新的方法

object.assign()

将一个对象的属性复制到另一个对象,很多开发者看到这个方法第一时间想到的就是确认该方法是深复制或者是浅复制

var obj1 = {a : {b: 1}}
var obj2 = Object.assign({}, obj1)
obj1.a.b = 2
console.log(obj2.a.b) // 2

很明显,该方法实现的是一种浅拷贝

🔔浅拷贝复制指向对象的指针,而不复制对象本身,新旧对象还是共享同一块内存

Object.setPrototypeOf()

Object.setPrototypeOf方法用来设置一个对象的prototype对象,返回参数对象本身,它的作用和直接设置__proto__属性相同

在ES6之前,__proto__属性只是一种事实的标准,不是ECMAScript标准中的内容,ES6将__proto__写入了附录中,但仍然不推荐直接使用该属性

Object.getPrototypeOf()

Object.setPrototypeOf方法配套,用于获得一个对象的原型对象

var Person = function (name, age) {
    this.name = name
    this.age = age
    this.greed = function () {
        console.log('Hello, I am', this.name)
    }
}

function Student() {}

// 设置prototype
// 方法一
var student = new Student()
Object.setPrototypeOf(student, Person)
//方法二
Student.prototype = Person
var student = new Student()
//方法三
var student = new Student()
student.__proto__ = Person

// 读取prototype
console.log(student.__proto__) // [Function: Person]
console.log(Object.getPrototypeOf(student).name) // Person

对象的遍历

先设置一个对象

var obj = {
    name: '0xGeekCat',
    age: 19,
    gender: 'male'
}

使用for / in遍历

for (let key in obj) {
    console.log(key, obj[key])
}

使用Object.keys()遍历

返回包含所有键值的数组,不包含不可枚举的属性

console.log(Object.keys(obj)) // [ 'name', 'age', 'gender' ]

使用Object.getOwnPropertyNames()遍历

作用和Object.keys相同,区别是返回全部的属性,无论是否可枚举

console.log(Object.getOwnPropertyNames(obj))

枚举属性

ES5中可以将一个对象的属性设置为不可枚举的,不可枚举的属性可以正常地通过a.b的形式访问,但无法通过for / in循环和Object.keys方法遍历

可以通过Object.defineProperty来设置一个属性是否可以被枚举

Object.defineProperty(obj, 'gender', {
    enumerable: false
})

genderenumerable属性设置为false后,只有getOwnPropertyNames可以遍历到该属性

ES6中的遍历方法

ES6在此基础上增加Object.getOwnPropertySymbols()Reflect.ownKeys()两个方法,它们都接受一个对象作为参数,前者会返回参数对象的全部Symbol属性,后者会返回全部属性

Class特性的引入,标志着在ECMAScript语言层面提供了对经典类的原生支持,对ES6来说,这种支持更多地是在语法层面上,其底层的实现并未发生变化。

ES5时期的JavaScript中,类的所有实例对象都从同一个原型对象上继承属性

function Person(gender, age) {
    this.gender = gender
    this.age = age
}

Person.prototype.getInfo = function () {
    return this.gender + ', ' + this.age
}

var person = new Person('male', 19)

ES6提供了更接近传统语言的class定义,这种新特性更多地是语法糖,底层实现与ES5实际上无差别

ES6中定义Person类

class Person {
    constructor(gender, age) {
        this.gender = gender
        this.age = age
    }

    getInfo() {
        return this.gender + ', ' + this.age
    }
}

var person = new Person('male', 19)

属性和构造函数

Class中的属性定义在构造函数中。构造函数负责类的初始化,包括初始化属性和调用其他类方法等,构造函数同样支持默认值参数。如果声明一个类的时候没有声明构造函数,那么会默认添加一个空的构造函数

构造函数只有在使用关键字new实例化对象时才会被调用

class Student {
    constructor(name = '0xGeekCat', gender = 'male') {
        this.name = name
        this.gender = gender
    }
}

var student = new Student()

类方法

类方法的定义无须使用function关键字,方法内部使用this来访问类属性,方法之间也不需要逗号间隔

class Student {
    constructor(name = '0xGeekCat', gender = 'male') {
        this.name = name
        this.gender = gender
    }

    getInfo() {
        console.log('name:', this.name, 'gender:', this.gender)
    }
}

类方法也可以作为属性定义在构造函数中,这时的写法略有不同

class Student {
    constructor(name = '0xGeekCat', gender = 'male') {
        this.name = name
        this.gender = gender
        this.getInfo = () => {
            console.log('name:', this.name, 'gender:', this.gender)
        }
    }
}

__proto__

ES5中类的实例通过__proto__属性来指向构造函数的prototype对象

console.log(student.__proto__ === Student.prototype) // true

__proto__属性本身不是ECMAScript规范内容,只是各大浏览器都对该属性进行了支持,才成为了事实上的标准,既然该属性指向类的prototype属性,那么表示可以用该属性修改prototype,但这也代表任何一个类的实例都可以修改原型对象,在实际开发中应该禁用这种做法

var student = new Student()

student.__proto__.sayHello = () => {
    console.log('hello')
}

student.sayHello() // hello

即使开发者完全不关注__proto__这个属性,也不会对开发工作带来消极的影响,ES6也建议在实际开发过程中认为这个属性不存在

getInfo方法和constructor方法虽然看似是定义在类的内部,但实际上还是定义在prototype,从侧面证明ES6class的实现依旧基于prototype

class Student {
    constructor(name = '0xGeekCat', gender = 'male') {
        this.name = name
        this.gender = gender
        this.getInfo = () => {
            console.log('name:', this.name, 'gender:', this.gender)
        }
    }
}

var student = new Student()

console.log(student.constructor === Student.prototype.constructor) // true
console.log(student.constructor.getInfo === Student.prototype.getInfo) // true

对象的__proto__属性指向类的原型,这点对ES5ES6均适用,类名本质上就是构造函数,ES6的写法仅仅是做了一层包装

console.log(student.constructor === Student) // true
console.log(Student.prototype.constructor === Student) // true

静态方法

在定义类时如果定义了方法,那么该类的每个实例在初始化时都会有一份该方法的备份。有时我们不希望一些方法被继承,而是希望作为父类的属性来使用

ES6中使用static关键字来声明静态方法,该方法只能通过类名来直接调用,而不能通过类的实例调用

screenshot

如果一个类继承一个包含静态方法的类,那么它可以通过super关键字来调用父类的静态方法,但包含super关键字的子类方法也必须是静态方法

class Person {
    static getName() {
        console.log('0xGeekCat')
    }
}

class Student extends Person{
    static getName() {
        return super.getName()
    }
}

Student.getName() // 0xGeekCat

类的继承

ES5中的继承

ES5中类的继承可以有多种方式,然而过多的选择有时反而会成为障碍,ES6统一了类继承的写法,避免开发者在不同写法的细节之中过多纠缠

先建立用于继承的基类Person

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

Person.prototype.getInfo = function () {
    return this.name + ', ' + this.age
}

修改原型链

这是最普遍的继承做法,通过将子类的prototype指向父类的实例来实现

function Student() {}

Student.prototype = new Person()
Student.prototype.name = '0xGeekCat'
Student.prototype.age = 19

var student = new Student()
student.getInfo() // 0xGeekCat, 19

在这种继承方式中,student对象既是子类的实例,也是父类的实例。然而也有缺点,在子类的构造函数中无法通过传递参数对父类继承的属性值进行修改,只能通过修改prototype的方式进行修改

调用父类的构造函数

function Person() {
    this.getInfo = () => {
        console.log('name:', this.name, 'gender:', this.gender)
        console.log(this) 
    }
}

Person.prototype.getInfo2 = function () {
    console.log(this.name + ', ' + this.age)
}

function Student(name, age, gender) {
    Person.call(this) 👈 要关注的一个细节 此时将Person的this指针指向Student,此时Student拥有Person的方法
    this.name = name
    this.age = age
    this.gender = gender
}

var student = new Student('0xGeekCat', 19, 'male')
student.getInfo()
student.getInfo2() 👈 第二个细节 这里报错

screenshot 1

这种方式避免了原型链继承不能传参的缺点,在这种情况下,student对象只是子类的实例不是父类的实例,而且只能调用父类中定义的方法,不能调用父类prototype原型上定义的方法

组合继承

function Person() {
    this.getInfo = () => {
        console.log('name:', this.name, 'gender:', this.gender)
        console.log(this)
    }
}

Person.prototype.getInfo2 = function () {
    console.log(this.name + ', ' + this.age)
}

function Student(name, age, gender) {
    Person.call(this)
    this.name = name
    this.age = age
    this.gender = gender
}

Student.prototype = new Person()

var student = new Student('0xGeekCat', 19, 'male')
student.getInfo()
student.getInfo2()

这种方式结合之前两种继承方式的优点,也是Node源码中标准的继承方式。唯一的问题是调用父类的构造函数两次,这造成了一定的内存浪费

  • 设置子类的prototype
  • 实例化子类新对象

ES6中的继承

在ES6中可以直接使用extends关键字来实现继承,形式上更加简洁。ES6class的改进就是为了避免开发者过多地在语法细节中纠缠

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

Person.prototype.getInfo = function () {
    return this.name + ', ' + this.age
}

class Student extends Person{
    constructor(name, age, gender) {
        super(name, age);
        this.gender = gender
    }

    getInfo() {
        return super.getInfo() + ', ' + this.gender
    }

    print() {
        var info = this.getInfo()
        console.log(info)
    }
}

var student = new Student('0xGeekCat', 19, 'male')
student.print() // 0xGeekCat, 19, male

super方法可以带参数,表示哪些父类的属性会被继承

🔔在子类中super方法是必须要调用的,原因在于子类本身没有自身的this对象,必须通过super方法拿到父类的this对象

如果子类没有定义constructor方法,那么在默认的构造方法内部自动调用super方法,并继承父类的全部属性

在子类的构造方法中必须先调用super方法,然后才能调用this关键字声明其他的属性,因为在super没有调用之前子类还没有this这一缘故

class Student extends Person{
    constructor(name, age, gender) {
        console.log(this) 👈 报错 ReferenceError
        super(name, age);
        this.gender = gender
        console.log(this) 👈 Student { name: '0xGeekCat', age: 19, gender: 'male' }
    }

    ...
}

var student = new Student('0xGeekCat', 19, 'male')

除了用在子类的构造函数中,super还可以用在类方法中来引用父类的方法

❗️**super只能调用父类方法,而不能调用父类的属性,因为方法是定义在原型链上的,属性则是定义在类的内部**

getInfo() {
    return super.name // undefined
}

🔔当子类函数被调用时使用的均为修改父类this得来的子类的this,即使使用super来调用父类的方法,使用的仍然是子类的this

class Person {
    constructor() {
        this.name = '0xGeekCat'
        this.gender = 'male'
    }

    getInfo() {
        return this.name + ', ' + this.gender
    }
}

class Student extends Person{
    constructor() {
        super();
        this.name = 'hacker'
        this.gender = 'male'
    }

    getInfo() {
        return 'hello, world'
    }

    print() {
        return super.getInfo()
    }
}

let student = new Student()
console.log(student.print()) // hacker, male

👆super调用了父类的方法,输出的内容却是子类的属性,说明super绑定了子类的this

还可以对super的属性赋值super.name ='hacker',这个赋值修改的是子类属性,如果打印super.name,会输出undefined

class Person {
    constructor() {
        this.name = '0xGeekCat'
    }
}

class Student extends Person{
    constructor() {
        super();
        super.name = 'hacker' 👈
    }

    getInfo() {
        console.log(super.name) // undefined
        console.log(this.name) // hacker
    }
}

let student = new Student()
student.getInfo()

ES6的模块化标准

区别于上一章提到的AMDCommonJSES2015也提出了一套模块化标准;但Node目前还不支持ES2015的模块标准,了解即可

❌此块内容本人暂时没有搞清楚,留在之后实践中继续研究

ES6同样使用export关键字来导出变量,但和commonJS略有不同

export const Name = '0xGeekCat'
export const Age = 10

也可以将多个要导出的对象打包

const Name = '0xGeekCat'
const Age = 10

export {Name, Age}

使用export导出变量时,通常要使用花括号{}将其包裹起来,否则会出现错误

导出方法

// 方法一
export function add(x, y) {
    return x + y
}

// 方法二
function add(x, y) {
    return x + y
}
export {add}

导入

var module = require('./module')

import Name from module
import {Name, Age} from module

使用babel来转换代码

ES6乃至ES7的新特性让人激动不已,开发者可能迫不及待地想在自己的工作中使用这些新语法,以便改善自己的工作效率

然而Node支持新特性也是有时间差的;此外生产环境中Node的版本往往较低,但开发者用的可能是最新版本。这代表即使在开发过程中使用了最新的特性,这些代码也无法在生产环境中运行,为了解决这些困境,babel应运而生

babel的初衷是为了解决新特性的代码无法在低版本的Node环境中运行的问题,它提供了一系列API,用来把使用新特性编写的代码编译为可以在低版本环境中运行的版本

安装与配置

对于初次使用babel的开发者来说,最为头疼的就是各种组件的配置,babel本身是由一系列的组件构成,想要使用babel的功能需要自行配置组件

👇在项目目录执行

npm install --save-dev babel-cli
npm install --save-dev babel-preset-es2015

安装成功后,可以在终端使用babel命令

screenshot 4

使用

要在项目中使用babel,需要增加名为.babelrc的配置文件,配置文件的主要目的是指定babel以何种标准进行转换

👇对使用ES2015语法的代码进行转换

{
  "presets": [
    "es2015"
  ]
}

书中关于stage的相关配置在实际中似乎并不需要

👇试着用babel转换代码,先编写一个简单的使用ES6特性的代码

screenshot 5

也可以使用-o参数将转换结果重定向到文件

screenshot 6

如果不想在命令行中使用babel进行转换,也可以使用代码来完成,需要安装额外的组件

npm install --save-dev babel-core 

❌不过书中这种方法在本人测试时出现错误

除了在本地进行转换之外,babel的官方网站提供了一个在线的试验场,可以将ES6或以上版本的代码放进去进行试验。其结果应该和本地结果还是略有偏差,个人更喜欢命令行操作

screenshot 7

可以看到babel转换后的代码有个显著的特征:那就是让人看不懂

即使是简单如继承一个类,转换成ES5的写法之后也变得让人费解,这也是有些开发者不喜欢babel的原因。即使babel是开源的,面对这么复杂的代码,也很难让人放心是否有潜在的问题

babel-polyfill

在默认情况下,babel只转换最新的JavaScript语法,但不会转换一些新的API,例如Promise、Iterator等,还有一些新增的对象方法也不会被转换,例如array.from()

转换这些特性还需要使用其他的第三方插件

npm install --save-dev babel-polyfill 

👇转换一个包含Promise的代码文件

import 'babel-polyfill'

function timeout(ms) {
    return new Promise(((resolve, reject) => {
        setTimeout(resolve, ms, 'done')
    }))
}

timeout(1000).then((value => {
    console.log(value)
}))

babel之后的结果

screenshot 3

babel的使用完全是基于配置的,如果想要使用ES2016或者ES2017的特性,只需要在配置文件中增加对应的配置就可以

babel存在的意义是能够让开发者使用最新的特性进行开发而不用受限于运行时的支持

reference

《新时期的Node.js入门》 李锴