目录
1.简介
2.变量声明
2.1 var命令
2.2 function命令
2.3 let命令
2.4 const命令
2.5 import命令
2.6 class命令
3.解构赋值
3.1下面是一些使用嵌套数组进行解构的例子。
3.2解构赋值允许指定默认值。
3.3解构赋值不仅适用于var命令,也适用于let和const命令。
3.4对于Set结构,也可以使用数组的解构赋值。
3.5对象的解构赋值
3.6变量的声明和赋值是一体的。
3.7和数组一样,解构也可以用于嵌套结构的对象。
3.8如果要将一个已经声明的变量用于解构赋值,必须非常小心。
3.9由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。
3.10 字符串的解构赋值
3.11 类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。
3.12 数值和布尔值的解构赋值
3.13 解构赋值的规则是,只要等号右边的值不是对象,就先将其转为对象。
3.14 函数参数的解构也可以使用默认值。
3.15 解构赋值用途:
4.编程风格
4.1 采用严格模式:'use strict';
4.2 let取代var
4.3 全局常量和线程安全
4.4 字符串
4.5 解构赋值
4.6 对象
4.7 数组
4.8 函数
4.9 Map结构
4.10 Class
4.11 模块
ECMAScript 6.0(以下简称ES6)是JavaScript语言的下一代标准,已经在2015年6月正式发布了。它的目标,是使得JavaScript语言可以用来编写复杂的大型应用程序,成为企业级开发语言。
JavaScript的创造者Netscape公司,将JavaScript提交给国际标准化组织ECMA,希望这种语言能够成为国际标准
ECMAScript和JavaScript的关系是,前者是后者的规格,后者是前者的一种实现,.
之所以不叫JavaScript,有两个原因。
一是商标,Java是Sun公司的商标,根据授权协议,只有Netscape公司可以合法地使用JavaScript这个名字,且JavaScript本身也已经被Netscape公司注册为商标。
二是想体现这门语言的制定者是ECMA,不是Netscape,这样有利于保证这门语言的开放性和中立性。
在线将ES6代码转为ES5代码(https://babeljs.io/repl/)
Babel是一个广泛使用的ES6转码器,可以将ES6代码转为ES5代码,从而在现有环境执行。这意味着,你可以用ES6的方式编写程序,又不用担心现有环境是否支持。
ES5只有2种声明变量的方法:var、function。
ES6共有6种声明变量的方法:var、function、let、const、import(require)、class。
几个简单的例子
//1
var a = 10;
var b = 20;
var c = 30;
//2
var a = 10,b = 20,c = 30;
//3
var arr = [1,2,3,4,5];
var a = arr[0];
var b = arr[1];
var c = arr[3];
//4
var obj = {
name: 'gary',
age: 20
}
var a = obj.name;
var b = obj.age;
没有用var关键字,使用直接赋值方式声明的是全局变量,例如:
a = 10;
全局对象是最顶层的对象,在浏览器环境指的是window对象,在Node.js指的是global对象。
ES5之中,全局对象的属性与全局变量是等价的。
window.a = 1;
a // 1
a = 2;
window.a // 2
var 方式定义的函数,不能先调用函数,后声明,只能先声明函数,然后调用。
function方式定义函数可以先调用,后声明。
aaa();//这样调用就会出错
var aaa = function(){
alert("aaa");
}
aaa();//这样就不会出错
//先调用后声明
bbb();
function bbb(){
alert("bbb");
}
块级有效
ES5只有全局作用域和函数作用域,没有块级作用域
ES6中,let实际上为JavaScript新增了块级作用域。
用来声明变量,用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。
//基本举例
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
//for循环的计数器,就很合适使用let命令。
例如:
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[5](); //10
a[6](); // 10
// i 改 为 let 声明
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[5](); //5
a[6](); // 6
//暂时性死区
只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
var tmp='dev';
if (true) {
console.log(tmp);
let tmp;
console.log(tmp);
tmp = 123;
console.log(tmp);
}
在let命令声明变量tmp之前,都属于变量tmp的“死区”。
不允许重复声明
let不允许在相同作用域内,重复声明同一个变量。
// 报错
function test() {
let a = 10;
var a = 1;
}
// 报错
function test() {
let a = 10;
let a = 1;
}
// 不能在函数内部重新声明参数。
function func(arg) {
let arg; // 报错
}
function func(arg) {
{
let arg; // 不报错
}
}
const声明一个只读的常量。
一旦声明,就必须立即初始化,不能留到以后赋值。也不能改变。
const PI = 3.1415;
console.log(PI); // 3.1415
PI = 3; // TypeError: Assignment to constant variable.
const的作用域与let命令相同:只在声明所在的块级作用域内有效,声明的常量,也与let一样不可重复声明。
const命令只是保证变量名指向的地址不变,并不保证该地址的数据不变,所以将一个对象声明为常量必须非常小心。
const foo = {};
foo.prop = 123;
console.log(foo.prop);// 123
foo = {}; // TypeError: "foo" is read-only
var命令和function命令声明的全局变量,依旧是全局对象的属性;
let命令、const命令、class命令声明的全局变量,不属于全局对象的属性。
也就是说,从ES6开始,全局变量将逐步与全局对象的属性脱钩。
var a = 1;
// 如果在Node的REPL环境,可以写成global.a
// 或者采用通用方法,写成this.a
window.a // 1
let b = 1;
window.b // undefined
模块的功能主要由 export 和 import 组成.
每一个模块都有自己单独的作用域,模块之间的相互调用关系是:
通过 export 来规定模块对外暴露的接口,
通过import来引用其它模块提供的接口。
同时还为模块创造了命名空间,防止函数的命名冲突。
ES6将一个文件视为一个模块,通过export 向外输出了一个变量。
一个模块也可以同时往外面输出多个变量。
//test.js
var name = 'Rainbow';
var age = '24';
export {name, age};
定义好模块的输出以后就可以在另外一个模块通过import引用。
import {name, age} from './test.js'
整体输入,module指令
//test.js
export function getName() {
return name;
}
export function getAge(){
return age;
}
通过 import * as 就完成了模块整体的导入。
import * as test form './test.js';
通过指令 module 也可以达到整体的输入。
module test from 'test.js';
test.getName();
// export default
不管关系模块输出了什么,通过 export default 指令就能加载到默认模块,
不需要通过花括号来指定输出的模块,
一个模块只能使用 export default 一次
// default 导出
export default function getAge() {}
// 或者写成
function getAge() {}
export default getAge;
// 导入的时候不需要花括号
import test from './test.js';
一条import 语句可以同时导入默认方法和其它变量.
import defaultMethod, { otherMethod } from 'xxx.js';
如果你用过纯面向对象语言,那么你会对class的语法非常熟悉。
class People {
constructor(name) { //构造函数
this.name = name;
}
sayName() {
console.log(this.name);
}
}
var p = new People("Tom");
p.sayName();
上面定义了一个People类,他有一个属性 name 和一个方法 sayName(),还有一个构造函数;
就像函数有函数声明和函数表达式两种定义方式,类也可以通过类表达式来定义:
let People = class {
constructor(name) { //构造函数
this.name = name;
}
sayName() {
console.log(this.name);
}
}
你可能以为类声明和类表达式的区别在于变量提升的不同。
但是事实是无论是类声明还是类表达式的方式来定义,都不会有变量提升。
通过关键字 extends 来继承一个类,并且可以通过 super 关键字来引用父类。
class Student extends People {
constructor(name, grade) { //构造函数
super(name); //通过 super 调用父类的构造函数的。
this.grade = grade;
}
sayGrade() {
console.log(this.grade);
}
}
上面的例子中我们定义了一个 Student ,他是 People 的子类。
下面我们给 name 属性定义 getter 和 setter
class People {
constructor(name) { //构造函数
console.log('constructor'); //TOM
this.name = name;
}
get name() {
console.log('get'); //TOM
return this._name.toUpperCase();
}
set name(name) {
console.log('set'); //TOM
this._name = name;
}
sayName() {
console.log('sayName'); //TOM
console.log(this.name);
}
}
var p = new People("tom");
console.log(p.name); //TOM
console.log(p._name); //tom
p.sayName(); //TOM
仔细看上面的例子,搞清楚最后三行分别会输出什么,就明白getter 和 setter该怎么用了。
输出日志:
可以发现,this.name = name 复制会调用set方法,this.name读取会调用get方法
主要是要区分 this._name 和 this.name 的区别。因为我们定义了 name 的读写器,而没有定义 _name 的读写器,所以访问这两个属性的结果是不同的。
但是要注意一点,不要这样写:
set name(name) {
this.name = name;
}
因为给 this.name =name 赋值的时候会调用 set name ,这样会导致无限递归直到栈溢出。
通过 static 关键字定义静态方法:
class People {
constructor(name) { //构造函数
this.name = name;
}
sayName() {
console.log(this.name);
}
static formatName(name) {
return name[0].toUpperCase() + name.sustr(1).toLowerCase();
}
}
console.log(People.formatName("tom"));
解构:ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。如果解构不成功,变量的值就等于undefined。
“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。
let [ foo, [[bar], baz] ] = [ 1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [x, , y] = [1, 2, 3];//不完全解构
x // 1
y // 3
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
var [foo = true] = [];
foo // true
[x, y = 'b'] = ['a'];
// x='a'
// y='b'
[x, y = 'b'] = ['a', undefined];
// x='a'
// y='b'
注意,ES6内部使用严格相等运算符(===),判断一个位置是否有值。
所以,如果一个数组成员不严格等于undefined,默认值是不会生效的。
默认值可以引用解构赋值的其他变量,但该变量必须已经声明。
let [x = 1, y = x] = []; // x=1; y=1
let [x = 1, y = x] = [2]; // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = []; // ReferenceError
var [v1, v2, ..., vN ] = array;
let [v1, v2, ..., vN ] = array;
const [v1, v2, ..., vN ] = array;
let [x, y, z] = new Set(["a", "b", "c"]);
x // "a"
解构不仅可以用于数组,还可以用于对象。
var { foo, bar } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
对象的解构与数组有一个重要的不同。
数组的元素是按次序排列的,变量的取值由它的位置决定;
对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
var { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
var { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined
如果变量名与属性名不一致,必须写成下面这样。
var { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'
这实际上说明,对象的解构赋值是下面形式的简写。
var { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
var { foo: baz } = { foo: "aaa", bar: "bbb" };
baz // "aaa"
foo // error: foo is not defined
对于let和const来说,变量不能重新声明,所以一旦赋值的变量以前声明过,就会报错。
let foo;
let {foo} = {foo: 1}; // SyntaxError: Duplicate declaration "foo"
let baz;
let {bar: baz} = {bar: 1}; // SyntaxError: Duplicate declaration "baz"
上面代码中,解构赋值的变量都会重新声明,所以报错了。
不过,因为var命令允许重新声明,所以这个错误只会在使用let和const命令时出现。
如果没有第二个let命令,上面的代码就不会报错。
let foo;
({foo} = {foo: 1}); // 成功
let baz;
({bar: baz} = {bar: 1}); // 成功
上面代码中,let命令下面一行的圆括号是必须的,否则会报错。
因为解析器会将起首的大括号,理解成一个代码块,而不是赋值语句。
var obj = {
p: [
'Hello',
{ y: 'World' }
]
};
var { p: [x, { y }] } = obj;
x // "Hello"
y // "World"
注意,这时p是模式,不是变量,因此不会被赋值。
var node = {
loc: {
start: {
line: 1,
column: 5
}
}
};
var { loc: { start: { line }} } = node;
line // 1
loc // error: loc is undefined
start // error: start is undefined
默认值生效的条件是,对象的属性值严格等于undefined。
// 错误的写法
var x;
{x} = {x: 1};
// SyntaxError: syntax error
上面代码的写法会报错,因为JavaScript引擎会将{x}理解成一个代码块,从而发生语法错误。
只有不将大括号写在行首,避免JavaScript将其解释为代码块,才能解决这个问题。
// 正确的写法
({x} = {x: 1});
var arr = [1, 2, 3];
var {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3
字符串也可以解构赋值。
这是因为此时,字符串被转换成了一个类似数组的对象。
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
let {length : len} = 'hello';
len // 5
解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
上面代码中,数值和布尔值的包装对象都有toString属性,因此变量s都能取到值。
由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
函数move的参数是一个对象,通过对这个对象进行解构,得到变量x和y的值。如果解构失败,x和y等于默认值。
function move({x = 0, y = 0} = {}) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
函数move的参数指定默认值,而不是为变量x和y指定默认值,所以会得到与前一种写法不同的结果。
function move({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]
(1)交换变量的值:[x, y] = [y, x];
数组的元素是按次序排列的,变量的取值由它的位置决定;
对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
上面代码交换变量x和y的值,这样的写法不仅简洁,而且易读,语义非常清晰。
(2)从函数返回多个值
函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。
// 返回一个数组
function example() {
return [1, 2, 3];
}
var [a, b, c] = example();
// 返回一个对象
function example() {
return {
foo: 1,
bar: 2
};
}
var { foo, bar } = example();
(3)函数参数的定义
解构赋值可以方便地将一组参数与变量名对应起来。
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
(4)提取JSON数据
解构赋值对提取JSON对象中的数据,尤其有用。
var jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
console.log(id, status, number);
// 42, "OK", [867, 5309]
上面代码可以快速提取JSON数据的值。
(5)函数参数的默认值
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
}) {
// ... do stuff
};
指定参数的默认值,就避免了在函数体内部再写var foo = config.foo || 'default foo';这样的语句。
(6)遍历Map结构
任何部署了Iterator接口的对象,都可以用for...of循环遍历。
Map结构原生支持Iterator接口,配合变量的解构赋值,获取键名和键值就非常方便。
var map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
for (let [key, value] of map) {
console.log(key + " is " + value);
}
// first is hello
// second is world
如果只想获取键名,或者只想获取键值,可以写成下面这样。
// 获取键名
for (let [key] of map) {
// ...
}
// 获取键值
for (let [,value] of map) {
// ...
}
(7)输入模块的指定方法
加载模块时,往往需要指定输入那些方法。解构赋值使得输入语句非常清晰。
const { SourceMapConsumer, SourceNode } = require("source-map");
变量必须声明后再使用
在块级作用域下,let完全可以取代var,因为两者语义相同,而且let没有副作用。
在let和const之间,建议优先使用const,
尤其是在全局环境,不应该设置变量,只应设置常量。
所有的函数都应该设置为常量。这符合函数式编程思想,有利于将来的分布式运算。
const声明常量还有两个好处,一是阅读代码的人立刻会意识到不应该修改这个值,二是防止了无意间修改变量值所导致的错误。
长远来看,JavaScript可能会有多线程的实现(比如Intel的River Trail那一类的项目),这时let表示的变量,只应出现在单线程运行的代码中,不能是多线程共享的,这样有利于保证线程安全。
静态字符串一律使用单引号或反引号,不使用双引号。
动态字符串使用反引号。
使用数组成员对变量赋值时,优先使用解构赋值。
函数的参数如果是对象的成员,优先使用解构赋值。
函数返回多个值,优先使用对象的解构赋值,而不是数组的解构赋值。便于以后添加返回值,及更改返回值的顺序
单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。
对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用Object.assign方法。
如果对象的属性名是动态的,可以在创造对象的时候,使用属性表达式定义。
对象的属性和方法,尽量采用简洁表达法,这样易于描述和书写。
使用扩展运算符(...)拷贝数组。
使用Array.from方法,将类似数组的对象转为数组。
立即执行函数可以写成箭头函数的形式。(...)=> { ...}
那些需要使用函数表达式的场合,尽量用箭头函数代替。这样更简洁,而且绑定了this。
简单的、单行的、不会复用的函数,建议采用箭头函数。
函数体较为复杂,行数较多,还是应该采用传统的函数写法。
所有配置项都应该集中在一个对象,放在最后一个参数,布尔值不可以直接作为参数。
不要在函数体内使用arguments变量,使用rest运算符(...)代替。因为rest运算符显式表明你想要获取参数,而且arguments是一个类似数组的对象,而rest运算符可以提供一个真正的数组。
注意区分Object和Map,只有模拟现实世界的实体对象时,才使用Object。如果只是需要key: value的数据结构,使用Map结构。因为Map有内建的遍历机制。
总是用Class,取代需要prototype的操作。因为Class的写法更简洁,更易于理解。
使用extends实现继承,因为这样更简单,不会有破坏instanceof运算的危险。
首先,Module语法是JavaScript模块的标准写法,坚持使用这种写法。
使用import取代require。
如果模块只有一个输出值,就使用export default,如果模块有多个输出值,就不使用export default,不要export default与普通的export同时使用。
不要在模块输入中使用通配符。因为这样可以确保你的模块之中,有一个默认输出(export default)。
如果模块默认输出一个对象,对象名的首字母应该大写。