ECMAScript 2015作为自上一次更新六年之后的Javascript大版本更新,而且这六年正是JS蓬勃发展的六年,带来了特别多的新特性,新方法。
对我们前端工作人员的编码习惯和编码模式都带来了翻天覆地的变化。
ES2015是ECMA Script的第六个大版本,从ES2015开始,ECMA Script不再使用版本号作为更新标志,而是改成年份来标记,但是大家还是愿意用ES6来称呼ES2015,甚至很多文章对后期更新的版本更新内容也一概用ES6来进行指代,比如异步终极解决方案async await 其实是ES2017的新增内容,但是很多科普文章还是将其称为ES6的新特性,所以大家一定要分清楚哦,看到ES6要知道他是单指ES2015还是泛指ES2015之后所有的新内容~
言归正传,我们开始ES6新特性的梳理,鉴于现在论坛里帮助大家梳理ES6新特性的文章、博客多如牛毛,本文只梳理一些容易被大家忽略的新特性,而不是照搬ES6的说明,如果有入门级别的需要,可以移步阮一峰大神的ES6入门文档:ES6 入门教程
1、新增的声明方法:let和const
let和const给JS带来了一个全新的作用域:块级作用域。在ES6以前,JS只有两种作用域:函数作用域和全局作用域,花括号内的变量之前是没有自己的独立作用域的,函数之外也可以访问函数的变量:
if(true){
var name = '小米';
}
console.log(name)//小米
这一点对复杂函数的编写是非常不利的,也是不安全的,新的let以及const方法定义的变量,只能在当前花括号的范围内访问,增加了安全性和规范性。
第二个不同:let和const会形成暂时性死区,ES6明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。在之前我们使用var声明变量,会存在一个变量提升的现象:
console.log(name);//undefined
var name = '小米';
我们在使用var变量声明之前打印该变量,会得到一个undefined,而不是抛出错误,这是因为使用var进行声明的变量,声明和赋值操作会分开,声明变量的操作会提升到最顶部优先执行,相当于执行以下操作:
var name;
console.log(name);//undefined
name = '小米';
而使用let和const来定义变量,假如在定义之前引用变量,会得到的错误:
console.log(name);// 抛出错误 name is not defined
let name = '小米';
let/const的这一特性可以很方便的帮我们解决嵌套循环中计数器的命名问题:
for(var i = 0;i++;i<3){
for(var i = 0;i++;i<3){
console.log(i);//由于i被重复命名 只会被执行三次
}
}
使用let后,即使使用同一个命名的计数器,代码也不会出现这样的bug
for(let i = 0;i++;i<3){
for(let i = 0;i++;i<3){//此let是实际解决问题的
console.log(i);//会正确执行9次
}
}
当然,我还是不推荐你在多层嵌套循环中使用同一个命名的计数器,这样会给我们维护代码造成阻碍。
我们在上边曾经使用两层for循环来对比var和let/const,实际上for循环是存在两层作用域的:
for(let i = 0;i++;i<3){
let i = 'Xiaomi'
console.log(i)//for循环能正常执行三次 并得到Xiaomi
}
这样看可能会让人有些困惑,我们把for循环拆解一下:
let i = 0;
if(i<3){
let i = 'Xiaomi'
console.log(i);
}
i ++;
if(i<3){
let i = 'Xiaomi'
console.log(i);
}
i++;
if(i<3){
let i = 'Xiaomi'
console.log(i);
}
这样看就清楚,使用let时各个块的作用域是不相影响的,所以上边的代码可以正常执行。
es6新增了一个基本数据类型——Symbol,也是JS第七个基本类型(BigInt目前是第三阶段提案,等通过后会成为JS第八个基本数据类型)。
我们知道ES5的Object对象的key值只支持字符串(es6提供的新Map对象可以解决这一问题,我们后边会讲到),这容易造成属性名的冲突,ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。
console.log(Symbol() === Symbol()); // false
console.log(Symbol(1) === Symbol(1)); // false
console.log(Symbol('foo') === Symbol('foo'));// false
Symbol函数可以接受一个字符串作为参数,此字符串的作用就是用于在控制台中更好的区分我们定义的不同Symbol对象。
Symbol函数提供了一个for方法,允许我们重新使用一个Symbol值:(利用全局环境登记来实现,可以跨iframe,并且在service worker中获取到)
const s1 = Symbol.for('foo');
const s2 = Symbol.for('foo');
console.log(s1 === s2); // true
console.log(Symbol('foo')); // Symbol('foo')
使用Symbol定义的属性无法被for…in…循环、Object.keys( )方法遍历出来,使用JSON.stringfy()进行转化也会忽略对象的Symbol属性:
const obj = {
[Symbol()]:'symbol value',
'foo':'normal value'
}
for(let k in obj){
console.log(k); // foo
}
console.log(obj);//{ foo: 'normal value', [Symbol()]: 'symbol value' }
console.log(Object.keys(obj));//[ 'foo' ]
console.log(JSON.stringify(obj));{"foo":"normal value"}
Symbol的这些特性都很适合来作为私有变量,之前我们在对象中定义私有变量都是靠约定:比如带有下划线的变量是私有成员,不允许外界访问。现在Symbol的特性可以帮助我们来更便捷的定义一个对象的私有变量:
//利用Symbol来定义私有变量
const name = Symbol();
let person = {
[name]:'xiaoming',
say(){
console.log(this[name])
}
}
console.log(person[Symbol]);// 我们无法再次创建同一个Symbol 因此变量无法被直接访问
person.say(); //'xiaoming'
另外,假如我们想要获取当前对象中的Symbol属性也不是全无办法,ES6提供了一个getOwnPropertySymbols(没错,类似getOwnProperty方法)方法来获取当前对象的全部Symbol属性:
const obj = {
[Symbol('自定义')]:'symbol value',
[Symbol('另一个自定义')]:'symbol value',
'foo':'normal value'
}
console.log(Object.getOwnPropertySymbols(obj));//[ Symbol(自定义), Symbol(另一个自定义) ]
es6新增的for…of… 循环,是一个理论上支持遍历所有类型数据的强大方法,其实现基础正是Symbol的iterator属性实现的,迭代器模式涉及的内容过多,我会专门写一篇博客来整理,届时会将链接附在这里。
let obj = {};
console.log(obj.toString()); //[object Object]
let obj1 = {
[Symbol.toStringTag]:'XObject'
}
console.log(obj1.toString());//[object XObject]