1、let/ const声明变量(项目中常用)
之前使用var关键字声明变量,无论声明在何处,都会被视为声明在它的作用域的最顶部(不在大括号内{}即在全局作用域的最顶部),而在es6之前JavaScript只有函数作用域和全局作用域。这就是变量提升。
console.log(a); //输出undefined if (true) { var a = 1; }
上面的代码实际上是:
var a; console.log(a); //输出undefined if (true) { a = 1; }
(1)let声明变量
let实际上为JavaScript新增了块级作用域。let声明的变量只在它所在的代码块内有效。
将上面变量提升的例子稍微变化一下:可以看出let声明的a只在代码块内有效。
if(true) { var a = 1; } console.log(a) //输出1
if(true) { let a = 1; } console.log(a) //报错ReferenceError: a is not defined
再看一个常见的例子:
var a = []; for (var i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 10
每次循环的i指向的其实都是同一个i,很显然最后的结果是10。那么如何达到想要的效果?
·闭包(ES5)
var a = []; for (var i = 0; i < 10; i++) { a[i] = (function (num) { return function () { console.log(num); }; })(i) } a[6](); // 6
·let声明变量(ES6)
var a = []; for (let i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 6
变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量。
使用let声明变量的话,不存在变量提升的情况,必须在声明以后才使用,否则会报错,这在语法上称为“暂时性死区”。并且let不允许在相同作用域内重复声明变量。
(2)const声明变量
const声明的变量是一个只读变量,一旦声明就必须马上初始化并且不能改变值,因此如果用const只声明而不赋值也会报错。
const的作用域与let相同,只在当前的块级作用域内有效。同样的,也不能变量提升,存在暂时性死区,不能重复声明。
本质:
const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指针,const只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
const foo = {}; // 为 foo 添加一个属性,可以成功 foo.prop = 123; foo.prop // 123 // 将 foo 指向另一个对象,就会报错 foo = {}; // TypeError: "foo" is read-only
总结:因此,可以知道什么时候用let什么时候用const:值会改变的变量用let声明,值是一个不会改变的常量就用const声明。
2、箭头函数(项目中常用)
Es6使用“箭头”(=>)定义函数。箭头函数使得表达更简洁。
箭头函数的写法:
const fn = (a, b) => a + b;
就相当于ES5标准下的:
var fn = function (a, b) { return a + b; }
箭头函数还可以与变量解构结合使用。
const full = ({ first, last }) => first + ‘ ‘ + last;
// 等同于 function full(person) { return person.first + ‘ ‘ + person.last; }
使用箭头函数有几点需要注意。
(1) 函数体内的this对象,是定义时所在的对象,而不是使用时所在的对象。如果在箭头函数内使用this,那么该this是外层的this,换句话说,就是箭头函数里根本没有自己的this,而是引用外层的this。
(2) 不可以当做构造函数,也就是说,不可以使用new命令,否则抛出错误。
(3) 不可以使用arguments对象,该对象在函数体内不存在。如果一定要用,可以使用rest参数代替。
上面的第一点尤其要注意,this对象的指向是可变的,但在箭头函数中,它是固定的。
function foo() { setTimeout(() => { console.log(‘id:‘, this.id); }, 100); } var id = 21; foo.call({ id: 42 }); // id: 42
上面代码中,setTimeout的参数是一个箭头函数,该箭头函数生效是在foo函数生成时,而它真正执行则是在100毫秒后。如果是普通函数,执行时的this执行全局对象window,输出21。而箭头函数的this总是绑定在定义生效时所在的作用域,因此输出42。
3、模板字符串
模板字符串解决了使用+号拼接字符串造成的不便利。模板字符串用(`)反引号标识,可以作普通字符串,也可以定义多行字符串,或者在字符串在嵌入变量。
// 普通字符串 `In JavaScript ‘\n‘ is a line-feed.`
// 多行字符串 `In JavaScript this is not legal.`
// 字符串中嵌入变量 let name = "Bob", time = "today"; `Hello ${name}, how are you ${time}?`
使用模板字符串表示多行字符串,所以的空格和缩进都会被保留在输出中。
使用${}包裹一个变量或表达式。
4、变量的解构赋值
(1)数组的解构赋值
[基本用法]
let [a, b, c] = [1, 2, 3];
上面代码表示。可以从数组中提取值,按照对应位置,给变量赋值。
本质上,这种写法属于“模式匹配”,只要等号两边模式相同,左边的变量就会被赋予对应的值。
如果解构不成功,变量的值就等于undefined。
let [foo] = []; foo //undefined let [bar, foo] = [1]; foo //undefined
另一种情况是不完全解构,即等号左边的模式只匹配一部分等号右边的数组,这种情况下,解构依然成功
let [x, y] = [1, 2, 3]; x // 1 y // 2
[默认值]
解构赋值允许制定默认值。
let [foo = true] = []; foo // true
es6内部使用严格相等运算符(===),判断一个位置是否有值。只有数组成员严格等于undefined,默认值才会生效。
let [x, y = ‘b‘] = [‘a‘]; // x=‘a‘, y=‘b‘ let [x, y = ‘b‘] = [‘a‘, undefined]; // x=‘a‘, y=‘b‘ let [x = ‘a‘] = [null]; x // null
null不严格等于undefined,因此默认值不生效
(2)对象的解构赋值
解构不仅可以用于数组,还可以应用于对象。
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" };
baz // "aaa"
foo // error: foo is not defined
foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。
(3)字符串的解构赋值
const [a, b, c, d, e] = ‘hello‘; a // "h" b // "e" c // "l" d // "l" e // "o"
还可以对字符串的length属性解构赋值
let { length: len } = ‘hello‘;
len // 5
(4)数值和布尔值的解构赋值
解构赋值是,登等号右边是数值或布尔值,则会先转为对象。
let { toString: s } = 123;
s === Number.prototype.toString // true
let { toString: s } = true;
s === Boolean.prototype.toString // true
(5)函数参数的解构赋值
函数的参数也可使用解构赋值。
function add([x, y]) { return x + y; } add([1, 2]); // 3
(6)用途
变量的解构赋值的用途:
·交换变量的值
let x = 1; let y = 2; [x, y] = [y, x];
·从函数返回多个值
函数只能返回一个值,如果要返回多个值,只能把他们放在数组或对象里返回。有了解构赋值,取出这些值非常方便。
// 返回一个数组 function example() { return [1, 2, 3]; } let [a, b, c] = example();
// 返回一个对象 function example() { return { foo: 1, bar: 2 }; } let { foo, bar } = example();
·函数参数的定义
解构赋值可以方便地将一组参数与变量名对应。
// 参数是一组有次序的值 function f([x, y, z]) { ... } f([1, 2, 3]);
// 参数是一组无次序的值 function f({ x, y, z }) { ... } f({ z: 3, y: 2, x: 1 });
·提取数据
解构赋值对提取JSON对象中的数据尤其有用。
let jsonData = { id: 42, status: "OK", data: [867, 5309] }; let { id, status, data: number } = jsonData; console.log(id, status, number); // 42, "OK", [867, 5309]
·函数参数的默认值
指定参数的默认值,就避免了在函数内部再写var foo = config.foo || ‘default foo’。
jQuery.ajax = function (url, { async = true, beforeSend = function () {}, cache = true, complete = function () {}, crossDomain = false, global = true, // ... more config }) { // ... do stuff };
5、函数参数的默认值
(1)默认用法
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有没有赋值,如果没有,则指定默认值为World。但这样的缺点在于,如果参数y赋值了,但对应的布尔值为false,则该赋值不起作用。如上面代码最后一行,y等于空字符,结果还是被改为默认值。
为避免这个问题,通常需先判断参数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
(2)与解构赋值默认值结合使用
函数参数默认值可以与解构赋值的默认值,结合起来使用。
function foo({ x, y = 5 }) { console.log(x, y); } foo({}) // undefined 5 foo({ x: 1 }) // 1 5 foo({ x: 1, y: 2 }) // 1 2 foo() // TypeError: Cannot read property ‘x‘ of undefined
上面的代码只使用了对象的解构赋值默认值,没有使用函数参数的默认值。只当函数foo的参数是一个对象时,变量x和y才会通过解构赋值生成。如果函数调用时没提供参数,变量x和变量y就不会生成,因此报错。提供函数的默认值就可以避免这种情况。
function foo({ x, y = 5 } = {}) { console.log(x, y); } foo() // undefined 5
上面的代码指定,没有提供参数,函数的参数默认为一个空对象。
// 写法一 function m1({ x = 0, y = 0 } = {}) { return [x, y]; } // 写法二 function m2({ x, y } = { x: 0, y: 0 }) { return [x, y]; }
上面两张写法都对函数的参数设定了默认值,区别是写法一函数参数的默认值是空对象,但设置了对象解构赋值的默认值;写法二函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值。
// 函数没有参数的情况 m1() // [0, 0] m2() // [0, 0] // x 有值,y 无值的情况 m1({ x: 3 }) // [3, 0] m2({ x: 3 }) // [3, undefined] // x 和 y 都无值的情况 m1({}) // [0, 0]; m2({}) // [undefined, undefined]
(3)作用域
设置了参数的默认值,函数进行声明初始化时,参数会形成单独的作用域,初始化结束,则该作用域消失。
var x = 1; function f(x, y = x) { console.log(y); } f(2) // 2
上面代码,y的默认值等于变量x。调用函数f时,参数形成一个单独的作用域。在这个作用域里,默认值变量x指向第一个参数x,而不是全局变量x,因此输出的是2。
let x = 1; function f(y = x) { let x = 2; console.log(y); } f() // 1
上面代码,函数f调用时,参数y=x形成一个单独作用域,在这个作用域里,变量x本身无定义,所以指向外层的全局变量x。函数调用时,函数体内部的局部变量x不影响默认值变量x。
var x = 1; function foo(x = x) { // ... } foo() // ReferenceError: x is not defined
上面代码,参数x=x形成单独作用域,实际执行的是let x = x,由暂时性死区的原因,这行代码会报错“x未定义”。
如过参数的默认值是一个函数,该函数的作用域也遵守这个规则。
let foo = ‘outer‘; function bar(func = () => foo) { let foo = ‘inner‘; console.log(func()); } bar(); // outer
上面代码,函数bar的参数func的默认值是一个匿名函数,返回值为变量foo。函数参数形成的单独作用域里,没有定义变量foo,因此foo指向外层的全局变量foo,因此输出outer。
var x = 1; function foo(x, y = function () { x = 2; }) { var x = 3; y(); console.log(x); } foo() // 3 x // 1
上面代码,函数foo的参数形成一个单独作用域。改作用域里,首先声明变量x,然后声明变量y,y的默认值是一个匿名函数。这个匿名函数内部的变量x,指向同一个作用域的第一个参数x。函数foo内部又声明了一个内部变量x,该变量与第一个参数x由于不是同一个作用域,所以不是同一个变量,因此执行y后,内部变量x和外部全局变量x的值都没变。
var x = 1; function foo(x, y = function () { x = 2; }) { x = 3; y(); console.log(x); } foo() // 2 x // 1
如果将var x = 3的var去除,函数foo的内部变量x就指向第一个参数x,与匿名函数内部的x是一致的,最后输出2,而外层的全局变量x不受影响。
6、rest参数
引入rest参数(…变量名),用于获取函数的多余参数,这样就不需要使用arguments对象。Rest参数搭配的变量是一个数组,将多余的参数放入数组中。
function add(...values) { let sum = 0; for (var val of values) { sum += val; } return sum; } add(2, 5, 3) // 10
add函数是一个求和函数,利用rest参数,可以向该函数传入任意数目的参数。
// arguments变量的写法 function sortNumbers() { return Array.prototype.slice.call(arguments).sort(); }
// rest参数的写法 const sortNumbers = (...numbers) => numbers.sort();
上面是rest参数代替arguments变量的例子。比较后可发现,rest参数的写法更自然更简洁。
arguments对象不是数组,是类似数组的对象,为使用数组方法,需先将其转为数组。Rest参数就不存在这个问题,它自身就是数组。
// arguments变量的写法 function sortNumbers() { return Array.prototype.slice.call(arguments).sort(); }
// rest参数的写法 const sortNumbers = (...numbers) => numbers.sort();
上面是利用rest参数改写数组push方法的例子。
需注意。Rest参数之后不能再有其他参数,即只能是最后一个参数,否则报错。
函数的length属性返回没有默认值的参数的个数,其中不包括rest参数。
(function (a) { }).length // 1 (function (...a) { }).length // 0 (function (a, ...b) { }).length // 1
7、对象的扩展
(1)Es6中可以直接写入变量和函数,作为对象的属性和方法,使书写更简洁。
const foo = ‘bar‘; const baz = { foo }; baz // {foo: "bar"}
// 等同于 const baz = { foo: foo };
此时,属性名为变量名,属性的值为变量的值。
function f(x, y) { return { x, y }; }
// 等同于 function f(x, y) { return { x: x, y: y }; } f(1, 2) // Object {x: 1, y: 2}
方法也可以简写。
在一个模块对外提供接口时非常适合使用简洁写法。
const getName = () => person.name;
const getAge = () => person.age;
commons的写法:
module.exports = {
getName: getName,
getAge: getAge,
};
Es6 modules的写法:
export default { getName, getAge };
(2)若自适应字面量方法定义对象(使用大括号),es5中只能像下面这样定义属性:
var obj = { foo: true, abc: 123 };
在es6中,就可以把表达式放在方括号内定义对象:
let propKey = ‘foo‘; let obj = { [propKey]: true, [‘a‘ + ‘bc‘]: 123 };
8、class
生成实例对象的传统方法使通过构造函数,与传统的面向对象语言差异很大,es6引入class类这个概念,通过class关键字可以定义类。Es6的class可以看作是一个语法糖,它的绝大部分功能,es5都可以做到,class写法只是让对象的写法更清晰。
es5生成实例对象:
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.getName = function () { return this.name; } var person = new Person(‘abc‘, 21);
es6用class改写
class Person { constructor(name, age) { this.name = name; this.age = age; } getName() { return this.name } } var person = new Person(‘abc‘, 21);
constructor是构造方法,this代表实例对象,es5的构造函数Person,对应的就是Person类的构造方法,定义类方法使不需要function关键字,直接把函数定义放进去,另外方法之间也不需要逗号分隔。
typeof Person // "function" Person === Person.prototype.constructor // true
上面表明,类的数据类型就是函数,类本身事项构造函数。
class Person { constructor() { // ... } getName() { // ... } }
// 等同于 Person.prototype = { constructor() {}, getName() {} };
类的所有方法都定义在类的prototype属性上。因此,类的新方法可以添加在prototype对象上。而Object.assign方法可以仿版地一次想类添加多个方法。
Object.assign(Person.prototype, {
getNmae() {},
getAge() {}
});
9、class的继承
1)Es6中,class可以通过extends关键字实现继承,比起es5的通过修改原型链实现继承方便的多。
class Person { constructor(name, age) { this.name = name; this.age = age; } getName() { return this.name } }
// Student类继承Person类 class Student extends Person { constructor(name, age, gender, classes) { super(name, age); this.gender = gender; this.classes = classes; } getGender() { return this.gender; } }
不用像es5那样需要考虑构造函数继承哈斯原型继承,只需要使用extends关键字并关注一个叫super的方法。
如果不使用super方法,在新建实例时就会报错。
Es5的继承,实质是县创造子类实例对象的this,在将父类的方法添加到this(Parent.apply(this))。 而es6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类构造函数修改this。
如果子类没有定义constructor(),也会被默认添加。即任何一个子类都有constructor方法。
class Student extends Person {} // 等同于 class Student extends Person { constructor(...args) { super(...args); } }
需要注意,只有调用super以后才能使用this,否则报错。因为上面说过,子类实例的构建,是基于父类实例加工,super方法返回父类实例。
1) super关键字既可以当作函数使用也可以当作对象使用。
·Super作为函数调用时,代表父类的构造函数,子类的构造数必须执行一次super函数。
class A {}
class B extends A {
constructor() {
super();
}
}
Super虽然代表了父类A的工业早函数,但返回的是子类B的实例,即super内部的this指的是B,因此super在这里相当于A.prototype.constructor.call(this)。
class A { constructor() { console.log(new.target.name); } } class B extends A { constructor() { super(); } } new A() // A new B() // B
new.target指向当前正在执行的函数。从上面的代码可以看出:在super执行时,它指向的是子类B的构造函数,即super内部的this指向B
并且,作为函数是,super只能用在子类的构造函数之中,否则报错。
·第二种情况,super作为对象。在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
class A { p() { return 2; } } class B extends A { constructor() { super(); console.log(super.p()); // 2 } } let b = new B();
上面代码中,子类B的super.p就是把super当做对象使用,此时super在普通方法中,指向A.prototype,因此super.p()就相当于A.prototype.p()。
注意:定义在父类实例上的方法、属性无法通过super调用。
class A { constructor() { this.x = 1; } print() { console.log(this.x); } } class B extends A { constructor() { super(); this.x = 2; } m() { super.print(); } } let b = new B(); b.m() // 2
super.print()虽然调用的是A.prototype.print(),但A.prototype.print()内部的this指向子类B,因此输出2,而不是1。实际上执行的是super.print.call(this)。
如果super作为对象,用在静态方法中,super就指向父类,而不是父类的原型对象。
class Parent { static myMethod(msg) { console.log(‘static‘, msg); } myMethod(msg) { console.log(‘instance‘, msg); } } class Child extends Parent { static myMethod(msg) { super.myMethod(msg); } myMethod(msg) { super.myMethod(msg); } } Child.myMethod(1); // static 1 var child = new Child(); child.myMethod(2); // instance 2
上面的代码,super在静态方法中指向父类,在普通方法中指向父类的原型对象。
10、promise(项目中ajax.js中使用)
promise的作用与回调方法(callback)一样,都是在某种情况下执行设定好的方法。但promise的多重链式调用能使代码更整洁,避免出现“回调地狱”(回调嵌套太多层)。在es6中,promise成为了原生对象可以直接使用。
Ajax请求的传统写法:

改为promise写法:

很显然,promise的写法把异步调用中使用回调函数的场景改为了.then()、.catch()等函数链式调用的方式,基于promise可以把复杂的异步调用方式进行模块化。
Promise的原理分析:
Promise对象共有三个状态,分别是:
·pending(进行中)
·resolved(已完成,又称为fullfilled)
·rejected(已失败)
由异步操作的结果决定当前是什么状态。状态的改变只有两种可能:
·从pending变为fullfilled
·从pending变为rejected
只要这两种情况发生,状态就不会再改变了,因此状态是不能逆向改变的。
构建promise:

Promise的构造函数接受一个函数作为参数,该函数的两个参数分别为resolve和reject两个函数。
·Resolve函数将promise对象的状态由pending变为resolved,异步操作成功时调用,并将异步操作的结果,作为参数传递出去。
·reject函数将状态由pending变为rejected,异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise的实例方法:
Promise对象拥有两个实例方法then()和catch()。
·then()方法接受两个函数作为参数,第一个是状态变为resolved时调用,第二个则是状态变为rejected是调用,第二个函数是可选的。
Promise实例生成以后,可以用then方法指定resolved状态和rejected状态的回调函数,即成功和失败的回调函数。
promise构造函数中通常都是异步的,所以then方法往往都先于resolve和reject方法执行。这两个函数作为成功和失败的回调函数,都接受promise对象传出的值作为参数。

then()方法将返回一个新的promise。
因此then可以链式调用,在新的对象上添加失败或成功的回调。
·catch()方法的作用是捕获promise的错误。
与then()方法的rejected回调作业几乎一致。
我们知道,如果 Promise 状态已经变成resolved,再抛出错误是无效的。

上面代码中,Promise 在resolve语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。

promise对象的错误会一直向后传递,直到被捕获,即错误总会被下一个catch所捕获。then方法指定的回调函数,若抛出错误,也会被下一个catch捕获。catch中也能抛错,则需要后面的catch来捕获。
因此一般来说,不要在then方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。
这样就能够在下一个catch()中统一处理这些错误。同时catch()也能够捕获then()中抛出的错误,所以建议不使用then()的rejected回调,而是统一使用catch()来处理错误。
跟传统的try/catch代码块不同的是,如果没有使用catch方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。Promise 内部的错误不会影响到 Promise 外部的代码。
11、Module(项目中常用)
ES6实现了模块功能,可以取代CommonJS和AMD规范,称为浏览器和服务器通用的模块解决方案。
模块功能主要由两个命令构成:export和import。
Export用于规定模块的对外接口,import用于输入其他模块提供的功能。
一个模块即一个独立文件。该文件内部的所有变量,在外部是无法获取的。如果需要读取模块内的某个变量,就必须使用export输出该变量。
(1)export
export可以输出变量:
// profile.js export var firstName = ‘Michael‘; export var lastName = ‘Jackson‘; export var year = 1958;
或者用大括号指定要输出的一组变量:
// profile.js var firstName = ‘Michael‘; var lastName = ‘Jackson‘; var year = 1958; export { firstName, lastName, year };
除了输出变量,还可以输出函数或class类
export function multiply(x, y) { return x * y; };
(2)import
使用export定义了,模块对外的几口以后,其他js文件就可以通过import加载这个模块。
// main.js import { firstName, lastName, year } from ‘./profile.js‘; function setName(element) { element.textContent = firstName + ‘ ‘ + lastName; }
Import接收一堆大括号,里面指定要从其他模块导入的变量名,变量名必须与被导入模块(profile.js)对外接口的名称相同。
Import输入的变量都是只读的,不允许在加载模块的脚本里改写接口。
From指定模块文件的位置,.js后缀可以省略。
Import具有提升效果,会提升到整个模块的头部首先执行。
(3)export default
使用export default为模块指定默认输出。
// export-default.js export default function () { console.log(‘foo‘); }
默认输出一个匿名函数
// import-default.js import customName from ‘./export-default‘; customName(); // ‘foo‘
加载该模块时,import为函数指定名字。
需要注意的是export default对应的import不需要使用大括号{}。而export对应的import需要使用大括号。