太原做网站

网站维护托管

ECMAScript6常用新特性总结

最近项目中很多代码都是ES6的语法,比如NodeJs、VueJs及Webpack等。所以今天来抽时间总结一下ES6在项目中经常用到的一些新特性语法。


一、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循环结合定时器的使用,如下代码:


for (var i=0; i<5; i++) {

setTimeout(function () {

console.log(i);

}, 0);

} // 5 5 5 5 5

如果换成let,则是:


for (let i=0; i<5; i++) {

setTimeout(function () {

console.log(i);

}, 0);

} // 0 1 2 3 4

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


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


2、不存在变量提升:


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


console.log(a); // undefined

var a = 1;

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


console.log(a); // ReferenceError: a is not defined

let a = 100;

console.log(a); // 100

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


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


3、暂时性死区:


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


var tmp = 123;

if (true) {

tmp = 'abc'; // ReferenceError

let tmp;

}

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


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


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


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


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


// 报错

function () {

let a = 10;

var a = 1;

}

// 报错

function () {

let a = 10;

let a = 1;

}

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


function func(arg) {

let arg; // 报错

}

function func(arg) {

{

let arg; // 不报错

}

}

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


var btn = document.getElementById('btn');

btn.onclick = function (ev) {

var ev = ev || window.event; // 重复声明参数,如果是let是不允许的

ev.cancelBubble = true;

};

二、const声明常量


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


const Work = 'WEB前端开发';

console.log(Work); // WEB前端开发

Work = 'SEO搜索引擎优化';

// TypeError: Assignment to constant variable.

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


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


const foo;

// SyntaxError: Missing initializer in const declaration

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


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


if (true) {

console.log(MAX); // ReferenceError

const MAX = 5;

}

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


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


const person = {};

const person = ''; // 报错

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


const person = {};

person.name = '赵一鸣';

console.log(person); // Object {name: "赵一鸣"}

三、块级作用域


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


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


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


var tmp = new Date();

function f() {

console.log(tmp);

if (false) {

var tmp = 'hello world';

}

}

f(); // undefined

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


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


var s = 'hello';

for (var i = 0; i < s.length; i++) {

console.log(s[i]);

}

console.log(i); // 5

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


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


function f1() {

let n = 5;

if (true) {

let n = 10;

}

console.log(n); // 5

}

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


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


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

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


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


// 情况一

if (true) {

function f() {}

}

// 情况二

try {

function f() {}

} catch(e) {

// ...

}

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


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


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


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


// 不报错

'use strict';

if (true) {

function f() {}

}

// 报错

'use strict';

if (true)

function f() {}

四、顶层对象的属性


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


var name = '赵一鸣';

console.log(window.name); // 赵一鸣

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


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


let name = '赵一鸣博客';

console.log(window.name); // undefined

五、变量的解构赋值


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


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


let a = 1;

let b = 2;

let c = 3;

ES6 允许写成下面这样:


let [a, b, c] = [1, 2, 3];

console.log(a); // 1

console.log(b); // 2

console.log(c); // 3

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


let [a, [b], c] = [1, [2], 3];

console.log(a);

console.log(b);

console.log(c);

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


let [foo] = [];

let [bar, foo] = [1];

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


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


let [a=1, b=2] = [3, 4];

console.log(a);

console.log(b);

3、对象的解构赋值:


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


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

foo // "aaa"

bar // "bbb"

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


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

foo // "aaa"

bar // "bbb"

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

baz // undefined

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


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


let {foo : baz} = {foo : 'aaa', bar : 'bbb'};

console.log(baz);

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


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

4、字符串的解构赋值:


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


const [a, b, c, d, e] = 'hello';

a // "h"

b // "e"

c // "l"

d // "l"

e // "o"

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


let {length : len} = 'hello';

console.log(len) // 5

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


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


function add([x, y]){

return x + y;

}

add([1, 2]); // 3

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


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


(1)交换变量的值:


let a = 1;

let b = 2;

console.log(a); // 1

console.log(b); // 2

[a, b] = [b, a];

console.log(a); // 2

console.log(b); // 1

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


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


// 返回一个数组

function example() {

return [1, 2, 3];

}

let [a, b, c] = example();

// 返回一个对象

function example() {

return {

foo: 1,

bar: 2

};

}

let { foo, bar } = example();

(3)函数参数的定义:


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


function fn1 ([a, b, c]) {

console.log(a, b, c);

}

fn1([1, 2, 3]); // 1 2 3

function fn2 ({a, b}) {

console.log(a, b);

}

fn2({b : 4, a : 5}); // 5 4

(4)提取JSON数据:


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


let person = {

name: '赵一鸣',

sex: "男",

work: ['web前端开发', 'SEO搜索引擎优化']

};

let {name, sex, work} = person;

console.log(name); // 赵一鸣

console.log(sex); // 男

console.log(work); // ["web前端开发", "SEO搜索引擎优化"]

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


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


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


import {mapActions, mapGetters} from Vuex;

六、关于数组的扩展


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


var div = document.getElementsByTagName('div');

var divArry = Array.from(div);

console.log(divArry); // [div, div, div, div, div, div]

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


var arry = Array.of(1, 2, 3, 4);

console.log(arry); // [1, 2, 3, 4]

七、对象Object的扩展


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


Object.assign(obj1, obj2, obj3);

2、获取对象的prototype:


Object.getPrototypeOf(obj)

八、函数的扩展


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


function fn(a=1, b=2){return a + b;}

console.log(fn());

console.log(fn(1, 2));

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


function fn (a, b, ...c) {

return c;

}

console.log(fn(1, 2, 3, 4, 5, 6)); // 3 4 5 6

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


3、箭头函数:


let fn = (a, b) => {

let c = a + b;

return c;

};

console.log(fn(1,2));

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


let fn = (a=1, b=2) => a + b;

console.log(fn(3, 4));

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


let fn1 = a => a;

console.log(fn1(1));

需要注意的是:


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


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


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


var obj1 = {

name : 'zhangsan',

fn : function(){

setTimeout(function () {

console.log(this);

}, 300);

}

};

obj1.fn(); // window

let obj2 = {

name : 'lisi',

fn () {

setTimeout(()=>{

console.log(this);

}, 300);

}

};

obj2.fn(); // obj2

function fn(){

setTimeout(()=>{

console.log(this);

}, 300);

}

fn(); // window

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


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


let set = new Set([1, 1, '1', 2, 3, '2']);

console.log(set); // Set(5) {1, "1", 2, 3, "2"}

console.log(set.size); // 5

set.add(5);

console.log(set); // Set(5) {1, "1", 2, 3, "2", 5}

set.delete('1'); // 返回值的true或false

console.log(set); // Set(5) {"1", 2, 3, "2", 5}

set.clear();

console.log(set); // Set(0) {}

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


let map = new Map([ ['name', 'zym'], ['age', 24], ['sex', 'man'] ]);

console.log(map); // Map(3) {"name" => "zym", "age" => 24, "sex" => "man"}

console.log(map.size); // 3

map.set('work', 'WEB前端开发');

console.log(map); // Map(4) {"name" => "zym", "age" => 24, "sex" => "man", "work" => "WEB前端开发"}

let obj = {};

map.set(obj, 'this is a obj');

console.log(map); // Map(5) {"name" => "zym", "age" => 24, "sex" => "man", "work" => "WEB前端开发", Object {} => "this is a obj"}

console.log(map.has('height')); //查找key是否存在,返回true或false

map.delete('age');

console.log(map); // Map(4) {"name" => "zym", "sex" => "man", "work" => "WEB前端开发", Object {} => "this is a obj"}

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


let p = new Promise((resolve, reject) => {

$.ajax({

type : 'post',

url : './test.php',

dataType : 'json',

data : {

uname : 'zym',

upwd : '123456'

},

success (result) {

result === 1 ? resolve(result) : reject('error');

}

});

});

p.then((res) => {

console.log(res);

$.ajax({

type : 'post',

url : './test1.php',

success (result) {

console.log(result);

}

});

}, function(res){

console.log(res);

});

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


let p1 = new Promise((resolve, reject) => {

setTimeout(() => {

console.log('p1');

resolve('1');

}, 2000);

});

let p2 = new Promise((resolve, reject) => {

setTimeout(() => {

console.log('p2');

resolve('2');

}, 4000);

});

Promise.all([p1, p2]).then((res) => {

let a = res[0] > res[1] ? 1 : 0;

console.log(a);

});

十二、class类


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


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


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


class Foo {

constructor() {

return Object.create(null);

}

}

console.log(new Foo() instanceof Foo) // false

例如以下一个完整的类:


class Person {

// 类的构造函数,实例化对象时自动调用

constructor (name, age, sex) {

/*

// 类的内部通过new.target返回当前类

if(new.target === Person){

throw new Error('本类不能被实例化,只能被继承!');

}

*/

this.name = name;

this.age = age;

this.sex = sex;

}

show () {

return '(' + this.name + ')';

}

// 获取属性时自动调用

get work () {

return 'WEB前端开发';

}

// 设置work属性时自动调用

set work (v){

console.log(v);

}

// 自定义一个静态方法,只能由类本身来调用,实例化的对象不能调用,会报错。 在函数外部,使用new.target会报错。

static run () {

return 'run';

}

}

let person = new Person('zym', 24, 'man');

console.log(person.constructor === Person.prototype.constructor); // true

console.log(Object.getOwnPropertyNames(person));

console.log(Object.keys(person));

console.log(Object.getPrototypeOf(person));

console.log(Person.name);

person.work = 123;

console.log(person.work);

console.log(Person.run());

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


类继承:


class A {

constructor () {

this.name = 'zhangsan';

}

showName () {

return this.name;

}

}

class B extends A {

constructor () {

super();

this.name = 'lisi';

}

}

const a = new A();

const b = new B();

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 模块就是对象,输入时必须查找对象属性。


// CommonJS模块

let { stat, exists, readFile } = require('fs');

// 等同于

let _fs = require('fs');

let stat = _fs.stat;

let exists = _fs.exists;

let readfile = _fs.readfile;

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


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


// ES6模块

import { stat, exists, readFile } from 'fs';

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


以上是在工作中经常会用到的一些ES6的新特性,后期有需要还会继续更新,部分文字参考网络!


发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

«   2020年10月   »
1234
567891011
12131415161718
19202122232425
262728293031
网站分类
搜索
最新留言
    文章归档
    友情链接