ES6

274 阅读53分钟

not ending 可能错误

阮一峰 ES6

2 let和const命令

let命令

  • 只在let命令所在的代码块内有效

  • for的一个特别之处:设置循环遍历的那部分是一个父作用域,而循环体内是一个单独的子作用域。

for(let i = 0; i< 3; i++) {
    let i = 'abc';
    console.log(i);
}
//abc
//abc
//abc

这表明函数内部的变量i和循环变量i不在同一个作用域,有各自单独的作用域。

  • 不存在变量声明:必须在声明后使用否则报错

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

    暂时性死区也意味着typeof也不是一个100%安全的操作。不容易发现的死区。

    function bar(x = y, y = 2){
    	return [x,y];//y是后定义的
    }
    
  • 不允许重复声明

    let不允许在相同作用域内,重复声明同一个变量。 因此不能在函数内部重新声明参数。

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

块级作用域

  • 为啥需要块级作用域 ES5只有全局作用域和函数作用域

  • 不合理场景一:函数内层变量可能覆盖外层变量

  • 不合理场景二:用来将技术的循环变量泄漏为全局变量

  • 块级作用域

ES6允许块级作用域的任意嵌套,但每一层都是一个单独的作用域

{{{{
	{let insane = "hello";}
	console.log(insane);//error,无法获取上一层的变量
}}}}

块级作用域的出现,使得广泛应用的匿名立即执行匿名表达式就不再必要了。

  • 块级作用域与函数声明

ES5规定,函数只能在顶层作用域和函数作用域中声明,不能在块级作用域中声明

if(true){
	function f(){}	
}
try{
	function f(){}
}catch(e)

以上在ES5中都是非法的,但浏览器没有遵守这个约定,还是支持在块级作用域中声明函数,因此以上两种不会报错。 ES6引入了块级作用域,明确允许在块级作用域中声明函数,但块级作用域之中的函数声明语句类似于let,在块级作用域之外不可引用 这一做法对老代码有很大的影响。 not ending ES6与浏览器兼容处理方法

const命令

const只读的常量,值不能被改变;即一旦被const声明就必须立即初始化;

值在块级作用域中有效,不提升,也存在暂时性死区,只能在声明的位置后面使用,

也不可重复声明。

本质:不是变量的值不能改动,而是变量指向的那个地址所保存的数据不得改动。

const foo = {};
foo.prop = 123;
foo.prop//123
foo = {}//报错

如果真的想将对象冻结,使用Object.freeze方法 将对象彻底冻结(对象和属性都冻结)

var constantize = (obj) => {
    Object.freeze(obj);
    Object.key(obj).forEach((key,i)){
        if(typwof obj[key]==='Object'){
            constantize(obj[key]);
        }
    }
}

顶层对象的属性

  • 顶层对象,在浏览器环境指的是window对象,在Node指的是global对象。在ES5中,顶层对象的属性和全局变量是等价的。

  • 顶层对象和全局对象挂钩被认为是败笔:

    引发了一些问题:

    首先,没法在编译时就报出变量未声明的错误,只有在运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);

    其次,程序员很容易不小心就创造了全局变量;

    最后,顶层对象的属性是导出可以读写的,这很不利与模块化编程。

    另外,window对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,这不合适

  • ES6,为了保持兼容性。var 和function定义的全局变量依旧是顶层对象的属性。但let、const、class命令声明的全局变量不属于顶层对象的属性(全局变量将逐步与顶层对象的属性脱钩)

globalThis对象

  • JavaScript语言存在一个顶层对象,它提供全局环境,但是实现不统一

    • 浏览器里面,是window,但Node和Web Worker没有window
    • 浏览器和Web Worker里面,self指向顶层对象,但Node没有self
    • Node里面,是global,但其他环境偶读不支持
  • 同一段代码为了能够在各种环境中,都能取得顶层对象,使用this变量,但有局限性

    • 全局环境中,this返回顶层对象。但Node模块和ES6模块中,this返回的当前模块
    • 函数里的this,如果函数不是作为对象的方法运行,而是单纯作为函数运行this指向性顶层函数。但在严格模式下,this返回undefined
    • 不管严格模式或普通模式,new Function('return this')(),总是返回全局对象???。但是浏览器使用了CSP(Content Security Policy内容安全策略),eval、new Function这些方法都可能无法使用。

    现在有一个提案,在语言标准的层面,引入globalThis作为顶层对象。即在任何环境下,globalThis都是存在的额,都可从它拿到顶层对象,指向全局环境下的this。

    //在所有情况下,都取到顶层对象。
    // 方法一
    (typeof window !== 'undefined'
     ? window
     : (typeof process === 'object' &&
        typeof require === 'function' &&
        typeof global === 'object')
       ? global
       : this);
    // 方法二
    var getGlobal = function () {
      if (typeof self !== 'undefined') { return self; }
      if (typeof window !== 'undefined') { return window; }
      if (typeof global !== 'undefined') { return global; }
      throw new Error('unable to locate global object');
    };
    

3. 变量的结构赋值

数组的解构解析

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

本质上,这种写法属于“模式匹配”。只有等号两边的模式相同,就会立即被赋值

let [a,b,c] = [1,2,3];
let [foo, [[bar], baz]] = [1, [[2], 3]];
let[, , thrid] = ["foo", "bar", "baz"];
let[x, ,y] = [1,2,3];

let[head, ...tail] = [1,2,3,4];
head//1
tail//[2,3,4]

let [x,y, ...z] = ['a']
x//'a'
y//undefined
z//[]!!!
  • 如果结构不成功,变量的值等于undefined
let [foo] = [];
let [bar,foo] = [1];
上面两行结构不成功,foo的值都会等于undefined
  • 但如果右边不是数组(即,不是可遍历的结构)将会报错
// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
//is not iterable

前五个是转为对象后不具备Iterator接口。最后一个是本身不具备Interator接口。

  • 对于set结构,也可以使用数组的解构赋值
let [x,y,z] = new Set(['a','b','c']);
x//'a'

事实上,只要某种数据结构具有Iterator接口,都可以采用数组形式的解构赋值。

  • 默认值
let [foo = true] = [];
foo // true

let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'

注:ES6内部使用===判断一个位置是否有值。所以只有当一个数组成员严格等于undefined,默认值才会生效

let [x = 1] = [undefined];
x // 1

let [x = 1] = [null];
x // null

null == undefined //true
null === undefined //false
  • 如果默认值是一个表达式,则这个表达式是惰性求值的,只有在用到的时候,才会求值
function f() {
  console.log('aaa');
}
let [x = f()] = [1];//f不会执行

//等价于下面
let x;
if ([1][0] === undefined) {
  x = f();
} else {
  x = [1][0];
}
//[0]是什么?error?????
  • 默认值可以引用解构赋值的其他变量,该变量必须已经声明
let [x = 1, y = x] = [];     // x=1; y=1
let [x = 1, y = x] = [2];    // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = [];     // ReferenceError: y is not defined
废话

对象的解构解析

解构不仅可用于数组,也可用于对象 注:数组的元素是按次序排列的,变量的取值由它的位置决定; 而对象的属性没有次序,变量必须与属性同名,才能取得正确的值。

let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"

let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined

对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量 如果变量名和属性名不一致,必须写成

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"

let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'

//简写
let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };

即对象的解构赋值的内部机制,是先找到同名属性,然后在赋给对应的变量。真正被赋值的是后者,而不是前者

一些嵌套的例子

let obj = {};
let arr = {};
({foo:obj.prop,bar:arr[0]}) = {foo:123,bar:true};
//Invalid left-hand side in assignment
  • 如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,将会报错,
let {foo:{bar}} = {baz:'baz'};

左边对象的foo属性,对应一个子对象。该子对象的bar属性,解构时会报错。因为foo此时等于undefined,在取子元素就会报错

  • 注:对象的结构解析可以取到继承的属性
const obj1 = {};
const obj2 = {foo:'bar'};
Object.setPrototypeOf(obj1,obj2);//给obj1设置
//obj1的原型对象是obj2.
const{foo}=obj1;
foo;//"bar"
  • 对象的解构也可以指定默认值
var {x = 3} = {};
x // 3

var {x, y = 5} = {x: 1};
x // 1
y // 5

var {x: y = 3} = {};
y // 3

var {x: y = 3} = {x: 5};
y // 5

var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"

//默认生效的条件,对象的属性值严格等于undefined
var {x = 3} = {x: undefined};
x // 3
var {x = 3} = {x: null};
x // null
  • 错误的写法
let x;
{x} = {x: 1};//{为一个代码块了}
//Uncaught SyntaxError: Unexpected token =

解构赋值允许等号左边的模式之中,不放置任何变量名。

由于数组本质是特殊的对象,因此可以对数组进行属性的解构。

字符串的解构解析

字符串被转换成了一个类似数组的对象

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

类似数组的对象都有一个length属性

let {length : len} = 'hello';
len // 5

数值和布尔值的解构解析

解构赋值规则,只要等号右边的值不是对象或数组,就会先将其转为对象。由于undefined和null无法转为对象,使用无法解构

let {toString: s} = 123;
s === Number.prototype.toString // true???

let {toString: s} = true;
s === Boolean.prototype.toString // true

let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

函数参数的解构赋值

圆括号问题

对于编译器来说,一个式子是模式还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才知道 如果模式中出现圆括号怎么处理? 建议只要有可能,就不要在模式中放置圆括号 例子.....

用途

  • 交换变量的值
let x = 1;
let y = 2;
[x, y] = [y, x];
  • 从函数返回多个值
function example(){
	return [1,2,3];
}
let [a,b,c] = example();

function example(){
	return {
		foo:1,
		bar:2
	}
}
let{foo,bar} = example();
//注:此处需写foo和bar
  • 函数参数的定义
//函数是一组有次序的值
function f([x,y,z]){...}
f([1,2,3]);
//函数是一组无次序的值
function f({x,y,z}){...}
f({z:3,y:2,x:1})
  • 提取JSON数据
let jsonData = {
	id:42,
	status: "OK",
	data: [867,5509]
};
let {id,status,data:number} = jsonData;
console.log(id, status, number)	;
//42,“OK”,[b67,5509]
  • 函数参数的默认值
  • 遍历Map解构

任何部署了Iterator接口的对象,都可以使用for...of循环遍历。

Map解构原生支持Interator接口,配合变量的结构赋值,获取简明和健值很方便。

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

// 获取键名
for (let [key] of map) {
  // ...
}

// 获取键值
for (let [,value] of map) {
  // ...
}
  • 输入模块的指定方法
const{SourceMapConsumer,SourceNode} = require("source-map")

4 字符串的拓展

5 字符串的新增方法

6 数值的拓展

7 函数的拓展

函数参数的默认值

  1. 基本用法

在ES6之前,不能直接为函数指定默认值,变通

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

但是当y是false时,就不起作用。为了避免,可先判断参数y是否被赋值

if(typeof y === 'undefined'){
    y = 'world';
}
  • ES6允许为函数的参数设置默认值,即直接写在参数的定义的后面
function log(x,y='world'){
    console.log(x,y);
}

除了简洁,ES6的写法还有两好处:

​ 阅读代码的人能立刻意识到哪些参数是可省略的,不用查看函数体或文档;

​ 其次,有利于将来的代码优化,即使未来的版本彻底拿掉这个参数,也不会导致以前的代码无法正常运行。

  • 参数变量是默认声明的,所以不能用let或const在此声明的
function foo(x=5) {
    let x = 1;
    const x = 2;
     // Uncaught SyntaxError: Identifier 'x' has already been declared
}
//用var不会报错
  • 使用参数默认值时,函数不能有同名参数
function foo(x,x,y=1){}
//Uncaught SyntaxError: Duplicate parameter name not allowed in this context
  • 容易忽略的地方:参数默认值不是传值的,而是每次都重新计算默认值表达式的值。即参数默认值是惰性求指的。
let x = 99;
function foo(p=x+1){
    console.log(p);
}
foo();//100
x = 100;
foo() //101
  1. 与解构赋值默s认值结合使用
function foo({x,y=5}){
    console.log(x,y);
}
foo({})
foo

8 数组的拓展

拓展运算符

(...)如同rest参数的逆运算,将数组转为用逗号分隔的参数序列。

console.log(...[1,2,3]);	//1 2 3
[...document.querySelectorAll('div')];	
//[<div>,<div>,<div>]

主要用于函数调用:将一个数组变成参数序列

function push(array,...items){
    array.push(...item);
}
function add(x,y){
    return x+y;
}
var numbers = [4,38];
add(...numbers);	//42

拓展运算符与正常的函数参数结合:

function f(v,w,x,y,z){}
var args = [0,1];
f(-1,...args,2,...[3]);

拓展运算符后面放置表达式

const arr = {
    ...(x>0?['a']:[]),
    'b',
};

...[]没有任何效果

[...[],1] //[1]

  • 代替数组的apply方法

由于拓展运算符可以展开数组,所以不需要用apply将数组转为函数的参数。

function f(x,y,x){}
var args = [0,1,2];
//ES5
f.apply(null,args)
//ES6
f(...args);

Math.max()函数将数组转为一个参数序列

//ES5
Math.max.apply(null,[14,3,2])
//ES6
Math.max([2,5,6])

通过push函数将一个数组加到另一个数组的尾部:push的参数不可以是数组

var arr1 = [0,1,2];
var arr2 = [3,4,5];
//ES5
Array.prototye.push.apply(arr1,arr2);//意义?
arr1.push.apply(arr1,arr2)
//ES6
arr1.push(...arr2);
//???
//ES5
new (Date.bind.apply(Date,[null,2015,1,1]))
//ES6
new Date(...[2015,1,1])
  • 拓展运算符的应用

合并数组

//ES5
//ES6
//ES5
//ES6

//ES5
[1,2].concat(mare);
//ES6
[1,2,...more];
//ES5
arr1.concat(arr2,arr3)
//ES6
[...arr1,...arr2,...arr3]

与解构赋值结合

//ES5
//ES6
//ES5
//ES6

函数的返回值

字符串

实现了Iterator接口的对象

Map和Set结构、Generator函数

Array.from()

用于将将两类对象成为真正的数组:类似数组的对象(array-like object)和可遍历对象(itreable)(包括ES6新增的数据结构Set和Map)

Array.of()

用于将一组值转换为数组。目的是弥补数组构造函数Array()的不足。因为参数的个数的不同会导致Array()的差异行为

数组实例的copyWithin()

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

参数:(target必写,start,end)

start,end默认问0,若为负数则表示倒数

数组实例的find()和findIndex()

find()找出第一个符合条件的数组成员。

参数:是一个回调函数,所有数组成员一次执行该回调函数

返回:若没有符合条件的则返回undefined

[1,2,10,4].find(function(value,index,arr){
    return value>9;
})//10

findIndex(),返回第一个符合条件数组的数组成员的位置。若无返回-1。

[1,2,10,4].find(function(value,index,arr){
    return value>9;
})//2

这两个方法还可接受第二个参数,用来绑定回调函数的this对象。?

另外,这两个方法都可以发现NaN。弥补了数组的IndexOf()方法的不足

[NaN].indexOf(NaN);	//-1
[NaN].findIndex(y=>Object.is(NaN,y)); //0

数组实例的fill()

用给定值填充一个数组(value,start,end)

['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'] 

数组实例的entries()、keys()和values()

用于遍历数组。它们都返回一个遍历器对象,可用for...of循环。

数组实例的includes()

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

数组的空位

9 Object的拓展

Object.assign()

Object.assign() 执行浅拷贝,不是深拷贝。它拷贝对象的第一层属性。嵌套的对象会在原始对象和副本对象之间共享。

  • 基本用法

Object(target,source1,source2,...):把source的可枚举属性复制到target。

若目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性

  • 注意点
  1. Object.assign是浅复制(地址的引用),不是深复制。即得到的是对这个对象的引用。
var obj1 = {a:{b:1},c:1};
var obj2 = Object.assign({},obj1);

obj1.a.b = 2; obj1.c = 2
obj2.a.b //2
obj2.c   //1

即复杂数据类型会变。

  1. 对于嵌套的对象,遇到同名属性,Object.assign是替换而不是添加
var target = {a:{b:'c',d:'e'}};
var source = {a:{b:'hello'}};
Object.assign(target,source)
//{a: {b: "hello"}}
//注意结果不是{a:{b:'hello',d:'e'}}
  1. Object.assign在处理数组时,会把数组当做对象处理
Object.assign([1,2,3],[4,5])
//[4,5,3]

Object.assign把数组视为属性名为0,1,2的对象

  • 常见用途
  1. 为对象添加属性
class Point {
    constructor(x,y){
        Object.assign(this,(x,y))
    }
}

通过assign将xy属性添加到Point类的对象的实例中

  1. 为对象添加方法
Object.assign(SomeClass.prototype,{
    someMethod(arg1,arg2){
        //...
    },
    antherMethod(){
        //...
    }
});
//等同于下面的写法
SomeClass.prototype.someMethod = function(arg1,arg2){
    //...
};
SomeClass.prototype.someMethod = function(arg1,arg2){
    //...
};
  1. 克隆对象
function clone(origin){
    return Object.assign({},origin);
}

但这种方法只能克隆原始对象自身的值,不能克隆继承的值。

若需要保持继承链

function clone(origin){
	let originProto = Object.getPrototypeOf(origin);
	return Object.assign(Object.create(originProto),origin);
}
  1. 合并多个对象

将对各对象合并到某个对象

const merge = (target,...sources) => Object.assign(target,...sources);

若希望合并返回一个新对象,可改写,对一个空对象合并

const merge = (...sources) => Object.assign({},...sources);

例子使用不成功???

  1. 为属性指定默认值
const DEFAULTS = {
	logLevel: 0,
	outputFormat: 'html'
};
function processContent(options){
    options = Object.assign({},DEFAULTS,options);
    console.log(options);
    //....
}

DEFAULTS对象是默认值,options对象是用户提供的参数。若两者有重名属性,options会覆盖DEFAULTS的。

注:DEFAULTS对象和options对象的所有属性只能是简单类型,而不能指向另一个对象,否则DEFAULTS对象的该属性不起作用

const DEFAULTS = {
	url: {
		host: 'example.com',
        port: 7070
	},
};
processContent({url: {port:8000}})
//{url:{port:8000}},host不存在了

属性的可枚举性

对象的每个属性都有一个描述对象(Descriptor),用于控制该属性的行为。

用**Object.getOwnPropertyDescriptor(obj,attr)**可以获取description。

let obj = { foo:124 };
Object.getOwnPropertyDescriptor(obj,'foo');

/*{
    configurable: true
	enumerable: true
	value: 124
	writable: true
}*/

ES5有3个操作会忽略enumerable(可枚举性)为false的属性

* for...in循环: 只遍历对象自身和继承的可枚举属性
* OBject.keys(): 返回对象自身可枚举属性的键名
* JSON.stringify(): 只串行化对象自身的可枚举属性

ES6新增了1个可操作Object.assign(),会忽略enumerable为false的属性,是负值对象自身的可枚举属性

以上4个操作中,只有for...in会返回继承的属性

引入enumerable的最初目的是让某些属性可以规避掉for...in操作。比如原型对象的toString方法以及数组的length属性,通过这种手段而不会被for...in遍历到了。

toString和length属性的enumber都是false,因此for...in不会遍历到这两个继承自原型的属性

Object.getOwnPropertyDescriptor(Object.prototype,'toString').enumerable;//false
Object.getOwnPropertyDescriptor([],'length').enumerable;//false

另外,ES6规定,所有Class的原型的方法都是不可枚举的。

总的来说,操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以尽量不要用for...in循环,而用Object.keys()替代

枚举:即遍历

属性的遍历

ES6共有5种可以遍历对象的属性

  • for...in

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

  • Object.keys(obj)

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

  • Object.getOwnPropertyNames(obj)

    返回一个数组,包含对象自身的所有属性(不含Symbol属性,但包括不可枚举属性

  • Object.getOwnPropertySymbols(obj)

    返回一个数组,包含对象自身的所有Symbol属性

  • Reflect.ownKeys(obj)

    返回一个数组,包含对象自身的所有属性,不管属性名是Symbol还是字符串,也不管是否可枚举。

    Reflect.ownKeys({[Symbol()]:0,b:0,10:0,2:0,a:0});
    //["2", "10", "b", "a", Symbol()]
    

    先是数值属性,然后字符串属性,最后Symbol属性

__proto__属性及拓展

  • __proto__

__proto__属性用于读取或设置当前对象的prototype对象。目前,所有浏览器(包括IE11)都部署了这个属性

//ES6
var obj = {
    method: function(){}
}
var someOtherObj = {a:1};
obj.__proto__ = someOtherObj;
//ES5
var obj = Object.create(someOtherObj);
//someOtherObj在__proto__上
obj.method = function(){
    //...不知道写啥
}

__proto__没有写入ES6的正文,而是写入了附录,原因proto

的前后双下画线说明它本质上一个内部实现,而不是一个正式的对外的API,只是由于浏览器广泛支持,才被加入了ES6。标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定要部署,而且新代码最好认为这个属性不存在。即不要用__proto__

__proto__调用的是Object.prototype.__proto__,实现如下

Object.defineProperty(Object.prototype,'__proto__',{
    get(){
        let _thisObj = Object(this);
        return Object.getPrototype(_thisObj);
    },
    set(proto){
        if(this===undefined||this===null){
            throw new TypeError();
        }
        if(!isObject(this)){
            retrn undefined;
        }
        let status = Reflect.setPrototypeOf(this,proto);
       	if(!status){
            thorw new TypeError();
        }
    };
});
function isObject(value){
    return Object(value) === value;
}

如果一个对象部署了__proto__属性,则该属性的值就是对象的原型。

Object.getPrototypeOf({__proto__:null});//null

  • Object.setPrototypeOf()

作用与__proto__相同,用来设置一个对象的prototype对象,返回参数对象本身。它是ES6正式推荐的设置原型对象的方法。

Object.setPrototypeOf(object,prototype)

// 用法
var o = Object.setPrototypeOf({},null);//o={什么也没有}
// 该方法等同于下面的函数
function(obj,proto){
    obj.__proto__ = proto;
    return obj;
}
// 例子
let proto = {};
let obj = {x:10};
Object.setPrototypeOf(obj,proto);
// obj有__proto__没有prototype
proto.y = 20;
proto.z = 40;
obj.x //10
obj,y //20
obj.z //40

若第一个参数不是对象,则会自动转为对象,但是由于返回的还是第一个参数,所以这个操作不会产生任何效果

Object.setPrototypeOf(1,{})===1; //true
Object.setPrototypeOf('foo',{})==='foo';  //true
Object.setPrototypeOf(true,{})===true;  //true
  • Object.getPrototypeOf()

    • 用于读取一个对象的prototype对象。Object.getPrototypeOf(obj)
function Rectangle(){
    //...
}
var rec = new Rectangle();
Object.getPrototypeOf(rec) === Rectangle.prototype; //true
Object.setPrototypeOf(rec,Object.prototype); //改变rec的prototype
Object.getPrototypeOf(rec) === Rectangle.prototype; //false

如果参数不是对象,则会自动转为对象


如果参数是undefined或null,它们无法转为对象,所以会报错

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

  • Object.keys()
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]
}

  • Object.values()

  • Object.entries()

对象的拓展运算符

ES2017将拓展运算符引入对象

  • 解构赋值

对象的解构赋值用于从一个对象取值,相当于将所有可比遍历的、但尚未被读取的属性分配到指定的对象上。所有的键和值都会复制到新对象上

let {x,y,...z} = {x:1,y:2,a:3,b:4};
x //1
y //2
z //{a:3,b:4}

由于解构赋值要求等号右边是一个对象,所以如果等号右边是undefined或null就会报错,因为它们无法转为对象。

解构赋值必须是最后一个参数,否则报错

解构赋值是浅复制。即如果一个键的值是符合类型,则解构赋值复制的是这个值的引用,而不是这个值的副本。

解构赋值不会复制继承原型对象的属性。

let o1 = {a:1};
let o2 = {b:2};
o2.__proto__ = o1;
let {...o3} = o2;
o3 //{b:2}
o3.a //undefined
o2 //{b:2,{__proto__:{a:1}}}

// 例子2
var o = Object.create({x:1,y:2});
//create是用于创建在__proto__上的
o.z =3;
let {x,...{y,z}} = o;//error
//以下没有测试
x //1
y,z是双重解构,只能读取o自身的属性,所以只有变量z可以赋值成功
y //undefined
z //3

解构赋值的另一个用处是拓展某个函数的参数,引入其他操作

function baseFunction({a,b}){
    //...
}
function wrapperFunction({x,y,...restConfig}){
    //使用x和y参数进行操作
    //其余参数传给原始函数
}
  • 拓展运算符

(...)用于取出参数对象的所有可遍历属性,将其复制到当前对象中。

Object.getOwnPropertyDescriptors()

ES5的Object.getOwnPropertyDescriptor()用来返回某个对象属性的描述对象(descriptor)

var obj = {p:'a'};
Object.getOwnPropertyDescriptor(obj,'p')
{
	configurable: true
	enumerable: true
	value: "a"
	writable: true
	__proto__: Object
}

ES2017引入Object.getOwnPropertyDescriptors(),返回指定对象所有自身属性(非继承属性)的描述对象

const obj = {
    foo: 123,
    get bar() { return 'abc' }
};
Object.getOwnPropertyDescriptors(obj);
{
    //{foo: {…}, bar: {…}}
    bar: {
		get: {Function: bar}
		set: undefined
        configurable: true
		enumerable: true
		__proto__: Object
    }
    foo: {
		value: 123
        configurable: true
		enumerable: true
		writable: true
		__proto__: Object
		__proto__: Object 
    }
}

该方法的实现很容易

function getOwnPropertyDescriptors(obj){
    const result = {};
    for(let key of Reflect.ownKeys(obj)){
        result[key] = Object.getOwnPropertyDescriptor(obj,key);
    }
    return result
}
//no test

该方法的引入主要是为了解决Object.assign()无法正确复制get和set属性的问题

const source = {
    set foo(value){
        console.log(value);
    }
};
const target1 = {};
Object.assign(target1,source);
Object.getOWnPropertyDescriptor(target1,'foo');
//no tast

Null传导运算符

若要读取对象内部的某个属性。

const firstName = (message
                   && message.body
                   && message.body.user
                   && message.body.user.firstName) || 'default';

这样层次判断很麻烦,

const firstName = message?.body?.user?.firstName || 'default'

以上?.运算符,只要其中一个返回null或undefined,就不再继续运算,而是返回undefined。 “Null传导运算符”有4种写法

  • obj?.prop:读取对象属性
  • obj.[expr]:读取对象属性
  • func?.(...args):函数或对象方法的调用
  • new C?.(...args):构造函数的调用

?.而非?是为了区别三元运算符

例子:

a?.b.c().d

Object.create()——ES5

用特定的原型对象创建一个新对象。用来做单一的继承

{}并不是真正意义上的空,因为它指向Object.prototype的链接。为了创建一个真正的空对象,使用Object.create(null)。它会创建一个没有任何属性的对象。这通常用来创建一个Map

Object.freeze()——ES5

Object.freeze() 实行浅冻结。要深冻结,需要递归冻结对象的每一个属性。

10 Symbol

概述

ES5的对象属性名都是字符串,容易造成冲突。引入Symbol让属性名独一无二

ES6的原始数据类型有:undefined,null,Boolen,String,Number,Object,Symbol

注:Symbol函数前不能使用new命令,会报错。因为Symbol是一个原始类型的值,不是对象。是一种类似于字符串的数据类型

let s = Symbol();
typeof s
// "symbol"

let s1 = Symbol('foo');
s1;//Symbol(foo)
s1.toString()//"Symbol(foo)"

  • 加参数只是方便控制台输出,不加输出就是Symbol()分不清;

参数只是对象当前Symbol值的描述,因此相同参数的Symbol函数的返回值是不相等的。

如果Symbol的参数是一个对象,就会调用该对象的toString方法。转为一个字符串,在生成一个Symbol值

  • Symbol值不能与其他类型的值进行运算,会报错

+连接符也不可以

但是Symbol值可以显示转为字符串,隐式的不行

let sym = Symbol('My symbol);
String(stm);//'Symbol(My symbol)'
sym.toString();//'Symbol(My symbol)'
???
  • Symbol也可以转为布尔值,不能转为数值
let sym = Symbol();
Boolean(sym) // true
!sym  // false

if (sym) {
  // ...
}

Number(sym) // TypeError
sym + 2 // TypeError

Symbol.prototype.description

实例属性description,直接返回Symbol的描述

const sym = Symbol('foo');
String(sym) // "Symbol(foo)"
sym.toString() // "Symbol(foo)"

const sym = Symbol('foo');
sym.description;//foo

作为属性名的Symbol

let mySymbol = Symbol();
//第一种写法
let a = {};
a[mySymbol] = 'hello';
//第二种写法
let a = {
	[mySymbol]:"hello"
}
//第三种写法
let a = {};
Object.definePrototype(a,mySymbol,{value:"hello"});
//以上三种写法都得到同样的结果
a[mySymbol];//"hello"

注:Symbol值作为对象属性名时,不能用点运算符。

实例:消除魔术字符串

属性名的遍历

Symbol.for(),Symbol.keyFor()

实例:模块的SingLeton模式

内置的Symbol值

11 Set和Map数据结构

Set

  • 基本用法

Set类似于数组,但成员的值都是唯一的,无重复。

Set本身是一个构造函数,用来生成Set数据机构

  • Set实例的属性和方法

    Set结构的实例的属性如下

    • Set.prototype.constructor:构造函数
    • Set.prototype.size:成员总数

    Set结构的方法分为两大类:操作方法和遍历方法

    • add(value):
    • delete(value):删除,返回布尔值表示是否成功
    • has(value):返回布尔值
    • clear():清空所有成员,没有返回值
  • 遍历操作

    Set结构的实例有4个遍历方法,可用于遍历成员

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

WeakSet

  • 含义

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

  1. WeabSet的成员只能是对象,而不能是其他类型的值
  2. WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用,即如果其他对象不再引用该对象,则垃圾回收机制会自动回收该对象所占用的内存,不应该考虑该对象是否还存在于weakSet之中。
  • 语法

Map

  • 含义和基本用法
  • 实例的属性和操作方法
  • 遍历方法
  • 与其他数据结构的互相转换

WeakMap

  • 含义
  • WeakMap的语法
  • WeakMap的示例
  • WeakMap的用途

12 Proxy

概述

Proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,即属于一种“元编程”(meta programming),即对象编程语言进行编程。

Proxy相当于在目标对象前设一个“拦截层”,对外界的访问进行过滤和改写。代理某些 操作,“代理器“

var obj = new Proxy({},{
    get:function(target,key,receiver){
        console.log(`getting ${key}!`);
        return Reflect.get(target,key,receiver);
    },
    set:function(target,key,value,receiver){
        console.log(`setting ${key}!`);
        return Reflect.set(target,key,value,receiver);
    }
});

以上是对一个空对象进行了一层拦截,重定义了属性的get和set行为。

//运行结果
obj.count = 1
setting count
1

++obj.count
getting count!
setting count!
2

说明Proxy实际上重载overload了点运算符,即用自己的定义覆盖了语言的原始定义。

  • ES6原生提供Proxy构造函数,用于生成Proxy实例。
var proxy = new Proxy(target,handler);
//target要拦截的目标对象,handler配置对象,用于定制拦截行为

例子:拦截读取属性行为

var proxy = new Proxy({},{
    get:function(target,property){
        return 35;
    }
});

proxy.time //35
proxy.name //35
proxy.title //35

注:要使Proxy起作用,必须对的Proxy实例(上例中是proxy对象)进行操作,而不是针对目标对象(上例中是空对象)进行操作。????

如果handler没有设置任何拦截,那就等同于直接通向原对象。

var target = {};
var handler = {};
var proxy = new Proxy(target,handler);
proxy.a = 'b';
target.a //'b'

上面代码中,handler是一个空对象,访问handler就等同于访问target

  • 一个技巧将Proxy设置到object.proxy属性,从而可以在object对象上调用
var object = {proxy:new Proxy(target,handler)};
  • Proxy实例也可作为其他对象的原型对象
var proxy = new Proxy({},{
    get:function(target,property){
        return 35;
    }
});

let obj = Object.create(proxy);
obj.time//35

上面代码,proxy对象是obj对象的原型,obj对象本身没有time属性,所以根据原型链会在proxy对象上读取该属性,导致被拦截

  • 同一个拦截器函数可以设置拦截多个操作
var handler = {
    get: function(taraget,name){
        if(name==='prototype'){
            return Object.prototype;
        }
        return 'Hello,' + name;
    },
    apply:function(target,thisBinding,args){
        return args[0];
    },
    construct:function(target,args){
        return {value:args[1]};
    }
}
var fproxy = new Proxy(function(x,y){
    return x+y;
},handler);

fproxy(1,2)//1
new fproxy(1,2) //{value:2}
fproxy.prototype === Object.prototype //true
fproxy.foo //'Hello,foo'

Proxy实例的方法

即Proxy支持的所有拦截操作

  • get(target,propKey,receiver)

拦截对象属性的读取

  • set(target,propKey,value,receiver)

拦截对象属性的设置

  • has(target,propKey)

拦截propKey in proxy的操作,返回一个布尔值

  • deleteProperty(target,propKey)

拦截delete proxy[propKey]的操作,返回一个布尔值

  • ownKeys(target)

拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Obkect.keys(proxy),返回一个数组。该方法返回目标对象所有自身属性的属性名,而Object.keys()的返回结果仅包括目标对象自身可遍历的属性

  • getOwnPropertyDescriptor(target,propKey)

拦截Object.getOwnPropertyDescriptor(target,propKey),返回属性的描述对象

  • defineProperty(target,propKey,propDesc)

拦截Object.defineProperty(target,propKey,propDesc),Object.defineProperties(target,propKey,propDesc),返回一个布尔值

  • preventExtensions(target)

拦截Object.preventExtensions(proxy),返回一个布尔值

  • getPrototypeOf(target)

拦截Object.getPrototypeOf(proxy),返回一个对象

  • isExtensible(target)

拦截Object.isExtensible(proxy),返回一个对象

  • setPrototypeOf(target,proto)

拦截Object.setPrototypeOf(target,proto),返回一个布尔值,如果目标对象是函数,则还有两种额外操作可以拦截

  • apply(target,object,args)

拦截Proxy实例,并将其作为函数调用的操作,比如proxy(...args)、proxy.call(object,...args)、proxy.apply(...)

  • construct(target,args)

拦截Proxy实例作为构造函数调用的操作,比如new Proxy(args).

Proxy.revocable()

返回一个可取消的Proxy实例。

this问题

实例:Web服务的客户端

Proxy对象可以拦截目标对象的任意属性,这使得它很合适用来编写Web服务的客户端

const service = createWebService('http://example.com/data');
service.employees().then(json=>{
    const employees = JSON.parse(json);
    //...
});

上面的代码新建了一个web服务的接口,这个接口返回各种数据。Proxy可以拦截这个对象的任意属性,所以不用为每一种数据写一个适配方法,只要写一个Proxy拦截即可。

function createWebService(barseUrl){
    return new Proxy({},{
        get(target,proxyKey,reciever){
            return () => httpGet(baseURl+'/'+propKey);
        }
    });
}

同理,Proxy也可用来实现数据库ORM层。

13 Reflect

概述

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

  • Reflect对象的设计目的如下:

    • 将Object对象的一些明显属于语言内部的方法放到Reflect对象上。

      (比如Object.defineProperty)

    现阶段,某些方法同时放在Object和Reflect对象上部署,未来的新方法将只在Reflect对象上部署。即从Reflect对象上可以获得语言内部的方法

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

    比如,Object.defineProperty(obj,name,desc)在无法定义属性时会跑错一个错误,而Reflect.defineProperty(obj,name,desc)则会返回false

    //旧写法
    try {
        Object.defineProperty(target,property,attributes);
        //success
    } catch(e){
        //failure
    }
    //新写法
    if(Reflect.defineProperty(target,property,attributes)){
    	//success
    } else {
        //failure
    }
    
    • 让Object操作都变成函数行为。
    //旧写法
    'assign' in Object //true
    //新写法
    Reflect.has(Object,'assign')//true
    
    • Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。

      这就使Proxy对象可以方便地调用对应的Reflect方法来完成默认行为,作为修改行为的基础。即如论Proxy如何修改默认行为,我们总可以在Reflect上获取默认行为。

    
    

静态方法

大部分与Object对象的同名方法的作用是相同的,而且与Proxy对象的方法是一一对应的。

实例:使用Proxy实现观察者模式

14 Promise对象

  • Promise的含义:

Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大

ES6将其写进了标准语言,统一了用法,原生提供了Promise对象。

Promise是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise是一个对象,从它可以获取异步操作的消息。Promise提供统一的API,各种异步操作都可以用同样的方法进行处理。

有了Promise对象,就可以将异步操作以同步的流程表达出来???,避免了层层嵌套的回调函数。此外,Promise对象提供统一个接口,使得控制异步操作更加容易。

  • Promise对象的两个特点:

    • 对象的状态不受外界的影响。

      Promise对象代表一个异步操作,有三种状态:

      • ending(进行中),
      • fulfilled(已成功),
      • rejected(已失败)。

      只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。“承诺”,其他手段无法改变。

    • 一旦状态改变,就不会再变,任何时候都可以得到这个结果。

      Promise对象的状态改变只有两种可能:

      • 从pending变为fulfilled
      • 从pending变为rejected

      此时状态就凝固, 不会在变了,会一直保持这个结果,称为resolved(已定型)

  • Promise也有一些缺点

    • 首先,无法取消promise,一旦新建它就会立即执行,无法中途取消。
    • 其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。!!!,那会处于什么状态呢
    • 第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。 如果某些事件不断地反复发生,一般来说,那使用**Stream模式?**是比部署Promise更好的选择。

基本用法

ES6规定,Promise对象是一个构造函数,用来生成Promise实例。

const promise = new Promise(function(resolve,reject){
    if(异步操作成功){
        resolve(value);
    } else {
        reject(error);
    }
})

参数resolve和reject是由JavaScript引擎提供的,不用自己部署。

resolve函数作用:将promise对象从pending变成resolved,在异步操作成功时调用,并将异步操作的结果,作为参数传递出去

reject函数作用:将promise对象的状态从“pending”变成“reject”,在异步操作失败时候调用,并将异步操作报出的作物,作为参数传递出去。

  • Promise实例生成后,可以用then方法分别指定resolve状态和rejected状态的回调函数
promise.then(function(value){
//success
},function(error){
//failure
})
  • then方法可以接受两个回调函数作为参数

第一个回调函数是Promise对象的状态变为resolved时调用, 第二个回调函数是Promise对象的状态变为rejected时调用。 其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。(就是then的参数可以是promise对象)

function timeout(ms){
    return new Promise((resolve,reject)=>{
        setTimeout(resolve, ms, 'done');
    });
}
timeout(100).then((value)=>{
	console.log(value);
})

timeout方法返回一个实例,表示一段事件后才会发生的结果。过了指定的时间(ms)参数后,promise实例的状态变为resolved,就会触发then方法绑定的回调函数

  • Promise新建后就会立即执行
let promise = new Promise(function(resolve, reject){
    console.log('Promise');
    resolve();
});
promise.then(function(){
	console.log('resolved');
});
console.log('Hi!');
//运行结果
//Promise
//Hi
//resolved

以上说明,Promise新建后立即执行,然后then方法指定的回到函数,将在当前脚本所有同步执行完后才会执行。所有resolved最后输出,废话

  • 一些例子

    • 异步加载图片的例子
    function loadImageAsync(url){
        return new Promise(function(resolve, reject){
            const image = new Image();
            image.onload = function(){
                resolve(image);
            };
            image.onerror = function(){
                reject(new Error('could not load image at'+url));
            };
            image.src = url;
        });
    }
    
    • 用Promise对象实现Ajax操作的例子
    const getJSON = function(url){
        const promise = new Promise(function(resolve, reject){
            const hanlder = function(){
                if(this.readyState!==4){ return }
                if(this.status===200){
                    resolve(this.response);
                } else {
                    reject(new Error(this.statusText));
                }
            };
            const client = new XMLHttpRequest();
            client.open("GET", url);
            client.onreadystatechange = handler;
            client.responseType = 'json';
            client.setRequestHeader("Accept", "application/json");
            client.send();
        });
        return promise;
    }
    getJSON('/post.json').then(function(json){
    	console.log('Contents:'+json);
    },function(error){
    	console.log('出错了',error);
    })
    

    注:上面代码resolve函数和reject函数调用时,都带有参数。当有参数,则他们的参数会被传递给回调函数。

    并将异步操作的结果,作为参数传递出去

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

    不用纠结于此处

    • resolve函数的参数除了正常的值以外,还可能是另一个Promise的实例。
    const p1 = new Promise(function (resolve, reject) {
      // ...
    });
    const p2 = new Promise(function (resolve, reject) {
      // ...
      resolve(p1);
      //即一个异步的操作的结果是返回另一个异步操作
      //此时p1的状态就决定了p2的状态。p2会根据p1的状态执行相应的代码。此时p1可能是pending,resolved,rejcted
    })
    
    const p1 = new Promise(function(resolve, reject){
    	setTimeout(()=> reject(new Error('fail')),3000)
    })
    const p2 = new Promise(function(resolve, reject){
    	setTimeout(()=> resolve(p1),1000)
    })
    p2.then(result => console.log(result))
    .catch(error => console.log(error));
    //Error:fail
    //p1决定p2的状态
    

    注:调用resolve或reject并不会终结Promise的函数的执行

    new Promise((resolve, reject) => {
    	resolve(1);//这个1是会传递给回调函数,就是会传递给then里面的r
    	console.log(2);//会执行
    }).then(r => {
    	console.log(r);
    });
    //2
    //1
    

    一般来说,调用resolve或reject以后,Promise的使命就完成了,后续操作应该可以放到then方法里面,而不应该直接写在resolve或reject的后面。所以,最后在它们前面加上return语句,这样就不会有意外。

    new Promise((resolve, reject)=>{
    	return resolve(1);
    	console.log(2);//如果不写return,这里会执行,但因为没有意义,因为Promise的使命完成了
    })
    

Promise.prototype.then()

then()方法的作用是为Promise实例添加!!状态改变!!时的回调函数

then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数

then方法返回的是一个新的promise实例!!!,链式写法

getJSON('/posts.json').then(function(json){
	return json.post;
}).then(function(post){
	//....??
})

依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。

采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待Promise对象的状态发生变化,才会被调用。

getJSON('/post/1.json').then(function(post){
	return getJSON(post.commentURL);
}).then(function(comments){
	console.log("resolved:",comments);
},function(err){
	console.log("rejected:",err);
})
//采用箭头函数的写法
getJSON("/post/1.json").then(
	post => getJSON(post.commentURL)
).then(
	comments => console.log("resolved:", comments),
	err => console.log("rejected:",err)
);

Promise.prototype.catch()

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

  • 即catch是改写了then
getJSON('/posts.json').then(function(posts){
	//...
}).catch(function(error){
	//处理getJSON和前一个回调函数运行时发生的错误。
	console.log("发生错误!",error);
})
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));
  • catch例子
//写法一
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);
});

可以看出,reject方法的作用等同于抛出错误

  • 如果Promise状态状态已经变成resolved,再抛出错误是无效的。因为Promise的状态一旦改变,就永久保持该状态,不会在变了。
const promise = new Promise(function(resolve,reject){
	resolve('ok');
	throw new Error('test');//状态不会变了
});
promise.then(function(value){console.log(value)})
	.catch(function(erro0r){console.log(error)})
//ok
  • Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。
getJSON('/post/1,json').then(function(post){
	return getJSON(post.commentURL);
}).then(function(comments){
	//some code
}).catch(function(error){
	//处理前面三个Promise产生的错误
})
  • 一般来说,不要在then方法里面定义Reject状态的回调函数(即then的第二个参数),总是使用catch方法。
// bad
promise
  .then(function(data) {
    // success
  },function(err) {
    // error
  });

// good
promise
  .then(function(data) { 
    // success
  })
  .catch(function(err) {
    // error
  });

建议使用catch方法,而不使用then方法的第二个参数

**有多个promise的时候写几个catch合适 ???**1个吧

  • 跟传统的try/catch代码块不同的是,如果没有使用catch方法指定错误处理的回调函数,Promise对象抛出的错误不会传递到外层代码,即不会有任何反应
const someAsyncThing = function(){
	return new Promise(function(resolve, reject){
		resolve(x+2);//报错,因为x没有声明
	})
};
someAsyncThing().then(function(){
	console.log('everything is great');
});

setTimeout(()=>{console.log(123)},2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123

上面的代码Promise中内部有语法错误,浏览器运行到这一行会打印错误提示,但不会退出进程,终止脚本。

这个脚本放在服务器执行,退出码是0(即表示成功)。不过,Node有一个unhandleRejection事件,专门监听未捕获的reject错误,上面的脚本会触发这个事件的监听函数,可以监听函数里面抛出的错误。

process.on('unhandleRejection', function(err,p){
	throw err;	
});

unhandleRejection事件的监听函数有两个参数,第一个是错误对象,第二个是报错的Promise实例,可以用来了解发送错误的环境信息。

someAsyncThing().then(function(){
	console.log('everything is great');
}).catch(err=>console.log(err));
setTimeout(()=>{console.log(123)},2000);
//运行结果
ReferenceError: x is not defined
    at <anonymous>:3:11
    at new Promise (<anonymous>)
    at someAsyncThing (<anonymous>:2:9)
    at <anonymous>:6:1
123

Promise.prototype.finally()

  • finally方法用于指定不管Promise对象最后状态如何,都会执行的操作。该方法是ES2018引入标准的。
promise
.then(result=>{...})
.catch(result=>{...})
.finally(()=>{...})

不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数

  • 例子:服务器使用promise处理请求,然后使用finally方法关掉服务器
server.listen(port)
	.then(function(){
		//...
	})
	.finally(server.stop);
  • finally方法的回调函数不接受任何参数

这意味着没有办法知道,前面的Promise状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,一个是与状态无关的,不依赖于Promise的执行结果。

  • finally本质上是then方法的特例
promise
.finally(() => {
  // 语句
});
// 等同于
promise
.then(
  result => {
    // 语句
    return result;
  },
  error => {
    // 语句
    throw error;
  }
);

如果不使用finally方法,同样的语句需要为成功和失败两种情况各写一次,有了finally方法,则只需要写一次。

  • finally的实现
Promise.prototype.finally = function(callback){
	let p = this.constructor;
	return this.then(
		value => p.resolve(callback()).then(() => value),
		reason => p.resolve(callback()).then(()=> {throw reason})
	);
};

即不管前面的promise是fulfulled还是rejected,都会执行回调函数callback。

从上面的实现可以看到,finally方法总是会返回原来的值。

//resolve的值是undefined
Promise.resolve(2).then(()=>{},()=>{});
//resolve的值是2
Promise.resolve(2).finally(()=>{});

//reject的值是undefined
Promise.resolve(3).then(()=>{},()=>{});
//reject的值是3
Promise.resolve(3).finally(()=>{});

Promise.all()

用于将多个Promise实例,包装成一个新的Promise实例

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

Promise.race()

将多个Promise实例,包装成一个新的Promise

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

Promise.allSelected

接收一组Promise实例作为参数,包装成一个新的Promise实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束,该方法由ES2020引入。

Promise.any()

接受一组Promise实例作为参数,包装成一个新的Promise实例。

Promise.resolve

用于将对象转为Promise对象。

Promise.rejected

返回一个新的Promise实例,该实例的状态为rejected

应用

  • 加载图片
const preloadImage = function(path){
	return new Promise(function(resolve,reject){
		const image = new Image();
		image.onload = resolve;
		image.onerror = reject;
		image.src = path;
	})
}
  • Generator与Promise的结合

Promise.try()

15Interator和for...of循环

Iterator(遍历器)的概念

默认Iterator接口

调用Iterator接口的场合

字符串的Iterator接口

遍历器对象的return()、throw()

for....of循环

ES6借鉴后端语言,引入for...of循环作为遍历所有数据结构的统一方法。

一个数据结构只要内部部署了Symbol.iterrator属性,就视为具有Iterator接口。就可用for...of循环调用的是数据结构的Symbol.iterator方法。

for...of使用范围包括:数组,Set,Map,某些类数组对象(argument对象,DOM NodeList对象),Generator对象,字符串.

for...in只能获取对象的键名,不能直接获取健值。ES6提供的for...of获取健值。

  • 数组

    数组的比那里去接口只返回具有数字索引的属性。

    let arr = [1,2,3];
    arr.foo = 'hello';
    for(let i in arr){ 
        console.log(i);//1,2,3,foo
    }
    for(let i of arr) {
        console.log(i);//1,2,3
    }
    
  • Set和Map

    遍历的顺序按照各个成员被添加进数据结构的顺序。

    Set遍历返回的是值。Map遍历返回的是数组[键名,键值]

    var engines = new Set(["aa","bb","cc"]);
    for(var e of engines){
        console.log(e);//aa,bb,cc
    }
    
    var es6 = new Map();
    es6.set("edition",6);
    es6.set("committee","t");
    for(var [name,value] of es6){
        console.log(name+":"+value);
        //edition:6 //committee:t
    }
    
    let map = new Map().set('a',1).set('b',2);
    for(let pair of map){
        console.log(pair);
        //["a", 1] //["b", 2]
    }
    for(let [key,value] of map){
        console.log(name+":"+value);
        //a:1 //b:2
    }
    
  • 计算生成的数据结构

    ES6数组,Set,Map有如下方法,调用后都会返回遍历器对象

    • entires()

      返回一个遍历器对象,用于遍历 [键名,键值]组成的数组。

    • keys() 键名

    • values() 键值

  • 类似数组的对象

    字符串、DOM NodeList对象、arguments对象

    对于字符串,for...of循环,可以正确识别32位UTF-16字符。

    不是所有类似数组的对象都具有Iterator接口,解决方,用Array.from方法将其转为数组。

  • 对象

    可用for...in循环遍历键名

    Object.keys(obj)

    for(var key of Object.keys)

  • 与其他遍历语法的比较

    数组,for循环;内置的forEach方法(无法退出forEach循环,break命令或return命令都不能奏效);for...in遍历键名。

  • for...of与for...in的比较

    for...in循环的缺点

    • for...in循环不仅可遍历数字键名,还会遍历手动添加的键,甚至包括原型链上的键。
    • 某些情况下,for...in循环会以任意顺序遍历键名。

    for...of循环是为遍历对象设计的,不使用于遍历数组。

    • 有着for...in一样的简洁语法,但没有fro...in的缺点
    • 不同于forEach方法,可与break、continue和return配合使用
    • 提供了遍历所有数据结构的统一操作接口

16Generator函数的语法

简介

1 基本概念

Generator函数是异步编程解决方案。可理解成是一个状态机,封装了多个内部状态。会返回一个遍历器对象(这个对象可以遍历Generator函数内部的每一个状态)。

Generator函数特征:function*,内部用yield定义不同的内部状态。

function* A();function *A(); function*A() function * A()

写法都可

function* helloGenerator(){
    console.log(0);
    yield 'hello';
    yield 'world';
    return 'ending'
}
var hw = helloGenerator();
hw();	//hw is not a function
hw.next(); //{value: "hello", done: false}
hw.next(); //{value: "world", done: false}
hw.next(); //{value: "ending", done: true}
hw.next(); //{value: "ending", done: true}

*fun()函数不会执行,返回的是一个指向内部状态 的执政对象。

*fun().next()才对。done表示遍历是否结束。


**2 yield表达式 **

  • yeild是暂停标准。惰性求值。

  • Generator函数可不用yield,为单纯的展缓执行函数

function* f(){
    console.log("执行了");
}
var generator = f();
setTimeout(function(){
    generator().next();
},2000);

若f是普通函数,generator就会执行。但f是一个Generator函数,就只有调用next方法时才会执行。

  • yield在不在Generator函数中用,会报错 SyntaxError:

  • yield也可用在表达式中,但要放在圆括号里面

    function* demo(){
        console.log('hello' + (yield 1));
        console.log((yield 123));
        let input = yield;//ok 赋值放在右边 
    }
    

3与Iterator接口的关系

Generator函数是遍历器生成函数,因此可把Generator赋值给对象的Symbol.iterator属性,从而使得该对象具有Iterator接口。

not ending

next方法的参数

next可带一个参数,该参数会被当做上一条yield语句的返回值,默认undefined。所以第一条next的参数默认无效。

想让第一个参数有效的方法:

for...of循环

for...of循环可自动遍历Generator函数生成的Iterator对象,且此时不用调用next方法。(return不会调用 done)

function *fun(){
    yield 1; yield 2; yield 3; yield 4;
    return 5;
}
for(let v of foo()){
    console.log(v);
}
//1 2 3 4

原生的JavaScript对象没有遍历接口,无法使用for...of循环,

可通过Generator函数加上这个接口就能用。

//为obj加上遍历器接口
function*  objectEntries(obj){
	let propKeys = Reflect.ownKeys(obj);
    
	for(let propKey of propKeys){
        yield [propKey,obj[propKey]]
    }
}
var jan = {first:'jane',last:'Doe'}
for(let [key,value] of objectEntries(jan)){
    console.log(`${key}:${value}`);
}
//first:jane
//last:Doe

或将Generator函数加到对象的Symbol.iterator属性上。

function*  objectEntries(){
	let propKeys = Reflect.ownKeys(this);
    
	for(let propKey of propKeys){
        yield [propKey,this[propKey]]
    }
}
var jan = {first:'jane',last:'Doe'};
jan[Symbol.iterator] = objectEntries;
for(let [key,value] of jan){
    console.log(`${key}:${value}`);
}

除for...of循环,拓展运算符、结构赋值和Array.from方法内部调用的都是遍历器接口。这意味着。可将Generator函数返回的Iterator对象作为参数。

Generator.prototype.throw()

throw方法在函数体外抛出错误,然后在Generator函数体内捕获。

Generator.prototype.return()

yield*表达式

如果在Generator函数内部调用另一个Generator函数,默认情况下是无效的。(会直接忽略)。用yield* func()解决

instagram

作为对象属性的Generator函数

Generator函数this

17 Generator函数的异步应用

JavaScript的执行环境是“单线程”的,若没有异步编程,会卡死。

传统方法

ES6诞生之前,异步编程方法有

  • 回调函数
  • 事件监听
  • 发布/订阅
  • Promise对象

基本概念

  • 异步:不连续的执行。同步:连续执行

  • 回调函数:

    程序分成两段,在第一阶段抛出的错误,在其上下文已经无法捕捉了,因此只能当做参数传入第二段

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

    回调地狱:回调函数多层嵌套,只要有一个修改,它的上层和下层回调函数都要跟着修改。(callback hell)

    Promise是一种新的写法,允许将回调函数的嵌套改成链式调用。

    但Promise最大的问题是代码冗余,原来的任务被Promise包装后,无论什么操作,看上去都是许多then的堆积,原来的语义变得和不清楚。有什么更好的写法呢?

Generator函数

1 协程

即多个线程互相协作,完成异步操作。

function *asyncJob(){
    //.........
    var f = yield readFile(fileA);//把执行权交给其他协程
    //...
}

2协程Generator函数实现

Generator函数就是一个封装的异步任务。

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

Generator函数可暂停执行和恢复执行,这是它能封装异步任务的根本原因。还有两个特性:函数体内外的数据交换和错误处理机制。

function* gen(x){
    try{
        var y = yield x+2;
    }catch(e){
        console.log(e);
    }
    return y;
}
var g = gen();
g.next();
g.throw('出错了');//出错了

Generator函数体外使用指针对象的throw方法抛出的错误可以被函数体内的try...catch代码捕获。即出错代码与处理错误的代码实现了时间和空间上的分离。

4异步任务的封装

例子:

确定啊,Generator函数将异步操作表示的很简洁,但流程管理不方便(即何时执行第一阶段、何时执行第二阶段)

Thunk函数

Thunk函数是自动执行Generator函数的一种方法

1 参数的求值策略

编译器:即函数的参数应该在何时求值

f(x+2)

传值调用:在进入函数体之前,先计算出。C语言——会造成性能损失

传名调用:在执行时才计算。Haskell语言。——推荐

2

3

co模块

1基本用法

github.com/tj/co 是在2013/6发布的小工具,用于Generator函数的自动执行。

var gen = function* {
    var f1 = yield readFile('etc/fstab');
    var f2 = yield readFile('/etc/shells');
    console.log(f1.toString());
    console.log(f2.toString());
}
//使用co模块无须编写Generator函数的执行器
var co = require('co');
co(gen);

co函数返回一个Promise对象,

co(gen).then(functino(){
	console.log('Generator函数执行完成')             
})

2co模块的原理

Generator的自动执行需要一种机制,当异步操作有了结果,这种机制要主动交回执行权。

有两种方法可以做到这一点。

  • 回调函数。将异步操作包装成Thunk函数,在回调函数里面交回执行权
  • Promise对象。将异步操作包装成Promise对象,用then方法交回执行权。

使用co的前提条件,Generator函数的yield命令后面只能是Thunk函数或Promise对象,如果数组或对象的成员全是Promise,也可用co。(co v4.0版本后,yield命令后只能是Promise对象,不再支持Thunk函数)

3基于Promise对象的自动执行

4源码

有空 自己写一遍

5 处理并发的异步操作

co支持并发的异步操作,即允许某些操作同时进行,等到他们全部完成才进行下一步。这是要把并发的操作都放在数组或对象里面,跟在yield语句后面。

实例:处理Stream

Node提供Stram模式读写数据,特定是一次只处理数据的一部分,数据被分成一块一块依次处理,就像“数据流”一样。这对处理大规模数据很有利。

Stream模式会用EventEmitter API,会释放三个事件。

  • data事件:下一块数据已经准备好
  • end事件:整个“数据流”处理“完成”
  • err事件:发生错误。

18async函数

含义

ES2017引入async函数。它是Generator函数的语法糖。

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

async函数对Generator函数的改进如下:

  • 内置执行器

    Generator函数的执行必须靠执行器,所以有了co模块,而async函数带执行器。即和普通函数一样。

    asyncReadFild();

  • 更好的语义

    async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果

  • 更广的适用性

    co模块规定,yield后面只能是Thunk函数或Promise对象,而async函数的await命令后面,可以是Promise对象和原始类型的值(数值、字符串和布尔值,但这是等于同步操作)。

  • 返回的是Promise

    async函数的返回值是Promise对象,这比Generator函数的返回值是Iterator对象方便了许多。

即asunc函数是由多个异步操作包装成的一个Promise对象,而await命令就是内部then命令的语法糖。

用法

const foo = async ()=>{}

语法

async函数语法规则总体是教简单,难点是错误处理机制。

返回Promise对象

async函数的实现原理

其他异步处理方法的比较

实例:按顺序完成异步操作

异步遍历器

19 Class

  • 基本语法

类中的this表示实例对象。

在类中写函数时,不需要写function。且方法见不能写逗号,会报错。

ES6中的类完全可看做构造函数中的另一种写法。

类的数据类型就是函数,类本身就指向构造函数。

类的所有方法都定义在类的prototype属性上。

由于类的方法(除constructor以外)都定义在prototype对象上,所以类的新方法可以添加在prototype对象上。Object.assign可以很方便地一次性向类添加多个类方法。

prototype对象的constructor属性直接指向“类”本身。

类的内部定义的所有方法都是不可枚举的。

类的属性名可以采用表达式[]。


严格模式:ES6就是出于严格模式下。

constructor方法:

若类没有显式定义,一个空的constructor方法会默认添加。js引擎会自动添加。

类必须使用new调用,否则报错。


类的实例对象:

与ES5一样,类的所有实例共享一个原型对象。

Class表达式:

不存在变量提升:

这与继承有关,必须保证子类在父类之后定义。


私有方法:

_变量名:不保险,在类的外部依然可以调用

私有属性:

#变量名(提案)


this的指向:

类的方法内部如果含有this,它将默认指向类的实例。但若一旦单独使用该方法,很可能会报错。

解决方案:在constrotor绑定this,

constructor(){

​ this.a = this.printName.bind(this);

​ //或者

​ this.a = (name='there')=>{

​ this.a ()

​ }

}

或用Proxy,在获取方法的时候自动绑定this


name属性:

Class的getter和setter函数

Class的Generator方法

Class的静态方法:

若一个方法前加上static,则该方法不会被实例继承,而是直接通过类调用,称为“静态方法"

Class的静态属性:

静态属性指的是Class本身的属性。即Class.propname,而不是定义在实例对象(this)上的属性

ES6明确规定,Class内部只有静态方法,没有静态属性。

Class的实例属性:

可以用等式写入类的定义中

Class的静态属性:


new.target属性

  • Class的继承

22 Module的语法

  1. 概述

JavaScript一种没有模块(module)体系。

Ruby的require,Python的import,css的@import。

在ES6之前,社区制定了一些模块加载方案,最主要的是CommonJS和AMD。

​ CommonJS用于服务器,AMD用于浏览器。ES6完全可取代他两,成为浏览器和服务器通用的模块解决方案。

​ ES6模块的设计实现是尽量静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS和AMD只能在运行时确定这些东西。比如CommonJS模块就是对象,输入时必须查找对象属性。

​ 除了静态加载带来的好处,ES6模块还有以下好处:

  • 不再需要UMD模块格式,将来服务器和浏览器都会支持ES6模块格式。目前通过各种工具库已经做到这一点。
  • 将来浏览器的新API可以用模块格式提供,不再需要做成全局变量或者navigator对象的属性
  • 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块来提供。
  1. 严格模式

ES66 会自动采用严格模式,不管有没有在模块头部加上“use strict"

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

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

注:严格模式是ES5引入的。

在ES6模块中,顶层this指向undefined,即不应该在顶层代码中使用this

  1. export命令

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果外部能读取模块内部的某个变量,就必须使用export关键字输出该变量。

  • import命令
  • 模块的整体加载
  • export default命令
  • export与inport命令
  • export与import 的复合写法
  • 模块的继承
  • 跨模块常量

23 Module的加载的实现

本章介绍如何在浏览器和Node之中加载ES6模块,以及实际开发中经常遇到的一些问题(比如循环加载)

浏览器加载

  1. 传统方法

在HTML网页中,浏览器通过