ES6简介
- ES6既是一个历史名词,也是一个泛指,含义是5.1版以后的JavaScript的下一代标准,涵盖了ES2015、ES2016、ES2017等等;
- ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现(另外的 ECMAScript 方言还有 Jscript 和 ActionScript);
let和const命令
- let和const声明的变量只在自己的块作用域里有效:
var a = [];
//每一轮循环,i都是相同的变量
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
//每一轮循环,i都是一个新的变量
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
- let和const不存在变量提升:
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况,提前使用后面声明的变量会报错
console.log(bar); // 报错ReferenceError
let bar = 2;
- let和const的暂时性死区:
var tmp = 123;
if (true) {
//只要使用const和let声明的变量,在该作用域里形成了封闭作用域,不受外部同名变量的影响
tmp = 'abc'; // ReferenceError
let tmp;
}
// 不报错
var x = x;
// 报错
let x = x;
// ReferenceError: x is not defined
- let和const声明的变量不容许重复声明:
// 报错
function () {
let a = 10;
var a = 1;
}
// 报错
function () {
let a = 10;
let a = 1;
}
5.const声明一个只读的变量,声明时必须赋值,const实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址不得改动
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only
6.ES6中,var命令和function命令声明的全局变量,依旧是顶层对象(global,window)的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从ES6开始,全局变量将逐步与顶层对象的属性脱钩。
7.global对象:
- 浏览器里面,顶层对象是window,但 Node 和 Web Worker 没有window。
- 浏览器和 Web Worker 里面,self也指向顶层对象,但是Node没有self。
- Node 里面,顶层对象是global,但其他环境都不支持。
解构赋值
- 数组的解构赋值
//普通解构
let [a, b, c] = [1, 2, 3];
//嵌套解构
let [foo, [[bar], baz]] = [1, [[2], 3]];
//部分解构
let [head, ...tail] = [1, 2, 3, 4];
=> head // 1
=> tail // [2, 3, 4]
//只要某种数据结构具有Iterator接口,都可以采用数组形式的解构赋值
function* fibs() {
let a = 0;
let b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
let [first, second, third, fourth, fifth, sixth] = fibs();
=> sixth // 5
//不具有Iterator接口的数据结构,使用数组解构会报错
let [foo] = NaN;
let [foo] = undefined;
//Set结构也可以使用解构
let [x, y, z] = new Set(['a', 'b', 'c']);
//解构时可以添加默认值
let [foo = true] = [];
=> foo // true
let [x = 'c', y = 'b'] = ['a'];
=> // x='a', y='b'
let [x, y = 'b'] = ['a', undefined];
=> // x='a', y='b'
- 对象的解构赋值
//普通解构,变量名字必须与对象的属性名字保持一致
let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
//变量重命名,真正被赋值的是变量baz,而此时的foo只是一个匹配模式而已,而非变量
var { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
//解决变量重名问题,表达式2必须用(),否则解析器会将其理解成代码块
let baz;
({bar: baz} = {bar: 1}); // 成功
//嵌套解构,此时的p不是变量,而是匹配模式
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"
//解构时可以添加默认值,默认值生效的条件是严格等于undefined
var {x = 3} = {};
x // 3
var {x, y = 5} = {x: 1};
x // 1
y // 5
- 字符串的解构
//字符串可以当作数组进行解构
const [a, ...b] = 'hello';
a // "h"
b // ["e", "l", "l", "o"]
- 其它类型数据解构
//解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象,然后解构
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
//由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
- 函数参数的解构
//函数参数的解构就当作普通传值就可以了
function add([x, y]){
return x + y;
}
add([1, 2]); // 3 相当于 let [x, y] = [1, 2];
//{x = -1, y = -1}表示有参数的情况下,默认值解构,{ x: 0, y: 0 }表示默认参数
function move({x = -1, y = -1} = { x: 0, y: 0 }) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, -1]
move({}); // [-1, -1]
move(); // [0, 0]
- 解构的圆括号问题
//变量声明语句中,不能带有圆括号,以下全部报错
let [(a)] = [1];
let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};
//赋值语句中,不能将整个模式,或嵌套模式中的一层,放在圆括号之中
({ p: a }) = { p: 42 };
([a]) = [5];
//可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号
[(b)] = [3]; // 正确
({ p: (d) } = {}); // 正确
[(parseInt.prop)] = [3]; // 正确
字符串的扩展
- 字符的 Unicode 表示法
//javascript采用Unicode表示法(\uxxxx)表示一个字符,但是该表示法只能表示\u0000~\uFFFF之间的字符,超过这个范围的字符必须用两个字节表示:
"\uD842\uDFB7"
// "?"
//用一个字节表示,识别错误
"\u20BB7"
// " 7"
//ES6对此作了改进,可以放在大括号里表示:
"\u{20BB7}"
// "?"
"\u{41}\u{42}\u{43}"
// "ABC"
//ES6中的字符串支持for...of循环遍历,并且可以正确识别大于0xFFFF的码点
var text = String.fromCodePoint(0x20BB7);
//普通的for循环无法识别
for (let i = 0; i < text.length; i++) {
console.log(text[i]);
}
// " "
// " "
//可以识别
for (let i of text) {
console.log(i);
}
// "?"
- includes/startsWith/endsWith 函数
- includes:返回布尔值,表示是否找到了参数字符串。
- startsWith:返回布尔值,表示参数字符串是否在源字符串的头部。
- endsWith:返回布尔值,表示参数字符串是否在源字符串的尾部。
//这三个方法都支持第二个参数,表示开始搜索的位置
var s = 'Hello world!';
s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true endsWith的行为与其他两个方法有所不同,它针对前n个字符
s.includes('Hello', 6) // false
- repeat 函数
repeat方法返回一个新字符串,表示将原字符串重复n次
'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""
- 模板字符串
//模板字符串中嵌入变量,需要将变量名写在${}之中
var x = 1;
var y = 2;
`${x} + ${y} = ${x + y}`
// "1 + 2 = 3"
`${x} + ${y * 2} = ${x + y * 2}`
// "1 + 4 = 5"
var obj = {x: 1, y: 2};
`${obj.x + obj.y}`
// 3
正则的扩展
- 正则的修饰符
- g修饰符:全局匹配
- i修饰符:不区分大小写
- m修饰符:区分换行符,^和$可以匹配每行的开始和结尾
- u修饰符(ES6):用来正确处理大于\uFFFF的Unicode字符
- y修饰符:y修饰符的作用与g修饰符类似,也是全局匹配。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义
var s = 'aaa_aa_a';
var r1 = /a+/g;
var r2 = /a+/y;
r1.exec(s) // ["aaa"]
r2.exec(s) // ["aaa"]
r1.exec(s) // ["aa"]
r2.exec(s) // null
- 新的属性
//sticky属性:表示是否设置了y修饰符
var r = /hello\d/y;
r.sticky // true
//flags属性:返回正则表达式的修饰符
/abc/ig.flags
// 'gi'
//source属性:返回正则表达式的正文(ES5)
/abc/ig.source
// "abc"
数值的扩展
- 二进制和八进制表示法:
ES6 提供了二进制和八进制数值的新写法,分别用前缀0b(或0B)和0o(或0O)表示
0b111110111 === 503 // true
0o767 === 503 // true
- Number.isFinite(), Number.isNaN()
它们与传统的全局方法isFinite()和isNaN()的区别在于,传统方法先调用Number()将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,Number.isFinite()对于非数值一律返回false, Number.isNaN()只有对于NaN才返回true,非NaN一律返回false
Number.isFinite(15); // true
Number.isFinite(0.8); // true
Number.isFinite(NaN); // false
Number.isFinite(Infinity); // false
Number.isFinite(-Infinity); // false
Number.isFinite('foo'); // false
Number.isFinite('15'); // false
Number.isFinite(true); // false
Number.isNaN(NaN) // true
Number.isNaN(15) // false
Number.isNaN('15') // false
Number.isNaN(true) // false
Number.isNaN(9/NaN) // true
Number.isNaN('true'/0) // true
Number.isNaN('true'/'true') // true
- Number.parseInt(), Number.parseFloat()
ES6将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变
Number.parseInt === parseInt // true
Number.parseFloat === parseFloat // true
- Number.isInteger()
Number.isInteger()用来判断一个值是否为整数。需要注意的是,在JavaScript内部,整数和浮点数是同样的储存方法,所以3和3.0被视为同一个值
Number.isInteger(25) // true
Number.isInteger(25.0) // true
- Number.isSafeInteger()
JavaScript能够准确表示的整数范围在-253到253之间(不含两个端点),超过这个范围,无法精确表示这个值,Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内
Number.isSafeInteger(3) // true
Number.isSafeInteger(1.2) // false
Number.isSafeInteger(9007199254740990) // true
Number.isSafeInteger(9007199254740992) // false
- Math增加的函数:
//Math.trunc方法用于去除一个数的小数部分,返回整数部分
Math.trunc(4.1) // 4
Math.trunc(4.9) // 4
Math.trunc(-4.1) // -4
//Math.sign方法用来判断一个数到底是正数、负数、还是零
// 参数为正数,返回+1;
// 参数为负数,返回-1;
// 参数为0,返回0;
// 参数为-0,返回-0;
// 其他值,返回NaN
//Math.cbrt方法用于计算一个数的立方根
Math.cbrt(-1) // -1
Math.cbrt(0) // 0
Math.cbrt(1) // 1
Math.cbrt(2) // 1.2599210498948734
//Math.hypot方法返回所有参数的平方和的平方根
Math.hypot(3, 4); // 5
Math.hypot(3, 4, 5); // 7.0710678118654755
Math.hypot(); // 0
//Math.expm1(x)返回ex - 1,即Math.exp(x) - 1
//Math.log1p(x)方法返回1 + x的自然对数,即Math.log(1 + x)。如果x小于-1,返回NaN
//Math.log10(x)返回以10为底的x的对数。如果x小于0,则返回NaN
//Math.log2(x)返回以2为底的x的对数。如果x小于0,则返回NaN
- 指数运算符
2 ** 2 // 4
2 ** 3 // 8
数组的扩展
- Array.from函数
Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括ES6新增的数据结构Set和Map),用于替代[].slice.call()方法
// arguments对象
function foo() {
var args = Array.from(arguments);
// ...
}
//只要部署了Iterator接口的数据结构,都可以使用该函数
Array.from('hello')
// ['h', 'e', 'l', 'l', 'o']
let namesSet = new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']
//类数组的转换,必须有length属性的对象
Array.from({ length: 3 });
// [ undefined, undefined, undefined ]
//Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组
Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);
- Array.of函数
Array.of总是返回参数值组成的数组。如果没有参数,就返回一个空数组
Array.of() // []
Array.of(undefined) // [undefined]
Array.of(1) // [1]
Array.of(1, 2) // [1, 2]
//与Array构造函数的差别:
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
- 数组实例copyWithin()函数
在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组
//用法:
Array.prototype.copyWithin(target, start = 0, end = this.length)
//target(必需):从该位置开始替换数据。
//start(可选):从该位置开始读取数据,默认为0。如果为负值,表示倒数。
//end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。
// 将3号位复制到0号位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4)
// [4, 2, 3, 4, 5]
- 数组实例的find()和findIndex()
这两个函数接受一个判断函数,find返回第一个满足条件的元素,findIndex返回第一个满足条件元素的位置
[1, 4, -5, 10].find((n) => n < 0)
// -5
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
})
// 2
- 数组实例的fill()
fill方法使用给定值,填充一个数组
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
new Array(3).fill(7)
// [7, 7, 7]
//从第二个元素开始,到2号位置之前结束
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']
- 数组实例的entries(),keys()和values()
ES6提供三个新的方法——entries(),keys()和values()——用于遍历数组。它们都返回一个遍历器对象,可以用for…of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历
- 数组实例的includes()
Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。该方法属于ES7,但Babel转码器已经支持
[1, 2, 3].includes(2); // true
[1, 2, 3].includes(4); // false
[1, 2, NaN].includes(NaN); // true
函数的扩展
- 函数参数默认值
//函数参数的默认值是惰性求值的
let x = 99;
function foo(p = x + 1) {
console.log(p);
}
foo() // 100
x = 100;
foo() // 101
//函数的length属性:
//指定了默认值,函数的length属性将失效,length是参数第一个指定默认值之前的参数个数:
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
//rest参数也不会加入length长度:
(function(...args) {}).length // 0
//利用参数默认值可以指定某个参数不能省略:
function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}
foo()
// Error: Missing parameter
- rest参数
ES6 引入 rest 参数(形式为“…变量名”),用于获取函数的多余参数
function push(array, ...items) {
items.forEach(function(item) {
array.push(item);
console.log(item);
});
}
var a = [];
push(a, 1, 2, 3)
- 扩展运算符
扩展运算符(spread)是三个点(…)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列
//rest参数和扩展运算符相结合使用
function push(array, ...items) {
array.push(...items); //扩展运算符
}
// 替代函数的apply方法
function f(x, y, z) {
// ...
}
var args = [0, 1, 2];
f(...args);
//数组合并新写法:
let newArr = [...arr1, ...arr2, ...arr3];
//解构赋值:
let [a, ...rest] = list
//扩展运算符内部调用的是数据结构的Iterator接口,因此只要具有Iterator接口的对象,都可以使用扩展运算符,比如Map结构
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
let arr = [...map.keys()]; // [1, 2, 3]
//generator函数的扩展运算符快
var go = function*(){
yield 1;
yield 2;
yield 3;
};
[...go()] // [1, 2, 3]
- 严格模式
ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错
// 报错
function doSomething(a, b = a) {
'use strict';
// code
}
// 报错
const doSomething = function ({a, b}) {
'use strict';
// code
};
- name属性
函数的name属性,返回该函数的函数名
function foo() {}
foo.name // "foo"
//将匿名函数赋值给变量,返回变量名字
var f = function () {};
// ES5
f.name // ""
// ES6
f.name // "f"
//将具名函数赋值给变量,返回该具名函数的名字
const bar = function baz() {};
bar.name // "baz"
//Function构造函数返回的函数实例,name属性的值为anonymous。
(new Function).name // "anonymous"
//bind返回的函数,name属性值会加上bound前缀。
function foo() {};
foo.bind({}).name // "bound foo"
- 箭头函数
//返回对象的两种方法
var getTempItem1 = id => ({ id: id, name: "Temp" });
var getTempItem2 = id => {return { id: id, name: "Temp" }};
//不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误
//不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用rest参数代替
//不可以使用yield命令,因此箭头函数不能用作Generator函数
- 尾调用和尾递归优化
ES6 的尾调用优化只在严格模式下开启,正常模式是无效的
//尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了
//m(x)和n(x)都属于尾部调用,可以优化
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);
}
//保留n次调用记录
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
//只保留一次调用记录
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
- 函数参数尾逗号
ES2017 允许函数的最后一个参数有尾逗号(trailing comma)
function clownsEverywhere(
param1,
param2,
) { /* ... */ }
clownsEverywhere(
'foo',
'bar',
);
对象的扩展
- 对象属性的简洁表示方法:
var birth = '2000/01/01';
var Person = {
name: '张三',
//等同于birth: birth
birth,
// 等同于hello: function ()...
hello() { console.log('我的名字是', this.name); }
};
ES5比较两个值是否相等,只有两个运算符:相等运算符()和严格相等运算符(=)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0
Object.is大部分行为和===一致,但是会解决NaN和+0/—0的问题:
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
- Object.assign:
Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)
Object.assign不能复制源对象继承的值
//为对象指定默认值:
const DEFAULTS = {
logLevel: 0,
outputFormat: 'html'
};
function processContent(options) {
options = Object.assign({}, DEFAULTS, options);
console.log(options);
// ...
}
- 属性遍历
- for…in
for…in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。
- Object.keys(obj)
Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)。
- Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)。
- Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有Symbol属性。
- Reflect.ownKeys(obj)
Reflect.ownKeys返回一个数组,包含对象自身的所有属性,不管属性名是 Symbol 或字符串,也不管是否可枚举。
- Object.keys(),Object.values(),Object.entries()
ES2017在Object.keys的基础上提供了Object.values(),Object.entries()函数,这三个函数都返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性,不包含Symbol值的属性
Symbol
- 基本概念:
//Symbol不能使用new操作符,返回的是一种新的类型
let s = Symbol();
typeof s // 'symbol'
//Symbol可以作为对象的属性,可以保证每个属性的名字独一无二
let s1 = Symbol();
let s2 = Symbol();
let obj = {
[s1]: "1",
[s2]: "2"
}
obj[s1] // "1"
obj[s2] // "2"
//Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分
var s1 = Symbol('foo');
var s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"
//独一无二
// 没有参数的情况
var s1 = Symbol();
var s2 = Symbol();
s1 === s2 // false
// 有参数的情况
var s1 = Symbol('foo');
var s2 = Symbol('foo');
s1 === s2 // false
//Symbol不能与其它类型进行运算
var sym = Symbol('My symbol');
"your symbol is " + sym
// TypeError: can't convert symbol to string
`your symbol is ${sym}`
// TypeError: can't convert symbol to string
//Symbol的值可以显示转换字符串,或者转换为布尔值
var sym = Symbol('My symbol');
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
var sym = Symbol();
Boolean(sym) // true
!sym // false
- Symbol的应用:
//Symbol 类型还可以用于定义一组常量,保证这组常量的值都是不相等的。
log.levels = {
DEBUG: Symbol('debug'),
INFO: Symbol('info'),
WARN: Symbol('warn')
};
- Object.getOwnPropertySymbols:
Symbol 作为属性名,该属性不会出现在for…in、for…of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有Symbol属性名
- Reflect.ownKeys
Reflect.ownKeys方法可以返回所有类型的键名,包括常规键名和 Symbol键名
- Symbol.for(),Symbol.keyFor
Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值
Symbol.for("bar") === Symbol.for("bar")
// true
Symbol("bar") === Symbol("bar")
// false
Symbol.keyFor方法返回一个已登记的 Symbol 类型值的key:(只有Symbol.for才会登记)
var s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
var s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
- Symbol.hasInstance
对象的Symbol.hasInstance属性,指向一个内部方法。当其他对象使用instanceof运算符,判断是否为该对象的实例时,会调用这个方法
class Even {
static [Symbol.hasInstance](obj) {
return Number(obj) % 2 === 0;
}
}
//相当于调用EVEN[Symbol.hasInstance](1)
1 instanceof Even // false
2 instanceof Even // true
12345 instanceof Even // false
- Symbol.isConcatSpreadable
对象的Symbol.isConcatSpreadable属性等于一个布尔值,表示该对象使用Array.prototype.concat()时,是否可以展开
//undefined表示可展开
let arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e']
arr1[Symbol.isConcatSpreadable] // undefined
let arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e']
- Symbol.species
对象的Symbol.species属性,指向当前对象的构造函数。创造实例时,默认会调用这个方法;
class MyArray extends Array {
static get [Symbol.species]() { return Array; }
}
var a = new MyArray(1,2,3);
var mapped = a.map(x => x * x);
//修改对象的构造函数后,指向Array
mapped instanceof MyArray // false
mapped instanceof Array // true
- Symbol.match/Symbol.replace/Symbol.search/Symbol.split
这几个属性分别在调用match,replace,search,split函数时,如果对象有该属性,会优先调用该属性,返回对应函数的返回值:
String.prototype.search(regexp)
// 等同于
regexp[Symbol.search](this)
class MySearch {
constructor(value) {
this.value = value;
}
[Symbol.search](string) {
return string.indexOf(this.value);
}
}
'foobar'.search(new MySearch('foo')) // 0
- Symbol.iterator
对象的Symbol.iterator属性,指向该对象的默认遍历器方法:
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
- Symbol.toPrimitive
对象的Symbol.toPrimitive属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值
Symbol.toPrimitive被调用时,会接受一个字符串参数,表示当前运算的模式,一共有三种模式:
- Number:该场合需要转成数值
- String:该场合需要转成字符串
- Default:该场合可以转成数值,也可以转成字符串
let obj = {
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return 123;
case 'string':
return 'str';
case 'default':
return 'default';
default:
throw new Error();
}
}
};
2 * obj // 246 //当作数字用
3 + obj // '3default' //当作default用
obj == 'default' // true //当作default用
String(obj) // 'str' //显式调用
- Symbol.toStringTag
对象的Symbol.toStringTag属性,指向一个方法。在该对象上面调用Object.prototype.toString方法时,如果这个属性存在,它的返回值会出现在toString方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[object Object]或[object Array]中object后面的那个字符串:
// 例一
({[Symbol.toStringTag]: 'Foo'}.toString())
// "[object Foo]"
// 例二
class Collection {
get [Symbol.toStringTag]() {
return 'xxx';
}
}
var x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"
Set和Map数据结构
- set的基本概念
set类似于数组,但是其中的元素不重复,使用严格(===)判断,NaN和NaN是同一个元素,所有对象都是不同的元素
//set构造函数:
const set = new Set([1, 2, 3, 4, 4]);
const s = new Set();
//add函数,添加元素,返回set本身
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
s.add(1).add(2).add(2);
//delete 删除某个元素,返回是否删除成功
s.delete(2);
//has函数,返回是否包含某个元素
s.has(2) // false
//size属性返回set的长度
s.size //4
- set支持的方法:(set中键名和键值相等)
- keys():返回键名的遍历器
- values():返回键值的遍历器
- entries():返回键值对的遍历器
- forEach():使用回调函数遍历每个成员
- map():返回一个新的数组
- filter():返回一个新的数组
以上方法都不会改变set结构本身,想在遍历过程中同步set,可以通过以下方式实现:
// 方法一
let set = new Set([1, 2, 3]);
set = new Set([...set].map(val => val * 2));
// set的值是2, 4, 6
// 方法二
let set = new Set([1, 2, 3]);
set = new Set(Array.from(set, val => val * 2));
// set的值是2, 4, 6
- WeakSet概念
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与Set的区别:
- WeakSet 的成员只能是对象,而不能是其他类型的值
- WeakSet的成员是对象的弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用
- WeakSet不存在size属性,因为它的元素随时都可能消失
- WeakSet不存在for,filter,map函数
- Map的基本概念
Map是键值对的集合,比普通对象更适合作为键值,Map的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题。
同名属性碰撞是指:只有对同一对象的引用(引用类型),Map结构才会视为同一个键(键是复杂类型的值),而如果Map的键是一个简单类型的值(数字,字符串,布尔值),只要两个值严格相等,就视为同一个键
let arr1 = [1], arr2 = [1];
let map = new Map();
map.set(arr1, 1);
map.set(arr2, 2); //后者不会覆盖前者
let obj = {};
obj[arr1] = 1;
obj[arr2] = 2; //后者会覆盖前者
//键值对的数组构造函数
const map1 = new Map([
['name', '张三'],
['title', 'Author']
]);
//无参构造函数:
const map2 = new Map();
map2.set('name', 'zhangsan');
map2.set({a: 1}, "lisi");
//Map的函数和属性
map1.size // 2
map1.set('a': 1) //返回set对象本身
map1.get('a') //返回属性对应的值
map1.has("a") //表示是否属性是否存在
map1.clear() //清除所有键值
map1.delete('a') //删除对应的键
- Map的遍历器:
- keys():返回键名的遍历器。
- values():返回键值的遍历器。
- entries():返回所有成员的遍历器。
- forEach():遍历 Map 的所有成员。
Map不支持map,filter等函数
//value,key分别是键和值,map是map本身
map.forEach(function(value, key, map) {
console.log("Key: %s, Value: %s", key, value);
});
const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
[...map.keys()]
// [1, 2, 3]
[...map.values()]
// ['one', 'two', 'three']
[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]
[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]
- Map与JSON之间的转换
因为JSON中key不容许是对象,所以将map转换为JSON之前,最好先将Map转换为数组,然后再转换为JSON;
//map -> json
function mapToArrayJson(map) {
return JSON.stringify([...map]);
}
// json -> map
function jsonToMap(jsonStr) {
return new Map(JSON.parse(jsonStr));
}
- WeakMap的基本概念
- WeakMap与WeakSet类似,WeakMap弱引用的只是键名,而不是键值。键值依然是正常引用,键值只能是对象
- WeakMap只有四个方法可用:get()、set()、has()、delete()
Proxy(代理)
- 概述
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy构造函数:Proxy接受两个参数。第一个参数是所要代理的目标对象(上例是一个空对象),即如果没有Proxy的介入,操作原来要访问的就是这个对象;第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作
//比配置对象有一个get方法,用来拦截对目标对象属性的访问请求。get方法的两个参数分别是目标对象和所要访问的属性
let proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
//proxy属性可以实现继承
let obj = Object.create(proxy);
obj.time // 35
常用代理拦截器:
- get(target, propKey, receiver)
- set(target, propKey, value, receiver)
- has(target, propKey) 拦截propKey in proxy的操作,返回一个布尔值。
- deleteProperty(target, propKey) 拦截delete proxy[propKey]的操作,返回一个布尔值。
- ownKeys(target) 拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy),返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
- 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)。
如果一个属性不可配置(configurable)和不可写(writable),则该属性不能被代理,通过 Proxy 对象访问该属性会报错
const target = Object.defineProperties({}, {
foo: {
value: 123,
writable: false,
configurable: false
},
});
const handler = {
get(target, propKey) {
return 'abc';
}
};
const proxy = new Proxy(target, handler);
proxy.foo
// TypeError: Invariant check failed
- Proxy.revocable 方法返回一个可取消的 Proxy 实例
Proxy.revocable方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例:
//当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked
- this问题
虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理:
const _name = new WeakMap();
class Person {
constructor(name) {
_name.set(this, name);
}
get name() {
return _name.get(this);
}
}
const jane = new Person('Jane');
jane.name // 'Jane'
const proxy = new Proxy(jane, {});
proxy.name // undefined
Reflect
- 概述
Reflect对象与Proxy对象一样,也是ES6为了操作对象而提供的新API:
- 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上
- Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础
- 静态方法
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.has(target,name)
- Reflect.ownKeys(target)
- Reflect.isExtensible(target)
- Reflect.preventExtensions(target)
- Reflect.getOwnPropertyDescriptor(target, name)
- Reflect.getPrototypeOf(target)
- Reflect.setPrototypeOf(target, prototype)
- 是用Reflect实现观察者模式:
//只要修改了属性,就会调用queuedObservers中的所有函数
const queuedObservers = new Set();
const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
queuedObservers.forEach(observer => observer());
return result;
}
Promise
- 缺点:
- 首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消;
- 其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部;
- 第三,当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成);
- Promise新建后立即执行
//Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以Resolved最后输出
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('Resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// Resolved
setTimeout(function () {
console.log('three');
}, 0);
Promise.resolve().then(function () {
console.log('two');
});
console.log('one');
// one
// two
// three
- Promise的resolve函数的参数可以是另外一个Promise
//上面代码中,p1是一个Promise,3秒之后变为rejected。p2的状态在1秒之后改变,resolve方法返回的是p1。由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以,后面的then语句都变成针对后者(p1)。又过了2秒,p1变为rejected,导致触发catch方法指定的回调函数
var p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
var p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => console.log(result))
.catch(error => console.log(error))
- 未捕获异常的处理
Node 有一个unhandledRejection事件,专门监听未捕获的reject错误:
//第二个参数是报错的Promise实例
process.on('unhandledRejection', function (err, p) {
console.error(err.stack)
});
- Promise.resolve函数
将现有对象转为Promise对象,有以下两个特殊情况:
//1.如果参数是Promise实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例
//2.如果参数是一个thenable对象,方法会将这个对象转为Promise对象,然后就立即执行thenable对象的then方法
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
});
Iterator
- 在ES6中,有三类数据结构原生具备Iterator接口:数组、某些类似数组的对象、Set和Map结构
- 自己定义iterator接口:
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
- Iterator 试用场所:
- for…of循环
- 解构赋值
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'];
- 扩展运算符
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']
- 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 }
- 计算生成的数据结构
有些数据结构是在现有数据结构的基础上,计算生成的。比如,ES6的数组、Set、Map 都部署了以下三个方法,调用后都返回遍历器对象。
- entries() 返回一个遍历器对象,用来遍历[键名, 键值]组成的数组。对于数组,键名就是索引值;对于 Set,键名与键值相同。Map 结构的 Iterator 接口,默认就是调用entries方法。
- keys() 返回一个遍历器对象,用来遍历所有的键名。
- values() 返回一个遍历器对象,用来遍历所有的键值。
- 遍历器对象的return方法
return方法的使用场合是,如果for…of循环提前退出(通常是因为出错,或者有break语句或continue语句),就会调用return方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return方法:
function readLinesSync(file) {
return {
next() {
if (file.isAtEndOfFile()) {
file.close();
return { done: true };
}
},
return() {
file.close();
return { done: true };
},
};
}
Generator函数
- 概述
- 书写规则
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· } //常用写法
function*foo(x, y) { ··· }
//yield只能是用在generator函数中:
var flat = function* (a) {
a.forEach(function (item) {
if (typeof item !== 'number') {
yield* flat(item); //普通函数中是用forEach函数会报错,forEach应该是用
} else {
yield item;
}
});
};
//yield表达式如果用在另一个表达式之中,必须放在圆括号里面
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}
//Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身
function* gen(){
// some code
}
var g = gen();
g[Symbol.iterator]() === g
// true
- 何时执行:只有调用next函数才会执行函数体,遇到最近一个yield暂停
- next方法参数:
yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值
function* f() {
for(var i = 0; true; i++) {
var reset = yield i;
if(reset) { i = -1; }
}
}
var g = f();
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }
- Generator.prototype.throw2()
Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在Generator函数体内捕获或者在函数体外捕获:
var g = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b
throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法:
var gen = function* gen(){
try {
yield console.log('a');
} catch (e) {
// ...
}
yield console.log('b');
yield console.log('c');
}
var g = gen();
g.next() // a
g.throw() // b
g.next() // c
Generator函数体内抛出的错误,也可以被函数体外的catch捕获:
function* foo() {
var x = yield 3;
var y = x.toUpperCase();
yield y;
}
var it = foo();
it.next(); // { value:3, done:false }
try {
it.next(42);
} catch (err) {
console.log(err);
}
一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用next方法,将返回一个value属性等于undefined、done属性等于true的对象:
function* g() {
yield 1;
console.log('throwing an exception');
throw new Error('generator broke!');
yield 2;
yield 3;
}
function log(generator) {
var v;
console.log('starting generator');
try {
v = generator.next();
console.log('第一次运行next方法', v);
} catch (err) {
console.log('捕捉错误1', v);
}
try {
v = generator.next();
console.log('第二次运行next方法', v);
} catch (err) {
console.log('捕捉错误2', v);
}
try {
v = generator.next();
console.log('第三次运行next方法', v);
} catch (err) {
console.log('捕捉错误3', v);
}
console.log('caller done');
}
log(g());
// starting generator
// 第一次运行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉错误2 { value: 1, done: false }
// 第三次运行next方法 { value: undefined, done: true }
// caller done
- Generator.prototype.return()
Generator函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历Generator函数
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }
- yield*
如果被代理的 Generator 函数有return语句,那么就可以向代理它的 Generator 函数返回数据
function *foo() {
yield 2;
yield 3;
return "foo";
}
function *bar() {
yield 1;
var v = yield *foo();
console.log( "v: " + v );
yield 4;
}
var it = bar();
it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}
- co库
被co包装的generator函数只能yield 函数,promise, generator,数组,和对象,不能yield 单个数值
如何使用co库:
let co = require('co');
function gen(a, b){
yield c = Promise.resolve(a + b);
return c + 5;
}
//方法1:
co(gen, 3, 4);
//方法2:
let newGeo = co.wrap(gen);
newGeo(3, 4);
async
async是generator函数的语法糖,是generator函数的改进:
//普通函数前加上async,所有的异步调用前加上await,该函数直接返回一个promise
var asyncReadFile = async function () {
var f1 = await readFile('/etc/fstab');
var f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
//co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)
yield和await并发执行:
//同步执行,效率低
let foo = await getFoo();
let bar = await getBar();
//如果两个函数执行不分前后顺序,可以使用下面两种方法,实现异步执行:
// 方法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 方法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
异步generator函数:
//yield (await file.readLine()),有yield的地方就是next函数停下来的地方,yield后面加了await关键字,只有等待这行完这个promise,next才会返回
async function* readLines(path) {
let file = await fileOpen(path);
try {
while (!file.EOF) {
yield await file.readLine();
}
} finally {
await file.close();
}
}
Generator函数与async函数的区别:
- 将*替换为了async且async提到了function的前面,表示函数中有异步操作
- 将yield替换为了await,表示紧跟后面的表达式需要等待结果
- async函数自带了执行器,不需要像Generator一样需要next方法才能执行
- yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)
- async返回的是一个Promise对象,Generator函数返回的是一个遍历器对象
类
- 类内部定义的所有方法是不可枚举的:
class Point {
constructor(x, y) {
// ...
}
toString() {
// ...
}
}
Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
- constructor函数
constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加
class Point {
}
// 等同于
class Point {
constructor() {}
}
//类必须使用new调用,否则会报错
class Foo {
constructor() {
return Object.create(null);
}
}
Foo()
// TypeError: Class constructor Foo cannot be invoked without 'new'
- 类不存在变量提升
new Foo(); // ReferenceError
class Foo {}
- 私有方法定义
可以将私有函数的名字定义为一个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;
}
// ...
};
- 类name属性返回类名本身
- 取值和存值函数:
改变类内部属性的存取行为
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'
- 静态方法
静态方法只能在通过类名调用,不会被类的实例继承
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
- new.target 属性
ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数
//可以确保构造函数只能通过new调用
function Person(name) {
if (new.target === Person) {
this.name = name;
} else {
throw new Error('必须使用 new 生成实例');
}
}
var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三'); // 报错
//子类继承父类时,new.target会返回子类
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); // 正确
- 继承
- 子类继承父类必须有super函数调用:
//子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的,因为子类实例的构建是通过父类的加工
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
this.color = color; // ReferenceError
super(x, y);
this.color = color; // 正确
}
}
- 继承关系的判断:
Object.getPrototypeOf(ColorPoint) === Point
// true
- super
//super()当作函数调用,只能使用在子类的构造函数中,并且super内部的this指针指向子类:
class A {
constructor() {
console.log(new.target.name);
}
}
class B extends A {
constructor() {
super();
}
}
new A() // A
new B() // B
//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就引用不到它
//ES6 规定,通过super调用父类的方法时,super会绑定子类的this
class A {
constructor() {
this.x = 1;
}
print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
}
m() {
super.print();
}
}
let b = new B();
b.m() // 2
- 继承的实质:
class A {
}
class B extends A {
}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
- ES6允许继承原生构造函数定义子类,因为ES6是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承
Boolean() Number() String() Array() Date() Function() RegExp() Error() Object()
//这意味着,ES6 可以自定义原生数据结构(比如Array、String等)的子类,这是 ES5 无法做到的
class MyArray extends Array {
constructor(...args) {
super(...args);
}
}
var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined
Decorator:
修饰器(Decorator)是一个函数,用来修改类的行为,修饰器本质就是编译时执行的函数
@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A;
//修饰器的应用1:修饰类
function testable(isTestable) {
return function(target) {
target.isTestable = isTestable;
}
}
@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true
@testable(false)
class MyClass {}
MyClass.isTestable // false
//修饰器的应用2:修饰类的方法:
//第一个参数是所要修饰的目标对象,第二个参数是所要修饰的属性名,第三个参数是该属性的描述对象
function readonly(target, name, descriptor){
descriptor.writable = false;
return descriptor;
}
class Person {
@readonly
name() { return `${this.first} ${this.last}` }
}
//如果同一个方法有多个修饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行
function dec(id){
console.log('evaluated', id);
return (target, property, descriptor) => console.log('executed', id);
}
class Example {
@dec(1)
@dec(2)
method(){}
}
// evaluated 1
// evaluated 2
// executed 2
// executed 1
//修饰器不能修饰函数,因为存在函数提升
var counter = 0;
var add = function () {
counter++;
};
@add
function foo() {
}
//相当于下面的定义:
@add
function foo() {
}
var counter;
var add;
counter = 0;
add = function () {
counter++;
};
模块
- CommonJS模块和ES6模块的却别
差异:
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用;
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口;
- ES6输入的模块变量只是一个“符号链接”,所以这个变量是只读的,不能对其进行重新赋值;