一 ECMAScript和JavaScript的关系
我们常常听到ECMAScript和JavaScript这两者,很多情况我们都分不清楚它们之间有什么区别,甚至很多时候我们常常认为它们是等价的。事实上,ECMAScript也是一门脚本语言,更多的来说是一种语言规范,而JavaScrip可以看做是ECMAScript的扩展语言,ECMAScript只是提供了最基本的语法,而JavaScrip实现了ECMAScript的语言标准,并做了扩展。因此,ECMAScript和JavaScript的关系是,前者是后者的规格,后者是前者的一种实现(另外的ECMAScript方言还有Jscript和ActionScript)。这里有张图形象的展示了ECMAScript和JavaScript的关系:JavaScript = ECMAScript + BOM +DOM。
二 ECMAScript的历史
可以看出ES6从开始制定到最后发布,整整用了15年。而我们常说的ES6最准确的说法应该是ECMAScript 2015(ES2015),只不过从ES2015后不在以年份来命名,而是以版本号来命名了,所以我们也常把ES2015叫做ES6。那为什么ES6这个版本显得如此重要呢?可以说ES6是ECMAScript最重要的一次更新了,我们来看看它做了哪些事:
- 对原有语法进⾏增强。
- 解决原有语法上的⼀些问题或者缺陷。
- 全新的对象、全新的⽅法、全新的功能。
- 全新的数据类型和数据结构。
三 ES2015(ES6) 的新特性
1. let命令
let用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。我们来看看它的用法,并和var比较一下:
块级作用域
ES6之前只有全局作用域和函数作用域,但let实际上为JavaScript新增了块级作用域,消除了变量覆盖问题。
function f1() {
var n = 5;
if (true) {
var n = 10;
}
console.log(n); // 10
}
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
不存在变量提升
let不像var那样会发生“变量提升”现象。主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。所以,变量一定要在声明后使用,否则报错。
console.log(foo); // 输出undefined
console.log(bar); // 报错ReferenceError
var foo = 2;
let bar = 2;
暂时性死区
只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。 let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”。暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
for (var i = 0; i < 3; i++) {
for (var i = 0; i < 3; i++) {
}
console.log( i) //3
}
for (let i = 0; i < 3; i++) {
for (let i = 0; i < 3; i++) {
}
console.log(i) //0,1,2
}
不允许重复声明
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; // 不报错
}
}
顶层对象的属性
顶层对象,在浏览器环境指的是window对象,在Node指的是global对象。ES5之中,顶层对象的属性与全局变量是等价的。
var a = 1;
console.log(window.a) //1
let a = 1;
console.log(window.a) //undefined
顶层对象的属性与全局变量挂钩,被认为是JavaScript语言最大的设计败笔之一。这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。另一方面,window对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。
ES6为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从ES6开始,全局变量将逐步与顶层对象的属性脱钩。
2. const命令
const和let实际上是差不多的,let的特性const也是具备的,只是const声明的是只读的常量。一旦声明,常量的值就不能改变。所以我们只会讨论const与let不一致的地方
const a =1;
a=2; // TypeError: Assignment to constant variable.
事实上const声明的数据更具体的含义指的是数据的地址不可变,对于基本类型来说也就是值不可变,但对于复杂类型来说,指的就是引用地址不能变,只有引用地址不变,是可以被修改的,例如:
const foo = {};
foo.prop = 123; //为foo添加一个成员属性,引用地址没有发生改变,可以操作
foo = {}; // TypeError: "foo" is read-only(引用地址发生改变,所以这个是不被允许的)
3. 变量的解构赋值
数组的解构赋值
只要某种数据结构具有Iterator接口,都可以采用数组形式的解构赋值。
var [a, b, c] = [1, 2, 3];
let [foo, [[bar], baz]] = [1, [[2], 3]];
let [ , , third] = ["foo", "bar", "baz"]; // third = "baz"
let [head, ...tail] = [1, 2, 3, 4]; //head =1 tail = [2, 3, 4]
let [x, y, ...z] = ['a']; //a='a' y = undefined z=[]
var [foo = true] = []; // foo = true
//ES6内部使用严格相等运算符(===),判断一个位置是否有值。所以,如果一个数组成员不严格等于undefined,默认值是不会生效的。
var [x = 1] = [undefined]; //x =1
var [x = 1] = [null];// x = null
let [x = 1, y = x] = [2]; // x=2; y=2
let [x = y, y = 1] = []; // ReferenceError
对象的解构赋值
var { foo, bar } = { foo: "aaa", bar: "bbb" };
let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj; //变量重命名
var {x = 3} = {};//x=3
var {x, y = 5} = {x: 1};//x=1 y=5
var {x:y = 3} = {};//y=3
var {x:y = 3} = {x: 5};//y=5
字符串的解构赋值
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
let {length : len} = 'hello';
len // 5
4.字符串的扩展
字符串的遍历器接口
ES6为字符串添加了遍历器接口(Iterator),使得字符串可以被for...of循环遍历。
for (let codePoint of 'foo') {
console.log(codePoint)
}
at()
给定字符串位置,返回正确的字符。
'abc'.at(0) // "a"
'𠮷'.at(0) // "𠮷"
startsWith(), endsWith()
- startsWith():返回布尔值,表示参数字符串是否在源字符串的头部。
- endsWith():返回布尔值,表示参数字符串是否在源字符串的尾部。
var s = 'Hello world!';
s.startsWith('Hello') // true
s.endsWith('!') // true
repeat()
repeat方法返回一个新字符串,表示将原字符串重复n次。
'x'.repeat(3) // "xxx"
'na'.repeat(0) // ""
'na'.repeat(2.9) // "nana" 参数如果是小数,会被取整。
'na'.repeat(Infinity)// RangeError 参数是负数或者Infinity,会报错
'na'.repeat(-1)// RangeError 参数是负数或者Infinity,会报错
'na'.repeat(-0.9) // "" 但是如果参数是0到-1之间的小数,则等同于0
'na'.repeat(NaN) // "" 参数NaN等同于0
'na'.repeat('na') // "" 参数是字符串,则会先转换成数字。
'na'.repeat('3') // "nanana" 参数是字符串,则会先转换成数字。
模板字符串
模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。
const name = 'tom'
// 可以通过 ${} 插入表达式,表达式的执行结果将会输出到对应位置
const msg = `hey, ${name} --- ${1 + 2} ---- ${Math.random()}`
console.log(msg)
const name = 'Tom'
const age = 18
function fn(strArr,name,age){
console.log(strArr,name,age) //[ '你好,我叫', ' , 今年', '。' ] Tom 18
return strArr[0] +name+ strArr[1] + age + strArr[2]
}
let r = fn`你好,我叫${name} , 今年${age}。`
console.log(r) //你好,我叫Tom , 今年18.
5. 数组的扩展
Array.from()
用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括ES6新增的数据结构Set和Map)。
所谓类似数组的对象,本质特征只有一点,即必须有length属性。因此,任何有length属性的对象,都可以通过Array.from方法转为数组,而此时扩展运算符就无法转换。
Array.from('hello') // ['h', 'e', 'l', 'l', 'o']
Array.from([1, 2, 3], (x) => x * x) // [1, 4, 9]
Array.from({ length: 3 });// [ undefined, undefined, undefined ]
Array.of()
用于将一组值,转换为数组。
Array.of(3, 11, 8) // [3,11,8]
Array.of() // []
Array.of(undefined) // [undefined]
//模拟实现
function ArrayOf(){
return [].slice.call(arguments);
}
数组实例的find()和findIndex()
数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。
数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。
find和findIndex方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2
[NaN].indexOf(NaN)
// -1
[NaN].findIndex(y => Object.is(NaN, y))
// 0
数组实例的fill()
fill方法使用给定值,填充一个数组。
['a', 'b', 'c'].fill(7)// [7, 7, 7]
new Array(3).fill(7)// [7, 7, 7]
// fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。
['a', 'b', 'c'].fill(7, 1, 2)// ['a', 7, 'c']
数组实例的keys()
keys()用于遍历数组。返回一个遍历器对象,可以用for...of循环进行遍历,keys()是对键名的遍历
for (let index of ['a', 'b'].keys()) {
console.log(index);
}
6. 对象的扩展
属性的简洁表示法
let name = 'Tom'
let fn = function () {
console.log("fn执行")
}
let obj ={
name,
fn
}
console.log(obj.name)
obj.fn()
Object.is()
用来比较两个值是否严格相等。
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
Object.assign()
用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
var target = { a: 1 };
var source1 = { b: 2 };
var source2 = { c: 3 };
let obj = Object.assign(target, source1, source2);//obj===target {a:1, b:2, c:3}
Object.assign(undefined) // 报错
Object.assign(null) // 报错
let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true
Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' }) // { a: 'b', Symbol(c): 'd' }
//Object.assign方法实行的是浅拷贝,而不是深拷贝。
//Object.assign拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。
let obj1 = {a:1,b:{c:2}}
let obj2 = Object.assign({},obj1)
obj1.b.c =9
console.log(obj2.b.c) //9
//Object.assign把数组视为属性名为0、1、2的对象,因此目标数组的0号属性4覆盖了原数组的0号属性1。
Object.assign([1, 2, 3], [4, 5]) // [4, 5, 3]
属性的可枚举性
对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptors(ES8)方法可以获取该属性的描述对象。
let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
// value: 123,
// writable: true,
// enumerable: true,
// configurable: true
// }
Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable
// false
Object.getOwnPropertyDescriptor([], 'length').enumerable
// false
//toString和length属性的enumerable都是false,因此for...in不会遍历到这两个继承自原型的属性。
//所有Class的原型的方法都是不可枚举的。
Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable
// false
属性的遍历
-
for...in
for...in循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)。 -
Object.keys(obj)
Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)。 -
Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)。 -
Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有Symbol属性。 -
Reflect.ownKeys(obj)
Reflect.ownKeys返回一个数组,包含对象自身的所有属性,不管是属性名是Symbol或字符串,也不管是否可枚举。
const obj = {};
Object.defineProperties(obj, {
property1: {enumerable: true, value: 1},
property2: {enumerable: false, value: 2},
[Symbol("property3")]: {enumerable: true, value: 3},
[Symbol("property4")]: {enumerable: false, value: 4},
10:{enumerable: true, value: 5},
11:{enumerable: false, value: 6},
"a":{enumerable: false, value: 7},
"b":{enumerable: true, value: 8},
});
for(let key in obj){
console.log(key) //10 property1 b
}
console.log(Object.keys(obj)); //["10", "property1", "b"]
console.log(Object.getOwnPropertyNames(obj));//["10", "11", "property1", "property2", "a", "b"]
console.log(Object.getOwnPropertySymbols(obj));//[Symbol(property3), Symbol(property4)]
console.log(Reflect.ownKeys(obj));//["10", "11", "property1", "property2", "a", "b", Symbol(property3), Symbol(property4)]
7. Symbol
ES6引入了一种新的原始数据类型Symbol,表示独一无二的值。它是JavaScript语言的第七种数据类型,前六种是:Undefined、Null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。
typeof Symbol() //"symbol"
Symbol() == Symbol() //false
const name = Symbol()
let obj = {
[name]: "Tom"
}
obj[name] // 'Tom'
obj.name //undefined
//Symbol值不能与其他类型的值进行运算,会报错。
var sym = Symbol('My symbol');
"your symbol is " + sym // TypeError: can't convert symbol to string
//Symbol值也可以转为布尔值,但是不能转为数值。
var sym = Symbol();
Boolean(sym) // true
Number(sym) // TypeError
//有时,我们希望重新使用同一个Symbol值,Symbol.for方法可以做到这一点
Symbol.for("bar") === Symbol.for("bar")//true
//Symbol.keyFor方法返回一个已登记的 Symbol 类型值的key。
var s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
8. Set和Map数据结构
Set
ES6提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。Set本身是一个构造函数,用来生成Set数据结构。Set函数可以接受一个数组(或类似数组的对象)作为参数,用来初始化。
向Set加入值的时候,不会发生类型转换,所以5和"5"是两个不同的值。
- add(value):添加某个值,返回Set结构本身。
- delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
- has(value):返回一个布尔值,表示该值是否为Set的成员。
- clear():清除所有成员,没有返回值。
//数组去重
Array.from(new Set([1,2,1,2,3])) //[1,2,3]
let s = new Set();
s.add(1).add(2).add(2); //Set(2) {1, 2}
// 注意2被加入了两次
s.size // 2
s.has(1) // true
s.has(2) // true
s.has(3) // false
s.delete(2);
s.has(2) // false
WeakSet
WeakSet结构与Set类似,也是不重复的值的集合。但是,它与Set有两个区别。
- WeakSet的成员只能是对象,而不能是其他类型的值。
- WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于WeakSet之中。这个特点意味着,无法引用WeakSet的成员,因此WeakSet是不可遍历的。
WeakSet可以接受一个数组或类似数组的对象作为参数。
var ws = new WeakSet();
ws.add(1); // TypeError: Invalid value used in weak set
ws.add(Symbol()); // TypeError: Invalid value used in weak set
var a = [[1,2], [3,4]];
ws.add(a); //WeakSet {Array(2), Array(2)}
var b = [3, 4];
ws.add(b); //WeakSet {Array(2)}
var obj = {};
var foo = {};
ws.add(window);
ws.add(obj);
ws.has(window); // true
ws.has(foo); // false
ws.delete(window);
ws.has(window); // false
Map
ES6提供了Map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
const obj = {}
obj[{ a: 1 }] = 'value'
console.log(Object.keys(obj)) //["[object Object]"]
var m = new Map();
var o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
//Map也可以接受一个数组作为参数
var map = new Map([
['name', '张三'],
['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"
//如果对同一个键多次赋值,后面的值将覆盖前面的值。
let map = new Map();
map
.set(1, 'aaa')
.set(1, 'bbb');
map.get(1) // "bbb"
new Map().get('asfddfsasadf') //undefined
//内存地址是不一样的
var map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined
let map = new Map();
map.set(NaN, 123);
map.get(NaN) // 123
map.set(-0, 123);
map.get(+0) // 123
WeakMap
WeakMap结构与Map结构基本类似,唯一的区别是它只接受对象作为键名(null除外),不接受其他类型的值作为键名,而且键名所指向的对象,不计入垃圾回收机制。WeakMap只有四个方法可用:get()、set()、has()、delete()。
9. Proxy
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
let obj ={b:2}
let proxy = new Proxy(obj,{
get(target, property,receiver){
//target是实例化Proxy时使用的对象,property是这次读取操作中想要获取的属性名,receiver则是这个实例化的Proxy自身,即proxy。
console.log(target, property,receiver,"get") //{b: 2, a: 1} "a" Proxy {b: 2, a: 1} "get"
// return target[property] //返回值作为访问的值
//Reflect内部封装了一系列对对象的操作,所以我们在实现自己逻辑后,可以调用Reflect
return Reflect.get(target, property,receiver)
},
set(target, property, value,receiver){
console.log(target, property, value,receiver,"set") //{b: 2} "a" 1 Proxy {b: 2} "set"
// target[property] = value
// return true
return Reflect.set(target, property, value,receiver)
},
deleteProperty(target,property){
console.log(target, property,"deleteProperty") //{b: 2} "a" "deleteProperty"
// delete target[property]
return Reflect.deleteProperty(target,property)
}
})
proxy.a =1
console.log(proxy.a) //1
delete proxy.a
console.log(proxy.a) //undefined
- Iterator和for...of循环 Iterator的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费。总之,能够提供Iterator接口的数据都能被for...of遍历。
在我们的数组(Array),对象(Object),Map和Set四种数据集合中,只有对象没有部署Iterator接口,所以不能被for...of遍历。
const obj = { foo: 123, bar: 456 }
for (const item of obj) { //报错 obj is not iterable
console.log(item)
}
//接下来我们为obj部署Iterator,使它能被遍历
obj[Symbol.iterator ] = function(){
let index = 0
const key = Object.keys(this);
return{
next:function(){
return index<key.length?{value:obj[key[index++]],done:false}:{value:undefined,done:true}
}
}
}
//在打印一下
for (const item of obj) { //123 456
console.log(item)
}
四 ES2016(ES7) 的新特性
1.求幂运算符(**)
Math.pow(3, 2) === 3 ** 2 // 9
2.数组实列的includes()
- includes():返回布尔值,表示是否找到了参数字符串。
[1, 2, 3].indexOf(3) > -1 // true
等同于:
[1, 2, 3].includes(3) // true
[1, 2, NaN].includes(NaN) // true
[1, 2, NaN].indexOf(NaN) // -1
五 ES2017(ES8) 的新特性
1. 数组实例的entries()和values()
entries()和values()——用于遍历数组。它们都返回一个遍历器对象,可以用for...of循环进行遍历,唯一的区别是values()是对键值的遍历,entries()是对键值对的遍历。
for (let elem of ['a', 'b'].values()) {
console.log(elem);
}
// 'a'
// 'b'
for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"
// 1 "b"
2.Object.getOwnPropertyDescriptors
Object.getOwnPropertyDescriptors方法可以获取该属性的描述对象。
const obj = {
foo: 123,
get bar() { return 'abc' }
};
Object.getOwnPropertyDescriptors(obj)
// { foo:
// { value: 123,
// writable: true,
// enumerable: true,
// configurable: true },
// bar:
// { get: [Function: bar],
// set: undefined,
// enumerable: true,
// configurable: true } }
3.padStart(),padEnd()
ES8推出了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart用于头部补全,padEnd用于尾部补全。
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'
4.async、await异步解决方案
console.log('A')
setTimeout(()=>console.log('B'),1000)
const start = new Date()
while(new Date- start<3000){}
console.log("C")
setTimeout(()=>console.log('D'),0)
new Promise((resolve,reject)=>{
console.log('E')
foo.bar(100)
})
.then(()=>console.log("F"))
.then(()=>console.log("G"))
.catch(()=>console.log("H"))
console.log('I')
//ACEIHBD
六 附录