not ending 可能错误
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 函数的拓展
函数参数的默认值
- 基本用法
在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
- 与解构赋值默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。
若目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性
- 注意点
- 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
即复杂数据类型会变。
- 对于嵌套的对象,遇到同名属性,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'}}
- Object.assign在处理数组时,会把数组当做对象处理
Object.assign([1,2,3],[4,5])
//[4,5,3]
Object.assign把数组视为属性名为0,1,2的对象
- 常见用途
- 为对象添加属性
class Point {
constructor(x,y){
Object.assign(this,(x,y))
}
}
通过assign将xy属性添加到Point类的对象的实例中
- 为对象添加方法
Object.assign(SomeClass.prototype,{
someMethod(arg1,arg2){
//...
},
antherMethod(){
//...
}
});
//等同于下面的写法
SomeClass.prototype.someMethod = function(arg1,arg2){
//...
};
SomeClass.prototype.someMethod = function(arg1,arg2){
//...
};
- 克隆对象
function clone(origin){
return Object.assign({},origin);
}
但这种方法只能克隆原始对象自身的值,不能克隆继承的值。
若需要保持继承链
function clone(origin){
let originProto = Object.getPrototypeOf(origin);
return Object.assign(Object.create(originProto),origin);
}
- 合并多个对象
将对各对象合并到某个对象
const merge = (target,...sources) => Object.assign(target,...sources);
若希望合并返回一个新对象,可改写,对一个空对象合并
const merge = (...sources) => Object.assign({},...sources);
例子使用不成功???
- 为属性指定默认值
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有两个区别
- WeabSet的成员只能是对象,而不能是其他类型的值
- 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()解决
作为对象属性的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的语法
- 概述
JavaScript一种没有模块(module)体系。
Ruby的require,Python的import,css的@import。
在ES6之前,社区制定了一些模块加载方案,最主要的是CommonJS和AMD。
CommonJS用于服务器,AMD用于浏览器。ES6完全可取代他两,成为浏览器和服务器通用的模块解决方案。
ES6模块的设计实现是尽量静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS和AMD只能在运行时确定这些东西。比如CommonJS模块就是对象,输入时必须查找对象属性。
除了静态加载带来的好处,ES6模块还有以下好处:
- 不再需要UMD模块格式,将来服务器和浏览器都会支持ES6模块格式。目前通过各种工具库已经做到这一点。
- 将来浏览器的新API可以用模块格式提供,不再需要做成全局变量或者navigator对象的属性
- 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块来提供。
- 严格模式
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
- export命令
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果外部能读取模块内部的某个变量,就必须使用export关键字输出该变量。
- import命令
- 模块的整体加载
- export default命令
- export与inport命令
- export与import 的复合写法
- 模块的继承
- 跨模块常量
23 Module的加载的实现
本章介绍如何在浏览器和Node之中加载ES6模块,以及实际开发中经常遇到的一些问题(比如循环加载)
浏览器加载
- 传统方法
在HTML网页中,浏览器通过