JavaScript ES6基础

本文主要介绍es6,包括了块级作用域、数组对象的解构赋值、...操作符、基本类型的扩展、引用类型的扩展(Array、Set、Map),函数的扩展、对象的扩展、class、异步等内容,最后是装饰器的简单介绍,还有一些特性并未加入,比如Generator、yield,proxy等内容。

块级作用域

在es5中,只有全局作用域与函数作用域,没有块级作用域。es6通过引入let与const,实际上新增了块级作用域。对于var还是与es5兼容。

let

  • let命令所声明的变量,只在所在的代码块内有效
  for (let i=0; i<10; i++){}
  console.log(i);   // ReferenceError
  • 不存在变量提升
    在es5中,var声明的变量,会发生变量提升,即脚本开始运行时,变量已经存在,但没有值。
  console.log(foo)  // 输出undefined
  console.log(bar)  // Error
  var foo = 3;
  let bar = 4;
  • 不允许重复声明
    let不允许在相同作用域内,重复声明同一个变量。

const

const如其他语言的const,在JavaScript中,与let相同也是在块级作用域中。

解构赋值

数组的解构赋值

像是照搬了python的处理,也增加了自己的处理,如:

  • 初识
    python中,可以使用 x,y,z = 1,2,3来定义变量
    JavaScripty中,使用 let [x,y,z] = [1,2,3] 或者直接 [x,y,z]=[1,2,3]

    python中可以使用 x,y = y,x来交换变量
    JavaScripty中需要使用 [x,y]=[y,x]

  • 默认值

    var [x, y="b"] = ["a"] // x='a', y='b'

    这里需要注意es6中使用 === undefined来判断是否有值

  var [x=1] = [undefined];  // x = 1
  var [y=1] = [null];  //y=null

对象的解构赋值

  • 初识
   var {foo,bar} = {foo:"aaa", bar:"bbb"}
   foo  // "aaa"
   bar  // "bbb"
  • 数组元素是按次序排列的,变量取值由位置决定,而对象属性没有次序,变量必须与属性名相同。
   var {bar,foo} = {foo:"aaa", bar:"bbb"}
   foo  // "aaa"
   bar  // "bbb"
  • 如果左边的变量名,与右边的属性名不一样,则必须如下:
  let obj = { first: 'hello', last: 'world' };
  let { first: one, last: two } = obj; 
  one // 'hello'
  two // 'world'

字符串的解构赋值

 const [a,b,c] = "world"
 a // "w"
 b // "o"
 c // "r"

应用

解构赋值应用之一就是函数参数的结构赋值,但后边又有...操作符及默认传参。
返回参数通过这种方式倒是可以一起返回多个参数

  • 返回多个参数
  function example() {
    return [1, 2, 3];
  }
  var [a, b, c] = example();
  • 提取Json或者对象
  var jsonData = {
    id: 42,
    status: "OK",
    data: [867, 5309]
  };

  let { id, status, data: number } = jsonData;

  console.log(id, status, number);
  // 42, "OK", [867, 5309]

运算符

spread运算符(...)

  • 初识
    将一个数组转为用逗号分隔的参数序列。
   console.log(...[1, 2, 3])
   // 1 2 3
   
   console.log(1, ...[2, 3, 4], 5)
   // 1 2 3 4 5
  • 应用
    • 合并数组
    [1, 2].concat(more)  // es5
    [1, 2, ...more]   // es6
  • 与解构赋值结合
     const [first, ...rest] = [1, 2, 3, 4, 5];
     first // 1
     rest  // [2, 3, 4, 5]
  • 字符串
    [...'hello']
    // [ "h", "e", "l", "l", "o" ]
  • 对象也可用
    扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中
  let z = { a: 3, b: 4 };
  let n = { ...z };
  n // { a: 3, b: 4 }

基本类型扩展

字符串的扩展

  • includes(), startsWith(), endsWith()
    es5中只有indexOf()方法来判断一个字符串是否包含在另一个字符串中。es6新增以上3个。
  var s = 'Hello world!';

  s.startsWith('Hello') // true
  s.endsWith('!') // true
  s.includes('o') // true

同时都支持第二个参数,表示开始搜索的位置

  • repeat()
    repeat方法返回一个新字符串,表示将原字符串重复n次
  'x'.repeat(3) // "xxx"
  • padStart(), padEnd()
    字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart用于头部补全,padEnd用于尾部补全。
  'x'.padStart(4, 'ab') // 'abax'
  'x'.padEnd(4, 'ab') // 'xaba'
  • 模板字符串
    模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

    // 字符串中嵌入变量

  var name = "Bob", time = "today";
  `Hello ${name}, how are you ${time}?`

数值的扩展

  • 二进制与八进制
    ES6提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示。

  • Number.isFinite(), Number.isNaN()

  Number.isFinite()用来检查一个数值是否为有限的(finite)。
  Number.isNaN()用来检查一个值是否为NaN。

  Number.isFinite(15); // true
  Number.isFinite(0.8); // true
  Number.isFinite(NaN); // false
  Number.isFinite(Infinity); // false
  Number.isFinite('foo'); // false
  Number.isFinite('15'); // false
  Number.isFinite(true); // false
  
  Number.isNaN(NaN) // true
  Number.isNaN(15) // false
  Number.isNaN('15') // false
  Number.isNaN(true) // false
  Number.isNaN(9/NaN) // true
  Number.isNaN('true'/0) // true
  • Number.isInteger()
    Number.isInteger()用来判断一个值是否为整数。需要注意的是,在JavaScript内部,整数和浮点数是同样的储存方法,所以3和3.0被视为同一个值。
  Number.isInteger(25) // true
  Number.isInteger(25.0) // true
  Number.isInteger(25.1) // false
  • Number.EPSILON
    ES6在Number对象上面,新增一个极小的常量Number.EPSILON。原因不言自明
  Number.EPSILON
  // 2.220446049250313e-16

引用类型扩展

数组的扩展

  • Array.from()
    Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括ES6新增的数据结构Set和Map)
  let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
  };
  let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

我觉得,在Map、set向数组的转换比较有用,arrayLike本来就是数组,谁也不会那样定义,arguments感觉在es6中在弱化。

  Array.from('hello')
  // ['h', 'e', 'l', 'l', 'o']

  let namesSet = new Set(['a', 'b'])
  Array.from(namesSet) // ['a', 'b']

Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

  Array.from([1, 2, 3], (x) => x * x)  // [1, 4, 9]
  • Array.of()
    Array.of方法用于将一组值,转换为数组。
  Array.of(3, 11, 8) // [3,11,8]
  Array.of(3) // [ 3 ]
  Array.of(3).length // 1
  • 数组实例的copyWithin()
    数组实例的copyWithin方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组.
    三个参数: target(必需),开始替换位置; start(可选),开始读取位置,默认0; end(可选),到该位置前停止读取数据,默认等于数组长度。
   [1, 2, 3, 4, 5].copyWithin(0, 3)
   // [4, 5, 3, 4, 5]
  • 数组实例的find()和findIndex()
    数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员,没有则返回undefined
  [1, 4, -5, 10].find((n) => n < 0)
  // -5

  [1, 5, 10, 15].find(function(value, index, arr) {
   return value > 9;
  }) // 10
 
  [1, 5, 10, 15].findIndex(function(value, index, arr) {
     return value > 9;
  }) // 2
  • 数组实例的fill()
    fill方法使用给定值,填充一个数组。
  new Array(3).fill(7)
  // [7, 7, 7]
  • 数组实例的entries(),keys()和values()
    entries(),keys()和values()——用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),可以用for...of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历
    对于数组感觉也很鸡肋
  for (let index of ['a', 'b'].keys()) {
    console.log(index);
  }
  // 0
  // 1
  
  for (let elem of ['a', 'b'].values()) {
    console.log(elem);
  }
  // 'a'
  // 'b'
  
  for (let [index, elem] of ['a', 'b'].entries()) {
    console.log(index, elem);
  }
  // 0 "a"
  // 1 "b"
  • 数组实例的includes()
    Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似
  [1, 2, 3].includes(2);     // true
  • 数组的空位
    数组的空位指,数组的某一个位置没有任何值.空位不是undefined,一个位置的值等于undefined,依然是有值的.
    ES5对空位的处理,已经很不一致了,大多数情况下会忽略空位。ES6则是明确将空位转为undefined
  Array.from(['a',,'b'])
  // [ "a", undefined, "b" ]
  
  扩展运算符(...)也会将空位转为undefined
  [...['a',,'b']]
  // [ "a", undefined, "b" ]

函数的扩展

  • 函数参数的默认值
    在ES6之前,不能直接为函数的参数指定默认值,ES6允许为函数的参数设置默认值,即直接写在参数定义的后面。默认参数与C++类似,最好放在后边。
  function log(x, y = 'World') {
    console.log(x, y);
  }

  log('Hello') // Hello World
  log('Hello', 'China') // Hello China

参数默认值可以与解构赋值的默认值,结合起来使用。

  function foo({x, y = 5}) {
    console.log(x, y);
  }
  
  foo({}) // undefined, 5
  foo({x: 1}) // 1, 5
  foo({x: 1, y: 2}) // 1, 2
  • 函数的length属性
    指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。
  (function (a) {}).length  // 1
  (function (a, b, c = 5) {}).length // 2
  • rest参数
    ES6引入rest参数(形式为“...变量名”),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。这个与python也类似,.args与..kargs
  function add(...values) {
    let sum = 0;
  
    for (var val of values) {
      sum += val;
    }
  
    return sum;
  }
  
  add(2, 5, 3) // 10

  var args = [0, 1, 2];
  add(...args);  // 3
  • 箭头函数
    参数多余一个用(),语句多于一条用{}
  var sum = (num1, num2) => { return num1 + num2; }

箭头函数可以与变量解构结合

  const full = ({ first, last }) => {first +' ' + last};
  // 等同于
  function full(person) {
   return person.first + ' ' + person.last;
  }

箭头函数有几个使用注意点。
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。
(4)不可以使用yield命令,因此箭头函数不能用作Generator函数。

  • 尾调用优化
    尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数,并返回,就是函数式变成可以链起来。
    正面实例:
 function f(x){
   return g(x);
 }

反面实例:

  // 情况一
  function f(x){
    let y = g(x);
    return y;
  }
  
  // 情况二
  function f(x){
    return g(x) + 1;
  }
  
  // 情况三
  function f(x){
    g(x);
  }

上面代码中,如果函数g不是尾调用,函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除 f(x) 的调用帧,只保留 g(3) 的调用帧。

这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。

  • 尾递归
    函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

    递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。

对象的扩展

  • 属性的简写
    ES6允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁

    变量

  var foo = 'bar';
  var baz = {foo};
  baz // {foo: "bar"}
  
  // 等同于
  var baz = {foo: foo};

函数

  var o = {
    method() {
      return "Hello!";
    }
  };
  
  // 等同于  
  var o = {
    method: function() {
      return "Hello!";
    }
  };
  • Object.assign()
    Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。
  var target = { a: 1 };
  var source1 = { b: 2 };
  var source2 = { c: 3 };
  
  Object.assign(target, source1, source2);
  target // {a:1, b:2, c:3}

Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。

  • 属性的遍历
    ES6一共有5种方法可以遍历对象的属性。

    (1)for...in
    for...in循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)。

    (2)Object.keys(obj)
    Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)。

    (3)Object.getOwnPropertyNames(obj)
    Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)。

    (4)Object.getOwnPropertySymbols(obj)
    Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有Symbol属性。

    (5)Reflect.ownKeys(obj)
    Reflect.ownKeys返回一个数组,包含对象自身的所有属性,不管是属性名是Symbol或字符串,也不管是否可枚举。

  • Object.keys()
    ES5引入了Object.keys方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。

    var obj = { foo: "bar", baz: 42 };
    Object.keys(obj)
    // ["foo", "baz"]

    Object.values(), Object.entries()在提案中。

set与map

这个大概是学习了C++

set

var set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]
set.size // 4

// 去除数组的重复成员
[...new Set(array)]

属性
Set.prototype.size:返回Set实例的成员总数。
Set实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。

操作方法:
add(value):添加某个值,返回Set结构本身。
delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
has(value):返回一个布尔值,表示该值是否为Set的成员。
clear():清除所有成员,没有返回值。

遍历方法:
keys():返回键名的遍历器
values():返回键值的遍历器
entries():返回键值对的遍历器
forEach():使用回调函数遍历每个成员

WeakSet
WeakSet结构与Set类似,也是不重复的值的集合。但是,它与Set有两个区别。
首先,WeakSet的成员只能是对象,而不能是其他类型的值。
其次,WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存

Map

  • 初识
    JavaScript的对象(Object),本质上是键值对的集合(Hash结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
    为了解决这个问题,ES6提供了Map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object结构提供了“字符串—值”的对应,Map结构提供了“值—值”的对应,是一种更完善的Hash结构实现。
  var map = new Map([
    ['name', '张三'],
    ['title', 'Author']
  ]);
  
  map.size // 2
  map.has('name') // true
  map.get('name') // "张三"
  • 注意
    这个map与C++类似,但有一点需要注意:
    只有对同一个对象的引用,Map结构才将其视为同一个键。
  var map = new Map();
  
  map.set(['a'], 555);
  map.get(['a']) // undefined

实际上这是两个['a']值,内存地址是不一样的,因此get方法无法读取该键,返回undefined

  • 操作方法:
    set(key, value)
    get(key)
    has(key)
    delete(key)
    clear()清空所有成员

  • 遍历方法:
    keys():返回键名的遍历器。
    values():返回键值的遍历器。
    entries():返回所有成员的遍历器。
    forEach():遍历Map的所有成员。

  • 相互转换
    数组转成Map

  let newMap =  new Map([[true, 7], [false, 6]])
    // Map {true => 7, false => 7}

Map转为数组
[...newMap]

对象转成Map

  function objToStrMap(obj) {
    let strMap = new Map();
    for (let k of Object.keys(obj)) {
      strMap.set(k, obj[k]);
    }
    return strMap;
  }
  
  objToStrMap({yes: true, no: false})
  // [ [ 'yes', true ], [ 'no', false ] ]

Map转成对象,如果所有Map键都是字符串,它可以转为对象,起始不是也可以先转换成字符串,再转

  function strMapToObj(strMap) {
    let obj = Object.create(null);
    for (let [k,v] of strMap) {
      obj[k] = v;
    }
    return obj;
  }
  
  let myMap = new Map().set('yes', true).set('no', false);
  strMapToObj(myMap)
  // { yes: true, no: false }

class

概述

es6终于脱离了es5要使用构造函数与原型相结合的方法来完成类定义的方式,提供了class关键字,在class内部声明的函数就被定义在了原型上,class内部有construtor函数,在其中定义的属性,就是在构造函数上。

es5代码

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);

等同于es6的

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

类的数据类型就是函数

typeof Point     //"function"
Point === Point.prototype.constructor  // true

constructor方法

constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。

constructor方法默认返回实例对象(即this)。

class表达式

与函数一样,类也可以使用表达式的形式定义。

const MyClass = class Me {
  getClassName() {
    return Me.name;
  }
};
let inst = new MyClass();
inst.getClassName();  // Me

class私有方法

es6中,并不直接提供私有方法。可以通过一些技术来模拟,但感觉都不好。

this的指向

类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。
以下边例子来说明:

class Logger {
  printName(name = 'there') {
    this.print(`Hello ${name}`);
  }

  print(text) {
    console.log(text);
  }
}

const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined

我稍微有点不理解,好不容易封装的函数,再解构出来用,几个意思啊?

class的继承

es5中的继承麻烦的要命,既要用subClass的原型链的constructor指向superClass的实例来继承函数,还要通过在subClass的构造函数中去调用superClass.call(this,..)来继承对象,简直不想让人用啊。
好在es6中增加了extends关键字来简化了继承,下边来看一下。

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

在构造函数中调用super()应该用于从构造函数中继承对象,然后外边的函数类似于原型继承了。
子类必须在constructor方法中调用super方法,否则新建实例时会报错。

类的prototype属性和__proto__属性

Class作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。
(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。
(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

Class的取值(getter)和存值函数(setter)

与ES5一样,在Class内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为.

class MyClass {
  constructor() {
    // ...
  }
  get prop() {
    return 'getter';
  }
  set prop(value) {
    console.log('setter: '+value);
  }
}

let inst = new MyClass();

inst.prop = 123;
// setter: 123

inst.prop
// 'getter'

class的静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用。
这个有点意思,我们看的构造函数(类)的prototype原型也就是静态的方法,又像Java/C++学习增加了static方法。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

父类的静态方法,可以被子类继承。

class的静态属性和实例属性

只有这种写法可行,因为ES6明确规定,Class内部只有静态方法,没有静态属性。对于这条在TypeScirpt中已经该了。

class MyClass {
  static myStaticProp = 42;   // 静态属性
  myProp = 43;                // 实例属性

  constructor() {
    console.log(MyClass.myProp); // 42
    console.log(this.myProp);   //43
  }
}

异步

前言

ES6诞生以前,异步编程的方法,大概有下面四种。
回调函数
事件监听
发布/订阅
Promise 对象
ES6将JavaScript异步编程带入了一个全新的阶段,ES7的Async函数更是提出了异步编程的终极解决方案。

异步
异步就像中断,启动一个耗时操作,然后程序可以继续运行,等耗时操作的中断返回再执行耗时操作的处理。

回调函数

所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。

fs.readFile('/etc/passwd', function (err, data) {
  if (err) throw err;
  console.log(data);
});

Promise

回调函数本身并没有问题,它的问题出现在多个回调函数嵌套。假定读取A文件之后,再读取B文件,代码如下。

fs.readFile(fileA, function (err, data) {
  fs.readFile(fileB, function (err, data) {
    // ...
  });
});

不难想象,如果依次读取多个文件,就会出现多重嵌套,Promise就是为了解决这个问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用.

var readFile = require('fs-readfile-promise');

readFile(fileA)
.then(function(data){
  console.log(data.toString());
})
.then(function(){
  return readFile(fileB);
})
.then(function(data){
  console.log(data.toString());
})
.catch(function(err) {
  console.log(err);
});

使用了fs-readfile-promise模块,它的作用就是返回一个Promise版本的readFile函数。Promise提供then方法加载回调函数,catch方法捕捉执行过程中抛出的错误。

async函数

ES7提供了async函数,使得异步操作变得更加方便。

var asyncReadFile = async function (){
  var f1 = await readFile('/etc/fstab');
  var f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

返回值是Promise。async函数的返回值是Promise对象

注意:
第一点,await命令后面的Promise对象,运行结果可能是rejected,,所以最好把await命令放在try...catch代码块中。

一般写法:

  async function myFunction() {
    await somethingThatReturnsAPromise()
    .catch(function (err) {
      console.log(err);
    };
  }

第二点,多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。

  let foo = await getFoo();
  let bar = await getBar();

getFoo和getBar是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo完成以后,才会执行getBar,完全可以让它们同时触发。

let [foo, bar] = await Promise.all([getFoo(), getBar()]);

第三点,await命令只能用在async函数之中,如果用在普通函数,就会报错。

    async function dbFuc(db) {
    let docs = [{}, {}, {}];
  
    // 报错
    docs.forEach(function (doc) {
      await db.post(doc);
    });
  }

上面代码会报错,因为await用在普通函数之中了。但是,如果将forEach方法的参数改成async函数,也有问题。

    async function dbFuc(db) {
    let docs = [{}, {}, {}];
  
    // 可能得到错误结果
    docs.forEach(async function (doc) {
      await db.post(doc);
    });
  }

原因是这时三个db.post操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用for循环。

  async function dbFuc(db) {
    let docs = [{}, {}, {}];
  
    for (let doc of docs) {
      await db.post(doc);
    }
  }

Decorator

简介

es的装饰器应该与python类似,使用闭包技术,将要修改的函数传入装饰函数即可。还有Generator,yield以及协程概念,都像是与python的相互借鉴。
修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。

function testable(target) {
  target.isTestable = true;
}

@testable
class MyTestableClass {}

console.log(MyTestableClass.isTestable) // true

@testable就是一个修饰器。它修改了MyTestableClass这个类的行为,为它加上了静态属性isTestable
如果觉得一个参数不够用,可以在修饰器外面再封装一层函数。

function testable(isTestable) {
  return function(target) {
    target.isTestable = isTestable;
  }
}

@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true

@testable(false)
class MyClass {}
MyClass.isTestable // false

类方法

修饰器不仅可以修饰类,还可以修饰类的属性。
此时,修饰器函数一共可以接受三个参数,第一个参数是所要修饰的目标对象,第二个参数是所要修饰的属性名,第三个参数是该属性的描述对象。

class Person {
  @readonly
  name() { return `${this.first} ${this.last}` }
}

function readonly(target, name, descriptor){
    // descriptor对象原来的值如下
    // {
    //   value: specifiedFunction,
    //   enumerable: false,
    //   configurable: true,
    //   writable: true
    // };
    descriptor.writable = false;
    return descriptor;
 }

@log修饰器,可以起到输出日志的作用。

class Math {
  @log
  add(a, b) {
    return a + b;
  }
}

function log(target, name, descriptor) {
  var oldValue = descriptor.value;    // descriptor.value就是方法add

  descriptor.value = function() {     // 在这里重新赋值desciptor.value,应该包含对原函数的调用
    console.log(`Calling "${name}" with`, arguments);
    return oldValue.apply(null, arguments);
  };

  return descriptor;
}

const math = new Math();

// passed parameters should get logged now
math.add(2, 4);

装饰器不能用于函数

修饰器只能用于类和类的方法,不能用于函数,因为存在函数提升

# 语言 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×