重探JavaScript ES6, ES7 and ES8[译]

491 阅读19分钟

对应英文原著版,Rediscovering JavaScript Master ES6, ES7, and ES8-2018

# Part One - The save alternatives

  • 注意代码断行之处
//automatic semicolon insertion (ASI)
function(){
    //error
    return  //ASI => return;
    {};
    //right
    return {
        //...
    };
}
  • 使用 === 强等代替 == 弱等,弱等存在type-coercion类型转换
  • 推荐的前置准备,应用严格模式 "use strict"并使用 lint 工具lint code
  • 使用 let / const 书写更安全的代码
    • 不要使用var
    • 尽可能优先使用const代替let,必要时才使用let
    • let / const 的优势:
      • 减少代码的错误,更容易追溯代码错误的原因
      • 防止变量被意外修改
      • 在函数式代码或箭头函数中使用是安全的
  • 推荐使用剩余参数[the rest parameter]
    • 对于函数的不定参数,两种解决方案:arguments / rest parameter
    • arguments缺点
      • 无法直观表明参数意图甚至产生误导
      • 类数组对象并非数组,效率较低
      • 导致代码混乱,无法使用数组提供的api
      • 向后兼容难以维护
  • 展开运算符[spread operator]
    • 符号与剩余参数一样,出现在函数调用时而非获取函数参数时
    • 作用与剩余参数相反,展开运算符将集合展开为分散值,剩余参数则将分散值组合为数组,注意不要混淆。
    • 可在构造函数中使用,任意可迭代对象均可使用,还可以用于合并对象或数组
const sam = { name: 'Sam', age: 2 };
console.log({...sam, age: 4, height: 100 });
[1,2,3,...[4,5,6]]
  • 为参数定义默认值
默认值的好处:
    1.简化代码,参数实际值与默认值不一致时才需指定
    2.便于增添新参数,无需影响原有代码
    3.根据是否提供含默认值的参数,能做到类似函数重载的功能
基本规则:
    1.提供有效值,使用有效值
    2.传入null值,使用null值,但由于null无法表明其意图,所以最好保守使用
    3.传入undefined,使用默认值而非undefined
eg: 
    fetchData(3, undefined, 'books'); //correct
    fetchData(3, , 'books'); //error
莫将含默认值的参数与普通参数混杂使用,推荐将含默认值的参数放于参数列表尾部
剩余参数无法设置初始默认值

# Part Two - Nice Additions

1.Iterators and Symbols

1.1 更便捷的增强for循环

// for...of 用于任何可迭代的对象[即实现了[Symbol.iterator]()的对象]
//Array.entries()方法返回一个 key + value 的迭代器
    const names = ['Sara', 'Jake', 'Pete', 'Mark', 'Jill'];
    for(const entry of names.entries()) {
    	console.log(entry);	//[ 0, 'Sara' ]...
    }
    for(const [i, name] of names.entries()) {
    	console.log(i + '--' + name);
    }

1.2 新原始数据类型 - Symbol

Symbols 有三个不同的用途:

  • 用于定义对象的属性,这些属性在常规的循环迭代中不会出现,它们并不是私有属性,只是与其它属性相比更加不容易被发现而已
  • 更简单的定义一个全局注册或对象字典
  • 在对象中定义一些特殊常用的方法;Symbol弥补了js中接口的空白,这也是Symbol一个最为重要的目的

此前,js中对象的所有属性在 for...in 中均可见,而Symbol属性在for...in 中不可见。若想在一个对象中存储某些特殊的元数据且要求这些数据在普通的循环中不可见,那么你就可以是用Symbol属性

//sample
const age = Symbol('ageValue');
const email = 'emailValue';
const sam = {
    first: 'Sam',
    [email]: 'sam@example.com',[age]: 2
};
for(const property in sam) {
    console.log(property + ' : ' + sam[property]);
}
//first : Sam
//emailValue : sam@example.com
console.log(Object.getOwnPropertyNames(sam));// [ 'first', 'emailValue' ]
/**
    1.Symbol属性在普通循环中不可见,但并不是私有或受保护的,
      任何访问对象的代码都能访问和修改该对象上的Symbol属性值 
    2.Object ’s getOwnPropertySymbols() 可以列举出所有的Symbol属性
*/
console.log('list of symbol properties');
console.log(Object.getOwnPropertySymbols(sam)); //[ Symbol(ageValue) ]
console.log(sam[age]);	//access 2
sam[age] = 3;	//change 3

1.使用Symbol进行全局注册

//Symbol()用于创建一个Symbol,接收一个没有实际意义的参数,
//每次被调用都会创建一个唯一的Symbol
const name = 'Tom';
const tom = Symbol(name);
console.log(typeof(tom));	//	symbol
//Symbol.for() vs Symbol()
//Symbol.for()接受一个key参数,在全局注册一个新的Symbol或返回已存在的Symbol
const masterWizard = Symbol.for('Dumbledore');
const topWizard = Symbol.for('Dumbledore');
console.log(typeof(masterWizard));	//symbol
console.log(masterWizard);		//Symbol(Dumbledore)
console.log(masterWizard === topWizard);//true
console.log('Dumbledore' === Symbol.keyFor(topWizard));//true

2.常用的Symbol

/*	
    String's search() 接收的参数不是一个 RegExp 的实例,
    那么它会将给定的参数传入 RegExp 构造器来创建一个 RegExp 对象。
    但是上述情况只有在给定的参数不支持名为 Symbol.search 的特殊方法时才有效;
    若给定参数自身支持Symbol.search 方法时,
    那么这个 Symbol.search 方法会被用于代替 String's Search 来执行。
*/
class SuperHero {
    constructor(name, realName) {
        this.name = name;	this.realName = realName;
    }
    toString() { return this.name; }
    //this -- SuperHero instance , value -- names string
    [Symbol.search](value) {
        console.info('this: ' + this + ', value: ' + value);
        return value.search(this.realName);
    }
}
const superHeroes = [
new SuperHero('Superman', 'Clark Kent'),
new SuperHero('Batman', 'Bruce Wayne'),
new SuperHero('Ironman', 'Tony Stark'),
new SuperHero('Spiderman', 'Peter Parker') ];
const names = 'Peter Parker, Clark Kent, Bruce Wayne';
for(const superHero of superHeroes) {
	console.log(`Result of search: ${names.search(superHero)}`);
}
//result
this: Superman, value: Peter Parker, Clark Kent, Bruce Wayne
Result of search: 14
this: Batman, value: Peter Parker, Clark Kent, Bruce Wayne
Result of search: 26
this: Ironman, value: Peter Parker, Clark Kent, Bruce Wayne
Result of search: -1
this: Spiderman, value: Peter Parker, Clark Kent, Bruce Wayne
Result of search: 0

3.自定义Iterators and Generators

/*
    我们可以使用 for...of 在循环迭代数组,是因为数组s实现 [Symbol.iterate]的函数。
    为class指定自定义迭代器:实现一个迭代器,使其也可以被迭代,
    此时Symbol类似充当了一个接口
*/
class CardDeck {
    constructor() {
        this.suitShapes = ['Clubs', 'Diamonds', 'Hearts', 'Spaces'];
    }
    [Symbol.iterator]() {
        let index = -1;
        const self = this;
        return {
            next() {
                index++;
                return {	// like generator
                	done: index >= self.suitShapes.length,
                	value: self.suitShapes[index]
                };
            }
        };
    }
}
//改进: 使用 yield 简化 iterator 的实现
*[Symbol.iterator]() {
    for(const shape of this.suitShapes) {
    	yield shape;
    }
}

2.箭头函数和函数式编程

  • 由于箭头函数是词法作用域,所以在其内使用的this/arguments/parameters/variables是非常安全的;
  • 函数式编程使用高阶函数[即接收一个函数作为参数 或者 返回一个函数作为结果] 以往只是接收或返回一个原始类型值或类实例,使用函数式编程,我们就可以将其扩展为接收或返回函数,这是一种执行函数组合的好方法。
const create = function(message) {
	console.log('First argument for create: ' + arguments[0]);
	return () => console.log('First argument seen by greet: ' + arguments[0]);
};
const greet = create('some value');//First argument for create: some value
greet('hi');//First argument seen by greet: some value

1.箭头函数的局限

  • 1.只能匿名声明
  • 2.不能作为构造函数,即使用 new 命令会报错
  • 3.Lexically Scoped[词法作用域,而非dynamic scoping,动态作用域]
  • 4.没有 prototype 属性
  • 5.不能作为生成器函数,不能使用 yield 命令
  • 6.单个抛出错误语句需要{}包裹
    • const mapFunction = () => throw new Error('fail'); //BROKEN CODE
    • const madFunction = () => { throw new Error('fail'); };//correct
  • 7.返回对象字面量需要{}包裹
    • const createObject = (name) => { firstName: name }; //broken code
    • const createObject = (name) => ({ firstName: name }); //correct
  • 8.不存在arguments对象,可用 rest arguments 剩余参数代替;
箭头函数不会产生作用域,没有this,即箭头函数中的this等于最近的上层作用域的this

2.bind/apply/call的不同

const greet = function(message, name) {
	console.log(message + ' ' + name);
};
const sayHi = greet.bind(null, 'hi');
sayHi('Joe');	// hi Joe

//bind 可用于函数柯里化
//若需要柯里化 n 个参数则需要传递 n+1 参数,其中首个参数用于绑定this。

3.new.target

//函数可以使用 new.target 来判断该函数是作为构造函数还是普通函数被调用了。
const f1 = function() {
    if(new.target) {
        console.log('called as a constructor');
    }else {
        console.log('called as a function');
    }
};
new f1();		
f1();
//箭头函数不能作为构造函数,所以箭头函数中没有也不能使用 new.target

4.何时使用箭头函数

  • 不要在类,对象字面量或原型对象prototype上使用箭头函数定义方法; 因为this的词法作用域会导致方法被调用时其中的this无法正确指向实例或方法的调用者
  • 当函数是单行简短的时候使用箭头函数
  • 当注册事件处理函数时,如果你希望函数中的this是动态作用域而不是词法作用域,那么 就不要使用箭头函数,反之才能使用箭头函数
  • 避免多行箭头函数作为函数的参数;单行简短的箭头函数作为函数的参数,并不会损害代码的可读性,尤其是在函数参数进行分行缩进显示的时候

3.字面量和解构

1.模板字符串和增强的对象字面量

//可内嵌表达式的模板字符串
//增强对象字面量的属性,方法以及计算属性
let name = "zheling"
{
    name,       //Shorthand Assignment Notation
    helloWorld(){},     //Shorthand Method Notation
    [`play${sport}`] : _=>{}    //Computed Properties
}

2.解构

  • 数组解构
//Extracting Array Values
const [firstName, middleName, lastName] = ['John', 'Quincy', 'Adams'];
//Ignoring Values
const [firstName, , lastName] = ['John', 'Quincy', 'Adams'];
//Extracting More than Available Values
const [firstName,, lastName, nickName] = ['John','Quincy','Adams'];//undefined
//Providing a Default Value
const [firstName,, lastName, nickName='Hello'] = ['John','Quincy','Adams'];
//Rest Extraction
const [firstName, ...otherParts] = ['John','Quincy','Adams'];
//Extracting into Existing Variable and Swapping
let [a, b] = [1, 2];
[a, b] = [b, a];
//Extracting Parameter Values
const printFirstAndLastOnly = function([first,, last]) {
	console.log(`first ${first} last ${last}`);
};
printFirstAndLastOnly(['John', 'Q', 'Adams']);
  • 对象解构
const weight = 'WeightKG';
const sam = {
    name: 'Sam',
    age: 2,
    height: 110,
    address: { street: '404 Missing St.'},
    shipping: { street: '500 NoName St.'},
    [weight]: 15,
    [Symbol.for('favoriteColor')]: 'Orange',
};
//Destructuring Objects
const { name: firstName, age: theAge } = sam;
//Extracting to Variables with the Same Name
const { name, age: theAge } = sam; //==> { name:name, age:theAge }
//Extracting Computed Properties
const { [weight]: wt, [Symbol.for('favoriteColor')]: favColor } = sam;
//Assigning Default Values
const { lat, lon, favorite = true} = {lat: 84.45, lon: -114.12};
//Extracting When Passing to a Function
const printInfo = function(person) { // function({name: theName, age: theAge})
	console.log(`${person.name} is ${person.age} years old`);
};
printInfo(sam);
//Deep Destructuring
const { name, address: { street } } = sam;
//Deal with Collisions
const { name, address: { street }, shipping: { street: shipStreet } } = sam;	
//Extracting into Existing Variables
let theName = '--';
({ name: theName } = sam);
//Extracting with …
const addAge = function(person, theAge) {
	return {...person, last: person.last.toUpperCase(), age: theAge };
}
const parker = { 
    first: 'Peter', 
    last: 'Parker',
    email: 'spiderman@superheroes.example.com' 
};
console.log(addAge(parker, 15));

# Part Three - OO and Modular Code

1.创建类

//构造函数的命名约定为首字母大写以区别于普通函数。
function Car() {this.turn = function(direction){console.log('...turning...');}}
//	or
function Car() {}
Car.prototype.turn = function(direction) { console.log('...turning...'); }
//	or
function Car() {}
Car.turn = function(direction) { console.log('...turning...'); 
/*
class类不会像函数声明类那样提升,只能先定义后使用。
默认有一个无参数的构造函数,构造函数无法直接调用,需要通过new来调用以初始化实例
为了让构造函数能够快速的创建实例对象,需要保持构造函数尽可能的简短。
*/
const NYD = `New Year's Day`;
class Holidays {
    constructor() {
    	this[NYD] = 'January 1';	//Defining Computed Members
    	this["Valentine's Day"] = 'February 14';
    }
    ['list holidays']() {	//new Holidays()["list holidays"]()
    	return Object.keys(this);
    }
}

2.创建属性

js中也提供了类似java中的setter/getter

//一个属性可以有getter和setter,若只有getter,那么该属性则是只读的
//intance.getAge(); 
getAge() {
	return new Date().getFullYear() - this.year;
}
//getter:直接返回一个字段值或计算值且是只读的
get age() {
	return new Date().getFullYear() - this.year;
}
/*不能直接调用 age() 因为其不是一个真正的方法,我们必须使用属性名来直接访问它。
/当我们执行赋值表达式 car.age = newValue 时,js将会查找 set age(value),若找到 setter 则完成赋值,若没有找到,那么js会忽略赋值表达式,但这在严格模式下会报错。
*/
Uncaught TypeError: Cannot set property name of #<Object> which has only a getter
一个属性的setter可用于在改变属性值之前进行一些检查验证.
但需要记住的是js中并没有提供一种封装属性权限的方式,即没有类似其它语言的私有属性的概念.
set distanceTraveled(value) {
    if(value < this.miles) {
    	throw new Error(`Sorry, can't set value to less than current distance traveled.`);
    }
    this.miles = value;
}

3.定义类成员

静态methods中的this是动态作用域,引用的是class对象本身并非实例对象, 类成员是通过类对象本身访问,实例成员不能通过类对象来访问。

Car.distanceFactor = 0.01; //This goes outside class Car {...}
static get ageFactor() { return 0.1; } //This goes inside class Car

4.类表达式

Class expressions 可用于在运行时动态创建类。 类表达式与类声明的两个主要区别:

  • 类表达式中类名是可选的,但在类声明中是必需的
  • 类表达式应对作为一个表达式来处理,即类表达式应该被返回,被传递给另一个函数或被存入一个变量中。
const createClass = function(...fields) {
    return class {
      	  	constructor(...values) {
       		fields.forEach((field, index) => this[field] = values[index]);
        }
    };
};

const Movie = class Show {
    constructor() {
    	console.log('creating instance...');
    	console.log(Show);
	}
};
console.log('Movie is the class name');
console.log(Movie);
const classic = new Movie('Gone with the Wind');
try {
    console.log('however...');
    console.log(Show);
} catch(ex) {
	console.log(ex.message);
}
//该例中,类名Show只在类内部可见,对外可见的类名只有Movie

5.新内置类:Set/Map,WeakSet/WeakMap

1.Set

//无顺序无重复值,Set: method = add/clear/delete/has...
const names = new Set(['Jack', 'Jill', 'Jake', 'Jack', 'Sara']);
names.add('Kate').add('Kara');//add返回当前的Set对象,可以链式调用
for(const name of names) {
	console.log(name);
}
names.forEach(name => console.log(name));
console.log(names.size); //size属性

//Set是可迭代对象,可用 for...of 迭代,也可使用展开运算符
//当需要像数组那样操作Set集合时可以将Set转为数组。
[...names].filter(name => name.startsWith('J')) //Array.from(names)
.map(name => name.toUpperCase())
.forEach(name => console.log(name));

2.Map

//Map是key-value键值对双列集合,key是唯一的且可以是任意的原始类型或对象。
//methods: size/has/keys/values/entries...
const scores = new Map([['Sara', 12], ['Bob', 11], ['Jill', 15]]);
scores.set('Jake', 14);
console.log(scores.size);
//use external iterator
for(const [name, score] of scores.entries()) {
	console.log(`${name} : ${score}`);
}
//internal iterator,forEach(value,key)
scores.forEach((score, name) => console.log(`${name} : ${score}`));
//Map是可迭代对象,可使用 for...of/entries()迭代,也可使用展开运算符。
//迭代项格式 : [key,value]

3.WeakSet/WeakMap

假设你想往 Set 中添加一个对象类型的值或往 Map 中使用一个对象作为key,
当该对象在后续不再使用时,它并不会被垃圾回收,因为Set/Map 中引用了该对象从而阻止其被清除。

这不利于内存的有效使用,在数据量大的应用程序中可能是一个大问题。
WeakSet,WeakMap用于解决这个问题,二者能够高效的使用内存。
WeakSet中的value和WeakMap中的key在应用程序不再需要它们的时候就会被垃圾回收,因此这些对象在没有任何通知的情况下可能随时被清除。
   
为了防止任何意外情况,WeakSet,WeakMap对元素作出了一些必要的限制:
1.Set:value/Map:key可能是原始类型或对象类型,要求WeakSet:value/WeakMap:key必须是对象类型,不能是原始类型。
2.weak集合无法被枚举也无法获取元素的数量,因为枚举期间集合中对象类型的value或key可能被垃圾回收,这会导致迭代出错。

The WeakSet 只提供了 add() , delete() , and has() 方法;
The WeakMap 只提供了 get() , delete() , set() , and has() 方法

const MAX = 100000000;
const map = new Map();	//const map = new WeakMap(); no error
for(let i = 0; i <= MAX; i++) {
	const key = {index: i};
	map.set(key, i);
}
console.log("DONE");
FATAL ERROR: invalid table size Allocation failed - JavaScript heap out of memory

6.使用继承

大部分主流语言都提供了类继承,但js却是通过原型来实现继承的。新的继承语法也不过是原型继承的语法糖而已。

基于原型的语言使用一个对象链来委派调用。原型继承依赖于原型链中下一个对象来作为它的父类。

基于类的继承相当不灵活,一旦继承了某个类,就会受到该类的局限。而在基于原型的语言中,继承是动态的,你可以在运行时改变父类。

1.原型链 / set Vs get 行为

class Counter {}
const counter1 = new Counter();
const counter2 = new Counter();
const counter1Prototype = Reflect.getPrototypeOf(counter1);
const counter2Prototype = Reflect.getPrototypeOf(counter2);
console.log(counter1 === counter2); //false
console.log(counter1Prototype === counter2Prototype); //true
//prototype chain: counter1/2 --- Counter{} --- {} --- null

//set and get
/*
    继承的目的在于重用属性和方法。
    类继承中,子类实例可以重用父类成员;
    在原型继承中,被重用的是原型对象prototype,当一个对象的属性或方法被访问时,该对象通过将调用委派给它的原型对象的方式来重用它的原型对象上的成员。
    但在获取属性与设置属性相比时,行为会有很大的差别。
*/
class Counter {}
Counter.prototype.count = 0;
Counter.prototype.increment = function() { this.count += 1; };
const counter1 = new Counter();
const counter2 = new Counter();
console.log(counter1.count); //0
console.log(counter2.count); //0
//this引用counter1,相当于为counter1添加count实例属性
counter1.increment();
console.log(counter1.count);//1
console.log(counter2.count);//0 
/*
规则:查找成员是深层的,而设置成员是浅层的。
(Gets search deep, but sets are always shallow.)
查找一个实例上的成员时,若实例上有该成员则直接返回,若没有则向上去实例的原型对象上查找,沿着原型链向上查找直到发现成员或原型链的顶端。
当设置一个成员时,则是直接在该实例对象上设置新成员。js并不会向上查找该成员是否已经存在,若实例对象上已存在该成员则直接覆盖,若不存在则是新添该成员。
*/

2.继承类

/*
    修改原型链:Reflect.setPrototypeOf(target,prototype);
修改原型链时需要非常谨慎,若是无意间改变的可能会导致无法预料且难以调试的行为。
    js在没有指定构造函数时会提供一个默认的构造函数,同样地对于子类也会提供一个默认的构造函数,默认地子类构造函数会自动调用并将所有的参数传递给父类的构造函数super().
*/
class Person {
    constructor(firstName, lastName) {
        console.log('initializing Person fields');
        this.firstName = firstName;
        this.lastName = lastName;
    }
    toString() {
        return `Name: ${this.firstName} ${this.lastName}`;
    }
    get fullName() { return `${this.firstName} ${this.lastName}`; }
    get surname() { return this.lastName; }
}
//inherit
class ReputablePerson extends Person {
    constructor(firstName, lastName, rating) {
        console.log('creating a ReputablePerson');
        super(firstName, lastName);
        this.rating = rating;
    }
    //overriding methods
    toString() {
    	return `${super.toString()} Rating: ${this.rating}`;
    }
    get fullName() {
    	return `Reputed ${this.surname}, ${super.fullName} `;
    }
}
/*
一个属性一个方法可能存在于父类,子类或实例对象上,所以需要小心地使用正确的语法来访问合适的成员,有如下规则:
	1.使用this来访问实例对象或子类的成员,注意this是动态作用域
	2.一个成员不存在于实例对象或子类而存在于父类,使用this;后续在子类中提供了该成员的覆盖,则以子类成员优先使用
	3.需要绕开实例对象或子类成员以访问父类成员使用 super()
	
这些情况下,我们才会使用 super. 前缀:
	1.访问父类中被子类所覆盖的成员
	2.在任何其它的成员中,当我们明确想要绕开子类,直接访问父类的成员时
*/
//即便是旧语法定义的类也能继承
function LegacyClass(value) {this.value = value;}
class NewClass extends LegacyClass {}
console.log(new NewClass(1));

3.使用 species 来管理实例的类型

//场景
class MyString extends String {}
class MyArray extends Array {}
const concString = new MyString().concat(new MyString());
const concArray = new MyArray().concat(new MyArray());
console.log(`instanceof MyString?: ${concString instanceof MyString}`);
console.log(`instanceof MyArray?: ${concArray instanceof MyArray}`);
//result
instanceof MyString?: false
instanceof MyArray?: true
/*
管理实例的类型
当实现一个父类的成员,我们可以:
    1.让返回的实例与父类相同
    2.让返回的实例与子类相同
    3.让子类告诉我们应该返回的类型
*/
class Names {
    constructor(...names) {
    	this.names = names;
    }
    //sticking to the base type
    filter1(selector) {
        //return base class type
    	return new Names(...this.names.filter(selector));
    }
    //Choosing Based on the Runtime Type
    filter2(selector) {
        const constructor = Reflect.getPrototypeOf(this).constructor;
        //return derived type
    	return new constructor(...this.names.filter(selector));
    }
    filter3(selector) {
        const constructor =
        Reflect.getPrototypeOf(this).constructor.kindHint ||
        Reflect.getPrototypeOf(this).constructor;	//return base class type
        return new constructor(...this.names.filter(selector));
    }
}
/*
Configuring the Type
当我们得到一个类时,其实得到了一个构造函数,可以从该类获取到它的静态方法和属性。
我们可以自定义一个静态属性,名称为 kindHint,用以告诉我们希望创建的实例类型,如果该属性不存在,
那么我们就退回去使用 constructor。
*/
class SpecializedNames extends Names {
    // made-up : kindHint is not undefined 
    static get kindHint() {
    	return Names;
    }
}
//usage
const subObj = new SpecializedNames('Java', 'C#', 'JavaScript');
console.log(subObj.filter1(name => name.startsWith('Java')));

subObj instanceof Names //true
subObj instancdof SpecializedNames //true
/**	
    使用 [Symbol.species] 改进
    上述方法是可行的但有一个隐患,就是自定义的 kindHint,名称是随意的,有可能不是唯一的,就会与其它成员造成冲突。
    回顾一下,Symbol就是为了解决接口和唯一标识的缺失问题而生的。
    我们可以使用Symbol来创建一个唯一的Symbol值,如:Symbol("KINDHINT"),但是js中已经为我们预定义了一个相同目的的Symbol---Symbol.species,其专门用于传达一个被用于创建子类对象的构造器。
*/
//improve previous eg
class SpecializedNames extends Names {
    static get [Symbol.species]() {
        return Names;
    }
}
filter3(selector) {
    const constructor =
        Reflect.getPrototypeOf(this).constructor[Symbol.species] ||
        Reflect.getPrototypeOf(this).constructor;
    return new constructor(...this.names.filter(selector));
}
//Array class也是类似上述的方式来使用 Symbol.species
class MyArray extends Array {
    //调用子类构造函数创建子类对象时使用的构造函数是 Array ,即创建结果是父类对象
	static get [Symbol.species]() { return Array; }
}
const concArray = new MyArray().concat(new MyArray());
console.log(`instanceof MyArray?: ${concArray instanceof MyArray}`);//false

7.使用模块化

简单来说,js中的一个模块就是一个包含变量,常量,类的独立的文件。模块中的代码默认在严格模式下执行,所以模块中的变量不会意外的溢出到全局作用域。

//module : right.mjs
console.log('executing right module');
const message = 'right called';
export const right = function() {
    console.log(message);
   };
// module : middle.mjs
console.log('executing middle module');
export const middle = function() {
	console.log('middle called');
};
// module : left.mjs
import { right } from './right';
import { middle } from './middle';
middle();
right();

//模块化的基本实现:闭包的自调用函数。
var DatePicker = (function(){
    return {
        init(){}
    }
})()

1.导出模块

//1.Inlining Exports:
export const FREEZING_POINT = 0;
export function f2c(fahrenheit) {
    return (fahrenheit - 32) / 1.8;
}
export class Thermostat {
    constructor() {
        //...initialization sequence
    }
}

//2.Exporting Explicitly: 
function c2f(celsius) {
	return celsius * 1.8 + 32;
}
const FREEZINGPOINT_IN_K = 273.15, BOILINGPOINT_IN_K = 373.15;
export { c2f, FREEZINGPOINT_IN_K };
//Exporting with a Different Name
export { c2k as celsiusToKelvin };

//3.Default Exports
export default function unitsOfMeasures() {
	return ['Celsius', 'Delisle scale', 'Fahrenheit', 'Kelvin', /*...*/];
}

//4.Reexporting from Another Module
//module : weather
export * from './temperature';
export { Thermostat, celsiusToKelvin } from './temperature';
export { Thermostat as Thermo, default as default } from './temperature';
/*	
    weather模块重导出了来自 temperature 模块中的 Thermostat 类,并赋予了一个新的名称 Thermo。
    weather模块的使用者将通过 Thermo 来访问 temperature 模块中的 Thermostat 类。
    同样地,weather模块将 temperature 模块的默认导出项作为自己的默认导出项。
*/

2.导入模块

//Importing Named Exports
import { FREEZING_POINT, celsiusToKelvin } from './temperature';
const fpInK = celsiusToKelvin(FREEZING_POINT);
//Resolving Conflicts
import { Thermostat } from './temperature';
import { Thermostat as HomeThermostat } from './home';
import * as home from './home';	//home.Thermostat
//Importing a Default Export
import { default as uom } from './temperature';
import uom from './temperature';
//Importing Both Default and Named Exports
import uom, { celsiusToKelvin, FREEZING_POINT as brrr } from './temperature';
//Importing Everything into a Namespace
import * as heat from './temperature';
/*
	通配符用于导入模块的所有导出项,但不包括默认导出项。默认导出项与通配符以逗号隔开分别导入。
*/
import uom, * as heat from './temperature';
//Importing for Side Effects
/*
	在少数场景下,某可能不需要导入任何东西而只是想要执行模块中的代码来获取其带来的副作用
*/
import 'some-side-effect-causing-module'

# Part Four - Going Meta 元编程

1.Promise

回调地狱:

  • 多层嵌套的回调难以维护与扩展,可读性差
  • 参数顺序被打乱,不连贯
  • 错误处理不连贯
/**
    从promise的状态推导出输出,promise有三种状态但并没有提供一个获取当前状态的查询方法,这是因为promise的状态随着时间会发生改变,我们获取的状态可能是与实际的状态是不同步的。
*/
/**
    promise一个优雅的特性是promise形成了一个管道,then和catch返回的是promise对象,所以这些方法可以以链式调用的方式来作一系列的过滤和转换操作
*/
const fs = require('fs-extra');
const countLinesWithText = function(pathToFile) {
    fs.readFile(pathToFile)
    .then(content => content.toString())
    .then(content => content.split('\n'))
    .then(lines => lines.filter(line => line.includes('THIS LINE')))
    .then(lines => lines.length)
    .then(count => checkLineExists(count))
    .then(count => console.log(`Number of lines with THIS LINE is ${count}`))
    .catch(error => console.log(`ERROR: ${pathToFile}, ${error.message}`));
    };
    const checkLineExists = function(count) {
    if(count === 0) 
        throw new Error('text does not exist in file');
	return count;
};
/*
    js提供两种可选的方式来处理多个异步任务:
    1.promise竞赛,结果取最先resolve或reject的promise:Promise.race
    2.等待所有的promise都resolve返回或其中一个reject返回:Promise.all
*/
//Racing Promises
Promise.race([createPromise(1000), createPromise(2000), createTimeout(3000)])
.then(result => console.log(`completed after ${result} MS`))
.catch(error => console.log(`ERROR: ${error}`));

//Gathering all promises
const fs = require('fs-extra');
const request = require('request-promise');
const countPrimes = function(number) {
    if(isNaN(number)) {
        return Promise.reject(`'${number}' is not a number`);
    }
    return request(`http://localhost:8084?number=${number}`)
    .then(count => `Number of primes from 1 to ${number} is ${count}`);
};
const countPrimesForEachLine = function(pathToFile) {
    fs.readFile(pathToFile)
    .then(content => content.toString())
    .then(content =>content.split('\n'))
    .then(lines => Promise.all(lines.map(countPrimes)))
    .then(counts => console.log(counts))
    .catch(error => console.log(error));
};
/*
    async-await的引入是为了让异步代码与同步代码在书写保持一致,即以同步代码的方式来书写异步代码。
    这并没有影响到异步方法的编写,但却在很大程度上改变了我们使用它们的方式。
关于该特性有两规则:
    • To be able to use an asynchronous function as if it were a synchronous
    function, optionally mark the promise-returning asynchronous function with the async keyword.
    • To call the asynchronous function as if it were a synchronous function,
    place the await keyword right in front of a call. The await keyword may be
    used only within functions marked async.
*/
const callCompute = async function(number) {
    try {
        const result = await computeAsync(number);
        console.log(`Result is ${result}`);
    } catch(ex) {
    	console.log(ex);
    }
}
/*
    promise可以完全解决回调地狱的问题,async - await并不是另一种替代方案而是作为一种增强方式。
    这里有一些原则可以让你决定是使用 then-catch 还是 async-await:
    • 若代码不在一个async function中,不能使用await,只能使用 then-catch;
    • 若你想将遗留的同步代码改为异步,推荐使用 async-await ,await 不会破坏原有的代码结构,而 then-catch 会破坏原有的代码结构。
    • 若将一个函数的异步版本转换为同步版本更容易时,此时 async-await 更优于 promise。
    • then-catch可能更适用于函数式编程,而 async-await 更适用于命令式编程。    
    await操作可以有返回值,这个返回值表示promise操作成功的返回值;
    await里面执行的异步操作发生reject,或发生错误,那么只能使用try...catch语法来进行错误处理

2.探索元编程

元编程是在运行时扩展编程的一种方式。元编程是js中最为复杂,
最新且最强大的特性之一。你可以使用元编程在你觉得适合的地方动态地扩展代码。
	元编程有两种方式:
	1.注入成员,member injection
	2.合成成员,member synthesis 
Injection is useful to add well-known methods to existing classes. 
Synthesis is useful to dynamically create methods and properties based on the current state of objects.
注入与合成:
    注入是一种让我们可以在不需要访问源码的前提下往类中添加或替换指定的方法或属性的技术。
    合成是相比注入更加动态,是更进一步的元编程,它需要比注入更多的成熟的实践。
当使用元编程时:
    1.保守使用,只有必要时才使用;
    2.不要在代码中随意放置注入或合并成员,应存放一个文件或目录,方便维护与定位;
    3.thorough code reviews
    4.编写严密的自动化测试,测试虽不能阻止错误的发生但却能防止错误的重复发生。好的测试可以增强自信,是减少元编程风险的最佳方式之一。

1.Member Injection

const text = new String('live');
try {
	text.reverse();
} catch(ex) {
	console.log(ex.message);
}
//往实例对象上注入方法
text.reverse = function() { return this.split('').reverse().join(''); };
//一般而言,相比于在类上或原型上,往实例对象上进行注入更安全
//往类原型对象上注入方法
String.prototype.reverse = function() { return this.split('').reverse().join(''); };
//注入属性
const today = new Date();
//Object.defineProperty(Date.prototype, 'isInLeapYear', {...});
Object.defineProperty(today, 'isInLeapYear', {
    get: function() {
    	const year = this.getFullYear();
    	return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
    }
});
console.log(`${today.getFullYear()} is a leap year?: ${today.isInLeapYear}`);
//Injecting Multiple Properties
Object.defineProperties(Array.prototype, {
    first: {
        get: function() { return this[0]; },
        set: function(value) { this[0] = value; }
    },
    last: {
        get: function() { return this[this.length - 1]; },
        set: function(value) { this[Math.max(this.length - 1, 0)] = value; }
    }
});

3.深入元编程

方法合成(Method synthesis)是元编程最强大的形式。你可以基于运行时上下文和对象的状态来改变 API 和 对象的行为。
方法合成直到近期新版本的js才有可能实现。在以往你所能做的只有 methods injection。

随着 method synthesis 以及两个新class,Reflect 和 Proxy的引入,你可以在方法执行期间拦截方法的调用并且自定义改变方法调用的命运与结果。
[With the introduction of method synthesis and two new classes— 
Reflect and Proxy —you can intercept method invocations right in the middle of their execution and change the fate of the calls, the way you like.]

Reflect是一个用于查找或访问属性、方法以及对象元数据的网关接口。
虽然Proxy代理是合成自定义行为的方法,但是最先进的装饰器是合成预定义行为的方法。换句话说,要创建自己的动态方法,请使用Proxy。
[While Proxy is the way to synthesize your own custom behavior, the state-of-
the-art decorator is the way to synthesize predefined behaviors. In other
words, to cook up your own dynamic methods, use Proxy. ]

1.Reflect

/*
两个主要目的:
    1.它是对象各种元操作的首选方案,如,Reflect提供了获取和设置一个对象的原型的方法以及检测对象中是否存在某个属性的该方法
    2.默认情况下,代理类将其方法路由到Reflect的方法。然后,当使用代理时,我们可以选择性的覆盖某些操作并方便地保留其余部分的默认实现。
*/
// 传统调用函数的三种方式:using () , call() , or apply() 
const greet = function(msg, name) {
	const pleasantry = typeof(this) === 'string' ? this : 'have a nice day';
	console.log(`${msg} ${name}, ${pleasantry}`);
};
greet('Howdy', 'Jane'); 
greet.call('howare you?', 'Howdy', 'Jane'); 
greet.apply('how are you?', ['Howdy', 'Jane']); 

//1.更受偏爱的替代方案 :通过Reflect来调用函数
Reflect.apply(greet, 'how are you?', ['Howdy', 'Jane']);
//此处看起来似乎是多余的,但Reflect ’s apply() 在改变函数的调用行为时非常有用

//2.Accessing the Prototype:获取和修改一个对象原型的优雅方式
const today = new Date();
console.log(Reflect.getPrototypeOf(today)); //Date{}
const myPrototype = {};
Reflect.setPrototypeOf(today, myPrototype);
console.log(Reflect.getPrototypeOf(today)); //{}

//3.Getting and Setting Properties
const sam = new Person(2);
const propertyName = 'age';
Reflect.set(sam, propertyName, 3);
console.log(Reflect.get(sam, propertyName));
//相比于使用 [] 获取和设置属性的方式并没有很明显的优势,反而更加冗长了。从这个角度看是这样,但当我们在 Proxy 的 context 下使用方法时,Reflect.get/set()就变得有用了。

//4.Reflect 还提供了类似 Object.keys() 的功能以及判断一个对象中是否存在某个属性的方法
console.log(Reflect.ownKeys(sam));
console.log(Reflect.has(sam, 'age'));

2.使用Proxy元编程

一个代理实例作为另一个对象或函数(称为target)的替身,它可以捕获(trap)和拦截target对象上的属性,方法或其它字段的访问与调用。

创建一个代理对象需要两个东西:

  • 被代理的target对象
  • 用于捕获和拦截target调用的处理程序 target调用指的是对target对象上的属性,方法或其它字段的访问、修改或调用。

1.基本使用

class Employee {
    constructor(firstName, lastName, yearOfBirth) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.yearOfBirth = yearOfBirth;
    }
	get fullname() { return `${this.firstName} ${this.lastName}`; }
	get age() { return new Date().getFullYear() - this.yearOfBirth; }
}
const printInfo = function(employee) {
    console.log(`First name: ${employee.firstName}`);
    console.log(`Fullname: ${employee.fullname}`);
    console.log(`Age: ${employee.age}`);
};
const john = new Employee('John', 'Doe', 2010);
//proxy
/*
 get()捕获器(trap),接收三个参数:
    • target : 被拦截捕获的目标对象
    • propertyName : 尝试获取的属性名称, 若调用 proxy.foo,那么 propertyName 即 "foo"
    • receiver : 接收到调用的 proxy 对象或者继承 proxy 的子类对象
*/
//使用 traps 来改变调用行为
const handler = {
    get: function(target, propertyName, receiver) {
            if(propertyName === 'firstName') {
                console.log(`target is john? ${john === target}`);
                console.log(`propertyName is ${propertyName}`);
                console.log(`receiver is proxyDoe? ${proxyDoe === receiver}`);
            }
            if(propertyName === 'age') {
                return `It's not polite to ask that question, dear`;
            }
            return Reflect.get(target, propertyName);
        }
};
const proxyDoe = new Proxy(john, handler);
printInfo(proxyDoe);

2.撤销代理

//可撤回的代理:执行 revoke function 之后代理对象不再能操作target对象了
const counterFactory = function() {
    class Counter {
        constructor() { this.value = 0; }
        increment() { this.value += 1; }
        get count() { return this.value; }
    }
    const { proxy: counterProxy, revoke: revokeFunction } =
    	Proxy.revocable(new Counter(), {});
    const leaseTime = 100;
    setTimeout(revokeFunction, leaseTime);
    return counterProxy;
};

3.使用Proxy拦截函数调用实现AOP

/*
	面向切面编程是元编程的一种特例,方法调用可能被 advices(通知) 拦截。通知是在特定上下文中执行的一段代码。
	AOP有三种类型的通知:
• Before advice: runs before the intended function call
• After advice: runs after the intended function call
• Around advice: runs instead of the intended function
	Proxy可用于实现AOP通知,使用 Reflect.apply()实现。
    默认情况下,代理处理程序(proxy's handler)将任何应用到代理上的调用路由到反射(Reflect)
*/
//no aop
const greet = function(message, name) {
	return `${message} ${name}!`;
};
const invokeGreet = function(func, name) {
	console.log(func('hi', name));
};
invokeGreet(greet, 'Bob');
//aop
const beforeAdvice = new Proxy(greet, {
    apply: function(target, thisArg, args) {
        const message = args[0];
        const msgInCaps = message[0].toUpperCase() + message.slice(1);
        return Reflect.apply(target, thisArg, [msgInCaps, ...args.slice(1)]);
    }
});
invokeGreet(beforeAdvice, 'Bob');
//a piece of code that runs after the call: toUpperCase 
const beforeAndAfterAdvice = new Proxy(greet, {
    apply: function(target, thisArg, args) {
        const newArguments = ['Howdy', ...args.slice(1)];
        const result = Reflect.apply(target, thisArg, newArguments);
        return result.toUpperCase();
    }
});
invokeGreet(beforeAndAfterAdvice, 'Bob');
//around advice
/*
	这就是一个环绕通知,它劫持调用并提供一个替代实现。环绕通知可能是有选择性的;它可以根据某些条件(参数的值、某些外部状态或配置参数等)绕过调用。
*/
const aroundAdvice = new Proxy(greet, {
    apply: function(target, thisArg, args) {
        if(args[1] === 'Doc') {
        	return "What's up, Doc?";
        }else {
        	return Reflect.apply(target, thisArg, args);
        }
    }
});
invokeGreet(aroundAdvice, 'Bob');
invokeGreet(aroundAdvice, 'Doc');
/*
	上例是假定没有发生错误,在实际中,我们需要做防御性的编程,用 try - finally 或 try - catch - finally 包裹 Reflect.apply() 。
	若无论结果成败都需要执行的代码则将通知代码放入 finally;
	若只有结果成功才需要执行的代码则将通知代码放入 try ,失败执行的放入 catch 中。
*/

4.使用代理合成成员

/*
    在编写代码时,我们可能并不知道需要被注入的属性的名称
*/
const langsAndAuthors = new Map([['JavaScript', 'Eich'], ['Java', 'Gosling']]);
const accessLangsMap = function(map) {
    console.log(`Number of languages: ${map.size}`);
    console.log(`Author of JavaScript: ${map.get('JavaScript')}`);
    console.log(`Asking fluently: ${map.JavaScript}`);
};
accessLangsMap(langsAndAuthors);
//result
Number of languages: 2
Author of JavaScript: Eich
Asking fluently: undefined

1.为实例对象合成成员

const handler = {
    //langsAndAuthors size/get/JavaScript Proxy{}
    get: function(target, propertyName, receiver) {
        if(Reflect.has(target, propertyName)) {
            const property = Reflect.get(target, propertyName);
            if(property instanceof Function) { //existing method, bind and return
                return property.bind(target);
            }
        	//existing property, return as-is
        	return property;
    	}
		//synthesize property: we assume it is a key
		return target.get(propertyName);
	}
};
const proxy = new Proxy(langsAndAuthors, handler);
accessLangsMap(proxy);
//result
Number of languages: 2
Author of JavaScript: Eich
Asking fluently: Eich
/*
    此种方案的缺点在于:作为动态属性的某个 key 只有在被基于某个特定Map实例的代理使用式才是有效的。
*/

2.直接为类合成成员

const proxy = new Proxy(Map.prototype, {
    //Map.prototype get/JavaScript langsAndAuthors
    //receiver : proxy 或者 继承proxy的对象
    get: function(target, propertyName, receiver) {
    	return receiver.get(propertyName);
    }
});
Reflect.setPrototypeOf(Map.prototype, proxy);
const langsAndAuthors = new Map([['JavaScript', 'Eich'], ['Java', 'Gosling']]);
console.log(langsAndAuthors.get('JavaScript')); //Eich
console.log(langsAndAuthors.JavaScript);//Eich

3.使用装饰者 Decorators

/*
js中Decorators逐渐进化为一个标准,nodejs中尚不支持Decorators。
许多库和框架都应用了现代JavaScript特性,使用这些库和框架的程序员常常依赖诸如Babel之类的编译器来将他们编写的代码转换成等价的老式JavaScript。
创建自定义的 Decorators:
	假定有一个Person类,其包含少数几个字段属性。用该类创建一个实例,叫 peter 并调用该实例的 toString() 方法。
console.log(peter.toString());
那么输出结果为:[object Object]
*/
//decorators.mjs - 为类实现toString方法
export const ToString = function(properties) {
	const exclude = (properties && properties.exclude) || [];
    return function(target) {
        target.prototype.toString = function() {
            return Object.keys(this)
                .filter(key => !exclude.includes(key))
                .map(key => `${key}: ${this[key]}`)
                .join(', ');
            };
            return target;
    }
}
class Person {
    constructor(firstName, lastName, age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
}
//usage
import Decorators from './decorators';
const overrideTargetToString = Decorators({exclude: ['age']});
	  overrideTargetToString(Person);
const peter = new Person('Peter', 'Parker', 23);
console.log(peter.toString());	//firstName: Peter, lastName: Parker