ES6详细介绍及使用

一、 ES6概念及发展史

1、ES6概念

    以前学习JavaScript的时候,对ES5是有了解过的,但是在学习Vue的时候,就会发现有很多新的写法是ES6中的,真是让人捉急。所以今天ES6他来了。原英文文档:https://262.ecma-international.org/6.0/#sec-functiondeclarationinstantiation

    W3C中对ECMAScript 6定义:ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了(所以也被叫作ES2015)。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

    ES6中大概做了哪些改变呢?

2、ES6发展史

版本时间描述

ECMA-262/

ECMAScript 1.0

19971996 年 11 月,JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给标准化组织 ECMA,希望这种语言能够成为国际标准。次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript,这个版本就是 1.0 版
ECMAScript 2.019981998年6月诞生了ECMAScript 2.0,这一版的内容更新是为了与ISO/IEC-16262保持严格一致,没有作任何新增、修改或删节处理。
ECMAScript 3.019991999年12月诞生了ECMAScript 3.0,此版本应用非常广泛,成为JavaScript语法基础(JavaScript = ECMAScript(核心) + DOM + BOM)。新增了对正则表达式、新控制语句、try-catch异常处理的支持,修改了字符处理、错误定义和数值输出等内容。
ECMAScript 4.020002000年,ES4.0 发布,新标准几乎是区别于ES3的新语言,由于4.0版的目标过于激进,各方对于是否通过这个标准,发生了严重分歧。争论了几年...以Yahoo、Microsoft、Google为首的大公司,反对JavaScript的大幅升级,主张小幅改动;以JavaScript创造者Brendan Eich为首的Mozilla公司,则坚持当前的草案。
ECMAScript 3.120082008年7月,ES3.1 发布,作为 ES4.0 的替代方案,中止了 ES4.0的发布;
ECMAScript5.020092009年,ES3.1 作为第五版(改名为ECMAScript 5)正式发布,由于变动太大,未进行推广应用;
ECMAScript5.120112011年,ES5.1 发布,成为国际标准;到了2012年底,所有主要浏览器都支持ECMAScript 5.1版的全部功能。
ECMAScript201520152015年6月,ECMAScript 6正式发布,并且更名为“ECMAScript 2015”,也是指 ES5.1 后的下一代JavaScript标准;

S6是继S5之后的一次主要改进,语言规范由ES5.1时代的245页扩充至600页。尽管ES6做了大量的更新,但是它依旧完全向后兼容以前的版本。

ES6增添了许多必要的特性,新功能包括:模块和类以及一些实用特性,例如Maps、Sets、Promises、生成器(Generators)等。

ECMAScript20162016TC39委员会计划,以后每年发布一个ECMAScirpt的版本,下一个版本在2016年发布,称为“ECMAScript 2016”

二、 块级作用域变量声明(let、const)

    先复习下ES5中的作用域

    在Java或C#中存在块级作用域,即:大括号也是一个作用域。而在ES5中每个函数作为一个作用域,在外部无法访问内部作用域中的变量(通常认为ES5存在两种作用域,即:全局作用域和函数作用域)。如果出现函数嵌套函数,就会出现作用域链。其寻找顺序为根据作用域链从内到外的优先级寻找,如果内层没有就逐步向上找,直到没找到抛出异常。

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<title></title>
	</head>
	<body>
	</body>
	<script>
		var xo = '全局';
		function Func(){
		    var xo = "局部";
		    function inner(){
		        var xo = '嵌套';
		        console.log(xo);
		    }
		    inner();
		}
		//在函数被调用之前作用域链已经存在:全局作用域 -> Func函数作用域 -> inner函数作用域
		//当执行Func时,会调用inner函数,在函数中就已经存在了xo变量。
		Func(); //嵌套
	</script>
</html>

    声明提前:

    在js中用var ,function声明的变量都将被前;

    函数声明的优先级大于变量声明的优先级(在赋值声明的时候首先会去方法域中寻找,而后才去变量域中寻找);

    在函数内部变量提升的优先级会小于函数参数;

<script>
	//声明提前等效于var a;console.log(a);a = 3;
	console.log(x);//undefined 而不会出现
  	var x = 3;
  
  	console.log(a);//function a(){console.log(x);}
  	function a(){
  		console.log(x);
  	}
  	var a = 1;
</script>

1、let命令

    ES6 新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。

1.1、let声明的变量不存在变量提升

// var 的情况
console.log(foo); // 输出undefined
var foo = 2;

// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

1.2、let声明的变量不允许重复

// 报错
function func() {
  let a = 10;
  var a = 1;
}

// 报错
function func() {
  let a = 10;
  let a = 1;
}

//不报错
function func(arg) {
  {
    let arg;
  }
}

1.3、let声明的变量块级作用域内有效

<script>
	var s = 'hello';
	for (var i = 0; i < s.length; i++) {
		console.log(s[i]);
	}
	//按习惯来说,这里的i,不应该是5
	console.log(i); // 5
	
	var s1 = 'hello';
	for(let j=0;j<s.length;j++){
	   console.log(s[0]);
	}
	//j不存在,这样比较符合我们的思想
	console.log(j); // Uncaught ReferenceError: j is not defined
</script>

2、const命令

    const声明一个只读的常量。一旦声明,常量的值就不能改变。const的作用域与let命令相同:只在声明所在的块级作用域内有效。const命令声明的常量也是不提升。const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。

const PI = 3.1415;
PI // 3.1415

PI = 3;// TypeError: Assignment to constant variable.

const foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

    注:顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。ES6 为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。

var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1

let b = 1;
window.b // undefined

三、 解构与赋值

    先看一个栗子:

//在ES5中我们会这么写
var arr =[111,222,333];
var first = arr[0];
var second = arr[1];
var third = arr[2];

//但是在ES6中我们可以这样写
let [first, second, third] = arr;

//本质上,这种写法属于“模式匹配”、“映射关系”

    什么是解构?从前,有一个叫庖丁的厨师,特别善于宰牛。庖丁心里先设想把牛(Array、Object等)分解成很多块,然后按照规划好的想法,一刀刀对应起来,就把牛分解了。把解构赋值说的更通俗点,有点类似于“庖丁解牛” 。

    定义:ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构。

    变量的解构赋值用途很多:1)交换变量的值,写法更简洁;2)提取 JSON 数据;3)从函数返回多个值;4函数参数的定义;5)函数参数的默认值;6)遍历 Map 结构(for...of);7)导入与导出模块(import/export);

    变量的解构赋值就是一种写法,掌握了这种写法可以让我们在书写 javascript 代码时可以更加的简单,迅捷。

1、数组的解构赋值

1.1、完全匹配模式

    只要等号两边模式一致,左边变量即可获取右边对应位置的值。

// ES6 之前
var a=1; 
var b=2; 
var c=3;
​
// ES6 之后
let [a,b,c] = [1,2,3];

1.2、支持对任意深度的嵌套数组解构

    能够非常迅速的获取二维数组、三维数组,甚至多维数组中的值。

let [foo, [[bar], baz]] = [1, [[2], 3]];
console.log(foo); // 1
console.log(bar); // 2
console.log(baz);  // 3

1.3、不需要匹配的位置可以置空

[,,third] = [1, 2, 3];
 console.log(third);  // 3

1.4、支持使用...扩展运算符

var [head, ...body] = [1, 2, 3, 4];
 console.log(body);  // [2, 3, 4]

2、对象的解构赋值

2.1、可以自定义属性名称

var {name, id: ID} = { name: 'jack', id: 1  };
​
ID // 1
id // Uncaught ReferenceError: id is not defined

2.2、可以对任意深度的嵌套对象进行解构 

//解析JSON
let jsonData = {
  id: 42,
  status: "OK",
  data: [867, 5309]
};

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

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

//解析嵌套对象
const node = {
  loc: {
    start: {
      line: 1,
      column: 5
    }
  }
};

let { loc, loc: { start }, loc: { start: { line }} } = node;
line // 1
loc  // Object {start: Object}
start // Object {line: 1, column: 5}

2.3、字符串解构

    字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
​
let { length:len } = 'hello';
console.log(len);  //5 (长度为5)

2.4、数值和布尔值的解构赋值

    解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。

// 数值和布尔值的包装对象都有toString属性
		let {toString: str1} = 111;
		str1 === Number.prototype.toString // true​
		let {toString: str2} = true;
		str2 === Boolean.prototype.toString // true​
		let { prop: x } = undefined; // TypeError:Cannot destructure property 'prop' of 'undefined' as it is undefined.
		let { prop: y } = null;      // TypeError

    上面代码中,数值和布尔值的包装对象都有toString属性,因此变量s都能取到值。解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefinednull无法转为对象,所以对它们进行解构赋值,都会报错。

2.5、函数参数的解构

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

add([1, 2]); // 3

四、 扩展运算符(spread)和rest运算符

1、数组的扩展运算符

1.1、将数组转换为参数序列

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

const numbers = [1, 2];
add(...numbers) // 3

1.2、复制/合并数组

const arr1 = [1, 2];
const arr2 = [...arr1]; //浅拷贝

const arr3 = [3, 4];
console.log([...arr1,...arr3]); // [1, 2, 3, 4]

// 本质也是:**将一个数组转为用逗号分隔的参数序列,然后置于数组中**

//注:如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。
const [first, ...rest, last] = [1, 2, 3, 4, 5];  //报错

const [first, last, ...rest] = [1, 2, 3, 4, 5]; //正确

1.3、将字符串转为真正的数组

    任何 Iterator 接口的对象(参阅 Iterator 一章),都可以用扩展运算符转为真正的数组。

[...'hello'] // [ "h", "e", "l", "l", "o" ]

2、对象的扩展运算符

    对象的扩展运算符(...)用于取出 参数对象 所有 可遍历属性 然后拷贝到当前对象。

2.1、合并对象

let age = {age: 15};
let name = {name: "Amy"};
let person = {...age, ...name};
person;  //{age: 15, name: "Amy"}

3、rest运算符

    rest运算符用来表示不确定参数个数,形如,...变量名,由...加上一个具名参数标识符组成。

function addNumbers(...numbers){
	let sum = 0;
    for (let i=0;i<numbers.length;i++) {
        sum+=numbers[i];
    }
        return sum;
}
console.log(addNumbers(1,2,3,4,5)); //15

扩展:实现原理

let bar = { a: 1, b: 2 };
let baz = Object.assign({}, bar); // { a: 1, b: 2 }

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

//Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。(如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性)。

五、字符串的扩展

1、模板字符串

    1)模板字符串使用(反引号)··

    2)嵌入变量使用${}

    传统的 JavaScript 语言,输出模板通常是自己拼接字符串。模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

//传统写法(ES6前):
$('#result').append(
  'There are <b>' + basket.count + '</b> ' +
  'items in your basket, ' +
  '<em>' + basket.onSale +
  '</em> are on sale!'
);

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

1.1、对字符串的拼接

// ES6写法:支持使用换行符
`In JavaScript \n is a line-feed.`

//ES6写法:支持换行
console.log(`string text line 1
string text line 2`);

// ES6写法:字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

1.2、对运算符的支持

//ES6写法
let result = `${1+2}`

console.log(result); //3

1.3、对html标签的支持

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<title></title>
	</head>
	<body>
		<div id="d"></div>
	</body>
	<script>
		let die = document.getElementById("d");
		die.innerHTML = `hello es6 <br \> hello world`
	</script>
</html>

效果:

2、新方法

    ES6对字符串对象,新增了几个方法,includes(),startsWith(),endsWith(),repeat();

    ES2017中:padStart(),padEnd(); ES2019中:trimStart(),trimEnd();

2.1、includes()

    返回布尔值,表示是否找到了参数字符串。

2.2、startsWith()

    返回布尔值,表示参数字符串是否在原字符串的头部。

2.3、endsWith()

    返回布尔值,表示参数字符串是否在原字符串的尾部。

2.4、repeat()

    返回一个新字符串,表示将原字符串重复n次。

let s = 'Hello world!';

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

//第二个参数,表示起始搜索位置
let s = 'Hello world!';

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

//表示将原字符串重复n次
'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""

六、数组的扩展

1、Array对象的新方法

    ES6在内置对象Array中新增了两个方法:Array.from() 和 Array.of()

1.1、Array.from()

    Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。

let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
};

// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']

// ES6的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

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

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

//如果参数是一个真正的数组,Array.from会返回一个一模一样的新数组。
Array.from([1, 2, 3])
// [1, 2, 3]

1.2、Array.of()

    Array.of() 方法用于将一组值,转换为数组。Array.of基本上可以用来替代Array()new Array()。

Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
Array.of() //[]

//比较
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]

2、Array实例的新方法

    Array实例中新增了一些方法: find()、findIndex()、fill()、entries()、keys()、values()、includes()、flat()、flatMap()、copyWithin()

2.1、find() 和 findIndex()

    用于查找满足特定条件的数组元素,均接受两个参数,一个回调函数,一个可选值用于指定回调函数内部的this,该回调函数可接受三个参数,数组的某个元素,该元素对应的索引位置,以及该数组自身。若想查找特定值使用indexOf()与lastIndexOf()方法会是更好的选择。

//查找第一个小于0的元素
[1, 4, -5, 10].find((n) => n < 0)
// -5

//查找第一个大于9的元素
[1, 5, 10, 15].find(function(value, index, arr) {
  return value > 9;
}) // 10

//查找第一个大于9的元素的位置
[1, 5, 10, 15].findIndex(function(value, index, arr) {
  return value > 9;
}) // 2

2.2、fill()

    fill方法使用给定值,填充一个数组。

['a', 'b', 'c'].fill(7)
// [7, 7, 7]

new Array(3).fill(7)
// [7, 7, 7]

//第二个和第三个参数,用于指定填充的起始位置和结束位置
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']

2.3、entries()、keys()、values()

    ES6 提供三个新的方法——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"

2.4、includes()

     Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。没有该方法之前,我们通常使用数组的indexOf方法,检查是否包含某个值。indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判。

[1, 2, 3].includes(2)     // true
[1, 2, 3].includes(4)     // false
[1, 2, NaN].includes(NaN) // true

[NaN].indexOf(NaN)
// -1

2.5、flat()、flatMap()

    数组的成员有时还是数组,Array.prototype.flat()用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。

[1, 2, [3, 4]].flat()
// [1, 2, 3, 4]

//flat()的参数为2,表示要“拉平”两层的嵌套数组。(默认是1)
[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]

    flatMap()方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。它与 map 和 深度值1的 flat 几乎相同,但 flatMap 通常在合并成一种方法的效率稍微高一些。

[2, 3, 4].flatMap((x) => [x, x * 2])
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
// [2, 4, 3, 6, 4, 8]

2.6、copyWithin()

    数组实例的copyWithin()方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。

它接受三个参数:

  • target(必需):从该位置开始替换数据。如果为负值,表示倒数。
  • start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。
  • end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。
[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]

// -2相当于3号位,-1相当于4号位
[1, 2, 3, 4, 5].copyWithin(0, -2, -1)
// [4, 2, 3, 4, 5]

3、Iterator和for...of 循环

3.1、Iterator和for...of 

    JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了MapSet。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是MapMap的成员是对象。以前我们遍历数组,我们通常会用for循环或者是forEach,遍历字符串甚至还会使用for...in,但是for...in他会遍历对象上所有可枚举的属性,包括自身的和原型链上的。而对于新数据类型Map,我们使用forEach遍历,也没问题。遍历的方法固然很多,但是我们需要根据数据类型选择遍历方法。能不能统一方法呢?这样就需要一种统一的接口机制,来处理所有不同的数据结构。

    Iterator(因为 javascript 语言里没有接口的概念,这里我们可以理解成它是一种特殊的对象 - 迭代器对象,返回此对象的方法叫做迭代器方法,此对象具有一个next方法)就是提供了一个统一的访问机制,不断的调用next()才能遍历完成,如果Iterator像java那样提供一个hasNext()方法的话,那么我们可以通过while进行遍历,事实上js中是没有的。之所以没有是因为ES6使用for...of...实现了对具有Symbol.iterator(可遍历)的数据结构的遍历,也就是说只要是包含Symbol.iterator属性的结构都可以使用for...of...进行遍历。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。

原生具备 Iterator 对象的数据结构如下:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象
//使用next()方法
let a = [1,2,3];
let it_arr = a[Symbol.iterator]();
it_arr.next();  // { value: 1, done: false }
it_arr.next();  // { value: 2, done: false }
it_arr.next();  // { value: 3, done: false }
it_arr.next();  // { value: undefined, done: true }

//类数组对象:我们可以直接将数组的遍历器对象赋值给对象
let obj = {
    0: "a",
    1: "b",
    2: "c",
    length: 3,
  }
  obj[Symbol.iterator] = Array.prototype[Symbol.iterator];
  for (let i of obj) { //如果注释上一行代码:Uncaught TypeError: obj is not iterable at
    console.log(i); // a b c
  }  

    ES6里规定,只要在对象的属性上部署了Iterator接口,具体形式为给对象添加Symbol.iterator属性,此属性指向一个迭代器方法,这个迭代器会返回一个特殊的对象 - 迭代器对象。Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值。此时,这个对象就是可迭代的,也就是可以被 for of 遍历。

    有人会很好奇的问Object对象为什么没有默认部署Iterator对象?因为obj可能有各种属性,不像数组的值是有序的。所以遍历的时候根本不知道如何确定他们的先后顺序,所以需要我们根据情况手动实现。比如:我们去改造一下Iterator迭代器对象:

var arr1 = [100, 200, 300];
  for (var o of arr1) {
    console.log(o); //100 200 300
  }
  let arr2 = [100, 200, 300];
  arr2[Symbol.iterator] = function () {
    var self = this;
    var i = 0;
    return {
      next: function () {
        var done = (i >= self.length);
        var value = !done ? self[i++] : undefined;
        return {
          done: done,
          value: i + "-" + value
        };
      }
    };
  }
  for (var o of arr2) {
    console.log(o); //1-100,2-200,3-300
  }

七、新增数据类型/数据结构

     ES5 的对象属性名都是字符串,这容易造成属性名的冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefinednull、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。Symbol 值通过Symbol函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。

1、新增数据类型:Symbol

1.1、Symbol类型的声明

let s = Symbol();

typeof s
// "symbol"

//可以接受一个字符串作为参数
let s1 = Symbol('foo');

s1.toString() // 可以显式转为字符串:"Symbol(foo)"

1.2、不能与其他类型的值进行运算

let sym = Symbol('My symbol');

"your symbol is " + sym
// TypeError: can't convert symbol to string
`your symbol is ${sym}`
// TypeError: can't convert symbol to string

1.3、作为属性名

    由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。(注意,Symbol 值作为对象属性名时,不能用点运算符。

let mySymbol = Symbol();

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

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

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

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


const mySymbol = Symbol();
const a = {};

a.mySymbol = 'Hello!';
a[mySymbol] // undefined
//因为点运算符后面总是字符串,所以不会读取mySymbol作为标识名所指代的那个值,导致a的属性名实际上是一个字符串,而不是一个 Symbol 值。
a['mySymbol'] // "Hello!"

1.4、属性名的遍历

    Symbol 作为属性名,遍历对象的时候,该属性不会出现在for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。

    另一个新的 API,Reflect.ownKeys()方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。

const obj = {};
const foo = Symbol('foo');

obj[foo] = 'bar';

for (let i in obj) {
  console.log(i); // 无输出
}

Object.getOwnPropertyNames(obj) // []
Object.getOwnPropertySymbols(obj) // [Symbol(foo)]


let obj = {
  [Symbol('my_key')]: 1,
  enum: 2,
  nonEnum: 3
};

Reflect.ownKeys(obj)
//  ["enum", "nonEnum", Symbol(my_key)]

2、新增数据结构:Set、WeakSet、Map、WeakMap

2.1、Set

    ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。Set本身是一个构造函数,用来生成 Set 数据结构。

2.1.1、Set的构造函数

     1)Set() :无参数构造

     2)Set([ iterable ]) :接受具有 iterable 接口的数据结构作为参数,如:Strin、数组等

//1、通过add方法向 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

//2、Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数
// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]

//例二
const s = new Set("hello");
console.log(s.size);//4
console.log(s);//{"h", "e", "l", "o"}

2.1.2、Set实例的属性

    1)constructor 属性--构造函数
    2)size 属性-实例成员的总数

let s = new Set("hello");
console.log(s.size);//4
console.log(s.constructor);//ƒ Set() { [native code] }

 2.1.3、Set实例的方法

    1)add(value):添加某个值
    2)delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
    3)has(value):返回一个布尔值
    4)clear():清除所有成员,没有返回值。

let s = new Set("hello");
s.add(1).add(2).add(2);
// 注意2被加入了两次
s.size // 2

s.has(1) // true
s.has(2) // true
s.has(3) // false

s.delete(2);
s.has(2) // false

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

let set = new Set(['red', 'green', 'blue']);

for (let item of set.keys()) {
  console.log(item);
}
// red
// green
// blue

for (let item of set.values()) {
  console.log(item);
}
// red
// green
// blue

for (let item of set.entries()) {
  console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]

let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ' : ' + value))
// 1 : 1
// 4 : 4
// 9 : 9

 2.2、WeakSet

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

    1)WeakSet 的成员只能是对象,而不能是其他类型的值。

    2)WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存。WeakSet 没有size属性,没有办法遍历它的成员。

const ws = new WeakSet();
const obj = {};
const foo = {};

ws.add(window);
ws.add(obj);

ws.has(window); // true
ws.has(foo);    // false

ws.delete(window);
ws.has(window);    // false

ws.size // undefined
ws.forEach // undefined

2.3、Map

    JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

2.3.1、Map的构造函数

    1)Map( ):无参数构造

    2)Map( [ iterable ]):可以接收一个可迭代对象(前提是每个成员都是一个双元素结构的迭代对象)作为参数

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

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


const map = new Map([
    ['F', 'no'],
    ['T', 'yes'],
]);
for (let [key, value] of map.entries()) {
   console.log(key, value); //F no  T yes
}

2.3.2、Map实例的属性

    1)constructor 属性--构造函数
    2)size 属性-实例成员的总数

let maps = new Map();
maps.set("a", "1");
maps.set("b", "2");
console.log(maps.size);//2
console.log(maps.constructor);//ƒ Map() { [native code] }

2.3.3、Map实例的方法

    1)set(key, value):添加键值
    2)get(key):取值。
    3)has(value):返回一个布尔值
    4)delete(key):删除某个键,返回一个布尔值
    5)clear():清除所有成员,没有返回值。

  const m = new Map();
  m.set('edition', 6);        // 键是字符串
  m.set(262, 'standard');     // 键是数值
  m.set(undefined, 'nah');    // 键是 undefined

  m.has(undefined);    // true
  m.delete(undefined);
  m.has(undefined);       // false
  m.clear();
  console.log(m.size);// 0

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

const map = new Map([
  ['F', 'no'],
  ['T',  'yes'],
]);

for (let key of map.keys()) {
  console.log(key);
}
// "F"
// "T"

for (let value of map.values()) {
  console.log(value);
}
// "no"
// "yes"

for (let item of map.entries()) {
  console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

// 或者
for (let [key, value] of map.entries()) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

// 等同于使用map.entries()
for (let [key, value] of map) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

2.4、WeakMap

    WeakMap结构与Map结构类似,也是用于生成键值对的集合。WeakMapMap的区别有两点:

    1)WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。

    2)WeakMap的键名所指向的对象,不计入垃圾回收机制。

  WeakMap的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放。WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。

    运用场景:Angular、Nest等依赖注入框架都使用了反射技术。其中核心的依赖reflect-metadata就会用到WeakMap。

    WeakMap只有四个方法可用:get()set()has()delete()

const wm = new WeakMap();

// size、forEach、clear 方法都不存在
wm.size // undefined
wm.forEach // undefined
wm.clear // undefined

八、函数的变化

1、函数参数的默认值

    ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。但是,下面的代码,如果参数y等于空字符,结果被改为默认值。

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

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World  参数y等于空字符,结果被改为默认值。(参数对应的布尔值不能false)


//对于:|| 和 && 的用法

//与其他语言不同,在JS中,a&&b或者a||b返回的是要么是a,要么是b;而其他语言中返回的是true or false

//a&&b 如果a为true,则执行b并返回b的值;如果a为false,则返回false不执行b;
//如果a为true,则返回a的值不执行b;如果a为false,则执行b并返回b的值;

    ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。 (不传或者传入undefined的时候才会触发默认值赋值)

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

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

2、rest 参数

    rest参数,这是一个新的概念,rest的中文意思是:剩下的部分。ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

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

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

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

  return sum;
}

add(2, 5, 3) // 10

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

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

// 报错:rest 参数之后不能再有其他参数
function f(a, ...b, c) {
  // ...
}

3、箭头函数

    ES6 允许使用“箭头”(=>)定义函数。箭头函数的产生,主要有两个目的:更简洁的语法和与父作用域共享关键字this。箭头函数适合于无复杂逻辑或者无副作用的纯函数场景下,例如用在map、reduce、filter的回调函数定义中。在了解箭头函数之前,需要先复习一下匿名函数及闭包等相关概念了。

3.1、ES5中的匿名函数及闭包

    什么是匿名函数?顾名思义:匿名函数就是没有实际名字的函数。通过匿名函数可以实现闭包,在ES6之前只有两个作用域,全局作用域和函数内的局部作用域,闭包的优点是可以在全局作用域使用局部作用域的变量。匿名函数及闭包的写法:

//声明一个普通函数,函数的名字叫fn
function fn(){
    console.log("穆瑾轩");
}

//匿名函数:形式一
(function (){
	console.log(this); //[object Window] 注:这里的this指向全局作用域
})();

//匿名函数:形式二
var obj={
	name:"穆瑾轩",
	age:18,
	fn:function(){
		  return "我叫"+this.name+"今年"+this.age+"岁了!";
	}
};
console.log(obj.fn());//我叫穆瑾轩今年18岁了!


//匿名函数:应用闭包
var name = "this is window"; //定义window的name属性,看this.name是否会调用到
var testObj = {
	name: "this is testObj",
	getName: function() {
	var self = this; //临时保存this对象
	var handle = function() {
		console.log(this); //输出:Window(this指向的是全局对象--window对象)-->闭包会将其空间扩展到外层函数空间之外,止步于全局空间
		console.log(this.name); //输出: this is window   
		console.log(self); //这样可以获取到的this即指向testObj对象(对象拥有name属性和getName() 方法)
		}
		handle();
	}
}
testObj.getName();


//引入this的初衷就是想在原型继承的情况下,拿到函数的调用者。JavaScript 中的 this 指向函数调用时的执行环境
var obj = {
    name: "obj.name",
    method: function () {
      console.log("method:" + this.name);
      return this;
    }
  }

console.log("obj.method() === obj:" + (obj.method() === obj)); //"obj.method() === obj:"true

3.2、ES6中为何引入箭头函数

    ES6新增了箭头函数,一定是为了解决某个问题。上面我们说到了闭包。闭包中的this会将其空间扩展到外层函数空间之外,止步于全局空间(取决于函数的运行环境上下文)。

let obj = {
    name: '张三',
    fn: function () {
    	let _this = this  //在定义是将this赋值给一个变量_this
        return function() {
            //console.log(this.name) //undefined
            console.log(_this.name)  //调用被赋值的_this去获取obj里的name
        }
    }
}
obj.fn()()

//打印结果: 张三

    普通函数的this是运行时绑定,正常来说我们希望this指向的是obj中的name。而箭头函数它做到了,箭头函数的this定义时绑定。这样就轻松地解决了普通函数this随着运行环境的改变而改变的问题了。

let obj = {
    name: '张三',
    fn: function () {
    	//此处有个this,该this指向obj,并且被箭头函数所绑定
        return () => {
            console.log(this.name)  //这里的this是向上寻找,找到function() {}内有一个this,并与之绑定,而这个this指向的就是obj
        }
    }
}
obj.fn()()

//打印结果: 张三

3.3、箭头函数的基本使用

    下面是ES6之前和ES6箭头函数写法的对比:

let f = v => v;

// 等同于
let f = function (v) {
  return v;
};

let fn = (num1, num2) => num1 + num2;
console.log(fn(1, 2));//3

// 等同于
let fn = function(num1, num2) {
  return num1 + num2;
};

语法格式规则总结:

    1)一个参数对应一个表达式。形如:param => expression;【当只有一个参数时,参数()可以省略(rest参数是一个例外,如(...args) => ...),function{}都消失了,所有的回调函数都只出现在了一行里。注:{}消失后,return关键字也跟着消失了。单行的箭头函数会提供一个隐式的return(这样的函数在其他编程语言中常被成为lamda函数)。

    2)多个参数对应一个表达式。形如:(param [, param]) => expression; 【参数()不可以省略】

    3)一个参数对应多行代码。形如:param => {statements;}【{}不可以省略,注:当箭头函数伴随着{}被声明,那么即使它是单行的,它也不会有隐式return。

    4)多个参数对应多行代码。形如:([param] [, param]) => {statements}【参数()不可以省略,{}不可以省略】

    5)特殊情况一:表达式里没有参数。形如:() => expression; 或 () => {statements;} 【参数()不可以省略】

    6)特殊情况二:返回一个空对象或者对象省略return时,需要加上()。形如:() => ({}) 或 ([param]) => ({ key: value });

//  一个参数对应一个表达式:param => expression; 例如:
 x => x+1;

// 多个参数对应一个表达式:(param [, param]) => expression; 例如:
 (x,y) => (x + y);

// 一个参数对应多个表示式:param => {statements;} 例如
 x = > { x++; return x;};

//  多个参数对应多个表达式:([param] [, param]) => {statements} 例如
 (x,y) => { x++;y++;return x*y;};

//表达式里没有参数:() => expression; 例如
 var flag = (() => 2)(); flag等于2
//() => {statements;} 例如
 var flag = (() => {return 1;})(); flag就等于1

//传入一个表达式,返回一个空对象或者对象省略return时:([param]) => ({ key: value });例如:
  () => ({}) // {}
  var fuc = (x) => ({key:x})
  alert(fuc(1));//{key:1}

箭头函数有几个使用注意点:

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

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

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

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

//1)下面有几个this
function foo() {
  return () => {
    return () => {
      return () => {
        console.log('id:', this.id);
      };
    };
  };
}

var f = foo.call({id: 1});

var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1

//上面代码之中,只有一个this,就是函数foo的this,所以t1、t2、t3都输出同样的结果。因为所有的内层函数都是箭头函数,都没有自己的this,它们的this其实都是最外层foo函数的this。  

//2)不能当做构造函数
let foo1 = () => ({});
console.log(foo1.prototype); //undefined--无构造函数
let foonew = function () {
    return {}
}
console.log(foonew.prototype);//{constructor: ƒ}

九、Promise对象

1、基本概念

1.1、分布式、高并发和多线程简诉

    在如今这个网络大环境下,在数量递增的同时,也要追求效率。也许你经常会听到分布式、高并发和多线程这些概念,他们三个总是相伴而生,但侧重点又有不同。

    分布式是从物理资源的角度去将不同的机器组成一个整体对外服务,技术范围非常广且难度非常大,有了这个基础,高并发、高吞吐等系统很容易构建;

    高并发是从业务角度去描述系统的能力,实现高并发的手段可以采用分布式,也可以采用诸如缓存、CDN等,当然也包括多线程;

    多线程则聚焦于如何使用编程语言将CPU调度能力最大化。线程不是一个计算机硬件的功能,而是操作系统提供的一种逻辑功能,线程本质上是进程中一段并发运行的代码。

    线程同步:是多个线程同时访问同一资源,等待资源访问结束,浪费时间,效率低。

    线程异步:访问资源时在空闲等待时同时访问其他资源,实现多线程机制。效率高,但也伴随着线程安全问题。

1.2、同步任务与异步任务

    同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;这种单线程很容易因为一个任务发生延迟,造成整体的耗时变长,为了解决这个问题,所以就有了异步这个概念。

    异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有等主线程任务执行完毕,"任务队列"开始通知主线程,请求执行任务,该任务才会进入主线程执行。简单说就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。

    总结:这种不连续的执行,就叫做异步。异步编程的语法目标,就是怎样让它更像同步编程。相应地,连续的执行,就叫做同步

    javascript是运行在浏览器端的语言,必须依赖javascript引擎来解析并执行代码,js引擎是单线程,也就是一个任务接着一个任务来执行程序。异步执行的最终结果,依然需要回到 JS 线程上进行处理。在JS中,异步的结果回到 JS 主线程的方式采用的是 “ 回调函数 ” 的形式 ,这也是早期实现异步编程的主要形式。于是出现了很多异步流程控制的包,如async.js 和Promise等。

1.3、Promise 的含义

      Promise,中文翻译为承诺,期约。Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。(有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。)它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

   Promise对象有以下两个特点:

    1)核心概念是状态,对象的状态不受外界影响。状态转换就是Promise执行异步事件的时机,它有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)

    2)Promise中的状态是不可逆转的。任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected

    Promise的缺点:

    1)无法取消Promise,一旦新建它就会立即执行,无法中途取消。

    2)如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段。

2、Promise 的基本用法

    ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。使用时需要用new关键词来创建实例对象。Promise构造函数中自带excutor执行器,excutor执行器中有2个JavaScript中默认的函数参数resolve,reject。

    resolve函数的作用是当Promise状态从padding转换到resolve时,可以把Promise中的对象或者变量当成参数传递出来供异步成功时调用,reject函数的作用是当Promise状态从padding转换到reject时候可以把Promise中的对象或者变量,以及系统报错当成参数传递出来供异步失败时调用。

    then是Promise原型上的一个方法,Promise.prototype.then() 所以通过构造函数创建的Promise实例对象也会自带then( )方法。then( )方法接受2个函数参数,作为Promise中异步成功和异步失败的2个回调函数。

2.1、Promise实例的基本代码结构

    1)构造函数接受一个函数作为参数,该函数接受两个参数resolvereject,它们是两个函数。

    2)原型方法then:Promise.prototype.then() 

    3)原型方法catch:Promise.prototype.catch()

    4)原型方法all:Promise.all()等

//ES6 箭头函数写法
let promise = new Promise((resolve,reject)=>{
    if(/判断条件/){
        resolve()//承诺实现
    }else{
		reject()//承诺失效
    }
})

promise.then(res=>{
	//处理承诺实现方法
},err=>{
    //处理承诺失效方法     
})

3、Promise 的方法

3.1、原型方法then-Promise.prototype.then()

    Promise 实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

    注意:Promise函数本身不是一个异步函数,在excutor执行器中运行的代码是同步的。执行异步的是then( )方法中的事件 

//案例:
console.log('步骤1');
new Promise((resolve,reject)=>{
    console.log('步骤2');
    resolve()
}).then(res=>{
    console.log('步骤3');
})
console.log('步骤4')

//执行结果:
   //步骤1
   //步骤2
   //步骤4
   //步骤3

//链式写法
getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // ...
});

 3.2、原型方法catch-Promise.prototype.catch() 

    Promise.prototype.catch()方法是.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数。

p.then((val) => console.log('fulfilled:', val))
  .catch((err) => console.log('rejected', err));

// 等同于
p.then((val) => console.log('fulfilled:', val))
  .then(null, (err) => console.log("rejected:", err));


//案例:
// 写法一
const promise = new Promise(function(resolve, reject) {
  try {
    throw new Error('test');
  } catch(e) {
    reject(e);
  }
});
promise.catch(function(error) {
  console.log(error);
});

// 写法二
const promise = new Promise(function(resolve, reject) {
  reject(new Error('test'));
});
promise.catch(function(error) {
  console.log(error);
});

3.3、原型方法all-Promise.all() 

   Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。p的状态由p1p2p3决定,分成两种情况。

    1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

    2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

const p = Promise.all([p1, p2, p3]);

案例:

function runAsync1() {
    var p = new Promise(function (resolve, reject) {
      //做一些异步操作
      setTimeout(function () {
        console.log('异步任务1执行完成');
        resolve('随便什么数据1');
      }, 1000);
    });
    return p;
  }
  function runAsync2() {
    var p = new Promise(function (resolve, reject) {
      //做一些异步操作
      setTimeout(function () {
        console.log('异步任务2执行完成');
        resolve('随便什么数据2');
      }, 2000);
    });
    return p;
  }
  function runAsync3() {
    var p = new Promise(function (resolve, reject) {
      //做一些异步操作
      setTimeout(function () {
        console.log('异步任务3执行完成');
        resolve('随便什么数据3');
      }, 2000);
    });
    return p;
  }

  //runAsync1 runAsync2 runAsync3 的状态都变成了fulfilled,才会传递给回调函数
  Promise.all([runAsync1(), runAsync2(), runAsync3()]).then(function (results) {
    console.log(results);
  });

 输出结果:

3.4、原型方法race-Promise.race()

    Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

const p = Promise.race([p1, p2, p3]);

案例:

//把上面案例的all改成race
Promise.race([runAsync1(), runAsync2(), runAsync3()]).then(function (results) {
    console.log(results);
  });

 输出结果:

4、总结与应用

​    PromiseECMAscript ES6原生的对象,是解决javascript语言异步编程产生回调地狱的一种方法。但它的本质也没有跳出回调问题,只是把嵌套关系优化成类似层级结构的写法来帮助开发者更容易处理异步中的逻辑代码。

    我们可以将图片的加载写成一个Promise,一旦加载完成,Promise的状态就发生变化。

const preloadImage = function (path) {
  return new Promise(function (resolve, reject) {
    const image = new Image();
    image.onload  = resolve;
    image.onerror = reject;
    image.src = path;
  });
};

十、Generator函数的引入

1、基本概念

1.1、Generator 函数简介

    Generator的中文名称是生成器。Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。它最大特点就是可以交出函数的执行权(即暂停执行)。Generator初衷应该并不是为异步而设计出来的,它还有其他功能(对象迭代、控制输出、部署Interator接口...),更多的是为了生成迭代器。在ES2017 标准引入了 async函数,它就是 Generator 函数的语法糖,使得异步操作变得更加方便。

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

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: true }

hw.next()
// { value: undefined, done: true }

1.2、Generator 函数的数据交换和错误处理 

    Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。next 方法返回值的 value 属性,是 Generator 函数向外输出数据;next 方法还可以接受参数,这是向 Generator 函数体内输入数据。

function* gen(x){
  var y = yield x + 2;
  return y;
}

var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }

//Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。

function* gen(x){
  try {
    var y = yield x + 2;
  } catch (e){ 
    console.log(e);
  }
  return y;
}

var g = gen(1);
g.next();
g.throw('出错了');
// 出错了

2、yield和yield*表达式的理解

    yieldyield*都是配合Generator进行使用的。

    yield是关键字,其语法如下:

[rv] = yield [expression];

//expression:是Generator函数返回的遍历器对象调用next方法时所得到的值;
//rv:是遍历其对象调用next方法时传递给next方法的参数

    yield*是表达式,有返回值,其语法如下:

yield* [[expression]];

//expression:是可遍历对象,可以是数组,也可以是另外一个Generator函数的执行表达式,等等

案例一:

function* _testYieldExpression() {
    let value = '';               
    value = yield 'yield value';  //yield value:是Generator函数返回的遍历器对象调用next方法时所得到的值;value 是遍历其对象调用next方法时传递给next方法的参数
    console.log(`1 value is: ${value}`);//1 value is: params from next

    value = yield 'yield value';
    console.log(`2 value is: ${value}`);//2 value is: undefined
    return 'over';
  }

  let _testIterator = _testYieldExpression();
  let _res = _testIterator.next();
  console.log(`1:no params to next, result is: ${_res.value}`);//1:no params to next, result is: yield value

  _res = _testIterator.next('params from next');
  console.log(`2:params to next, result is: ${_res.value}`);//2:params to next, result is: yield value

  _res = _testIterator.next();
  console.log(`3:params to next, result is: ${_res.value}`);//3:params to next, result is: over

 输出结果:

     从上面的执行结果中看出:第一次调用next()方法输出:1:no params to next, result is: yield value。第二次调用next()方法输出:1 value is: params from next       和      2:params to next, result is: yield value 第三次调用next()方法输出:2 value is: undefined    和   3:params to next, result is: over

案例二:

function* g3() {
  yield* [1, 2];  //可以是数组,也可以是另外一个Generator函数的执行表达式,等等
  yield* '34';    //
  yield* Array.from(arguments);
}

var iterator = g3(5, 6);

console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: "3", done: false}
console.log(iterator.next()); // {value: "4", done: false}
console.log(iterator.next()); // {value: 5, done: false}
console.log(iterator.next()); // {value: 6, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

案例三:为什么需要 yield*

function* foo() {
  yield 'a';
  yield 'b';
}

function* bar() {
  yield 'x';
  // 手动遍历 foo()
  for (let i of foo()) {
    console.log(i);
  }
  yield 'y';
}

//for...of循环可以自动遍历 Generator 函数运行时生成的Iterator对象
for (let v of bar()){
  console.log(v);
}
// x
// a
// b
// y

//原生的 JavaScript 对象没有遍历接口,无法使用for...of循环,通过 Generator 函数为它加上这个接口,就可以用了。

function* objectEntries(obj) {
  let propKeys = Reflect.ownKeys(obj);

  for (let propKey of propKeys) {
    yield [propKey, obj[propKey]];
  }
}

let jane = { first: 'Jane', last: 'Doe' };

for (let [key, value] of objectEntries(jane)) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

    foobar都是 Generator 函数,在bar里面调用foo,就需要手动遍历foo。如果有多个 Generator 函数嵌套,写起来就非常麻烦。ES6 提供了yield*表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。

十一、代理(Proxy)与反射(Reflect

1、代理-Proxy

1.1、Proxy简介

    proxy的中文有代理的意思,在java中,也许你见过这两种方式的代理:静态代理和动态代理。代理模式(英语:Proxy Pattern)是程序设计中的一种设计模式,就是给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。

    在MDN上对于 Proxy 的解释是:Proxy 对象允许你拦截并定义基本语言操作的自定义行为(例如,属性查找,赋值,枚举,函数调用等)。简单来说: 也可以理解为在目标对象之前架设一层拦截,外部所有的访问都必须先通过这层拦截,因此提供了一种机制,可以对外部的访问进行过滤和修改。尽管它不像其他ES6功能用的普遍,但Proxy有许多用途,包括运算符重载,对象模拟,简洁而灵活的API创建,对象变化事件,甚至Vue 3背后的内部响应系统提供动力。

    在ES5出现以前,JS环境中的对象包含许多不可枚举和不可写的属性,但开发者不能定义自己的不可枚举或不可写属性,于是ES5引入了Object.defineProperty()方法来支持开发者去做JS引擎早就可以实现的事情。在vue2.x中就是通过Object.defineProperty()来实现数据劫持的,但是在vue3.x中使用ES6中的Proxy来替代Object.defineProperty()。

    Proxy 本质上属于元编程非破坏性数据劫持,在原对象的基础上进行了功能的衍生而又不影响原对象,符合松耦合高内聚的设计理念。

    Object.defineProperty只能劫持对象的属性,不能直接代理对象。虽然 Object.defineProperty 通过为属性设置 getter/setter 能够完成数据的响应式,但是它并不算是实现数据的响应式的完美方案,某些情况下需要对其进行修补,这也是它的缺陷,主要表现在两个方面:1)无法检测到对象属性的新增或删除;2)不能监听数组的变化。

    ES6原生提供了Proxy构造函数,用来生成Proxy实例:

var proxy = new Proxy(target, handler);

//Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。
//其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。

//target: 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
//handler: 对该代理对象的各种操作行为处理(为空对象的情况下,基本可以理解为是对第一个参数做的一次浅拷贝)

案例:

let obj = {
  a: 1,
  b: 2,
}

const p = new Proxy(obj, {
  get(target, key, value) {
    if (key === 'c') {
      return '我是自定义的一个结果';
    } else {
      return target[key];
    }
  },
  set(target, key, value) {
    if (value === 4) {
      target[key] = '我是自定义的一个结果';
    } else {
      target[key] = value;
    }
  }
})

//都有的属性
console.log(obj.a) // 1
console.log(p.a) // 1
//都没有的属性
console.log(obj.c) // undefined
console.log(p.c) // 我是自定义的一个结果
//obj添加name属性
obj.name = '李白';
console.log(obj.name); // 李白
console.log(p.name); // 李白

//p添加age属性
p.age = 4;
console.log(obj.age); // 我是一个自定义结果
console.log(p.age); //我是一个自定义结果

1.2、Proxy 支持的拦截操作

Proxy 支持的拦截操作:
get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']。
set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。
has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。

//注意:如果一个属性不可配置 || 不可写,则该属性不可被代理,通过 Proxy 访问该属性会报错。

1.2.1、Proxy 实例的方法-get()

    get(target, propKey, receiver)方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。

var person = {
  name: "张三"
};

var proxy = new Proxy(person, {
  get: function(target, propKey) {
    if (propKey in target) {
      return target[propKey];
    } else {
      throw new ReferenceError("Prop name \"" + propKey + "\" does not exist.");
    }
  }
});

proxy.name // "张三"
proxy.age // 抛出一个错误

1.2.2、Proxy 实例的方法-set()

    set(target, propKey, value, receiver)set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。

let validator = {
  set: function(obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('The age is not an integer');
      }
      if (value > 200) {
        throw new RangeError('The age seems invalid');
      }
    }

    // 对于满足条件的 age 属性以及其他属性,直接保存
    obj[prop] = value;
  }
};

let person = new Proxy({}, validator);

person.age = 100;

person.age // 100
person.age = 'young' // 报错
person.age = 300 // 报错

1.2.3、Proxy 实例的方法-apply()

    apply方法拦截函数的调用、callapply操作。apply方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。

var handler = {
  apply (target, ctx, args) {
    return Reflect.apply(...arguments);
  }
};

1.2.4、Proxy 实例的方法-has()

    has()方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符。has()方法可以接受两个参数,分别是目标对象、需查询的属性名。

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.2.5、Proxy 实例的方法-construct()

    construct()方法用于拦截new命令,下面是拦截对象的写法。

const handler = {
  construct (target, args, newTarget) {
    return new target(...args);
  }
};

1.2.6、Proxy 实例的方法-deleteProperty()

    deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。

var handler = {
  deleteProperty (target, key) {
    invariant(key, 'delete');
    delete target[key];
    return true;
  }
};
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`);
  }
}

var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: Invalid attempt to delete private "_prop" property

1.2.6、Proxy 实例的方法-defineProperty()

    defineProperty()方法拦截了Object.defineProperty()操作。

var handler = {
  defineProperty (target, key, descriptor) {
    return false;
  }
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = 'bar' // 不会生效

1.2.7、Proxy 实例的方法-其他

    getOwnPropertyDescriptor()、getPrototypeOf()、isExtensible()、ownKeys()、preventExtensions()、setPrototypeOf()

//getOwnPropertyDescriptor
var handler = {
  getOwnPropertyDescriptor (target, key) {
    if (key[0] === '_') {
      return;
    }
    return Object.getOwnPropertyDescriptor(target, key);
  }
};
var target = { _foo: 'bar', baz: 'tar' };
var proxy = new Proxy(target, handler);
Object.getOwnPropertyDescriptor(proxy, 'wat')
// undefined
Object.getOwnPropertyDescriptor(proxy, '_foo')
// undefined
Object.getOwnPropertyDescriptor(proxy, 'baz')
// { value: 'tar', writable: true, enumerable: true, configurable: true }

//getPrototypeOf
var proto = {};
var p = new Proxy({}, {
  getPrototypeOf(target) {
    return proto;
  }
});
Object.getPrototypeOf(p) === proto // true

1.2.8、Proxy.revocable()

    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

2、反射-Reflect

   Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。其主要作用:

    1)将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。

    2)修改某些Object方法的返回结果,让其变得更合理。

    3)让Object操作都变成函数行为。

    4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。

Reflect对象一共有 13 个静态方法。

  • Reflect.apply(target, thisArg, args)
  • Reflect.construct(target, args)
  • Reflect.get(target, name, receiver)
  • Reflect.set(target, name, value, receiver)
  • Reflect.defineProperty(target, name, desc)
  • Reflect.deleteProperty(target, name)
  • Reflect.has(target, name)
  • Reflect.ownKeys(target)
  • Reflect.isExtensible(target)
  • Reflect.preventExtensions(target)
  • Reflect.getOwnPropertyDescriptor(target, name)
  • Reflect.getPrototypeOf(target)
  • Reflect.setPrototypeOf(target, prototype)

上面这些方法的作用,大部分与Object对象的同名方法的作用都是相同的,而且它与Proxy对象的方法是一一对应的

十二、ES6中的Class介绍

1、Class基本用法

1.1、Class简介

    传统的javascript中只有对象,没有类的概念。它是基于原型的面向对象语言。原型对象特点就是将自身的属性共享给新对象。这样的写法相对于其它传统面向对象语言(比如 C++ 和 Java)差异很大,很容易让新学习这门语言的程序员感到困惑。ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。

    类中有:constructor构造,有static关键字(声明静态方法),也可以通过 extends 关键字实现继承,也有super 关键字,这很像java中的类。基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。我们看个栗子:

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 + ')';
  }
}

console.log(typeof Point);   // "function"
console.log(Point === Point.prototype.constructor);   // true

//类完全可以看作构造函数的另一种写法:Point === Point.prototype.constructor
//实际上,类中所有的方法都定义在类的prototype属性上

使用class关键字需要注意一下几点:

    1)class内部定义的方法都是不可枚举的,ES5中函数的写法是可以枚举的;
    2)生成类的写法需使用 new 命令,否则会报错;
    3)constructor 方法默认返回实例对象(即 this );
    4)类和模块的内部,默认就是严格模式,所以不需要使用 use strict 指定运行模式;
    5)类不存在变量提升;
    6)类的方法内部如果含有this,它默认指向类的实例。

class Point {
  constructor(x, y) {
    // ...
  }

  toString() {
    // ...
  }
}


Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]


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

Point.prototype.toString = function () {
  // ...
};

Object.keys(Point.prototype)
// ["toString"]
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]


// 报错
var point = Point(2, 3);
// 正确 使用new
var point = new Point(2, 3);

//不存在变量提示
new Foo(); // ReferenceError
class Foo {}

1.2、constructor 方法和普通方法

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

    与 ES5 一样,在“类”的内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

class Point {
}

// 等同于
class Point {
  constructor() {}
}



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'

2、Class 表达式

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

const MyChild = class Child {
  toString() {
    console.log(Child.name); //name属性总是返回紧跟在class关键字后面的类名。
  }
};
//类的名字是MyChild而不是Child,Child只在Class内部代码可用
let mychild = new MyChild();
mychild.toString(); // Child
//如果函数内部用不到Child,也可以省略
const MyChild = class {
  // ...
};

3、Class 的静态方法

    类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

class Foo {
  static className() {
    console.log("heyushuo");
  }
}
Foo.className(); //heyushuo 不能通过实例调用会报错
// 注意,如果静态方法包含this关键字,这个this指的是类,而不是实例。
class Foo {
  static bar() {
    this.baz();
  }
  static baz() {
    console.log("hello");
  }
  baz() {
    console.log("world");
  }
}
Foo.bar(); // hello
// 1.静态方法bar调用了this.baz,这里的this指的是Foo类,而不是Foo的实例,等同于调用Foo.baz。
// 2.静态方法可以与非静态方法重名。
// 父类的静态方法,可以被子类继承。
class Foo {
  static classMethod() {
    return "hello";
  }
}
class Bar extends Foo {}
Bar.classMethod(); // 'hello'

4、Class 的继承

    Class 可以通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多

class Parent {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  toString() {
    console.log("年龄:" + this.age + "姓名:" + this.name);
  }
}
class Child extends Parent {
  constructor(name, age, height) {
    super(name, age); //调用父类的constructor(构造方法),子类必须在 constructor 方法中调用 super 方法,使子类获得自己的 this 对象,否则新建实例时会报错。
    this.height = height;
  }
  sayInfo() {
    super.toString(); // 调用父类的toString()
    console.log(`身高:${this.height}`);
  }
}
var person = new Child("heyushuo", 24, 180);
person.sayInfo(); //年龄:24姓名:heyushuo   身高:180

// 父类的静态方法,也会被子类继承。
class A {
  static hello() {
    console.log("hello world");
  }
}
class B extends A {}
B.hello(); // hello world
// hello()是A类的静态方法,B继承A,也继承了A的静态方法。

5、super 关键字

    super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。

    super 作为函数

  • super 作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次 super 函数。
  • super 虽然代表了父类 A 的构造函数,但是返回的是子类 B 的实例,即 super 内部的 this 指的是 B
  • super()只能用在子类的构造函数之中,用在其他地方就会报错。
class A {}
class B extends A {
  constructor() {
    super();
  }
}
// 注意, super虽然代表了父类A的构造函数, 但是返回的是子类B的实例,
//即super内部的this指的是B,因此super() 在这里相当于A.prototype.constructor.call(this)。

    super 作为对象时

  • 在普通方法中指向父类的原型对象,在静态方法中指向父类
  • super 指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过 super 调用的。
  • 子类普通方法中通过 super 调用父类的方法时,方法内部的this 指向当前的子类实例。
  • super 作为对象,用在静态方法之中,这时super 将指向父类,在普通方法之中指向父类的原型对象
class A {}

class B extends A {
  constructor() {
    super();
    console.log(super); // 报错
  }
}

class A {}

class B extends A {
  constructor() {
    super();
    console.log(super.valueOf() instanceof B); // true
  }
}

let b = new B();

//上面代码中,super.valueOf()表明super是一个对象,因此就不会报错。同时,由于super使得this指向B的实例,所以super.valueOf()返回的是一个B的实例。

 6、ES6中的Class实现

    ES6中Class的底层还是通过构造函数去创建的。有兴趣的可以使用Babel去转换代码(Babel是一个工具链,主要用于在当前和较旧的浏览器或环境中将ECMAScript 2015+代码转换为JavaScript的向后兼容版本-https://babeljs.io/repl

转换前:

class Parent {
  constructor(a){
    this.filed1 = a;
  }
  filed2 = 2;
  func1 = function(){}
}

经过babel转码之后:

//调用_classCallCheck方法判断当前函数调用前是否有new关键字,若构造函数前面没有new则构造函数的prototype不会出现在this的原型链上,返回false
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

var Parent = function Parent(a) {
  _classCallCheck(this, Parent);

  _defineProperty(this, "filed2", 2);

  _defineProperty(this, "func1", function () {});

  this.filed1 = a;
};

 执行过程:

    1)调用_classCallCheck方法判断当前函数调用前是否有new关键字,若构造函数前面没有new则构造函数的prototype不会出现在this的原型链上,返回false

 2)将class内部的变量及函数赋值给this

 3)执行constructor内部的逻辑

十三、模块(Module)

1、Module简介

    历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的require、Python 的import,甚至就连 CSS 都有@import,模块化已是常态,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。

    在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

    模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

//--------在CommonJs中------------
//file.js
module.exports = value;

// 引入value
const value = require('file.js')


//--------在ES6中------------
// const.js
export const value = 'xxx';


import { value } from 'const.js'

    ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";

    严格模式主要有以下限制:

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval不会在它的外层作用域引入变量
  • evalarguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.callerfn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protectedstaticinterface

2、export 命令

    ES6模块只支持静态导出,你只可以在模块的最外层作用域使用export,不可在条件语句中使用,也不能在函数作用域中使用。从分类上级讲, exports 主要有三种:

    1)Named Exports (Zero or more exports per module)-具名导出:这种方式导出多个函数/变量。

    2)Default Exports (One per module)-默认导出:导出一个默认函数/类。

    3)Hybrid Exports-混合导出:也就是 上面第一点和第二点结合在一起的情况。

2.1、Named Exports-具名导出

    只需要在变量或函数前面加 export 关键字即可。

//------ lib.js ------
export const sqrt = Math.sqrt;

export function square(x) {
    return x * x;
}
export function diag(x, y) {
    return sqrt(square(x) + square(y));
}

//------ main.js 使用方式1 ------
import { square, diag } from './lib.js';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

//------ main.js 使用方式2 ------
import * as lib from './lib.js';
console.log(lib.square(11)); // 121
console.log(lib.diag(4, 3)); // 5

引用:

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title></title>
</head>

<body>
</body>
<!--需要加上类型为:module-->
<script type="module" src="./main.js"></script>

</html>

    如果直接使用浏览器运行的话,会出现跨域问题:同源策略禁止读取位于 file:///D:/vueTest/vue-test-big/src/... 的远程资源,跨源请求仅支持协议方案:http、data、chrome、chrome扩展、https。

    原因:HTML使用type="module"会默认产生跨域请求,我们是在本地打开的文件,而file协议并不支持。

 

解决方案:Visual Studio Code下载Live Server 插件

输出结果:

2.2、Default exports-默认导出

    这种方式比较简单,一般用于一个类文件,或者功能比较单一的函数文件使用。一个模块中只能有一个export default默认输出。export default与export的主要区别有两个:不需要知道导出的具体变量名, 导入(import)时不需要{}。

//导出一个函数------ myFunc.js ------
export default function () {console.log("default导出函数")};

//------ main.js ------
import myFunc from './myFunc.js';
myFunc();

//------ MyClass.js ------
class MyClass{}

//导出一个Class
export default MyClass;

//------ Main.js ------
import MyClass from './MyClass.js';

2.3、Mixed exports-混合导出

    混合导出,也就是 上面第一点和第二点结合在一起的情况。

//mixed.js
export function myFunc () {
  console.log("混合导出");
}

export const MY_CONST = 'hh';

export default class MyClass {
  say () {
    console.log("混合类导出");
  }
}

//main.js
import MyClass, { myFunc, MY_CONST } from './mixed.js';
new MyClass().say();
myFunc();
console.log(MY_CONST);

输出结果:

注意:一般情况下,export输出的变量就是在原文件中定义的名字,但也可以用 as 关键字来指定别名,这样做一般是为了简化或者语义化export的函数名。

//------ lib.js ------
export function getUserName(){
  // ...
};
export function setName(){
  // ...
};

//输出别名,在import的时候可以同时使用原始函数名和别名
export {
  getName as get, //允许使用不同名字输出两次
  getName as getNameV2,
  setName as set
}

3、imoprt命令

    import的用法和export是一一对应的,但是import支持静态导入和动态导入两种方式,动态import支持晚一些,兼容性要差一些。

3.1、静态-导入全部

    当export有多个函数或变量时,可以使用 * as 关键字来导出所有函数及变量,同时 as 后面跟着的名称做为该模块的命名空间。

//导出lib的所有函数及变量
import * as lib from './lib.js';

//以 lib 做为命名空间进行调用,类似于object的方式
console.log(lib.square(11)); // 121

3.2、静态-按需导入

    从模块文件中导入单个或多个函数,与 * as namepage 方式不同,这个是按需导入,可指定需要导入的内容。

//导入square和 diag 两个函数
import { square, diag } from './lib.js';

// 只导入square 一个函数
import { square } from './lib.js';

// 导入默认模块
import MyClass from './mixed.js';

 注:和 export 一样,import也可以用 as 关键字来设置别名,当import的两个类的名字一样时,可以使用 as 来重设导入模块的名字,也可以用as 来简化名称。

// 用 as 来 简化函数名称
import {
  reallyReallyLongModuleExportName as shortName,
  anotherLongModuleName as short
} from './modules/my-module.js';

 3.3、动态导入

    静态import在首次加载时候会把全部模块资源都下载下来。我们实际开发时候,有时候需要动态import(dynamic import)。ES2020提案 引入import()函数,支持动态加载模块。

十四、其他

    ES6中不仅仅对字符串和数组进行了扩展,还对对内置对象Object、Number和Math也进行了扩展。如(只列出部分):

    Object.is() 方法判断两个值是否为同一个值。
    Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。
    Number.isSafeInteger() 方法用来判断传入的参数值是否是一个“安全整数”(safe integer)。
    Number.isNaN() 方法确定传递的值是否为 NaN,并且检查其类型是否为 Number。它是原来的全局 isNaN() 的更稳妥的版本。
    Math.acosh(x) 返回x的反双曲余弦(inverse hyperbolic cosine)。
    Math.imul()返回两个数以32位带符号整数形式相乘的结果,返回的也是一个32位的带符号整数
    Math.trunc() 用于除去一个数的小数部分,返回整数部分。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐