推荐前端开发学习javaScript网站
https://zh.javascript.info/object-basics?map
根据阮一峰阮一峰ES6学习整理
上面学的差不多了,可以看看引擎,对于底层优化有很大帮助
a compiler engineer that loves giving talks.
A tour of V8: Garbage Collection
目录
https://zh.javascript.info/object-basics?map
let、const、class命令声明的全局变量,不属于顶层对象的属性。
7、for..of entries() keys() values()
3、Object.keys(),Object.values(),Object.entries()
(二)Object.getPrototypeOf() 从子类上获取父类
一、let、const
(一)let
匿名立即执行函数 块级作用域 IIFE
{
//TODO
}
ES6的块级作用域必须有大括号{}
1、不存在变量提升 暂时性死区
先定义再使用
{
alert(a);//TDZ
let a = 5;
}
2、同一作用域里,不能重复定义变量
{
{
let a = 1;
}
let a = 2;
//a = 2
}
(二)const
1、只读常量,一旦声明,常量的值就不能改变
2、必须在定义的时候就赋值,才能使用,不能后赋值
3、只在声明所在的块级作用域内有效。
const arr=['apple','orange'];
// arr = ['banana']//Assignment to constant variable.
arr.push('banana')
console.log(arr);//["apple", "orange", "banana"]
let、const、class命令声明的全局变量,不属于顶层对象的属性。
var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1
let b = 1;
window.b // undefined
二、解构赋值
(一)结构两边格式要保持一致
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p, p: [x, { y }] } = obj;
x // "Hello"
y // "World"
p // ["Hello", {y: "World"}]
(二)变量名与属性名不一致时
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'
(三)指定默认值
var {x, y = 5} = {x: 1};
x // 1
y // 5
默认值生效的条件是,对象的属性值严格等于undefined
。
var {x = 3} = {x: undefined};
x // 3
var {x = 3} = {x: null};
x // null
// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error
因为 JavaScript 引擎会将{x}
理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。
// 正确的写法
let x;
({x} = {x: 1});
(四)函数参数结构赋值
function move({x = 0, y = 0} = {}) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
(五)用途
(1)交换变量的值
let x = 1;
let y = 2;
[x, y] = [y, x];
上面代码交换变量x
和y
的值,这样的写法不仅简洁,而且易读,语义非常清晰。
(2)从函数返回多个值
函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。
// 返回一个数组
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();
// 返回一个对象
function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();
(3)函数参数的定义
解构赋值可以方便地将一组参数与变量名对应起来。
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
(4)提取 JSON 数据
解构赋值对提取 JSON 对象中的数据,尤其有用。
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
console.log(id, status, number);
// 42, "OK", [867, 5309]
上面代码可以快速提取 JSON 数据的值。
(5)函数参数的默认值
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
} = {}) {
// ... do stuff
};
指定参数的默认值,就避免了在函数体内部再写var foo = config.foo || 'default foo';
这样的语句。
(6)遍历 Map 结构
任何部署了 Iterator 接口的对象,都可以用for...of
循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。
const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
for (let [key, value] of map) {
console.log(key + " is " + value);
}
// first is hello
// second is world
如果只想获取键名,或者只想获取键值,可以写成下面这样。
// 获取键名
for (let [key] of map) {
// ...
}
// 获取键值
for (let [,value] of map) {
// ...
}
(7)输入模块的指定方法
加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。
const { SourceMapConsumer, SourceNode } = require("source-map");
三、字符串
(一)字符串模板
`${变量名}`
<ul>
</ul>
<script>
let data = [
{title: 'haha',num: 100},
{title: 'lala',num: 90},
{title: 'dada',num: 80}
]
window.onload = function(){
let oUl = document.querySelector('ul');
for(let i = 0 ; i < data.length; i++){
var oLi = document.createElement('li');
oLi.innerHTML = `<span>${data[i].title}</span>
<span>${data[i].num}</span>
<a href="">详情</a>`;
oUl.appendChild(oLi);
}
}
(二)查找字符串
str.indexOf('a') 找到返回索引,没找到返回-1
str.includes('a') 返回true/false
判断浏览器
if(navigator.userAgent.includes('Chrome')){
alert(true)
}
字符串以谁开头
str.startsWith(检测的东西) (url)
以谁结尾
str.endsWith(检测的东西)
重复字符串
str.repeat(重复次数)
填充字符串
向前填充
str.padStr(str.length+padStr.length,填充的东西)
向后填充
str.padStr(str.length+padStr.length,填充的东西)
四、函数
(一)函数参数可以是默认参数
函数参数默认已经定义,不能再使用let或const声明
function show(a = 10){
let a = 100;
console.log(a);
}
show();//Identifier 'a' has already been declared
(二)扩展运算符,reset
...numbers
展开
let arr = [1,2,3,4];
console.log(...arr);//1 2 3 4
重置
function f(...a){
console.log(a);// [1, 2, 3, 4]
}
f(1,2,3,4)
剩余运算符
必须放在参数最后
function f(a,b,...c){
console.log(a,b,c);//1 2 [3, 4, 5]
}
f(1,2,3,4,5)
复制数组
let arr = [1,2,3];
let arr2 = [...arr];
(三)箭头函数
() => {}
let f = v => v;
let f = () => 5;
let sum = (sum1, sum2) => sum1 + sum2;
let sum = (num1, num2) => { return num1 + num2; }
let getTempItem = id => ({id: id, nam2: "temp"});
[1,2,3].map(x => x*x);
const numbers = (...nums) => nums;
numbers(1,2,3,4)
this问题
函数体内的this
对象,就是定义时所在的对象,而不是使用时所在的对象
this对象的指向是可变的,但是在箭头函数中,他是固定的
要维护一个 this 上下文的时候,就可以使用箭头函数。
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
var id = 21;
foo.call({ id: 42 });
// id: 42
箭头函数使this从动态变成静态
第一个场合是定义对象的方法,且该方法内部包括this
。
const cat = {
lives: 9,
jumps: () => {
this.lives--;
}
}
上面代码中,cat.jumps()
方法是一个箭头函数,这是错误的。调用cat.jumps()
时,如果是普通函数,该方法内部的this
指向cat
;如果写成上面那样的箭头函数,使得this
指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致jumps
箭头函数定义时的作用域就是全局作用域。
第二个场合是需要动态this
的时候,也不应使用箭头函数。
var button = document.getElementById('press');
button.addEventListener('click', () => {
this.classList.toggle('on');
});
上面代码运行时,点击按钮会报错,因为button
的监听函数是一个箭头函数,导致里面的this
就是全局对象。如果改成普通函数,this
就会动态指向被点击的按钮对象。
使用注意点
箭头函数有几个使用注意点。
(1)函数体内的this
对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new
命令,否则会抛出一个错误。
(3)不可以使用arguments
对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield
命令,因此箭头函数不能用作 Generator 函数。
五、数组
(一)循环
for foreach(item,index,arr){} map(item,index,arr){ return } filter(item,index,arr){ return true }
arr.som() 数组里只要有一个元素符合,就返回true
arr.every() 数组里所有的元素豆芽符合,才返回true
arr.reduce((prev,cur,index,arr)=>{return prev + cur}) 数组求和
map
正常情况下,需要配合return ,返回一个新数组
重新整理数据结构
let arr = [
{title: 'aaa',read:100,hot: true},
{title: 'bbb',read:100,hot: true},
{title: 'bbb',read:100,hot: true}
];
let newArr = arr.map((item,index,arr) => {
let json = {};
json.t = `@${item.t}`;
json.r = item.read + 200;
json.h = item.hot == true && 'haha';
return json;
})
console.log(newArr);
// Array(3)
// 0: {t: "@undefined", r: 300, h: "haha"}
// 1: {t: "@undefined", r: 300, h: "haha"}
// 2: {t: "@undefined", r: 300, h: "haha"}
filter
如果返回条件为true,留下来
let arr = [
{title: 'aaa',read:100,hot: true},
{title: 'bbb',read:100,hot: false},
{title: 'bbb',read:100,hot: true}
];
let newArr = arr.filter((item,index,arr) => {
return item.hot == false;
})
console.log(newArr);
// 0: {title: "bbb", read: 100, hot: false}
(二)es6 数组操作
1、扩展运算符
复制数组
const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;
合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]
2、Array.form()
将类数组转换成真正的数组
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
// ES6的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
3、Array.of()
将一组值转换为数组
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
4、find() findIndex()
返回找到的第一个符合条件的成员
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10
findIndex
方法的用法与find
方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1
。
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2
function f(v){
return v > this.age;
}
let person = {name: 'John', age: 20};
[10, 12, 26, 15].find(f, person); // 26
上面的代码中,find
函数接收了第二个参数person
对象,回调函数中的this
对象指向person
对象。
5、fill()
填充数组
6、includes()
返回boolean值
[1, 2, 3].includes(2) // true
[1, 2, 3].includes(4) // false
[1, 2, NaN].includes(NaN) // true
7、for..of entries() keys() values()
可以用for...of
循环进行遍历,唯一的区别是keys()
是对键名的遍历、values()
是对键值的遍历,entries()
是对键值对的遍历。
for (let index of ['a', 'b'].keys()) {
console.log(index);
}
// 0
// 1
for (let elem of ['a', 'b'].values()) {
console.log(elem);
}
// 'a'
// 'b'
for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"
// 1 "b"
六、对象
(一)简洁写法
(二)Symbol 新增的对象类型
防止属性名的冲突
let s = Symbol();
console.log(typeof s);// "symbol"
Symbol函数前不能使用new命令,是因为生成的 Symbol 是一个原始类型的值,不是对象,不能添加属性
七个数据类型: string number object boolean undefined function symbol
1、描述
const sym = Symbol('foo');
String(sym) // "Symbol(foo)"
sym.toString() // "Symbol(foo)"
sym.description // "foo"
2、作为属性名的Symbol
let mySymbol = Symbol();
// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
let a = {
[mySymbol]: 'Hello!'
};
// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上写法都得到同样结果
a[mySymbol] // "Hello!"
上面代码通过方括号结构和Object.defineProperty
,将对象的属性名指定为一个 Symbol 值。
注意,Symbol 值作为对象属性名时,不能用点运算符。
const mySymbol = Symbol();
const a = {};
a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"
上面代码中,因为点运算符后面总是字符串,所以不会读取mySymbol
作为标识名所指代的那个值,导致a
的属性名实际上是一个字符串,而不是一个 Symbol 值。
同理,在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。
let s = Symbol();
let obj = {
[s]: function (arg) { ... }
};
obj[s](123);
上面代码中,如果s
不放在方括号中,该属性的键名就是字符串s
,而不是s
所代表的那个 Symbol 值。
3、Symbol.for
Symbol.for("bar") === Symbol.for("bar")
// true
Symbol("bar") === Symbol("bar")
// false
上面代码中,由于Symbol()
写法没有登记机制,所以每次调用都会返回一个不同的值。
Symbol.keyFor()
方法返回一个已登记的 Symbol 类型值的key
。
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
上面代码中,变量s2
属于未登记的 Symbol 值,所以返回undefined
。
Symbol 作为key,用for in循环,循环不出来
let sym = Symbol('aaaaa');
let json = {
a: 'aa',
b: 'bb',
[sym]: 'sss'
}
for (const key in json) {
console.log(key)// a b
}
(三)Super
super
,指向当前对象的原型对象。
const proto = {
foo: 'hello'
};
const obj = {
foo: 'world',
find() {
return super.foo;
}
};
Object.setPrototypeOf(obj, proto);
obj.find() // "hello"
上面代码中,对象obj.find()
方法之中,通过super.foo
引用了原型对象proto
的foo
属性。
注意,super
关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。
(四)新增的方法
1、Onbject.is()
用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
2、Object.assign()
用来合并对象
用途:赋值对象,合并参数
obj._proto_ = aomoOtherObj;
Es6: var obj = Object.create(someOtherObj);(生成操作)
3、Object.keys(),Object.values(),Object.entries()
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]
七、Set和WeakSet
(一)Set
类似于数组,成员的值都是唯一的,有序的
可以用作去除重复的数组
// 去除数组的重复成员
[...new Set(array)]
上面的方法也可以用于,去除字符串里面的重复字符。
[...new Set('ababbc')].join('')
// "abc"
Set.prototype.add(value)
:添加某个值,返回 Set 结构本身。Set.prototype.delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。Set.prototype.has(value)
:返回一个布尔值,表示该值是否为Set
的成员。Set.prototype.clear()
:清除所有成员,没有返回值。
s.add(1).add(2).add(2);
// 注意2被加入了两次
s.size // 2
s.has(1) // true
s.has(2) // true
s.has(3) // false
s.delete(2);
s.has(2) // false
提供了去除数组重复成员的另一种方法。
function dedupe(array) {
return Array.from(new Set(array));
}
dedupe([1, 1, 2, 3]) // [1, 2, 3]
Set.prototype.keys()
:返回键名的遍历器Set.prototype.values()
:返回键值的遍历器Set.prototype.entries()
:返回键值对的遍历器Set.prototype.forEach()
:使用回调函数遍历每个成员-
keys
方法和values
方法的行为完全一致。 -
let set = new Set(['red', 'green', 'blue']); for (let item of set.keys()) { console.log(item); } // red // green // blue for (let item of set.values()) { console.log(item); } // red // green // blue for (let item of set.entries()) { console.log(item); } // ["red", "red"] // ["green", "green"] // ["blue", "blue"]
省略
values
方法,直接用for...of
循环遍历 Set。 -
let set = new Set(['red', 'green', 'blue']); for (let x of set) { console.log(x); } // red // green // blue
forEach()
let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ' : ' + value))
// 1 : 1
// 4 : 4
// 9 : 9
去除数组的重复元素
let arr = [1,1,2,3];
let arr2 = [...new Set(arr)]
console.log(arr2);//[1,2,3]
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}
// 差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}
(二)WeakSet
WeakSet 的成员只能是对象,而不能是其他类型的值。 没有size
const ws = new WeakSet();
ws.add(1)
// TypeError: Invalid value used in weak set
ws.add(Symbol())
// TypeError: invalid value used in weak set
WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
// const b = [1,2];
// const w = new WeakSet(b);
// console.log(w);//: Invalid value used in weak set
const b = [[1,2]];
const w = new WeakSet(b);
console.log(w);//:WeakSet {Array(2)}[[Entries]]0: Array(2)value: (2) [1, 2]__proto__: WeakSet
八、Map 和 WeakMap
(一)Map
键值对,键可以任意,“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
遍历:
const map = new Map([
['F', 'no'],
['T', 'yes'],
]);
for (let key of map.keys()) {
console.log(key);
}
// "F"
// "T"
for (let value of map.values()) {
console.log(value);
}
// "no"
// "yes"
for (let item of map.entries()) {
console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"
// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
// 等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
forEach
map.forEach(function(value, key, map) {
console.log("Key: %s, Value: %s", key, value);
})
(二)WeakMap
WeakMap
只接受对象作为键名(null
除外),不接受其他类型的值作为键名,WeakMap
的键名所指向的对象,不计入垃圾回收机制。
没有遍历操作(keys,values,entries),没有size属性,不支持clear
常见用途
DOM节点作为键名
let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();
myWeakmap.set(myElement, {timesClicked: 0});
myElement.addEventListener('click', function() {
let logoData = myWeakmap.get(myElement);
logoData.timesClicked++;
}, false);
myElement
是一个 DOM 节点,每当发生click
事件,就更新一下状态。我们将这个状态作为键值放在 WeakMap 里,对应的键名就是myElement
。一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险。
九、Promise
异步编程的解决方案
(一)语法
et a = 2;
let promise = new Promise(function(resolve,reject){
if( a == 1){
resolve('success');
}else{
reject('error');
}
})
promise.then(res=> {
console.log(res);
},err => {
console.log(err);
})
模拟用户登录获取用户信息
let status = 1;
let userLogin = ( resolve, reject) => {
setTimeout(() => {
if(status == 1){
resolve({data: 'aa', msg:'success', token: 'aaaaaaa'});
}else{
reject('fail');
}
},2000)
}
let getUserInfo = ( resolve, reject) => {
setTimeout(() => {
if(status == 1){
resolve({data: 'bb', msg:'success', token: 'bbbbbb'});
}else{
reject('fail');
}
},3000)
}
new Promise(userLogin).then(res => {
console.log('success');
// console.log(res);
return new Promise(getUserInfo);
}).then(res => {
console.log('get info');
console.log(res)
})
(二)catch
一般来说,不要在then
方法里面定义 Reject 状态的回调函数(即then
的第二个参数),总是使用catch
方法。
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});
// good
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});
上面代码中,第二种写法要好于第一种写法,理由是第二种写法可以捕获前面then
方法执行中的错误,也更接近同步的写法(try/catch
)。因此,建议总是使用catch
方法,而不使用then
方法的第二个参数。
一般总是建议,Promise 对象后面要跟catch
方法,这样可以处理 Promise 内部发生的错误。catch
方法返回的还是一个 Promise 对象,因此后面还可以接着调用then
方法。
(三)Promise.all
Promise.all()
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
(1)只有p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数。
(2)只要p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p
的回调函数。
十、Generator函数
解决异步编程,遍历器(Iterator)对象生成函数。
(一)特征
一是,function
关键字与函数名之间有一个星号;二是,函数体内部使用yield
表达式,定义不同的内部状态(yield
在英语里的意思就是“产出”)。
function* gen(){
yield 'yellow';
yield 'green';
return 'color';
}
let g = gen();
console.log(g.next());//{value: "yellow", done: false}
console.log(g.next());//{value: "green", done: false}
console.log(g.next());//{value: "color", done: true}
console.log(g.next());//{value: undefined, done: true}
for ... of
for(let val of g){
console.log(val);//yellow green
}
next()
、throw()
、return()
这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield
表达式。
(二)yield* 表达式
任何数据结构只要有 Iterator 接口,就可以被yield*
遍历。
let read = (function* () {
yield 'hello';
yield* 'hello';
})();
read.next().value // "hello"
read.next().value // "h"
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"
function* inner() {
yield 'hello!';
}
function* outer1() {
yield 'open';
yield inner();
yield 'close';
}
var gen = outer1()
gen.next().value // "open"
gen.next().value // 返回一个遍历器对象
gen.next().value // "close"
function* outer2() {
yield 'open'
yield* inner()
yield 'close'
}
var gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"
上面例子中,outer2使用了yield*,outer1没使用。结果就是,outer1返回一个遍历器对象,outer2返回该遍历器对象的内部值。
在有return
语句时,则需要用var value = yield* iterator
的形式获取return
语句的值
(三)作为对象属性的Generator函数
如果一个对象的属性是 Generator 函数,可以简写成下面的形式。
let obj = {
* myGeneratorMethod() {
···
}
};
它的完整形式如下,与上面的写法是等价的。
let obj = {
myGeneratorMethod: function* () {
// ···
}
};
(四)应用
Generator 可以暂停函数执行,返回任意表达式的值。这种特点使得 Generator 有多种应用场景。
异步编程
“协程”: 多个线程相互合作,完成异步任务
- 第一步,协程
A
开始执行。 - 第二步,协程
A
执行到一半,进入暂停,执行权转移到协程B
。 - 第三步,(一段时间后)协程
B
交还执行权。 - 第四步,协程
A
恢复执行。
function* asyncJob() {
// ...其他代码
var f = yield readFile(fileA);
// ...其他代码
}
上面代码的函数asyncJob
是一个协程,它的奥妙就在其中的yield
命令。它表示执行到此处,执行权将交给其他协程。也就是说,yield
命令是异步两个阶段的分界线。
协程遇到yield
命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield
命令,简直一模一样。
步操作需要暂停的地方,都用yield
语句注明
十一、Class
(一)定义
通过class关键字,定义类。
- 定义“类”方法的时候,前面不需要加上function这个关键字。直接把函数定义放进去就可以, 方法间不需要逗号分隔。
class Point{
constructor(x,y) {
this.x = x;
this.y = y;
}
toString(){
return `(${this.x},${this.y})`;
}
}
var point = new Point(1,2);
console.log(point.toString());//(1,2)
console.log(typeof Point);//function
console.log(typeof point)//object
console.log(Point == Point.prototype.constructor);//true
类的数据类型就是函数,类本身就指向构造函数
- 在类的实例上面调用方法,其实就是调用原型上的方法。
console.log(point.toString == Point.prototype.toString);//true
- 由于类的方法都定义在
prototype
对象上面,所以类的新方法可以添加在prototype
对象上面。Object.assign
方法可以很方便地一次向类添加多个方法。
Object.assign(Bar.prototype, {
toString(){
console.log("toString")
},
toValue(){}
})
b.toString();//toString
- 类的内部所有定义的方法,都是不可枚举的(non-enumerable)
即可以通过for..in 遍历,不可以通过object.keys()遍历
通过Object.getOwnPropertyNames()获取圆形属性的方法名
class Point {
constructor(x, y) {
// ...
}
toString() {
// ...
}
}
Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
什么是枚举属性呢?
- 枚举属性是可以通过直接赋值,和属性初始化的属性。标识符是true。
- 可以通过Object.defineProperty等定义的属性。标识符是false。
- 可枚举属性可以通过for ... in循环遍历(除非该属性名是一个Symbol)。
- 详细解释见对象的属性和属性
(二)constructor方法
类的默认方法,通过new声称对象,如果没有显式定义,一个空的constructor
方法会被默认添加。
类必须通过new调用
(三)类的实例
实例的属性除非显式定义在其本身(即定义在this
对象上),否则都是定义在原型上(即定义在class
上)。
生产环境中,我们可以使用 Object.getPrototypeOf
方法来获取实例对象的原型,然后再来为原型添加方法/属性。
class Point {
constructor(x, y){
this.x = x;
this.y = y;
}
toString() {
}
}
var p1 = new Point(1,2);
var p2 = new Point(2,3);
console.log(p1._proto_ == p2._proto_); // true
let proto = Object.getPrototypeOf(p1);
console.log(proto.constructor.name)// Point
proto.printName = function() {
return 'oops'
};
console.log(p1.printName());// oops
console.log(p2.printName());// oops
var p3 = new Point(3,4);
console.log(p3.printName())//oops
上面代码在p1
的原型上添加了一个printName
方法,由于p1
的原型就是p2
的原型,因此p2
也可以调用这个方法。
不推荐使用实例的_proto_ 属性改写原型。会改变类的原始定义。
(四)取值函数(getter) 存值函数(setter)
class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
let inst = new MyClass();
inst.prop = 123;
// setter: 123
inst.prop
// 'getter'
(五)属性表达式
let methodName = 'getArea';
class Square {
[methodName]() {
}
}
console.log(Object.getOwnPropertyNames(Square.prototype));//["constructor", "getArea"]
Square类的方法名时getArea
(六)Class表达式
内部可以用类的名字调用,但是在外部,只能使用类实例化对象调用。
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
可以写出立即执行函数 ,person 是一个立即执行的类的实例
let person = new class{
constructor(name) {
this.name = name;
console.log(this.name);//张三
console.log(name);//张三
}
sayName() {
console.log(this.name);//张三
}
}("张三")
console.log( person.sayName());//undefined
(七)注意点
1、类和模块内部默认就是严格模式,不需要使用use strict.
2、不存在变量提升,与继承有关。
{
let Foo = class{};
class Bar extends Foo {
}
}
3、ES6 的类只是 ES5 的构造函数的一层包装,所以函数的许多特性都被Class
继承,包括name
属性。
console.log(Foo.name);//Foo
console.log(Bar.name);//Bar
4、Generator方法
方法前加*号
class Foo {
constructor(...args) {
this.args = args;
}
* [Symbol.iterator]() {
for (const arg of this.args) {
yield arg;
}
}
}
for (const x of new Foo('hello', 'world')) {
console.log(x);
}
5、this指向
如果将方法单独提出来,方法中的this指向会指向该方法运行时所在的环境,实际指向的是undefined。
class Logger {
constructor() {
this.getThis = () => this; //true
this.printName = this.printName.bind(this);//Hello three
}
printName(name = 'three') {
this.print(`Hello ${ name }`);
}
print(text) {
console.log(text);
}
}
const logger = new Logger();
const { printName} = logger;
console.log(logger.getThis() === logger);//true
printName();//Uncaught TypeError: Cannot read property 'print' of undefined
解决办法:
在构造方法中绑定this,会找到print方法
使用箭头函数。this指向定义是坐在的对象。this会总是指向实例对象。
(八)静态方法
- 类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上
static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
class Foo {
static classMethod() {
return 'hello';
}
}
console.log(Foo.classMethod());//hello
var foo = new Foo();
console.log(foo.classMethod())//foo.classMethod is not a function
- 静态方法中的this指的是类,而不是实例
class Foo {
static classMethod() {
this.baz();
}
static baz() {
console.log('static baz');
}
baz() {
console.log('baz');
}
}
Foo.classMethod();//static baz
等同于调用Foo.baz();
- 父类的静态方法,可以被子类继承
class Bar extends Foo {}
Bar.classMethod();//static baz
(九)静态属性
Class本身的属性。Class.propName。
class Foo {}
Foo.prop = "haha";
console.log(Foo.prop);//haha
在类Foo上定义了静态属性prop。
新提案,累的静态属性写在实力属性的前面,加上static关键字。
class Foo {
static prop = "haha";
}
console.log(Foo.prop);//haha
(十)实例属性
实例属性指的是在构造函数方法中定义的属性,属性和方法都是不一样的引用地址
写法:
- 构造函数中
class IncreasingCounter {
constructor() {
this._count = 0;
}
}
- 定义在类的最顶层(建议)
foo类有两个实例属性,一目了然。
class foo {
bar = 'hello';
baz = 'world';
constructor() {
// ...
}
}
(十一)私有方法和私有属性
只能在类的内部访问的方法和属性,外部不能访问。有利于代码的封装。
但是ES6不提供,只能通过变通方法实现。
提案:为class加私有属性,在属性名前,加#
class Point {
#x;
constructor(x = 0) {
this.#x = +x;
}
get x() {
return this.#x;
}
set x(value) {
this.#x = +value;
}
}
let point = new Point();
console.log( point.x = 4 );
也可用作私有属性,私有方法。
私有属性和私有方法前,也可以加上static关键字,表示静态的私有属性或私有方法。
十二、Class继承extends
(一)简介
子类必须在constructor方法中,在this之前调用super方法,否则新建实例会报错。
class Point {
constructor(x,y) {
this.x = x;
this.y = y;
}
toString() {
return this.x + this.y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y);
this.color = color;
}
toString() {
return this.color + ' ' + super.toString();
}
}
let colorPoint = new ColorPoint('red','yellow','blue');
console.log( colorPoint.toString() );// blue redyellow
将子类构造方法constructor中this之前的super去掉,会报如下错误:
如果子类没有定义constructor方法,这个方法会被默认添加,不管有没有显示定义,任何一个子类都有constructor方法。
class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
constructor(...args) {
super(...args);
}
}
colorPoint 是ColorPoint 和 Point两个类的实例
console.log(colorPoint instanceof ColorPoint);//true
console.log(colorPoint instanceof Point);//true
父类的静态方法也会被子类继承
class A {
static hello() {
console.log('hello world');
}
}
class B extends A {
}
B.hello() // hello world
(二)Object.getPrototypeOf() 从子类上获取父类
Object.getPrototypeOf(ColorPoint) === Point // true
(三)super关键字
既可以当作函数使用,也可以当作对象使用。
1、 当作函数
super作为回调函数,代表父类的构造函数,必须写在子类的constructor方法中,并且写在this之前。
super()相当于A.prototype.constructor.call(this)
class A {
constructor() {
console.log(new.target.name);
}
}
class B extends A {
constructor() {
super();
}
}
new A() // A
new B() // B
new.target()指向当前正在执行的函数,super()执行时,指向的是子类B的构造函数,也就是说super()内部的this指向的是子类B。
2、作为对象
普通方法中,指向父类的原型对象;在静态方法中,指向父类。
<script>
class A {
p() {
return 2;
}
}
class B extends A {
constructor() {
super();
console.log(super.p() == A.prototype.p())//true
console.log(super.p());//2
}
}
let b = new B();
子类B中super.b()就是讲super当做一个对象。指向A.prototype,所以super.p() 相当于A.prototype.p().
class Parent {
static myMethod(msg){
console.log('static', msg);
}
myMethod(msg) {
console.log('instance', msg);
}
}
class Child extends Parent {
static myMethod(msg){
super.myMethod(msg);
}
myMethod(msg){
super.myMethod(msg);
}
}
Child.myMethod(1);// static 1
var child = new Child();
child.myMethod(2); //instance 2
上面代码中,super
在静态方法之中指向父类,在普通方法之中指向父类的原型对象。
定义在父类实例上的方法和属性是无法通过super调用的。
class A {
constructor() {
this.p = 2;
}
}
class B extends A {
get m() {
return super.p;
}
}
let b = new B();
b.m // undefined
上面代码中,p
是父类A
实例的属性,super.p
就引用不到它。
如果属性定义在父类的原型对象上,super
就可以取到.A.prototype.x = 2;super.x //2
在子类普通方法中通过super调用父类方法时,方法内部的this指向当前子类实例。
class A {
constructor() {
this.x = 1;
}
}
class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3; //相当于 this.x
console.log(super.x); // undefined A.prototype.x
console.log(this.x); // 3
}
}
let b = new B();
(四)类的prototype属性和 _proto_属性
class A {}
class B extends A {}
A.__proto__
ƒ () { [native code] }
B.__proto__
class A {}
B.__proto__ === A
true
B.prototype
A {constructor: ƒ}constructor: class Barguments: (...)caller: (...)length: 0prototype: A {constructor: ƒ}constructor: class B__proto__: Objectname: "B"__proto__: class A[[FunctionLocation]]: es6.html:18[[Scopes]]: Scopes[2]__proto__: Object
A.prototype
{constructor: ƒ}constructor: class A__proto__: Object
B.prototype.__proto__
{constructor: ƒ}constructor: class Aarguments: (...)caller: (...)length: 0prototype: {constructor: ƒ}name: "A"__proto__: ƒ ()[[FunctionLocation]]: es6.html:17[[Scopes]]: Scopes[2]__proto__: Object
A.prototype.__proto__
constructor: ƒ Object()
arguments: (...)
caller: (...)
length: 1
name: "Object"
B.prototype.__proto__ == A.prototype
true
Class 作为构造函数的语法糖,同时有prototype
属性和__proto__
属性,因此同时存在两条继承链。
- 子类的
__proto__
属性,表示构造函数的继承,总是指向父类。B.__proto__ === A - 子类
prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的prototype
属性。 B.prototype.__proto__ == A.prototype
可以理解为:
作为一个对象,子类B的原型(_proto_属性)是父类A;
作为一个构造函数,子类B的源性对象(prototype属性)是父类A的原型对象(prototype属性)的实例。
子类继承Object:
A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true
不存在任何继承
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true
类的原型的原型,是父类的原型
var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');
p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true
(五)原生构造函数的继承
原生构造函数是指语言内置的构造函数,通常用来生成数据结构。
Boolean() 、Number() 、 String() 、Aqqay()、 Date()、 Function()、 RegExp()、 Error()、Object()
class MyArray extends Array {
constructor(...args){
super(...args);
}
}
var arr = new MyArray();
arr[0] = 12;
console.log(arr.length);
console.log(arr[0])
注意,继承Object
的子类,有一个行为差异。
class NewObj extends Object{
constructor(){
super(...arguments);
}
}
var o = new NewObj({attr: true});
o.attr === true // false
上面代码中,NewObj
继承了Object
,但是无法通过super
方法向父类Object
传参。这是因为 ES6 改变了Object
构造函数的行为,一旦发现Object
方法不是通过new Object()
这种形式调用,ES6 规定Object
构造函数会忽略参数。
(六)Mixin模式的实现
Mixin是指多个对象合成一个新的对象,新对象具有各个组成成员的接口。
十三、Module模块化
(一)import 和 default
- ES6 模块不是对象,而是通过
export
命令显式指定输出的代码,再通过import
命令输入。 - 编译时加载,自动采用严格模式。
import { stat, exists, readFile } from 'fs';
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export { firstName, lastName, year };
- 主要由export 和 import 命令构成。
export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能。 - export命令使用 as 关键字重命名。
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
- 注意: export必须与模块内部变量一一对应。
// 报错
export 1;
// 报错
var m = 1;
export m;
正确写法:
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
- export 和 import 如果处在块级作用域中,就会报错,没法静态化。
- import有提升效果,会提升到整个模块的头部,首先执行。
- 整体加载,用 * 指定一个对象,所有输出值都加载在这个对象上面。
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
(二)export default 命令
- export default 命令
为模块指定默认输出。
// export-default.js
export default function () {
console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'
- 不需要知道原模块的函数名。
- 注意,export default命令在一个模块中只能使用一次,所以import后面不使用大括号。
- 正是因为
export default
命令其实只是输出一个叫做default
的变量,所以它后面不能跟变量声明语句。
// 正确
export var a = 1;
// 正确
var a = 1;
export default a;
// 错误
export default var a = 1;
- 同一个import命令同时输入默认方法和其他接口
import _, { each, forEach } from 'lodash';
- 对应上面代码的
export
语句如下。
export default function (obj) {
// ···
}
export function each(obj, iterator, context) {
// ···
}
export { each as forEach };
- 输出类
// MyClass.js
export default class { ... }
// main.js
import MyClass from 'MyClass';
let o = new MyClass();
(三)export 和 default 的复合写法
- 在一个模块中,先后输入输出,import 和 export语句可以结合。
import { foo, bar } from 'my_module';
export { foo, bar };
结合成:
export { foo, bar } from 'my_module';
- 默认接口的写法如下。
export { default } from 'foo';
- 具名接口改为默认接口的写法如下。
export { es6 as default } from './someModule';
// 等同于
import { es6 } from './someModule';
export default es6;
- 同样地,默认接口也可以改名为具名接口。
export { default as es6 } from './someModule';
(四)模块的继承
//circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
circleplus.js 继承 circle.js 并将circle.js 中的area 方法改名为circleArea
//circleplus.js
export { area as circleArea } from 'circle'
export var e = 2.718492738;
export default function(x) {
return Math.exp(x);
}
其中继承的是实现语句,只是对其方法进行了改名,export * 会忽略circle中 default方法
export * from 'circle'
//main.js
import * as math from 'circleplus.js';
import exp from 'circleplus';
console.log( exp(math.e) );
(五)跨模常量
- 由于const命令声明的常量只在当前代码快有效,想要设置跨模块(跨多个文件)的常量可以写成下面:
/ constants.js 模块
export const A = 1;
export const B = 3;
export const C = 4;
// test1.js 模块
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3
// test2.js 模块
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3
- 如果使用的常量非常多,可以建一个专门的
constants
目录,将各种常量写在不同的文件里面,保存在该目录下。
//db.js
export const db = {
url: 'http://my.couchdbserver.local:5984',
admin_username: 'admin',
admin_password: 'admin password'
};
// user.js
export const user = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];
在index.js中将输出的常量及合并。
// index.js
export { db } from './db';
export { user } from './user';
在script.js中使用,直接加载index.js
//script.js
import {db, user } from './index';
(六)import()函数
import命令是在编译时执行,被javascript静态分析,只能在模块的顶层,不能再代码之中。
引入import()函数,支持动态加载模块。
import(specifier)
import
函数的参数specifier
,指定所要加载的模块的位置。import
命令能够接受什么参数,import()
函数就能接受什么参数,两者区别主要是后者为动态加载。
适用场合
1、按需加载
import()可以在需要的时候,再加载某个模块。
button.addEventListener('click', event => {
import('./dialogBox.js')
.then(dialogBox => {
dialogBox.open();
})
.catch(error => {
/* Error handling */
})
});
上面代码中,import()
方法放在click
事件的监听函数之中,只有用户点击了按钮,才会加载这个模块。
2、条件加载
import()
可以放在if
代码块,根据不同的情况,加载不同的模块。
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
}
3、动态的模块路径
import()
允许模块路径动态生成。
import(f())
.then(...);
上面代码中,根据函数f
的返回结果,加载不同的模块。
注意点
1、import() 加载成功以后,输出的接口可以用解构获得。
import('./myModule.js')
.then(({export1, export2}) => {
// ...·
});
export1
和export2
都是myModule.js
的输出接口,可以解构获得。
2、具名输入
import('./myModule.js')
.then(({default: theDefault}) => {
console.log(theDefault);
});
3、同时加载多个模块
Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
])
.then(([module1, module2, module3]) => {
···
});
4、import() 也可以用在async 函数之中
async function main() {
const myModule = await import('./myModule.js');
const {export1, export2} = await import('./myModule.js');
const [module1, module2, module3] =
await Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
]);
}
main();
(七)浏览器加载
1、两种异步加载的方式:defer async
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
defer 和 async 的区别:
defer
要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async
一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。
defer
是“渲染完再执行”,async
是“下载完就执行”。
另外,如果有多个defer
脚本,会按照它们在页面出现的顺序加载,而多个async
脚本是不能保证加载顺序的。
2、浏览器加载ES6模块
type="module"
属性。
异步加载,不会造成堵塞。等到整个页面渲染完,在执行模块脚本。等同于defer属性。
<script type="module" src="./foo.js"></script>
一旦使用了async
属性,<script type="module">
就不会按照在页面出现的顺序执行,而是只要该模块加载完成,就执行该模块。
(八)ES6模块 与 commenJs
AMD , CMD, CommonJS,ES6 Module,UMD之间的区别
ES6 的import
有点像 Unix 系统的“符号连接”,原始值变了,import
加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
由于 ES6 输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错。
// lib.js
export let obj = {};
// main.js
import { obj } from './lib';
obj.prop = 123; // OK
obj = {}; // TypeError
(九)node.js加载
Node.js 要求 ES6 模块采用 .mjs
后缀文件名。也就是说,只要脚本文件里面使用import
或者export
命令,那么就必须采用 .mjs
后缀名。Node.js 遇到 .mjs
文件,就认为它是 ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定"use strict"
。
.mjs
文件总是以 ES6 模块加载,.cjs
文件总是以 CommonJS 模块加载,.js
文件的加载取决于package.json
里面type
字段的设置。
{
"type": "module"
}
设置了之后,就被解释为ES6模块
package.json
文件有两个字段可以指定模块的入口文件:main
和exports
。比较简单的模块,可以只使用main
字段,指定模块加载的入口文件。
// ./node_modules/es-module-package/package.json
{
"type": "module",
"main": "./src/index.js"
}
上面代码指定项目的入口脚本为./src/index.js
,它的格式为 ES6 模块。如果没有type
字段,index.js
就会被解释为 CommonJS 模块。
然后,import
命令就可以加载这个模块。
// ./my-app.mjs
import { something } from 'es-module-package';
// 实际加载的是 ./node_modules/es-module-package/src/index.js
上面代码中,运行该脚本以后,Node.js 就会到./node_modules
目录下面,寻找es-module-package
模块,然后根据该模块package.json
的main
字段去执行入口文件。
这时,如果用 CommonJS 模块的require()
命令去加载es-module-package
模块会报错,因为 CommonJS 模块不能处理export
命令。
(十)内部变量
- 是
this
关键字。ES6 模块之中,顶层的this
指向undefined
;CommonJS 模块的顶层this
指向当前模块,这是两者的一个重大差异。 -
其次,以下这些顶层变量在 ES6 模块之中都是不存在的。
arguments
require
module
exports
__filename
__dirname
(十一)循环加载
a
依赖b
,b
依赖c
,c
又依赖a
这样的情况.
强耦合。
// a.js
var b = require('b');
// b.js
var a = require('a');
CommonJS 输入的是被输出值的拷贝,不是引用。
CommonJS 模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。所以,输入变量的时候,必须非常小心。
var a = require('a'); // 安全的写法
var foo = require('a').foo; // 危险的写法
exports.good = function (arg) {
return a.foo('good', arg); // 使用的是 a.foo 的最新值
};
exports.bad = function (arg) {
return foo('bad', arg); // 使用的是一个部分加载时的值
};
上面代码中,如果发生循环加载,require('a').foo
的值很可能后面会被改写,改用require('a')
会更保险一点。
十四、async、await
(一)async
它就是 Generator 函数的语法糖。
异步。
常见于koa2.koa2推荐阅读
async function fn() {// 表示异步,这个函数里面有异步操作
let result = await ..... //表示后面有结果需要等待
}
注意: await 只能放在 async 函数中
(二)读取文件案例
Promise
const fs = require('fs');
const readFile = function(file) {
// 封装Promise
return new Promise(( resolve, reject ) => {
fs.readFile(file, (err,data) => {
if(err) reject(err);
resolve(data.toString());
});
});
}
readFile('data/a.txt').then(res => {
console.log(res);
return readFile('data/b.txt');
}).then(res => {
console.log(res);
return readFile('data/c.txt');
}).then(res => {
console.log(res);
})
Generator
function * gen() {
yield readFile('data/a.txt');
yield readFile('data/b.txt');
yield readFile('data/c.txt');
}
let g1 = gen();
g1.next().value.then(res => {
console.log(res);
return g1.next().value;
}).then(res => {
console.log(res);
return g1.next().value;
}).then(res => {
console.log(res);
})
async
通常与await配合使用。
异步。
async function f() {
let f1 = await readFile('data/a.txt');
console.log(f1);
let f2 = await readFile('data/b.txt');
console.log(f2);
let f3 = await readFile('data/c.txt');
console.log(f3);
}
f();
运行结果都是:node promise.js
aaa bbb ccc
(三)特点
1、await 只能放到async 函数中。
2、相比generator语义化更强。
3、await后面可以使Promise对象,也可以是数字、字符串、布尔
4、async函数返回一个promise对象
async function fn() {
return 'hello'
}
console.log(fn())//Promise
fn().then(res => {
console.log(res);//hello
})
失败:
async function fn() {
throw new Error('error');
}
fn().then(res => {
console.log(res);
}).catch(err => {
console.log(err);
// Error: error
// at fn(es6.html: 19)
// at es6.html: 21
})
5、只要await语句后面Promise状态变成reject,那么整个async函数会中断执行。
async function fn() {
let a = await Promise.resolve('success');
console.log(a);//success
await Promise.reject('error');
}
fn().catch(err => {
console.log(err);//error
})
当把reject放在resolve上面
async function fn() {
await Promise.reject('error');
let a = await Promise.resolve('success');
console.log(a);
}
fn().catch(err => {
console.log(err);//error
})
没有执行到resolve(),只输出error
(四)解决async函数中跑出错误,中断影响之后代码
1、try{}catch(e){}
async function fn() {
try {
await Promise.reject('error');
} catch (error) {}
let a = await Promise.resolve('success');
console.log(a);//success
}
fn().catch(err => {
console.log(err);
})
只输出 success
2、Promise本身catch
async function fn() {
await Promise.reject('error').catch(err => {
console.log(err)//error
});
let a = await Promise.resolve('success');
console.log(a);//success
}
fn();
个人建议:只要有await,都放在 try catch中。
如果在项目中执行没有联系的await Promise 时,可以用Promise.all([])
async function f() {
let [a, b, c] = await Promise.all([
readFile('data/a.txt'),
readFile('data/b.txt'),
readFile('data/c.txt')
]);
console.log(a);//aaa
console.log(b);//bbb
console.log(c);//ccc
}
f();
十五、Proxy
代理器,属于一种“元编程”,即对编程语言进行编程。
扩展增强对象的一些功能。
作用:vue中拦截
预警,上报,扩展功能,统计,增强对象等。
proxy是设计模式中的代理模式。javascript设计模式——代理模式
语法
new Proxy(target, handler); 返回一个对象。
target:被代理的对象。是一个对象
handler: 对代理的对象进行什么操作。是一个json中放方法。
支持拦截操作方法
这些方法都是写在handler中的。
- get(target, propKey, receiver):拦截对象属性的读取,比如
proxy.foo
和proxy['foo']
。
const obj = {
name: 'gsy'
}
const myObj = new Proxy(obj, {
get(target, property, receiver) {
console.log(target, property);
// Object "name"
// name: "gsy
}
})
myObj.name;
myObj.aaa;// property = "aaa"
myObj点什么 property就是什么,如果没有或者不知道目标对象是什么,Proxy第一个参数可以传{}
案例一:拦截读取操作判断对象中是否有该属性
const obj = {
name: 'gsy'
}
const myObj = new Proxy(obj, {
get(target, property, receiver) {
if(property in target){
return target[property];
}else {
console.warn(`${ property } 不在 ${ target } 上`);
throw new ReferenceError(`${ property } 不在 ${ target } 上`);
}
}
})
// console.log( myObj.name );
myObj.aaa;
案例二:实现生成DOM节点的通用函数dom
const dom = new Proxy({}, {
get(target, property) {
// console.log(property);//dom.a is not a function
return function(attr = {},...children) {
// console.log(property);//a li li ul div
const el = document.createElement(property);
// 设置属性
for (let prop of Object.keys(attr)) {
el.setAttribute(prop,attr[prop]);
}
// 创建节点
for (let child of children) {
if(typeof child == 'string'){
child = document.createTextNode(child);
}
el.appendChild(child)
}
return el;
}
}
})
const el = dom.div(
{id:'div1',class:'div1'},
'i am div',
dom.a({href:'http://www.baidu.com'},
'baidu'),
dom.ul(
dom.li({},'111'),
dom.li({},'222')
)
)
document.body.appendChild(el);
- set(target, propKey, value, receiver):拦截对象属性的设置,比如
proxy.foo = v
或proxy['foo'] = v
,返回一个布尔值。
依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// 对于满足条件的 age 属性以及其他属性,直接保存
obj[prop] = value;
}
};
let person = new Proxy({}, validator);
person.age = 100;
person.age // 100
person.age = 'young' // 报错
person.age = 300 // 报错
结合get
和set
方法,就可以做到防止这些内部属性被外部读写。
const handler = {
get(target, property){
invariant(property, 'get')
return target[property];
},
set(target, property, value){
invariant(property,'set');
target[property] = value;
return true;
}
}
function invariant(property, action){
if(property[0] == '_'){
throw new Error(`Invalid attempt to ${action} private "${property}" property`);
}
}
let proxy = new Proxy({},handler);
// proxy._aaa;//Invalid attempt to get private "_aaa" property
proxy._aaa = 'aaa';//nvalid attempt to set private "_aaa" property
- has(target, propKey):拦截
propKey in proxy
的操作,返回一个布尔值。
has
方法用来拦截HasProperty
操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in
运算符。
has
方法可以接受两个参数,分别是目标对象、需查询的属性名。
let stu1 = {name: '张三', score: 59};
let stu2 = {name: '李四', score: 99};
let handler = {
has(target, prop) {
if (prop === 'score' && target[prop] < 60) {
console.log(`${target.name} 不及格`);
return false;
}
return prop in target;
}
}
let oproxy1 = new Proxy(stu1, handler);
let oproxy2 = new Proxy(stu2, handler);
'score' in oproxy1
// 张三 不及格
// false
'score' in oproxy2
// true
for (let a in oproxy1) {
console.log(oproxy1[a]);
}
// 张三
// 59
for (let b in oproxy2) {
console.log(oproxy2[b]);
}
// 李四
// 99
- deleteProperty(target, propKey):拦截
delete proxy[propKey]
的操作,返回一个布尔值。
deleteProperty
方法用于拦截delete
操作,如果这个方法抛出错误或者返回false
,当前属性就无法被delete
命令删除。
let json = {
a:1,
b:2
}
let newJson = new Proxy(json, {
deleteProperty(target, property){
console.log(`您要删除${ property } 吗`);
delete target[property];
},
has(target, property){
console.log(`判断是否有${ property }`);
return true;
}
})
console.log('a' in newJson);
// 判断是否有a
// true
delete newJson.a;
console.log(newJson)
// 您要删除a 吗
// Proxy {b: 2}
- ownKeys(target):拦截
Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
、for...in
循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()
的返回结果仅包括目标对象自身的可遍历属性。 - getOwnPropertyDescriptor(target, propKey):拦截
Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。 - defineProperty(target, propKey, propDesc):拦截
Object.defineProperty(proxy, propKey, propDesc)
、Object.defineProperties(proxy, propDescs)
,返回一个布尔值。 - preventExtensions(target):拦截
Object.preventExtensions(proxy)
,返回一个布尔值。 - getPrototypeOf(target):拦截
Object.getPrototypeOf(proxy)
,返回一个对象。 - isExtensible(target):拦截
Object.isExtensible(proxy)
,返回一个布尔值。 - setPrototypeOf(target, proto):拦截
Object.setPrototypeOf(proxy, proto)
,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 - apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如
proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。分别是目标对象、目标对象的上下文对象(this
)和目标对象的参数数组。 - construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如
new proxy(...args)
。
construct
方法用于拦截new
命令,下面是拦截对象的写法。
var handler = {
construct (target, args, newTarget) {
return new target(...args);
}
};
construct
方法可以接受两个参数。
target
:目标对象args
:构造函数的参数对象newTarget
:创造实例对象时,new
命令作用的构造函数(下面例子的p
)
construct
方法返回的必须是一个对象,否则会报错。
this问题
目标对象内部的this
关键字会指向 Proxy 代理。
const target = {
m: function () {
console.log(this === proxy);
}
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m() // false
proxy.m() // true
上面代码中,一旦proxy
代理target.m
,后者内部的this
就是指向proxy
,而不是target
。
十六、Reflect
reflect 反射,增强方法的一些功能。
function sum(a, b) {
return a + b;
}
let newSum = new Proxy(sum, {
apply(target, context, args) {
console.log(target, context, args);
console.log(...arguments);
// ƒ sum(a, b) {
// return a + b;
// }
// undefined
//(2)[2, 3]
return '拦截函数sum()';
}
})
console.log(newSum(2, 3)) //拦截函数sum()
在使用apply()拦截函数sum()时,sum()函数不能执行。
这是需要reflect反射,
其中apply()中的参数与 ...arguments值是相等的,
return Reflect.apply(...arguments);//5
Reflect.apply(目标函数名,this指向,参数数组)
效果相当于call() aplly()
function show(...args){
console.log(this);
console.log(args);
}
show.call('aaa',1,2,3);
show.apply('aaa',[1,2,3])
Reflect.apply(show,'aaa',[1,2,3]);
// String {"aaa"}
// (3) [1, 2, 3]
下面的这些都是Object.xxx语言内部的方法,将来可能都放在Reflect身上,直接通过Reflect使用。
比如:
‘assign’ in Object -----> Reflect.has(Object, 'assign')
Reflect
对象一共有 13 个静态方法。
- Reflect.apply(target, thisArg, args)
- Reflect.construct(target, args)
Reflect.construct
方法等同于new target(...args)
,这提供了一种不使用new
,来调用构造函数的方法
- Reflect.get(target, name, receiver)
- Reflect.set(target, name, value, receiver)
- Reflect.defineProperty(target, name, desc)
Reflect.defineProperty
方法基本等同于Object.defineProperty
,用来为对象定义属性。未来,后者会被逐渐废除,请从现在开始就使用Reflect.defineProperty
代替它。
- Reflect.deleteProperty(target, name)
- Reflect.has(target, name)
- Reflect.ownKeys(target)
Reflect.ownKeys
方法用于返回对象的所有属性,基本等同于Object.getOwnPropertyNames
与Object.getOwnPropertySymbols
之和。
var myObject = {
foo: 1,
bar: 2,
[Symbol.for('baz')]: 3,
[Symbol.for('bing')]: 4,
};
// 旧写法
Object.getOwnPropertyNames(myObject)
// ['foo', 'bar']
Object.getOwnPropertySymbols(myObject)
//[Symbol(baz), Symbol(bing)]
// 新写法
Reflect.ownKeys(myObject)
// ['foo', 'bar', Symbol(baz), Symbol(bing)]
如果Reflect.ownKeys()
方法的第一个参数不是对象,会报错。
- Reflect.isExtensible(target)
Reflect.isExtensible
方法对应Object.isExtensible
,返回一个布尔值,表示当前对象是否可扩展。
const myObject = {};
// 旧写法
Object.isExtensible(myObject) // true
// 新写法
Reflect.isExtensible(myObject) // true
- Reflect.preventExtensions(target)
Reflect.preventExtensions
对应Object.preventExtensions
方法,用于让一个对象变为不可扩展。它返回一个布尔值,表示是否操作成功。
var myObject = {};
// 旧写法
Object.preventExtensions(myObject) // Object {}
// 新写法
Reflect.preventExtensions(myObject) // true
- Reflect.getOwnPropertyDescriptor(target, name)
Reflect.getOwnPropertyDescriptor
基本等同于Object.getOwnPropertyDescriptor
,用于得到指定属性的描述对象,将来会替代掉后者。
- Reflect.getPrototypeOf(target)
Reflect.getPrototypeOf
方法用于读取对象的__proto__
属性,对应Object.getPrototypeOf(obj)
。
- Reflect.setPrototypeOf(target, prototype)
Reflect.setPrototypeOf
方法用于设置目标对象的原型(prototype),对应Object.setPrototypeOf(obj, newProto)
方法。它返回一个布尔值,表示是否设置成功。
十七、编程风格
(一)变量 块级作用域
- 建议不再使用
var
命令,而是使用let
命令取代 - 在
let
和const
之间,建议优先使用const
,尤其是在全局环境,不应该设置变量,只应设置常量。
// bad
var a = 1, b = 2, c = 3;
// good
const a = 1;
const b = 2;
const c = 3;
// best
const [a, b, c] = [1, 2, 3];
const
声明常量还有两个好处,一是阅读代码的人立刻会意识到不应该修改这个值,二是防止了无意间修改变量值所导致的错误。
(二)字符串
静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。
// bad
const a = "foobar";
const b = 'foo' + a + 'bar';
// acceptable
const c = `foobar`;
// good
const a = 'foobar'; // 静态字符串
const b = `foo${a}bar`;
(三)结构赋值
-
使用数组成员对变量赋值时,优先使用解构赋值。
const arr = [1, 2, 3, 4]; // bad const first = arr[0]; const second = arr[1]; // good const [first, second] = arr; // 1 2
-
函数的参数如果是对象的成员,优先使用解构赋值。
// bad function getFullName(user) { const firstName = user.firstName; const lastName = user.lastName; } // good function getFullName(obj) { const { firstName, lastName } = obj; } // best function getFullName({ firstName, lastName }) { }
-
如果函数返回多个值,优先使用对象的解构赋值,而不是数组的解构赋值。这样便于以后添加返回值,以及更改返回值的顺序。
// bad function processInput(input) { return [left, right, top, bottom]; } // good function processInput(input) { return { left, right, top, bottom }; } const { left, right } = processInput(input);
(四)对象
-
单行定义的对象,最后成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾一个。
// bad const a = { k1: v1, k2: v2, }; const b = { k1: v1, k2: v2 }; // good const a = { k1: v1, k2: v2 }; const b = { k1: v1, k2: v2, };
-
对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用
Object.assign
方法。// bad const a = {}; a.x = 3; // if reshape unavoidable const a = {}; Object.assign(a, { x: 3 }); // good const a = { x: null }; a.x = 3;
-
如果对象的属性名是动态的,可以在创造对象的时候,使用属性表达式定义。
// bad const obj = { id: 5, name: 'San Francisco', }; obj[getKey('enabled')] = true; // good const obj = { id: 5, name: 'San Francisco', [getKey('enabled')]: true, };
最好采用属性表达式,在新建obj
的时候,将该属性与其他属性定义在一起。这样一来,所有属性就在一个地方定义了。
- 对象的属性和方法,尽量采用简洁表达法,这样易于描述和书写。
let ref = 'ref';
const atom = {
ref,//简洁写法
value: 1,
addValue(value) {
return atom.value + value;
},
};
(五)数组
- 使用扩展运算符 ... 拷贝数组
const foo = document.querySelectorAll('.foo');
console.log(foo);//对象数组 NodeList(3) [div.foo, div.foo, div.foo]
const nodes = Array.from(foo);
console.log(nodes)//数组 (3) [div.foo, div.foo, div.foo]
console.log([...foo]);//结果同Array.from
(六)函数
- 使用箭头函数(替代匿名函数,立即执行函数,Function.prototype.bind)
立即执行函数
(() => {
console.log('lijizhixing ')
})()
匿名函数
// bad
[1, 2, 3].map(function (x) {
return x * x;
});
// good
[1, 2, 3].map((x) => {
return x * x;
});
// best
let arr = [1,2,3].map( x => x * x);
console.log(arr);//[1, 4, 9]
Function.prototype.bind
,不应再用 self/_this/that 绑定 this
// bad
const self = this;
const boundMethod = function(...params) {
return method.apply(self, params);
}
// acceptable
const boundMethod = method.bind(this);
// best
const boundMethod = (...params) => method.apply(this, params);
const bound = (...params) => Reflect.apply(method,this,params)
- 简单的、单行的、不会复用的函数,建议采用箭头函数。如果函数体较为复杂,行数较多,还是应该采用传统的函数写法。
-
所有配置项都应该集中在一个对象,放在最后一个参数,布尔值不可以直接作为参数。
// bad function divide(a, b, option = false ) { } // good function divide(a, b, { option = false } = {}) { }
- 在函数体内用 rest(...)运算符替代arguments。
arguments是一个类数组对象,rest运算符是一个真正的对象。
// bad
function concatenateAll() {
const args = Array.prototype.slice.call(arguments);
return args.join('');
}
// good
function concatenateAll(...args) {
return args.join('');
}
-
使用默认值语法设置函数参数的默认值。
// bad function handleThings(opts) { opts = opts || {}; } // good function handleThings(opts = {}) { // ... }
(七)Map结构
注意区分 Object 和 Map,只有模拟现实世界的实体对象时,才使用 Object。如果只是需要key: value
的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。
let map = new Map(arr);
for (let key of map.keys()) {
console.log(key);
}
for (let value of map.values()) {
console.log(value);
}
for (let item of map.entries()) {
console.log(item[0], item[1]);
}
(八)Class
-
总是用 Class,取代需要 prototype 的操作。因为 Class 的写法更简洁,更易于理解。
// bad function Queue(contents = []) { this._queue = [...contents]; } Queue.prototype.pop = function() { const value = this._queue[0]; this._queue.splice(0, 1); return value; } // good class Queue { constructor(contents = []) { this._queue = [...contents]; } pop() { const value = this._queue[0]; this._queue.splice(0, 1); return value; } } let queue = new Queue('hello'); console.log(queue.pop());//h
- 使用
extends
实现继承,因为这样更简单,不会有破坏instanceof
运算的危险。
// bad
const inherits = require('inherits');
function PeekableQueue(contents) {
Queue.apply(this, contents);
}
inherits(PeekableQueue, Queue);
PeekableQueue.prototype.peek = function() {
return this._queue[0];
}
// good
class PeekableQueue extends Queue {
peek() {
return this._queue[0];
}
}
(九)模块
- 使用import取代require
- 使用export取代module.export
// commonJS的写法
var React = require('react');
var Breadcrumbs = React.createClass({
render() {
return <nav />;
}
});
module.exports = Breadcrumbs;
// ES6的写法
import React from 'react';
class Breadcrumbs extends React.Component {
render() {
return <nav />;
}
};
export default Breadcrumbs;
- 如果模块只有一个输出值,就使用
export default
,如果模块有多个输出值,就不使用export default
,export default
与普通的export
不要同时使用。 -
不要在模块输入中使用通配符。因为这样可以确保你的模块之中,有一个默认输出(export default)。
// bad import * as myObject from './importModule'; // good import myObject from './importModule';
-
如果模块默认输出一个函数,函数名的首字母应该小写。
function makeStyleGuide() { } export default makeStyleGuide;
-
如果模块默认输出一个对象,对象名的首字母应该大写。
const StyleGuide = { es6: { } }; export default StyleGuide;
(十)ESLint
见官网