1.let和constlet和const
共同点:声明的变量仅在块级作用域内有效;不存在变量提升;暂时性死区;不允许重复声明;全局变量将逐步与顶层对象的属性脱钩。let和const
不同点:const
一旦声明变量,就必须立即初始化,不能留到以后赋值。
代码示例一:
for(let i=1;i<5;i++){
setTimeout(function(){
console.log(i)
},1000)
}
代码示例二:
var a = [];
for (var i = 0; i < 10; i++) {
var c = i;
a[i] = function () {
console.log(c);
};
}
a[6](); // 9
如果使用let,声明的变量仅在块级作用域内有效,最后输出的是6。
var a = [];
for (var i = 0; i < 10; i++) {
let c = i;
a[i] = function () {
console.log(c);
};
}
a[6](); // 6
代码示例三:
块级作用域:
function f() { console.log('I am outside!'); }
(function () {
if(false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}
f();
}());
上面代码在ES5中运行,会得到“I am inside!”,但是在ES6中运行,会得到“I am outside!”。这是因为ES5存在函数提升,不管会不会进入if代码块,函数声明都会提升到当前作用域的顶部,得到执行;而ES6支持块级作用域,不管会不会进入if代码块,其内部声明的函数皆不会影响到作用域的外部。
function f1() { console.log('I am outside!!!!'); }
(function () {
if(true) {
// 重复声明一次函数f
function f1() { console.log('I am inside!!!!'); }
}
f1(); // I am outside!!!!
}());
2.变量的解构赋值
(1)数组的解构赋值(如果等号的右边不是数组(或者严格地说,不是可遍历的结构),那么将会报错):
let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
var [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
对于Set结构,也可以使用数组的解构赋值。
[a, b, c] = new Set(["a1", "b1", "c1"])
a // "a1"
b // "b1"
c // "c1"
解构赋值允许指定默认值。
let [x, y = 'b'] = ['a']; // x='a', y='b'
(2)对象的解构赋值(只要等号右边的值不是对象,就先将其转为对象。比如undefined和null无法转为对象,所以对它们进行解构赋值,就会报错):
数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
对象的解构赋值是下面形式的简写:
let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
如果变量名与属性名不一致,必须写成下面这样。
var { foo: baz } = { foo: "aaa", bar: "bbb" };
baz // "aaa"
对象的解构也可以指定默认值。
var {x, y = 5} = {x: 1};
x // 1
y // 5
下面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行,否则JavaScript引擎会将{x}理解成一个代码块,从而发生语法错误。
({x} = {x: 1});
解构赋值代码示例:
// 返回一个数组
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();
// 返回一个对象
function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();
3.字符串的扩展
(1)Unicode字符表示法: JavaScript允许采用“uxxxx”形式表示一个字符,其中“xxxx”表示字符的码点。超过0xFFFF的数值(比如u20BB7),将码点放入大括号,就能正确解读该字符。
"\u{20BB7}"
// "?"
(2)codePointAt(),fromCodePoint()
对于需要4个字节储存的字符(Unicode码点大于0xFFFF的字符),codePointAt()
能够正确处理,返回一个字符的码点。
var s = '?a';
for (let ch of s) {
console.log(ch.codePointAt(0).toString(16));
}
// 20bb7
// 61
codePointAt()
是测试一个字符由两个字节还是由四个字节组成的最简单方法。
function is32Bit(c) {
return c.codePointAt(0) > 0xFFFF;
}
is32Bit("?") // true
is32Bit("a") // false
String.fromCodePoint(0x20BB7)
// "?"
(3)正则表达式的u修饰符
--点字符
--Unicode字符表示法
--量词
--i修饰符
--预定义模式:由此可以写出一个正确返回字符串长度的函数。
function codePointLength(text) {
var result = text.match(/[\s\S]/gu);
return result ? result.length : 0;
}
var s = "??";
s.length // 4
codePointLength(s) // 2
(4)includes(), startsWith(), endsWith(),repeat(),padStart(),padEnd()
var s = "Hello world!";
s.startsWith("world", 6) // true
s.endsWith("Hello", 5) // true
s.includes("Hello", 6) // false
'x'.repeat(3) // "xxx"
'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
注:endsWith()
的行为与includes(), startsWith()
两个方法有所不同。它针对前n个字符,而其他两个方法针对从第n个位置直到字符串结束。
(5)正则表达式的y修饰符
ES6为正则表达式添加了y修饰符,叫做“粘连”(sticky)修饰符。它的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始,不同之处在于,g修饰符只确保剩余位置中存在匹配,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。
var r = /hello\d/y;
r.sticky // true
(6)模板字符串
传统的JavaScript语言写法:
$('#result').append(
'There are <b>' + basket.count + '</b> ' +
'items in your basket, ' +
'<em>' + basket.onSale +
'</em> are on sale!'
);
ES6模板字符串:
$('#result').append(`
There are <b>${basket.count}</b> items
in your basket, <em>${basket.onSale}</em>
are on sale!
`);
4.数值的扩展
(1)二进制和八进制表示法
ES6提供了二进制和八进制数值的新的写法,分别用前缀0b和0o表示。
0b111110111 === 503 // true
0o767 === 503 // true
(2)Number.isFinite(), Number.isNaN()
:只对数值有效,非数值一律返回false。
(3)Number.parseInt(), Number.parseFloat()
:行为完全保持不变,逐步减少全局性方法,使得语言逐步模块化。
(4)Number.isInteger(),Number.EPSILON,Number.MAX_SAFE_INTEGER,Number.MIN_SAFE_INTEGER,Number.isSafeInteger()
5.数组的扩展
(1)Array.from()
:用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括数据结构Set和Map)。
代码示例:
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
Array.from()
还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。
Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]
(2)Array.of()
:用于将一组值,转换为数组。
Array.of(3) // [3]
6.函数的扩展
(1)函数参数的默认值:
ES6允许为函数的参数设置默认值,即直接写在参数定义的后面。
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误:
function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}
foo()
//Uncaught Error: Missing parameter
参数mustBeProvided的默认值等于throwIfMissing函数的运行结果(即函数名之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行(即如果参数已经赋值,默认值中的函数就不会运行)。
如果传入undefined,将触发该参数等于默认值,null则没有这个效果:
function foo(x=5, y=6){
console.log(x,y);
}
foo(undefined, null)
// 5 null
指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真:
(function(a){}).length // 1
(function(a=5){}).length // 0
(function(a, b, c=5){}).length // 2
注:定义了默认值的参数,必须是函数的尾部参数,其后不能再有其他无默认值的参数。这是因为有了默认值以后,该参数可以省略,只有位于尾部,才可能判断出到底省略了哪些参数。
(2)rest参数:
ES6 引入 rest 参数(形式为“...变量名”),用于获取函数的多余参数,这样就不需要使用arguments
对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
function add(...values) {
console.log(values);
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3)
// [2,5,3]
// 10
注:rest参数之后不能再有其他参数,否则会报错。
函数的length属性,不包括rest参数:
(function(a) {}).length // 1
(function(...a) {}).length // 0
(function(a, ...b) {}).length // 1
(3)扩展运算符:
扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5
// ES6的写法
Math.max(...[14, 3, 77])
// 等同于
Math.max(14, 3, 77);
// ES6的合并数组
[...arr1, ...arr2, ...arr3]
注:扩展运算符内部调用的是数据结构的Iterator接口,因此只要具有Iterator接口的对象,都可以使用扩展运算符。
(4)箭头函数:
function(x, y) {
x++;
y--;
return x + y;
}
(x, y) => {x++; y--; return x+y}
应用示例:
class Animal {
constructor(){
this.type = 'animal'
}
says(say){
setTimeout( () => {
console.log(this.type + ' says ' + say)
}, 1000)
}
}
var animal = new Animal()
animal.says('hi') //animal says hi
并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,它的this是继承外面的,因此内部的this就是外层代码块的this。
箭头函数有几个使用注意点:
函数体内的this对象,绑定定义时所在的对象,而不是使用时所在的对象。
不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
不可以使用arguments对象,该对象在函数体内不存在。
由于this在箭头函数中被绑定,所以不能用call()、apply()、bind()
这些方法去改变this的指向。
7.对象的扩展
(1)属性的简洁表示法:
function f(x, y) {
return {x, y};
}
// 等同于
function f(x, y) {
return {x: x, y: y};
}
f(1, 2) // Object {x: 1, y: 2}
方法简写:
var o = {
method() {
return "Hello!";
}
};
// 等同于
var o = {
method: function() {
return "Hello!";
}
};
(2)属性名表达式:
ES6 允许字面量定义对象时,用表达式作为对象的属性名,即把表达式放在方括号内。
var lastWord = 'last word';
var a = {
'first word': 'hello',
[lastWord]: 'world'
};
a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"
(3)Object.is()
:用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false
Object.is(NaN, NaN)
// true
(4)Object.assign()
:用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
var target = { a: 1, b: 1 };
var source1 = { b: 2, c: 2 };
var source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
为对象添加属性:
class Point {
constructor(x, y) {
Object.assign(this, {x, y});
}
}
为属性指定默认值:
const DEFAULTS = {
logLevel: 0,
outputFormat: 'html'
};
function processContent(options) {
let options = Object.assign({}, DEFAULTS, options);
}
8.Symbol()
(1)Symbol()
函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。
var s1 = Symbol('foo');
var s2 = Symbol('foo');
typeof s1 // "symbol"
s1 === s2 // false
(2)作为属性名的Symbol:
由于每一个Symbol值都是不相等的,这意味着Symbol值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。(注:Symbol值作为对象属性名时,不能用点运算符)
var mySymbol = Symbol();
var a = {
[mySymbol]: 'Hello!'
};
(3)Symbol.for(),Symbol.keyFor()
:Symbol.for()
方法接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值,否则就新建并返回一个以该字符串为名称的Symbol值。
var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');
s1 === s2 // true
Symbol.keyFor()
方法返回一个已登记的 Symbol 类型值的key。
Symbol.keyFor(s1) // "foo"
var s3 = Symbol("foo");
Symbol.keyFor(s3) // undefined
9.Set和Map数据结构
(1)Set(类似于数组,但是成员的值都是唯一的,没有重复的值):
var set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]
let set = new Set(['red', 'green', 'blue']);
var array = Array.from(set);
console.log(array); // ["red", "green", "blue"]
Set数据结构有以下方法:
add(value):添加某个值,返回Set结构本身。
delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
has(value):返回一个布尔值,表示该值是否为Set的成员。
clear():清除所有成员,没有返回值。
(2)set遍历操作:
let set = new Set(['red', 'green', 'blue']);
keys()
、values()
、entries()
:
for (let item of set.keys()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.values()) {
console.log(item);
}
// red
// green
// blue
//等同于
for (let item of set) {
console.log(item);
}
Set.prototype[Symbol.iterator] === Set.prototype.values // true
for (let item of set.entries()) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]
forEach()
:
let set = new Set([1, 2, 3]);
set.forEach((value, key) => console.log(value * 2) )
// 2
// 4
// 6
数组的map()
和filter()
也可以用于Set结构了:
let set = new Set([1, 2, 3]);
set = new Set([...set].map(x => x * 2));
// Set {2, 4, 6}
let set = new Set([1, 2, 3, 4, 5]);
set = new Set([...set].filter(x => (x % 2) == 0));
// Set {2, 4}
使用Set()
,可以很容易地实现并集(Union)和交集(Intersect):
let a = new Set([1,2,3]);
let b = new Set([4,3,2]);
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}
let intersect = new Set([...a].filter(x => b.has(x)));
// Set {2, 3}
(3)Map(类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键):
var map = new Map([
['name', '张三'],
['title', 'Author']
]);
注:只有对同一个对象的引用,Map结构才将其视为同一个键:
var map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined
上面代码的set和get方法,表面是针对同一个键,但实际上这是两个值,内存地址是不一样的,因此get方法无法读取该键,返回undefined。由上可知,Map的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。
Map数据结构有以下属性和方法:
size:返回成员总数。
set(key, value):设置key所对应的键值,然后返回整个Map结构,
如果key已经有值,则键值会被更新,否则就新生成该键。
get(key):读取key对应的键值,如果找不到key,返回undefined。
has(key):返回一个布尔值,表示某个键是否在Map数据结构中。
delete(key):删除某个键,返回true。如果删除失败,返回false。
clear():清除所有成员,没有返回值。
(4)map遍历操作:keys()
、values()
、entries()
:
let map = new Map([
['F', 'no'],
['T', 'yes'],
]);
for (let key of map.keys()) {
console.log(key);
}
// "F"
// "T"
for (let value of map.values()) {
console.log(value);
}
// "no"
// "yes"
for (let item of map.entries()) {
console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"
// 等同于
for (let [key, value] of map.entries()) {
console.log(key, value);
// 等同于
for (let [key, value] of map) {
console.log(key, value);
}
Map.prototype[Symbol.iterator] === Map.prototype.entries // true
forEach()
:
let map = new Map([
['F', 'no'],
['T', 'yes'],
]);
map.forEach(function(value, key, map) {
console.log("Key: %s, Value: %s", key, value);
});
// Key: F, Value: no
// Key: T, Value: yes
Map结构转为数组结构,比较快速的方法是结合使用扩展运算符(...)。
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
[...map.keys()]
// [1, 2, 3]
[...map.values()]
// ['one', 'two', 'three']
[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]
[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]
10.Iterator和for...of循环
任何部署了Iterator接口的对象,都可以用for...of循环遍历。
在ES6中,有三类数据结构原生具备Iterator接口:数组、某些类似数组的对象、Set和Map结构。
let arr = [4,5,6,0];
let iter = arr[Symbol.iterator]();
iter.next(); // Object {value: 4, done: false}
iter.next(); // Object {value: 5, done: false}
iter.next(); // Object {value: 6, done: false}
iter.next(); // Object {value: 0, done: false}
iter.next(); // Object {value: undefined, done: true}
对于这三类数据结构,不用自己写遍历器生成函数,for...of循环会自动遍历它们。
for(let i of [4,5,6,0]){
console.log(i);
} // 4 5 6 0
除此之外,其他数据结构(主要是对象)的Iterator接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for...of循环遍历。
下面是为对象添加Iterator接口的例子:
let obj = {
data: [ 'hello', 'world' ],
[Symbol.iterator]() {
const self = this;
let index = 0;
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++],
done: false
};
} else {
return { value: undefined, done: true };
}
}
};
}
};
11.Generator()
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
for (let v of hw) {
console.log(v);
}
// hello
// world
yield*语句:如果yield命令后面跟的是一个遍历器,需要在yield命令后面加上星号,表明它返回的是一个遍历器。
let delegatedIterator = (function* () {
yield 'Hello!';
yield 'Bye!';
}());
let delegatingIterator = (function* () {
yield 'Greetings!';
yield* delegatedIterator;
yield 'Ok, bye.';
}());
for(let value of delegatingIterator) {
console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."
12.class
class Animal {
constructor(){
this.type = 'animal'
}
says(say){
console.log(this.type + ' says ' + say)
}
}
let animal = new Animal()
animal.says('hello') //animal says hello
class Cat extends Animal {
constructor(){
super()
this.type = 'cat'
}
}
let cat = new Cat()
cat.says('hello') //cat says hello
子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '('+this.x+', '+this.y+')';
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y);
this.color = color;
}
toString() {
return this.color+' '+super.toString();
}
}
var colorPoint = new ColorPoint(3,6,'purple');
colorPoint.toString()
"purple (3, 6)"
13.Module(import
和export
命令只能在模块的顶层,不能在代码块之中)
(1)export
命令:
export命令后面,使用大括号指定所要输出的一组变量。
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};
下面代码使用as关键字,重命名了函数v1和v2的对外接口。重命名后,v2可以用不同的名字输出两次。
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
同样的,function和class的输出,也必须遵守这样的写法。
// 报错
function f() {}
export f;
// 正确
export function f() {};
// 正确
function f() {}
export {f};
(2)import
命令:
import {firstName, lastName, year} from './profile';
import命令要使用as关键字,将输入的变量重命名。
import { lastName as surname } from './profile';
export default class
命令用于指定模块的默认输出。
export default class Person{
constructor(name, age){
this.name = name;
this.age = age;
}
say(){
return `我是${this.name},今年${this.age}岁了。`;
}
}
import命令后面才不用加大括号,因为只可能对应一个方法。
import Person from './second.js';
export class
:
export class Fun {
constructor(color){
this.color = color;
}
sayHello(){
console.log(`Hello,${this.color}`);
}
}
import { Fun } from './second.js';
正是因为export default命令其实只是输出一个叫做default的变量,所以它后面不能跟变量声明语句。
// 正确
export var a = 1;
// 正确
var a = 1;
export default a;
// 错误
export default var a = 1;
// 正确
export default 42;
// 报错
export 42;
示例:
var hobby = 'tour';
export default hobby;
import hobby from './second.js';
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。