一、ECMAScript和JavaScript
实际上JavaScript是ECMAScript的扩展语言,ECMAScript只是提供了最基本的语法(约定了代码该如何编写,例如怎样定义变量和函数,如何实现循环语句等)。JavaScript实现了ECMAScript标准,并且在这个基础上进行了扩展,使得我们可以在浏览器中操作DOM和BOM,在Node环境中进行读写文件等操作。
在浏览器环境中,JavaScript = ECMAScript + Web APIs【BOM/DOM】
在Node环境中,JavaScript = ECMAScript + Node APIs【fs/net/etc.】
二、ECMAScript 的发展过程
三、ECMAScript 2015 的新特性
对原有语法进⾏增强,更便捷,更易用(例如解构,展开、参数默认值、模板字符串等等)
解决原有语法上的⼀些问题或者缺陷(例如let、const块级作用域)
全新的对象、全新的⽅法、全新的功能(例如promise、proxy、Object.assign方法等等)
全新的数据类型和数据结构(例如symbol、map、set等等)
1、let和const
1、let的使用和注意事项
<1>let 的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。
{
let a = 10;
var b = 1;
}
console.log(a) // ReferenceError: a is not defined.
console.log(b) // 1
<2>var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined; let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
<3>在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
<4>let不允许在相同作用域内,重复声明同一个变量。
2、const 的使用和注意事项
<1>const声明一个只读的常量。一旦声明,常量的值就不能改变,声明时必须立即初始化,不能留到以后赋值。
ps:这里的不能修改是指存储的内存地址,修改常量中的属性成员是可以的。
const foo;
// SyntaxError: Missing initializer in const declaration
<2>作用域与let命令相同:只在声明所在的块级作用域内有效。
<3>const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。
2、箭头函数
箭头函数 Arrow functions
箭头函数与 this(箭头函数中没有this的机制, 所以箭头函数不会改变this的指向)
const person = {
name: 'zhangsan',
// sayHi: function() {
// console.log(`hi, my name is ${this.name}`)
// }
sayHi: () => { console.log(`hi, my name is ${this.name}`) },
sayHiAsync: function() {
// const _this = this
// setTimeout(() => {
// console.log(_this.name)
// }, 1000)
// 在工作中用到_this = this 的情况,都可以用箭头函数去避免
setTimeout(() => {
console.log(this.name)
}, 1000)
}
}
// person.sayHi() // hi, my name is zhangsan
person.sayHi() // hi, my name is undefined
person.sayHiAsync() // zhangsan
3、Class(类)
// 创建类
class Person {
// 构造函数
constructor(name) {
this.name = name
}
// 实例方法
say() {
console.log(`my name is ${this.name}`)
}
}
const p = new Person('tom')
p.say() // my name is tom
静态成员 static
// 创建类
class Person {
// 构造函数
constructor(name) {
this.name = name
}
// 实例方法
say() {
console.log(`my name is ${this.name}`)
}
// 静态方法 挂载到类上面的,所以this总是指向类
static create(name) {
return new Person(name)
}
}
// 静态方法是使用类直接调用的
const p = Person.create('jack')
p.say() // my name is jack
类的继承 extends
class Person {
constructor(name) {
this.name = name
}
say() {
console.log(`my name is ${this.name}`)
}
}
// 使用extends 实现继承
class Student extends Person {
constructor(name, number) {
// super始终指向父类,调用super就是调用父类的构造函数
super(name)
this.number = number
}
hello () {
// super始终指向父类,调用super实例方法就是调用父类的实例方法
super.say(name)
console.log(`my school number is ${this.number}`)
}
}
const p = new Student('jack', '11')
p.hello()
4、Promise
Promise ⼀种更优的异步编程解决⽅案,解决了传统异步编程中回调函数嵌套过深的问题。详细介绍参见
5、Generators
避免异步编程中回调嵌套过深,提供更好的异步编程方案
⽣成器 Generator
// 在普通函数的基础上加上 * 号就是生成器函数
function * foo() {
console.log(111111)
// yield 暂停执行 和 return 作用类似,将结果返回出去
yield 1000
console.log(2222222)
yield 2000
}
const fn = new foo()
console.log(fn.next())
// 111111
// {value: 1000, done: false}
console.log(fn.next())
// 2222222
// {value: 2000, done: false}
console.log(fn.next())
// {value: undefined, done: true}
⽣成器应⽤ Generator
// 1、实现发号器
function * createMaker() {
let id = 1
while(true) {
yield id++
}
}
const addID = createMaker()
console.log(addID.next().value)
console.log(addID.next().value)
// 2、实现对象的 iterator 方法
const obj = {
life: ['吃饭', '睡觉', '打豆豆'],
work: ['html', 'css', 'javascript'],
learn: ['语文', '数学', '外语'],
[Symbol.iterator]: function * () {
const all = [...this.life, ...this.work, ...this.learn]
for (const item of all) {
yield item
}
}
}
for (const item of obj) {
console.log(item)
}
6、Modules
ES Modules 语⾔层⾯的模块化标准,将在模块化开发中介绍
7、模板字面量
模板字符串字⾯量 Template literals(支持换行,支持插值表达式)
const name = 'zhangsan'
const msg = `hello, ${name}! --- ${ 1+ 2 } === ${Math.random()}`
console.log(msg) // hello, zhangsan! --- 3 === 0.24367109177674284
模板字符串标签函数 Tagged templates(使用标签函数,必须先定义标签函数,实际上就是使用标签函数对模板字符串进行加工)
// 标签函数中接收到的参数是数组
// const str = console.log`hello world` // [ 'hello world' ]
const name = 'zhangsan'
const age = 18
const sex = 1
// 标签函数接收到的第一个参数是所有静态内容组成的数组,后面的参数为插值表达式中的值
function tagFun(str, name, age, sex) {
// console.log(str, name, age, sex)
// 对模板字符串进行操作并返回操作后的结果
return str[0] + name + str[1] + age + str[2] + sex
}
const r = tagFun`hello, ${name}, year is ${age} sex is ${sex}`
// [ 'hello, ', ', year is ', ' sex is ', '' ] zhangsan 18 1
console.log(r) // hello, zhangsan, year is 18 sex is 1
字符串的扩展⽅法(includes/startsWith/endsWith)
const str = "Error: foo is not defined."
console.log(str.startsWith('Error')) // true
console.log(str.endsWith('.')) // true
console.log(str.includes('is')) // true
8、默认参数
参数默认值 Default parameters
如果有多个参数,带有默认值的参数要放到最后
// function fun (enable) {
// enable = enable === undefined ? true : enable
// console.log(enable)
// }
// fun() // true
// fun(1) // 1
// 上面等价于下面
// function fun (enable = true) {
// console.log(enable)
// }
// fun() // true
// fun(1) // 1
// 如果有多个参数,带有默认值的参数要放到最后
function fun (a, enable = true) {
console.log('a', a, 'enable', enable)
}
fun(1, false) // a 1 enable false
9、对象字⾯量增强
对象字⾯量增强 Enhanced object literals
const name = "zhangsan"
const obj = {
// name: name,
name, // 如果属性名和变量名一致,可以简写
age: 18,
// methods: function() {
// console.log(1111)
// }
methods() { // : function 可以简写为一个()
console.log(1111)
},
// Math.random(): 123,
// 计算属性名
[Math.random()]: 123
}
console.log(obj)
对象扩展⽅法 Object.assign
const target = {
a: 1,
b: 2
}
const obj1 = {
c: 3,
d: 4
}
const obj2 ={
a: 11,
e: 5
}
// Object.assign 接收多个对象,第一个对象是目标对象,后面对象的属性值都会存入到或者覆盖到目标对象中
const r = Object.assign(target, obj1, obj2)
console.log(target) //{ a: 11, b: 2, c: 3, d: 4, e: 5 }
console.log(target === r) // true
// 使用Object.assign复制对象,修改对象内的成员不会影响原始数据
const newObj = Object.assign({}, r)
newObj.a = 123
console.log(r) // { a: 11, b: 2, c: 3, d: 4, e: 5 }
console.log(newObj) // { a: 123, b: 2, c: 3, d: 4, e: 5 }
对象扩展⽅法 Object.is(用于判断两个值是否相等)
console.log(
// 0 == false // true
// 0 === false // false
// +0 === -0 // true
// NaN == NaN // false
// Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
)
// 建议还是使用三等去判断
10、解构分配
数组的解构 Destructuring(根据数组的位置进行提取)
const arr = [1, 2, 3, 4]
// 从数组中取出数据给变量赋值
const [a, b, c, d] = arr
// 提取某个位置的数据给变量赋值
const [, , e] = arr
// 剩余参数的提取
const [f, ...g] = arr
// 如果数组中没有值返回undefined,还可以给定默认值
const [, , , , h, i = 'default' ] = arr
console.log(a, b, c, d) // 1 2 3 4
console.log(e) // 3
console.log(f) // 1
console.log(g) // [2, 3, 4]
console.log(h) // undefined
console.log(i) // default
对象的解构 Destructuring(根据对象的属性名进行提取)
对象解构和数组一样,没有匹配到的成员发挥undefined,也可以给定默认值
const obj = { name: 'zhangsan', age: 18 }
// 使用对象的属性名进行提取
const { name } = obj
console.log(name) // zhangsan
// 使用重命名方式解决命名冲突
const { name: new_name } = obj
console.log(new_name) // zhangsan
// 可以在重命名后继续给定默认值
const { name: new_name2 = 'lisi' } = obj
console.log(new_name2) // zhangsan
11、展开操作符
剩余参数 Rest parameters
function fn () {
// arguments 中 是伪数组
console.log(arguments)
}
fn(1, 2, 3) // [Arguments] { '0': 1, '1': 2, '2': 3 }
function fn2 (a, ...arg) {
// 使用展开操作符 得到的arg 是数组 取代Arguments去接收无限参数
// 只可以存放在形参的位置,只可以放在最后,并且只可以使用1次
console.log(arg)
}
fn2(1, 2, 3) // [ 1, 2, 3 ]
展开数组 Spread
const arr = ['a', 'b', 'c']
// console.log(
// arr[0],
// arr[1],
// arr[2]
// )
// 对于参数不固定的时候,这种通过下标的方式便行不通了
// 之前可以使用apply的方式, apply第一个参数是this指向,后面的参数是需要传递的参数
console.log.apply(console, arr)
// 使用展开操作符更便捷
console.log(...arr)
12、for … of 循环
普通的for循环,适合遍历数组;for in 适合遍历键值对;还有有许多函数式的遍历方法,比如数组对象的forEach方法,这些方法都有一定的局限性。ES6引入了for of 循环,用于遍历所有数据结果的统一方式
可迭代接⼝ Iterable(实现iterable接口是for…of的前提)
// 所有被for...of循环遍历的数据都必须实现iterator接口
const arr = ['a', 'b', 'c']
const iterator = arr[Symbol.iterator]()
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
const arr2 = new Set([1, 2, 3])
const iterator2 = arr2[Symbol.iterator]()
console.log(iterator2.next())
console.log(iterator2.next())
console.log(iterator2.next())
console.log(iterator2.next())
实现可迭代接⼝ Iterable
// 对象内部必须挂载一个iterator方法
// 这个方法需要返回一个对象,这个对象内部需要有next方法
// next方法需要返回一个对象,这个对象有两个属性成员,一个是value,一个done
const obj = {
name: 'zhagnsan',
age: 20,
sex: '男',
[Symbol.iterator]: function () {
let index = 0
const arr = [this.name, this.age, this.sex]
return {
next () {
const result = {
value: arr[index],
done: index >= arr.length
}
index++
return result
}
}
}
}
for (const item of obj) {
console.log(item)
}
迭代器模式 Iterator
// 迭代器模式对外部提供统一遍历接口,让外部不用再关心数据内部的结构是怎样的
const obj = {
life: ['吃饭', '睡觉', '打豆豆'],
learn: ['html', 'css', 'javascript'],
work: ['搬砖', '喝茶', '上厕所'],
[Symbol.iterator]: function () {
let index = 0
const all = [...this.life, ...this.learn, ...this.work]
return {
next() {
const result = {
value: all[index],
done: index >= all.length
}
index++
return result
}
}
}
}
for (const item of obj) {
console.log(item)
}
13、Map 和 Set
Set 数据结构【set内部的成员是不允许重复的】
const arr = new Set()
// add方法会返回集合本身,所以可以链式调用
arr.add(1).add(2).add(3).add(4).add(2)
// console.log(arr)
// 可以使用循环方法进行遍历
// arr.forEach(i => console.log(i))
// for (let i of arr) console.log(i)
// size查询长度,has查询是否包含特定值,delete删除指定值,clear清空全部内容
console.log(arr.size)
console.log(arr.has(100))
console.log(arr.delete(1))
console.log(arr)
arr.clear()
console.log(arr)
// 最常用的是为数组去重
const array = [1, 2, 3, 2, 5, 3]
// const r = Array.from(new Set(array))
const r = [...new Set(array)]
console.log(r)
Map 数据结构【与对象类似,本质上都是键值对的集合】
const obj = []
obj[123] = 'value'
obj[true] = 'value'
obj[{a: 1}] = 'value'
console.log(Object.keys(obj))
// ["123", "true", "[object Object]"] 都被toString()转换为了字符串
// 如果用对象作为键存储数据,就会出现问题,所得到的所有结果都是一样的
// map就可以解决这个问题
const m = new Map()
const tom = { name: 'tom' }
const jack = { name: 'jack' }
m.set(tom, 90)
m.set(jack, 96)
console.log(m)
// 可以使用get方法获取指定值
console.log(m.get(jack))
// 和set一样,map实例也具有has方法,delete方法,clear方法
m.has(tom)
m.delete(jack)
m.clear()
console.log(m)
14、Proxy
Proxy 代理对象(门卫,专门为对象设置访问代理器)
ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。
var proxy = new Proxy(target, handler);
Proxy 对象的所有用法,都是上面这种形式,不同的只是handler
参数的写法。其中,new Proxy()
表示生成一个Proxy
实例,target
参数表示所要拦截的目标对象,handler
参数也是一个对象,用来定制拦截行为
<1>get()
get
方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。
var person = {
name: "张三"
};
var proxy = new Proxy(person, {
get: function(target, propKey) {
if (propKey in target) {
return target[propKey];
} else {
throw new ReferenceError("Prop name \"" + propKey + "\" does not exist.");
}
}
});
proxy.name // "张三"
proxy.age // 抛出一个错误
上面代码中,作为构造函数,Proxy
接受两个参数。第一个参数是所要代理的目标对象(上例是person对象),即如果没有Proxy
的介入,操作原来要访问的就是这个对象;第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作
<2>set()
set
方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。
假定Person
对象有一个age
属性,该属性应该是一个不大于 200 的整数,那么可以使用Proxy
保证age
的属性值符合要求。
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// 对于满足条件的 age 属性以及其他属性,直接保存
obj[prop] = value;
}
};
let person = new Proxy({}, validator);
person.age = 100;
person.age // 100
person.age = 'young' // 报错
person.age = 300 // 报错
<3>deleteProperty()
deleteProperty
方法用于拦截delete
操作,如果这个方法抛出错误或者返回false
,当前属性就无法被delete
命令删除。
var handler = {
deleteProperty (target, key) {
invariant(key, 'delete');
delete target[key];
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: Invalid attempt to delete private "_prop" property
上面代码中,deleteProperty
方法拦截了delete
操作符,删除第一个字符为下划线的属性会报错
Proxy vs. Object.defineProperty()
15、Reflect
1、一个静态类,统⼀的对象操作 API(内部封装了一系列对对象的底层操作)
2、Reflect中的方法名与Proxy对象中的处理对象的方法成员是完全一致的
// Reflect成员方法就是Proxy处理对象的默认实现
const obj = {
foo: 123,
bar: 456
}
const p = new Proxy(obj, {
get(target, propert) {
return Reflect.get(target, propert)
}
})
console.log(p.foo) // 123
3、Reflect存在的意义:统一提供了一套用于操作对象的API
16、Symbol
⼀种全新的原始数据类型,表示一个独一无二的值,最主要的作用就是为对象添加独一无二的属性名
// 1、Symbol创建的数据的类型就是symbol
const s = Symbol()
console.log(s) // Symbol()
console.log(typeof s) // symbol
// 2、Symbol创建的每一个数据都是不同的
console.log(Symbol() === Symbol()) // false
///3、Symbol允许创建数据的时候传入字符串,作为描述文本
console.log(Symbol('foo')) // Symbol(foo)
console.log(Symbol('bar')) // Symbol(bar)
///4、对象可以使用Symbol类型的值作为属性名
const obj = {}
obj[Symbol()] = '123'
obj[Symbol()] = '456'
console.log(obj) // { [Symbol()]: '123', [Symbol()]: '456' }
///5、也可以使用计算属性名的方式在对象字面量当中使用Symbol作为属性名
const obj2 = {
[Symbol()]: '111'
}
console.log(obj2) // { [Symbol()]: '111' }
// 6、模拟实现对象的私有成员
// 在a.js文件中
const name = Symbol()
const person = {
[name]: 'tom',
say() {
console.log(this[name])
}
}
// 在b.js文件中
person.say() // tom
四、ECMAScript 2016
1、数组实例对象的includes方法,用于检查数组中是否包含指定元素变得更加简单
const arr = ['foo', 1, NaN, false]
// 传统的检测
console.log(arr.indexOf('foo')) // 0
console.log(arr.indexOf('bar')) // -1
console.log(arr.indexOf(NaN)) // -1 --=检查不到NaN
console.log(arr.indexOf(false)) // 3
// 通过includes检测
console.log(arr.includes('foo')) // true
console.log(arr.includes('bar')) // false
console.log(arr.includes(NaN)) // true --可以检测到NaN
console.log(arr.includes(false)) // true
2、指数运算符
console.log(Math.pow(2, 10)) // 以前求2的10次方
console.log(2 ** 10) // 运用指数运算符求2的10次方
五、ECMAScript 2017
1、Object对象的三个扩展方法【Object.value()、Object.entries()、Object.getOwnPropertyDescriptors()】
const obj = {
foo: 'v1',
bar: 'v2'
}
// 1、与Object.keys()类似,Object.values()返回对象中值组成的数组
console.log(Object.values(obj)) // [ 'v1', 'v2' ]
// 2、Object.entries()方法是以数组的方式返回对象中的键值对
console.log(Object.entries(obj)) // [ [ 'foo', 'v1' ], [ 'bar', 'v2' ] ]
for (let [key, value] of Object.entries(obj)) {
console.log(key, value)
}
// foo v1
// bar v2
// 将entries对象转换为Map对象
console.log(new Map(Object.entries(obj))) // Map(2) { 'foo' => 'v1', 'bar' => 'v2' }
// 3、Object.getOwnPropertyDescriptors()获取对象中完整的属性描述信息
// ES5之后可以为对象定义getter和setter属性,但是这种属性不能通过Object.assign()方法去复制的
const p1 = {
firstName: 'lei',
lastName: 'wang',
get fullName() {
return this.firstName + ' ' + this.lastName
}
}
// const p2 = Object.assign({}, p1)
// p2.firstName = 'hua'
// console.log(p2.fullName) // lei wang --fullName函数作为普通字符串被复制
// 获取p1的详细描述
const des = Object.getOwnPropertyDescriptors(p1)
const p2 = Object.defineProperties({}, des)
p2.firstName = 'hua'
console.log(p2.fullName) // hua wang
2、两个字符串填充方法【padEnd、padStart】
const books = {
html: 5,
css: 10,
javascript: 15
}
// 对其输出的字符串长度,为输出的数字位数不足时导0
for (let [name, count] of Object.entries(books)) {
console.log(`${name.padEnd(16, '- ')} | ${count.toString().padStart(3, '0')}`)
}
// html- - - - - - | 005
// css- - - - - - - | 010
// javascript- - - | 015
3、允许函数的参数列表最后一位添加结束的逗号【定义函数和调用函数都允许】
4、Aysnc/Await【本质上是promise的语法糖】