ECMAScript6常用新特性总结

 

一、let声明变量

1、基本用法:

ES6 新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。

如下代码:

{
  let a = 10;
  var b = 1;
}

a // ReferenceError: a is not defined.
b // 1

  

上面代码在代码块之中,分别用let和var声明了两个变量。然后在代码块之外调用这两个变量,结果let声明的变量报错,var声明的变量返回了正确的值。这表明,let声明的变量只在它所在的代码块有效。

还有我们经常会遇到的坑:for循环结合定时器的使用,如下代码:

{
  let a = 10;
  var b = 1;
}

a // ReferenceError: a is not defined.
b // 1

  

如果换成let,则是:

for (var i=0; i<5; i++) {
	setTimeout(function () {
		console.log(i);
	}, 0);
} // 5 5 5 5 5

  

以上两段代码出现不同结果的原因就是var声明的是全局变量,就算setTimeout设置的是0ms,但是也是等for循环内的全局变量i执行到5时,才会执行,因此每次打印的结果都是5。而let声明的变量只在其所在代码块内有效,也就是说每次for循环都保存了一次i的值,因此最后依次打印出0、1、2、3、4。

同样的:《JavaScript中闭包结合for循环的使用》也是这个原因。

2、不存在变量提升:

通过var声明的变量,会出现变量提升的情况,这是因为程序是从上到下执行,执行之前先声明,代码如下:

1 console.log(a); // undefined
2 var a = 1;

但是let声明的变量不存在变量提升:

1 console.log(a); // ReferenceError: a is not defined
2 let a = 100;
3 console.log(a); // 100

想要使用let声明的变量,在使用之前必须先声明,否则会报错。

相关阅读:《JavaScript闭包+作用域+变量提升》。

3、暂时性死区:

只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响,如下代码:

1 var tmp = 123;
2  
3 if (true) {
4   tmp = 'abc'// ReferenceError
5   let tmp;
6 }

上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。

ES6明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

4、不允许重复声明同一个变量:

let不允许在相同作用域内,重复声明同一个变量。如下代码:

01 // 报错
02 function () {
03   let a = 10;
04   var a = 1;
05 }
06  
07 // 报错
08 function () {
09   let a = 10;
10   let a = 1;
11 }

因此,不能在函数内部重新声明参数。

1 function func(arg) {
2   let arg; // 报错
3 }
4  
5 function func(arg) {
6   {
7     let arg; // 不报错
8   }
9 }

之前有个小习惯,发生一个事件的时候,有时候要用到事件对象中的属性,就习惯了在事件函数内重复声明参数,如下代码:

1 var btn = document.getElementById('btn');
2 btn.onclick = function (ev) {
3     var ev = ev || window.event; // 重复声明参数,如果是let是不允许的
4     ev.cancelBubble = true;
5 };

二、const声明常量

const声明一个只读的常量。一旦声明,常量的值就不能改变。

1 const Work = 'WEB前端开发';
2 console.log(Work); // WEB前端开发
3  
4 Work = 'SEO搜索引擎优化';
5 // TypeError: Assignment to constant variable.

上面代码表明改变常量的值会报错。

const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。

1 const foo;
2 // SyntaxError: Missing initializer in const declaration

上面代码表示,对于const来说,只声明不赋值,就会报错。

const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用,如下代码:

1 if (true) {
2   console.log(MAX); // ReferenceError
3   const MAX = 5;
4 }

const声明的常量,也与let一样不可重复声明。

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址(栈内存),因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址(堆内存),保存的只是一个指针,const只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。

1 const person = {};
2 const person = ''// 报错

改变const常量对象的属性则不会报错,代码如下:

1 const person = {};
2 person.name = '赵一鸣';
3 console.log(person); // Object {name: "赵一鸣"}

三、块级作用域

1、为什么需要块级作用域呢?

ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。

第一种场景,内层变量可能会覆盖外层变量,代码如下:

01 var tmp = new Date();
02  
03 function f() {
04   console.log(tmp);
05   if (false) {
06     var tmp = 'hello world';
07   }
08 }
09  
10 f(); // undefined

以上代码,在函数f内外都声明了tmp变量,因此在函数内部会优先读取局部变量tmp,但是在if内判断为false,不会继续执行,再加上变量提升的原因,最后打印结果是undefined。

第二种场景,用来计数的循环变量泄露为全局变量,代码如下:

1 var s = 'hello';
2  
3 for (var i = 0; i < s.length; i++) {
4   console.log(s[i]);
5 }
6  
7 console.log(i); // 5

上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。

let实际上为 JavaScript 新增了块级作用域。

1 function f1() {
2   let n = 5;
3   if (true) {
4     let n = 10;
5   }
6   console.log(n); // 5
7 }

上面的函数有两个代码块,都声明了变量n,运行后输出5。这表示外层代码块不受内层代码块的影响。如果两次都使用var定义变量n,最后输出的值才是10。

ES6 允许块级作用域的任意嵌套,如下代码:

1 {{{{{let insane = 'Hello World'}}}}};

2、块级作用域与函数声明:

ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明,如下代码:

01 // 情况一
02 if (true) {
03   function f() {}
04 }
05  
06 // 情况二
07 try {
08   function f() {}
09 catch(e) {
10   // ...
11 }

上面两种函数声明,根据 ES5 的规定都是非法的。

但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。

ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。

ES6 的块级作用域允许声明函数的规则,只在使用大括号的情况下成立,如果没有使用大括号,就会报错,如下代码:

01 // 不报错
02 'use strict';
03 if (true) {
04   function f() {}
05 }
06  
07 // 报错
08 'use strict';
09 if (true)
10   function f() {}

四、顶层对象的属性

在全局环境下声明一个变量,这个变量实际是作为window全局对象的属性,代码如下:

1 var name = '赵一鸣';
2 console.log(window.name); // 赵一鸣

顶层对象的属性与全局变量挂钩,被认为是JavaScript语言最大的设计败笔之一。这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。另一方面,window对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。

ES6为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从ES6开始,全局变量将逐步与顶层对象的属性脱钩,如下代码:

1 let name = '赵一鸣博客';
2 console.log(window.name); // undefined

五、变量的解构赋值

1、ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。

以前,为变量赋值,只能直接指定值,代码如下:

1 let a = 1;
2 let b = 2;
3 let c = 3;

ES6 允许写成下面这样:

1 let [a, b, c] = [1, 2, 3];
2 console.log(a); // 1
3 console.log(b); // 2
4 console.log(c); // 3

本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。

1 let [a, [b], c] = [1, [2], 3];
2 console.log(a);
3 console.log(b);
4 console.log(c);

如果解构不成功,变量的值就等于undefined,如下代码:

1 let [foo] = [];
2 let [bar, foo] = [1];

以上两种情况都属于解构不成功,foo的值都会等于undefined。

2、解构赋值允许指定默认值:

1 let [a=1, b=2] = [3, 4];
2 console.log(a);
3 console.log(b);

3、对象的解构赋值:

解构不仅可以用于数组,还可以用于对象。

1 let { foo, bar } = { foo: "aaa", bar: "bbb"};
2 foo // "aaa"
3 bar // "bbb"

对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

1 let { bar, foo } = { foo: "aaa", bar: "bbb"};
2 foo // "aaa"
3 bar // "bbb"
4  
5 let { baz } = { foo: "aaa", bar: "bbb" };
6 baz // undefined

上面代码的第一个例子,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。第二个例子的变量没有对应的同名属性,导致取不到值,最后等于undefined。

如果变量名与属性名不一致,必须写成下面这样:

1 let {foo : baz} = {foo : 'aaa', bar : 'bbb'};
2 console.log(baz);

这实际上说明,对象的解构赋值是下面形式的简写:

1 let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb"};

4、字符串的解构赋值:

字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象,代码如下:

1 const [a, b, c, d, e] = 'hello';
2 // "h"
3 // "e"
4 // "l"
5 // "l"
6 // "o"

类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值:

1 let {length : len} = 'hello';
2 console.log(len) // 5

5、函数参数的结垢赋值:

函数的参数也可以使用解构赋值:

1 function add([x, y]){
2   return x + y;
3 }
4  
5 add([1, 2]); // 3

上面代码中,函数add的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x和y。对于函数内部的代码来说,它们能感受到的参数就是x和y。

6、变量的解构赋值有很多用途,例如:

(1)交换变量的值:

1 let a = 1;
2 let b = 2;
3 console.log(a); // 1
4 console.log(b); // 2
5 [a, b] = [b, a];
6 console.log(a); // 2
7 console.log(b); // 1

(2)从函数返回多个值:

函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便,代码如下:

01 // 返回一个数组
02  
03 function example() {
04   return [1, 2, 3];
05 }
06 let [a, b, c] = example();
07  
08 // 返回一个对象
09  
10 function example() {
11   return {
12     foo: 1,
13     bar: 2
14   };
15 }
16 let { foo, bar } = example();

(3)函数参数的定义:

解构赋值可以方便地将一组无序参数与变量名对应起来:

1 function fn1 ([a, b, c]) {
2     console.log(a, b, c);
3 }
4 fn1([1, 2, 3]); // 1 2 3
5  
6 function fn2 ({a, b}) {
7     console.log(a, b);
8 }
9 fn2({b : 4, a : 5}); // 5 4

(4)提取JSON数据:

解构赋值对提取JSON对象中的数据,尤其有用,如下代码:

01 let person = {
02   name: '赵一鸣',
03   sex: "男",
04   work: ['web前端开发''SEO搜索引擎优化']
05 };
06  
07 let {name, sex, work} = person;
08 console.log(name); // 赵一鸣
09 console.log(sex); // 男
10 console.log(work); // ["web前端开发", "SEO搜索引擎优化"]

上面代码可以快速提取 JSON 数据的值。

(5)输入模块的指定方法:

加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰,代码如下:

1 import {mapActions, mapGetters} from Vuex;

六、关于数组的扩展

1、将dom集合(或者是类数组)转为数组:

1 var div = document.getElementsByTagName('div');
2 var divArry = Array.from(div);
3 console.log(divArry); // [div, div, div, div, div, div]

2、接收一串参数,转为数组:

1 var arry = Array.of(1, 2, 3, 4);
2 console.log(arry); // [1, 2, 3, 4]

七、对象Object的扩展

1、合并对象,将obj2、obj3等对象的属性合并到obj1里边,类似于jQuery的$.extend({opt1, opt2, opt3})方法:

1 Object.assign(obj1, obj2, obj3);

2、获取对象的prototype:

1 Object.getPrototypeOf(obj)

八、函数的扩展

1、函数可以设置默认参数值:

1 function fn(a=1, b=2){return a + b;}
2 console.log(fn());
3 console.log(fn(1, 2));

2、函数剩余参数组成的数组:

1 function fn (a, b, ...c) {
2     return c;
3 }
4 console.log(fn(1, 2, 3, 4, 5, 6)); // 3 4 5 6

上面的代码,...c代表a和b参数后面所有的参数,是一个数组。

3、箭头函数:

1 let fn = (a, b) => {
2     let c = a + b;
3     return c;
4 };
5 console.log(fn(1,2));

箭头函数如果只有一行,可以省略大括号:

1 let fn = (a=1, b=2) => a + b;
2 console.log(fn(3, 4));

如果只有一个参数,并且没有默认值的时候,可以省略小括号:

1 let fn1 = a => a;
2 console.log(fn1(1));

需要注意的是:

(1)使用箭头函数定义的函数(构造函数),不能被new;

(2)箭头函数不存在arguments;

(3)this永远指向定义时所在的对象;

01 var obj1 = {
02     name : 'zhangsan',
03     fn : function(){
04         setTimeout(function () {
05             console.log(this);
06         }, 300);
07     }
08 };
09 obj1.fn(); // window
10  
11 let obj2 = {
12     name : 'lisi',
13     fn () {
14         setTimeout(()=>{
15             console.log(this);
16         }, 300);
17     }
18 };
19 obj2.fn(); // obj2
20  
21 function fn(){
22     setTimeout(()=>{
23         console.log(this);
24     }, 300);
25 }
26 fn(); // window

九、Set:ES6新增的数据结构,类似于数组

SET内元素的值都是唯一的,如果加入了重复的值,它会自动去重。

1 let set = new Set([1, 1, '1', 2, 3, '2']);
2 console.log(set); // Set(5) {1, "1", 2, 3, "2"}
3 console.log(set.size); // 5
4 set.add(5);
5 console.log(set); // Set(5) {1, "1", 2, 3, "2", 5}
6 set.delete('1'); // 返回值的true或false
7 console.log(set); // Set(5) {"1", 2, 3, "2", 5}
8 set.clear();
9 console.log(set); // Set(0) {}

十、Map:ES6新增的数据结构,类似键值对的对象,可以用object当作key,也可以用任意类型的数据当作key

01 let map = new Map([ ['name''zym'], ['age', 24], ['sex''man'] ]);
02 console.log(map); // Map(3) {"name" => "zym", "age" => 24, "sex" => "man"}
03 console.log(map.size); // 3
04 map.set('work''WEB前端开发');
05 console.log(map); // Map(4) {"name" => "zym", "age" => 24, "sex" => "man", "work" => "WEB前端开发"}
06 let obj = {};
07 map.set(obj, 'this is a obj');
08 console.log(map); // Map(5) {"name" => "zym", "age" => 24, "sex" => "man", "work" => "WEB前端开发", Object {} => "this is a obj"}
09 console.log(map.has('height')); //查找key是否存在,返回true或false
10 map.delete('age');
11 console.log(map); // Map(4) {"name" => "zym", "sex" => "man", "work" => "WEB前端开发", Object {} => "this is a obj"}

十一、promise 是一个对象,用来传递异步操作的消息。它代表了某个未来才会知道结果的事件(通常是一个异步操作),并且这个事件提供统一的 API,可供进一步处理

01 let p = new Promise((resolve, reject) => {
02     $.ajax({
03         type : 'post',
04         url : './test.php',
05         dataType : 'json',
06         data : {
07             uname : 'zym',
08             upwd : '123456'
09         },
10         success (result) {
11             result === 1 ? resolve(result) : reject('error');
12         }
13     });
14 });
15  
16 p.then((res) => {
17     console.log(res);
18     $.ajax({
19         type : 'post',
20         url : './test1.php',
21         success (result) {
22             console.log(result);
23         }
24     });
25 }, function(res){
26     console.log(res);
27 });

promise.all() 只有当所有Promise对象都为成功时才会执行then里面的方法:

01 let p1 = new Promise((resolve, reject) => {
02     setTimeout(() => {
03         console.log('p1');
04         resolve('1');
05     }, 2000);
06 });
07  
08 let p2 = new Promise((resolve, reject) => {
09    setTimeout(() => {
10        console.log('p2');
11        resolve('2');
12    }, 4000);
13 });
14  
15 Promise.all([p1, p2]).then((res) => {
16     let a = res[0] > res[1] ? 1 : 0;
17     console.log(a);
18 });

十二、class类

constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。

定义一个空的类Point,JavaScript 引擎会自动为它添加一个空的constructor方法。

constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。

1 class Foo {
2   constructor() {
3     return Object.create(null);
4   }
5 }
6  
7 console.log(new Foo() instanceof Foo) // false

例如以下一个完整的类:

01 class Person {
02     // 类的构造函数,实例化对象时自动调用
03     constructor (name, age, sex) {
04         /*
05             // 类的内部通过new.target返回当前类
06             if(new.target === Person){
07                 throw new Error('本类不能被实例化,只能被继承!');
08             }
09         */
10  
11         this.name = name;
12         this.age = age;
13         this.sex = sex;
14     }
15  
16     show () {
17         return '(' this.name + ')';
18     }
19  
20     // 获取属性时自动调用
21     get work () {
22         return 'WEB前端开发';
23     }
24  
25     // 设置work属性时自动调用
26     set work (v){
27         console.log(v);
28     }
29  
30     // 自定义一个静态方法,只能由类本身来调用,实例化的对象不能调用,会报错。 在函数外部,使用new.target会报错。
31     static run () {
32         return 'run';
33     }
34 }
35  
36 let person = new Person('zym', 24, 'man');
37 console.log(person.constructor === Person.prototype.constructor); // true
38  
39 console.log(Object.getOwnPropertyNames(person));
40 console.log(Object.keys(person));
41 console.log(Object.getPrototypeOf(person));
42 console.log(Person.name);
43  
44 person.work = 123;
45 console.log(person.work);
46  
47 console.log(Person.run());

ES6中的class类只是对ES5中的构造函数做了一个封装,相当于外边包了一个壳子,看起来和其他编程语言的类一样。

类继承:

01 class A {
02     constructor () {
03         this.name = 'zhangsan';
04     }
05  
06     showName () {
07         return this.name;
08     }
09 }
10  
11 class B extends A {
12     constructor () {
13         super();
14         this.name = 'lisi';
15     }
16 }
17  
18 const a = new A();
19 const b = new B();
20  
21 console.log(b.showName());

ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。

ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。如果子类没有定义constructor方法,这个方法会被默认添加。

(1)super作为函数时,指向父类的构造函数。super()只能用在子类的构造函数之中,用在其他地方就会报错:super虽然代表了父类的构造函数,但是返回的是子类的实例,即super内部的this指的是子类。因此super()在这里相当于A.prototype.constructor.call(this)。

(2)super作为对象时,指向父类的原型对象。

如果子类没有定义constructor方法, 这个方法会被默认添加, 也就是说, 不管有没有显式定义, 任何一个子类都有constructor方法。

在子类的构造函数中, 只有调用super之后, 才可以使用this关键字, 否则会报错。 这是因为子类实例的构建, 是基于对父类实例加工, 只有super方法才能返回父类实例。

十三、Module模块扩展

历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的require、Python 的import,甚至就连 CSS 都有@import,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。

在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

1 // CommonJS模块
2 let { stat, exists, readFile } = require('fs');
3  
4 // 等同于
5 let _fs = require('fs');
6 let stat = _fs.stat;
7 let exists = _fs.exists;
8 let readfile = _fs.readfile;

上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取3个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

1 // ES6模块
2 import { stat, exists, readFile } from 'fs';

上面代码的实质是从fs模块加载3个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象

posted @ 2018-10-29 16:15  北京-小义  阅读(541)  评论(0编辑  收藏  举报