一. let/const:javascript
1. “暂时性死区”概念:在代码块内,使用let/const
命令声明变量以前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。“暂时性死区”也意味着typeof
再也不是一个百分之百安全的操做。html
2. 块做用域与函数声明:java
function f() { console.log('I am outside!'); } (function () { if (false) { // 重复声明一次函数f function f() { console.log('I am inside!'); } } f(); }()); // I am inside!
上面代码至关于如下代码:node
// ES5 环境 function f() { console.log('I am outside!'); } (function () { function f() { console.log('I am inside!'); } if (false) { } f(); }());
在 ES5 中运行,会获得“I am inside!”,由于在if
内声明的函数f
会被提高到函数头部。ES6 就彻底不同了,理论上会获得“I am outside!”。由于块级做用域内声明的函数相似于let
,对做用域以外没有影响。可是,若是你真的在 ES6 浏览器中运行一下上面的代码,是会报错的。原来,若是改变了块级做用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻所以产生的不兼容问题,ES6 在附录 B里面规定,浏览器的实现能够不遵照上面的规定,有本身的行为方式。es6
var
,即会提高到全局做用域或函数做用域的头部。根据这三条规则,在浏览器的 ES6 环境中,块级做用域内声明的函数,行为相似于var
声明的变量。web
// 浏览器的 ES6 环境 function f() { console.log('I am outside!'); } (function () { if (false) { // 重复声明一次函数f function f() { console.log('I am inside!'); } } f(); }()); // Uncaught TypeError: f is not a function
上面的代码在符合 ES6 的浏览器中,都会报错,由于实际运行的是下面的代码:算法
// 浏览器的 ES6 环境 function f() { console.log('I am outside!'); } (function () { var f = undefined; if (false) { function f() { console.log('I am inside!'); } } f(); }()); // Uncaught TypeError: f is not a function
3. const:const
实际上保证的,并非变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,所以等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的(即老是指向另外一个固定的地址)。编程
4. 声明变量:json
ES6 一共有 6 种声明变量的方法:var, function, let, const, imort, class数组
5. var
命令和function
命令声明的全局变量,依旧是顶层对象的属性;另外一方面规定,let
命令、const
命令、class
命令声明的全局变量,不属于顶层对象的属性:
var a = 1; // 若是在 Node 的 REPL 环境,能够写成 global.a // 或者采用通用方法,写成 this.a window.a // 1 let b = 1; window.b // undefined
二. 解构赋值:数组/对象/字符串/数组/布尔值
1. 只要某种数据结构具备 Iterator 接口,均可以采用数组形式的解构赋值(数组,Set,Generator等)。
2. 原则:解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。因为undefined
和null
没法转为对象,因此对它们进行解构赋值,都会报错
3.
function move({x, y} = { x: 0, y: 0 }) { return [x, y]; } move({x: 3, y: 8}); // [3, 8] move({x: 3}); // [3, undefined] move({}); // [undefined, undefined] move(); // [0, 0]
4. 解构赋值可使用圆括号的状况:只有一种:赋值语句的非模式部分,可使用圆括号。
[(b)] = [3]; // 正确 ({ p: (d) } = {}); // 正确 [(parseInt.prop)] = [3]; // 正确
上面三行语句均可以正确执行,由于首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是p
,而不是d
;第三行语句与第一行语句的性质一致。
三. 字符串扩展:
1. 模板字符串:
(1)模板字符串之中还能调用函数。若是大括号中的值不是字符串,将按照通常的规则转为字符串。好比,大括号中是一个对象,将默认调用对象的toString
方法。(模板字符串的大括号内部,就是执行 JavaScript 代码)
function fn() { return "Hello World"; } `foo ${fn()} bar` // foo Hello World bar
(2)标签模板:紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template)。
alert`123` // 等同于 alert(123)
let a = 5; let b = 10; tag`Hello ${ a + b } world ${ a * b }`; // 等同于 tag(['Hello ', ' world ', ''], 15, 50);
四. 函数的扩展:
1. 函数的 length 属性:
(function (a) {}).length // 1 (function (a = 5) {}).length // 0 (function (a, b, c = 5) {}).length // 2
上面代码中,length
属性的返回值,等于函数的参数个数减去指定了默认值的参数个数。这是由于length
属性的含义是,该函数预期传入的参数个数。某个参数指定默认值之后,预期传入的参数个数就不包括这个参数了。同理,后文的 rest 参数也不会计入length
属性。
(function(...args) {}).length // 0
若是设置了默认值的参数不是尾参数,那么length
属性也再也不计入后面的参数了。
(function (a = 0, b, c) {}).length // 0 (function (a, b = 1, c) {}).length // 1
2. 做用域:
一旦设置了参数的默认值,函数进行声明初始化时,参数会造成一个单独的做用域(context)。等到初始化结束,这个做用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
var x = 1; function f(x, y = x) { console.log(y); } f(2) // 2
var x = 1; function foo(x, y = function() { x = 2; }) { x = 3; y(); console.log(x); } foo() // 2 x // 1
3. rest 参数:
// 正确的 function push(array, ...items) { items.forEach(function(item) { array.push(item); console.log(item); }); } var a = []; push(a, 1, 2, 3) // 报错 function f(a, ...b, c) { // ... }
length
属性,不包括 rest 参数。(function(a) {}).length // 1 (function(...a) {}).length // 0 (function(a, ...b) {}).length // 1
4. 严格模式:只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,不然会报错。
函数执行的时候,先执行函数参数,而后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,可是参数却应该先于函数体执行。
// 报错 function doSomething(value = 070) { 'use strict'; return value; }
5. name 属性:
Function
构造函数返回的函数实例,name
属性的值为anonymous
。(new Function).name // "anonymous"
bind
返回的函数,name
属性值会加上bound
前缀。function foo() {}; foo.bind({}).name // "bound foo" (function(){}).bind({}).name // "bound "
6. 箭头函数:
注意点:
(1)函数体内的this
对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不能够看成构造函数,也就是说,不可使用new
命令,不然会抛出一个错误。
(3)不可使用arguments
对象,该对象在函数体内不存在。若是要用,能够用 rest 参数代替。
(4)不可使用yield
命令,所以箭头函数不能用做 Generator 函数。
上面四点中,第一点尤为值得注意。this
对象的指向是可变的,可是在箭头函数中,它是固定的。
this
指向的固定化,并非由于箭头函数内部有绑定this
的机制,实际缘由是箭头函数根本没有本身的this
,致使内部的this
就是外层代码块的this
。正是由于它没有this
,因此也就不能用做构造函数。
function Timer() { this.s1 = 0; this.s2 = 0; // 箭头函数 setInterval(() => this.s1++, 1000); // 普通函数 setInterval(function () { this.s2++; }, 1000); } var timer = new Timer(); setTimeout(() => console.log('s1: ', timer.s1), 3100); setTimeout(() => console.log('s2: ', timer.s2), 3100); // s1: 3 // s2: 0
下面的代码之中有几个this? function foo() { return () => { return () => { return () => { console.log('id:', this.id); }; }; }; } var f = foo.call({id: 1}); 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
,就是函数foo
的this
,因此t1
、t2
、t3
都输出一样的结果。由于全部的内层函数都是箭头函数,都没有本身的this
,它们的this
其实都是最外层foo
函数的this
。
除了this
,如下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments
、super
、new.target
。
因为箭头函数没有本身的this
,因此固然也就不能用call()
、apply()
、bind()
这些方法去改变this
的指向。
(function() { return [ (() => this.x).bind({ x: 'inner' })() ]; }).call({ x: 'outer' }); // ['outer']
注:不适合使用箭头函数的场合:
(1)定义函数的方法,且该方法内部包括this
。
const cat = { lives: 9, jumps: () => { this.lives--; } }
上面代码中,cat.jumps()
方法是一个箭头函数,这是错误的。调用cat.jumps()
时,若是是普通函数,该方法内部的this
指向cat
;若是写成上面那样的箭头函数,使得this
指向全局对象,所以不会获得预期结果。
(2)须要动态this
的时候,也不该使用箭头函数。
var button = document.getElementById('press'); button.addEventListener('click', () => { this.classList.toggle('on'); });
上面代码运行时,点击按钮会报错,由于button
的监听函数是一个箭头函数,致使里面的this
就是全局对象。若是改为普通函数,this
就会动态指向被点击的按钮对象。
7. 嵌套的箭头函数:
部署管道机制(pipeline)的例子,即前一个函数的输出是后一个函数的输入:
const pipeline = (...funcs) => val => funcs.reduce((a, b) => b(a), val); const plus1 = a => a + 1; const mult2 = a => a * 2; const addThenMult = pipeline(plus1, mult2); addThenMult(5) // 12
若是以为上面的写法可读性比较差,也能够采用下面的写法。
const plus1 = a => a + 1; const mult2 = a => a * 2; mult2(plus1(5)) // 12
8. 尾调用优化:
尾调用:指某个函数的最后一步是调用另外一个函数。尾调用不必定出如今函数尾部,只要是最后一步操做便可。
// 如下都不属于尾调用 // 状况一 function f(x){ let y = g(x); return y; } // 状况二 function f(x){ return g(x) + 1; } // 状况三 function f(x){ g(x); }
// 状况三至关于如下
function f(x){ g(x); return undefined; }
function f(x) { if (x > 0) { return m(x) } return n(x); }
上面代码中,函数m
和n
都属于尾调用,由于它们都是函数f
的最后一步操做。
9. 尾递归:
递归很是耗费内存,由于须要同时保存成千上百个调用记录,很容易发生"栈溢出"错误(stack overflow)。但对于尾递归来讲,因为只存在一个调用记录,因此永远不会发生"栈溢出"错误。
function factorial(n) { if (n === 1) return 1; return n * factorial(n - 1); } factorial(5) // 120
上面代码是一个阶乘函数,计算n的阶乘,最多须要保存n个调用记录,复杂度 O(n) 。
若是改写成尾递归,只保留一个调用记录,复杂度 O(1) 。
function factorial(n, total) { if (n === 1) return total; return factorial(n - 1, n * total); } factorial(5, 1) // 120
function factorial(n, total = 1) { if (n === 1) return total; return factorial(n - 1, n * total); } factorial(5) // 120
上面代码中,参数 total 有默认值1,因此调用时不用提供这个值。
总结一下,递归本质上是一种循环操做。纯粹的函数式编程语言没有循环操做命令,全部的循环都用递归实现,这就是为何尾递归对这些语言极其重要。对于其余支持"尾调用优化"的语言(好比Lua,ES6),只须要知道循环能够用递归代替,而一旦使用递归,就最好使用尾递归。
五. 数组的扩展:
1. 扩展运算符:
扩展运算符(spread)是三个点(...
)。它比如 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
console.log(...[1, 2, 3]) // 1 2 3 console.log(1, ...[2, 3, 4], 5) // 1 2 3 4 5 [...document.querySelectorAll('div')] // [<div>, <div>, <div>]
若是扩展运算符后面是一个空数组,则不产生任何效果。
扩展运算符若是放在括号中,JavaScript 引擎就会认为这是函数调用。若是这时不是函数调用,就会报错。
(...[1, 2]) // Uncaught SyntaxError: Unexpected number console.log((...[1, 2])) // Uncaught SyntaxError: Unexpected number console.log(...[1, 2]) // 1 2
扩展运算符内部调用的是数据结构的 Iterator 接口,所以只要具备 Iterator 接口的对象,均可以使用扩展运算符,好比 Map 结构。
let map = new Map([ [1, 'one'], [2, 'two'], [3, 'three'], ]); let arr = [...map.keys()]; // [1, 2, 3]
2. Array.from():
(1) Array.from
方法用于将两类对象转为真正的数组:相似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
(2) Array.from()
的另外一个应用是,将字符串转为数组,而后返回字符串的长度。由于它能正确处理各类 Unicode 字符,能够避免 JavaScript 将大于\uFFFF
的 Unicode 字符,算做两个字符的 bug。
3. Array.of():Array.of
方法用于将一组值,转换为数组。Array.of
老是返回参数值组成的数组。若是没有参数,就返回一个空数组。
4. copyWithin():
数组实例的copyWithin
方法,在当前数组内部,将指定位置的成员复制到其余位置(会覆盖原有成员),而后返回当前数组。也就是说,使用这个方法,会修改当前数组。
Array.prototype.copyWithin(target, start = 0, end = this.length)
它接受三个参数。
这三个参数都应该是数值,若是不是,会自动转为数值。
[1, 2, 3, 4, 5].copyWithin(0, 3) // [4, 5, 3, 4, 5]
上面代码表示将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2。
5. find() / findIndex():
数组实例的find
方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,全部数组成员依次执行该回调函数,直到找出第一个返回值为true
的成员,而后返回该成员。若是没有符合条件的成员,则返回undefined
。
数组实例的findIndex
方法的用法与find
方法很是相似,返回第一个符合条件的数组成员的位置,若是全部成员都不符合条件,则返回-1
。
[NaN].indexOf(NaN) // -1 [NaN].findIndex(y => Object.is(NaN, y)) // 0
上面代码中,indexOf
方法没法识别数组的NaN
成员,可是findIndex
方法能够借助Object.is
方法作到。
6. fill():给定值,填充一个数组。接受第二个和第三个参数,用于指定填充的起始位置和结束位置。
['a', 'b', 'c'].fill(7) // [7, 7, 7] new Array(3).fill(7) // [7, 7, 7] ['a', 'b', 'c'].fill(7, 1, 2) // ['a', 7, 'c']
7. entries() / keys() / values() :
ES6 提供三个新的方法——entries()
,keys()
和values()
——用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),能够用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']
8. includes():Array.prototype.includes
方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes
方法相似。第二个参数表示搜索的起始位置,默认为0
。
[1, 2, 3].includes(2) // true [1, 2, 3].includes(4) // false [1, 2, NaN].includes(NaN) // true [1, 2, 3].includes(2) // true [1, 2, 3].includes(4) // false [1, 2, NaN].includes(NaN) // true
indexOf
方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,因此要去比较是否不等于-1
,表达起来不够直观。二是,它内部使用严格相等运算符(===
)进行判断,这会致使对NaN
的误判。
下面代码用来检查当前环境是否支持该方法,若是不支持,部署一个简易的替代版本。
const contains = (() => Array.prototype.includes ? (arr, value) => arr.includes(value) : (arr, value) => arr.some(el => el === value) )(); contains(['foo', 'bar'], 'baz'); // => false
另外,Map 和 Set 数据结构有一个has
方法,须要注意与includes
区分。
has
方法,是用来查找键名的,好比Map.prototype.has(key)
、WeakMap.prototype.has(key)
、Reflect.has(target, propertyKey)
。has
方法,是用来查找值的,好比Set.prototype.has(value)
、WeakSet.prototype.has(value)
。9. flat() / flatMap():
数组的成员有时仍是数组,Array.prototype.flat()
用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。
[1, 2, [3, 4]].flat() // [1, 2, 3, 4]
flat()
默认只会“拉平”一层,若是想要“拉平”多层的嵌套数组,能够将flat()
方法的参数写成一个整数,表示想要拉平的层数,默认为1。若是无论有多少层嵌套,都要转成一维数组,能够用Infinity
关键字做为参数。
[1, 2, [3, [4, 5]]].flat() // [1, 2, 3, [4, 5]] [1, 2, [3, [4, 5]]].flat(2) // [1, 2, 3, 4, 5]
[1, [2, [3]]].flat(Infinity) // [1, 2, 3]
flatMap()
方法对原数组的每一个成员执行一个函数(至关于执行Array.prototype.map()
),而后对返回值组成的数组执行flat()
方法。该方法返回一个新数组,不改变原数组。flatMap()
只能展开一层数组。
// 至关于 [[2, 4], [3, 6], [4, 8]].flat() [2, 3, 4].flatMap((x) => [x, x * 2]) // [2, 4, 3, 6, 4, 8]
flatMap()
方法的参数是一个遍历函数,该函数能够接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。还能够有第二个参数,用来绑定遍历函数里面的this
。
arr.flatMap(function callback(currentValue[, index[, array]]) { // ... }[, thisArg])
10. 数组的空位:
数组的空位指,数组的某一个位置没有任何值。好比,Array
构造函数返回的数组都是空位。空位不是undefined
,一个位置的值等于undefined
,依然是有值的。空位是没有任何值,in
运算符能够说明这一点。
0 in [undefined, undefined, undefined] // true 0 in [, , ,] // false
ES5 对空位的处理,已经很不一致了,大多数状况下会忽略空位。
forEach()
, filter()
, reduce()
, every()
和some()
都会跳过空位。map()
会跳过空位,但会保留这个值join()
和toString()
会将空位视为undefined
,而undefined
和null
会被处理成空字符串。// forEach方法 [,'a'].forEach((x,i) => console.log(i)); // 1 // filter方法 ['a',,'b'].filter(x => true) // ['a','b'] // every方法 [,'a'].every(x => x==='a') // true // reduce方法 [1,,2].reduce((x,y) => x+y) // 3 // some方法 [,'a'].some(x => x !== 'a') // false // map方法 [,'a'].map(x => 1) // [,1] // join方法 [,'a',undefined,null].join('#') // "#a##" // toString方法 [,'a',undefined,null].toString() // ",a,,"
ES6 则是明确将空位转为undefined
。好比 Array.from():
Array.from(['a',,'b']) // [ "a", undefined, "b" ]
但不是全部 ES6 的方法对于空位的处理规则都是一致的,因此建议避免出现空位。
六. 对象的扩展:
1. 表达式还能够用于定义方法名。
let obj = { ['h' + 'ello']() { return 'hi'; } }; obj.hello() // hi
注意,属性名表达式与简洁表示法,不能同时使用,会报错。
// 报错 const foo = 'bar'; const bar = 'abc'; const baz = { [foo] }; // 正确 const foo = 'bar'; const baz = { [foo]: 'abc'};
注意,属性名表达式若是是一个对象,默认状况下会自动将对象转为字符串[object Object]
,这一点要特别当心。
const keyA = {a: 1}; const keyB = {b: 2}; const myObject = { [keyA]: 'valueA', [keyB]: 'valueB' }; myObject // Object {[object Object]: "valueB"}
2. 若是对象的方法使用了取值函数(getter
)和存值函数(setter
),则name
属性不是在该方法上面,而是该方法的属性的描述对象的get
和set
属性上面,返回值是方法名前加上get
和set
。
const obj = { get foo() {}, set foo(x) {} }; obj.foo.name // TypeError: Cannot read property 'name' of undefined const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo'); descriptor.get.name // "get foo" descriptor.set.name // "set foo"
若是对象的方法是一个 Symbol 值,那么name
属性返回的是这个 Symbol 值的描述。
const key1 = Symbol('description'); const key2 = Symbol(); let obj = { [key1]() {}, [key2]() {}, }; obj[key1].name // "[description]" obj[key2].name // ""
ES6 规定,全部 Class 的原型的方法都是不可枚举的。总的来讲,操做中引入继承的属性会让问题复杂化,大多数时候,咱们只关心对象自身的属性。因此,尽可能不要用for...in
循环,而用Object.keys()
代替。
3. 属性的遍历:for...in / Object.keys(obj) / Object.getOwnPropertyNames(obj) / Object.getOwnPropertySymblos(obj) / Reflect.ownKeys(obj):
以上的 5 种方法遍历对象的键名,都遵照一样的属性遍历的次序规则。
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 }) // ['2', '10', 'b', 'a', Symbol()]
附:Reflect.ownKeys
返回一个数组,包含对象自身的全部键名,无论键名是 Symbol 或字符串,也无论是否可枚举。
4. super 关键字:super
指向当前对象的原型对象
const proto = { foo: 'hello' }; const obj = { foo: 'world', find() { return super.foo; } }; Object.setPrototypeOf(obj, proto); obj.find() // "hello"
注意:super必须
用在对象的方法之中,只有对象方法的简写法可让 JavaScript 引擎确认,定义的是对象的方法。
JavaScript 引擎内部,super.foo
等同于 Object.getPrototypeOf(this).foo
(属性)或 Object.getPrototypeOf(this).foo.call(this)
(方法)。
5. 对象的解构赋值:
对象的解构赋值用于从一个对象取值,至关于将目标对象自身的全部可遍历的(enumerable)、但还没有被读取的属性,分配到指定的对象上面。全部的键和它们的值,都会拷贝到新对象上面。
因为解构赋值要求等号右边是一个对象,因此若是等号右边是undefined
或null
,就会报错,由于它们没法转为对象。
let { x, y, ...z } = null; // 运行时错误 let { x, y, ...z } = undefined; // 运行时错误
另外,扩展运算符的解构赋值,不能复制继承自原型对象的属性。
与数组的扩展运算符同样,对象的扩展运算符后面能够跟表达式。
const obj = { ...(x > 1 ? {a: 1} : {}), b: 2, };
扩展运算符的参数对象之中,若是有取值函数get
,这个函数是会执行的。
// 并不会抛出错误,由于 x 属性只是被定义,但没执行 let aWithXGetter = { ...a, get x() { throw new Error('not throw yet'); } }; // 会抛出错误,由于 x 属性被执行了 let runtimeError = { ...a, ...{ get x() { throw new Error('throw now'); } } };
6. Object.is():比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。不一样之处只有两个:一是+0
不等于-0
,二是NaN
等于自身。
Object.is('foo', 'foo') // true Object.is({}, {}) // false +0 === -0 //true NaN === NaN // false Object.is(+0, -0) // false Object.is(NaN, NaN) // true
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 });
7. Object.assign():用于对象的合并,将源对象(source)的全部可枚举属性,复制到目标对象(target)。
因为undefined
和null
没法转成对象,因此若是它们做为首参数,就会报错。
Object.assign(undefined) // 报错 Object.assign(null) // 报错
let obj = {a: 1}; Object.assign(obj, undefined) === obj // true Object.assign(obj, null) === obj // true
Object.assign
拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false
)。
附:
Object.getPrototypeOf()
方法返回指定对象的原型(内部[[Prototype]]
属性的值)。
Object.create()
方法建立一个新对象,使用现有的对象来提供新建立的对象的__proto__。
8. 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" }
附:URLSearchParams

七. Symbol:新的原始数据类型Symbol
,表示独一无二的值。
1. Symbol 函数前不能使用 new 命令,不然会报错。这是由于生成的 Symbol 是一个原始类型的值,不是对象。也就是说,因为 Symbol 值不是对象,因此不能添加属性。基本上,它是一种相似于字符串的数据类型。
2. 属性名的遍历:
Symbol 做为属性名,该属性不会出如今for...in
、for...of
循环中,也不会被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回。可是,它也不是私有属性,有一个Object.getOwnPropertySymbols
方法,能够获取指定对象的全部 Symbol 属性名。
const obj = {}; let a = Symbol('a'); let b = Symbol('b'); obj[a] = 'Hello'; obj[b] = 'World'; const objectSymbols = Object.getOwnPropertySymbols(obj); objectSymbols // [Symbol(a), Symbol(b)]
因为以 Symbol 值做为名称的属性,不会被常规方法遍历获得。咱们能够利用这个特性,为对象定义一些非私有的、但又但愿只用于内部的方法。
let size = Symbol('size'); class Collection { constructor() { this[size] = 0; } add(item) { this[this[size]] = item; this[size]++; } static sizeOf(instance) { return instance[size]; } } let x = new Collection(); Collection.sizeOf(x) // 0 x.add('foo'); Collection.sizeOf(x) // 1 Object.keys(x) // ['0'] Object.getOwnPropertyNames(x) // ['0'] Object.getOwnPropertySymbols(x) // [Symbol(size)]
上面代码中,对象x
的size
属性是一个 Symbol 值,因此Object.keys(x)
、Object.getOwnPropertyNames(x)
都没法获取它。这就形成了一种非私有的内部方法的效果。
3. Symbol.for() / Symbol.keyFor():
Symbol.for():接受一个字符串做为参数,而后搜索有没有以该参数做为名称的 Symbol 值。若是有,就返回这个 Symbol 值,不然就新建并返回一个以该字符串为名称的 Symbol 值。
Symbol.keyFor():返回一个已登记的 Symbol 类型值的 key。
let s1 = Symbol.for('foo'); let s2 = Symbol.for('foo'); s1 === s2 // true // 不一样于 Symbol() Symbol.for("bar") === Symbol.for("bar") // true Symbol("bar") === Symbol("bar") // false
let s1 = Symbol.for("foo"); Symbol.keyFor(s1) // "foo" let s2 = Symbol("foo"); Symbol.keyFor(s2) // undefined
4. 内置的 Symbol 值(11个):
Symbol.hasInstance / Symbol.isConcatSpreadable / Symbol.species / Symbol.match等
例:对象的Symbol.hasInstance
属性,指向一个内部方法。当其余对象使用instanceof
运算符,判断是否为该对象的实例时,会调用这个方法。好比,foo instanceof Foo
在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)
。
class MyClass { [Symbol.hasInstance](foo) { return foo instanceof Array; } } [1, 2, 3] instanceof new MyClass() // true
八. Set 和 Map 数据结构:
1. Set:Set
自己是一个构造函数,用来生成 Set 数据结构。
Set 函数能够接受一个数组(或者具备 iterable 接口的其余数据结构)做为参数,用来初始化,且 Set 结构不会添加剧复的值。
// 例一 const set = new Set([1, 2, 3, 4, 4]); [...set] // [1, 2, 3, 4] // 例二 const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]); items.size // 5 // 例三 const set = new Set(document.querySelectorAll('div')); set.size // 56 // 相似于 const set = new Set(); document .querySelectorAll('div') .forEach(div => set.add(div)); set.size // 56 [...new Set('ababbc')].join('') // "abc"
向 Set 加入值的时候,不会发生类型转换,Set 内部判断两个值是否不一样,使用的算法叫作“Same-value-zero equality”,它相似于精确相等运算符(===
),主要的区别是NaN
等于自身。
2. Set 实例的属性和方法:
属性:size 至关于数组的 length;
方法:操做方法(用于操做数据)和遍历方法(用于遍历成员)。
操做方法:
add(value)
:添加某个值,返回 Set 结构自己。delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。has(value)
:返回一个布尔值,表示该值是否为Set
的成员。clear()
:清除全部成员,没有返回值。 遍历方法:(Set
的遍历顺序就是插入顺序)
keys()
:返回键名的遍历器values()
:返回键值的遍历器entries()
:返回键值对的遍历器forEach()
:使用回调函数遍历每一个成员let set = new Set(['red', 'green', 'blue']); for (let item of set.keys()) { console.log(item); } // red // green // blue for (let item of set.values()) { console.log(item); } // red // green // blue for (let item of set.entries()) { console.log(item); } // ["red", "red"] // ["green", "green"] // ["blue", "blue"]
因为 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),keys
方法和values
方法的行为彻底一致。
// Set 结构的实例默承认遍历,它的默认遍历器生成函数就是它的values方法 =》这意味着,能够省略values方法,直接用for...of循环遍历 Set Set.prototype[Symbol.iterator] === Set.prototype.values // true let set = new Set(['red', 'green', 'blue']); for (let x of set) { console.log(x); } // red // green // blue
3. WeakSet:结构与 Set 相似,也是不重复的值的集合,但成员只能是对象,而不能是其余类型的值;WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,若是其余对象都再也不引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
=》WeakSet 不能遍历,是由于成员都是弱引用,随时可能消失,遍历机制没法保证成员的存在
// 做为构造函数,WeakSet 能够接受一个数组或相似数组的对象做为参数。(实际上,任何具备 Iterable 接口的对象,均可以做为 WeakSet 的参数。)该数组的全部成员,都会自动成为 WeakSet 实例对象的成员。 const a = [[1, 2], [3, 4]]; const ws = new WeakSet(a); // WeakSet {[1, 2], [3, 4]} const b = [3, 4]; const ws = new WeakSet(b); // Uncaught TypeError: Invalid value used in weak set(…)
一些方法:
4. Map:键值对集合,区别于普通 Object 的一点就是,该键能够是任意类型,好比对象;
(1) 不只仅是数组,任何具备 Iterator 接口、且每一个成员都是一个双元素的数组的数据结构(详见《Iterator》一章)均可以看成 Map 构造函数的参数。这就是说,Set 和 Map 均可以用来生成新的 Map。
(2) 若是对同一个键屡次赋值,后面的值将覆盖前面的值。但只有对同一个对象的引用,Map 结构才将其视为同一个键。
// set和get方法,表面是针对同一个键,但实际上这是两个值,内存地址是不同的,所以get方法没法读取该键,返回undefined const map = new Map(); map.set(['a'], 555); map.get(['a']) // undefined
(3) 一些属性/方法:
size属性:返回 Map 结构的成员总数;
set(key, value):set
方法设置键名key
对应的键值为value
,而后返回整个 Map 结构(所以可链式写法)。若是key
已经有值,则键值会被更新,不然就新生成该键;
get(key):读取key
对应的键值,若是找不到key
,返回 undefined;
has(key):返回一个布尔值,表示某个键是否在当前 Map 对象之中;
delete(key):删除某个键,返回true
。若是删除失败,返回false;
clear():清除全部成员,没有返回值;
keys():返回键名的遍历器;
values()
:返回键值的遍历器;
entries()
:返回全部成员的遍历器;
forEach()
:遍历 Map 的全部成员;
注:Map 的遍历顺序就是插入顺序; Map 结构的默认遍历器接口(Symbol.iterator
属性),就是entries
方法。
(4) Map 转为数组:使用扩展运算符(...
),转为数组后才可使用数组相关 map() / filter() 方法;
5. WeakMap:结构与 Map 结构相似,也是用于生成键值对的集合,2 点区别:WeakMap
只接受对象做为键名(null
除外),不接受其余类型的值做为键名;WeakMap
的键名所指向的对象,不计入垃圾回收机制。
基本上,若是你要往对象上添加数据,又不想干扰垃圾回收机制,就可使用 WeakMap。一个典型应用场景是,在网页的 DOM 元素上添加数据,就可使用WeakMap
结构。当该 DOM 元素被清除,其所对应的WeakMap
记录就会自动被移除。
注:WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。
const wm = new WeakMap(); let key = {}; let obj = {foo: 1}; wm.set(key, obj); obj = null; wm.get(key) // Object {foo: 1}
上面代码中,键值obj
是正常引用。因此,即便在 WeakMap 外部消除了obj
的引用,WeakMap 内部的引用依然存在。
一些方法:WeakMap
只有四个方法可用:get()
、set()
、has()
、delete()
。
// Countdown类的两个内部属性_counter和_action,是实例的弱引用,因此若是删除实例,它们也就随之消失,不会形成内存泄漏 const _counter = new WeakMap(); const _action = new WeakMap(); class Countdown { constructor(counter, action) { _counter.set(this, counter); _action.set(this, action); } dec() { let counter = _counter.get(this); if (counter < 1) return; counter--; _counter.set(this, counter); if (counter === 0) { _action.get(this)(); } } } const c = new Countdown(2, () => console.log('DONE')); c.dec()
九. Proxy:
1. Proxy 能够理解成,在目标对象以前架设一层“拦截”,外界对该对象的访问,都必须先经过这层拦截,所以提供了一种机制,能够对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操做,能够译为“代理器”。
Proxy 对象的全部用法,都是下面这种形式,不一样的只是handler
参数的写法。其中,new Proxy()
表示生成一个Proxy
实例,target
参数表示所要拦截的目标对象,handler
参数也是一个对象,用来定制拦截行为。
var proxy = new Proxy(target, handler);
var obj = new Proxy({}, { get: function (target, key, receiver) { // target:目标对象;key:key-value中的key值;receiver:当前proxy代理对象; console.log(`getting ${key}!`); return Reflect.get(target, key, receiver); }, set: function (target, key, value, receiver) { console.log(`setting ${key}!`); return Reflect.set(target, key, value, receiver); } });
// 如下为上述代码运行后结果 obj.count = 1 // setting count! ++obj.count // getting count! // setting count! // 2
var handler = { get: function(target, name) { if (name === 'prototype') { return Object.prototype; } return 'Hello, ' + name; }, // 参数:目标对象、目标对象的上下文对象和目标对象的参数数组 apply: function(target, thisBinding, args) { return args[0]; }, // 参数:target 目标对象;args:构造函数的参数对象;newTarget:创造实例对象时,new命令做用的构造函数 construct: function(target, args) { return {value: args[1]}; } }; var fproxy = new Proxy(function(x, y) { return x + y; }, handler); fproxy(1, 2) // 1 new fproxy(1, 2) // {value: 2} fproxy.prototype === Object.prototype // true fproxy.foo === "Hello, foo" // true
2. Proxy 的一些方法:
具体参数及使用方法:http://es6.ruanyifeng.com/#docs/proxy
proxy
对象getReceiver
属性是由对象提供的,receiver
指向proxy
对象。
若是一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,不然经过 Proxy 对象访问(get)该属性会报错。
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
若是目标对象自身的某个属性,不可写且不可配置,那么set
方法将不起做用,不是报错。
严格模式下,set
代理若是没有返回true
,就会报错(也就是若是没有return true就会报错)。
3. this 问题:
Proxy 代理的状况下,目标对象内部的this
关键字会指向 Proxy 代理。this
指向变化会致使 Proxy 没法代理目标对象。
十. Reflect:
1. Reflect
对象与Proxy
对象同样,也是 ES6 为了操做对象而提供的新 API。
设计目的:
(1) 将Object
对象的一些明显属于语言内部的方法(好比Object.defineProperty
),放到Reflect
对象上;
(2) 修改某些Object
方法的返回结果,让其变得更合理;好比,Object.defineProperty(obj, name, desc)
在没法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)
则会返回false;
(3) 让Object
操做都变成函数行为。某些Object
操做是命令式,好比name in obj
和delete obj[name]
,而Reflect.has(obj, name)
和Reflect.deleteProperty(obj, name)
让它们变成了函数行为;
(4) Reflect
对象的方法与Proxy
对象的方法一一对应,只要是Proxy
对象的方法,就能在Reflect
对象上找到对应的方法;
2. 静态方法:
3. 使用 Proxy 实现观察者模式:
观察者模式(Observer mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行。
// 思路是函数返回一个原始对象的 Proxy 代理,拦截赋值操做,触发充当观察者的各个函数
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; }observable
上面代码中,先定义了一个Set
集合,全部观察者函数都放进这个集合。而后,observable
函数返回原始对象的代理,拦截赋值操做。拦截函数set
之中,会自动执行全部观察者。
1. Promise: Promise 对象是一个构造函数;
2. Promise 对象的特色:
(1)对象的状态不受外界影响。Promise
对象表明一个异步操做,有三种状态:pending
(进行中)、fulfilled
(已成功)和rejected
(已失败);
(2)一旦状态改变,就不会再变,任什么时候候均可以获得这个结果。Promise
对象的状态改变,只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。只要这两种状况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型).
3. 使用:
const p1 = new Promise(function (resolve, reject) { setTimeout(() => reject(new Error('fail')), 3000) }) const p2 = new Promise(function (resolve, reject) { setTimeout(() => resolve(p1), 1000) }) p2 .then(result => console.log(result)) .catch(error => console.log(error)) // Error: fail
执行顺序:Promise 新建后就会当即执行。
let promise = new Promise(function(resolve, reject) { console.log('Promise'); resolve(); }); promise.then(function() { console.log('resolved.'); }); console.log('Hi!'); // Promise // Hi! // resolved
上面代码中,Promise 新建后当即执行,因此首先输出的是Promise
。而后,then
方法指定的回调函数,将在当前脚本全部同步任务执行完才会执行,因此resolved
最后输出。
new Promise((resolve, reject) => { resolve(1); console.log(2); }).then(r => { console.log(r); }); // 2 // 1
上面代码中,调用resolve(1)
之后,后面的console.log(2)
仍是会执行,而且会首先打印出来。这是由于当即 resolved 的 Promise 是在本轮事件循环的末尾执行,老是晚于本轮循环的同步任务。
通常来讲,调用resolve
或reject
之后,Promise 的使命就完成了,后继操做应该放到then
方法里面,而不该该直接写在resolve
或reject
的后面。因此,最好在它们前面加上return
语句,这样就不会有意外。
new Promise((resolve, reject) => { return resolve(1); // 后面的语句不会执行 console.log(2); })
4. Promise.prototype.then():
getJSON("/posts.json").then(function(json) { return json.post; }).then(function(post) { // ... });
上面的代码使用then
方法,依次指定了两个回调函数。第一个回调函数完成之后,会将返回结果做为参数,传入第二个回调函数。
5. “Promise 会吃掉错误”:Promise 内部的错误(任何报错)不会影响到 Promise 外部的代码;
6. Promise.prototype.finally():finally
方法用于指定无论 Promise 对象最后状态如何,都会执行的操做。
7. Promise.all():用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
Promise.all
方法接受一个数组做为参数,p1
、p2
、p3
都是 Promise 实例,若是不是,就会先调用下面讲到的Promise.resolve
方法,将参数转为 Promise 实例,再进一步处理。(Promise.all
方法的参数能够不是数组,但必须具备 Iterator 接口,且返回的每一个成员都是 Promise 实例。)
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 实例,本身定义了catch
方法,那么它一旦被rejected
,并不会触发Promise.all()
的catch
方法,以下:
const p1 = new Promise((resolve, reject) => { resolve('hello'); }) .then(result => result) .catch(e => e); const p2 = new Promise((resolve, reject) => { throw new Error('报错了'); }) .then(result => result) .catch(e => e); Promise.all([p1, p2]) .then(result => console.log(result)) .catch(e => console.log(e)); // ["hello", Error: 报错了]
上面代码中,p1
会resolved
,p2
首先会rejected
,可是p2
有本身的catch
方法,该方法返回的是一个新的 Promise 实例,p2
指向的其实是这个实例。该实例执行完catch
方法后,也会变成resolved
,致使Promise.all()
方法参数里面的两个实例都会resolved
,所以会调用then
方法指定的回调函数,而不会调用catch
方法指定的回调函数。若是p2
没有本身的catch
方法,就会调用Promise.all()
的catch
方法。
8. Promise.race():Promise.race
方法一样是将多个 Promise 实例,包装成一个新的 Promise 实例。区别在于:只要p1
、p2
、p3
之中有一个实例率先改变状态,p
的状态就跟着改变(或)。那个率先改变的 Promise 实例的返回值,就传递给p
的回调函数。
9. Promise.resolve():
当即resolve
的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。
setTimeout(function () { console.log('three'); }, 0); Promise.resolve().then(function () { console.log('two'); }); console.log('one'); // one // two // three
上面代码中,setTimeout(fn, 0)
在下一轮“事件循环”开始时执行,Promise.resolve()
在本轮“事件循环”结束时执行,console.log('one')
则是当即执行,所以最早输出。
10. Promise.reject():Promise.reject()
方法的参数,会原封不动地做为reject
的理由,变成后续方法的参数。这一点与Promise.resolve
方法不一致。
const thenable = { then(resolve, reject) { reject('出错了'); } }; Promise.reject(thenable) .catch(e => { console.log(e === thenable) }) // true
上面代码中,Promise.reject
方法的参数是一个thenable
对象,执行之后,后面catch
方法的参数不是reject
抛出的“出错了”这个字符串,而是thenable
对象。
11. Promise.try():Promise.try
就是模拟try
代码块,就像promise.catch
模拟的是catch
代码块。
十二. Iterator 和 for...of 循环:
1. 一个数据结构只要部署了Symbol.iterator
属性,就被视为具备 iterator 接口,就能够用for...of
循环遍历它的成员。也就是说,for...of
循环内部调用的是数据结构的Symbol.iterator
方法。
for...of
循环可使用的范围包括数组、Set 和 Map 结构、某些相似数组的对象(好比arguments
对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。
2. 原生具有 Iterator 接口的数据结构以下。
3. 遍历器(Iterator)就是这样一种机制。它是一种接口,为各类不一样的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就能够完成遍历操做(即依次处理该数据结构的全部成员)。
Iterator 的遍历过程是这样的。
(1)建立一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
(2)第一次调用指针对象的next
方法,能够将指针指向数据结构的第一个成员。
(3)第二次调用指针对象的next
方法,指针就指向数据结构的第二个成员。
(4)不断调用指针对象的next
方法,直到它指向数据结构的结束位置。
每一次调用next
方法,都会返回数据结构的当前成员的信息。具体来讲,就是返回一个包含value
和done
两个属性的对象。其中,value
属性是当前成员的值,done
属性是一个布尔值,表示遍历是否结束。
十三. Generator函数:
1. 语法上,首先能够把Generator 函数理解成,Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,仍是一个遍历器对象生成函数。返回的遍历器对象,能够依次遍历 Generator 函数内部的每个状态。
形式上,Generator 函数是一个普通函数,可是有两个特征。一是,function
关键字与函数名之间有一个星号;二是,函数体内部使用yield
表达式,定义不一样的内部状态(yield
在英语里的意思就是“产出”)。
function* helloWorldGenerator() { yield 'hello'; yield 'world'; return 'ending'; } var hw = helloWorldGenerator();
而后,Generator 函数的调用方法与普通函数同样,也是在函数名后面加上一对圆括号。不一样的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。
下一步,必须调用遍历器对象的next
方法,使得指针移向下一个状态。也就是说,每次调用next
方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield
表达式(或return
语句)为止。换言之,Generator 函数是分段执行的,yield
表达式是暂停执行的标记,而next
方法能够恢复执行。
hw.next() // { value: 'hello', done: false } hw.next() // { value: 'world', done: false } hw.next() // { value: 'ending', done: true } hw.next() // { value: undefined, done: true }
总结一下,调用 Generator 函数,返回一个遍历器对象,表明 Generator 函数的内部指针。之后,每次调用遍历器对象的next
方法,就会返回一个有着value
和done
两个属性的对象。value
属性表示当前的内部状态的值,是yield
表达式后面那个表达式的值;done
属性是一个布尔值,表示是否遍历结束。
2. yield 表达式:
因为 Generator 函数返回的遍历器对象,只有调用next
方法才会遍历下一个内部状态,因此其实提供了一种能够暂停执行的函数。yield
表达式就是暂停标志。
遍历器对象的next
方法的运行逻辑以下。
(1)遇到yield
表达式,就暂停执行后面的操做,并将紧跟在yield
后面的那个表达式的值,做为返回的对象的value
属性值。
(2)下一次调用next
方法时,再继续往下执行,直到遇到下一个yield
表达式。
(3)若是没有再遇到新的yield
表达式,就一直运行到函数结束,直到return
语句为止,并将return
语句后面的表达式的值,做为返回的对象的value
属性值。
(4)若是该函数没有return
语句,则返回的对象的value
属性值为undefined
。
须要注意的是,yield
表达式后面的表达式,只有当调用next
方法、内部指针指向该语句时才会执行,所以等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
3. for...of循环:
for...of
循环能够自动遍历 Generator 函数运行时生成的Iterator
对象,且此时再也不须要调用next
方法。
function* foo() { yield 1; yield 2; yield 3; yield 4; yield 5; return 6; } for (let v of foo()) { console.log(v); } // 1 2 3 4 5
这里须要注意,一旦next
方法的返回对象的done
属性为true
,for...of
循环就会停止,且不包含该返回对象,因此上面代码的return
语句返回的6
,不包括在for...of
循环之中。
4. yield*:yield*
后面的 Generator 函数(没有return
语句时),等同于在 Generator 函数内部,部署一个for...of
循环。
任何数据结构只要有 Iterator 接口,就能够被yield*
遍历。
5. 做为对象属性的 Generator 函数:
若是一个对象的属性是 Generator 函数,能够简写成下面的形式。
let obj = { * myGeneratorMethod() { ··· } }; 等同于 let obj = { myGeneratorMethod: function* () { // ··· } };
6. generator 的 this:
Generator 函数老是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的prototype
对象上的方法。可是,若是把g
看成普通的构造函数,并不会生效,由于g
返回的老是遍历器对象,而不是this
对象。
Generator 函数也不能跟new
命令一块儿用,会报错。
如何绑定 this:用call:
function* F() { this.a = 1; yield this.b = 2; yield this.c = 3; } var obj = {}; var f = F.call(obj); f.next(); // Object {value: 2, done: false} f.next(); // Object {value: 3, done: false} f.next(); // Object {value: undefined, done: true} obj.a // 1 obj.b // 2 obj.c // 3
或者
function* gen() { this.a = 1; yield this.b = 2; yield this.c = 3; } function F() { return gen.call(gen.prototype); } var f = new F(); f.next(); // Object {value: 2, done: false} f.next(); // Object {value: 3, done: false} f.next(); // Object {value: undefined, done: true} f.a // 1 f.b // 2 f.c // 3
7. 含义:
(1)状态机:
var clock = function* () { while (true) { console.log('Tick!'); yield; console.log('Tock!'); yield; } }; 等同于 var ticking = true; var clock = function() { if (ticking) console.log('Tick!'); else console.log('Tock!'); ticking = !ticking; }
(2)协程:
一个线程(或函数)执行到一半,能够暂停执行,将执行权交给另外一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种能够并行执行、交换执行权的线程(或函数),就称为协程。
在内存中,子例程只使用一个栈(stack),而协程是同时存在多个栈,但只有一个栈是在运行状态,也就是说,协程是以多占用内存为代价,实现多任务的并行。
因为 JavaScript 是单线程语言,只能保持一个调用栈。引入协程之后,每一个任务能够保持本身的调用栈。这样作的最大好处,就是抛出错误的时候,能够找到原始的调用栈。不至于像异步操做的回调函数那样,一旦出错,原始的调用栈早就结束。
Generator 函数是 ES6 对协程的实现,但属于不彻底实现。Generator 函数被称为“半协程”(semi-coroutine),意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。若是是彻底执行的协程,任何函数均可以让暂停的协程继续执行。
(3)上下文:
JavaScript 代码运行时,会产生一个全局的上下文环境(context,又称运行环境),包含了当前全部的变量和对象。而后,执行函数(或块级代码)的时候,又会在当前上下文环境的上层,产生一个函数运行的上下文,变成当前(active)的上下文,由此造成一个上下文环境的堆栈(context stack)。
这个堆栈是“后进先出”的数据结构,最后产生的上下文环境首先执行完成,退出堆栈,而后再执行完成它下层的上下文,直至全部代码执行完成,堆栈清空。
Generator 函数不是这样,它执行产生的上下文环境,一旦遇到yield
命令,就会暂时退出堆栈,可是并不消失,里面的全部变量和对象会冻结在当前状态。等到对它执行next
命令时,这个上下文环境又会从新加入调用栈,冻结的变量和对象恢复执行。
function* gen() { yield 1; return 2; } let g = gen(); console.log( g.next().value, g.next().value, );
上面代码中,第一次执行g.next()
时,Generator 函数gen
的上下文会加入堆栈,即开始运行gen
内部的代码。等遇到yield 1
时,gen
上下文退出堆栈,内部状态冻结。第二次执行g.next()
时,gen
上下文从新加入堆栈,变成当前的上下文,从新恢复执行。
8. 应用:
(1)异步操做的同步化表达:
Generator 函数的暂停执行的效果,意味着能够把异步操做写在yield
表达式里面,等到调用next
方法时再日后执行。这实际上等同于不须要写回调函数了,由于异步操做的后续操做能够放在yield
表达式下面,反正要等到调用next
方法时再执行。因此,Generator 函数的一个重要实际意义就是用来处理异步操做,改写回调函数。
(2)控制流管理:
step1(function (value1) { step2(value1, function(value2) { step3(value2, function(value3) { step4(value3, function(value4) { // Do something with value4 }); }); }); }); 或者 Promise.resolve(step1) .then(step2) .then(step3) .then(step4) .then(function (value4) { // Do something with value4 }, function (error) { // Handle any error from step1 through step4 }) .done(); 都可写为: function* longRunningTask(value1) { try { var value2 = yield step1(value1); var value3 = yield step2(value2); var value4 = yield step3(value3); var value5 = yield step4(value4); // Do something with value4 } catch (e) { // Handle any error from step1 through step4 } } scheduler(longRunningTask(initialValue)); function scheduler(task) { var taskObj = task.next(task.value); // 若是Generator函数未结束,就继续调用 if (!taskObj.done) { task.value = taskObj.value scheduler(task); } }
注意,上面这种作法,只适合同步操做,即全部的task
都必须是同步的,不能有异步操做。由于这里的代码一获得返回值,就继续往下执行,没有判断异步操做什么时候完成。
(3)部署 Iterator 接口:
利用 Generator 函数,能够在任意对象上部署 Iterator 接口。
function* iterEntries(obj) { let keys = Object.keys(obj); for (let i=0; i < keys.length; i++) { let key = keys[i]; yield [key, obj[key]]; } } let myObj = { foo: 3, bar: 7 }; for (let [key, value] of iterEntries(myObj)) { console.log(key, value); } // foo 3 // bar 7
(4)做为数据结构:
Generator 能够看做是一个数组结构,由于 Generator 函数能够返回一系列的值,这意味着它能够对任意表达式,提供相似数组的接口。
好比:
function doStuff() { return [ fs.readFile.bind(null, 'hello.txt'), fs.readFile.bind(null, 'world.txt'), fs.readFile.bind(null, 'and-such.txt') ]; } for (task of doStuff()) { // task是一个函数,能够像回调函数那样使用它 }
9.
调用 Generator 函数,会返回一个内部指针(即遍历器)g
。这是 Generator 函数不一样于普通函数的另外一个地方,即执行它不会返回结果,返回的是指针对象。调用指针g
的next
方法,会移动内部指针(即执行异步任务的第一段),指向第一个遇到的yield
语句,上例是执行到x + 2
为止。
换言之,next
方法的做用是分阶段执行Generator
函数。每次调用next
方法,会返回一个对象,表示当前阶段的信息(value
属性和done
属性)。value
属性是yield
语句后面表达式的值,表示当前阶段的值;done
属性是一个布尔值,表示 Generator 函数是否执行完毕,便是否还有下一个阶段。
10. Thunk 函数:
(1)参数的求值策略:
var x = 1; function f(m) { return m * 2; } f(x + 5)
上面代码先定义函数f
,而后向它传入表达式x + 5
。请问,这个表达式应该什么时候求值?
"传值调用"(call by value),即在进入函数体以前,就计算x + 5
的值(等于 6),再将这个值传入函数f
。C 语言就采用这种策略。
“传名调用”(call by name),即直接将表达式x + 5
传入函数体,只在用到它的时候求值。Haskell 语言采用这种策略。
(2)编译器的“传名调用”实现,每每是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫作 Thunk 函数。
JavaScript 语言是传值调用,它的 Thunk 函数含义有所不一样。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数做为参数的单参数函数。
任何函数,只要参数有回调函数,就能写成 Thunk 函数的形式。下面是一个简单的 Thunk 函数转换器:
// ES5版本 var Thunk = function(fn){ return function (){ var args = Array.prototype.slice.call(arguments); return function (callback){ args.push(callback); return fn.apply(this, args); } }; }; // ES6版本 const Thunk = function(fn) { return function (...args) { return function (callback) { return fn.call(this, ...args, callback); } }; };
十四. Async 函数:
1. async 函数就是 Generator 函数的语法糖。async
函数就是将 Generator 函数的星号(*
)替换成 async
,将yield
替换成 await。
2. async
函数的返回值是 Promise 对象。进一步说,async
函数彻底能够看做多个异步操做,包装成的一个 Promise 对象,而await
命令就是内部then
命令的语法糖。
3. 用法:async
函数返回一个 Promise 对象,可使用then
方法添加回调函数。当函数执行的时候,一旦遇到await
就会先返回,等到异步操做完成,再接着执行函数体内后面的语句。
async function getStockPriceByName(name) { const symbol = await getStockSymbol(name); const stockPrice = await getStockPrice(symbol); return stockPrice; } getStockPriceByName('goog').then(function (result) { console.log(result); });
4. async 函数的实现原理:async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
async function fn(args) { // ... } // 等同于 function fn(args) { return spawn(function* () { // ... }); } function spawn(genF) { return new Promise(function(resolve, reject) { const gen = genF(); function step(nextF) { let next; try { next = nextF(); } catch(e) { return reject(e); } if(next.done) { return resolve(next.value); } Promise.resolve(next.value).then(function(v) { step(function() { return gen.next(v); }); }, function(e) { step(function() { return gen.throw(e); }); }); } step(function() { return gen.next(undefined); }); }); }
5. 异步遍历器:Async Iterator,为异步操做提供原生的遍历器接口,即value
和done
这两个属性都是异步产生。
(1)异步遍历器的最大的语法特色,就是调用遍历器的 next
方法,返回的是一个 Promise 对象。
对象的异步遍历器接口,部署在Symbol.asyncIterator
属性上面。无论是什么样的对象,只要它的Symbol.asyncIterator
属性有值,就表示应该对它进行异步遍历。
(2)for await ... of:for...of
循环用于遍历同步的 Iterator 接口。新引入的for await...of
循环,则是用于遍历异步的 Iterator 接口。
(3)异步 Generator 函数出现之后,JavaScript 就有了四种函数形式:普通函数、async 函数、Generator 函数和异步 Generator 函数。请注意区分每种函数的不一样之处。基本上,若是是一系列按照顺序执行的异步操做(好比读取文件,而后写入新内容,再存入硬盘),可使用 async 函数;若是是一系列产生相同数据结构的异步操做(好比一行一行读取文件),可使用异步 Generator 函数。
十五. Class:
1. ES5 与 Class:
// ES5: function Point(x, y) { this.x = x; this.y = y; } Point.prototype.toString = function () { return '(' + this.x + ', ' + this.y + ')'; }; var p = new Point(1, 2); // Class 方式: class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return '(' + this.x + ', ' + this.y + ')'; } }
2. 静态方法:类至关于实例的原型,全部在类中定义的方法,都会被实例继承。若是在一个方法前,加上static
关键字,就表示该方法不会被实例继承,而是直接经过类来调用,这就称为“静态方法”。
class Foo { static classMethod() { return 'hello'; } } Foo.classMethod() // 'hello' var foo = new Foo(); foo.classMethod() // TypeError: foo.classMethod is not a function
Foo
类的classMethod
方法前有static
关键字,代表该方法是一个静态方法,能够直接在Foo
类上调用(Foo.classMethod()
),而不是在Foo
类的实例上调用。若是在实例上调用静态方法,会抛出一个错误,表示不存在该方法。
注意,若是静态方法包含this
关键字,这个this
指的是类,而不是实例。
父类的静态方法,能够被子类继承。静态方法也是能够从super
对象上调用的。
class Foo { static classMethod() { return 'hello'; } } class Bar extends Foo { static classMethod() { return super.classMethod() + ', too'; } } Bar.classMethod() // "hello, too"
3. 实例属性的新写法:
class IncreasingCounter { constructor() { this._count = 0; } get value() { console.log('Getting the current value!'); return this._count; } increment() { this._count++; } } // 也能够 class IncreasingCounter { _count = 0; get value() { console.log('Getting the current value!'); return this._count; } increment() { this._count++; } }
4. 静态属性:
// 老写法 class Foo { // ... } Foo.prop = 1; // 新写法 class Foo { static prop = 1; }
5. new target 属性:
new
是从构造函数生成实例对象的命令。ES6 为new
命令引入了一个new.target
属性,该属性通常用在构造函数之中,返回new
命令做用于的那个构造函数。若是构造函数不是经过new
命令或Reflect.construct()
调用的,new.target
会返回undefined
,所以这个属性能够用来肯定构造函数是怎么调用的。
function Person(name) { if (new.target !== undefined) { this.name = name; } else { throw new Error('必须使用 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
命令调用。
Class 内部调用new.target
,返回当前 Class。
须要注意的是,子类继承父类时,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); // 正确
6. 继承:
(1)Class 经过extends
关键字实现继承;
super
关键字,表示父类的构造函数,用来新建父类的this
对象(super 既能够当函数使用(表明调用父类的构造函数),也能够当对象使用(在普通方法中,指向父类的原型对象;在静态方法中,指向父类))。
子类必须在constructor
方法中调用super
方法,不然新建实例时会报错。这是由于子类本身的this
对象,必须先经过父类的构造函数完成塑造,获得与父类一样的实例属性和方法,而后再对其进行加工,加上子类本身的实例属性和方法。若是不调用super
方法,子类就得不到this
对象。
class Point { } class ColorPoint extends Point { constructor(x, y, color) { super(x, y); // 调用父类的constructor(x, y) this.color = color; } toString() { return this.color + ' ' + super.toString(); // 调用父类的toString() } }
(2)Object.getPrototypeOf
方法能够用来从子类上获取父类,能够用这个方法判断,一个类是否继承了另外一个类。
Object.getPrototypeOf(ColorPoint) === Point // true
(3)mixin:
多个类的接口“混入”(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); } } } // 使用 class DistributedEdit extends mix(Loggable, Serializable) { // ... }
十六. Module:
1. 编译时加载:下面代码的实质是从fs
模块加载 3 个方法,其余方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 能够在编译时就完成模块加载。
// ES6模块 import { stat, exists, readFile } from 'fs';
2. ES6 模块的好处:
UMD
模块格式了,未来服务器和浏览器都会支持 ES6 模块格式。目前,经过各类工具库,其实已经作到了这一点。navigator
对象的属性。Math
对象),将来这些功能能够经过模块提供。3. 模块功能主要由两个命令构成:export
和import
。export
命令用于规定模块的对外接口,import
命令用于输入其余模块提供的功能。
4. export
命令能够出如今模块的任何位置,只要处于模块顶层就能够。若是处于块级做用域内,就会报错,import
命令也是如此。这是由于处于条件代码块之中,就无法作静态优化了,违背了 ES6 模块的设计初衷。
function foo() { export default 'bar' // SyntaxError } foo()
5. import
命令具备提高效果,会提高到整个模块的头部,首先执行。
6. export default 命令:
export default
命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,所以export default
命令只能使用一次。因此,import命令后面才不用加大括号,由于只可能惟一对应export default
命令。
本质上,export default
就是输出一个叫作default
的变量或方法,而后系统容许你为它取任意名字。因此,下面的写法是有效的。
// modules.js function add(x, y) { return x * y; } export {add as default}; // 等同于 // export default add; // app.js import { default as foo } from 'modules'; // 等同于 // import foo from 'modules';
7. import():返回一个 Promise 对象。
import()
函数能够用在任何地方,不只仅是模块,非模块的脚本也可使用。它是运行时执行,也就是说,何时运行到这一句,就会加载指定的模块,因此能够按需加载/条件加载。另外,import()
函数与所加载的模块没有静态链接关系,这点也是与import
语句不相同。import()
相似于 Node 的require
方法,区别主要是前者是异步加载,后者是同步加载。
8. 加载规则:浏览器加载 ES6 模块,也使用<script>
标签,可是要加入type="module"
属性。
<script type="module" src="./foo.js"></script>
上面代码在网页中插入一个模块foo.js
,因为type
属性设为module
,因此浏览器知道这是一个 ES6 模块。
浏览器对于带有type="module"
的<script>
,都是异步加载,不会形成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>
标签的defer
属性。
若是网页有多个<script type="module">
,它们会按照在页面出现的顺序依次执行。
<script>
标签的async
属性也能够打开,这时只要加载完成,渲染引擎就会中断渲染当即执行。执行完成后,再恢复渲染。
一旦使用了async
属性,<script type="module">
就不会按照在页面出现的顺序执行,而是只要该模块加载完成,就执行该模块。
ES6 模块也容许内嵌在网页中,语法行为与加载外部脚本彻底一致。
<script type="module"> import utils from "./utils.js"; // other code </script>
对于外部的模块脚本(上例是foo.js
),有几点须要注意。
use strict
。import
命令加载其余模块(.js
后缀不可省略,须要提供绝对 URL 或相对 URL),也可使用export
命令输出对外接口。this
关键字返回undefined
,而不是指向window
。也就是说,在模块顶层使用this
关键字,是无心义的。利用顶层的this
等于undefined
这个语法点,能够侦测当前代码是否在 ES6 模块之中。
9. ES6 模块与 CommonJS 模块的差别:
第一个,CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
第二个差别是由于 CommonJS 加载的是一个对象(即module.exports
属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
10. Node 加载:
Node 对 ES6 模块的处理比较麻烦,由于它有本身的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。目前的解决方案是,将二者分开,ES6 模块和 CommonJS 采用各自的加载方案。
Node 要求 ES6 模块采用.mjs
后缀文件名。也就是说,只要脚本文件里面使用import
或者export
命令,那么就必须采用.mjs
后缀名。require
命令不能加载.mjs
文件,会报错,只有import
命令才能够加载.mjs
文件。反过来,.mjs
文件里面也不能使用require
命令,必须使用import
。
若是模块名不含路径,那么import
命令会去node_modules
目录寻找这个模块。
若是脚本文件省略了后缀名,好比import './foo'
,Node 会依次尝试四个后缀名:./foo.mjs
、./foo.js
、./foo.json
、./foo.node
。若是这些脚本文件都不存在,Node 就会去加载./foo/package.json
的main
字段指定的脚本。若是./foo/package.json
不存在或者没有main
字段,那么就会依次加载./foo/index.mjs
、./foo/index.js
、./foo/index.json
、./foo/index.node
。若是以上四个文件仍是都不存在,就会抛出错误。
最后,Node 的import
命令是异步加载,这一点与浏览器的处理方法相同。
ES6 模块之中,顶层的this
指向undefined
;CommonJS 模块的顶层this
指向当前模块。
11. SystemJS:
它是一个垫片库(polyfill),能够在浏览器内加载 ES6 模块、AMD 模块和 CommonJS 模块,将其转为 ES5 格式。它在后台调用的是 Google 的 Traceur 转码器。
System.import
使用异步加载,返回一个 Promise 对象。
十七. 编程风格:
1. 全局常量和线程安全:
在let
和const
之间,建议优先使用const
,尤为是在全局环境,不该该设置变量,只应设置常量。
const
优于let
有几个缘由。一个是const
能够提醒阅读程序的人,这个变量不该该改变;另外一个是const
比较符合函数式编程思想,运算不改变值,只是新建值,并且这样也有利于未来的分布式运算;最后一个缘由是 JavaScript 编译器会对const
进行优化,因此多使用const
,有利于提升程序的运行效率,let
和const
的本质区别,实际上是编译器内部的处理不一样。
全部的函数都应该设置为常量。
2. 不要在函数体内使用 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(''); }
3. 使用默认值语法设置函数参数的默认值。
// bad function handleThings(opts) { opts = opts || {}; } // good function handleThings(opts = {}) { // ... }
4. 使用extends
实现继承,由于这样更简单,不会有破坏instanceof
运算的危险。
// bad const inherits = require('inherits'); function PeekableQueue(contents) { Queue.apply(this, contents); } inherits(PeekableQueue, Queue); PeekableQueue.prototype.peek = function() { return this._queue[0]; } // good class PeekableQueue extends Queue { peek() { return this._queue[0]; } }
十八. 修饰器:
修饰器(Decorator)函数,用来修改类的行为。
修饰器是一个对类进行处理的函数。修饰器函数的第一个参数,就是所要修饰的目标类。
@testable class MyTestableClass { // ... } function testable(target) { target.isTestable = true; } MyTestableClass.isTestable // true
上面代码中,@testable
就是一个修饰器。它修改了MyTestableClass
这个类的行为,为它加上了静态属性isTestable
。testable
函数的参数target
是MyTestableClass
类自己。
十九. ArrayBuffer:
1.
ArrayBuffer
对象、TypedArray
视图和DataView
视图是 JavaScript 操做二进制数据的一个接口。它们都是以数组的语法处理二进制数据,因此统称为二进制数组。
这个接口的原始设计目的,与 WebGL 项目有关。所谓 WebGL,就是指浏览器与显卡之间的通讯接口,为了知足 JavaScript 与显卡之间大量的、实时的数据交换,它们之间的数据通讯必须是二进制的,而不能是传统的文本格式。文本格式传递一个 32 位整数,两端的 JavaScript 脚本与显卡都要进行格式转化,将很是耗时。这时要是存在一种机制,能够像 C 语言那样,直接操做字节,将 4 个字节的 32 位整数,以二进制形式原封不动地送入显卡,脚本的性能就会大幅提高。
二进制数组就是在这种背景下诞生的。它容许开发者以数组下标的形式,直接操做内存,大大加强了 JavaScript 处理二进制数据的能力,使得开发者有可能经过 JavaScript 与操做系统的原生接口进行二进制通讯。
二进制数组由三类对象组成:
(1)ArrayBuffer
对象:表明内存之中的一段二进制数据,能够经过“视图”进行操做。“视图”部署了数组接口,这意味着,能够用数组的方法操做内存。
(2)TypedArray
视图:共包括 9 种类型的视图,好比Uint8Array
(无符号 8 位整数)数组视图, Int16Array
(16 位整数)数组视图, Float32Array
(32 位浮点数)数组视图等等。
(3)DataView
视图:能够自定义复合格式的视图,好比第一个字节是 Uint8(无符号 8 位整数)、第2、三个字节是 Int16(16 位整数)、第四个字节开始是 Float32(32 位浮点数)等等,此外还能够自定义字节序。
简单说,ArrayBuffer
对象表明原始的二进制数据,TypedArray
视图用来读写简单类型的二进制数据,DataView
视图用来读写复杂类型的二进制数据。
注意,二进制数组并非真正的数组,而是相似数组的对象。
2. TypedArray 视图:
ArrayBuffer
对象做为内存区域,能够存放多种类型的数据。同一段内存,不一样数据有不一样的解读方式,这就叫作“视图”(view)。ArrayBuffer
有两种视图,一种是TypedArray
视图,另外一种是DataView
视图。前者的数组成员都是同一个数据类型,后者的数组成员能够是不一样的数据类型。
目前,TypedArray
视图一共包括 9 种类型,每一种视图都是一种构造函数。
Int8Array
:8 位有符号整数,长度 1 个字节。Uint8Array
:8 位无符号整数,长度 1 个字节。Uint8ClampedArray
:8 位无符号整数,长度 1 个字节,溢出处理不一样。Int16Array
:16 位有符号整数,长度 2 个字节。Uint16Array
:16 位无符号整数,长度 2 个字节。Int32Array
:32 位有符号整数,长度 4 个字节。Uint32Array
:32 位无符号整数,长度 4 个字节。Float32Array
:32 位浮点数,长度 4 个字节。Float64Array
:64 位浮点数,长度 8 个字节。这 9 个构造函数生成的数组,统称为TypedArray
视图。它们很像普通数组,都有length
属性,都能用方括号运算符([]
)获取单个元素,全部数组的方法,在它们上面都能使用。普通数组与 TypedArray 数组的差别主要在如下方面。
new Array(10)
返回一个普通数组,里面没有任何成员,只是 10 个空位;new Uint8Array(10)
返回一个 TypedArray 数组,里面 10 个成员都是 0。ArrayBuffer
对象之中,要获取底层对象必须使用buffer
属性。