ECMAScript6 入门

Posted by Yinode on Friday, November 24, 2017

TOC

最近尝试了解一些函数式编程(Function program)的东西,但是很多的语法都用到了ES6,所以先开始学习一下ES6吧。特别是promise对象和箭头函数。

主要的功能并不是详细的记录,毕竟在网上都可以很方便的找到,所以主要是为了让自己对ES6的新特性有一个映像。

感谢阮一峰老师提供的ES6入门 http://es6.ruanyifeng.com

let 和 const

这两个声明变量的方式是ES6之中很有代表性的部分,接下来对一些特性进行一下总结记录。

let

  • 不存在变量提升(不会存在var未声明就变成undefiend的情况)

  • 暂时性死区 如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。当变量被声明就解除死区

  • 不允许重复声明

  • 块级作用域 (需配合let)

const

const的本质是无法修改这个变量的内存地址 也就是说如果 const a = {}; a.name = "jack" 是可以被接受的

他拥有和let相似的特性:不提升、死区、不允许重复声明,块作用域

全局对象

在浏览器环境中 let a = 1 不会变成window的属性。


解构

解构是指从数组和对象中提取值,对变量进行赋值。

例子

let [a, b, c] = [1, 2, 3];

他尝试进行一种模式匹配,如果不成功,则undefined。两边必须都为数组,如果LHR不可以被迭代器迭代,则会报错。解构赋值允许指定默认值。

解构的常见用途

值的交换

 let x = 1;
	let y = 2;
	
	[x, y] = [y, x]; //直接对xy的值进行交换 而不需要tmp来缓存

遍历map结构

 var map = new Map();
	map.set('first', 'hello');
	map.set('second', 'world');
	
	for (let [key, value] of map) {
	  console.log(key + " is " + value);
	}

输入一个模块的某些方法

const { SourceMapConsumer, SourceNode } = require("source-map");

字符串扩展

Unicode

ES只支持\u0000~\uFFFF之间的UTF表示法

在ES6之中得到了改进 可以使用大括号来表示大于这个范围的UTF字符串

"\u{20BB7}"

字符串的遍历

ES6为字符串添加了遍历器接口,使得字符串可以被for…of循环遍历。

includes(), startsWith(), endsWith()

在ES之中增加了检测一个字符串中是否包含参数字符串的方法只有IndexOf一种,现在增加了三种

var s = 'Hello world!';
	
	s.startsWith('world', 6) // true
	s.endsWith('Hello', 5) // true
	s.includes('Hello', 6) // false

分别为是否包含,是否在起始位置包含,是否在结尾处包含

repeat()

字面意思,返回字符串,代表将原字符串重复指定次数

padStart(),padEnd()

用于字符串的补全

模板字符串

在ES5之中写一个将被插入到HTML之中的HTML代码体验并不是很好,特别是要加入一些变量,这意味着复杂的引号。在ES6之中添加了模版字符串的概念。


$('#result').append(`
	  There are <b>${basket.count}</b> items
	   in your basket, <em>${basket.onSale}</em>
	  are on sale!
	`);

模版字符串以反引号起始和结束,中途需要用到反引号需转义

如果在模版字符串中使用变量标识符,需要使用 ${varname} 来使用

 let name  = "zhang";
	let box = `<div>${name}</div>`;
	
	$("p").append(box);

标签模板

标签模版实质上是特殊的一种函数调用方式。其调用方法为在函数名的后面接着一个字符模版。字符模版可以被认为是调用的参数

    var a = 5;
	var b = 10;
	
	tag`Hello ${ a + b } world ${ a * b }`;
	// 等同于
	tag(['Hello ', ' world ', ''], 15, 50);

tag函数的第一个参数是一个数组,该数组的成员是模板字符串中那些没有变量替换的部分,也就是说,变量替换只发生在数组的第一个成员与第二个成员之间、第二个成员与第三个成员之间,以此类推。

var total = 30;
var msg = passthru`The total is ${total} (${total*1.05} with tax)`;

function passthru(literals) {
  var result = '';
  var i = 0;

  console.log(literals[0]);
 
  while (i < literals.length) {
    result += literals[i++];
    if (i < arguments.length) {
      result += arguments[i];
    }
  }

  return result;

  //literals中包含的是一个数组 其中包含了无法被解析的字符串
  //argumens中的0为literals 12...成功被变量解析的部分 并且他从1开始
  //literals中的字符串每一项可以被认为是两个变量之间的部分 
  //也就是说结构基本上是间隔分布的 字符 变量 字符 变量 字符
  //以上代码就是将接受的东西原生去返回
}
 

以下为tag函数的应用实例。过滤用户的恶意输入。


var message =
  SaferHTML`<p>${sender} has sent you a message.</p>`;

function SaferHTML(templateData) {
  var s = templateData[0];
  for (var i = 1; i < arguments.length; i++) {
    var arg = String(arguments[i]);

    // 通过正则匹配所有恶意内容替换
    s += arg.replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");

    // 拼接后续的字符串
    s += templateData[i];
  }
  return s;
}
 

数值部分

  • Number.MAX_SAFE_INTEGER
  • Number.MIN_SAFE_INTEGER
  • Number.EPSILON 用于检测浮点精度
  • Number.isInteger()
  • Number.parseInt()
  • Number.parseFloat()
  • Number.isFinite()
  • Number.isNaN()

可以看到ES6想让JS往模块化的方向走,上面的parseInt和parseFloat都被整合进了Number里面,尽量减少对全局作用域的依赖。

数组

  • Array.from() 讲一个类数组或者对象转换成数组
  • Array.of() 将传入的参数创建成一个数组,弥补传统Array构造函数的不足
  • 数组实例的copyWithin() 用于数组部分的替换
  • find() 用于找出第一个符合条件的数组成员 参数为回调函数 如果返回为ture则返回这个值
  • findIndex() 类似find 但返回是一个index下标

遍历

entries(),keys()和values() ES6提供三个新的方法——entries(),keys()和values()——用于遍历数组。它们都返回一个遍历器对象。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"



如果不使用for…of循环,可以手动调用遍历器对象的next方法,进行遍历。

数组实例的 includes()

Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。

空位

在ES5中是对于空位的处理不太一致,知道ES6更加严格了一点,但是应该避免空位的出现。

函数

函数的默认值

在ES6之前需要用 var y = y||"world" 这样来做到参数设置默认值,现在允许在直接创建默认值

function log(x, y = 'World') {
  console.log(x, y);
}

如果参数默认值是变量,那么参数就不是传值的,而是每次都重新计算默认值表达式的值。

function fetch(url, { method = 'GET' } = {}) {
  console.log(method);
}

fetch('http://example.com')
// "GET"

双重默认值 如果没有传入对象就利用一个空对象,如果传入对象就在内部进行一个解构。

通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。

function f(x = 1, y) {
  return [x, y];
}

f() // [1, undefined]
f(2) // [2, undefined])
f(, 1) // 报错
f(undefined, 1) // [1, 1]

除非你写Undefined 不然是不能省略参数而调用默认值的。

函数的length

指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。 因为函数的length属性指的是函数的预期参数的个数,而指定了默认值了之后就会从预期参数中移除。

作用域

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。

关键在于声明初始化的时候 以及后期函数运行期间的变量是分成两个层次的,具有严格的先后顺序。

应用

  • 利用默认值来做到如果不传入参数就爆一个异常

    
    function throwIfMissing() {
    throw new Error('Missing parameter');
    }
    
    function foo(mustBeProvided = throwIfMissing()) {
    return mustBeProvided;
    }
    
    foo()
    // Error: Missing parameter
    
    

rest参数

以前通常调用函数内部的所有参数数组需要将arguments转换成数组 这个rest可以来简化这一操作


function add(...values) {
  let sum = 0;

  for (var val of values) {
    sum += val;
  }

  return sum;
}

add(2, 5, 3) // 10

//取代arguments取代arguments取代arguments取代arguments

// arguments变量的写法
function sortNumbers() {
  return Array.prototype.slice.call(arguments).sort();
}

// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();


注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。

箭头函数

var f = () => 5;
// 等同于
var f = function () { return 5 };

var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
  return num1 + num2;
};

主要规则如下

  • 如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
  • 如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。
  • 箭头函数可以与变量解构结合使用。

用法展示

  • 简化回调函数
  • // 正常函数写法
    [1,2,3].map(function (x) {
    return x * x;
    });
    
    // 箭头函数写法
    [1,2,3].map(x => x * x);
    
    
  • 与rest结合

    const numbers = (...nums) => nums;
    
    numbers(1, 2, 3, 4, 5)
    // [1,2,3,4,5]
    
    const headAndTail = (head, ...tail) => [head, tail];
    
    headAndTail(1, 2, 3, 4, 5)
    // [1,[2,3,4,5]]
    
    

注意点

  1. 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

  2. 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

  3. 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

  4. 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

箭头函数导致this总是指向函数定义生效时所在的对象.

转换成ES5代码之后其实利用的也是that这方法来生成一个tmp保存this对象。虽然可能只是一种语法糖,但确实很方便

除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。所以你无法对箭头函数使用bind方法apply call方法。

一定要小心这个问题,自身永远不拥有this,都是引用的外层。

非常适合在callback这种情况下下使用,其他情况还是少用为妙。

this绑定

箭头函数可以绑定this对象,大大减少了显式绑定this对象的写法(call、apply、bind)。但是,箭头函数并不适用于所有场合,所以ES7提出了“函数绑定”(function bind)运算符,用来取代call、apply、bind调用。虽然该语法还是ES7的一个提案,但是Babel转码器已经支持。

蹦床函数

function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();
  }
  return f;
}

上面就是蹦床函数的一个实现,它接受一个函数f作为参数。只要f执行后返回一个函数,就继续执行。注意,这里是返回一个函数,然后执行该函数,而不是函数里面调用函数,这样就避免了递归执行,从而就消除了调用栈过大的问题。

function tco(f) {
  var value;
  var active = false;
  var accumulated = [];

  return function accumulator() {
    accumulated.push(arguments);
    if (!active) {
      active = true;
      while (accumulated.length) {
        value = f.apply(this, accumulated.shift());
      }
      active = false;
      return value;
    }
  };
}

var sum = tco(function(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1)
  }
  else {
    return x
  }
});

sum(1, 100000)
// 100001

上面代码中,tco函数是尾递归优化的实现,它的奥妙就在于状态变量active。默认情况下,这个变量是不激活的。一旦进入尾递归优化的过程,这个变量就激活了。然后,每一轮递归sum返回的都是undefined,所以就避免了递归执行;而accumulated数组存放每一轮sum执行的参数,总是有值的,这就保证了accumulator函数内部的while循环总是会执行。这样就很巧妙地将“递归”改成了“循环”,而后一轮的参数会取代前一轮的参数,保证了调用栈只有一层。

对象

属性的简洁表示法

function f(x, y) {
  return {x, y};
}

// 等同于

function f(x, y) {
  return {x: x, y: y};
}

f(1, 2) // Object {x: 1, y: 2}

也就是说他允许直接使用变量作为对象的属性或者方法,其中变量的标识符等于属性的名字,变量的内容等同于属性的内容

适用于模块的暴露

module.exports = { getItem, setItem, clear };
// 等同于
module.exports = {
  getItem: getItem,
  setItem: setItem,
  clear: clear
};

属性名表达式

let propKey = 'foo';

let obj = {
  [propKey]: true,
  ['a' + 'bc']: 123
};

ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。

适用于你需要在name上调用表达式的情况

不要与简洁表达式法同用,报错

Object.assign()

Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。

他的实现原理和jQuery的exanted基本类似,注意是浅复制也就是复制指针的方式,当源对象内部有属性为对象时,这种复制是复制指针的,source和target对象共享一个内部属性对象。如果要深复制完全隔离。可以通过转成字符串对象,然后再转换成js对象。

var target = { a: 1 };

var source1 = { b: 2 };
var source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

主要用途,添加属性,添加方法,克隆对象,实现一个对象合并几个对象,实现默认值(参数1为目标对象,2为默认的对象内部有默认参数,3为用户传入的具体对象参数)

原型对象三大操作

Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作) 在ES6中都是推荐的标准

Object.keys(),Object.values(),Object.entries()

  • ES2017 引入了跟Object.keys配套的Object.values和Object.entries,作为遍历一个对象的补充手段,供for…of循环使用。

这三者单独使用都是返回一个相应内容的数组,前提是可遍历。

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

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

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

Object.entries方法的另一个用处是,将对象转为真正的Map结构。x

let {keys, values, entries} = Object;
let obj = { a: 1, b: 2, c: 3 };

for (let key of keys(obj)) {
  console.log(key); // 'a', 'b', 'c'
}

for (let value of values(obj)) {
  console.log(value); // 1, 2, 3
}

for (let [key, value] of entries(obj)) {
  console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
}

Symbol

一种新的数据类型,用于避免属性名冲突。

所有的symbol都是不相等的,传入的参数也只是描述符而已。


var mySymbol = Symbol();

// 第一种写法
var a = {};
a[mySymbol] = 'Hello!';

// 第二种写法
var a = {
  [mySymbol]: 'Hello!'
};

// 第三种写法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上写法都得到同样结果
a[mySymbol] // "Hello!"


所有的symbol都不会被遍历出来,但是可以由Object.getOwnPropertySymbols获取

Set Map

set

类似数组,但里面没有重复的值


const s = new Set();

[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));

for (let i of s) {
  console.log(i);
}
// 2 3 5 4


上面代码通过add方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。

Set 函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。

首先,WeakSet 的成员只能是对象,而不能是其他类型的值。其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

所以他比较适合临时存放数据,

map

传统的JS对象也是类似map的键值对,但是键只能为字符串。

而在ES6新增的map中,允许值-值,键可以是任意一种数据。

所以map是一种更加完善的hash结构



const m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')
m.get(o) // "content"

m.has(o) // true
m.delete(o) // true
m.has(o) // false

Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。


const map = new Map([
  ['name', '张三'],
  ['title', 'Author']
]);

map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"

map不用考虑键名碰撞的问题,因为实际上键里保存的是内存地址。

结合数组的map方法、filter方法,可以实现 Map 的遍历和过滤(Map 本身没有map和filter方法)。


const map0 = new Map()
  .set(1, 'a')
  .set(2, 'b')
  .set(3, 'c');

const map1 = new Map(
  [...map0].filter(([k, v]) => k < 3)
);
// 产生 Map 结构 {1 => 'a', 2 => 'b'}

const map2 = new Map(
  [...map0].map(([k, v]) => [k * 2, '_' + v])
    );
// 产生 Map 结构 {2 => '_a', 4 => '_b', 6 => '_c'}

Proxy

可以理解为在某个对象的外层套了一个壳 当你set或者get操作的时候,可以进行拦截,比如get的时候检查属性是否存在,set的时候参数是否正确。

Reflect

他的作用有很多,一部分是简化操作,一部分是确保在代理的时候也能调用原生的操作。

promise

promise是一种异步解决方案,


Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 Pending 变为 Resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 Pending 变为 Rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise实例生成以后,可以用then方法分别指定Resolved状态和Reject状态的回调函数。

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

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

promise新建后会立即执行


var getJSON = function(url) {
  var promise = new Promise(function(resolve, reject){
    var client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.responseType = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();

    function handler() {
      if (this.readyState !== 4) {
        return;
      }
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
  });

  return promise;
};

getJSON("/posts.json").then(function(json) {
  console.log('Contents: ' + json);
}, function(error) {
  console.error('出错了', error);
});

Iterator 和 for…of 循环

JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了Map和Set。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是Map,Map的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。

遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是ES6创造了一种新的遍历命令for…of循环,Iterator接口主要供for…of消费。

Iterator 的遍历过程是这样的。

(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。

(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。

(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

下面是一个模拟next方法返回值的例子。


function makeIterator(array) {
  var nextIndex = 0;
  return {
    next: function() {
      return nextIndex < array.length ?
        {value: array[nextIndex++]} :
        {done: true};
    }
  };
}

默认 Iterator 接口

Iterator 接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即for…of循环(详见下文)。当使用for…of循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。

一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是”可遍历的“(iterable)。

原生具备 Iterator 接口的数据结构如下。

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象

Generator 函数的语法

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。本章详细介绍 Generator 函数的语法和 API,它的异步编程应用请看《Generator 函数的异步应用》一章。

Generator 函数有多种理解角度。从语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。


function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(hello和world),即该函数有三个状态:hello,world 和 return 语句(结束执行)。

然后,Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。

下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

yield

如果把Generator函数看成是一种可以断点的函数,那么yield可以被认为是设置断点的方法。