0、@babel/polyfill
Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API,比如Iterator、Generator、Set、Map、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。
举例来说,ES6 在Array对象上新增了Array.from方法。Babel 就不会转码这个方法。如果想让这个方法运行,必须使用babel-polyfill,为当前环境提供一个垫片
数组
扩展运算符 …
注意,只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错。
(...[1, 2])//扩展运算符所在的括号不是函数调用。
// Uncaught SyntaxError: Unexpected number
console.log((...[1, 2]))//扩展运算符所在的括号不是函数调用。
// Uncaught SyntaxError: Unexpected number
console.log(...[1, 2])
// 1 2
实现了 Iterator 接口的对象
任何定义了遍历器(Iterator)接口的对象(参阅 Iterator 一章),都可以用扩展运算符转为真正的数组。
扩展运算符背后调用的是遍历器接口(Symbol.iterator
)
let nodeList = document.querySelectorAll('div');
let array = [...nodeList];
上面代码中,querySelectorAll
方法返回的是一个NodeList
对象。它不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于NodeList
对象实现了 Iterator 。
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))
let arr = [...arrayLike];
let arr1=Array.from(arrayLike);// ["a", "b", "c"]
上面代码中,arrayLike
是一个类似数组的对象,但是没有部署 Iterator 接口,扩展运算符就会报错。这时,可以改为使用Array.from
方法将arrayLike
转为真正的数组。
Array.from()
Array.from
方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
所谓类似数组的对象,本质特征只有一点,即必须有length
属性。
只要是部署了 Iterator 接口的数据结构,Array.from
都能将其转为数组。
常见的类似数组的对象是 DOM 操作返回的 NodeList 集合,以及函数内部的arguments
对象。
Array.from
都可以将它们转为真正的数组。Array.from
将它转为真正的数组。
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
// ES6的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
Array.of()
Array.of
方法用于将一组值,转换为数组。
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
Array.of() // []
Array.of(undefined) // [undefined]
Array.of
方法可以用下面的代码模拟实现。
function ArrayOf(){
return [].slice.call(arguments);
}
数组实例的 fill()
fill
方法使用给定值,填充一个数组。
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
new Array(3).fill(7)
// [7, 7, 7]
['a', 'b', 'c'].fill(7, 1, 2)//从 1 号位开始,向原数组填充 7,到 2 号位之前结束
// ['a', 7, 'c']
//如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。
let arr = new Array(3).fill({name: "Mike"});
arr[0].name = "Ben";
arr
// [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]
let arr = new Array(3).fill([]);
arr[0].push(5);
arr
// [[5], [5], [5]]
数组实例的 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"
如果不使用for...of
循环,可以手动调用遍历器对象的next
方法,进行遍历。
let letter = ['a', 'b', 'c'];
let entries = letter.entries();
console.log(entries.next().value); // [0, 'a']
console.log(entries.next().value); // [1, 'b']
console.log(entries.next().value); // [2, 'c']
数组实例的 includes()
Array.prototype.includes
方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes
方法类似。ES2016 引入了该方法。
[1, 2, 3].includes(2) // true
[1, 2, 3].includes(4) // false
[1, 2, NaN].includes(NaN) // true
没有该方法之前,我们通常使用数组的indexOf
方法,检查是否包含某个值。
if (arr.indexOf(el) !== -1) {
// ...
}
[NaN].indexOf(NaN)// -1 indexOf它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判。
[NaN].includes(NaN)// true
数组实例的 flat(),flatMap()
数组的成员有时还是数组,Array.prototype.flat()
用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。
[1, 2, [3, [4, 5]]].flat()//默认只会“拉平”一层
// [1, 2, 3, [4, 5]]
[1, 2, [3, [4, 5]]].flat(2)//flat()的参数为2,表示要“拉平”两层的嵌套数组
// [1, 2, 3, 4, 5]
[1, 2, [3, 4]].flat()// [1, 2, 3, 4]
[1, 2, , 4, 5].flat()// [1, 2, 4, 5] 如果原数组有空位,flat()方法会跳过空位。
flatMap()
方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()
),然后对返回值组成的数组执行flat()
方法。该方法返回一个新数组,不改变原数组。
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]
//flatMap()只能展开一层数组。
// 相当于 [[[2]], [[4]], [[6]], [[8]]].flat()
[1, 2, 3, 4].flatMap(x => [[x * 2]])
// [[2], [4], [6], [8]]
//遍历函数返回的是一个双层的数组,但是默认只能展开一层,因此flatMap()返回的还是一个嵌套数组。
对象
可枚举性
对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor
方法可以获取该属性的描述对象。
let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
// value: 123,
// writable: true,
// enumerable: true,
// configurable: true
// }
目前,有四个操作会忽略enumerable
为false
的属性。
for...in
循环:只遍历对象自身的和继承的可枚举的属性。Object.keys()
:返回对象自身的所有可枚举的属性的键名。JSON.stringify()
:只串行化对象自身的可枚举的属性。Object.assign()
: 忽略enumerable
为false
的属性,只拷贝对象自身的可枚举的属性。
ES6 规定,所有 Class 的原型的方法都是不可枚举的。
总的来说,操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用for...in
循环,而用Object.keys()
代替。
对象遍历属性
ES6 一共有 5 种方法可以遍历对象的属性。
(1)for…in
for...in
循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。
(2)Object.keys(obj)
Object.keys
返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。
(3)Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames
返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
(4)Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols
返回一个数组,包含对象自身的所有 Symbol 属性的键名。
(5)Reflect.ownKeys(obj)
Reflect.ownKeys
返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。
以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。
- 首先遍历所有数值键,按照数值升序排列。
- 其次遍历所有字符串键,按照加入时间升序排列。
- 最后遍历所有 Symbol 键,按照加入时间升序排列。
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
解构赋值
扩展运算符的解构赋值,不能复制继承自原型对象的属性。
let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let { ...o3 } = o2;//对象o3复制了o2,但是只复制了o2自身的属性,没有复制它的原型对象o1的属性。
o3 // { b: 2 }
o3.a // undefined
Object 对象的新增方法
Object.is()
ES5 比较两个值是否相等,只有两个运算符:相等运算符(==
)和严格相等运算符(===
)。它们都有缺点,前者会自动转换数据类型,后者的NaN
不等于自身,以及+0
等于-0
。
Object.is
用来解决这个问题。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
ES5 可以通过下面的代码,部署Object.is
。
Object.defineProperty(Object, 'is', {
value: function(x, y) {
if (x === y) {
// 针对+0 不等于 -0的情况
return x !== 0 || 1 / x === 1 / y;
}
// 针对NaN的情况
return x !== x && y !== y;
},
configurable: true,
enumerable: false,
writable: true
});
Object.assign()
Object.assign
方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。Object.assign
拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false
)。- 属性名为 Symbol 值的属性,也会被
Object.assign
拷贝。
const target = { a: 1, b: 1 };
const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);//第一个参数是目标对象,后面的参数都是源对象。
target // {a:1, b:2, c:3}
//如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
//
const obj = {a: 1};
Object.assign(obj) === obj // true 如果只有一个参数,Object.assign会直接返回该参数。
/
typeof Object.assign(2) // "object" 如果该参数不是对象,则会先转成对象,然后返回。
Object.assign(undefined) // 报错 undefined和null无法转成对象,所以如果它们作为参数,就会报错。
Object.assign(null) // 报错
///
let obj = {a: 1};
//非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同。首先,这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,如果undefined和null不在首参数,就不会报错。
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true
注意点:
- 浅拷贝
- 同名属性的替换
- 数组的处理
Object.assign
可以用来处理数组,但是会把数组视为对象。
Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3]
- 取值函数的处理
Object.assign
只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。
const source = {
get foo() { return 1 }
};
const target = {};
Object.assign(target, source)
// { foo: 1 }
Object.assign
常见用途:
-
为对象添加属性
class Point { constructor(x, y) { Object.assign(this, {x, y}); }}
-
为对象添加属性
Object.assign(SomeClass.prototype, { someMethod(arg1, arg2) { ··· }, anotherMethod() { ··· } });
-
克隆对象
let obj= Object.assign({}, origin);
-
合并多个对象
const merge = (target, ...sources) => Object.assign(target, ...sources);
-
为属性指定默认值
options = Object.assign({}, DEFAULTS, options);
Object.getOwnPropertyDescriptors()
ES5 的Object.getOwnPropertyDescriptor()
方法会返回某个对象属性的描述对象(descriptor)。ES2017 引入了Object.getOwnPropertyDescriptors()
方法,返回指定对象所有自身属性(非继承属性)的描述对象。
该方法的引入目的,主要是为了解决Object.assign()
无法正确拷贝get
属性和set
属性的问题。
const obj = {
foo: 123,
get bar() { return 'abc' }
};
Object.getOwnPropertyDescriptors(obj)
// { foo:
// { value: 123,
// writable: true,
// enumerable: true,
// configurable: true },
// bar:
// { get: [Function: get bar],
// set: undefined,
// enumerable: true,
// configurable: true } }
Object.setPrototypeOf()
Object.setPrototypeOf
方法的作用与__proto__
相同,用来设置一个对象的prototype
对象,返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。\
function setPrototypeOf(obj, proto) {
obj.__proto__ = proto;
return obj;
}
Object.getPrototypeOf()
该方法与Object.setPrototypeOf
方法配套,用于读取一个对象的原型对象
function Rectangle() {
// ...
}
const rec = new Rectangle();
Object.getPrototypeOf(rec) === Rectangle.prototype// true
Object.setPrototypeOf(rec, Object.prototype);
Object.getPrototypeOf(rec) === Rectangle.prototype// false
Object.getPrototypeOf(rec) === Rectangle.prototype.__proto__ // true
//参数不是对象,会被自动转为对象。
Object.getPrototypeOf(1) === Number.prototype // true
Object.getPrototypeOf('foo') === String.prototype // true
Object.getPrototypeOf(true) === Boolean.prototype // true
Object.getPrototypeOf(null)//参数是undefined或null,它们无法转为对象,所以会报错。
// TypeError: Cannot convert undefined or null to object
Object.getPrototypeOf(undefined)
Object.keys(),Object.values(),Object.entries()
Object.fromEntries()
Object.fromEntries()
方法是Object.entries()
的逆操作,用于将一个键值对数组转为对象。
该方法的主要目的,是将键值对的数据结构还原为对象,因此特别适合将 Map 结构转为对象。
// 例一
const entries = new Map([
['foo', 'bar'],
['baz', 42]
]);
Object.fromEntries(entries)
// { foo: "bar", baz: 42 }
// 例二
const map = new Map().set('foo', true).set('bar', false);
Object.fromEntries(map)
// { foo: true, bar: false }
该方法的一个用处是配合URLSearchParams
对象,将查询字符串转为对象。
Object.fromEntries(new URLSearchParams('foo=bar&baz=qux'))
// { foo: "bar", baz: "qux" }
1 函数
1.1函数的length属性
length
属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,后文的 rest 参数也不会计入length
属性。
(function (a) {}).length // 1
(function (a = 5) {}).length // 0 指定默认值以后,预期传入的参数个数就不包括这个参数了
(function (a, b, c = 5) {}).length // 2
(function(...args) {}).length // 0 rest 参数也不会计入length属性
(function (a = 0, b, c) {}).length // 0 如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。
(function (a, b = 1, c) {}).length // 1
1.2 参数默认值的位置
通常情况下,定义了默认值的参数,应该是函数的尾参数,因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。
// 例一
function f(x = 1, y) {
return [x, y];
}
f() // [1, undefined]
f(2) // [2, undefined])
f(, 1) // 报错
f(undefined, 1) // [1, 1]
function foo(x = 5, y = 6) {
console.log(x, y);
}
foo(undefined, null) //如果传入undefined,将触发该参数等于默认值,null则没有这个效果。
// 5 null
1.3 作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
var x = 1;
//参数y的默认值等于变量x。调用函数f时,参数形成一个单独的作用域。在这个作用域里面,默认值变量x指向第一个参数x,而不是全局变量x,所以输出是2。
function f(x, y = x) {
console.log(y);
}
f(2) // 2
let x = 1;
//函数f调用时,参数y = x形成一个单独的作用域。这个作用域里面,变量x本身没有定义,所以指向外层的全局变量x。函数调用时,函数体内部的局部变量x影响不到默认值变量x。
function f(y = x) {
let x = 2;
console.log(y);
}
f() // 1
1.4 rest参数
ES6 引入 rest 参数(形式为...变量名
),用于获取函数的多余参数,这样就不需要使用arguments
对象了。rest 参数搭配的变量是一个数组
注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
function push(array, ...items) {
items.forEach(function(item) {
array.push(item);
console.log(item);
});
}
var a = [];
push(a, 1, 2, 3)
(function(a, ...b) {}).length // 1 函数的length属性,不包括 rest 参数。
1.5 严格模式
ES2016 规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式
这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。
// 报错 参数value的默认值是八进制数070,但是严格模式下不能用前缀0表示八进制,所以应该报错
function doSomething(value = 070) {
'use strict';
return value;
}
两种方法可以规避这种限制。第一种是设定全局性的严格模式,这是合法的。
'use strict';
function doSomething(a, b = a) {
// code
}
第二种是把函数包在一个无参数的立即执行函数里面。
const doSomething = (function () {
'use strict';
return function(value = 42) {
return value;
};
}());
1.6 箭头函数
箭头函数有几个使用注意点。
-
(1)函数体内的
this
对象,就是定义时所在的对象,而不是使用时所在的对象。 -
(2)不可以当作构造函数,也就是说,不可以使用
new
命令,否则会抛出一个错误。 -
(3)不可以使用
arguments
对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。 -
(4)不可以使用
yield
命令,因此箭头函数不能用作 Generator 函数。由于箭头函数没有自己的
this
,所以当然也就不能用call()
、apply()
、bind()
这些方法去改变this
的指向。function foo() { return () => { return () => { return () => { console.log('id:', this.id); }; }; }; } //只有一个this,就是函数foo的this,所以t1、t2、t3都输出同样的结果。因为所有的内层函数都是箭头函数,都没有自己的this,它们的this其实都是最外层foo函数的this。 var f = foo.call({id: 1}); //由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向。 var t1 = f.call({id: 2})()(); // id: 1 var t2 = f().call({id: 3})(); // id: 1 var t3 = f()().call({id: 4}); // id: 1
不适合场景
由于箭头函数使得this
从“动态”变成“静态”,下面两个场合不应该使用箭头函数。
- 第一个场合是定义对象的方法,且该方法内部包括
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.7 尾调用优化 Tail call optimization
什么是尾调用?
尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。
function f(x){
return g(x);//函数f的最后一步是调用函数g,这就叫尾调用。
}
//尾调用不一定出现在函数尾部,只要是最后一步操作即可。
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);
}
//函数m和n都属于尾调用,因为它们都是函数f的最后一步操作。
以下三种情况,都不属于尾调用。
// 情况一
function f(x){
let y = g(x);
return y;
}
// 情况二
function f(x){
return g(x) + 1;
}
// 情况三
function f(x){
g(x);
}
尾调用优化
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。
尾递归优化只在严格模式下生效
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同于
function f() {
return g(3);
}
f();
// 等同于
g(3);
这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。
注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。
function addOne(a){
var one = 1;
function inner(b){
return b + one;
}
return inner(a);//函数不会进行尾调用优化,因为内层函数inner用到了外层函数addOne的内部变量one。
}
尾递归
函数调用自身,称为递归。如果尾调用自身,就称为尾递归.
ES6 中只要使用尾递归,就不会发生栈溢出(或者层层递归造成的超时),相对节省内存。
非尾递归的 Fibonacci 数列实现如下。
function Fibonacci (n) {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
Fibonacci(10) // 89
Fibonacci(100) // 超时
Fibonacci(500) // 超时
尾递归优化过的 Fibonacci 数列实现如下。
尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity
1.8 新特性
修改后的toString()
方法,明确要求返回一模一样的原始代码。以前会省略注释和空格。
function /* foo comment */ foo () {}
foo.toString()
// "function /* foo comment */ foo () {}"
-
try...catch
结构,以前明确要求catch
命令后面必须跟参数,接受try
代码块抛出的错误对象。ES2019 做出了改变,允许catch
语句省略参数。try { // ... } catch { // ... }
2、Proxy
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。
var proxy = new Proxy(target, handler);
Proxy 支持的拦截操作一览,一共 13 种。
-
get(target, propKey, receiver):拦截对象属性的读取,比如
proxy.foo
和proxy['foo']
。 -
set(target, propKey, value, receiver):拦截对象属性的设置,比如
proxy.foo = v
或proxy['foo'] = v
,返回一个布尔值。 -
has(target, propKey):拦截
propKey in proxy
的操作,返回一个布尔值。 -
deleteProperty(target, propKey):拦截
delete proxy[propKey]
的操作,返回一个布尔值。 -
ownKeys(target):拦截
Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
、for...in
循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()
的返回结果仅包括目标对象自身的可遍历属性。 -
getOwnPropertyDescriptor(target, propKey):拦截
Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。 -
defineProperty(target, propKey, propDesc):拦截
Object.defineProperty(proxy, propKey, propDesc)
、Object.defineProperties(proxy, propDescs)
,返回一个布尔值。 -
preventExtensions(target):拦截
Object.preventExtensions(proxy)
,返回一个布尔值。 -
getPrototypeOf(target):拦截
Object.getPrototypeOf(proxy)
,返回一个对象。 -
isExtensible(target):拦截
Object.isExtensible(proxy)
,返回一个布尔值。 -
setPrototypeOf(target, proto):拦截
Object.setPrototypeOf(proxy, proto)
,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 -
apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如
proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。 -
construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如
new proxy(...args)
。
2 Reflect
Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。
(1) 将Object
对象的一些明显属于语言内部的方法(比如Object.defineProperty
),放到Reflect
对象上。现阶段,某些方法同时在Object
和Reflect
对象上部署,未来的新方法将只部署在Reflect
对象上。也就是说,从Reflect
对象上可以拿到语言内部的方法。
(2) 修改某些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
}
(3) 让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
(4)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;
}
});
上面代码中,Proxy方法拦截target对象的属性赋值行为。它采用Reflect.set方法将值赋值给对象的属性,确保完成原有的行为,然后再部署额外的功能。
下面是另一个例子。
var loggedObj = new Proxy(obj, {
get(target, name) {
console.log('get', target, name);
return Reflect.get(target, name);
},
deleteProperty(target, name) {
console.log('delete' + name);
return Reflect.deleteProperty(target, name);
},
has(target, name) {
console.log('has' + name);
return Reflect.has(target, name);
}
});
上面代码中,每一个Proxy对象的拦截操作(get、delete、has),内部都调用对应的Reflect方法,保证原生行为能够正常执行。添加的工作,就是将每一个操作输出一行日志。
有了Reflect对象以后,很多操作会更易读。
// 老写法
Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1
// 新写法
Reflect.apply(Math.floor, undefined, [1.75]) // 1
Reflect
对象一共有 13 个静态方法。
-
Reflect.apply(target, thisArg, args)
-
Reflect.construct(target, args)
-
Reflect.get(target, name, receiver)
-
Reflect.set(target, name, value, receiver)
-
Reflect.defineProperty(target, name, desc)
-
Reflect.deleteProperty(target, name)
Reflect.deleteProperty
方法等同于delete obj[name]
,用于删除对象的属性。const myObj = { foo: 'bar' }; // 旧写法 delete myObj.foo; // 新写法 Reflect.deleteProperty(myObj, 'foo');
该方法返回一个布尔值。如果删除成功,或者被删除的属性不存在,返回
true
;删除失败,被删除的属性依然存在,返回false
。
-
Reflect.has(target, name):
Reflect.has
方法对应name in obj
里面的in
运算符var myObject = { foo: 1, }; // 旧写法 'foo' in myObject // true // 新写法 Reflect.has(myObject, 'foo') // true
-
Reflect.ownKeys(target)
-
Reflect.isExtensible(target)
-
Reflect.preventExtensions(target)
-
Reflect.getOwnPropertyDescriptor(target, name)
-
Reflect.getPrototypeOf(target)
-
Reflect.setPrototypeOf(target, prototype)
上面这些方法的作用,大部分与Object
对象的同名方法的作用都是相同的,而且它与Proxy
对象的方法是一一对应的
3、async
async 函数是什么?一句话,它就是 Generator 函数的语法糖
await 命令
正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
任何一个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
另一种方法是await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。
async function f() {
await Promise.reject('出错了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// 出错了
// hello world
使用注意点
-
第一点,前面已经说过,
await
命令后面的Promise
对象,运行结果可能是rejected
,所以最好把await
命令放在try...catch
代码块中。 -
第二点,多个
await
命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
let foo = await getFoo();
let bar = await getBar();
//getFoo和getBar是两个独立的异步操作(即互不依赖),这样比较耗时,因为只有getFoo完成以后,才会执行getBar
以下两种写法,getFoo
和getBar
都是同时触发
//getFoo和getBar都是同时触发,这样就会缩短程序的执行时间
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
- 第三点,await命令只能用在async函数之中,如果用在普通函数,就会报错。
如果确实希望多个请求并发执行,可以使用Promise.all方法。当三个请求都会resolved时,下面两种写法效果相同。
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = await Promise.all(promises);
console.log(results);
}
// 或者使用下面的写法
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = [];
for (let promise of promises) {
results.push(await promise);
}
console.log(results);
}
- 第四点,async 函数可以保留运行堆栈。
const a = async () => {
await b();
c();
};
上面代码中,b()运行的时候,a()是暂停执行,上下文环境都保存着。一旦b()或c()报错,错误堆栈将包括a()。
async 函数的实现原理 § [⇧](http://es6.ruanyifeng.com/#docs/async
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {//spawn函数就是自动执行器
// ...
});
}
3.1并发执行
(async () => {
const listPromise = getList();
const anotherListPromise = getAnotherList();
await listPromise;
await anotherListPromise;
})();
也可以使用 Promise.all():
(async () => {
Promise.all([getList(), getAnotherList()]).then(...);
})();
3.2继发与并发
问题:给定一个 URL 数组,如何实现接口的继发和并发?
async 继发实现:
// 继发一
async function loadData() {
var res1 = await fetch(url1);
var res2 = await fetch(url2);
var res3 = await fetch(url3);
return "whew all done";
}
// 继发二
async function loadData(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}
async 并发实现:
// 并发一
async function loadData() {
var res = await Promise.all([fetch(url1), fetch(url2), fetch(url3)]);
return "whew all done";
}
// 并发二
async function loadData(urls) {
// 并发读取 url
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
// 按次序输出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
3.3async 的一些讨论:
async 会取代 Generator 吗?
Generator 本来是用作生成器,使用 Generator 处理异步请求只是一个比较 hack 的用法,在异步方面,async 可以取代 Generator,但是 async 和 Generator 两个语法本身是用来解决不同的问题的。
async 会取代 Promise 吗?
- async 函数返回一个 Promise 对象
- 面对复杂的异步流程,Promise 提供的 all 和 race 会更加好用
- Promise 本身是一个对象,所以可以在代码中任意传递
- async 的支持率还很低,即使有 Babel,编译后也要增加 1000 行左右。
4、set,weakset和Map,weakmap
4.1 set weakset
set
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
方法与属性:constructor,size,add(),has(),delete(),clear()
遍历操作:keys(),values(),entries(),forEach()
WeakSet
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。
首先,WeakSet 的成员只能是对象,而不能是其他类型的值。
其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
WeakSet 结构有以下三个方法。
- WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
- WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
- WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中。
WeakSet 不能遍历,是因为成员都是弱引用,随时可能消失,遍历机制无法保证成员的存在,很可能刚刚遍历结束,成员就取不到了。WeakSet 的一个用处,是储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏
WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失
下面是 WeakSet 的另一个例子。
const foos = new WeakSet()
class Foo {
constructor() {
foos.add(this)
}
method () {
if (!foos.has(this)) {
throw new TypeError('Foo.prototype.method 只能在Foo的实例上调用!');
}
}
}
上面代码保证了Foo的实例方法,只能在Foo的实例上调用。这里使用 WeakSet 的好处是,foos对实例的引用,不会被计入内存回收机制,所以删除实例的时候,不用考虑foos,也不会出现内存泄漏。
4.2 Map weakMap
Map
Object 结构提供了**“字符串—值”的对应,Map 结构提供了“值—值”**的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。
Map 结构的实例有以下属性和操作方法
size set() get() has() delete() clear()
遍历方法
keys() values() entries() forEach()
Map 结构转为数组结构,比较快速的方法是使用扩展运算符(...
)。
WeakMap
WeakMap
与Map
的区别有两点。
首先,WeakMap
只接受对象作为键名(null
除外),不接受其他类型的值作为键名。
其次,WeakMap
的键名所指向的对象,不计入垃圾回收机制。
总之,WeakMap
的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap
结构有助于防止内存泄漏。
注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。
WeakMap 与 Map 在 API 上的区别主要是两个,一是没有遍历操作(即没有keys()
、values()
和entries()
方法),也没有size
属性。
二是无法清空,即不支持clear
方法。因此,WeakMap
只有四个方法可用:get()
、set()
、has()
、delete()
。
5.promise
Promise
对象有以下两个特点。
(1)对象的状态不受外界影响。Promise
对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise
这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
(2**)一旦状态改变,就不会再变**,任何时候都可以得到这个结果。Promise
对象的状态改变,只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise
对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
Promise也有一些缺点:
首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。
其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
如果某些事件不断地反复发生,一般来说,使用 Stream 模式是比部署Promise更好的选择。
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1
///一般来说,调用resolve或reject以后,Promise 的使命就完成了,最好在它们前面加上return语句
new Promise((resolve, reject) => {
return resolve(1);
// 后面的语句不会执行
console.log(2);
})
上面代码中,调用resolve(1)
以后,后面的console.log(2)
还是会执行,并且会首先打印出来。这是因为立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。
Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。
Promise.prototype.then()
Promise.prototype.catch()
Promise.prototype.finally()
finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
上面代码中,不管promise
最后的状态,在执行完then
或catch
指定的回调函数以后,都会执行finally
方法指定的回调函数。
finally
方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled
还是rejected
。这表明,finally
方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
finally
本质上是then
方法的特例。
promise
.finally(() => {
// 语句
});
// 等同于
promise
.then(
result => {
// 语句
return result;
},
error => {
// 语句
throw error;
}
);
Promise.all()
Promise.all
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
p
的状态由p1
、p2
、p3
决定,分成两种情况。
(1)只有p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数。
(2)只要p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p
的回调函数
Promise.race()
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
Promise.resolve()
Promise.reject()
Promise.try()
Promise.try(() => database.users.get({id: userId}))
.then(...)
.catch(...)
事实上,Promise.try就是模拟try代码块,就像promise.catch模拟的是catch代码块。
try可以实现同步函数同步执行,异步函数异步执行
6 Iterator(遍历器) 和 for … of
6.1 js数据结构四种:
数组Array,对象Object,Set,Map
需要一种统一的接口机制,来处理所有不同的数据结构。
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
6.2 Iterator 的作用有三个:
- 一是为各种数据结构,提供一个统一的、简便的访问接口;
- 二是使得数据结构的成员能够按某种次序排列;
- 三是 ES6 创造了一种新的遍历命令
for...of
循环,Iterator 接口主要供for...of
消费。
当使用for...of
循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。
for...of
循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments
对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。
ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator
属性,或者说,一个数据结构只要具有Symbol.iterator
属性,就可以认为是“可遍历的”(iterable)。
const obj = {
[Symbol.iterator] : function () {
return {
next: function () {
return {
value: 1,
done: true
};
}
};
}
};
如果Symbol.iterator
方法对应的不是遍历器生成函数(即会返回一个遍历器对象),解释引擎将会报错。
6.3 原生具备 Iterator 接口的数据结构如下。
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
6.4对象Object不具备Iterator 接口
对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作 Map 结构使用,ES5 没有 Map 结构,而 ES6 原生提供了
对于普通的对象,for...of
结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。但是,这样情况下,for...in
循环依然可以用来遍历键名。
let es6 = {
edition: 6,
committee: "TC39",
standard: "ECMA-262"
};
for (let e in es6) {
console.log(e);
}
// edition
// committee
// standard
for (let e of es6) {
console.log(e);
}
// TypeError: es6[Symbol.iterator] is not a function
上面代码表示,对于普通的对象,for...in
循环可以遍历键名,for...of
循环会报错。
一种解决方法是,使用Object.keys方法将对象的键名生成一个数组,然后遍历这个数组。
for (var key of Object.keys(someObject)) {
console.log(key + ': ' + someObject[key]);
}
另一个方法是使用 Generator 函数将对象重新包装一下。
function* entries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}
for (let [key, value] of entries(obj)) {
console.log(key, '->', value);
}
// a -> 1
// b -> 2
// c -> 3
6.5 调用Iterator的场景
(1)解构赋值
对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator方法。
let set = new Set().add('a').add('b').add('c');
let [x,y] = set;
// x='a'; y='b'
let [first, ...rest] = set;
// first='a'; rest=['b','c'];
(2)扩展运算符
扩展运算符(…)也会调用默认的 Iterator 接口。
// 例一
var str = 'hello';
[...str] // ['h','e','l','l','o']
// 例二
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']
上面代码的扩展运算符内部就调用 Iterator 接口。
实际上,这提供了一种简便机制,可以将任何部署了 Iterator 接口的数据结构,转为数组。也就是说,只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组。
let arr = [...iterable];
(3)yield*
yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
let generator = function* () {
yield 1;
yield* [2,3,4];
yield 5;
};
var iterator = generator();
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }
(4)其他场合
由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。
-
for…of
-
Array.from()
-
Map(), Set(), WeakMap(), WeakSet()
(比如new Map([[‘a’,1],[‘b’,2]]))
-
Promise.all()
-
Promise.race()
6.6 与其他遍历方法比较
以数组为例,JavaScript 提供多种遍历语法。
-
for循环
-
forEach
-
for …in
-
for…of :
for...of
循环用于遍历同步的 Iterator 接口
最原始的写法就是for
循环,
写法比较麻烦,因此引入了forEach()
无法中途跳出forEach
循环,break
命令或return
命令都不能奏效。
for...in
循环有几个缺点。
- 数组的键名是数字,但是
for...in
循环是以字符串作为键名“0”、“1”、“2”等等。 for...in
循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。- 某些情况下,
for...in
循环会以任意顺序遍历键名。
总之,for...in
循环主要是为遍历对象而设计的,不适用于遍历数组。
for...of
循环相比上面几种做法,有一些显著的优点。
- 有着同
for...in
一样的简洁语法,但是没有for...in
那些缺点。 - 不同于
forEach
方法,它可以与break
、continue
和return
配合使用。 - 提供了遍历所有数据结构的统一操作接口。
for (var n of fibonacci) {
if (n > 1000)
break;//如果当前项大于 1000,就会使用break语句跳出for...of循环。
console.log(n);
}
6.6 异步遍历器
ES2018 引入了“异步遍历器”(Async Iterator),为异步操作提供原生的遍历器接口,即value
和done
这两个属性都是异步产生。
异步遍历器的最大的语法特点,就是调用遍历器的next方法,返回的是一个 Promise 对象。
asyncIterator
.next()
.then(
({ value, done }) => /* ... */
);
新引入的for await...of
循环,则是用于遍历异步的 Iterator 接口。
注意,for await...of
循环也可以用于同步遍历器。
7 Generator 函数
Generator 函数是 ES6 提供的一种异步编程解决方案
yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
//第二次运行next方法的时候不带参数,导致 y 的值等于2 * undefined(即NaN),除以 3 以后还是NaN,因此返回对象的value属性也等于NaN。
//第三次运行Next方法的时候不带参数,所以z等于undefined,返回对象的value属性等于5 + NaN + undefined,即NaN
var b = foo(5);
b.next() // { value:6, done:false }//第一次调用b的next方法时,返回x+1的值6;
b.next(12) // { value:8, done:false }//第二次调用next方法,将上一次yield表达式的值设为12,因此y等于24,返回y / 3的值8
b.next(13) // { value:42, done:true }//第三次调用next方法,将上一次yield表达式的值设为13,因此z等于13,这时x等于5,y等于24,所以return语句的值等于42。
注意,由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。
next()
、throw()
、return()
这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield
表达式。
next()
是将yield
表达式替换成一个值。
const g = function* (x, y) {
let result = yield x + y;
return result;
};
const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}
gen.next(1); // Object {value: 1, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = 1;
throw()
是将yield
表达式替换成一个throw
语句。
gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));
return()
是将yield
表达式替换成一个return
语句。
gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;
异步
- 回调函数
- 事件监听
- 发布/订阅
- Promise 对象
- Generator 函数
- async/await
8 class
Es6 类的内部所有定义的方法,都是不可枚举的(non-enumerable)。
class Point {
constructor(x, y) {
// ...
}
toString() {
// ...
}
}
Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
toString
方法是Point
类内部定义的方法,它是不可枚举的。这一点与 ES5 的行为不一致。
var Point = function (x, y) {
// ...
};
Point.prototype.toString = function() {
// ...
};
Object.keys(Point.prototype)
// ["toString"]
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
上面代码采用 ES5 的写法,toString
方法就是可枚举的。
类必须使用new调用,否则会报错。
静态方法
在一个方法前,加上static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
class Foo {
static classMethod() {
return 'hello';
}
static bar() {
this.baz();//this指的是类,而不是实例。
}
static baz() {//静态方法可以与非静态方法重名。
console.log('hello');
}
baz() {
console.log('world');
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()//静态方法不能通过实例访问
// TypeError: foo.classMethod is not a function
class Bar extends Foo {
static baz() {
return super.baz() + ', too';//静态方法也是可以从super对象上调用的。
}
}
Bar.classMethod();//hello //父类的静态方法,可以被子类继承
Bar.baz();//hello too
注意,如果静态方法包含this
关键字,这个this
指的是类,而不是实例。
私有方法和属性
1、命名上加以区别
class Widget {
// 公有方法
foo (baz) {
this._bar(baz);
}
// 私有方法
_bar(baz) {
return this.snaf = baz;
}
// ...
}
2、将私有方法移出模块
class Widget {
foo (baz) {
bar.call(this, baz);
}
// ...
}
function bar(baz) {
return this.snaf = baz;
}
3、利用Symbol
值的唯一性,将私有方法的名字命名为一个Symbol
值
const bar = Symbol('bar');
const snaf = Symbol('snaf');
export default class myClass{
// 公有方法
foo(baz) {
this[bar](baz);
}
// 私有方法
[bar](baz) {
return this[snaf] = baz;
}
// ...
};
//也不是绝对不行,Reflect.ownKeys()依然可以拿到它们。
const inst = new myClass();
Reflect.ownKeys(myClass.prototype)
// [ 'constructor', 'foo', Symbol(bar) ]
4、使用#
表示
class Foo {
#a;
#b;
constructor(a, b) {
this.#a = a;
this.#b = b;
}
#sum() {
return #a + #b;
}
printSum() {
console.log(this.#sum());
}
}
const f = new Foo();
f.#a // 报错
f.#a = 42 // 报错
new.target
Class 内部调用new.target
,返回当前 Class。
利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error('本类不能实例化');
}
}
}
class Rectangle extends Shape {
constructor(length, width) {
super();
// ...
}
}
var x = new Shape(); // 报错
var y = new Rectangle(3, 4); // 正确
需要注意的是,子类继承父类时,new.target
会返回子类。
注意,在函数外部,使用new.target
会报错。
Object.getPrototypeOf() § ⇧
Object.getPrototypeOf
方法可以用来从子类上获取父类。
Object.getPrototypeOf(ColorPoint) === Point
// true
因此,可以使用这个方法判断,一个类是否继承了另一个类。
继承
Class 可以通过extends
关键字实现继承
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
//在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错,这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
super关键字
super
这个关键字,既可以当作函数使用,也可以当作对象使用
第一种情况,super
作为函数调用时,代表父类的构造函数。
作为函数时,super()
只能用在子类的构造函数之中,用在其他地方就会报错。
class A {}
class B extends A {
constructor() {
super();//注意,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B的实例,因此super()在这里相当于A.prototype.constructor.call(this)。
}
}
第二种情况,super
作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
ES6 规定,在子类普通方法中通过super
调用父类的方法时,方法内部的this
指向当前的子类实例。
class A {
constructor() {
this.x = 1;
}
print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3;//super.x赋值为3,这时等同于对this.x赋值为3
console.log(super.x); // undefined 当读取super.x的时候,读的是A.prototype.x,所以返回undefined
}
m() {
super.print();//this->b super.print()相当于A.prototype.print()
}
}
let b = new B();
b.m() // 3
注意,使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。
类的 prototype 属性和__proto__属性
(1)子类的__proto__
属性,表示构造函数的继承,总是指向父类。
(2)子类prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的prototype
属性。
class A {
}
class B extends A {
}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
这样的结果是因为,类的继承是按照下面的模式实现的。
class A {
}
class B {
}
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于 B.prototype.__proto__ = A.prototype;
// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);
//等同于 B.__proto__ === A
const b = new B();
Object.setPrototypeOf = function (obj, proto) {
obj.__proto__ = proto;
return obj;
}
不存在继承:
class A {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true
子类实例的__proto__
属性的__proto__
属性,指向父类实例的__proto__
属性。也就是说,子类的原型的原型,是父类的原型。
var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');
p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true
//等价于
ColorPoint.prototype.__proto__ === Point.prototype
- Boolean()
- Number()
- String()
- Array()
- Date()
- Function()
- RegExp()
- Error()
- Object()
ES5这些原生构造函数是无法继承的,
function MyArray() {
Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
});
之所以会发生这种情况,是因为子类无法获得原生构造函数的内部属性,通过Array.apply()
或者分配给原型对象都不行。原生构造函数会忽略apply
方法传入的this
,也就是说,原生构造函数的this
无法绑定,导致拿不到内部属性。
ES5 是先新建子类的实例对象this
,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。比如,Array
构造函数有一个内部属性[[DefineOwnProperty]]
,用来定义新属性时,更新length
属性,这个内部属性无法在子类获取,导致子类的length
属性行为不正常。
ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象this
,然后再用子类的构造函数修饰this
,使得父类的所有行为都可以继承。下面是一个继承Array
的例子。
class MyArray extends Array {
constructor(...args) {
super(...args);
}
}
var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined
Mixin
Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口
const a = {
a: 'a'
};
const b = {
b: 'b'
};
const c = {...a, ...b}; // {a: 'a', b: 'b'}
//c对象是a对象和b对象的合成,具有两者的接口
将多个类的接口“混入”(mix in)另一个类。
function mix(...mixins) {
class Mix {
constructor() {
for (let mixin of mixins) {
copyProperties(this, new mixin()); // 拷贝实例属性
}
}
}
for (let mixin of mixins) {
copyProperties(Mix, mixin); // 拷贝静态属性
copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
}
return Mix;
}
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !== 'constructor'
&& key !== 'prototype'
&& key !== 'name'
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
//mix函数,可以将多个对象合成为一个类。使用的时候,只要继承这个类即可
class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}
9 Module
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。都只能在运行时确定
CommonJS : 运行时加载
AMD: 运行时加载,异步的 requirejs
CMD: 运行时加载 依赖就近,同步的 seajs
ES6 模块: “编译时加载”或者静态加载 ,在编译时就完成模块加载, 设计思想是尽量的静态化
尤其需要注意this
的限制。ES6 模块之中,顶层的this
指向undefined
,即不应该在顶层代码使用this
。
Export
模块功能主要由两个命令构成:export
和import
。export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能。
// 报错
export 1;
// 报错
var m = 1;
export m;
// 正确 写法一
export var m = 1;
// 正确 写法二
var m = 1;
export {m};
// 正确 写法三
var n = 1;
export {n as m};
// 正确
export function f() {};
// 正确
function f() {}
export {f};
export
命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import
命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷
function foo() {
export default 'bar' // SyntaxError
}
foo()
-
export default
命令用于指定模块的默认输出,一个模块只能有一个默认输出,
import
- 注意,
import
命令具有提升效果,会提升到整个模块的头部,首先执行
foo();
import { foo } from 'my_module';
上面的代码不会报错,因为import的执行早于foo的调用。这种行为的本质是,import命令是编译阶段执行的,在代码运行之前。
- 由于
import
是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
// 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;
// 报错
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
- 如果
import
语句会执行所加载的模块,因此可以有下面的写法。
import 'lodash';
- import语句是 Singleton 模式。
import 'lodash';
import 'lodash';
//上面代码加载了两次lodash,但是只会执行一次。
//虽然foo和bar在两个语句中加载,但是它们对应的是同一个my_module实例。也就是说,import语句是 Singleton 模式。
import { foo } from 'my_module';
import { bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module';
-
使用整体加载,即用星号(
*
)指定一个对象import * as circle from './circle';
//import语句可以与export语句写在一起。
export { foo, bar } from 'my_module';
// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };
import()
引入import()
函数,完成动态加载。
const main = document.querySelector('main');
//import()返回一个 Promise 对象
import(`./section-modules/${someVariable}.js`)
.then(module => {
module.loadPageInto(main);
})
.catch(err => {
main.textContent = err.message;
});
import()
函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。另外,import()
函数与所加载的模块没有静态连接关系,这点也是与import
语句不相同。
import()
类似于 Node 的require
方法,区别主要是前者是异步加载,后者是同步加载。
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(...);
}
上面代码中,如果满足条件,就加载模块 A,否则加载模块 B。
- (3)动态的模块路径
import()
允许模块路径动态生成。
import(f())
.then(...);
上面代码中,根据函数f
的返回结果,加载不同的模块。
注意点:
//1 import()也可以用在 async 函数之中。
async function main() {
const myModule = await import('./myModule.js');
const {export1, export2} = await import('./myModule.js');//2 使用对象解构赋值的语法,获取输出接口
const [module1, module2, module3] =
await Promise.all([//3 同时加载多个模块
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
]);
}
main();
js文件异步加载 defer
和async
defer
与async
的区别是:defer
要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async
一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer
是“渲染完再执行”,async
是“下载完就执行”。另外,如果有多个defer
脚本,会按照它们在页面出现的顺序加载,而多个async
脚本是不能保证加载顺序的
-
ES6模块之中,顶层的
this
关键字返回undefined
,而不是指向window
。也就是说,在模块顶层使用this
关键字,是无意义的。利用顶层的
this
等于undefined
这个语法点,可以侦测当前代码是否在 ES6 模块之中。const isNotModuleScript = this !== undefined;
以下顶层变量在 ES6 模块之中都是不存在的。
arguments
require
module
exports
__filename
__dirname
ES6 模块与 CommonJS 模块的差异
它们有两个重大差异。
-
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
-
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
CommonJS 模块的顶层
this
指向当前模块,
CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
get counter2() {
return counter
},
};
// main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3 因为mod.counter是一个原始类型的值,会被缓存。
console.log(mod.counter2); // 4 //通过取值器函数动态获取值
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 模块输入的变量counter是活的,完全反应其所在模块lib.js内部的变化。
Es6模块加载CommonJs模块
由于 ES6 模块是编译时确定输出接口,CommonJS 模块是运行时确定输出接口,所以采用import
命令加载 CommonJS 模块时,要整体输入
// 正确的写法一
import * as express from 'express';
const app = express.default();
// 正确的写法二
import express from 'express';
const app = express();
CommonJS 模块加载 ES6 模块
CommonJS 模块加载 ES6 模块,不能使用require
命令,而要使用import()
函数。ES6 模块的所有输出接口,会成为输入对象的属性。
// es.js
export let foo = { bar:'my-default' };
export { foo as bar };
export function f() {};
export class c {};
export default f2() {};
// cjs.js
const es_namespace = await import('./es');
// es_namespace = {
// get foo() {return foo;}
// get bar() {return foo;}
// get f() {return f;}
// get c() {return c;}
// get default() { } //default接口变成了es_namespace.default属性
// }
循环加载
“循环加载”(circular dependency)指的是,a
脚本的执行依赖b
脚本,而b
脚本的执行又依赖a
脚本。
CommonJS模块加载原理:
CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果
CommonJS 的一个模块,就是一个脚本文件。require
命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。
CommonJ模块的循环加载
CommonJS 模块的重要特性是加载时执行,即脚本代码在require
的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
//a.js
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
//b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');
//main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
//----------------------结果
//在 b.js 之中,a.done = false
//b.js 执行完毕
//在 a.js 之中,b.done = true
//a.js 执行完毕
//在 main.js 之中, a.done=true, b.done=true
一是,在b.js之中,a.js没有执行完毕,只执行了第一行。
二是,main.js执行到第二行时,不会再次执行b.js,而是输出缓存的b.js的执行结果,即它的第四行。exports.done = true;
ES6 模块的循环加载
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
function foo() { return 'foo' }//通过将foo写成函数来解决。因为函数具有提升作用
export {foo};
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo());
function bar() { return 'bar' }
export {bar};
10 装饰器
@testable
class MyTestableClass {
// ...
}
function testable(target) {
target.isTestable = true;
}
MyTestableClass.isTestable // true
注意,装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,装饰器能在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数。
装饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。
11 编程风格
-
块级作用域
-
let取代var:
let
没有副作用,不存在变量提升 -
const优于let:
-
一个是
const
可以提醒阅读程序的人,这个变量不应该改变; -
另一个是
const
比较符合函数式编程思想,运算不改变值,只是新建值,而且这样也有利于将来的分布式运算; -
最后一个原因是 JavaScript 编译器会对
const
进行优化,所以多使用const
,有利于提高程序的运行效率,也就是说let
和const
的本质区别,其实是编译器内部的处理不同。
-
-
-
字符串
静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。
-
解构赋值
- 使用数组成员对变量赋值时,优先使用解构赋值。
- 函数的参数如果是对象的成员,优先使用解构赋值。
- 如果函数返回多个值,优先使用对象的解构赋值,而不是数组的解构赋值。这样便于以后添加返回值,以及更改返回值的顺序。
-
函数
-
简单的、单行的、不会复用的函数,建议采用箭头函数。如果函数体较为复杂,行数较多,还是应该采用传统的函数写法。
-
那些使用匿名函数当作参数的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了 this。
箭头函数取代
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);
-
不要在函数体内使用 arguments 变量,使用 rest 运算符(…)代替。因为 rest 运算符显式表明你想要获取参数,而且 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 = {}) { // ... }
-
function foo(x = 5, y = 6) {
console.log(x, y);
}
foo(undefined, null)//x参数对应undefined,结果触发了默认值,y参数等于null,就没有触发默认值。
// 5 null
```
一个容易忽略的地方是,参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,**参数默认值是惰性求值的**。
```javascript
let x = 99;
function foo(p = x + 1) {//参数p的默认值是x + 1。这时,每次调用函数foo,都会重新计算x + 1
console.log(p);
}
foo() // 100
x = 100;
foo() // 101
```
-
数组
- 使用扩展运算符(…)拷贝数组。
- 使用 Array.from 方法,将类似数组的对象转为数组。
-
Map 结构
注意区分 Object 和 Map,只有模拟现实世界的实体对象时,才使用 Object。如果只是需要
key: value
的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。 -
Class
-
总是用 Class,取代需要 prototype 的操作。因为 Class 的写法更简洁,更易于理解。
-
使用extends实现继承,因为这样更简单,不会有破坏instanceof运算的危险
-
-
模块
-
使用
import
取代require
。 -
使用
export
取代module.exports
。 -
如果模块默认输出一个对象,对象名的首字母应该大写。
-
如果模块默认输出一个函数,函数名的首字母应该小写。
-