阮一峰老师ECMAScript 6看完后的学习总结
globalThis
顶层对象
JavaScript 语言存在一个顶层对象,它提供全局环境(即全局作用域),所有代码都是在这个环境中运行。但是,顶层对象在各种实现里面是不统一的。在浏览器环境指的是
window
对象,在Node
指的是global
对象。ES5
之中,顶层对象的属性与全局变量是等价的。
var a = 2;
window.a // 2
复制代码
ES6
为了改变这一点,一方面规定,为了保持兼容性,var
命令和function
命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let
命令、const
命令、class
命令声明的全局变量,不属于顶层对象的属性。也就是说,从ES6
开始,全局变量将逐步与顶层对象的属性脱钩。
var a = 2;
window.a // 2
let b = 3
window.b //undefined
复制代码
- 浏览器里面,顶层对象是
window
,但Node
和Web Worker
没有window
。 - 浏览器和
Web Worker
里面,self
也指向顶层对象,但是Node
没有self
。 Node
里面,顶层对象是global
,但其他环境都不支持。
ES2020
在语言标准的层面,引入globalThis
作为顶层对象。也就是说,任何环境下,globalThis
都是存在的,都可以从它拿到顶层对象,指向全局环境下的this
。
Destructuring
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构
Array Destructuring
let [a, , b, ...c] = [1, 2, 3, 4, 5]
a // 1
b // 3
c // [4,5]
//error
let [a] = 1 //不具备 Iterator 接口
let [a, b = 2, c = 3] = [1, , undefined]
a //1,
b //2,
c //3
let [x = 1, y = x] = []; // x=1; y=1
let [x = y, y = 1] = []; // ReferenceError: y is not defined
//交换位置
let x = 1, y = 2;
[y, x] = [x, y]
x // 2
y // 1
复制代码
Object Destructuring
Object
的解构与Array
有一个重要的不同。Array的元素是按次序排列的,变量的取值由它的位置决定;而Object
的prop
没有次序,变量必须与prop
同名,才能取到正确的value
。
let { foo, baz } = { foo: 'aaa', bar: 'bbb' };
foo // 'aaa'
baz // undefined
//解构对象方法
let { log, sin, cos } = Math;
const { log } = console;
复制代码
以上说明,对象的解构赋值是下面形式的简写
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p: [x, { y }] } = obj;
p // p is not defined // p是模式,不参与赋值
x // "Hello"
y // "World"
let { p, p: [x, { y }] } = obj;
x // "Hello"
y // "World"
p // ["Hello", {y: "World"}]
复制代码
Tip
:对象的解构赋值可以取到继承的属性。
const obj1 = {};
const obj2 = { foo: 'bar' };
Object.setPrototypeOf(obj1, obj2);
const { foo } = obj1;
foo // "bar"
复制代码
设置默认值时,默认值生效的条件是,对象的属性值严格等于
undefined
。
var { message: msg = 'Something went wrong', say } = { say() { console.log(`say`) } };
msg // Something went wrong
say // function say(){}
复制代码
Symbol
表示独一无二的值。它是
JavaScript
语言的第七种数据类型.前六种是:undefined
、null
、布尔值(Boolean)
、字符串(String)
、数值(Number)
、对象(Object)
let sy = Symbol('a')
let sy1 = Symbol('a')
sy === sy1 // false
sy.toString() === sy1.toString() // true
// ES2019 提供 description
sy.description === sy1.description // true
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
s1 === s2 // true
复制代码
Symbol.for()
与Symbol()
这两种写法,都会生成新的Symbol
。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()
不会每次调用就返回一个新的Symbol
类型的值,而是会先检查给定的key
是否已经存在,如果不存在才会新建一个值。比如,如果你调用Symbol.for("foo")
30 次,每次都会返回同一个Symbol
值,但是调用Symbol("foo")
30 次,会返回 30 个不同的Symbol
值。
Symbol.iterator
对象的Symbol.iterator属性,指向该对象的默认遍历器方法。这个着重记了一下
const myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3] // ...的底层是for...of
复制代码
对象进行
for...of
循环时,会调用Symbol.iterator
方法,返回该对象的默认遍历器,
class Collection {
*[Symbol.iterator]() {
let i = 0;
while(this[i] !== undefined) {
yield this[i];
++i;
}
}
}
let myCollection = new Collection();
myCollection[0] = 1;
myCollection[1] = 2;
for(let value of myCollection) {
console.log(value);
}
// 1
// 2
复制代码
Symbol.toStringTag
对象的
Symbol.toStringTag
属性,指向一个方法。在该对象上面调用Object.prototype.toString
方法时,如果这个属性存在,它的返回值会出现在toString
方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[object Object]
或[object Array]
中object
后面的那个字符串。
({[Symbol.toStringTag]: 'Foo'}.toString())
// "[object Foo]"
class Collection {
get [Symbol.toStringTag]() {
return 'xxx';
}
}
let x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"
复制代码
Set
&& Map
ES6
提供了新的数据结构Set
,Map
Set
成员的值都是唯一的,没有重复的值,Set内的元素是强类型,会进行类型检查。
let set = new Set([1, true, '1', 'true', 1])
// Set(4) {1, true, "1", "true"}
复制代码
Set
处理并集(Union)
、交集(Intersect)
和差集(Difference)
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);
// 并集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}
// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}
// (a 相对于 b 的)差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}
复制代码
Map
类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
const map = new Map([
['name', '张三'],
['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"
复制代码
数据间的转换
Map To Array
const myMap = new Map()
.set(true, 7)
.set({foo: 3}, ['abc']);
[...myMap]
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
复制代码
Array To Map
new Map([
[true, 7],
[{foo: 3}, ['abc']]
])
// Map {
// true => 7,
// Object {foo: 3} => ['abc']
// }
复制代码
Map To Object
function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [k,v] of strMap) {
obj[k] = v;
}
return obj;
}
const myMap = new Map()
.set('yes', true)
.set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }
复制代码
Object To Map
let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj));
复制代码
Map To JSON
function strMapToJson(strMap) {
return JSON.stringify(strMapToObj(strMap));
}
let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'
复制代码
JSON To Map
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}
复制代码
WeakSet
&&WeakMap
所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakSet
&&WeakMap
里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
Proxy
概述
Proxy
可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy
这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
而Object.defineProperty
是使用的数据劫持:直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。数据劫持最典型的应用 -----> 双向的数据绑定(一个常用的面试题),
Vue 2.x
利用Object.defineProperty()
,并且把内部解耦为Observer
,Dep
, 并使用Watcher
相连Vue
在3.x
版本之后改用Proxy
进行实现
Proxy
与Object.defineProperty
的对比
Object.defineProperty
- 只能监听对象(
Object
),不能监听数组的变化,无法触发push
,pop
,shift
,unshift
,splice
,sort
,reverse
。 - 必须遍历对象的每个属性
- 只能劫持当前对象属性,如果想深度劫持,必须深层遍历嵌套的对象
"use strict" let obj = {}; let value = 1 Object.defineProperty(obj, 'listenA', { writable: true, //可修改 enumerable: true, // 可枚举 for...in... Object.keys() configurable: true, // 可配置,可删除 get: () => value, set: val => { console.log(`set obj.listenA .. ${val}`); value = val }, }); obj.listenA = 2 //set obj.listenA .. 2 console.log(obj.listenA) // 2 复制代码
- 只能监听对象(
Proxy
- 可以直接监听对象而非属性
- 可以直接监听数组的变化
// 代理整个对象 let proxyObj = new Proxy({}, { get: (target, key, receiver) => { console.log(`getting ${key}!`); return target[key]; }, set: (target, key, value, receiver) => { console.log(target, key, value, receiver); return target[key] = value; }, }); proxyObj.val = 1; // {} val 1 {} proxyObj.val; // getting val! //代理数组 let proxyArr = new Proxy([], { get: (target, key, receiver) => { console.log(`getting ${key}!`); return target[key]; }, set: (target, key, value, receiver) => { console.log(target, key, value, receiver); return (target[key] = value); }, }); proxyArr[0] = 1; // {} val 1 {} console.log(proxyArr[0]); // getting val! // 1 console.log(proxyArr); // [1] 复制代码
Reflect
Reflect
翻译过来是反射的意思,与Proxy
对象一样,也是ES6
为了操作对象而提供的新API
。有一下几个作用
- 将
Object
对象的一些明显属于语言内部的方法(比如Object.defineProperty
),放到Reflect
对象上。现阶段,某些方法同时在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)) { // 成功返回true
// success
} else {
// failure
}
复制代码
- 让
Object
操作都变成函数行为。某些Object
操作是命令式,比如name in obj
和delete obj[name]
,而Reflect.has(obj, name)
和Reflect.deleteProperty(obj, name)
让它们变成了函数行为。
// 老写法
'assign' in Object // true
// 新写法
Reflect.has(Object, 'assign') // true
复制代码
Reflect
对象的方法与Proxy
对象的方法一一对应,只要是Proxy
对象的方法,就能在Reflect
对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect
方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy
怎么修改默认行为,你总可以在Reflect
上获取默认行为。
Proxy(target, {
set: function(target, name, value, receiver) {
var success = Reflect.set(target, name, value, receiver);
if (success) {
console.log('property ' + name + ' on ' + target + ' set to ' + value);
}
return success;
}
});
复制代码
async function
ES2017
标准引入了async
函数,使得异步操作变得更加方便,由于async函数
返回的是Promise
对象,可以作为await
命令的参数。
async function timeout(ms) {
await new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
}
asyncPrint('hello world', 50);
复制代码
返回 Promise
对象
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
复制代码
async
函数内部抛出错误,会导致返回的Promise
对象变为reject
状态。抛出的错误对象会被catch
方法回调函数接收到
async function f() {
throw new Error('出错了');
}
f().then(
v => console.log('resolve', v),
e => console.log('reject', e)
)
//reject Error: 出错了
复制代码
Promise
对象的状态变化
async
函数返回的Promise
对象,必须等到内部所有await
命令后面的Promise
对象执行完,才会发生状态改变,除非遇到return
语句或者抛出错误。也就是说,只有async
函数内部的异步操作执行完,才会执行then
方法指定的回调函数。
async function getTitle(url) {
let response = await fetch(url);
let html = await response.text();
return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('http://localhost:8080/').then(console.log(123))
复制代码
await
命令
正常情况下,
await
命令后面是一个Promise
对象,返回该对象的结果。如果不是Promise
对象,就直接返回对应的值。
async function f() {
// 等同于
// return 123;
return await 123;
}
f().then(v => console.log(v))
// 123
复制代码
任何一个
await
语句后面的Promise
对象变为reject
状态,那么整个async
函数都会中断执行。
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行
}
复制代码
如果希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个
await
放在try...catch
结构里面,这样不管这个异步操作是否成功,第二个await
都会执行。
async function f() {
try {
await Promise.reject('出错了');
} catch(e) {
}
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// hello world
复制代码
async
函数的实现原理
将 Generator 函数和自动执行器,包装在一个函数里
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
复制代码
严格模式
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上
"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
)
export
&& import
用
export
&&import
来进行模块的导出导入。
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
// 报错
function f() {}
export f;
// 正确
export function f() {};
// 正确
function f() {}
export {f};
import 'lodash';
import 'lodash'; //加载了两次lodash,但是只会执行一次。
export { foo, bar } from 'my_module'; //当前模块不能直接使用foo和bar
// 接口改名
export { foo as myFoo } from 'my_module';
// 整体输出
export * from 'my_module';
//默认接口
export { default } from 'foo';
export { default as es6 } from './someModule';
作者:前端Up主