ECMAScript6学习笔记
ECMAScript6发布于2015年,是下一代ECMAScript的标准。当前ES6的支持情况如下:
- 最新的浏览器:Chrome 54、Opera 41、Safari 10、Firefox 50、Edge 14、iOS 10等已经支持ES6的大部分特性
- 利用Babel等JavaScript编译器,可以把ES6编译为老旧浏览器支持的脚本。使用Babel 6.5 + core-js 2.4可以支持大部分的ES6特性。编译器的存在意味着你可以放心的使用ES6编写代码,却不需要担心浏览器能否支持
- 在服务器端,Node.js 6.5、7.x支撑大部分ES6特性
JavaScript原本由网景公司发明,它将其提交给标准化组织ECMA管理,ECMA发布语言标准后,将其命名为ECMAScript。可以认为我们常说的JavaScript是ECMAScript标准的实现。
本节介绍ES7(2016)版本引入的新特性。
判断数组是否包含指定的值。
指数操作符。
本节介绍ES8(2017)版本引入的新特性。
async用于声明一个函数是异步的。await用于等待一个异步函数执行完成。await只能出现在async函数的函数体中(不能是内嵌函数的函数体)。
async会导致一个函数的返回值变成Promise:
1 2 3 4 5 6 |
async function testAsync() { return "hello async"; } const result = testAsync(); console.log(result); // Promise { 'hello async' } |
对于上面这个返回直接量的函数,async会直接利用 Promise.resolve()将其封装为Promise。 如果函数没有返回值,则将undefined封装为Promise。
await用于等待一个表达式的结果:
- 如果表达式的结果是直接量,则直接返回
- 如果表达式的结果是一个Promise,则它会阻塞直到该Promoise被resolve
本质上async就是Promise的语法糖。下面的例子,让代码变得简洁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
function takeLongTime(n) { return new Promise(resolve => { setTimeout(() => resolve(n + 200), n); }); } function step1(n) { console.log(`step1 with ${n}`); return takeLongTime(n); } function step2(n) { console.log(`step2 with ${n}`); return takeLongTime(n); } function step3(n) { console.log(`step3 with ${n}`); return takeLongTime(n); } // Promise语法 function doIt() { console.time("doIt"); const time1 = 300; step1(time1) .then(time2 => step2(time2)) .then(time3 => step3(time3)) .then(result => { console.log(`result is ${result}`); console.timeEnd("doIt"); }); } doIt(); // async/wait语法 async function doIt() { console.time("doIt"); const time1 = 300; const time2 = await step1(time1); const time3 = await step2(time2); const result = await step3(time3); console.log(`result is ${result}`); console.timeEnd("doIt"); } doIt(); |
要注意await后面的Promise可能会被Rejected,因此最好:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
async function f() { try { await returnPromise(); } catch (err) { console.log(err); } } // 或者 async function f() { await returnPromise().catch(function (err){ console.log(err); }); } |
返回所有属性的值。 Object.keys()用于返回所有属性的名字。
返回所有属性键值对的数组。
字符串补白。
返回对象自身属性的描述符。
创建通用的原始二进制数据缓冲区,类似于ArrayBuffer。
提供SharedArrayBuffer上的原子操作。
ES引入了一系列的新语法,本章内容将一一介绍。
该关键字类似于var,用于声明一个变量。但是,let声明的变量的作用域仅仅在当前代码块。也就是说,let为JavaScript引入了块级作用域:
1 2 3 4 5 6 |
{ var user1 = 'meng'; let user0 = 'alex'; } user1; user0; // ReferenceError: user0 is not defined |
let关键字适用于for循环中的计数器变量定义。
let变量不存在var变量那种所谓变量提升(Hoisting)的特性,也就是说,必须先声明后使用,否则会报ReferenceError错。
而且,只要在一个代码块中的任何地方声明了 let varname ,则块外部声明的任何名为varname的变量都被屏蔽:
1 2 3 4 5 6 |
var varname = true; { varname = false; // ReferenceError: varname is not defined typeof varname; // ReferenceError: varname is not defined 而不是返回undefined let varname; } |
上例中,在let声明出现之前,变量varname不可用的现象称为temporal dead zone。
所谓全局对象,是指浏览器中的window/self变量、Node.js中的global变量。在全局作用域通过var声明的变量,自动成为全局对象的属性。
ES6规定,以var、function声明的全局变量,仍然是全局对象的属性,但是以let、const、class关键字声明的全局变量,不作为全局对象的属性。
let关键字为JavaScript引入了块级作用域,其特性如下:
- 块级作用域可以无限嵌套
- 内部块可以看到外部块的let变量
- 内部块可以覆盖外部块的let变量定义
- 外部块不能读写内部块定义的let变量
有了块级作用域后,以防止污染命名空间为目的的立即调用函数表达式(IIFE)不再需要:
1 2 3 4 5 6 7 8 |
// IIFE风格 (function () { var tmp = {}; }()); // 等价的块作用域风格 { let tmp = {}; } |
ES5规定,函数只能在全局作用域,或者函数作用域的最外层声明,不能定义在代码块中。但是,很多浏览器没有遵循此标准,仍然允许在代码块中声明函数。
ES6引入了块级作用域,在代码块中声明的函数,仅仅在块内可见,就好像加了let声明的变量一样。 但是,为了 避免兼容性问题,ES6允许在浏览器环境中运行的JavaScript不遵循此规定,而是:
- 允许在块级作用域中声明函数
- 函数声明会提升到全局或者函数作用域
由于不同浏览器的行为不同,应该避免在块级作用域声明函数,如果一定要声明,应该使用函数表达式语法:
1 2 3 4 5 6 7 8 9 10 |
{ // 使用这种语法,明确指定函数仅仅在块中可用 let func = function(){ } // 这种语法,可能会导致提升,由于浏览器的差异可能导致意外行为 function func(){ } } |
该关键字用于声明一个常量,常量一旦声明,其值就不能改变。在声明常量式必须同时指定初始化式,否则会报错:
1 2 |
const PI; // SyntaxError: Missing initializer in const declaration |
const变量在作用域上的特征、与全局对象的关系,与let变量一致。
对于对象、数组等类型,const实际上仅仅限定变量指针的不变性,这类似于Java中的常量。要实现C语言那样的常量,可以调用ES5引入的 Object.freeze() 方法。
JavaScript语言采用了基于原型的继承机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 构造函数,定义了一个类型 function Fruit( weight ) { // 构造函数中,可以使用this访问正在构造的对象 this.weight = weight; } // 构造函数的prototype属性,即为类型的原型对象,每个实例都继承原型的属性、方法 Fruit.prototype.getWeight = function () { return this.weight; } function Apple( weight ) { this.weight = weight; } // 可以设置原型对象,达到继承的效果 Apple.prototype = new Fruit(); var apple = new Apple( 1.2 ); console.log( apple.getWeight() ); // 1.2 |
这种机制与Java、Python等基于类的继承机制差异很大,容易让JavaScript新手困惑。
ES6引入类似于传统OO语言的类型定义语法:
1 2 3 4 5 6 7 8 9 10 11 12 |
// 定义类型 class Fruit { // 定义构造函数 constructor( weight ) { this.weight = weight; // 默认返回this,但是你可以返回任意对象 } // 方法之间不需要逗号间隔,不需要function关键字 getWeight() { return this.weight; } } |
这种语法本质上仅仅是语法糖,并没有改变JavaScript的继承机制 :
1 2 3 |
console.log( typeof Fruit ); // 类型仍然是function // class块中的方法,仍然是声明在原型对象上的 console.log( Fruit.prototype.getWeight ); |
注意:
- class块中的constructor函数,不通过new是无法调用的,会报错
- class块中的定义的方法,都是不可枚举的
- 除非显式的定义在this上面,属性/方法都是定义在原型对象上的:
1234var f = new Fruit( 1.2 );console.log( f.hasOwnProperty( 'weight' ) ); // trueconsole.log( f.hasOwnProperty( 'getWeight' ) ); // falseconsole.log( f.__proto__.hasOwnProperty( 'getWeight' ) ); // true - 类的所有实例,共享一个原型对象
- 不存在Hoisting,即必须先声明class才能使用:
12new Fruit(); // ReferenceError: Fruit is not definedclass Fruit {} - class块内部,默认是严格模式
类似于函数,class也支持以表达式方式声明:
1 2 3 4 5 6 |
const Fruit = class F { getClassName() { return F.name; } }; console.log( new Fruit().getClassName() ); // F |
上面的例子中,对外暴露出来的类名是Fruit,F仅仅可以在class内部使用,代表当前类,F可以省略。
可以定义一个立即构造的匿名类:
1 2 3 4 5 |
let apple = new class { constructor( weight ) { this.weight = weight; } }( 1.2 ); |
class块中定义的所有方法都对外部可见,尽管如此,我们可以利用Symbol的唯一性,来定义无法引用的私有方法:
1 2 3 4 5 |
const checkWeight = Symbol( 'checkWeight' ); class Fruit { [checkWeight]() { } } |
ES6允许定义类的继承关系,使用类似于Java的extends关键字:
1 2 3 4 5 6 7 |
// 使用extends关键字继承 class Apple extends Fruit { constructor( weight ) { // 调用父构造器 super( weight ); } } |
在构造子类的实例时,必须先创建父类的实例,并在其基础上添加新的属性/方法。 子类构造器必须调用父类的构造器,然后才能使用this关键字。
属性__proto__用于读写一个对象的原型对象。子类的__proto__等于父类,代表了构造函数之间的继承关系:
1 |
Apple.__proto__ === Fruit // true |
子类的prototype属性的__proto__属性,等于父类的prototype属性,代表了原型对象之间的继承关系:
1 |
Apple.prototype.__proto__ === Fruit.prototype // true |
子类实例的__proto__的__proto__属性,等于父类实例的__proto__属性。
此关键字可以:
- 代表父类的构造函数,只能出现在子类的构造函数中
- 代表父类的原型对象,注意定义在父类实例上的方法/属性,无法通过super访问到。当通过super调用父类的方法/属性时,方法自动绑定到子类的this,这意味着属性值会直接设置到子类实例上:
12345678910class Fruit {}class Apple extends Fruit {constructor() {super();super.name = 'Apple';console.log( super.name ); // undefinedconsole.log( this.name ); // Apple}}new Apple();
在ES5中,无法扩展Boolean、Number、String、Array、Date、Function、RegExp、Error、Object等原生构造函数。这是因为子类型无法获得原生构造函数的内部属性。
ES5是先创建子类的this,然后再把父类的属性添加到此this上,而父类的内部属性无法获取。
ES6则是先创建父类的this,再基于子类构造函数修饰这个this,因此父类的所有行为都可以继承:
1 2 3 4 5 6 7 8 |
class NegIdxArray extends Array { constructor() { super( ...arguments ); } } var nia = new NegIdxArray(); nia[ 0 ] = 0; console.log( nia.length ); // 打印1,行为正常 |
当继承Object时,需要注意,ES6禁止向Object()传递任何参数。
可以在class块内定义getter/setter,其行为与ES5保持一致:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Fruit { constructor( weight ) { this._weight = weight; } get weight() { return this._weight; } set weight( weight ) { console.log( `Setting weight: ${weight}`); this._weight = weight; } } var f = new Fruit( 1.2 ); f.weight = 1.5; // Setting weight: 1.5 |
可以在class块内部定义生成器方法,例如下面的例子:
1 2 3 4 5 6 7 8 9 10 11 |
class Foo { constructor( ...args ) { this.args = args; } // 将Foo变为Iterable类型 * [Symbol.iterator]() { for ( let arg of this.args ) { yield arg; } } } |
可以在方法定义上添加static限定符,这样的方法不会被实例继承,但是可以被子类继承。静态方法只能通过类名调用:
1 2 3 4 5 6 7 8 9 |
class Fruit { static newInstance() { return new Fruit(); } } class Apple extends Fruit { } console.log( Fruit.newInstance() ); // Fruit {} |
ES6规定,class内部仅仅包含方法的定义,不包括任何属性。要声明属性,可以:
- ClassName.prop:这样的属性是静态属性,必须在class块外部定义
- this.prop:这样的属性是实例属性,必须在构造函数、实例方法中使用
ES7包含了静态属性语法的提案,现在可以通过Babel转换器得以支持,语法如下:
1 2 3 4 |
class ClassName { static staticProp = 0; instanceProp = 0; } |
ES6允许使用new.target这个表达式:
- 如果构造函数不是通过new关键字调用,则在构造函数中new.target的值为undefined,否则返回当前类的名称
- 以new调用子类构造函数时,new.target总是等于子类的名字,即使该表达式出现在父类构造器中
使用new.target,我们可以定义“抽象类”:
1 2 3 4 5 6 7 |
class Shape { constructor() { if ( new.target === Shape ) { throw new Error( `Type Shape is abstract.` ); } } } |
JavaScript一直没有模块体系,不能自然的把大型应用程序拆分为相互依赖的小文件,并通过简单的方法拼装。
在ES6之前,一些开源框架引入了模块化方案,例如CommonJS和AMD,两者分别用于服务器和浏览器。现在ES6在语言层面上添加了通用的模块化支持。
CommonJS、AMD之类的框架,都是在运行时完成模块依赖关系的处理、导入/导出变量的推导的,例如:
1 2 3 4 5 6 |
// CommonJS模块 let { stat, exists, readFile } = require('fs'); // 等同于 let _fs = require('fs'); let stat = _fs.stat, exists = _fs.exists, readfile = _fs.readfile; |
上述代码相当于整体的加载fs模块生成一个临时对象,然后再读取该对象的属性,导出为变量的。这是典型的运行时加载,因为临时对象必须在运行时才能创建。相比之下,ES6中的模块并非对象,采用的加载方式是静态加载,利用 import 、 export 关键字显式的导入/导出变量:
1 |
import { stat, exists, readFile } from 'fs'; |
如果要在浏览器中引用一个ES6模块,需要使用如下标签:
1 |
<script type="module" src="foo.js"></script> |
注意:ES6模块动采用严格模式,不需要在模块头部显式指定。
该关键字用于规范模块的对外接口。一个模块是一个独立文件,其内部所有变量,外部无法读取。如果希望某个变量被外部访问,就必须使用export关键字来导出此变量,export语句可以位于模块作用域的任何位置,不能位于块、函数体内部。示例:
1 2 3 4 5 6 7 8 9 |
let name = 'Alex'; let age = 30; // 导出方式一: export let name; // 导出方式二:导出多个变量。这种方式更好,在脚本尾部统一导出,容易看清导出的内容 export {name, age}; // 函数、类的导出语法类似 export function func(){} export class cls{} |
默认情况下,导出变量的名字与其在模块中的名字一致,但可以使用as关键字修改为任意名字:
1 2 3 4 5 |
export { name as userName, age as userAge, age as userAge2 // 同一变量可以导出多次 }; |
导出的变量,与模块内对应变量是动态绑定的,在运行期间,模块内变量的值发生变化则导出变量也跟着变化:
1 2 3 |
export var num = 1; setTimeout( () => num = 2, 1000 ); // 在一秒后,任何导入num的脚本中num的值均为2 |
这一动态绑定的特性与CommonJS完全不同,CommonJS的导出变量不会动态更新。
该关键字用于输入其它模块提供的功能。一旦某个模块通过export暴露了某些变量,其它JS文件就可以通过import命令引用它们:
1 2 |
// 不需要模块的指定.js后缀 import {name, age} from './user'; |
import也支持as关键字:
1 |
import {name as userName} from './user'; |
import具有Hoisting效果,即import语句总是相当于在脚本最顶端执行。
使用通配符 * 可以导入模块所有暴露的变量:
1 2 |
import * as user from './user'; console.log( user.name ); |
通配符导入,会自动忽略模块中的默认导出。
通过import命令导入变量时,你必须知道这些变量的导出名。模块作者可以指定一个变量为“默认导出”:
1 2 3 4 5 6 7 8 9 |
// 默认导出函数,可以是匿名函数 export default function () { } // 默认导出变量 var name = 'Alex'; export default name; // 默认导出类 export default class {} |
这样,导入该模块的JS,可以用任意名字指代此默认导出:
1 |
import customName from './export-default'; |
进行默认导入时,变量名周围不能加大括号。
ES6模块,导出的是变量引用,而CommonJS导出的则是值的拷贝。
CommonJS导入的变量,与定义它的模块中的那个变量没有联动关系,各自独立变化。
ES6导入的变量,是一个“常指针”,不能对导入的变量进行重新赋值:
1 2 3 4 |
import { obj } from './lib'; obj.prop = 123; // OK obj = {}; // TypeError |
ES6的导入是在编译期处理的,这意味着,要导入什么模块在编译期就必须确定,不能依赖于运行时:
1 2 3 4 5 |
const path = './' + fileName; // CommonJS 运行时决定加载的模块 const myModual = require(path); // ES6 不支持: import * from path; // 语法错误:string literal expected |
目前有一个提案,建议引入 import() 函数,以支持运行时加载。该函数与CommonJS的 require() 不同之处是,它是异步加载,返回一个Promise。
目前的浏览器、Nodejs都不支持ES6模块。除了Babel以外,还可以使用:
- ES6模块编译器:可以将ES6模块转换为CommonJS或者AMD模块,以便在浏览器中使用
- SystemJS:支持在浏览器中加载ES6、AMD、CommonJS模块,并将其转换为ES5代码:
123456<script src="system.js"></script><script>// import方法是异步的,返回一个Promise对象// 这里的app.js可以使用ES6模块语法System.import('./app.js');</script>
ES6引入一种新语法,允许当赋值语句的左值、右值遵循相同的模式(Pattern)时,进行自动赋值。
1 2 3 4 |
let [a, b, c] = [ 1, 2, 3 ]; // 1 2 3 [ a, [ [ b ], c ] ] = [ 4, [ [ 5, 0 ], 6 ] ]; // 4 5 6 [ a, b, c ] = [ , , 9 ]; // 4 5 9 [ a, b ] = [ 1, 2, 3 ]; // 1 2 9 |
除了let变量,var、const变量也支持使用解构赋值。解构赋值可以部分成功,失败的变量自动赋值为undefined。
实际上,任何Iterator都可以作为数组解构的右值:
1 2 3 4 5 6 7 8 9 10 11 |
// Set结构 let [a, b, c] = new Set( [ 0, 0, 0 ] ); // 生成器函数 function* gnerator() { let a = 0; while ( true ) { yield ++a; } } [ a, b, c ] = gnerator() console.log( [ a, b, c ] ); // [ 1, 2, 3 ] |
前面提到过,结构赋值失败时,变量赋值为undefined(即使此变量赋值前是有值的)。ES6允许设置解构赋值的默认值,语法如下:
1 2 |
// 在变量名后面增加 = value let [a,b = 2,c = 3] = [ 1, null ]; |
判断Pattern对应位置是否有值时,ES6使用严格相等操作符===与undefined比较,因此上面的例子中,b的默认值2不会生效。
默认值可以指定为一个表达式,该表达式仅仅在需要时(Pattern对应位置为undefined)才会求值:
1 2 3 4 |
function defaultValue() { console.log( 'This function won't be invoked' ); } let [a = defaultValue()] = [ null ]; |
与适用于Iterator的方括号语法类似,对象语法也可以用于解构赋值:
1 2 |
const { a, b, c }={ a: 1, b: 2, c: 3 } console.log( c ); // 3 |
方括号语法根据位置去匹配,而对象解构赋值使用属性名去匹配,位置顺序不重要。
如果变量名与属性名不一样,必须使用下面的语法:
1 2 |
const { propname:varname }={ propname: 1 }; console.log( varname );// 1 |
对象解构也可以设置默认值: const { propname:varname = 1 }={}
对已声明的变量进行解构赋值,可能会被JavaScript引擎当作代码块来解释进而导致语法错误:
1 2 3 4 |
var a, b, c; { a, b, c } = { a: 1, b: 2, c: 3 }; //SyntaxError: Unexpected token = // 正确的写法如下: ({ a, b, c } = { a: 1, b: 2, c: 3 }) |
使用对象解构赋值,可能很方便的把对象的属性、方法赋值给多个变量,例如:
1 |
let { log, sin, cos } = Math; |
字符串作为解构赋值的右值,可以把字符逐个赋值给变量:
1 |
let { a, b, c } = 'alex'; |
数字/布尔值作为解构赋值的右值时,引擎先将其转换为对象:
1 2 |
let {toString: s} = true; s === Boolean.prototype.toString |
函数的参数,可以作为解构赋值的左值:
1 2 3 4 5 6 7 8 9 10 11 |
function add( [x,y] ) { return x + y; } console.log( add( [ 1, 2 ] ) ); //3 console.log( add( 1, 2 ) ); // 错误:调用必须使用解构语法 // 对象解构语法、默认值都是支持的 function add( { x, y = 1 } ) { return x + y; } console.log(add( { x: 1 } )); // 2 |
函数的返回值,可以作为解构赋值的右值:
1 2 3 4 |
function pair() { return [ -1, 1 ]; } let [l,r ] = pair(); |
可以看到,上例中的用法实现了类似Python函数调用的多个返回值的效果。
合理使用解构赋值,可以让代码更加简洁,下面是一些应用场景举例:
交换变量的值: [x, y] = [y, x]; | ||
从函数返回多个值 | ||
为函数参数提供默认值:因为解构赋值的左值可以提供默认值 | ||
更丰富的参数风格:
|
||
快速数据提取: 解构赋值可以使用非常简洁的代码从JSON中提取数据:
|
||
遍历map: 任何Iterator都可以使用for...of循环来迭代,配合解构赋值,可以很方便的获得map的键值:
|
||
导入模块的指定成员:
|
展开运算符也是以 ... 作为前导(类似于函数的Rest参数),展开运算符用于把数组展开为若干个值,主要用于函数调用:
1 2 |
console.log( ...[ 1, 2, 3, 4, 5 ] ); // 1 2 3 4 5 console.log( [ 1, 2, 3, 4, 5 ] ); // [ 1, 2, 3, 4, 5 ] |
展开运算符可以代替函数的apply方法。
展开运算符也可以用来合并数组:
1 2 |
var array = [ 1, 2, 3 ]; var array2 = [ 0, ...array ]; |
也可以和解构赋值联合使用(用在左值列表的结尾):
1 2 |
let [a, ...rest] = [ 1, 2, 3, 4, 5 ]; console.log( rest ); // [ 2, 3, 4, 5 ] |
展开运算符还可以:
- 正确的把字符串转换为字符的数组,支持所有Unicode: [...'hello']
- 把任何Iterator转换为真正的数组: [...iterator]
和数组类似,对象也支持使用展开操作符:
1 2 3 4 5 6 |
// 用于解构赋值,z = { a: 3, b: 4 } let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }; // 取出对象z的可枚举属性,存放到对象n中。等同于使用Object.assign方法 let z = { a: 3, b: 4 }; let n = { ...z }; |
ES6允许定义对象属性时,指定一个变量:
1 2 3 4 5 6 7 8 |
let name = 'Alex'; let age = 30; let user = { name, age }; // 另外一个例子 function func( x, y ) { return { x, y }; } |
这样,变量名就作为属性名,变量值则作为属性值。
和属性类似,方法也允许简写:
1 2 3 4 5 6 7 8 9 10 |
var user = { getName(){ return this.name }, // ES5的Getter/Setter类似于方法简写 get name(){ }, // 如果某个方法是生成器,则必须加*前缀 * generator(){yield 0;} } |
ES6允许在花括号语法中,用表达式来组成属性名、方法名:
1 2 3 |
var user = { ['name' + i]: 'Alex' } |
这种写法不能和属性名简写同时使用。 另外,作为属性名的表达式会自动转换为字符串。
ES6允许为函数参数提供默认值: function log( x, base = 10 ){}
使用默认值后,函数的length属性将返回没有指定默认值的参数个数(而不是全部参数个数)。
ES6可以将参数列表中剩余的所有参数暴露为一个数组:
1 2 3 |
function(a, b, ...theArgs) { // 第三个及之后的参数,存放在theArgs变量中 } |
Rest参数必须作为最后一个形参。
函数的name属性返回函数的名称,ES6将其纳入标准。
在全局作用域下,this指向全局对象:浏览器中的window/self或者Node.js中的global
在Node.js模块、ES6模块的定义中,this指向当前模块
在函数作用域下,如果:
- 函数作为对象的方法调用,则this指向当前对象
- 普通模式下,函数不作为方法调用,则this指向全局对象
- 严格模式下,函数不作为方法调用,则this指向undefined
该操作符是ES7提案的一部分,目前可以通过Babel支持它,用于绑定函数到目标对象并调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 绑定 foo::bar // 等价于bar.bind(foo) // 绑定并调用 function showThis() { console.log( this ); } ""::showThis(); // 等价于showThis.bind("")() // 将obj的foo方法绑定到obj,并返回绑定后函数 var method = obj::obj.foo; // 上述代码等价于 var method = ::obj.foo; |
在函数式编程领域,所谓尾调用(Tail Call)是指作为函数体最后一个步骤的函数调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function f( x ) { return g( x ); } // 下面三个都不是尾调用 function f( x ) { let y = g( x ); return y; } function f( x ) { return g( x ) + 1; } function f( x ) { g( x ); // 这边相当于有一个 return undefined } |
在使用递归结构时,尾调用具有优化的空间:由于尾调用作为最后一步,因此可以安全的丢弃调用者的栈帧并将其作为尾调用函数的栈帧,这样栈长度就不会增长,也不会因为过深的递归导致太多的内存占用甚至栈溢出。ES6明确要求解释器优化尾递归调用。
要利用尾调用优化来提升程序性能,你需要注意调整自己的代码,例如下面的阶乘实现就是正面的例子:
1 2 3 4 5 |
function factorial( n, total ) { if ( n === 1 ) return total; // 尾递归调用 return factorial( n - 1, n * total ); } |
这个例子用到了递归,但是代价却与循环差不多。
关键字 => 用于定义所谓箭头函数,类似于其它语言中的Lambda:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 最后一个语句作为返回值 // 单个参数 var square = x => x ** 2; // 多个参数 var add = ( a, b ) => a + b; // 多个语句,必须使用花括号,最后一个语句return var sayHello = who => { var greetings = `Hello ${who}`; console.log( greetings ); return true; } // 与解构赋值联用 ( ( { name, age } ) => { console.log( `${name} is ${age} years old` ); } )( { name: 'Alex', age: 30 } ); |
关于箭头函数,需要注意:
- 函数体中的this,就是箭头函数定义处的this,与调用时刻无关
- 不能将箭头函数作为构造函数调用,即不能new
- 不可以使用arguments对象
- 不支持yield关键字,因为其不能作为生成器
JavaScript之前的版本支持 \uxxxx 的Unicode字符表示法,最多支持0xFFFF之前的字符。ES6则引入 \u{xxxxx} 这样的表示法,只要把代码点(Code point)放在花括号内,可以表示任意范围的Unicode字符:
1 |
"\u{41}\u{42}\u{43}" // abc |
JavaScript中拼写HTML模板异常的繁琐,需要很多字符串拼接操作。ES6引入了新的模板字符串语法,它允许声明多行字符串,以反引号( ` )限定:
1 2 3 4 5 6 7 |
var user = { name: 'Alex', age: 30 }; var html = ` <div> <div>${user.name}</div> <div>${user.age + 1}</div> </div> `; |
模板字符串中的空白符会被原样保留,而且,模板中支持插入当前上下文中的变量,类似于Shell的变量替换或者Java的EL表达式。 ${} 内部可以包含任意表达式,例如函数调用、其它模板字符串。 如果模板字符串引用未声明的变量将会导致报错。
标签模板(tagged template)是模板字符串的一种高级应用。模板模板是紧跟着函数名之后的模板字符串,该函数会被调用,来处理模板字符串:
1 2 3 4 5 |
var name = 'Alex'; console.log`${name} Hello ${name}` // 等价于 console.log( [ 'Hello ', '' ], name, name ); // 打印: [ '', ' Hello ', '' ] 'Alex' 'Alex' |
从上面的例子可以看出,所谓标签模板本质上是函数调用表达式,将模板字符串解析为参数的规则如下:
- 模板字符串以变量为界,切分为字符串数组。该数组为函数的第一个实参,如果模板以变量开头或者结尾、或者出现连续两个变量,则添加空串到数组中,
- 变量全部取出,依据出现的顺序,依次作为函数的第2-N个实参
标签模板的函数体,通常都执行这样的逻辑:
- 遍历变量数组(即2-N个实参)
- 原样连接第一实参对应索引的元素到结果字符串
- 处理当前变量,连接到结果字符串
- 遍历完毕后, 返回结果字符串
例如下面的代码,可以过滤用户输入中的不安全字符:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// 打印: [ '', ' Hello ', '' ] 'Alex' 'Alex' // 这些变量来自用户输入 var name = 'Alex<script>'; var age = 30; function escapeHTML( str ) { var arg = String( str ); return arg.replace( /&/g, "&" ).replace( /</g, "<" ).replace( />/g, ">" ); } function safehtml( literals, ...values ) { var index = 0; var result = ''; for ( let seg of literals ) { result += seg;//原样加入 // 每个元素后面都跟着一个变量,除了最后一个元素 if ( index < values.length ) { result += escapeHTML( values[ index++ ] ); } } return result; } // 这个模板需要被安全的插入到DOM var result = safehtml` <div>Hello ${name}, You are ${age} years old</div> `; console.log( result ); // <div>Hello Alex<script>, You are 30 years old</div> |
标签模板也提供了一种直白的语法,来把其它语言的代码嵌入到JavaScript语言中,例如jsx函数可以把DOM字符串转换为React对象。
ES6引入新的二进制、八进制数值直接量语法:
1 2 3 4 |
var bin = 0b1011; bin = 0B1011; var oct = 0o777; oct = 0O777; |
ES6允许正则式构造函数第二个参数指定的标记,覆盖第一个参数中内置的标记:
1 |
console.log( new RegExp( /abc/ig, 'i' ).flags ); // 打印i,第一个参数的ig被替换 |
该修饰符用于支持4字节表示的Unicode字符,以及支持\u{xxx}语法的字符串:
1 2 |
/\u{61}/.test('a') // false /\u{61}/u.test('a') // true |
该修饰符类似于g,也用于全局匹配,但是y限定后一次匹配必须从上一次匹配成功的下一个字符开始。
正则式对象的sticky属性指示该修饰符是否被设置。
数组的空位是没有任何值的索引点:
1 2 |
// 构造函数返回的数组都是空位 console.log( Array( 2 ) ); // [ , ] |
注意:空位与undefined不同,后者是一个确实的值。要判断某个索引是否空位可以使用in运算符:
1 2 |
console.log( 0 in [] ); // false console.log( 0 in [ undefined ] ); // true |
在ES5中:
- forEach()、 filter()、every()、some()函数会跳过空位,不执行回调
- join()、toString()则将空位作为undefined看待
ES6明确的把空位作为undefined看待。
ES6引入了一种新的数据类型,叫做Symbol,用于表示独一无二的值(类似于Java的枚举)。该类型与Undefined、Null、Boolean、String、Number、Object并列,成为第七种类型。
注意:
- 尽管Boolean、String、Number、Symbol不是对象类型,但是它们的行为类似于不可变对象
- 数组不是独立的类型,typeof返回object
- 尽管函数属于对象类型,但是其typeof返回function
Symbol变量通过调用Symbol函数获得:
1 2 3 |
let name = Symbol(); // 可以入参一个字符串,但是不影响不相等性 let name = Symbol('name'); |
注意:
- 不能在Symbol调用前面增加new,因为Symbol不是Object类型,也因此,不能给Symbol添加属性
- 任何两个Symbol均不相等
- Symbol不能与其它类型进行运算,但是你可以显式的将其转换为String、Boolean类型:
123var sym = Symbol('name');String(sym) // Symbol('name')Boolean(sym) ; // true
现在对象的属性名,除了支持字符串类型之外,还可以支持Symbol类型:
1 2 3 4 |
let obj = { [name]: 'Alex' } console.log( obj[ name ] ); // Alex |
由于每个Symbol都不相等,因此将其作为属性名时,可以防止冲突。访问由Symbol标识的属性时,只能用方括号语法,不能用点号语法。
可以使用Symbol定义一组常量:
1 2 3 4 5 |
log.levels = { DEBUG: Symbol( 'debug' ), INFO: Symbol( 'info' ), WARN: Symbol( 'warn' ) }; |
类似于数组,但是成员的值必须唯一,即没有重复值:
1 2 3 |
var set = new Set(); [ 1, 2, 3, '3' ].map( e => set.add( e ) ); console.log( set ); // Set { 1, 2, 3, '3' } |
可以看到,判断元素相等时,使用的是精确相等(Object.is)。Set构造函数有几个变体:
1 2 |
let set = new Set( [ 1, 2, 3, 3 ] ); // 传入数组 console.log( set.size ); // 3 |
利用Set构造函数,可以给数组去重: [...new Set(array)]
与Set类似,但是成员只能是Object类型,该集合持有对象的弱引用,GC将无视这些引用。WeakSet没有size属性,也不能遍历。但是你可以调用其add/delete/has方法
JavaScript对象本质上就是一个Hash结构,可以作为Map使用。但是这种Map的Key类型只能是String/Symbol。
ES6引入的Map,支持任何类型的对象作为映射的键:
1 2 3 4 5 6 7 |
var map = new Map(); // 默认构造器 map = new Map( [ [ 'name', 'Alex' ], [ 'age', 30 ], [ 0 , 1 ] ] ); console.log( map.has( 'age' ) ); // true |
Map在判断两个键是否相等时,使用类似于Object.is()判断。
类似于WeakSet,键是对象的弱引用。仅仅支持将对象作为键。你可以调用其get()、set()、has()、delete()方法,不支持遍历。
现在JavaScript中用来表示集合的结构包括:Array、Map、Set,它们都是可迭代(Iteratble)对象。所有可迭代对象都可以使用 for ... of 循环,来遍历集合的全部成员。
任何对象,只要其具有名为 Symbol.iterator 的方法,则被认为是可迭代的。对前述方法进行调用,会返回一个迭代器对象(Iterator),你可以简单的返回具有next()方法的简单对象,JavaScript引擎负责自动设置其原型为Generator.prototype:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
const obj = { [Symbol.iterator]: function () { // 返回迭代器 return { next: function () { return { value: 1, done: false }; } }; } }; for ( val of obj ) { console.log( val ); // 无限打印 } |
for..of循环遍历迭代器的过程如下:
- 第N次调用迭代器的next()方法,返回包含了集合的第N个元素的数据结构
- 返回的数据结构为: { value : '当前元素', done: '遍历是否已经结束' } 。如果done为true则value无意义,不需要指定
- 根据next()的返回值,判断遍历是否完成
除了必须的next()方法,迭代器对象还可以提供额外的两个方法。如果不提供,默认实现由Generator.prototype提供。这两个方法的说明如下:
-
return() :该方法会在for...of循环提前退出(出错、break)时自动调用。可以利用此方法提供资源清理逻辑:
12345678910111213141516function readLines( file ) {return {next() {// 正常退出if ( file.eof() ) {file.close();return { done: true };}},// 中途退出,默认逻辑是简单的把入参设置到结果数据结构的value字段return( v ) {file.close();return { value : v, done: true };},};} -
throw() :配合生成器使用,用于在生成器外部抛出错误,在生成器内部捕获:
1234567891011121314151617181920var g = function*() {try {yield;}catch ( e ) {console.log( e ); // 1}};var i = g();i.next();try {// 注意不能用throw关键字抛出,该关键字无法影响生成器i.throw( 1 ); // 这个错误在内部捕获i.throw( 2 ); // 这个错误在外部捕获,因为第一个错误已经触发yield并导致生成器结束}catch ( e ) {console.log( e ); // 2}
以下场景下,解释器自动遍历目标对象的迭代器: 解构赋值、扩展运算符、 yield* 、 for...of 、 Array.from 、 Promise.all() 、 Promise.race() 等等。
让Symbol.iterator返回一个生成器,可以简化代码:
1 2 3 4 5 6 7 8 9 |
let obj = { // Symbol.iterator方法可以返回生成器 * [Symbol.iterator]() { yield 1; yield 2; yield* [ 3, 4, 5 ]; } }; console.log( [ ...obj ] ); // [ 1, 2, 3, 4, 5 ] |
生成器是ES6提供的异步编程解决方案。它是一种特殊的函数,其声明语法如下:
1 2 3 4 5 6 7 |
// 要声明一个函数为生成器,必须在function后附加 * 号 function* generator() { // 生成器函数体可以包含yield语句 yield 1; yield 2; return 3; } |
对生成器函数执行调用操作,你会得到一个迭代器对象,但是生成器的函数体不会 执行:
1 2 3 4 5 6 7 8 9 |
function* generator() { yield 1; yield 2; return 3; } let iterable = generator(); // 此时generator的函数体并未执行 for ( e of iterable ) { console.log( e ); // 1 2 } |
每一个yield语句后面的表达式的值,将作为迭代器的元素,注意return语句后面的表达式不作为迭代器元素。关键字yield的本义就是“产出”。
你可以手工调用上述迭代器的next()方法:
1 2 3 4 |
let iterable = generator(); console.log( iterable.next() ); // { value: 1, done: false } console.log( iterable.next() ); // { value: 2, done: false } console.log( iterable.next() ); // { value: 3, done: true } return语句后的表达式作为value |
从效果上来看,执行生成器函数体时,每当遇到yield就返回(暂停)一次。返回后,生成器函数体的内部状态仍然被保留。当再一次调用next()时,自动恢复内部状态,从上一次退出的那个点继续向下执行。这种行为特点与普通函数完全不同。
对生成器执行附带入参,可以为生成器的yield语句提供“返回值”,这里有一个特别容易误会的地方:
1 2 3 4 5 |
var generator = (function *() { console.log( yield 1); })(); generator.next( 2 ); // 什么都不打印 generator.next( 3 ); // 打印3 |
你可以通过next()调用来启动生成器,并让它在yield一个值(上面的1)后立即暂停。如果生成器当前正因为执行了yield语句而处于暂停中,你再一次调用next()可以让其恢复执行,这时你才可以为next()调用附带参数,此参数作为返回时yield语句的值。
上面的next(2)调用,导致生成器在标记红色的地方暂停:console.log( yield 1),打印语句根本不会执行,因此提供参数2没有任何意义。
1 2 3 4 5 6 7 8 9 |
function* numbers() { yield* [ 4, 5, 6 ] } function* generator() { yield* [ 1, 2, 3 ]; // 逐个yield数组 yield* numbers(); // 逐个yield生成器 } // 展开操作符能够遍历迭代器 console.log( [ ...generator() ] ); // [ 1, 2, 3, 4, 5, 6 ] |
yield语句可以产生一个值,但是该语句本身的返回值默认是undefined。你可以在next()调用是提供一个参数,这个参数自动成为yield的返回值。带参数的next()调用类似于Python语言生成器的send()方法,这种情况下生成器函数成为协程。
协程是不依赖于线程的、轻量级的异步编程技术,它通过几个例程(函数)相互协作来完成逻辑。下面是一个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
// 服务器代理 var serverAgent = (function*() { var clientToRequest = new Map(); while ( true ) { // 1、服务器运行到yield,放弃控制权,暂停 // 4、服务器从暂停中恢复,从yield的返回值得到客户端ID,并异步发送Ajax请求 clientToRequest.set( yield, sendRequest() ); // 5、遍历客户端请求的列表,判断Ajax请求状态 for ( e of clientToRequest ) { var cid = e[ 0 ]; var xhr = e[ 1 ]; if ( isDone( xhr ) ) { // 6、如果Ajax请求已经完毕,则调用客户端处理,并放弃控制权 clients[ cid ].next( xhr.responseText ); } } } })(); serverAgent.next(); // 0、启动服务器代理 // 很多客户端 var clients = []; for ( i of [ 1, 2, 3, 4, 5 ] ) { var client = (function*() { // 3、客户端向服务器代理发送请求,附带自己的ID // 7、客户端从yield返回,返回值为服务器代理提供的响应对象。异步处理的同步化表达 var response = yield serverAgent.next( i ); // 8、处理响应 console.log( response ); })(); clients.push( client ); client.next(); // 2、启动客户端 } |
使用协程进行异步处理,代码要比回调简洁的多。
很多JavaScript框架都提供了Promise这种异步编程框架,终于现在可以在语言级别原生的支持了。比起传统的基于回调函数、事件的异步处理,Promise更加合理和强大。
一个Promise保存着一个未来才能完成的事件(通常是异步操作)的结果。Promise所关联的异步操作,其状态只能是Pending(正在进行)、Resolved(已经解决)、Rejected(已经失败)之一。只有异步操作的结果可以决定状态到底是哪一个。状态只能从Pending变为Resolved或者Rejected,而且只能改变一次。
Promise具有一些缺点:
- 一旦建立就立即执行,无法取消
- 如果不提供回调,其内部抛出的异常无法反应到外部
- 无法对Pending阶段进行细分
Promise是一个构造函数,你可以调用它创建Promise:
1 2 3 4 5 6 7 8 9 10 |
// 创建者代码: var promise = new Promise( function ( resolve, reject ) { // 执行异步操作... if ( succeed ) { resolve( value ); // 将Promise置为已解决 } else { reject( error ); // 将Promise置为已失败 } } ); |
一旦Promise被创建,通过构造函数传入的回调就会立即执行。此回调的两个参数resolve、reject,是引擎提供的函数,你可以调用它们而把Promise置为已解决、已失败状态之一。
要知道,构造函数由Promise的创建者调用,但是Promise肯定是提供给其它模块使用的。那么其它模块拿到这个Promise有什么意义呢?
其它模块可以调用它的then方法,注册当Promise状态改变后,需要执行的回调函数:
1 2 3 4 5 6 |
// 客户代码: promise.then( function ( value ) { // 成功时执行的回调 }, function ( error ) { // 失败时执行的回调 } ); |
下面是一个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function timeout( ms ) { return new Promise( ( resolve, reject ) => { // 此回调会立即执行,因此下面的语句立即打印 console.log( 'Starting process...' ); // 异步的设置Promise为已解决状态 setTimeout( resolve, ms, 'Success' ); } ); } // 客户代码 // 由于setTimeout是异步操作,它必然在then调用之后,才可能执行回调 timeout( 1000 ).then( ( value ) => { // 异步操作完成时,打印Promise创建者提供的值 console.log( value ); } ); |
下面是一个基于Promise处理Ajax的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
var getJSON = function ( url ) { var promise = new Promise( function ( resolve, reject ) { var client = new XMLHttpRequest(); client.open( "GET", url ); client.onreadystatechange = handler; client.responseType = "json"; client.setRequestHeader( "Accept", "application/json" ); client.send(); function handler() { if ( this.readyState !== 4 ) { return; } if ( this.status === 200 ) { resolve( this.response ); } else { reject( new Error( this.statusText ) ); } }; } ); return promise; }; // 客户代码 getJSON( "/posts.json" ).then( function ( json ) { // 处理JSON }, function ( error ) { } ); |
总之,都是这样的套路:服务方执行异步操作,并给客户一个承诺,客户拿到承诺后注册期望的操作,服务方完成异步操作后,调用客户端注册的操作,完成处理。
Promise的创建者,可以向resolve函数传递另外一个Promise对象:
1 2 3 4 5 6 7 8 |
var p1 = new Promise(function (resolve, reject) { setTimeout(() => reject(new Error('Failed')), 3000); }); var p2 = new Promise(function (resolve, reject) { // 向resolve传递另外一个Promise p1 setTimeout(() => resolve(p1), 100); }); |
P2没Resolve为一个“依然未Resolve”的Promise,此时JS引擎不会简单的直接调用客户通过then注册的回调函数(即100ms后回调不会被调用)。而是另外的那个Promise(P1)的状态决定当前Promise(P2)的状态——如果P1处于Pending则P2会等待,直到P1变为Resolved/Rejected,才调用客户回调:
1 2 |
// 于3秒后失败,而不是100ms后成功 p2.then(result => console.log(result), error => console.log(error)); |
基于这种用法,可以很容易的管理多个异步操作,例如只在所有这些操作都成功的时候,才认为整个事务是成功的。
前面的例子已经使用过此方法,它的基本作用是注册当Promise状态改变后,需要执行的回调函数。该方法可以有两个入参,它们都必须是函数,第一个作为Promise变为Resolved时的回调,第二个可选,作为Promise变为Rejected时的回调。
then()方法的返回值是一个新的Promise实例,因此可以使用链式调用。链式调用时,前面then的回调函数的返回值,将会传递给后面then的回调函数:
1 2 3 4 5 6 7 |
new Promise((resolve, reject) => { setTimeout(resolve, 1000, 'Succeeded'); }).then(result => { console.log(result); return 'OK'; }).then(result => console.log(result)); // 1秒后连续打印Succeeded、OK |
如果前面then的回调函数的返回值是一个Promise,则后面then的回调函数直到此Promise状态改变后才被调用:
1 2 3 4 5 6 7 |
// getJSON函数返回一个Promise getJSON("/post/1.json").then(function (post) { // 继续调用getJSON,返回一个Promise return getJSON(post.commentURL); }).then(result=>{ // 此回调在第二次getJSON返回的Promised被Resolved时才调用 }); |
此方法等价于 then(null, rejection) 调用,用于指定Rejected时的回调函数。链式调用时,前面then方法抛出的异常,也经由后面的catch方法捕获。
注意,当Promise状态已经变成Resolved后,再抛出异常是无效的:
1 2 3 4 5 6 7 8 9 10 |
var promise = new Promise(function (resolve, reject) { resolve('OK'); throw new Error(); // 无效 }); promise.then(function (value) { console.log(value) }).catch(function (error) { console.log(error) }); // 打印OK |
一般情况下,都使用catch方法,而不是then的第二回调。
该静态方法用于将多个Promise实例包装为单个新的Promise实例: var p = Promise.all([p1, p2, p3]) ,当:
- 所有成员Promise都变为Resolved后,整体Promise变为Resolved。成员Promise传递给resolve回调的参数,构成一个数组,传递给整体Promise的then回调
- 一旦任何一个Promise变为Rejected,整体Promise立即变为Rejected,此Rejected Promise传递给reject回调的参数,传递给整体Promise的then第二回调或者catch回调
该静态方法也是用于将多个Promise实例包装为单个新的Promise实例。但是,只要有一个成员Promise的状态发生改变,则整体Promise的状态也发生改变,并且与前面成员Promise的状态一致。
该静态方法用于把一个对象转换为Promise:
1 2 |
// 把jQuery生成的deferred对象转换为Promise var p = Promise.resolve($.ajax('/data.json')); |
该方法的参数,有四种情形:
- 参数是一个Promise对象,这种情况下resolve不对其进行任何修改,直接将其作为返回值
- 是一个thenable对象,例如:
12345let thenable = {then: function(resolve, reject) {resolve(true);}};新建的Promise对象会立即调用这里的then方法
- 如果参数不具有then方法, 或者不是对象,则该方法返回一个已经Resolved的Promise,resolve的入参作为此Promise的then回调的入参
- 不传递参数,返回一个已经Resolved的Promise
返回一个Rejected的Promise实例。
Promise的链式调用,不管以then还是catch结尾,其内部的异常都不会抛出到外部。为了确保未处理的异常能够抛出,可以在调用链的结尾添加一个done()调用。
该方法指定一个无论什么情况下,都会执行的回调。示例:
1 2 3 4 5 6 7 8 |
// 使用垫片库获得finally支持 require( 'promise.prototype.finally' ).shim(); var assert = require('assert'); // 使用finally: Promise.resolve(42).finally(function () { assert.equal(arguments.length, 0); }); |
该对象提供以下功能:
语言内部功能相关方法 Object对象某些方法,例如Object.defineProperty(),现在同时在Reflect上部署,未来的新方法统一部署到Reflect上 注意,Reflect改进了Object某些方法的行为,例如defineProperty失败时,Object返回异常,而Reflect返回false |
与Proxy配合 Reflect提供了与Proxy的Handler一一对应的方法,调用这些方法,可以执行“默认行为” |
ES6引入了代理(Proxy),利用它可以实现类似于JDK动态代理、Python装饰器/元类的功能。
Proxy在被代理对象上设置一个拦截器,对此对象的访问必须经由此拦截器,拦截器可以改变方法调用、属性读写的行为。例如下面的拦截器改写了属性读写的默认行为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
let target = {}; // 改变属性读写的行为,打印调试信息 var proxy = new Proxy( target, { get: function ( target, key, receiver ) { 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 ); } } ); proxy.age = 1; // setting age proxy.age++; // getting age, setting age |
Proxy构造函数接受两个参数,第一个是被代理对象,第二个是处理器(Handler)。需要拦截的操作不同,则处理器写法也不同:
Handler的键 | 拦截的操作 | ||
get |
签名:get(target, propKey, receiver)。拦截对象属性、数组元素的读取操作 将对象的原型设置为提供了get处理器的代理,对象自动获得代理的get行为:
|
||
set | 签名:set(target, propKey, value, receiver)。拦截对象属性、数组元素的设置操作 | ||
apply |
仅当代理的目标是函数时使用 示例:
|
||
construct | 仅当代理目标是作为构造器的函数时使用 签名:construct(target, args) 拦截构造函数调用操作:new proxy(...args) |
||
has | 签名:has(target, propKey)。拦截 prop in obj、hasOwnProperty操作,返回布尔值 | ||
deleteProperty | 签名:deleteProperty(target, propKey)。拦截delete obj[prop]操作,返回布尔值 | ||
ownKeys | 签名:ownKeys(target) 拦截:Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy) 返回对象自身所有属性的数组,注意Object.keys仅返回可枚举属性 |
||
getOwnPropertyDescriptor | 签名:getOwnPropertyDescriptor(target, propKey) 拦截:Object.getOwnPropertyDescriptor(proxy, propKey),返回属性描述符 |
||
defineProperty | 签名:defineProperty(target, propKey, propDesc) 拦截:Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回布尔值 |
||
preventExtensions | 签名:preventExtensions(target)。拦截Object.preventExtensions(proxy),返回布尔值 | ||
getPrototypeOf | 签名:getPrototypeOf(target)。拦截Object.getPrototypeOf(proxy),返回原型对象 | ||
isExtensible | 签名:isExtensible(target)。返回布尔值 | ||
setPrototypeOf | 签名:setPrototypeOf(proxy, proto)。拦截Object.setPrototypeOf(proxy, proto),返回布尔值 |
考虑到浏览器的兼容性问题,不能直接在其中运行ES6代码。即使你仅仅需要支持最新版本的现代浏览器,模ES6模块之类的特性也不被支持。
因此,必须要进行转码(transpile),将ES6语法转换为通用的ES5语法,并通过垫片(Polyfill)库来适配ES5中缺失的ES6 API。
这是一个广泛使用的JavaScript编译器,请参考:使用Babel进行JS转码
Google提供的JavaScript转码器。Traceur也允许直接在网页中使用ES6代码:
1 2 3 4 5 6 7 8 9 10 11 |
<!-- Traceur库 --> <script src="https://google.github.io/traceur-compiler/bin/traceur.js"></script> <!-- 这两个脚本用于支持浏览器环境 --> <script src="https://google.github.io/traceur-compiler/bin/BrowserSystem.js"></script> <script src="https://google.github.io/traceur-compiler/src/bootstrap.js"></script> <!-- 注意type设置为module--> <script type="module"> // 导入用户模块 import './Greeter.js'; // 可以包含任意ES6代码 </script> |
Traceur可以对支持的ES6特性进行细粒度的控制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<script> // 创建一个全局变量,引用Traceur加载器 window.System = new traceur.runtime.BrowserTraceurLoader(); // 进行细粒度控制 var metadata = { traceurOptions: { experimental: true, properTailCalls: true, symbols: true, arrayComprehension: true, asyncFunctions: true, asyncGenerators: exponentiation, forOn: true, generatorComprehension: true } }; // 加载自己的模块 System.import( './module.js', { metadata: metadata } ).catch( function ( ex ) { // 加载失败时的处理 } ); </script> |
属性/方法 | 说明 |
is() |
该方法补充了既有的相等比较运算符:
当然,对于 {} == {} 三种运算符都返回false,因为复杂对象比较的是地址 |
assign() |
类似于Ext.apply,可以将1-N个源对象的属性覆盖到目标对象,声明在后面的源对象同名属性,优先级高 该方法进行的是浅拷贝 |
__proto__ |
用于读取、设置当前对象的原型对象,作用类似于setPrototypeOf()/getPrototypeOf()静态方法 |
ES6支持多种遍历对象属性的方法:
for...in 遍历对象自身定义和继承得到的可枚举属性,不包含Symbol属性 |
Object.keys(obj) 返回对象自身定义的可枚举属性的名称的数组,不包含Symbol属性 |
Object.values(obj) 返回对象自身定义的可枚举属性的值的数组,不包含Symbol属性 |
Object.entries(obj) 类似上面,但是数组的元素是[key, value] |
Object.getOwnPropertyNames(obj) 返回对象自身定义的全部属性的数组,不包含Symbol属性,包含不可枚举属性 |
Reflect.ownKeys(obj) 返回自身所有属性的数组,包括不可能枚举属性,包括Symbol属性 |
方法 | 说明 | ||||
for() |
根据键查找已经通过Symbol.for登记的Symbol,如果不存在,则定义一个并登记、返回:
注意,Symbol()调用生成的Symbol不参与查找,其值总是独特的:
|
||||
keyFor() | 返回一个已经登记的Symbol的键 |
方法 | 说明 | ||||
codePointAt() |
返回字符串目标位置上(以2字节为单位划分位置)的字符对应的代码点 在JavaScript内部,字符使用UTF-16表示(二字节),对于需要4字节粗存储的Unicode字符,会被认为是两个字符。charAt、charCodeAt不能正确的识别4字节的Unicode字符 codePointAt方法解决此问题,它可以正确处理4字节字符,并返回正确的10进制代码点的值:
使用for ... of循环可以获得字符串中所有字符的代码点:
|
||||
fromCodePoint() | 该静态函数用于把代码点转换为字符 | ||||
includes() startsWith() endsWith() |
这些子串搜索函数补充了indexOf() 这三个函数都支持第二个参数,用于指定搜索的起始位置 |
||||
repeat() |
返回字符串重复N遍的副本 |
||||
padStart() padEnd() |
使用指定的字符串左侧/右侧补全字符串,确保它到达一定的长度 |
||||
raw() |
一个标签模板,处理字符串,将其中的反斜杠替换为双反斜杠:
|
方法 | 说明 | ||
isFinite() |
判断一个数字是否为有限的:
|
||
isNaN() | 判断参数能否转换为数字 | ||
parseFloat() | 解析为浮点数 | ||
parseFloat() | 解析为整数 | ||
isInteger() |
判断是否整数,由于JavaScript以相同的方式存储整数和浮点数,因此6.0和6被作为同一个值看待:
|
||
isSafeInteger() | 判断是否安全整数,即在-2^53到2^53之间,超过此范围的整数,其值无法精确表示 |
方法 | 说明 |
trunc() | 取整数,不进行舍入 |
sign() | 判断值的正负 |
cbrt() | 求立方根 |
clz32() | JavaScript中整数使用4字节表示,该函数判断整数的二进制形式,具有多少个前导的0 |
imul() | 返回两个整数以32位带符号整数的方式的乘积 |
fround() | 返回一个树的单精度浮点数形式 |
hypot() | 返回所有参数平方和的平方根 |
expm1(x) | 计算e^x - 1 |
log1p(x) | 计算 1+ x的自然对数,即Math.log(1 + x) |
log10(x) | 计算以10为底的对数 |
log2(x) | 计算以2为底的对数 |
sinh(x) cosh(x) tanh(x) asinh(x) acosh(x) atanh(x) |
双曲正弦 双曲余弦 双曲正切 反双曲正弦 反双曲余弦 反双曲正切 |
方法 | 说明 | ||
from() |
用于将两类对转换为真正的数组:
|
||
of() | 将一组对象转换为数组: Array.of( 'Hello', 'World' ); // [ 'Hello', 'World' ] | ||
copyWithin() |
从数组内部替换元素:
|
||
find() | 返回第一个匹配回调的成员: [ -1, 0, 1, 2, 3 ].find( n => n > 0 ) | ||
findIndex() | 返回第一个匹配回调的成员的索引,该函数与find()都接收第二个参数,绑定回调的this | ||
fill() | 使用指定的值填充数组 | ||
entries() |
返回数组键值对的迭代器:
|
||
keys() | 返回索引数字的迭代器 | ||
values() | 返回数组元素的迭代器 | ||
includes() | 判断数组是否包含指定的元素 |
属性/方法 | 说明 |
size | 返回成员总数 |
add() | 添加一个元素,返回集合本身 |
delete() | 删除一个元素,返回是否成功 |
has() | 判断是否包含某个成员 |
clear() | 清除所有成员 |
keys() values() | 返回遍历值的迭代器,遍历顺序与插入顺序一致 |
entries() | 返回条目的迭代器,其key/value一直 |
forEach() | 对每个成员执行一个回调 |
filter() | 返回通过回调的元素构成的子集 |
属性/方法 | 说明 |
size | 返回成员总数 |
set() | 设置键值对,返回Map本身 |
get() | 根据键获得值,如果找不到key则返回undefined |
delete() | 删除一个键,返回是否成功 |
has() | 判断是否包含某个键 |
clear() | 清除所有成员 |
keys() | 返回遍历键的迭代器,遍历顺序与插入顺序一致 |
values() | 返回遍历值的迭代器,遍历顺序与插入顺序一致 |
entries() | 返回条目的迭代器,条目是[key,value]数组 |
forEach() | 对每个成员执行一个回调 |
Leave a Reply