目录
ECMAScript 6 简介
1、箭头函数
2、类
3、增强对象字面量
4、模板字符串
5、解构赋值
6、默认参数、剩余参数、展开操作符
7、let、const
8、迭代器(迭代器)、for of
9、Generators(生成器)
10、Unicode
11、模块
12、模块加载器(提案)
13、map、set、weakmap、weaakset
14、Proxy
15、Symbols
16、继承
17、Promise
18、Math + Number + String + Array + Object APIs
19、二进制,八进制
20、Reflect API
21、尾递归
ECMAScript 6 简介
ECMAScript 6,也称为ECMAScript2015,是ECMAScript标准的最新版本。ES6是该语言的一次重大更新,也是自2009年ES5标准化以来该语言的首次更新。目前正在主要的JavaScript引擎中实现这些功能。
有关ECMAScript 6语言的完整规范,请参阅ES6标准。
(https://262.ecma-international.org/6.0/)
ES6包括以下新功能:
- 箭头函数
- 类
- 增强对象字面量
- 模板字符串
- 解构赋值
- 默认参数、剩余参数、展开操作符
- let、const
- Iterators(迭代器)、for of
- Generators(生成器)
- unicode
- 模块
- 模块加载器(提案,未正式成为ES6的一部分)
- map、set、weakmap、weaakset
- Proxy
- Symbol
- 继承
- Promise
- Math + Number + String + Array + Object APIs
- 二进制,八进制
- Reflect API
- 尾递归
1、箭头函数
箭头是使用=>语法的函数简写。与函数不同,箭头与其周围的代码共享相同的词法this。
let age = [20, 22, 24, 26].map(res=>res+1) // [21, 23, 25, 27]
// 词法 this
var people = {
name: 'zhangsan',
age: 23,
course: ['语文', '数学', '英语'],
getCourse () {
this.course.forEach(res =>
console.log(this.name + ' 课程 ' + res)
);
}
}
people.getCourse()
// zhangsan 课程 语文
// zhangsan 课程 数学
// zhangsan 课程 英语
// 然后我们把getCourse()方法改一下
this.course.forEach(function(res) {
console.log(this.name + ' 课程 ' + res)
});
// 课程 语文
// 课程 数学
// 课程 数学
// this.name 没有获取到,这时this指向windows对象
2、类
在ES6中,类是基于原型的面向对象的简单补充,类支持基于类型的继承,父级super调用,实例,静态方法,构造函数
class animal {
constructor(age) {
this.walk = '走';
}
}
class monkey extends animal {
constructor(age, name) {
super(age); // 调用父类的constructor(age)
this.age = 12;
}
static defaultColor() {
rerun 'white'
}
}
let mon = new monkey(33) // 进行实例化
console.log(mon) // {walk: '走', age: 33}
monkey.defaultColor() // 'white' 这里静态方法可以直接调用,不需要实例化
3、增强对象字面量
var faObj = {
get Pname () {return '父对象'},
toString () {
return 'String'
}
}
var handler = function () {return 'handler'}
var obj = {
// __proto__
__proto__: faObj,
// 简短写法 ‘handler: handler’
handler,
// 定义当前对象的toString() 方法
toString() {
// 调用父对象toString()方法
return "d " + super.toString();
},
// 字面量值会进行自动计算 prop_111: 111
[ 'prop_' + (() => 111)() ]: 111
};
console.log(obj)
// {handler: f(), prop_111: 111, toString: toString(), Pname: ()}
4、模板字符串
模板字面量是用反引号(`)分隔的字面量,允许多行字符串、带嵌入表达式的字符串插值和一种叫带标签的模板的特殊结构。
// 多行字符串
console.log('string text line 1\n' + 'string text line 2');
// "string text line 1
// string text line 2"
// 嵌入表达式的字符串插值
var name = "zhangsan", age = 23;
console.log(`Hello ${name}, age ${age}?`)
// 'Hello zhangsan, age 23?'
// 带标签的模板
var age = 23, sex = '男'
function tag (string, exp1, exp2) {
return `hello ${string} ${exp1} ${exp2}`
}
tag`zhangsan ${age} ${sex}`
// 'hello zhangsan , , 23 男'
5、解构赋值
解构赋值语法是一种 Javascript 表达式。可以将数组中的值或对象的属性取出,赋值给其他变量。
var [a,b,c] = [1,2,3] // 数组赋值,进行匹配
const obj = { a: 1, b: 2 };
const { a, b } = obj; // 对象解构
const foo = ['one', 'two', 'three'];
const [red, yellow, green] = foo; // 数组解构
// 剩余解构
const { a, ...others } = { a: 1, b: 2, c: 3 };
console.log(others); // { b: 2, c: 3 }
const [first, ...others2] = [1, 2, 3];
console.log(others2); // [2, 3]
// 变量置换
let a = 1,b = 2;
[a, b] = [b, a];
6、默认参数、剩余参数、展开操作符
function f(x, y=1) { // y 默认为1,如果传参,则覆盖默认值
return x + y;
}
f(1) == 2 // true
f(1, 2) == 3 // true
function f(x, ...y) {
// y 是一个数据组
return x * y.length;
}
f(2, "hello", true, 123) == 6
function f(x, y, z) {
return x + y + z;
}
// 将数组的每个元素作为参数传递
f(...[1,2,3]) == 6
7、let、const
let 语句声明一个块级作用域的局部变量,并可以初始化为一个值(可选)。
let 声明的变量作用域只在其声明的块或子块内部,这一点,与 var 相似。二者之间最主要的区别在于 var 声明的变量的作用域是整个封闭函数。
function varTest() {
var x = 1;
{
var x = 2; // 相同
console.log(x); // 2
}
console.log(x); // 2
}
function letTest() {
let x = 1;
{
let x = 2; // 不同
console.log(x); // 2
}
console.log(x); // 1
}
var 声明的变量会在整个封闭函数内有影响,而let之只在区块内有影响。
在全局作用域中,let 和 var 不一样,它不会在全局对象上创建属性。
var x = 'global';
let y = 'global';
console.log(this.x); // "global"
console.log(this.y); // undefined
在同一个函数或块作用域中重复声明同一个变量会抛出 SyntaxError。
if (x) {
let foo;
let foo; // SyntaxError thrown.
}
从一个代码块的开始直到代码执行到声明变量的行之前,let 或 const 声明的变量都处于“暂时性死区”(Temporal dead zone,TDZ)中。
{ // TDZ starts at beginning of scope
console.log(bar); // undefined
console.log(foo); // ReferenceError
var bar = 1;
let foo = 2; // End of TDZ (for foo)
}
const 基本用法
常量是块级范围的,非常类似用 let 语句定义的变量。但常量的值是无法(通过重新赋值)改变的,也不能被重新声明。
常量在声明的时候可以使用大小写,但通常情况下全部用大写字母。
cont MY_AGE = 20;
MY_AGE = 20; // caught TypeError: Assignment to constant variable.
const MY_AGE = 20; // 尝试重新声明会报错
const NAAME; // Uncaught SyntaxError: Missing initializer in const declaration
常量可以定义成对象和数组
const MY_OBJECT = {'key': 'value'};
const MY_ARRAY = []
const 声明创建一个值的只读引用。但这并不意味着它所持有的值是不可变的,只是变量标识符不能重新分配。例如,在引用内容是对象的情况下,这意味着可以改变对象的内容(例如,其参数)。
8、迭代器(迭代器)、for of
for...of语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句
const array1 = ['a', 'b', 'c'];
for (const element of array1) {
console.log(element);
}
// a
// b
// c
迭代器协议定义了产生一系列值(无论是有限个还是无限个)的标准方式,当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。
function* fibonacci() { // 一个生成器函数
let [prev, curr] = [0, 1];
for (;;) { // while (true) {
[prev, curr] = [curr, prev + curr];
yield curr;
}
}
for (let n of fibonacci()) {
console.log(n);
// 当 n 大于 1000 时跳出循环
if (n >= 1000)
break;
}
迭代基于这些鸭子类型的接口(仅使用TypeScript类型语法进行说明):
interface IteratorResult { // 迭代器返回结果接口
done: boolean;
value: any;
}
interface Iterator { // 迭代器接口
next(): IteratorResult;
}
interface Iterable { // 可迭代接口
[Symbol.iterator](): Iterator
}
9、Generators(生成器)
生成器使用函数*和yield简化迭代器的编写。声明为函数*的函数返回一个Generator实例。生成器是迭代器的子类型,其中包括额外的next和throw。这些使值能够返回到生成器,因此yield是一种返回值(或抛出)的表达式形式。
Generator 构造函数并不是全局可用。Generator 的实例必须从生成器函数返回:
function* generator() {
yield 1;
yield 2;
yield 3;
}
const gen = generator(); // "Generator { }"
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
10、Unicode
渐进增强支持完整的Unicode,包括在字符串中实现新的Unicode迭代,以及在新的正则中使用u模式的Unicode 码位,新API可以在21位码位级别处理字符串。增加支持用JavaScript构建全局应用程序。
// 与 ES5.1 一样
"".length == 2
// 新的正则表达式行为,选择加入“u”模式
"".match(/./u)[0].length == 2
// 新的处理形式
"\u{20BB7}"==""=="\uD842\uDFB7"
// 新的字符串操作
"".codePointAt(0) == 0x20BB7
// 使用 for-of 迭代码位
for(var c of "") {
console.log(c);
}
在Unicode中,码位的总范围为0x0到0x10FFFF,共1,114,112个码位。2048个用于编码代理(UTF-16),66个非字符码位(例如BOM),137,468个预留给私人使用,最终剩余974,530用于普通字符分配。
码位的最大值为0x10FFFF,对应二进制有21位,我们将2^16个值分为一组,则Unicode总共可以分为17份,每一份称之为平面(Plane),每一个平面有65,536(2^16)个码位。
ES6 新的写法,"\u{20bb7}",在新的Unicode码,在javascript中,采用UTF-16的编码方式,最新Unicode标准(v15.0)中一个字符占用的最大的二进制为21位。所以就会出现“u20bb”这种形式,首先我们先计算后4位,0bb7 转换为二进制为 101110110111,分成2部分计算
UTF-16 bit 分布:
标准值 | UTF-16 |
xxxxxxxxxxxxxxxx | xxxxxxxxxxxxxxxx |
000uuuuuxxxxxxxxxxxxxxxx | 110110wwwwxxxxxx 110111xxxxxxxxxx |
注:wwww = uuuuu - 1
0x20bb7 - 0x10000 = 1 0000 1011 1011 0111
110111拼接后面10位 = 110111111011011 转换成16进制 ==> DFB7
前面只剩下7位1000010,不够10位,前面补3个0,变成0001 0000 10,再把110110拼接在这个的前面组成 1101 1000 0100 0010 转换成16进制 ==> D842
11、模块
组件定义模块是语言级的支持。 编码方式源于流行的JavaScript模块加载器(AMD,CommonJS)。由主机定义的默认加载程序定义的运行时行为。 隐式异步模型-在请求的模块可用和处理之前,不执行代码。
静态的 import 语句用于导入由另一个模块导出的绑定。
在浏览器中,import 语句只能在声明了 type="module" 的 script 的标签中使用。
// lib/math.js
export function sum(x, y) {
return x + y;
}
export var pi = 3.141593; // 通过export 进行导出
// app.js
import * as math from "lib/math";
console.log("2π = " + math.sum(math.pi, math.pi));
// other
import {sum, pi} from "lib/math";
console.log("2π = " + sum(pi, pi));
一些附加功能包括export default和export *:
// lib/mathplusplus.js
export * from "lib/math";
export var e = 2.71828182846;
export default function(x) {
return Math.log(x);
}
// app.js
import ln, {pi, e} from "lib/mathplusplus";
console.log("2π = " + ln(e)*pi*2);
动态 import
标准用法的 import 导入的模块是静态的,会使所有被导入的模块,在加载时就被编译(无法做到按需编译,降低首页加载速度)。有些场景中,你可能希望根据条件导入模块或者按需导入模块,这时你可以使用动态导入代替静态导入。下面的是你可能会需要动态导入的场景。
- 当静态导入的模块很明显的降低了代码的加载速度且被使用的可能性很低,或者并不需要马上使用它。
- 当静态导入的模块很明显的占用了大量系统内存且被使用的可能性很低。
- 当被导入的模块,在加载时并不存在,需要异步获取。
- 当导入模块的说明符,需要动态构建。(静态导入只能使用静态说明符)
- 当被导入的模块有副作用(这里说的副作用,可以理解为模块中会直接运行的代码),这些副作用只有在触发了某些条件才被需要时。(原则上来说,模块不能有副作用,但是很多时候,你无法控制你所依赖的模块的内容)
请不要滥用动态导入(只有在必要情况下采用)。静态框架能更好的初始化依赖,而且更有利于静态分析工具和 tree shaking 发挥作用。
import('./mathlib.js')
.then(res=>
console.log(res)
);
let math_module = await import('./mathlib.js');
export(导出)
export 语句用于从模块中导出实时绑定的函数、对象或原始值,以便其他程序可以通过 import 语句使用它们。被导出的绑定值依然可以在本地进行修改。在使用 import 进行导入时,这些绑定值只能被导入模块所读取,但在 export 导出模块中对这些绑定值进行修改,所修改的值也会实时地更新。
// 导出单个特性
export let name1, name2, …, nameN; // 同样还有 var, const
export let name1 = …, name2 = …, …, nameN; // 同样还有 var, const
export function FunctionName(){...}
export class ClassName {...}
// 导出列表
export { name1, name2, …, nameN };
// 重命名导出
export { variable1 as name1, variable2 as name2, …, nameN };
// 解构导出并重命名
export const { name1, name2: bar } = o;
// 默认导出
export default expression;
export default function (…) { … } // 同样还有 class, function*
export default function name1(…) { … } // 同样还有 class, function*
export { name1 as default, … };
// 导出模块合集
export * from …; // does not set the default export
export * as name1 from …; // Draft ECMAScript® 2O21
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
export { default } from …;
12、模块加载器(提案)
类似实现的包,可参考(npm i es-module-loader)
模块加载器支持:
- 动态加载
- 状态隔离
- 全局命名空间隔离
- 编译钩子
- 嵌套虚拟化
可以配置默认的模块加载器,并且可以构造新的加载器,以在隔离或受约束的上下文中评估和加载代码。
// 动态加载 – ‘System’ 是默认的加载器
System.import('lib/math').then(function(m) {
console.log("2π = " + m.sum(m.pi, m.pi));
});
// 创建可执行的沙箱 – new Loaders
var loader = new loader({
global: fixup(window) // replace ‘console.log’
});
loader.eval("console.log('hello world!');");
// 直接操作模块缓存
System.get('jquery');
System.set('jquery', Module({$: $})); //警告:未确定...
13、map、set、weakmap、weaakset
Map 对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者基本类型)都可以作为一个键或一个值。
const map1 = new Map();
map1.set('a', 1);
map1.set('b', 2);
map1.set('c', 3);
console.log(map1.get('a')); // 输出: 1
map1.set('a', 97);
console.log(map1.get('a')); // 输出:97
console.log(map1.size); // 输出:3
map1.delete('b');
console.log(map1.size); // 输出:2
Object和Map的比较:
Object 和 Map 类似的是,它们都允许你按键存取一个值、删除键、检测一个键是否绑定了值。因此(并且也没有其他内建的替代方式了)过去我们一直都把对象当成 Map 使用。
Map | Object | |
附带的键 | Map 默认情况不包含任何键。只包含显式插入的键。 | 一个 Object 有一个原型,原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。 |
键的类型 | 一个 Map 的键可以是任意值,包括函数、对象或任意基本类型。 | 一个 Object 的键必须是一个 String 或是 Symbol。 |
键的顺序 | Map 中的键是有序的。因此,当迭代的时候,一个 Map 对象以插入的顺序返回键值 | 虽然 Object 的键目前是有序的,但并不总是这样,而且这个顺序是复杂的。因此,最好不要依赖属性的顺序。 |
统计个数 | Map 的键值对个数可以轻易地通过 size 属性获取。 | Object 的键值对个数只能手动计算。 |
迭代 | Map 是 可迭代的 的,所以可以直接被迭代。 | Object 没有实现 迭代协议,所以使用 JavaSctipt 的 for...of 表达式并不能直接迭代对象 |
性能 | 在频繁增删键值对的场景下表现更好。在频繁增删键值对的场景下表现更好。 | 在频繁添加和删除键值对的场景下未作出优化。 |
序列化和解析 | 没有元素的序列化和解析的支持。 JSON.stringify和JSON.parse都支持第二个参数,replacer和reviver,可以定义自己的规则实现序列化。 | 原生的由 Object 到 JSON 的序列化支持,使用 JSON.stringify()。 原生的由 JSON 到 Object 的解析支持,使用 JSON.parse()。 |
Set
Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。
Set对象是值的集合,你可以按照插入的顺序迭代它的元素。Set 中的元素只会出现一次,即 Set 中的元素是唯一的。
因为 Set 中的值总是唯一的,所以需要判断两个值是否相等。在 ECMAScript 规范的早期版本中,这不是基于和===操作符中使用的算法相同的算法。具体来说,对于 Set,+0(+0 严格相等于 -0)和 -0 是不同的值。然而,在 ECMAScript 2015 规范中这点已被更改。有关详细信息,请参阅浏览器兼容性表中的“Key equality for -0 and 0”。
另外,NaN 和 undefined 都可以被存储在 Set 中,NaN 之间被视为相同的值(NaN 被认为是相同的,尽管 NaN !== NaN)。
let mySet = new Set();
mySet.add(1); // Set [ 1 ]
mySet.has(1); // true
mySet.has(3); // false
mySet.size; // 5
mySet.delete(1); // true,从 set 中移除 1
WeakMap
WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。
在 JavaScript 里,map API 可以 通过使其四个 API 方法共用两个数组(一个存放键,一个存放值)来实现。给这种 map 设置值时会同时将键和值添加到这两个数组的末尾。从而使得键和值的索引在两个数组中相对应。当从该 map 取值的时候,需要遍历所有的键,然后使用索引从存储值的数组中检索出相应的值。
但这样的实现会有两个很大的缺点:
- 首先赋值和搜索操作都是 O(n) 的时间复杂度(n 是键值对的个数),因为这两个操作都需要遍历全部整个数组来进行匹配。
- 另外一个缺点是可能会导致内存泄漏,因为数组会一直引用着每个键和值。这种引用使得垃圾回收算法不能回收处理他们,即使没有其他任何引用存在了。
相比之下,原生的 WeakMap 持有的是每个键对象的“弱引用”,这意味着在没有其他引用存在时垃圾回收能正确进行。原生 WeakMap 的结构是特殊且有效的,其用于映射的 key _只有_在其没有被回收时才是有效的。
正由于这样的弱引用,WeakMap 的 key 是不可枚举的(没有方法能给出所有的 key)。如果 key 是可枚举的话,其列表将会受垃圾回收机制的影响,从而得到不确定的结果。因此,如果你想要这种类型对象的 key 值的列表,你应该使用 Map。
const wm1 = new WeakMap(),
wm2 = new WeakMap(),
wm3 = new WeakMap();
const o1 = {},
o2 = function() {},
o3 = window;
wm1.set(o1, 37);
wm1.set(o2, 'azerty');
wm2.set(o1, o2); // value 可以是任意值,包括一个对象或一个函数
wm2.set(o3, undefined);
wm2.set(wm1, wm2); // 键和值可以是任意对象,甚至另外一个 WeakMap 对象
wm1.get(o2); // "azerty"
wm2.get(o2); // undefined,wm2 中没有 o2 这个键
wm2.get(o3); // undefined,值就是 undefined
wm1.has(o2); // true
wm2.has(o2); // false
wm2.has(o3); // true (即使值是 undefined)
wm3.set(o1, 37);
wm3.get(o1); // 37
wm1.has(o1); // true
wm1.delete(o1);
wm1.has(o1); // false
for (let i of wm1) {
console.log(i)
};
// caught TypeError: wm1 is not iterable
WeakSet
WeakSet 对象允许你将弱保持对象存储在一个集合中。
WeakSet 对象是一些对象值的集合。且其与 Set 类似,WeakSet 中的每个对象值都只能出现一次。在 WeakSet 的集合中,所有对象都是唯一的。
它和 Set 对象的主要区别有:
- WeakSet 只能是对象的集合,而不能像 Set 那样,可以是任何类型的任意值。
- WeakSet 持弱引用:集合中对象的引用为弱引用。如果没有其他的对 WeakSet 中对象的引用,那么这些对象会被当成垃圾回收掉。
这也意味着 WeakSet 中没有存储当前对象的列表。正因为这样,WeakSet 是不可枚举的。
使用示例:
const ws = new WeakSet();
const foo = {};
const bar = {};
ws.add(foo);
ws.add(bar);
ws.has(foo); // true
ws.has(bar); // true
ws.delete(foo); // 从 set 中删除 foo 对象
ws.has(foo); // false,foo 对象已经被删除了
ws.has(bar); // true,bar 依然存在
用例:检测循环引用
// 对 传入的 subject 对象 内部存储的所有内容执行回调
function execRecursively(fn, subject, _refs = new WeakSet()) {
// 避免无限递归
if (_refs.has(subject)) {
return;
}
fn(subject);
if (typeof subject === "object") {
_refs.add(subject);
for (const key in subject) {
execRecursively(fn, subject[key], _refs);
}
}
}
const foo = {
foo: "Foo",
bar: {
bar: "Bar",
},
};
foo.bar.baz = foo; // 循环引用!
execRecursively((obj) => console.log(obj), foo);
对象的数量或它们的遍历顺序无关紧要,因此,WeakSet 比 Set 更适合(和执行)跟踪对象引用,尤其是在涉及大量对象时。
14、Proxy
Proxy 允许创建具有可用于宿主对象的全部行为范围的对象。 可用于拦截、对象虚拟化、日志记录/分析等。
const handler = {
get: function(obj, prop) {
return prop in obj ? obj[prop] : 37;
}
};
let target = {}
const p = new Proxy(target, handler);
p.a = 1;
p.b = undefined;
console.log(p.a, p.b); // 1, undefined
console.log(target.a, target.b); // 1, undefined
console.log('c' in p, p.c); // false, 37
这里为了创建一个完整的 traps 列表示例,我们将尝试代理化一个非原生对象,这特别适用于这类操作
var handler =
{
get:...,
set:...,
has:...,
deleteProperty:...,
apply:...,
construct:...,
getOwnPropertyDescriptor:...,
defineProperty:...,
getPrototypeOf:...,
setPrototypeOf:...,
enumerate:...,
ownKeys:...,
preventExtensions:...,
isExtensible:...
}
15、Symbols
symbol 是一种基本数据类型(primitive data type)。Symbol() 函数会返回 symbol 类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的 symbol 注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:"new Symbol()"。
每个从 Symbol() 返回的 symbol 值都是唯一的。一个 symbol 值能作为对象属性的标识符;这是该数据类型仅有的目的。
const symbol1 = Symbol();
const symbol2 = Symbol(42);
const symbol3 = Symbol('foo');
console.log(typeof symbol1);
// 输出: "symbol"
console.log(symbol2 === 42);
// 输出: false
console.log(symbol3.toString());
// 输出: "Symbol(foo)"
console.log(Symbol('foo') === Symbol('foo'));
// 输出: false
Symbol("foo") === Symbol("foo"); // false
16、继承
对于使用过基于类的语言 (如 Java 或 C++) 的开发者们来说,JavaScript 实在是有些令人困惑 —— JavaScript 是动态的,本身不提供一个 class 的实现。即便是在 ES2015/ES6 中引入了 class 关键字,但那也只是语法糖,JavaScript 仍然是基于原型的。
当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象(object)都有一个私有属性(称之为 __proto__)指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。
let f = function () {
this.a = 1;
this.b = 2;
}
let o = new f(); // {a: 1, b: 2}
// 在 f 函数的原型上定义属性
f.prototype.b = 3;
f.prototype.c = 4;
console.log(o.a); // 1
console.log(o.b); // 2 优先找实例上的属性
console.log(o.c); // 4 优先找实例上的属性,然后从原型对象上找
使用语法结构创建的对象:
var o = {a: 1};
// o 这个对象继承了 Object.prototype 上面的所有属性
// o 自身没有名为 hasOwnProperty 的属性
// hasOwnProperty 是 Object.prototype 的属性
// 因此 o 继承了 Object.prototype 的 hasOwnProperty
// Object.prototype 的原型为 null
// 原型链如下:
// o ---> Object.prototype ---> null
var a = ["yo", "whadup", "?"];
// 数组都继承于 Array.prototype
// (Array.prototype 中包含 indexOf, forEach 等方法)
// 原型链如下:
// a ---> Array.prototype ---> Object.prototype ---> null
function f(){
return 2;
}
// 函数都继承于 Function.prototype
// (Function.prototype 中包含 call, bind 等方法)
// 原型链如下:
// f ---> Function.prototype ---> Object.prototype ---> null
使用构造器创建的对象:
在 JavaScript 中,构造器其实就是一个普通的函数。当使用 new 操作符 来作用这个函数时,它就可以被称为构造方法(构造函数)
function Graph() {
this.vertices = [];
this.edges = [];
}
Graph.prototype = {
addVertex: function(v){
this.vertices.push(v);
}
};
var g = new Graph();
// g 是生成的对象,他的自身属性有 'vertices' 和 'edges'。
// 在 g 被实例化时,g.[[Prototype]] 指向了 Graph.prototype。
使用Object.create创建的对象:
var a = {a: 1};
// a ---> Object.prototype ---> null
var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (继承而来)
var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null
var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); // undefined,因为 d 没有继承 Object.prototype
使用class关键字创建的对象:
ECMAScript6 引入了一套新的关键字用来实现 class。使用基于类语言的开发人员会对这些结构感到熟悉,但它们是不同的。JavaScript 仍然基于原型。这些新的关键字包括 class, constructor,static,extends 和 super。
"use strict";
class Polygon {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
class Square extends Polygon {
constructor(sideLength) {
super(sideLength, sideLength);
}
get area() {
return this.height * this.width;
}
set sideLength(newLength) {
this.height = newLength;
this.width = newLength;
}
}
var square = new Square(2);
性能:
在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。另外,试图访问不存在的属性时会遍历整个原型链。
遍历对象的属性时,原型链上的每个可枚举属性都会被枚举出来。要检查对象是否具有自己定义的属性,而不是其原型链上的某个属性,则必须使用所有对象从 Object.prototype 继承的 hasOwnProperty (en-US) 方法。下面给出一个具体的例子来说明它:
console.log(g.hasOwnProperty('vertices'));
// true
console.log(g.hasOwnProperty('nope'));
// false
console.log(g.hasOwnProperty('addVertex'));
// false
console.log(g.__proto__.hasOwnProperty('addVertex'));
// true
hasOwnProperty (en-US) 是 JavaScript 中唯一一个处理属性并且不会遍历原型链的方法。(译者注:原文如此。另一种这样的方法:Object.keys())
注意:检查属性是否为 undefined 是不能够检查其是否存在的。该属性可能已存在,但其值恰好被设置成了 undefined。
17、Promise
Promise 对象用于表示一个异步操作的最终完成(或失败)及其结果值。
此特性在 Web Worker 中可用
一个 Promise 对象代表一个在这个 promise 被创建出来时不一定已知值的代理。它让你能够把异步操作最终的成功返回值或者失败原因和相应的处理程序关联起来。这样使得异步方法可以像同步方法那样返回值:异步方法并不会立即返回最终的值,而是会返回一个 promise,以便在未来某个时候把值交给使用者。
一个 Promise 必然处于以下几种状态之一:
- 待定(pending):初始状态,既没有被兑现,也没有被拒绝。
- 已兑现(fulfilled):意味着操作成功完成。
- 已拒绝(rejected):意味着操作失败。
待定状态的 Promise 对象要么会通过一个值被兑现,要么会通过一个原因(错误)被拒绝。当这些情况之一发生时,我们用 promise 的 then 方法排列起来的相关处理程序就会被调用。如果 promise 在一个相应的处理程序被绑定时就已经被兑现或被拒绝了,那么这个处理程序也同样会被调用,因此在完成异步操作和绑定处理方法之间不存在竞态条件。
因为 Promise.prototype.then 和 Promise.prototype.catch 方法返回的是 promise,所以它们可以被链式调用。
如果一个 promise 已经被兑现或被拒绝,那么我们也可以说它处于 已敲定(settled) 状态。你还会听到一个经常跟 promise 一起使用的术语:已决议(resolved),它表示 promise 已经处于已敲定状态,或者为了匹配另一个 promise 的状态被“锁定”了。
Promise 的链式调用
我们可以用 Promise.prototype.then()、Promise.prototype.catch() 和 Promise.prototype.finally() 这些方法将进一步的操作与一个变为已敲定状态的 promise 关联起来。
例如 .then() 方法需要两个参数,第一个参数作为处理已兑现状态的回调函数,而第二个参数则作为处理已拒绝状态的回调函数。每一个 .then() 方法还会返回一个新生成的 promise 对象,这个对象可被用作链式调用,就像这样:
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('foo');
}, 300);
});
myPromise
.then(value => { return value + ' and bar'; })
.then(value => { return value + ' and bar again'; })
.then(value => { return value + ' and again'; })
.then(value => { return value + ' and again'; })
.then(value => { console.log(value) })
.catch(err => { console.log(err) });
// foo and bar and bar again and again and again
链式调用中的 promise 们就像俄罗斯套娃一样,是嵌套起来的,但又像是一个栈,每个都必须从顶端被弹出。链式调用中的第一个 promise 是嵌套最深的一个,也将是第一个被弹出的。
(promise D, (promise C, (promise B, (promise A) ) ) )
当存在一个 nextValue 是 promise 时,就会出现一种动态的替换效果。return 会导致一个 promise 被弹出,但这个 nextValue promise 则会被推入被弹出 promise 原来的位置。对于上面所示的嵌套场景,假设与 "promise B" 相关的 .then() 返回了一个值为 "promise X" 的 nextValue 。那么嵌套的结果看起来就会是这样:
(promise D, (promise C, (promise X) ) )
静态方法:
Promise.all(iterable)
这个方法返回一个新的 promise 对象,等到所有的 promise 对象都成功或有任意一个 promise 失败。
如果所有的 promise 都成功了,它会把一个包含 iterable 里所有 promise 返回值的数组作为成功回调的返回值。顺序跟 iterable 的顺序保持一致。
一旦有任意一个 iterable 里面的 promise 对象失败则立即以该 promise 对象失败的理由来拒绝这个新的 promise。
Promise.allSettled(iterable)
等到所有 promise 都已敲定(每个 promise 都已兑现或已拒绝)。
返回一个 promise,该 promise 在所有 promise 都敲定后完成,并兑现一个对象数组,其中的对象对应每个 promise 的结果。
Promise.any(iterable)
接收一个 promise 对象的集合,当其中的任意一个 promise 成功,就返回那个成功的 promise 的值。
Promise.race(iterable)
等到任意一个 promise 的状态变为已敲定。
当 iterable 参数里的任意一个子 promise 成功或失败后,父 promise 马上也会用子 promise 的成功返回值或失败详情作为参数调用父 promise 绑定的相应处理函数,并返回该 promise 对象。
Promise.reject(reason)
返回一个状态为已拒绝的 Promise 对象,并将给定的失败信息传递给对应的处理函数。
Promise.resolve(value)
返回一个状态由给定 value 决定的 Promise 对象。如果该值是 thenable(即,带有 then 方法的对象),返回的 Promise 对象的最终状态由 then 方法执行结果决定;否则,返回的 Promise 对象状态为已兑现,并且将该 value 传递给对应的 then 方法。
18、Math + Number + String + Array + Object APIs
Number.EPSILON
Number.isInteger(Infinity) // false
Number.isNaN("NaN") // false
Math.acosh(3) // 1.762747174039086
Math.hypot(3, 4) // 5
Math.imul(Math.pow(2, 32) - 1, Math.pow(2, 32) - 2) // 2
"abcde".includes("cd") // true
"abc".repeat(3) // "abcabcabc"
Array.from(document.querySelectorAll('*')) // Returns a real Array
Array.of(1, 2, 3) // Similar to new Array(...), but without special one-arg behavior
[0, 0, 0].fill(7, 1) // [0,7,7]
[1, 2, 3].find(x => x == 3) // 3
[1, 2, 3].findIndex(x => x == 2) // 1
[1, 2, 3, 4, 5].copyWithin(3, 0) // [1, 2, 3, 1, 2]
["a", "b", "c"].entries() // iterator [0, "a"], [1,"b"], [2,"c"]
["a", "b", "c"].keys() // iterator 0, 1, 2
["a", "b", "c"].values() // iterator "a", "b", "c"
Object.assign(Point, { origin: new Point(0,0) })
19、二进制,八进制
为二进制(b)和八进制(o)添加了两种新的数字文字形式。
0b111110111 === 503 // true
0o767 === 503 // true
20、Reflect API
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 proxy handler (en-US) 的方法相同。Reflect 不是一个函数对象,因此它是不可构造的。
与大多数全局对象不同 Reflect 并非一个构造函数,所以不能通过 new 运算符对其进行调用,或者将 Reflect 对象作为一个函数来调用。Reflect 的所有属性和方法都是静态的(就像 Math 对象)。
Reflect.apply(target, thisArgument, argumentsList)
对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和 Function.prototype.apply() 功能类似。
Reflect.construct(target, argumentsList[, newTarget])
对构造函数进行 new 操作,相当于执行 new target(...args)。
Reflect.defineProperty(target, propertyKey, attributes)
和 Object.defineProperty() 类似。如果设置成功就会返回 true
Reflect.deleteProperty(target, propertyKey)
作为函数的delete操作符,相当于执行 delete target[name]。
Reflect.get(target, propertyKey[, receiver])
获取对象身上某个属性的值,类似于 target[name]。
Reflect.getOwnPropertyDescriptor(target, propertyKey)
类似于 Object.getOwnPropertyDescriptor()。如果对象中存在该属性,则返回对应的属性描述符,否则返回 undefined。
Reflect.getPrototypeOf(target)
类似于 Object.getPrototypeOf()。
Reflect.has(target, propertyKey)
判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。
Reflect.isExtensible(target)
类似于 Object.isExtensible().
Reflect.ownKeys(target)
返回一个包含所有自身属性(不包含继承属性)的数组。(类似于 Object.keys(), 但不会受enumerable 影响).
Reflect.preventExtensions(target)
类似于 Object.preventExtensions()。返回一个Boolean。
Reflect.set(target, propertyKey, value[, receiver])
将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
Reflect.setPrototypeOf(target, prototype)
设置对象原型的函数。返回一个 Boolean,如果更新成功,则返回 true。
const duck = {
name: 'Maurice',
color: 'white',
greeting: function() {
console.log(`Quaaaack! My name is ${this.name}`);
}
}
Reflect.has(duck, 'color');
// true
Reflect.has(duck, 'haircut');
// false
Reflect.ownKeys(duck);
// [ "name", "color", "greeting" ]
Reflect.ownKeys(duck);
// [ "name", "color", "greeting" ]
21、尾递归
尾部位置的调用保证不会无限增长堆栈。使递归算法在面对没有超出边界的输入时是安全的。
之前直接用递归实现:
function factorial(n){
if(n === 1) return 1;
return n * factorial( n -1 );
}
使用return 返回的是一个表达式,会在堆栈中进行存储,当数超过堆栈限定的值时,会导致堆栈溢出。
使用尾递归实现:
function factorial(n, acc = 1) {
'use strict';
if (n <= 1) return acc;
return factorial(n - 1, n * acc);
}
// 当今大多数实现中会导致堆栈溢出,
// 但在ES6中的任意输入上是安全的
factorial(100000)