【前端学习】ECMAScript 6入门

ECMAScript 6入门

let和const命令

  1. let命令声明的变量只在let命令所在的代码块内有效
  2. let所声明的变量一定要在声明后使用,否则报错
  3. 块级作用域的出现,实际上使得广泛应用的匿名立即执行函数表达式(匿名IIFE)不再必要了
  4. const只能保证指向实际数据的指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了
  5. ES6规定,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性

变量的解构赋值

  1. 对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,而对象的属性没有次序
  2. 对象的解构赋值可以取到继承的属性
  3. 字符串也可以解构赋值,类似数组的对象都有一个length属性,还可以对这个属性解构赋值
  4. 解构赋值的规则是:只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错
  5. 函数的参数也可以使用解构赋值,下面是一个使用默认值的函数参数解构例子:
function move({x = 0, y = 0} = {}) {
  return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
  1. 可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号
  2. 变量的解构赋值可以用于从函数返回多个值:
// 返回一个数组
function example() {
  return [1, 2, 3];
}
let [a, b, c] = example();

// 返回一个对象
function example() {
  return {
    foo: 1,
    bar: 2
  };
}
let { foo, bar } = example();

字符串的扩展

  1. ES6为字符串添加了遍历器接口,使得字符串可以被for...of循环遍历
for (let codePoint of 'foo') {
  console.log(codePoint)
}
  1. 如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中
  2. 模板字符串中,可以使用<%...%>放置JavaScript代码,使用<%= ... %>输出JavaScript表达式
let template = `
<ul>
  <% for (let i = 0; i < data.supplies.length; i++) { %>
    <li><%= data.supplies[i] %></li>
  <% } %>
</ul>
`
  1. 模板字符串可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。如果模板字符里面有变量,就会先将模板字符串处理成多个参数,再调用函数
let a = 5;
let b = 10;

function tag(s, v1, v2) {
  console.log(s[0]);
  console.log(s[1]);
  console.log(s[2]);
  console.log(v1);
  console.log(v2);
  return 'OK';
}
tag`Hello ${ a + b } world ${ a * b }`;
// "Hello "
// " world "
// ""
// 15
// 50
// "OK"

字符串的新增方法

  1. codePointAt()方法是测试一个字符由两个字节还是由四个字节组成的最简单方法
  2. ES6提供了新的字符串方法includes()startsWith()endsWith()
  3. ES2017引入了字符串补全长度的功能,padStart()用于头部补全,padEnd()用于尾部补全
  4. ES2019对字符串实例新增了trimStart()trimEnd()两个方法,用于消除空格

正则的扩展

  1. ES6可以使用第二个参数为正则对象指定修饰符,但返回的正则表达式会忽略原有正则表达式的修饰符,只使用新指定的修饰符
  2. ES6对正则表达式添加了u修饰符,含义为“Unicode模式”,用来正确处理大于\uFFFF的Unicode字符。利用这一点,可以写出一个正确返回字符串长度的函数
function codePointLength(text) {
  var result = text.match(/[\s\S]/gu);
  return result ? result.length : 0;
}

var s = '𠮷𠮷';
s.length  // 4
codePointLength(s)  // 2
  1. ES6对正则表达式添加了y修饰符,它的设计本意就是让头部匹配的标志^在全局匹配中都有效
  2. ES6新增了dotAll模式,即点(dot)代表一切字符
  3. 先行断言指的是,x只有在y前面才匹配,必须写成/x(?=y)/,先行否定断言指的是,x只有不在y前面才匹配,必须写成/x(?!y)/。后行断言正好相反,x只有在y后面才匹配,必须写成/(?<=y)x/。后行否定断言指的是x只有不在y后面才匹配,必须写成/(?<!y)x/,下面是几个例子:
/\d+(?=%)/.exec('100% of US presidents have been male') // ['100']
/\d+(?!%)/.exec('that\'s all 44 of them') // ['44']
/(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill')  // ['100']
/(?<!\$)\d+/.exec('it\'s worth about €90')  // ['90']
  1. 后行断言的组匹配,与正常情况下结果是不一样的,其反斜杠引用也与通常的顺序相反,必须放在对应的那个括号之前
  2. ES2018引入了一种新的类的写法\p{...}\P{...},允许正则表达式匹配符合Unicode某种属性的所有字符
  3. ES2018引入了具名组匹配,允许为每一个组匹配指定一个名字。字符串替换时,使用$<组名>引用具名组
const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;

const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj.groups.year;  // 1999
const month = matchObj.groups.month;  // 12
const day = matchObj.groups.day;  // 31

数值的扩展

  1. ES6在Number对象上,新提供了Number.isFinite()Number.isNaN()两个方法
  2. Number.EPSILON可以用来设置“能够接受的误差范围”,它的实质是一个可以接受的最小误差范围
function withinErrorMargin (left, right) {
  return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);
}

0.1 + 0.2 === 0.3 // false
withinErrorMargin(0.1 + 0.2, 0.3) // true

1.1 + 1.3 === 2.4 // false
withinErrorMargin(1.1 + 1.3, 2.4) // true
  1. ES2016新增了一个指数运算符(**),它的一个特点是右结合,而不是常见的左结合
  2. ES2020引入了一种新的数据类型BigInt(大整数),只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示,但它不能与普通数值进行混合运算

函数的扩展

  1. ES6允许为函数的参数设置默认值,即直接写在参数定义的后面
  2. 参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的
let x = 99;
function foo(p = x + 1) {
  console.log(p);
}

foo() // 100

x = 100;
foo() // 101
  1. 参数默认值可以与解构赋值的默认值结合起来使用
// 写法一
// 函数参数默认值是空对象,但是设置了对象解构赋值的默认值
function m1({x = 0, y = 0} = {}) {
  return [x, y];
}

// 写法二
// 函数参数默认值是有具体属性的对象,但是没有设置对象解构赋值的默认值
function m2({x, y} = {x: 0, y: 0}) {
  return [x, y];
}

// 函数没有参数的时候
m1()  // [0, 0]
m2()  // [0, 0]

// x和y都有值的时候
m1({x: 3, y: 8})  // [3, 8]
m2({x: 3, y: 8})  // [3, 8]

// x有值,y无值的情况
m1({x: 3})  // [3, 0]
m2({x: 3})  // [3, undefined]

// x和y都无值的情况
m1({})  // [0, 0]
m2({})  // [undefined, undefined]

m1({z: 3})  // [0, 0]
m2({z: 3})  // [undefined, undefined]
  1. 通常情况下,定义了默认值的参数,应该是函数的尾参数
  2. ES6引入rest参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中
  3. 箭头函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。this对象的指向是可变的,但是在箭头函数中,它是固定的
  4. 以下两个场合不应该使用箭头函数:
  • 定义对象的方法,且该方法内部包括this
  • 需要动态this的时候

数组的扩展

  1. 扩展运算符(spread)是三个点(...)。它好比rest参数的逆运算,将一个数组转为用逗号分割的参数序列。它主要用于函数调用
function push(array, ...items) {
  array.push(...items);
}

function add(x, y) {
  return x + y;
}

const numbers = [4, 38];
add(...numbers) // 42
  1. 扩展运算符还可以将字符串转为真正的数组,如[...'hello']。这有一个重要的好处,那就是能够正确识别四个字节的Unicode字符
  2. Array.from方法支持类似数组的对象和没有部署遍历器接口(Symbol.iterator)的对象。这些对象都可以通过Array.from方法转为数组,而此时扩展运算符就无法转换
  3. indexOf方法无法识别数组的NaN成员,但是findIndex方法可以借助Object.is方法左到:
[NaN].indexOf(NaN)
// -1

[NaN].findIndex(y => Object.is(NaN, y))
// 0
  1. ES6明确将空位转为undefined
  2. ES2019明确规定,Array.prototype.sort()的默认排序算法必须稳定。这个规定已经做到了,现在JavaScript各个主要实现的默认排序算法都是稳定的

对象的扩展

  1. ES6又新增了一个类似的关键字super,指向当前对象的原型对象
  2. 对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }
x // 1
y // 2
z // { a:3, b: 4 }
  1. 对象的扩展运算符等同于使用Object.assign()方法
  2. 如果想完整克隆一个对象,还拷贝对象原型的属性,可以采用下面的写法:
// 写法一
const clone1 = {
  __proto__: Object.getPrototypeOf(obj),
  ...obj
}

// 写法二
const clone2 = Object.assign(
  Object.create(Object.getPrototypeOf(obj)),
  obj
);

// 写法三
const clone3 = Object.create(
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
)
  1. 扩展运算符的参数对象之中,如果有取值函数get,这个函数是会执行的
  2. ES2020引入了链判断运算符?.,简化层层运算
const firstName = message?.body?.user?.firstName || 'default';
const fooValue = myForm.querySelector('input[name=foo]')?.value;

对象的新增方法

  1. Object.assign()方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)
  2. Object.getOwnPropertyDescriptors()方法的引入目的,主要是为了解决Object.assign()无法正确拷贝get属性和set属性的问题。它配合Object.defineProperties()方法就可以实现正确拷贝
const shallowMerge = (target, source) => Object.defineProperties(
  target,
  Object.getOwnPropertyDescriptors(source)
);
  1. Object.getOwnPropertyDescriptors()方法的另一个用途,是配合Object.create()方法,将对象属性克隆到一个新对象,这属于浅拷贝
const shallowClone = (obj) => Object.create(
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
)
  1. Object.getOwnPropertyDescriptors()方法可以实现一个对象继承另一个对象
const obj = Object.create(
  prot,
  Object.getOwnPropertyDescriptors({
    foo: 123,
  })
);

Symbol

  1. 每一个Symbol值都是不相等的,这意味着Symbol值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块组成的情况非常有用,能防止某一个键被不小心改写或覆盖
  2. Symbol作为属性名,遍历对象时不会出现在for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。但它也不是私有属性,有一个Object.getOwnPropertySymbols()方法可以获得指定对象所有Symbol属性名
  3. Reflect.ownKeys()方法可以返回所有类型的键名,包括常规键名和Symbol键名

Set和Map数据结构

  1. 向Set加入值时认为NaN等于自身,而精确相等运算符认为NaN不等于自身
  2. Set结构的键名就是键值(两者是同一个值)
  3. WeakSet的成员会随时消失,不适合引用。同时,WeakSet不可遍历
  4. Map结构提供了“值——值”的对应,是一种更完善的Hash结构实现

Proxy

  1. Proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程
  2. 要使得Proxy起作用,必须针对Proxy实例进行操作,而不是针对目标对象进行操作。下面是一个用Proxy进行拦截的实例:
var obj = new Proxy({}, {
  get: function (target, propKey, receiver) {
    console.log(`getting ${propKey}!`);
    return Reflect.get(target, propKey, receiver);
  },
  set: function (target, propKey, value, receiver) {
    console.log(`setting ${propKey}!`);
    return Reflect.set(target, propKey, value, receiver);
  }
})

obj.count = 1
// setting count!
++obj.count
// getting count
// setting count
// 2
  1. has方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效,典型的操作就是in运算符。下面的例子使用has方法隐藏某些属性,不被in运算符发现:
var handler = {
  has (target, key) {
    if (key[0] === '_') {
      return false;
    }
    return key in target;
  }
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
'_prop' in proxy  // false
  1. 虽然for...in循环也用到了in运算符,但是has拦截对for...in循环不生效
  2. Proxy.revocable()方法返回一个可取消的Proxy实例。它的一个使用场景是目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问
let target = {};
let handler = {};

let {proxy, revoke} = Proxy.revocable(target, handler);

proxy.foo = 123;
proxy.foo // 123

revoke();
proxy.foo // TypeError: Revoked
  1. 在Proxy代理的情况下,目标对象内部的this关键字会指向Proxy代理

Reflect

  1. Reflect对象得设计目的有这样几个:

(1) 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty)放到Reflect对象上
(2) 修改某些Object方法的返回结果,让其变得更合理
(3) 让Object操作都变成函数行为
(4) Reflect对象的方法与Proxy对象的方法一一对应

  1. 用Proxy可以写一个观察者模式的最简单实现,即实现observableobserve这两个函数。思路是observable函数返回一个原始对象的Proxy代理,拦截赋值操作,触发充当观察者的各个函数
const queuedObservers = new Set();

const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});

function set(target, key, value, receiver) {
  const result = Reflect.set(target, key, value, receiver);
  queuedObservers.forEach(observer => observer());
  return result
}

const person = observable({
  name: '张三',
  age: 20
});

function print() {
  console.log(`${person.name}, ${person.age}`)
}

observe(print);
person.name = '李四';
// 输出
// 李四, 20

Promise对象

  1. 一般来说,调用resolvereject以后,Promise的使命就完成了,后续操作应该放到then方法里面,而不应该直接写在resolvereject的后面。所以,最好在它们前面加上return语句防止意外
  2. 采用链式的then,可以指定一组按照次序调用的回调函数
  3. Promise.race()方法是将多个Promise实例包装成一个新的Promise实例。这些Promise实例中,只要有一个实例率先改变状态,新实例的状态就会跟着改变。那个率先改变的Promise实例返回值,就传递给新实例的回调函数
  4. 由于Promise.try为所有操作提供了统一的处理机制,所以如果想用then方法管理流程,最好都用Promise.try包装一下。这样有许多好处,其中一点就是可以更好地管理异常

Iterator 和 for...of 循环

  1. Iterator的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费
  2. yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口
  3. for...of获得的是对象的键值,而for...in获得的是对象的键名
  4. for...of直接遍历普通的对象会出错

Generator 函数的语法

  1. 一个生成Hello World的Generator函数helloWorldGenerator的例子如下:
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
var hw = helloWorldGenerator();

hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: false }
hw.next()
// { value: undefined, done: true }
  1. 由于Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的Symbol.iterator属性,从而使得该对象具有Iterator接口
  2. yield表达式本身没有返回值,或者说总是返回undefinednext方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值
  3. for...of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法
  4. Generator 函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历 Generator 函数
  5. ES6提供了yield*表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数
  6. 可以并行执行、交换执行权的线程(或函数),就称为协程。它是以多占用内存为代价,实现多任务的并行

Generator 函数的异步应用

  1. JavaScript语言中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
  return function (callback) {
    return fs.readFile(fileName, callback);
  };
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);
  1. Thunk函数是Generator函数自动执行的一种方案,生产环境可使用thunkify。下面是一个读取文件的例子:
var fs = require('fs');
var thunkify = require('thunkify');
var readFileThunk = thunkify(fs.readFile);

var gen = function* () {
  var r1 = yield readFileThunk('/etc/fstab');
  console.log(r1.toString());
  var r2 = yield readFileThunk('/etc/shells');
  console.log(r2.toString());
}

var run = function (fn) {
  var gen = fn();
  
  function next(err, data) {
    var result = gen.next(data);
    if (result.done) return;
    result.value(next);
  }

  next();
}

run(gen);

async 函数

  1. 前文的gen函数改写成async函数如下:
const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
}
  1. async函数的执行与普通函数一样,只需要一行,不需要调用next方法或者使用co模块
  2. async函数完全可以看作多个异步操作包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖
  3. 有时,我们希望即使一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await放在try...catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行
  4. 只有async函数内部是继发执行,外部不受影响。一个并发读取远程 URL 并按顺序输出的例子如下:
async function logInOrder(urls) {
  // 并发读取远程URL
  const textPromises = urls.map(async url => {
    const response = await fetch(url);
    return response.text();
  });

  // 按次序输出
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}

Class 的基本语法

  1. 基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能 ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已
  2. 与 ES5 一样,实例的属性除非定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)
  3. 与 ES5 一样,类的所有实例共享一个原型对象
  4. 类的内部方法如果含有this,它默认指向类的实例,但是,必须非常小心,一旦单独使用该方法,很可能报错。一个比较简单的解决方法是,在构造方法中绑定this
class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
    // 也可以使用箭头函数:this.getThis = () => this
  }

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

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

const logger = new Logger();
const { printName } = logger;
printName();
  1. Class 内部调用new.target,返回当前 Class

Class 的继承

  1. 由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的
  2. ES6 规定,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例

Module 的语法

  1. 模块功能主要由两个功能组成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能
  2. 模块整体加载所在的那个对象应该是可以静态分析的,所以不允许运行时改变,下面的写法都是不允许的
import * as circle from './circle';

// 下面两行都是不允许的
circle.foo = 'hello';
circle.area = function () {};
  1. export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任何名字
  2. require是运行时加载模块,import命令无法取代require的动态加载功能,但 ES2020 提案引入import()函数,支持动态加载模块

Module 的加载实现

  1. defer是“渲染完再执行”,async是“下载完就执行”。如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的
  2. 浏览器加载 ES6 模块,也使用<script>标签,但是要加入type="module"属性
  3. 模块之中,顶层的this关键字返回undefined,而不是指向window
  4. ES6 模块与 CommonJS 模块有两个重大差异:
  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
  1. CommonJS 模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值
  2. ES6 模块是动态引用,如果使用import从一个模块加载变量,那些变量不会被缓存,而是成为一个指向被加载对象的引用,需要开发者自己保证,真正取值的时候能够取到值

编程风格

  1. 使用数组成员对变量赋值时,优先使用解构赋值
  2. 函数的参数如果是对象的成员,优先使用解构赋值
  3. 如果函数返回多个值,优先使用对象的解构赋值,而不是数组的解构赋值
  4. 如果对象添加属性不可避免,要使用Object.assign方法

异步遍历器

  1. 异步遍历器最大的语法特点,就是调用遍历器的next方法,返回的是一个 Promise 对象
  2. Node v10 支持异步遍历器,Stream 就部署了这个接口。下面是读取文件的传统写法与异步遍历器写法的差异:
// 传统写法
function main(inputFilePath) {
  const readStream = fs.createReadStream(
    inputFilePath,
    { encoding: 'utf8', highWaterMark: 1024 }
  );
  readStream.on('data', (chunk) => {
    console.log('>>> ' + chunk);
  });
  readStream.on('end', () => {
    console.log('### DONE ###');
  });
}

// 异步遍历器写法
async function main(inputFilePath) {
  const readStream = fs.createReadStream(
    inputFilePath,
    { encoding: 'utf8', highWaterMark: 1024 }
  );
  for await (const chunk of readStream) {
    console.log('>>> ' + chunk)
  }
  console.log('### DONE ###')
}
  1. 异步 Generator 函数内部,能够同时使用awaityield命令,可以这样理解,await命令用于将外部操作产生的值输入函数内部,yield命令用于将函数内部的值输出
posted @ 2020-05-25 18:20  アカツキ  阅读(116)  评论(0编辑  收藏  举报