学习前端框架vue、react等之前学习下ES6的新特性会方便理解,因为框架中会大量的使用这些特性,如果不了解可能会比较懵逼。

ES6

ES6其实是一个泛指,ESECMAScript的缩写,ECMAScript是一种由Ecma国际在标准ECMA-262中定义的脚本语言规范, 目前只有js对其进行了实现,而ES6是由于ES在6这个版本做了一个大的版本升级,后续的版本只有少量的新增,所以常用ES6作为一个分割点,其代指js的新特性。截至目前ES已更新到11,一般1年一更新。

let和const关键字

letconst关键字是ES6中新增的两个变量定义的关键字,主要用来解决var关键字的三个弊端。

  • 在块中没有作用域,可以全局使用
  • 变量可以重复声明,可以做到变量提升
  • 不能声明不可变的变量

块级作用域

if (true) {
  var a = 10;
}
// a=10
console.log("a=" + a);


//-------- for的计数变量泄漏,代码块外部依旧可以用到,容易造成bug------------//
var str = "hello";
for (var i = 0; i < str.length; i++) {
  console.log(str[i]);
}
console.log(i);// i is 5

正常来说变量a只能在if块中使用,而实际上在全局都可以使用(不包含function块)。在js初期只有function这样的作用域,只有在function中定义的变量其他函数才不能使用,而像ifforwhile等用{}包裹的代码块中用var定义的变量是可以全局使用的。所以ES6的这次新增的let关键字就是为了解决这个问题。

{
  let a = 10;
}
// a is undefined
console.log("a=" + a);

let str = "hello";
for (let i = 0; i < str.length; i++) {
  console.log(str[i]);
}
// Uncaught ReferenceError: i is not defined
console.log(i);

变量重复声明

使用var声明的变量是可以重复声明的,在其他语言中基本上是不会出现这种情况的,所以ES6这次新增的letconst也解决了这个问题。

var a = 10;
var a = 20;
// a = 20
console.log(a);

//-------------------let-----------------------//
let b = 10;
let b = 20;
// Error: Identifier 'b' has already been declared
console.log(b);

变量提升

var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。

// foo is undefined
console.log(foo);
var foo = 200;

let的出现就修复了这一问题,让变量未定义之前不可使用。

// Uncaught ReferenceError: Cannot access 'foo2' before initialization
console.log(foo2);
let foo2 = 100;

无法声明常量

在其他语言中大多可以声明一个不能更改的常量,而在ES6之前是不存在这样的用法的,使用var声明的变量总是可以改变的。而ES6增加的const就解决了这一问题。使用const声明变量必须初始化变量否则将会报错。

const foo = "123";
console.log(foo);

//-------- Uncaught SyntaxError: Missing initializer in const declaration ---------//
const bar;
console.log(bar);

虽然const声明的变量说的是不可更改,但是如果声明的变量是一个对象,那么这个对象本身不能更改,但它的属性是可以被更改的。

const obj = {name: "bennett", gender: "male"};
console.log(obj);
obj.name = "V";
obj.gender = "female";
console.log(obj);

这个用法和其他语言中的常量声明是一致的,都是让变量在内存中的地址不可更改。

箭头函数

箭头函数是ES6中对函数写法的优化,和Java8中的lambda表达式在用法上基本一样的。

// 正常函数写法
function fun(x) {
  console.log("ES5: " + x);
}

// ES6的箭头函数写法1:
const func = x => console.log("ES6: " + x);
// ES6的箭头函数写法2:
const func2 = () => console.log("hello arrow function");
// ES6的箭头函数写法3:
const func3 = () => {
  for (let i = 0; i < 10; i++) {
    func2();
  }
};
// ES6的箭头函数写法4:
const func4 = (x,y) => {
  return x * y;
}
// ES6的箭头函数写法5:
const func5 = () => {return 0};

fun("hello function");
func("hello arrow function");

总结下就是:

  • 函数无参数或者参数大于1时,括号()不可省略
  • 方法体语句大于1时,花括号{}不可省略
  • 函数单个参数,一条语句时可省略括号和花括号,方法体默认会加上return

小细节:箭头函数返回一个对象并且需要是省略了方法体的花括号时需要加上()以区分是方法体的{}还是对象的{}

// const obj = id => {id: id, name: "bennett"};
const obj = id => ({id: id, name: "bennett"});
console.log(obj);

this问题

普通函数中this指向的是调用者,如果没有调用者默认指向window。而箭头函数中没有属于自己的this,它里面的this是继承自父级的this,也即箭头函数中的this指向的是父级的调用者,如果没有调用者则指向window

const obj = {
        fun: function () {
            console.log(this)
        },
  			fun2: () => console.log(this)
    };
// this is obj
obj.fun();
// this is window
obj.fun2();

数组的新增方法

ES6中对数组新增了几个函数和Java8中流的几个操作函数是一样的。

// filter方法:方法体要返回一个布尔值
[10,20, 30, 40, 99, 0, 22, 31, 333, 321,]
  .filter(x => x > 60) // [99,333, 321]
// map方法:方法体返回什么,数组中就会被替换成什么
.map(x => x * 2) // [198, 666, 642]
// reduce方法:方法体返回聚合方式的值,这个值就是最终的值
.reduce((x,y) => x+y); // 1506

// some方法: 是否存在一个符合表达式的值
['a','b'].some(x => x !== 'a') // true
// every方法: 是否所以的值都符合表达式
['a','b'].every(x => x==='a') // false

新增的数据结构

  • Set
  • Map

新增的SetMap数据结构跟其他语言的SetMap是一样的,用法跟Java中的用法类似

//-------------Set--------------//
const set = new Set();
set.add('aaaa');
set.add(1111);
set.add(1111);
set.add(1111);
set.add({'obj': 'obj'});
console.log(set);
set.delete(1111);
console.log(set);
set.clear();
console.log(set);

//-------------Map--------------//
const map = new Map();
map.set('name', 'bennett');
map.set(1111, 110);
map.set('key', {'obj': 'obj'});
console.log(map);
map.delete('key');
console.log(map);
map.clear();
console.log(map);

字符串的新增方法和模版字符串

ES6后新增的字符串方法还是有不少的,这里简要介绍几个,重点介绍模版字符串。

  • includes(str):是否包含参数字符串,返回布尔值
  • startWith(str):是否以参数字符串开头,返回布尔值
  • endWith(str):是否以参数字符串结尾,返回布尔值
  • repeat(num):对原字符串重复n次,n>=0。返回新字符串
  • trimStart():去除开头的空白符,返回新字符串
  • trimEnd():去除结尾的空白符,返回新字符串
  • replaceAll(str1, str2):将字符串中的str1全部替换成str2

ES6新增的模版字符串在React中进行了大量的应用,React中经常使用jsx语法就是采用的模版字符串。模版字符串可以将你输入的内容全部保留,无论你输入的是什么。它使用``修饰。

let title = '模版字符串';
let body = 'body';
let str = `<div>
<h1>${title}</h1>
<h3>${body}</h3>
</div>`
let str1 = 'sdfsfd'
+ 'sfsd';
console.log(str);
console.log(str1);

在普通字符串中输入回车符,IDE一般会将字符串自动为你用+连接两段字符串,但是在模版字符串中的回车也会被原样输出,它的作用就像html中的pre标签一样。使用模版字符串去存放一些htmljson字符串等会非常方便,字符串内还可以使用变量,使用${foo}即可替换成变量的值。

解构赋值

解构赋值是ES6中新增的一种变量赋值方式,它要求以下几点:

  • 左右两边结构一样
  • 右边必须有值
  • 声明和赋值不能分开

数组解构赋值

如果要为一个数组中的几个值进行取出来声明成单个变量,那么常规用法如下:

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

使用解构赋值方式如下:

const [x, y, z] = [1,2,3];
console.log(x);// x = 1
console.log(y);// y = 2
console.log(z);// z = 3

对象的解构赋值

同样的如果是一个对象要取出某些属性声明为单个变量,常规用法还是跟上面类似,一个一个赋值,但是如果使用解构赋值的方式就会非常简单。

const {name, age, sex} = {name: 'bennett', age: 18, sex: 'man'}
console.log(name);
console.log(age);
console.log(sex);

**注意:**数组的解构赋值是按照数组下标为变量进行赋值,所以顺序不可变,但是变量名随意;而对象的解构赋值是用属性名进行赋值,所以顺序无所谓,但是变量名必须和对象中的属性名一致(如果想要变量名已存在,则可以使用:来解决冲突)。

const {name: myName, age, sex} = {name: 'bennett', age: 18, sex: 'man'}

字符串的解构赋值

字符串也支持解构赋值,这是因为此时,字符串被转换成了一个类似数组的对象。

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

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

let {length : len} = 'hello';
len // 5

支持默认值

解构赋值的时候支持给变量默认值,但是默认值要想生效则是在变量严格等于(===undefined时才可。

const [a = 1, b = 2, c = 3] = [10, 20, undefined];
console.log(a);// a = 10
console.log(b);// b = 20
console.log(c);// c = 3

const {x = 123, y = x, z = 'nnnn'} = {x: 111, y: 222};// 默认值若要使用变量,则变量必须已声明
console.log(x);// x = 111
console.log(y);// y = 222
console.log(z);// z = 'nnnn'

函数的参数

函数的默认值

ES6之前要想给函数的参数赋默认值只能采用下面的方式:

function log(x, y) {
  y = y || 'World';
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World

为了给变量y赋空值则还需要判断变量是否以赋值,if (typeof y === 'undefined'){y = 'World';}如果每个参数都需要这样判断,那么将会非常繁琐。而ES6之后支持直接在函数参数后赋值:

function log(x, y = 'World') {
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello 

赋默认值的函数不允许定义同名参数,未赋默认值的则可以。

// 不报错
function fun(x, x, y) {
    // ...
}

// 报错
function fun2(x, x, y = 1) {
    // ...
}

默认值如果使用了变量进行计算得出,那么它会根据这个变量的当前值进行计算,也即是若变量值发生变化,之后调用函数时会重新计算。以及一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。

let foo = 66;
var x = 1;
function fun(x = foo + 1, y = x + 1) {
  console.log(x);
  console.log(y);
}
fun();// x=67,y=68

foo = 100;
fun();// x=101,y=102

Rest参数

ES6 引入rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

function add(...values) {
  let sum = 0;

  for (let val of values) {
    sum += val;
  }

  return sum;
}

add(2, 5, 3) // 10

下面是一个 rest 参数代替arguments变量的例子。

// arguments变量的写法
function sortNumbers() {
  return Array.prototype.slice.call(arguments).sort();
}

// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();
扩展运算符...

这个算是rest参数的一种逆应用,可以将一个数组展开然后扩展到另一个数组中,比如:

const arr1 = [1,2,3];
const arr2 = [7,8,9];
const arr = [...arr1,4,5,6,...arr2];
console.log(arr);//1,2,3,4,5,6,7,8,9

以及将一个数组展开作为一个函数的参数:

const arr = [1,2,3];
function fun(x, y, z) {
  console.log(x);
  console.log(y);
  console.log(z);
}

fun(...arr);// 1 2 3

与解构赋值一起使用,并且扩展运算符变量必须位于数组的最后一位,否则会报错:

const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest  // [2, 3, 4, 5]

const [first, ...rest] = [];
first // undefined
rest  // []

const [first, ...rest] = ["foo"];
first  // "foo"
rest   // []

const [...butLast, last] = [1, 2, 3, 4, 5];
// 报错

const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 报错

Class的用法

ES6class的语法就是js中的语法糖,在ES6之前也可以通过构造函数实现,而现在引入的class语法将这种实现变的跟C++Java一致了,更容易让初学者理解。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

const point = new Point(0,0);
point.toString();

上面代码定义了一个“类”,可以看到里面有一个constructor()方法,这就是构造方法,而this关键字则代表实例对象。这种新的 Class 写法,本质上与本章开头的 ES5 的构造函数Point是一致的。

Point类除了构造方法,还定义了一个toString()方法。注意,定义toString()方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法与方法之间不需要逗号分隔,加了会报错。

总之与Java中的class用法相似,包括未提到的extends继承关键字、super关键字、this关键字,大概理解就好。

JSON的应用

  • 新增的JSON的串行化,JSON.stringify(obj)
  • 反串行化,JSON.parse(str)
  • 简写JSON
    • 属性和值名字一样可以简写(此处的名字指属性名和值的变量名)
    • 方法一样可以简写
let name = "bennett";
let sex = "man";
let age = 18;
let json = {name, sex, age, fun() {
    console.log("this is sample function.");
  }
};

console.log(json);
json.fun();

console.log(JSON.stringify(json));
let jsonStr = '{"hello": "world", "vue": "3.0", "json": "parse"}';
console.log(JSON.parse(jsonStr));

Module模块

使用import代替commonJSrequire(),原本使用commonJS加载模块是

// CommonJS加载方式(此处使用ES6的解构赋值)
let { stat, exists, readfile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

// ES6加载模块
//import { stat, exists, readFile } from 'fs';

严格模式

ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";

严格模式主要有以下限制。

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval不会在它的外层作用域引入变量
  • evalarguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.callerfn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protectedstaticinterface

上面这些限制,模块都必须遵守。由于严格模式是 ES5 引入的,不属于 ES6,所以请参阅相关 ES5 书籍,本书不再详细介绍了。

其中,尤其需要注意this的限制。ES6 模块之中,顶层的this指向undefined,即不应该在顶层代码使用this

export命令

模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个 JS 文件,里面使用export命令输出变量。

// profile.js
export let firstName = 'Michael';
export let lastName = 'Jackson';
export let year = 1958;
export function log(x) {
  console.log(x);
}
export class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

// 默认导出的模块,函数可以是匿名函数或者非匿名函数。默认导出的模块可以是变量/函数/类等
// export default 20;//还可以是一个值,非默认的是不可以这样用的。
export default function (x) {
  return Math.exp(x);
}
// 非匿名函数默认导出
//export default function foo() {
//  console.log('foo');
//}

// 或者写成

//function foo() {
//  console.log('foo');
//}

//export default foo;

// 或者使用下面的方式export,可以使用as命令重命名对外接口的名字
// export { firstName, lastName, year, log as logger, Point};

import命令

使用export命令的模块都可以使用import命令引入使用,比如上面导出的模块,下面使用import加载使用。as命令不光可以在导出时重命名,在import引入时也可以重命名,用来解决命名冲突的问题。导出默认模块时不需要加{},可以直接使用自定义的名称(默认导出的模块是没有名称的,即使有变量名等对外是无效的,需要导入是指定名称)。

// test.js
import exp, {firstName, lastName, year as pYear, log, Point} from './profile.js';// from '<path>' path must start with either "/", "./", or "../".
// 还可以整体加载,然后用profile.<foo>使用
//import * as profile from './profile.js';// 整体加载后的默认模块名为default
log(firstName);
log(lastName);
log(pYear);
const point = new Point(10, 110);
point.toString();
logger(exp(3.1415926));

htmlscript标签中需要指定type属性为module如此才能以模块的方式解析,否则直接以js的方式解析会报错。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script src="test.js" type="module">
</script>
</body>
</html>

export的继承

export还有与import复合的写法,而这样的写法就可以实现export的继承。

// 模块改名
export {foo as myFoo} from 'some_module';
// 把模块改成默认模块
export {foo as default} from 'some_module';
// 把默认的改成具名模块
export {default as foo} from 'some_module';

// 整体加载
export * from 'some_module';
export * as some from 'some_module';

继承实现就是将原本的整体加载进来,然后再export自己的模块。

动态加载模块import()

之前的import命令是以静态的方式加载模块,它无法实现require()的动态加载模块功能,而import()的出现则解决了这个问题,import()返回一个 Promise 对象。下面是一个例子。

const main = document.querySelector('main');

import(`./section-modules/${someVariable}.js`)
  .then(module => {
    module.loadPageInto(main);
  })
  .catch(err => {
    main.textContent = err.message;
  });

require()import()的区别:

  • require()同步加载模块
  • import()异步加载模块

import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。

import('./myModule.js')
.then(({export1, export2}) => {
  // ...·
});

如果模块有default输出接口,可以用参数直接获得。

import('./myModule.js')
.then(myModule => {
  console.log(myModule.default);
});

上面的代码也可以使用具名输入的形式。

import('./myModule.js')
.then(({default: theDefault}) => {
  console.log(theDefault);
});

如果想同时加载多个模块,可以采用下面的写法。

Promise.all([
  import('./module1.js'),
  import('./module2.js'),
  import('./module3.js'),
])
.then(([module1, module2, module3]) => {
   //···
});

ES6还有很多新的东西,如果只是为了学习vue则暂时不需要那么多,若遇到再查资料也行。

参考资料

作者:阮一峰

文献链接:ECMAScript6入门