本文作者:IMWeb 张颖 原文出处:IMWeb社区 未经同意,禁止转载
个人感觉ECMAScript 6总体上来说:添加了块级作用域,增加了一些语法糖,增强了字符串的处理,引入Generator函数控制函数的内部状态的变化,原生提供了Promise对象,引入了Class(类)的概念,并且在语言规格的层面上实现了模块功能。 注:
1、ES6的支持性可以查看:http://kangax.github.io/compat-table/es6/
2、Google V8引擎已经部署了ES6的部分特性,使用Node.js 0.12版,可以试验这些特性。
3、使用Traceur转码器、Babel转码器等可以将ES6方式编写的程序转为ES5代码。
一个花括号{}代表一个块级作用域,作用域嵌套时外层代码块不受内层代码块的影响,立即执行匿名函数(IIFE)原本的作用是为了形成局部作用域,防止变量污染,块级作用域的的出现使得获得广泛应用的立即执行匿名函数不再必要了。
需要注意: 函数本身的作用域,在其所在的块级作用域之内。ES5存在函数提升,不管函数在何处声明,函数声明都会提升到当前作用域的顶部,得到执行;而ES6支持块级作用域,其内部声明的函数皆不会影响到作用域的外部。
function f() { console.log('I am outside!'); }
(function () {
if(false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}
f();
}());
//ES5 result: I am inside!
//ES6 result: I am outside!
let命令,用来声明变量,它的用法类似于var,但是所声明的变量,只在let命令所在代码块内有效。
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6]();
//result: 10
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6]();
//result: 6
使用时有几点需要需要注意:
var tmp = 123;
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
const用来声明常量,一旦声明,常量的值就不能改变。
使用时需注意:
const foo = {};
foo.prop = 123;
foo.prop
// 123
foo = {} // 不起作用
const foo = Object.freeze({});
foo.prop = 123; // 不起作用
全局对象是最顶层的对象,在浏览器环境指的是window对象,在Node.js指的是global对象。ES5规定,所有全局变量都是全局对象的属性。
ES6规定,var命令和function命令声明的全局变量,属于全局对象的属性;let命令、const命令、class命令声明的全局变量,不属于全局对象的属性。
ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。解构只能用于数组或对象,所以应该注意,其他原始类型的值都可以转为相应的对象,除了undefined和null。
本质上,解构写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。
let [foo, [[bar], baz]] = [1, [[2], 3]];
let [x, y='b'] = ['a', undefined]; // x='a', y='b'
let [a, b, c] = new Set(["a", "b", "c"]);
var { foo, bar } = { foo: "aaa", bar: "bbb" };
var {x, y = 5} = {x: 1};
console.log(x, y) // 1, 5
let { log, sin, cos } = Math;
function move({x=0, y=0} = {}) {
return [x, y];
}
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
JavaScript内部,字符以UTF-16的格式储存,每个字符固定为2个字节。对于那些需要4个字节储存的字符(Unicode码点大于0xFFFF的字符),JavaScript会认为它们是两个字符。在ECMAScript6中,增强了对码点大于0xFFFF的字符的整体处理和正则匹配。
具体增加的一些处理方法如下:
var s = " ";
/^.$/.test(s) // false
/^.$/u.test(s) // true
使用规则:
`In JavaScript'\n' is a line-feed.`
// 多行字符串
console.log(`string text line 1
string text line 2`);
// 字符串中嵌入变量
var name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
//反斜杠转义
var greeting = `\`Yo\` World!`;
//对象属性
var obj = {x: 1, y: 2};
console.log(`${obj.x + obj.y}`)
//函数调用
function fn() {
return "Hello World";
}
console.log(`foo ${fn()} bar`);
模板字符串可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。函数的参数第一个为模板字符串中没有变量替换的部分组成的数组,第一个参数之后的参数,都是模板字符串各个变量依次被替换后的值。
var a = 5;
var b = 10;
tag`Hello ${ a + b } world ${ a * b}`;
//等价于
tag(['Hello ', ' world '], 15, 50);
注意处理函数的第一个参数,拥有一个raw属性。它也是一个数组,成员与处理函数的第一个参数完全一致,唯一的区别是字符串是被转义前的原始格式。
二进制和八进制数值的新的写法,分别用前缀0b和0o表示
0b111110111 === 503 // true
0o767 === 503 // true
- Math.trunc(x)方法用于去除一个数的小数部分,返回整数部分;
- Math.sign(x)方法用来判断一个数到底是正数、负数、还是零;
- Math.acosh(x) 返回x的反双曲余弦(inverse hyperbolic cosine);
- Math.asinh(x) 返回x的反双曲正弦(inverse hyperbolic sine);
- Math.atanh(x) 返回x的反双曲正切(inverse hyperbolic tangent);
- Math.cbrt(x) 返回x的立方根;
- Math.clz32(x) 返回x的32位二进制整数表示形式的前导0的个数;
- Math.cosh(x) 返回x的双曲余弦(hyperbolic cosine);
- Math.expm1(x) 返回e?x - 1;
- Math.fround(x) 返回x的单精度浮点数形式;
- Math.hypot(...values) 返回所有参数的平方和的平方根;
- Math.imul(x, y) 返回两个参数以32位整数形式相乘的结果;
- Math.log1p(x) 返回1 + x的自然对数;
- Math.log10(x) 返回以10为底的x的对数;
- Math.log2(x) 返回以2为底的x的对数;
- Math.tanh(x) 返回x的双曲正切(hyperbolic tangent)。
数组推导就是直接通过现有数组生成新数组的一种简化写法,通过for...of结构,允许多重循环。注:新数组会立即在内存中生成,这时如果原数组是一个很大的数组,将会非常耗费内存。
var a1 = [1, 2, 3, 4];
var a2 = [for (i of a1) i * 2];
a2 // [2, 4, 6, 8]
var years = [ 1954, 1974, 1990, 2006, 2010, 2014 ];
[for (year of years) if (year > 2000 && year < 2010) year];
// [ 2006]
Array.from({ 0: "a", 1: "b", 2: "c", length: 3 });
// [ "a", "b" , "c" ]
Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array(3) // [undefined, undefined, undefined]
ES6允许直接写入变量和函数,作为对象的属性和方法。
var Person = {
name: '张三',
//等同于birth: birth
birth,
// 等同于hello: function ()...
hello() { console.log('我的名字是', this.name); }
};
ES6允许字面量定义对象时,用表达式作为对象的属性名,即把表达式放在方括号内,允许变量渗入 key 中。
var lastWord = "last word";
var a = {
"first word": "hello",
[lastWord]: "world"
};
a["first word"] // "hello"
a[lastWord] // "world"
a["last word"] // "world"
Object.is():用来比较两个值是否严格相等。它与严格比较运算符(===)的行为基本一致,不同之处只有两个:一是+0不等于-0,二是NaN等于自身。
Object.assign():用来将源对象(source)的所有可枚举属性,复制到目标对象(target)。它至少需要两个对象作为参数,第一个参数是目标对象,后面的参数都是源对象。如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
var target = { a: 1, b: 1 };
var source1 = { b: 2, c: 2 };
var source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
var mySymbol = Symbol('Test');
mySymbol.name
// Test
// 有参数的情况
var s1 = Symbol("foo");
var s2 = Symbol("foo");
s1 === s2 // false
//不能与其他类型的值进行运算
var sym = Symbol('My symbol');
'' + sym
// TypeError: Cannot convert a Symbol value to a string
//可以转换为字符串
String(sym)
// 'Symbol(My symbol)'
注意:
var person = {
name: "张三"
};
var proxy = new Proxy(person, {
get: function(target, property) {
return property in target ? target[property] : "米有";
}
});
proxy.name // "张三"
proxy.title // "米有"
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked
Proxy支持的拦截操作:
属性 | 返回值 | 拦截操作 |
---|---|---|
defineProperty(target, propKey, propDesc) | 布尔值(Boolean) | Object.defineProperty(proxy, propKey, propDesc) |
deleteProperty(target, propKey) | 布尔值 | delete proxypropKey |
enumerate(target) | 遍历器 | for (x in proxy) |
get(target, propKey, receiver) | 类型不限 | 对象属性的读取 |
set(target, propKey, value, receiver) | 布尔值 | 对象属性的设置 |
getOwnPropertyDescriptor(target, propKey) | 属性的描述对象 | Object.getOwnPropertyDescriptor(proxy, propKey) |
getPrototypeOf(target) | 对象 | Object.getPrototypeOf(proxy) |
has(target, propKey) | 布尔值 | propKey in proxy |
isExtensible(target) | 布尔值 | Object.isExtensible(proxy) |
ownKeys(target) | 数组 | Object.getOwnPropertyPropertyNames(proxy)、Object.getOwnPropertyPropertySymbols(proxy)、Object.keys(proxy) |
preventExtensions(target) | 布尔值 | Object.preventExtensions(proxy) |
setPrototypeOf(target, proto) | 布尔值 | Object.setPrototypeOf(proxy, proto) |
apply(receiver, ...args) | 不限 | 函数的调用、call和apply操作 |
construct | 对象 | Proxy实例作为构造函数调用的操作,比如new proxy(···) |
操作 | 作用 |
---|---|
add | 添加属性 |
update | 属性值的变化 |
delete | 删除属性 |
setPrototype | 设置原型 |
reconfigure | 属性的attributes对象发生变化 |
preventExtensions | 对象被禁止扩展 |
ES6允许为函数的参数设置默认值,使用=形式直接写在参数定义的后面。
使用注意事项:
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
}
var p = new Point();
// p = { x:0, y:0 }
fetch(url, { method='GET' } = {}){
console.log(method);
}
...+变量名形式与...+数组形式相当于互逆操作:
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
(function(a, ...b) {}).length // 1
const date = new Date(...[2015, 1, 1]);
函数定义的简化表示法,
var f = () => 5;
// 等同于
var f = function (){ return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};
使用箭头函数需要注意:
为什么尾调用会优化:函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。所以如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。
尾调用的应用——尾递归:递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。比如阶乘函数的为递归改写:
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
Set结构类似于数组,但是成员的值都是唯一的,没有重复的值。
var items = new Set([1,2,3,4,5,5,5,5]);
for (i of items) {console.log(i)}
// 1 2 3 4 5
items.size
// 5
注意:向Set加入值的时候,不会发生类型转换,所以5和“5”是两个不同的值。Set内部判断两个值是否不同,使用的算法类似于精确相等运算符(===),唯一的例外是NaN等于自身。这意味着,两个对象总是不相等的。
let set = new Set();
set.add({})
set.size // 1
set.add({})
set.size // 2
Set结构有以下属性:
Set数据结构有以下方法:
Set结构有一个values方法,返回一个遍历器,同时Set结构的默认遍历器就是它的values方法,所以可以直接用for...of循环进行遍历。
WeakSet是一个与Set类似的结构,也是不重复的值的集合。但是,它与Set有两个区别:
注意Map的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。只有对同一个对象的引用,Map结构才将其视为同一个键。
var map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined
Map结构的属性和方法:
同样与WeakSet类似,WeakMap结构与Map结构基本类似,唯一的区别是它只接受对象作为键名(null除外),不接受原始类型的值作为键名,而且键名所指向的对象,不计入垃圾回收机制,有助于防止内存泄漏。
遍历器属于一种接口规格,任何数据结构只要部署这个接口,就可以完成遍历操作,即依次处理该结构的所有成员。它的作用有两个,一是为各种数据结构,提供一个统一的、简便的接口,二是使得数据结构的成员能够按某种次序排列。
简化的讲,所谓Iterator接口,就是指调用这个接口,会返回一个遍历器对象。该对象具备next方法,每次调用该方法,会返回一个具有value和done两个属性的新对象,指向部署了Iterator接口的数据结构的一个成员。下面代码就展示了一个典型的Iterator接口:
function idMaker(){
var index = 0;
return {
next: function(){
return {value: index++, done: false};
}
}
}
var it = idMaker();
it.next().value // '0'
it.next().value